From 0cd5642273b1b3d21135bae0099a2996c80c8313 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Fri, 2 Aug 2024 11:22:39 -0400 Subject: [PATCH] Allow macrocall in function def syntax (#456) * Allow macrocall in function def syntax Goes with https://github.com/JuliaLang/julia/pull/55040 * Update src/parser.jl Co-authored-by: Claire Foster * Test to cover function declaration with `var""` syntax * Make `function (@f(x)) body end` an ambiguity error This case is ambiguous as it might be either one of the following; require the user to explicitly disambiguate between them ``` function (@f(x),) body end function @f(x) body end ``` For the same reasons, `function ($f) body end` is also ambiguous. Also fix parsing of `function (f(x),) end` to correctly emit a tuple. --------- Co-authored-by: Claire Foster --- src/parser.jl | 34 +++++++++++++++++++++++++--------- test/diagnostics.jl | 3 +++ test/parser.jl | 14 ++++++++++++++ 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/parser.jl b/src/parser.jl index 4f97b8d5..8da46517 100644 --- a/src/parser.jl +++ b/src/parser.jl @@ -2128,14 +2128,15 @@ function parse_function_signature(ps::ParseState, is_function::Bool) # * The whole function declaration, in parens bump(ps, TRIVIA_FLAG) is_empty_tuple = peek(ps, skip_newlines=true) == K")" - opts = parse_brackets(ps, K")") do _, _, _, _ + opts = parse_brackets(ps, K")") do had_commas, had_splat, num_semis, num_subexprs _parsed_call = was_eventually_call(ps) _needs_parse_call = peek(ps, 2) ∈ KSet"( ." - _is_anon_func = !_needs_parse_call && !_parsed_call + _is_anon_func = (!_needs_parse_call && !_parsed_call) || had_commas return (needs_parameters = _is_anon_func, is_anon_func = _is_anon_func, parsed_call = _parsed_call, - needs_parse_call = _needs_parse_call) + needs_parse_call = _needs_parse_call, + maybe_grouping_parens = !had_commas && !had_splat && num_semis == 0 && num_subexprs == 1) end is_anon_func = opts.is_anon_func parsed_call = opts.parsed_call @@ -2146,7 +2147,14 @@ function parse_function_signature(ps::ParseState, is_function::Bool) # function (x,y) end ==> (function (tuple-p x y) (block)) # function (x=1) end ==> (function (tuple-p (= x 1)) (block)) # function (;x=1) end ==> (function (tuple-p (parameters (= x 1))) (block)) + # function (f(x),) end ==> (function (tuple-p (call f x)) (block)) + ambiguous_parens = opts.maybe_grouping_parens && + peek_behind(ps).kind in KSet"macrocall $" emit(ps, mark, K"tuple", PARENS_FLAG) + if ambiguous_parens + # Got something like `(@f(x))`. Is it anon `(@f(x),)` or named sig `@f(x)` ?? + emit(ps, mark, K"error", error="Ambiguous signature. Add a trailing comma if this is a 1-argument anonymous function; remove parentheses if this is a macro call acting as function signature.") + end elseif is_empty_tuple # Weird case which is consistent with parse_paren but will be # rejected in lowering @@ -2175,19 +2183,23 @@ function parse_function_signature(ps::ParseState, is_function::Bool) end end end - if peek(ps, skip_newlines=true) == K"end" && !is_anon_func && !parsed_call - return false - end if needs_parse_call # Parse function argument list # function f(x,y) end ==> (function (call f x y) (block)) # function f{T}() end ==> (function (call (curly f T)) (block)) # function A.f() end ==> (function (call (. A f)) (block)) parse_call_chain(ps, mark) - if peek_behind(ps).kind != K"call" + sig_kind = peek_behind(ps).kind + if sig_kind in KSet"Identifier var $" && peek(ps, skip_newlines=true) == K"end" + # function f end ==> (function f) + # function $f end ==> (function $f) + return false + elseif sig_kind == K"macrocall" + min_supported_version(v"1.12", ps, mark, "macro call as function signature") + elseif sig_kind != K"call" # function f body end ==> (function (error f) (block body)) emit(ps, mark, K"error", - error="Invalid signature in $(is_function ? "function" : "macro") definition") + error="Invalid signature in $(is_function ? "function" : "macro") definition") end end if is_function && peek(ps) == K"::" @@ -3511,7 +3523,11 @@ function parse_atom(ps::ParseState, check_identifiers=true) # + ==> + # .+ ==> (. +) # .= ==> (. =) - bump_dotsplit(ps, emit_dot_node=true) + if is_dotted(peek_token(ps)) + bump_dotsplit(ps, emit_dot_node=true) + else + bump(ps, remap_kind=K"Identifier") + end if check_identifiers && !is_valid_identifier(leading_kind) # += ==> (error +=) # ? ==> (error ?) diff --git a/test/diagnostics.jl b/test/diagnostics.jl index 1482a604..8371ede2 100644 --- a/test/diagnostics.jl +++ b/test/diagnostics.jl @@ -41,6 +41,9 @@ end @test diagnostic("\n+ (x, y)") == Diagnostic(3, 3, :error, "whitespace not allowed between prefix function call and argument list") + @test diagnostic("function (\$f) body end") == + Diagnostic(10, 13, :error, "Ambiguous signature. Add a trailing comma if this is a 1-argument anonymous function; remove parentheses if this is a macro call acting as function signature.") + @test diagnostic("A.@B.x", only_first=true) == Diagnostic(3, 4, :error, "`@` must appear on first or last macro name component") @test diagnostic("@M.(x)") == diff --git a/test/parser.jl b/test/parser.jl index 18ad2eb2..caf40f9e 100644 --- a/test/parser.jl +++ b/test/parser.jl @@ -571,6 +571,11 @@ tests = [ "function (x,y) end" => "(function (tuple-p x y) (block))" "function (x=1) end" => "(function (tuple-p (= x 1)) (block))" "function (;x=1) end" => "(function (tuple-p (parameters (= x 1))) (block))" + "function (f(x),) end" => "(function (tuple-p (call f x)) (block))" + "function (@f(x);) end" => "(function (tuple-p (macrocall-p @f x) (parameters)) (block))" + "function (@f(x)...) end" => "(function (tuple-p (... (macrocall-p @f x))) (block))" + "function (@f(x)) end" => "(function (error (tuple-p (macrocall-p @f x))) (block))" + "function (\$f) end" => "(function (error (tuple-p (\$ f))) (block))" "function ()(x) end" => "(function (call (tuple-p) x) (block))" "function (A).f() end" => "(function (call (. (parens A) f)) (block))" "function (:)() end" => "(function (call (parens :)) (block))" @@ -589,6 +594,7 @@ tests = [ "function f end" => "(function f)" "function f \n\n end" => "(function f)" "function \$f end" => "(function (\$ f))" + "function var\".\" end" => "(function (var .))" "macro f end" => "(macro f)" # Function argument list "function f(x,y) end" => "(function (call f x y) (block))" @@ -611,6 +617,11 @@ tests = [ # body "function f() \n a \n b end" => "(function (call f) (block a b))" "function f() end" => "(function (call f) (block))" + # Macrocall as sig + ((v=v"1.12",), "function @callmemacro(a::Int) \n 1 \n end") => "(function (macrocall-p @callmemacro (::-i a Int)) (block 1))" + ((v=v"1.12",), "function @callmemacro(a::T, b::T) where T <: Int64\n3\nend") => "(function (where (macrocall-p @callmemacro (::-i a T) (::-i b T)) (<: T Int64)) (block 3))" + ((v=v"1.12",), "function @callmemacro(a::Int, b::Int, c::Int)::Float64\n4\nend") => "(function (::-i (macrocall-p @callmemacro (::-i a Int) (::-i b Int) (::-i c Int)) Float64) (block 4))" + ((v=v"1.12",), "function @f()() end") => "(function (call (macrocall-p @f)) (block))" # Errors "function" => "(function (error (error)) (block (error)) (error-t))" ], @@ -1000,6 +1011,9 @@ tests = [ "public[7] = 5" => "(= (ref public 7) 5)" "public() = 6" => "(function-= (call public) 6)" ]), + JuliaSyntax.parse_stmts => [ + ((v = v"1.12",), "@callmemacro(b::Float64) = 2") => "(= (macrocall-p @callmemacro (::-i b Float64)) 2)" + ], JuliaSyntax.parse_docstring => [ """ "notdoc" ] """ => "(string \"notdoc\")" """ "notdoc" \n] """ => "(string \"notdoc\")"