From f14996dabca4434169d2bf1d196f284668f47503 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Mon, 17 Nov 2025 14:35:08 -0800 Subject: [PATCH 1/2] Bypass `SyntaxNode` in JuliaLowering; fix `kw` bug --- JuliaLowering/src/compat.jl | 72 ++++++----- JuliaLowering/src/desugaring.jl | 28 +++-- JuliaLowering/src/hooks.jl | 4 +- JuliaLowering/src/macro_expansion.jl | 5 +- JuliaLowering/src/syntax_graph.jl | 180 +++++++++++++++++++++------ JuliaLowering/src/syntax_macros.jl | 4 +- JuliaLowering/test/compat.jl | 2 + JuliaLowering/test/functions_ir.jl | 4 +- JuliaLowering/test/quoting.jl | 6 +- JuliaLowering/test/typedefs_ir.jl | 2 +- JuliaSyntax/src/integration/expr.jl | 19 ++- JuliaSyntax/src/julia/kinds.jl | 1 + 12 files changed, 231 insertions(+), 96 deletions(-) diff --git a/JuliaLowering/src/compat.jl b/JuliaLowering/src/compat.jl index 133fb55151bcb..a7a480c1b08b3 100644 --- a/JuliaLowering/src/compat.jl +++ b/JuliaLowering/src/compat.jl @@ -34,9 +34,7 @@ end graph = syntax_graph(ctx) toplevel_src = if isnothing(lnn) # Provenance sinkhole for all nodes until we hit a linenode - dummy_src = SourceRef( - SourceFile("No source for expression"), - 1, JS.GreenNode(K"None", 0)) + dummy_src = SourceRef(SourceFile("No source for expression"), 1, 0) _insert_tree_node(graph, K"None", dummy_src) else lnn @@ -46,18 +44,24 @@ end return out end -function _expr_replace!(@nospecialize(e), replace_pred::Function, replacer!::Function, +function _expr_replace(@nospecialize(e), replace_pred::Function, replacer::Function, recurse_pred=(@nospecialize e)->true) if replace_pred(e) - replacer!(e) - end - if e isa Expr && recurse_pred(e) - for a in e.args - _expr_replace!(a, replace_pred, replacer!, recurse_pred) - end + replacer(e) + elseif e isa Expr && recurse_pred(e) + Expr(e.head, [_expr_replace(a, replace_pred, replacer, recurse_pred) for a in e.args]...) + else + e end end +function _eq_to_kw(@nospecialize(e)) + _expr_replace(e, + (x)->x isa Expr && x.head === :(=), + (x)->Expr(:kw, x.args...), + (x)->x.head in (:escape, :var"hygienic-scope")) +end + function _to_iterspec(exs::Vector, is_generator::Bool) if length(exs) === 1 && exs[1].head === :filter @assert length(exs[1].args) >= 2 @@ -85,19 +89,25 @@ Parameters are expected to be at `e.args[pos]`. e.g. orderings of (a,b,c;d;e;f): Expr: (tuple (parameters (parameters (parameters f) e) d) a b c) SyntaxTree: (tuple a b c (parameters d) (parameters e) (parameters f)) + +`ensure_kw` converts `=` to `kw` within parameters blocks (needed for ref, +curly, vect, and braces). """ -function collect_expr_parameters(e::Expr, pos::Int) +function collect_expr_parameters(e::Expr, pos::Int, ensure_kw::Bool) params = expr_parameters(e, pos) isnothing(params) && return copy(e.args) args = Any[e.args[1:pos-1]..., e.args[pos+1:end]...] - return _flatten_params!(args, params) + return _flatten_params!(args, params, ensure_kw) end -function _flatten_params!(out::Vector{Any}, params::Expr) +function _flatten_params!(out::Vector{Any}, params::Expr, ensure_kw) p,p_esc = unwrap_esc(params) p1 = expr_parameters(p, 1) if !isnothing(p1) - push!(out, p_esc(Expr(:parameters, p.args[2:end]...))) - _flatten_params!(out, p_esc(p1)) + p_args = ensure_kw ? map(_eq_to_kw, p.args[2:end]) : p.args[2:end] + push!(out, p_esc(Expr(:parameters, p_args...))) + _flatten_params!(out, p_esc(p1), ensure_kw) + elseif ensure_kw && params isa Expr && params.head === :parameters + push!(out, Expr(:parameters, map(_eq_to_kw, params.args)...)) else push!(out, params::Any) end @@ -262,7 +272,7 @@ function _insert_convert_expr(@nospecialize(e), graph::SyntaxGraph, src::SourceA elseif e.head === :macrocall @assert nargs >= 2 a1,a1_esc = unwrap_esc(e.args[1]) - child_exprs = collect_expr_parameters(e, 3) + child_exprs = collect_expr_parameters(e, 3, false) if child_exprs[2] isa LineNumberNode src = child_exprs[2] end @@ -297,7 +307,7 @@ function _insert_convert_expr(@nospecialize(e), graph::SyntaxGraph, src::SourceA a2, a2_esc = unwrap_esc(e.args[2]) if a2 isa Expr && a2.head === :tuple st_k = K"dotcall" - tuple_exprs = collect_expr_parameters(a2_esc(a2), 1) + tuple_exprs = collect_expr_parameters(a2_esc(a2), 1, false) child_exprs = pushfirst!(tuple_exprs, e.args[1]) elseif a2 isa QuoteNode child_exprs[2] = a2_esc(a2.value) @@ -311,10 +321,16 @@ function _insert_convert_expr(@nospecialize(e), graph::SyntaxGraph, src::SourceA if !(e2 isa Expr && e2.head === :braces) child_exprs = Any[e.args[1], Expr(:braces, e.args[2:end]...)] end - elseif e.head in (:tuple, :vect, :braces) - child_exprs = collect_expr_parameters(e, 1) - elseif e.head in (:curly, :ref) - child_exprs = collect_expr_parameters(e, 2) + elseif e.head === :ref + child_exprs = collect_expr_parameters(e, 2, true) + elseif e.head === :curly + child_exprs = collect_expr_parameters(e, 2, true) + map!(_eq_to_kw, child_exprs[2:end]) + elseif e.head in (:vect, :braces) + child_exprs = collect_expr_parameters(e, 1, true) + elseif e.head === :tuple + child_exprs = collect_expr_parameters(e, 1, false) + map!(_eq_to_kw, child_exprs) elseif e.head === :try child_exprs = Any[e.args[1]] # Expr: @@ -368,7 +384,7 @@ function _insert_convert_expr(@nospecialize(e), graph::SyntaxGraph, src::SourceA a isa LineNumberNode && continue a isa Expr && a.head === :(=) ? push!(lam_eqs, a) : push!(lam_args, a) end - !isempty(lam_eqs) && push!(lam_args, Expr(:parameters, lam_eqs...)) + !isempty(lam_eqs) && push!(lam_args, Expr(:parameters, map(_eq_to_kw, lam_eqs)...)) child_exprs[1] = a1_esc(Expr(:tuple, lam_args...)) elseif !(a1 isa Expr && (a1.head in (:tuple, :where))) child_exprs[1] = a1_esc(Expr(:tuple, a1)) @@ -376,7 +392,7 @@ function _insert_convert_expr(@nospecialize(e), graph::SyntaxGraph, src::SourceA src = maybe_extract_lnn(e.args[2], src) child_exprs[2] = maybe_unwrap_arg(e.args[2]) elseif e.head === :call - child_exprs = collect_expr_parameters(e, 2) + child_exprs = collect_expr_parameters(e, 2, false) a1,a1_esc = unwrap_esc(child_exprs[1]) if a1 isa Symbol a1s = string(a1) @@ -408,7 +424,7 @@ function _insert_convert_expr(@nospecialize(e), graph::SyntaxGraph, src::SourceA # (do (call f args...) (-> (tuple lam_args...) (block ...))) # SyntaxTree: # (call f args... (do (tuple lam_args...) (block ...))) - callargs = collect_expr_parameters(e.args[1], 2) + callargs = collect_expr_parameters(e.args[1], 2, false) if e.args[1].head === :macrocall st_k = K"macrocall" if callargs[2] isa LineNumberNode @@ -433,11 +449,9 @@ function _insert_convert_expr(@nospecialize(e), graph::SyntaxGraph, src::SourceA child_exprs = child_exprs[2:end] # TODO handle docstrings after refactor elseif (e.head === :using || e.head === :import) - _expr_replace!(e, - (e)->(e isa Expr && e.head === :.), - (e)->(e.head = :importpath)) - elseif e.head === :kw - st_k = K"=" + e2 = _expr_replace(e, (e)->(e isa Expr && e.head === :.), + (e)->Expr(:importpath, e.args...)) + child_exprs = e2.args elseif e.head in (:local, :global) && nargs > 1 # Possible normalization # child_exprs = Any[Expr(:tuple, child_exprs...)] diff --git a/JuliaLowering/src/desugaring.jl b/JuliaLowering/src/desugaring.jl index 30b10d2dbf766..42f7ce0f00c4c 100644 --- a/JuliaLowering/src/desugaring.jl +++ b/JuliaLowering/src/desugaring.jl @@ -86,7 +86,7 @@ function check_no_parameters(ex::SyntaxTree, msg) end function check_no_assignment(exs, msg="misplaced assignment statement in `[ ... ]`") - i = findfirst(kind(e) == K"=" for e in exs) + i = findfirst(kind(e) == K"=" || kind(e) == K"kw" for e in exs) if !isnothing(i) throw(LoweringError(exs[i], msg)) end @@ -1622,7 +1622,7 @@ function expand_named_tuple(ctx, ex, kws; # x ==> x = x name = to_symbol(ctx, kw) value = kw - elseif k == K"=" + elseif k == K"kw" # x = a if kind(kw[1]) != K"Identifier" && kind(kw[1]) != K"Placeholder" throw(LoweringError(kw[1], "invalid $field_name name")) @@ -1864,7 +1864,7 @@ function remove_kw_args!(ctx, args::SyntaxList) for i in 1:length(args) arg = args[i] k = kind(arg) - if k == K"=" + if k == K"kw" if isnothing(kws) kws = SyntaxList(ctx) end @@ -2263,7 +2263,7 @@ end function expand_function_arg(ctx, body_stmts, arg, is_last_arg, is_kw) ex = arg - if kind(ex) == K"=" + if kind(ex) == K"kw" default = ex[2] ex = ex[1] else @@ -2824,8 +2824,10 @@ function keyword_function_defs(ctx, srcref, callex_srcref, name_str, typevar_nam kwcall_body_tail ] else - scope_nest(ctx, kw_names, kw_values, kwcall_body_tail) + scope_nest(ctx, has_kw_slurp ? kw_names[1:end-1] : kw_names, + kw_values, kwcall_body_tail) end + main_kwcall_typevars = trim_used_typevars(ctx, kwcall_arg_types, typevar_names, typevar_stmts) push!(kwcall_method_defs, method_def_expr(ctx, srcref, callex_srcref, kwcall_mtable, @@ -3212,9 +3214,13 @@ function expand_arrow_arglist(ctx, arglist, arrowname) # https://github.com/JuliaLang/JuliaSyntax.jl/pull/522 if k == K"block" @chk numchildren(arglist) == 2 + kw = kind(arglist[2]) === K"=" ? + @ast(ctx, arglist[2], [K"kw" children(arglist[2])...]) : + arglist[2] + arglist = @ast ctx arglist [K"tuple" arglist[1] - [K"parameters" arglist[2]] + [K"parameters" kw] ] elseif k != K"tuple" arglist = @ast ctx arglist [K"tuple" @@ -3795,7 +3801,7 @@ function _rewrite_ctor_new_calls(ctx, ex, struct_name, global_struct_name, ctor_ ) end # Rewrite a call to new() - kw_arg_i = findfirst(e->(k = kind(e); k == K"=" || k == K"parameters"), children(ex)) + kw_arg_i = findfirst(e->(k = kind(e); k == K"kw" || k == K"parameters"), children(ex)) if !isnothing(kw_arg_i) throw(LoweringError(ex[kw_arg_i], "`new` does not accept keyword arguments")) end @@ -4113,7 +4119,7 @@ function expand_struct_def(ctx, ex, docs) struct_name isnothing(docs) ? nothing_(ctx, ex) : docs[1] ::K"SourceLocation"(ex) - [K"=" + [K"kw" "field_docs"::K"Identifier" [K"call" "svec"::K"core" field_docs...] ] @@ -4160,7 +4166,7 @@ end function expand_curly(ctx, ex) @assert kind(ex) == K"curly" check_no_parameters(ex, "unexpected semicolon in type parameter list") - check_no_assignment(children(ex), "misplace assignment in type parameter list") + check_no_assignment(children(ex), "misplaced assignment in type parameter list") typevar_stmts = SyntaxList(ctx) type_args = SyntaxList(ctx) @@ -4479,7 +4485,7 @@ function expand_forms_2(ctx::DesugaringContext, ex::SyntaxTree, docs=nothing) throw(LoweringError(ex[end], "unexpected semicolon in tuple - use `,` to separate tuple elements")) end expand_forms_2(ctx, expand_named_tuple(ctx, ex, children(ex[1]))) - elseif any_assignment(children(ex)) + elseif any(kind(c) == K"kw" for c in children(ex)) expand_forms_2(ctx, expand_named_tuple(ctx, ex, children(ex))) else expand_forms_2(ctx, @ast ctx ex [K"call" @@ -4524,7 +4530,7 @@ function expand_forms_2(ctx::DesugaringContext, ex::SyntaxTree, docs=nothing) ctx.mod ::K"Value" [K"inert" ex] [K"parameters" - [K"=" + [K"kw" "expr_compat_mode"::K"Identifier" ctx.expr_compat_mode::K"Bool" ] diff --git a/JuliaLowering/src/hooks.jl b/JuliaLowering/src/hooks.jl index ca7ba9a0c3de1..9bf04e1a80183 100644 --- a/JuliaLowering/src/hooks.jl +++ b/JuliaLowering/src/hooks.jl @@ -15,7 +15,7 @@ function core_lowering_hook(@nospecialize(code), mod::Module, file = file isa Ptr{UInt8} ? unsafe_string(file) : file line = !(line isa Int) ? Int(line) : line - local st0 = nothing + local st0, st1 = nothing, nothing try st0 = code isa Expr ? expr_to_syntaxtree(code, LineNumberNode(line, file)) : code if kind(st0) in KSet"toplevel module" @@ -32,7 +32,7 @@ function core_lowering_hook(@nospecialize(code), mod::Module, ex = to_lowered_expr(st5) return Core.svec(ex, st5, ctx5) catch exc - @info("JuliaLowering threw given input:", code=code, st0=st0, file=file, line=line, mod=mod) + @info("JuliaLowering threw given input:", code=code, st0=st0, st1=st1, file=file, line=line, mod=mod) rethrow(exc) # TODO: Re-enable flisp fallback once we're done collecting errors diff --git a/JuliaLowering/src/macro_expansion.jl b/JuliaLowering/src/macro_expansion.jl index 6ea642e376508..1ce18594c3f29 100644 --- a/JuliaLowering/src/macro_expansion.jl +++ b/JuliaLowering/src/macro_expansion.jl @@ -436,8 +436,9 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree) # TODO: Upstream should set a general flag for detecting parenthesized # expressions so we don't need to dig into `green_tree` here. Ugh! plain_symbol = has_flags(ex, JuliaSyntax.COLON_QUOTE) && - kind(ex[1]) == K"Identifier" && - (sr = sourceref(ex); sr isa SourceRef && kind(sr.green_tree[2]) != K"parens") + kind(ex[1]) == K"Identifier" && ( + prov = flattened_provenance(ex); + length(prov) >= 1 && kind(prov[end][end]) != K"parens") if plain_symbol # As a compromise for compatibility, we treat non-parenthesized # colon quoted identifiers like `:x` as plain Symbol literals diff --git a/JuliaLowering/src/syntax_graph.jl b/JuliaLowering/src/syntax_graph.jl index c8145aa1b93c3..299c834a376a0 100644 --- a/JuliaLowering/src/syntax_graph.jl +++ b/JuliaLowering/src/syntax_graph.jl @@ -1,3 +1,8 @@ +# TODO: This whole file should probably be moved to JuliaSyntax. +import .JuliaSyntax: ParseStream, RedTreeCursor, reverse_toplevel_siblings, + has_toplevel_siblings, _unsafe_wrap_substring, parse_julia_literal, is_trivia, + is_prefix_op_call, @isexpr, SyntaxHead, COLON_QUOTE, is_syntactic_operator + const NodeId = Int """ @@ -180,28 +185,6 @@ function setflags!(graph, id::NodeId, f::UInt16) graph.syntax_flags[id] = f end -function _convert_nodes(graph::SyntaxGraph, node::SyntaxNode) - id = newnode!(graph) - sethead!(graph, id, head(node)) - if !isnothing(node.val) - v = node.val - if v isa Symbol - # TODO: Fixes in JuliaSyntax to avoid ever converting to Symbol - setattr!(graph, id, name_val=string(v)) - else - setattr!(graph, id, value=v) - end - end - setattr!(graph, id, source=SourceRef(node.source, node.position, node.raw)) - if !is_leaf(node) - cs = map(children(node)) do n - _convert_nodes(graph, n) - end - setchildren!(graph, id, cs) - end - return id -end - """ syntax_graph(ctx) @@ -333,12 +316,11 @@ end struct SourceRef file::SourceFile first_byte::Int - # TODO: Do we need the green node, or would last_byte suffice? - green_tree::JuliaSyntax.GreenNode + last_byte::Int end JuliaSyntax.sourcefile(src::SourceRef) = src.file -JuliaSyntax.byte_range(src::SourceRef) = src.first_byte:(src.first_byte + span(src.green_tree) - 1) +JuliaSyntax.byte_range(src::SourceRef) = src.first_byte:src.last_byte # TODO: Adding these methods to support LineNumberNode is kind of hacky but we # can remove these after JuliaLowering becomes self-bootstrapping for macros @@ -449,17 +431,6 @@ end const SourceAttrType = Union{SourceRef,LineNumberNode,NodeId,Tuple} -function SyntaxTree(graph::SyntaxGraph, node::SyntaxNode) - ensure_attributes!(graph, kind=Kind, syntax_flags=UInt16, source=SourceAttrType, - value=Any, name_val=String) - id = _convert_nodes(graph, node) - return SyntaxTree(graph, id) -end - -function SyntaxTree(node::SyntaxNode) - return SyntaxTree(SyntaxGraph(), node) -end - attrsummary(name, value) = string(name) attrsummary(name, value::Number) = "$name=$value" @@ -574,10 +545,6 @@ end syntax_graph(ex::SyntaxTree) = ex._graph -function JuliaSyntax.build_tree(::Type{SyntaxTree}, stream::JuliaSyntax.ParseStream; kws...) - SyntaxTree(JuliaSyntax.build_tree(SyntaxNode, stream; kws...)) -end - JuliaSyntax.sourcefile(ex::SyntaxTree) = sourcefile(sourceref(ex)) JuliaSyntax.byte_range(ex::SyntaxTree) = byte_range(sourceref(ex)) @@ -604,6 +571,31 @@ function JuliaSyntax._expr_leaf_val(ex::SyntaxTree, _...) end end +function JuliaSyntax.fixup_Expr_child(::Type{<:SyntaxTree}, head::SyntaxHead, + @nospecialize(arg), first::Bool) + isa(arg, Expr) || return arg + k = kind(head) + in_call = !first && (k == K"ref" || + (k in KSet"call dotcall" && is_prefix_call(head))) + eq_head_in_params = k in KSet"vect ref braces curly" ? :(=) : :kw + coalesce_dot = k in KSet"call dotcall curly" || + (k == K"quote" && has_flags(head, COLON_QUOTE)) + if @isexpr(arg, :kw) && !in_call + arg.head = :(=) + elseif @isexpr(arg, :., 1) && arg.args[1] isa Tuple + h, a = arg.args[1]::Tuple{SyntaxHead,Any} + arg = ((coalesce_dot && first) || is_syntactic_operator(h)) ? + Symbol(".", a) : Expr(:., a) + elseif @isexpr(arg, :parameters) + for pa in arg.args + if pa isa Expr && pa.head in (:kw, :(=)) + pa.head = eq_head_in_params + end + end + end + return arg +end + Base.Expr(ex::SyntaxTree) = JuliaSyntax.to_expr(ex) #-------------------------------------------------- @@ -826,3 +818,111 @@ end # end # out # end + +#------------------------------------------------------------------------------- +# Conversion from the raw parsed tree +# TODO: move to JuliaSyntax. Replace SyntaxNode? + +function JuliaSyntax.build_tree(::Type{SyntaxTree}, stream::ParseStream; + filename=nothing, first_line=1) + cursor = RedTreeCursor(stream) + graph = SyntaxGraph() + sf = SourceFile(stream; filename, first_line) + if has_toplevel_siblings(cursor) + # There are multiple toplevel nodes (e.g. due to a parse error) + cs = SyntaxList(graph) + for c in reverse_toplevel_siblings(cursor) + is_trivia(c) && !is_error(c) && continue + push!(cs, SyntaxTree(graph, sf, c)) + end + source = SourceRef(sf, first_byte(stream), last_byte(stream)) + id = newnode!(graph) + setchildren!(graph, id, reverse(cs).ids) + setattr!(graph, id; source, kind=K"wrapper") + return SyntaxTree(graph, id) + else + return SyntaxTree(graph, sf, cursor) + end +end + +function SyntaxTree(graph::SyntaxGraph, sf::SourceFile, cursor::RedTreeCursor) + ensure_attributes!(graph, kind=Kind, syntax_flags=UInt16, + source=SourceAttrType, value=Any, name_val=String) + green_id = GC.@preserve sf begin + raw_offset, txtbuf = _unsafe_wrap_substring(sf.code) + offset = raw_offset - sf.byte_offset + _insert_green(graph, sf, txtbuf, offset, cursor) + end + out = _green_to_ast(SyntaxTree(graph, green_id)) + @assert !isnothing(out) "SyntaxTree requires >0 nontrivia nodes" + return out +end + +# TODO: Do we really need all trivia? K"parens" can be good to keep, but things +# like K"(" and whitespace might not be useful. +function _insert_green(graph::SyntaxGraph, sf::SourceFile, + txtbuf::Vector{UInt8}, offset::Int, + cursor::RedTreeCursor) + id = newnode!(graph) + sethead!(graph, id, head(cursor)) + setattr!(graph, id, source=SourceRef(sf, first_byte(cursor), last_byte(cursor))) + if !is_leaf(cursor) + cs = NodeId[] + for c in reverse(cursor) + push!(cs, _insert_green(graph, sf, txtbuf, offset, c)) + end + setchildren!(graph, id, reverse(cs)) + else + v = parse_julia_literal(txtbuf, head(cursor), byte_range(cursor) .+ offset) + if v isa Symbol + # TODO: Fixes in JuliaSyntax to avoid ever converting to Symbol + setattr!(graph, id, name_val=string(v)) + elseif !isnothing(v) + setattr!(graph, id, value=v) + end + end + return id +end + +# Leaves are shared. Unlike `mapchildren`, doesn't bother checking for +# unchanged children so internal nodes can be shared, since the likelihood of +# not deleting trivia under an internal node is practically zero. +function _green_to_ast(ex::SyntaxTree; eq_to_kw=false) + is_trivia(ex) && !is_error(ex) && return nothing + is_leaf(ex) && return ex + + graph = syntax_graph(ex) + k = kind(ex) + call_with_kw = (k in KSet"curly ref" || + k === K"dotcall" && is_prefix_call(ex) || + k === K"call" && (is_prefix_call(ex) || + is_prefix_op_call(ex) && numchildren(ex) > 2)) + if call_with_kw + cs = SyntaxList(ex) + for c in children(ex) + c2 = _green_to_ast(c; eq_to_kw=length(cs)>0) + !isnothing(c2) && push!(cs, c2) + end + makenode(graph, ex, ex, cs) + elseif k in KSet"tuple parameters" + makenode(graph, ex, ex, _map_green_to_ast(children(ex); eq_to_kw=true)) + elseif k in KSet"parens var char" + cs = _map_green_to_ast(children(ex); + eq_to_kw=(k === K"parens" ? eq_to_kw : false)) + @assert length(cs) === 1 + cs[1] + elseif k === K"=" && eq_to_kw + makenode(graph, ex, ex, _map_green_to_ast(children(ex)); kind=K"kw") + else + makenode(graph, ex, ex, _map_green_to_ast(children(ex))) + end +end + +function _map_green_to_ast(cs::SyntaxList; eq_to_kw=false) + out = SyntaxList(cs) + for c in cs + c2 = _green_to_ast(c; eq_to_kw) + !isnothing(c2) && push!(out, c2) + end + return out +end diff --git a/JuliaLowering/src/syntax_macros.jl b/JuliaLowering/src/syntax_macros.jl index a08ddde1fba67..c8b32d1a11a1f 100644 --- a/JuliaLowering/src/syntax_macros.jl +++ b/JuliaLowering/src/syntax_macros.jl @@ -22,6 +22,8 @@ function _apply_nospecialize(ctx, ex) elseif k == K"..." || k == K"::" || k == K"=" if k == K"::" && numchildren(ex) == 1 ex = @ast ctx ex [K"::" "_"::K"Placeholder" ex[1]] + elseif k == K"=" + ex = @ast ctx ex [K"kw" ex[1] ex[2]] end mapchildren(c->_apply_nospecialize(ctx, c), ctx, ex, 1:1) else @@ -283,7 +285,7 @@ function _at_eval_code(ctx, srcref, mod, ex) mod [K"quote" ex] [K"parameters" - [K"=" + [K"kw" "expr_compat_mode"::K"Identifier" ctx.expr_compat_mode::K"Bool" ] diff --git a/JuliaLowering/test/compat.jl b/JuliaLowering/test/compat.jl index a7fce558e9f40..a39f096746193 100644 --- a/JuliaLowering/test/compat.jl +++ b/JuliaLowering/test/compat.jl @@ -217,6 +217,8 @@ const JL = JuliaLowering "f(((a = 1)))", "(((a = 1)),)", "(;((a = 1)),)", + "(a = 1) |> f", + "(a = 1)'", "a.b", "a.@b x", "f.(x,y)", diff --git a/JuliaLowering/test/functions_ir.jl b/JuliaLowering/test/functions_ir.jl index a537757b881ba..7851215870286 100644 --- a/JuliaLowering/test/functions_ir.jl +++ b/JuliaLowering/test/functions_ir.jl @@ -1532,7 +1532,7 @@ end 18 (call core.svec %₁₅ %₁₆ %₁₇) 19 --- method core.nothing %₁₈ slots: [slot₁/#self#(!read) slot₂/x(!read) slot₃/y(!read)] - 1 (meta :generated (new JuliaLowering.GeneratedFunctionStub TestMod.#f_only_generated@generator#0 SourceRef(SourceFile("@generated function f_only_generated(x, y)\n generator_code(x,y)\nend", 0, nothing, 1, [1, 44, 68]), 1, (macrocall (macro_name 1-1::@-t 2-10::Identifier) 11-11::Whitespace-t (function 12-19::function-t 20-20::Whitespace-t (call 21-36::Identifier 37-37::(-t 38-38::Identifier 39-39::,-t 40-40::Whitespace-t 41-41::Identifier 42-42::)-t) (block 43-47::NewlineWs-t (call 48-61::Identifier 62-62::(-t 63-63::Identifier 64-64::,-t 65-65::Identifier 66-66::)-t) 67-67::NewlineWs-t) 68-70::end-t))) (call core.svec :#self# :x :y) (call core.svec))) + 1 (meta :generated (new JuliaLowering.GeneratedFunctionStub TestMod.#f_only_generated@generator#0 SourceRef(SourceFile("@generated function f_only_generated(x, y)\n generator_code(x,y)\nend", 0, nothing, 1, [1, 44, 68]), 1, 70) (call core.svec :#self# :x :y) (call core.svec))) 2 (meta :generated_only) 3 (return core.nothing) 20 latestworld @@ -1578,7 +1578,7 @@ end 18 (call core.svec %₁₅ %₁₆ %₁₇) 19 --- method core.nothing %₁₈ slots: [slot₁/#self#(!read) slot₂/x slot₃/y slot₄/maybe_gen_stuff slot₅/nongen_stuff] - 1 (meta :generated (new JuliaLowering.GeneratedFunctionStub TestMod.#f_partially_generated@generator#0 SourceRef(SourceFile("function f_partially_generated(x, y)\n nongen_stuff = bothgen(x, y)\n if @generated\n quote\n maybe_gen_stuff = some_gen_stuff(x, y)\n end\n else\n maybe_gen_stuff = some_nongen_stuff(x, y)\n end\n (nongen_stuff, maybe_gen_stuff)\nend", 0, nothing, 1, [1, 38, 71, 89, 103, 154, 166, 175, 225, 233, 269]), 1, (function 1-8::function-t 9-9::Whitespace-t (call 10-30::Identifier 31-31::(-t 32-32::Identifier 33-33::,-t 34-34::Whitespace-t 35-35::Identifier 36-36::)-t) (block 37-41::NewlineWs-t (= 42-53::Identifier 54-54::Whitespace-t 55-55::=-t 56-56::Whitespace-t (call 57-63::Identifier 64-64::(-t 65-65::Identifier 66-66::,-t 67-67::Whitespace-t 68-68::Identifier 69-69::)-t)) 70-74::NewlineWs-t (if 75-76::if-t 77-77::Whitespace-t (macrocall (macro_name 78-78::@-t 79-87::Identifier)) (block 88-96::NewlineWs-t (quote (block 97-101::quote-t 102-114::NewlineWs-t (= 115-129::Identifier 130-130::Whitespace-t 131-131::=-t 132-132::Whitespace-t (call 133-146::Identifier 147-147::(-t 148-148::Identifier 149-149::,-t 150-150::Whitespace-t 151-151::Identifier 152-152::)-t)) 153-161::NewlineWs-t 162-164::end-t)) 165-169::NewlineWs-t) 170-173::else-t (block 174-182::NewlineWs-t (= 183-197::Identifier 198-198::Whitespace-t 199-199::=-t 200-200::Whitespace-t (call 201-217::Identifier 218-218::(-t 219-219::Identifier 220-220::,-t 221-221::Whitespace-t 222-222::Identifier 223-223::)-t)) 224-228::NewlineWs-t) 229-231::end-t) 232-236::NewlineWs-t (tuple-p 237-237::(-t 238-249::Identifier 250-250::,-t 251-251::Whitespace-t 252-266::Identifier 267-267::)-t) 268-268::NewlineWs-t) 269-271::end-t)) (call core.svec :#self# :x :y) (call core.svec))) + 1 (meta :generated (new JuliaLowering.GeneratedFunctionStub TestMod.#f_partially_generated@generator#0 SourceRef(SourceFile("function f_partially_generated(x, y)\n nongen_stuff = bothgen(x, y)\n if @generated\n quote\n maybe_gen_stuff = some_gen_stuff(x, y)\n end\n else\n maybe_gen_stuff = some_nongen_stuff(x, y)\n end\n (nongen_stuff, maybe_gen_stuff)\nend", 0, nothing, 1, [1, 38, 71, 89, 103, 154, 166, 175, 225, 233, 269]), 1, 271) (call core.svec :#self# :x :y) (call core.svec))) 2 TestMod.bothgen 3 (= slot₅/nongen_stuff (call %₂ slot₂/x slot₃/y)) 4 TestMod.some_nongen_stuff diff --git a/JuliaLowering/test/quoting.jl b/JuliaLowering/test/quoting.jl index 93ace74e948f2..07441e8d6bb0c 100644 --- a/JuliaLowering/test/quoting.jl +++ b/JuliaLowering/test/quoting.jl @@ -28,9 +28,11 @@ end (call g z) ├─ (call g z) │ └─ (call g z) - │ └─ @ string:3 + │ └─ (call g ✘ z ✘) + │ └─ @ string:3 └─ ($ y) - └─ @ string:5 + └─ ($ $ y) + └─ @ string:5 """ @test sprint(io->showprov(io, ex[1][3])) == raw""" begin diff --git a/JuliaLowering/test/typedefs_ir.jl b/JuliaLowering/test/typedefs_ir.jl index 280f2719d6d6c..10d0b17865e89 100644 --- a/JuliaLowering/test/typedefs_ir.jl +++ b/JuliaLowering/test/typedefs_ir.jl @@ -181,7 +181,7 @@ X{S, T=w} #--------------------- LoweringError: X{S, T=w} -# └──┘ ── misplace assignment in type parameter list +# └──┘ ── misplaced assignment in type parameter list ######################################## # Simple abstract type definition diff --git a/JuliaSyntax/src/integration/expr.jl b/JuliaSyntax/src/integration/expr.jl index 53de5f55f0ee4..545ec2a83e23f 100644 --- a/JuliaSyntax/src/integration/expr.jl +++ b/JuliaSyntax/src/integration/expr.jl @@ -145,7 +145,7 @@ end # Shared fixups for Expr children in cases where the type of the parent node # affects the child layout. -function fixup_Expr_child(head::SyntaxHead, @nospecialize(arg), first::Bool) +function fixup_Expr_child(::Type, head::SyntaxHead, @nospecialize(arg), first::Bool) isa(arg, Expr) || return arg k = kind(head) eq_to_kw_in_call = ((k == K"call" || k == K"dotcall") && @@ -210,7 +210,8 @@ function parseargs!(retexpr::Expr, loc::LineNumberNode, cursor, source, txtbuf:: @assert expr !== nothing firstchildhead = head(child) firstchildrange = byte_range(child) - pushfirst!(args, fixup_Expr_child(head(cursor), expr, r === nothing)) + pushfirst!(args, fixup_Expr_child( + typeof(cursor), head(cursor), expr, r === nothing)) end return (firstchildhead, firstchildrange) end @@ -281,7 +282,7 @@ function node_to_expr(cursor, source, txtbuf::Vector{UInt8}, txtbuf_offset::UInt expr = node_to_expr(child, source, txtbuf, txtbuf_offset) @assert expr !== nothing # K"block" does not have special first-child handling, so we do not need to keep track of that here - pushfirst!(args, fixup_Expr_child(head(cursor), expr, false)) + pushfirst!(args, fixup_Expr_child(typeof(cursor), head(cursor), expr, false)) pushfirst!(args, source_location(LineNumberNode, source, first(byte_range(child)))) end isempty(args) && push!(args, loc) @@ -655,11 +656,15 @@ function build_tree(::Type{Expr}, stream::ParseStream, source::SourceFile) entry = Expr(:block) for child in Iterators.filter(should_include_node, reverse_toplevel_siblings(cursor)) - pushfirst!(entry.args, fixup_Expr_child(wrapper_head, node_to_expr(child, source, txtbuf), false)) + pushfirst!(entry.args, fixup_Expr_child( + RedTreeCursor, wrapper_head, + node_to_expr(child, source, txtbuf), false)) end length(entry.args) == 1 && (entry = only(entry.args)) else - entry = fixup_Expr_child(wrapper_head, node_to_expr(cursor, source, txtbuf), false) + entry = fixup_Expr_child( + RedTreeCursor, wrapper_head, + node_to_expr(cursor, source, txtbuf), false) end return entry end @@ -668,7 +673,9 @@ function to_expr(node) source = sourcefile(node) txtbuf_offset, txtbuf = _unsafe_wrap_substring(sourcetext(source)) wrapper_head = SyntaxHead(K"wrapper",EMPTY_FLAGS) - return fixup_Expr_child(wrapper_head, node_to_expr(node, source, txtbuf, UInt32(txtbuf_offset)), false) + return fixup_Expr_child( + typeof(node), wrapper_head, + node_to_expr(node, source, txtbuf, UInt32(txtbuf_offset)), false) end Base.Expr(node::SyntaxNode) = to_expr(node) diff --git a/JuliaSyntax/src/julia/kinds.jl b/JuliaSyntax/src/julia/kinds.jl index dd25663b14ef3..59bcf9b560b4a 100644 --- a/JuliaSyntax/src/julia/kinds.jl +++ b/JuliaSyntax/src/julia/kinds.jl @@ -1026,6 +1026,7 @@ register_kinds!(JuliaSyntax, 0, [ "char" # A char string node (containing delims + char data) "macrocall" "parameters" # the list after ; in f(; a=1) + "kw" "toplevel" "tuple" "ref" From f962f9e912057ca802557b5e536cb15c632110be Mon Sep 17 00:00:00 2001 From: Em Chu Date: Tue, 18 Nov 2025 16:09:14 -0800 Subject: [PATCH 2/2] Fixes I misplaced --- JuliaLowering/src/syntax_graph.jl | 31 ++++++++++++++---------------- JuliaLowering/src/syntax_macros.jl | 4 +++- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/JuliaLowering/src/syntax_graph.jl b/JuliaLowering/src/syntax_graph.jl index 299c834a376a0..04b78f4a730c6 100644 --- a/JuliaLowering/src/syntax_graph.jl +++ b/JuliaLowering/src/syntax_graph.jl @@ -828,21 +828,18 @@ function JuliaSyntax.build_tree(::Type{SyntaxTree}, stream::ParseStream; cursor = RedTreeCursor(stream) graph = SyntaxGraph() sf = SourceFile(stream; filename, first_line) - if has_toplevel_siblings(cursor) - # There are multiple toplevel nodes (e.g. due to a parse error) - cs = SyntaxList(graph) - for c in reverse_toplevel_siblings(cursor) - is_trivia(c) && !is_error(c) && continue - push!(cs, SyntaxTree(graph, sf, c)) - end - source = SourceRef(sf, first_byte(stream), last_byte(stream)) - id = newnode!(graph) - setchildren!(graph, id, reverse(cs).ids) - setattr!(graph, id; source, kind=K"wrapper") - return SyntaxTree(graph, id) - else - return SyntaxTree(graph, sf, cursor) - end + source = SourceRef(sf, first_byte(stream), last_byte(stream)) + cs = SyntaxList(graph) + for c in reverse_toplevel_siblings(cursor) + is_trivia(c) && !is_error(c) && continue + push!(cs, SyntaxTree(graph, sf, c)) + end + # There may be multiple non-trivia toplevel nodes (e.g. parse error) + length(cs) === 1 && return only(cs) + id = newnode!(graph) + setchildren!(graph, id, reverse(cs).ids) + setattr!(graph, id; source, kind=K"wrapper") + return SyntaxTree(graph, id) end function SyntaxTree(graph::SyntaxGraph, sf::SourceFile, cursor::RedTreeCursor) @@ -889,8 +886,6 @@ end # not deleting trivia under an internal node is practically zero. function _green_to_ast(ex::SyntaxTree; eq_to_kw=false) is_trivia(ex) && !is_error(ex) && return nothing - is_leaf(ex) && return ex - graph = syntax_graph(ex) k = kind(ex) call_with_kw = (k in KSet"curly ref" || @@ -913,6 +908,8 @@ function _green_to_ast(ex::SyntaxTree; eq_to_kw=false) cs[1] elseif k === K"=" && eq_to_kw makenode(graph, ex, ex, _map_green_to_ast(children(ex)); kind=K"kw") + elseif is_leaf(ex) + return ex else makenode(graph, ex, ex, _map_green_to_ast(children(ex))) end diff --git a/JuliaLowering/src/syntax_macros.jl b/JuliaLowering/src/syntax_macros.jl index c8b32d1a11a1f..2587c7aca366f 100644 --- a/JuliaLowering/src/syntax_macros.jl +++ b/JuliaLowering/src/syntax_macros.jl @@ -19,7 +19,9 @@ function _apply_nospecialize(ctx, ex) k = kind(ex) if k == K"Identifier" || k == K"Placeholder" || k == K"tuple" setmeta(ex; nospecialize=true) - elseif k == K"..." || k == K"::" || k == K"=" + elseif k == K"..." || k == K"::" || k == K"=" || k == K"kw" + # The @nospecialize macro is responsible for converting K"=" to K"kw". + # Desugaring uses this helper internally, so we may see K"kw" too. if k == K"::" && numchildren(ex) == 1 ex = @ast ctx ex [K"::" "_"::K"Placeholder" ex[1]] elseif k == K"="