From 6e07e7d12655fef3d7e2772d687cf82e34584287 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Thu, 14 Aug 2025 13:32:57 -0700 Subject: [PATCH 01/12] `copy_ast`: Add option to not recurse on `.source`, clarify docs --- src/ast.jl | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/ast.jl b/src/ast.jl index 46aa82e5..e0617891 100644 --- a/src/ast.jl +++ b/src/ast.jl @@ -428,15 +428,21 @@ end """ -Copy AST `ex` into `ctx` +Recursively copy AST `ex` into `ctx`. The resulting tree contains new nodes +only, and does not contain nodes with multiple parents. + +Special provenance handling: If `copy_source` is true, treat the `.source` +attribute as a reference and recurse on its contents. Otherwise, treat it like +any other attribute. """ -function copy_ast(ctx, ex) +function copy_ast(ctx, ex; copy_source=true) # TODO: Do we need to keep a mapping of node IDs to ensure we don't # double-copy here in the case when some tree nodes are pointed to by # multiple parents? (How much does this actually happen in practice?) - s = ex.source + s = get(ex, :source, nothing) # TODO: Figure out how to use provenance() here? - srcref = s isa NodeId ? copy_ast(ctx, SyntaxTree(ex._graph, s)) : + srcref = !copy_source ? s : + s isa NodeId ? copy_ast(ctx, SyntaxTree(ex._graph, s)) : s isa Tuple ? map(i->copy_ast(ctx, SyntaxTree(ex._graph, i)), s) : s if !is_leaf(ex) From 253fa1dd9685ca9611603bedafc4e785f492d4aa Mon Sep 17 00:00:00 2001 From: Em Chu Date: Thu, 14 Aug 2025 13:37:31 -0700 Subject: [PATCH 02/12] `ensure_attributes!`: Make signature reflect function body (passing the NamedTuple variant is an error here, and can't be mutated anyway) --- src/syntax_graph.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/syntax_graph.jl b/src/syntax_graph.jl index a7a2b28a..e7da856d 100644 --- a/src/syntax_graph.jl +++ b/src/syntax_graph.jl @@ -39,7 +39,7 @@ function Base.show(io::IO, ::MIME"text/plain", graph::SyntaxGraph) _show_attrs(io, graph.attributes) end -function ensure_attributes!(graph::SyntaxGraph; kws...) +function ensure_attributes!(graph::SyntaxGraph{<:Dict}; kws...) for (k,v) in pairs(kws) @assert k isa Symbol @assert v isa Type From 59fff2690cc71fcc119799ad3ef75940183e5107 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Thu, 14 Aug 2025 13:43:18 -0700 Subject: [PATCH 03/12] Add graph utils: `unfreeze_attrs`, `attrtypes` Missing utilities in line with existing ones (`freeze_attrs`, `attrnames`) --- src/syntax_graph.jl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/syntax_graph.jl b/src/syntax_graph.jl index e7da856d..98ee913c 100644 --- a/src/syntax_graph.jl +++ b/src/syntax_graph.jl @@ -22,6 +22,10 @@ function freeze_attrs(graph::SyntaxGraph) SyntaxGraph(graph.edge_ranges, graph.edges, frozen_attrs) end +function unfreeze_attrs(graph::SyntaxGraph) + SyntaxGraph(graph.edge_ranges, graph.edges, Dict(pairs(graph.attributes)...)) +end + function _show_attrs(io, attributes::Dict) show(io, MIME("text/plain"), attributes) end @@ -33,6 +37,10 @@ function attrnames(graph::SyntaxGraph) keys(graph.attributes) end +function attrtypes(graph::SyntaxGraph) + [(k, typeof(v).parameters[2]) for (k, v) in pairs(graph.attributes)] +end + function Base.show(io::IO, ::MIME"text/plain", graph::SyntaxGraph) print(io, typeof(graph), " with $(length(graph.edge_ranges)) vertices, $(length(graph.edges)) edges, and attributes:\n") @@ -54,7 +62,7 @@ function ensure_attributes!(graph::SyntaxGraph{<:Dict}; kws...) end function ensure_attributes(graph::SyntaxGraph; kws...) - g = SyntaxGraph(graph.edge_ranges, graph.edges, Dict(pairs(graph.attributes)...)) + g = unfreeze_attrs(graph) ensure_attributes!(g; kws...) freeze_attrs(g) end From a75065e58d355c0a643f92db9934d3872ebbfaf5 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Mon, 25 Aug 2025 11:05:48 -0700 Subject: [PATCH 04/12] Print more information when node does not have attribute --- src/syntax_graph.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/syntax_graph.jl b/src/syntax_graph.jl index 98ee913c..cf1e9dcc 100644 --- a/src/syntax_graph.jl +++ b/src/syntax_graph.jl @@ -213,7 +213,9 @@ function Base.getproperty(ex::SyntaxTree, name::Symbol) name === :_id && return getfield(ex, :_id) _id = getfield(ex, :_id) return get(getproperty(getfield(ex, :_graph), name), _id) do - error("Property `$name[$_id]` not found") + attrstr = join(["\n $n = $(getproperty(ex, n))" + for n in attrnames(ex)], ",") + error("Property `$name[$_id]` not found. Available attributes:$attrstr") end end From 26b81e2a1ccf058a0699048f3c7d9841e371ed09 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Mon, 25 Aug 2025 11:07:43 -0700 Subject: [PATCH 05/12] Fix printing for identifier-like kinds `String`/`Cmd` `MacroName` --- src/syntax_graph.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/syntax_graph.jl b/src/syntax_graph.jl index cf1e9dcc..45e59a77 100644 --- a/src/syntax_graph.jl +++ b/src/syntax_graph.jl @@ -438,7 +438,7 @@ attrsummary(name, value::Number) = "$name=$value" function _value_string(ex) k = kind(ex) - str = k == K"Identifier" || k == K"MacroName" || is_operator(k) ? ex.name_val : + str = k in KSet"Identifier MacroName StringMacroName CmdMacroName" || is_operator(k) ? ex.name_val : k == K"Placeholder" ? ex.name_val : k == K"SSAValue" ? "%" : k == K"BindingId" ? "#" : From be719829c3cf0fd4fd8f0be92a53d8bab3f0f30e Mon Sep 17 00:00:00 2001 From: Em Chu Date: Mon, 25 Aug 2025 11:08:49 -0700 Subject: [PATCH 06/12] Do not coerce attrs to NamedTuple unnecessarily For `ensure_attributes` and `delete_attributes`, the output graph's `.attributes` now have the same type (`Dict` or `NamedTuple`) as the input. Add `delete_attributes!` defined only on dict-attrs to be consistent with `ensure_attributes!` --- src/syntax_graph.jl | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/syntax_graph.jl b/src/syntax_graph.jl index 45e59a77..4a55f6ca 100644 --- a/src/syntax_graph.jl +++ b/src/syntax_graph.jl @@ -60,19 +60,31 @@ function ensure_attributes!(graph::SyntaxGraph{<:Dict}; kws...) end graph end +function ensure_attributes(graph::SyntaxGraph{<:Dict}; kws...) + g = unfreeze_attrs(graph) + ensure_attributes!(g; kws...) +end -function ensure_attributes(graph::SyntaxGraph; kws...) +function ensure_attributes(graph::SyntaxGraph{<:NamedTuple}; kws...) g = unfreeze_attrs(graph) ensure_attributes!(g; kws...) freeze_attrs(g) end -function delete_attributes(graph::SyntaxGraph, attr_names...) - attributes = Dict(pairs(graph.attributes)...) +function delete_attributes!(graph::SyntaxGraph{<:Dict}, attr_names::Symbol...) for name in attr_names - delete!(attributes, name) + delete!(graph.attributes, name) end - SyntaxGraph(graph.edge_ranges, graph.edges, (; pairs(attributes)...)) + graph +end + +function delete_attributes(graph::SyntaxGraph{<:Dict}, attr_names::Symbol...) + delete_attributes!(unfreeze_attrs(graph), attr_names...) +end + +function delete_attributes(graph::SyntaxGraph{<:NamedTuple}, attr_names::Symbol...) + g = delete_attributes!(unfreeze_attrs(graph), attr_names...) + freeze_attrs(g) end function newnode!(graph::SyntaxGraph) From 0520c94c859e2ce838c4ca83bd88b8c9a39d8708 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Mon, 25 Aug 2025 11:44:59 -0700 Subject: [PATCH 07/12] Remove ineffective call to `freeze_attrs` converting from SyntaxNode Funny to realize it wasn't doing anything. If we want freezing, it should go after lowering anyway. Also clarify the `SyntaxTree(graph, syntaxnode)` signature. --- src/syntax_graph.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/syntax_graph.jl b/src/syntax_graph.jl index 4a55f6ca..a39eb0b5 100644 --- a/src/syntax_graph.jl +++ b/src/syntax_graph.jl @@ -434,10 +434,10 @@ end const SourceAttrType = Union{SourceRef,LineNumberNode,NodeId,Tuple} -function SyntaxTree(graph::SyntaxGraph, node::SyntaxNode) +function SyntaxTree(graph::SyntaxGraph{<:Dict}, node::SyntaxNode) ensure_attributes!(graph, kind=Kind, syntax_flags=UInt16, source=SourceAttrType, value=Any, name_val=String) - id = _convert_nodes(freeze_attrs(graph), node) + id = _convert_nodes(graph, node) return SyntaxTree(graph, id) end From 356d61a54af5f2273280eceb044c0276908b2311 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Mon, 25 Aug 2025 12:15:56 -0700 Subject: [PATCH 08/12] Test `ensure`, `delete` attrs, `attrtypes` --- test/syntax_graph.jl | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/syntax_graph.jl b/test/syntax_graph.jl index 153dbc88..caa2ad90 100644 --- a/test/syntax_graph.jl +++ b/test/syntax_graph.jl @@ -1,3 +1,45 @@ +@testset "SyntaxGraph attrs" begin + st = parsestmt(SyntaxTree, "function foo end") + g_init = JuliaLowering.unfreeze_attrs(st._graph) + gf1 = JuliaLowering.freeze_attrs(g_init) + gu1 = JuliaLowering.unfreeze_attrs(gf1) + + # Check that freeze/unfreeze do their jobs + @test gf1.attributes isa NamedTuple + @test gu1.attributes isa Dict + @test Set(keys(gf1.attributes)) == Set(keys(gu1.attributes)) + + # ensure_attributes + gf2 = JuliaLowering.ensure_attributes(gf1, test_attr=Symbol, foo=Type) + gu2 = JuliaLowering.ensure_attributes(gu1, test_attr=Symbol, foo=Type) + # returns a graph with the same attribute storage + @test gf2.attributes isa NamedTuple + @test gu2.attributes isa Dict + # does its job + @test (:test_attr, Symbol) in JuliaLowering.attrtypes(gf2) + @test (:foo, Type) in JuliaLowering.attrtypes(gf2) + @test Set(keys(gf2.attributes)) == Set(keys(gu2.attributes)) + # no mutation + @test !((:test_attr, Symbol) in JuliaLowering.attrtypes(gf1)) + @test !((:foo, Type) in JuliaLowering.attrtypes(gf1)) + @test Set(keys(gf1.attributes)) == Set(keys(gu1.attributes)) + + # delete_attributes + gf3 = JuliaLowering.delete_attributes(gf2, :test_attr, :foo) + gu3 = JuliaLowering.delete_attributes(gu2, :test_attr, :foo) + # returns a graph with the same attribute storage + @test gf3.attributes isa NamedTuple + @test gu3.attributes isa Dict + # does its job + @test !((:test_attr, Symbol) in JuliaLowering.attrtypes(gf3)) + @test !((:foo, Type) in JuliaLowering.attrtypes(gf3)) + @test Set(keys(gf3.attributes)) == Set(keys(gu3.attributes)) + # no mutation + @test (:test_attr, Symbol) in JuliaLowering.attrtypes(gf2) + @test (:foo, Type) in JuliaLowering.attrtypes(gf2) + @test Set(keys(gf2.attributes)) == Set(keys(gu2.attributes)) +end + @testset "SyntaxTree" begin # Expr conversion @test Expr(parsestmt(SyntaxTree, "begin a + b ; c end", filename="none")) == From 06ba02468b209d2f6f1efd830f9b34156cbc6290 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Wed, 27 Aug 2025 09:15:32 -0700 Subject: [PATCH 09/12] `unfreeze_attrs`: produce `Dict{Symbol, Any}` instead of `Dict` Co-authored-by: Shuhei Kadowaki --- src/syntax_graph.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/syntax_graph.jl b/src/syntax_graph.jl index a39eb0b5..c2fe3092 100644 --- a/src/syntax_graph.jl +++ b/src/syntax_graph.jl @@ -23,7 +23,8 @@ function freeze_attrs(graph::SyntaxGraph) end function unfreeze_attrs(graph::SyntaxGraph) - SyntaxGraph(graph.edge_ranges, graph.edges, Dict(pairs(graph.attributes)...)) + unfrozen_attrs = Dict{Symbol,Any}(pairs(graph.attributes)...) + SyntaxGraph(graph.edge_ranges, graph.edges, unfrozen_attrs) end function _show_attrs(io, attributes::Dict) From 668021c83b287532c23fa345d99e9a3951851ee3 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Thu, 28 Aug 2025 13:01:20 -0700 Subject: [PATCH 10/12] Fix `copy_ast` copying too much https://github.com/c42f/JuliaLowering.jl/pull/48#discussion_r2308301753 --- src/ast.jl | 54 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/src/ast.jl b/src/ast.jl index e0617891..a0e65ce1 100644 --- a/src/ast.jl +++ b/src/ast.jl @@ -428,33 +428,47 @@ end """ -Recursively copy AST `ex` into `ctx`. The resulting tree contains new nodes -only, and does not contain nodes with multiple parents. +Recursively copy AST `ex` into `ctx`. Special provenance handling: If `copy_source` is true, treat the `.source` attribute as a reference and recurse on its contents. Otherwise, treat it like any other attribute. """ -function copy_ast(ctx, ex; copy_source=true) - # TODO: Do we need to keep a mapping of node IDs to ensure we don't - # double-copy here in the case when some tree nodes are pointed to by - # multiple parents? (How much does this actually happen in practice?) - s = get(ex, :source, nothing) - # TODO: Figure out how to use provenance() here? - srcref = !copy_source ? s : - s isa NodeId ? copy_ast(ctx, SyntaxTree(ex._graph, s)) : - s isa Tuple ? map(i->copy_ast(ctx, SyntaxTree(ex._graph, i)), s) : - s - if !is_leaf(ex) - cs = SyntaxList(ctx) - for e in children(ex) - push!(cs, copy_ast(ctx, e)) - end - ex2 = makenode(ctx, srcref, ex, cs) +function copy_ast(ctx, ex::SyntaxTree; copy_source=true) + graph1 = syntax_graph(ex) + graph2 = syntax_graph(ctx) + !copy_source && check_same_graph(graph1, graph2) + id2 = _copy_ast(graph2, graph1, ex._id, Dict{NodeId, NodeId}(), copy_source) + return SyntaxTree(graph2, id2) +end + +function _copy_ast(graph2::SyntaxGraph, graph1::SyntaxGraph, + id1::NodeId, seen, copy_source) + let copied = get(seen, id1, nothing) + isnothing(copied) || return copied + end + id2 = newnode!(graph2) + seen[id1] = id2 + src1 = get(SyntaxTree(graph1, id1), :source, nothing) + src2 = if !copy_source + src1 + elseif src1 isa NodeId + _copy_ast(graph2, graph1, src1, seen, copy_source) + elseif src1 isa Tuple + map(i->_copy_ast(graph2, graph1, i, seen, copy_source), src1) else - ex2 = makeleaf(ctx, srcref, ex) + src1 end - return ex2 + copy_attrs!(SyntaxTree(graph2, id2), SyntaxTree(graph1, id1), true) + setattr!(graph2, id2; source=src2) + if !is_leaf(graph1, id1) + cs = NodeId[] + for cid in children(graph1, id1) + push!(cs, _copy_ast(graph2, graph1, cid, seen, copy_source)) + end + setchildren!(graph2, id2, cs) + end + return id2 end #------------------------------------------------------------------------------- From 7e28e0959b95f2ad79ac7a3468ed8ab200e4d822 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Thu, 28 Aug 2025 13:02:15 -0700 Subject: [PATCH 11/12] Apply suggestions from code review Co-authored-by: Claire Foster --- src/syntax_graph.jl | 14 ++++++++++---- test/syntax_graph.jl | 16 ++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/syntax_graph.jl b/src/syntax_graph.jl index c2fe3092..6a4aa594 100644 --- a/src/syntax_graph.jl +++ b/src/syntax_graph.jl @@ -22,6 +22,7 @@ function freeze_attrs(graph::SyntaxGraph) SyntaxGraph(graph.edge_ranges, graph.edges, frozen_attrs) end +# Create a copy of `graph` where the attribute list is mutable function unfreeze_attrs(graph::SyntaxGraph) unfrozen_attrs = Dict{Symbol,Any}(pairs(graph.attributes)...) SyntaxGraph(graph.edge_ranges, graph.edges, unfrozen_attrs) @@ -38,8 +39,8 @@ function attrnames(graph::SyntaxGraph) keys(graph.attributes) end -function attrtypes(graph::SyntaxGraph) - [(k, typeof(v).parameters[2]) for (k, v) in pairs(graph.attributes)] +function attrdefs(graph::SyntaxGraph) + [(k=>typeof(v).parameters[2]) for (k, v) in pairs(graph.attributes)] end function Base.show(io::IO, ::MIME"text/plain", graph::SyntaxGraph) @@ -48,19 +49,24 @@ function Base.show(io::IO, ::MIME"text/plain", graph::SyntaxGraph) _show_attrs(io, graph.attributes) end -function ensure_attributes!(graph::SyntaxGraph{<:Dict}; kws...) +function ensure_attributes!(graph::SyntaxGraph; kws...) for (k,v) in pairs(kws) @assert k isa Symbol @assert v isa Type if haskey(graph.attributes, k) v0 = valtype(graph.attributes[k]) v == v0 || throw(ErrorException("Attribute type mismatch $v != $v0")) + elseif graph.attributes isa NamedTuple + throw(ErrorException(""" + ensure_attributes!: $k is not an existing attribute, and the graph's attributes are frozen. \ + Consider calling non-mutating `ensure_attributes` instead.""")) else graph.attributes[k] = Dict{NodeId,v}() end end graph end + function ensure_attributes(graph::SyntaxGraph{<:Dict}; kws...) g = unfreeze_attrs(graph) ensure_attributes!(g; kws...) @@ -435,7 +441,7 @@ end const SourceAttrType = Union{SourceRef,LineNumberNode,NodeId,Tuple} -function SyntaxTree(graph::SyntaxGraph{<:Dict}, node::SyntaxNode) +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) diff --git a/test/syntax_graph.jl b/test/syntax_graph.jl index caa2ad90..559a9c2a 100644 --- a/test/syntax_graph.jl +++ b/test/syntax_graph.jl @@ -16,12 +16,12 @@ @test gf2.attributes isa NamedTuple @test gu2.attributes isa Dict # does its job - @test (:test_attr, Symbol) in JuliaLowering.attrtypes(gf2) - @test (:foo, Type) in JuliaLowering.attrtypes(gf2) + @test (:test_attr=>Symbol) in JuliaLowering.attrdefs(gf2) + @test (:foo=>Type) in JuliaLowering.attrdefs(gf2) @test Set(keys(gf2.attributes)) == Set(keys(gu2.attributes)) # no mutation - @test !((:test_attr, Symbol) in JuliaLowering.attrtypes(gf1)) - @test !((:foo, Type) in JuliaLowering.attrtypes(gf1)) + @test !((:test_attr=>Symbol) in JuliaLowering.attrdefs(gf1)) + @test !((:foo=>Type) in JuliaLowering.attrdefs(gf1)) @test Set(keys(gf1.attributes)) == Set(keys(gu1.attributes)) # delete_attributes @@ -31,12 +31,12 @@ @test gf3.attributes isa NamedTuple @test gu3.attributes isa Dict # does its job - @test !((:test_attr, Symbol) in JuliaLowering.attrtypes(gf3)) - @test !((:foo, Type) in JuliaLowering.attrtypes(gf3)) + @test !((:test_attr=>Symbol) in JuliaLowering.attrdefs(gf3)) + @test !((:foo=>Type) in JuliaLowering.attrdefs(gf3)) @test Set(keys(gf3.attributes)) == Set(keys(gu3.attributes)) # no mutation - @test (:test_attr, Symbol) in JuliaLowering.attrtypes(gf2) - @test (:foo, Type) in JuliaLowering.attrtypes(gf2) + @test (:test_attr=>Symbol) in JuliaLowering.attrdefs(gf2) + @test (:foo=>Type) in JuliaLowering.attrdefs(gf2) @test Set(keys(gf2.attributes)) == Set(keys(gu2.attributes)) end From 18d8d1e077b6636b538d6d6db5bb0aadb0d4ac9c Mon Sep 17 00:00:00 2001 From: Em Chu Date: Thu, 28 Aug 2025 13:57:22 -0700 Subject: [PATCH 12/12] Add tests for `copy_ast` --- test/syntax_graph.jl | 49 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/syntax_graph.jl b/test/syntax_graph.jl index 559a9c2a..60fd10dd 100644 --- a/test/syntax_graph.jl +++ b/test/syntax_graph.jl @@ -58,4 +58,53 @@ end @test kind(tree2) == K"block" @test kind(tree2[1]) == K"Identifier" && tree2[1].name_val == "x" @test kind(tree2[2]) == K"Identifier" && tree2[2].name_val == "some_unique_identifier" + + "For filling required attrs in graphs created by hand" + function testgraph(edge_ranges, edges, more_attrs...) + kinds = Dict(map(i->(i=>K"block"), eachindex(edge_ranges))) + sources = Dict(map(i->(i=>LineNumberNode(i)), eachindex(edge_ranges))) + SyntaxGraph( + edge_ranges, + edges, + Dict(:kind => kinds, :source => sources, more_attrs...)) + end + + @testset "copy_ast" begin + # 1 --> 2 --> 3 src(7-9) = line 7-9 + # 4 --> 5 --> 6 src(i) = i + 3 + # 7 --> 8 --> 9 + g = testgraph([1:1, 2:2, 0:-1, 3:3, 4:4, 0:-1, 5:5, 6:6, 0:-1], + [2, 3, 5, 6, 8, 9], + :source => Dict(enumerate([ + map(i->i+3, 1:6)... + map(LineNumberNode, 7:9)...]))) + st = SyntaxTree(g, 1) + stcopy = JuliaLowering.copy_ast(g, st) + # Each node should be copied once + @test length(g.edge_ranges) === 18 + @test st._id != stcopy._id + @test st ≈ stcopy + @test st.source !== stcopy.source + @test st.source[1] !== stcopy.source[1] + @test st.source[1][1] !== stcopy.source[1][1] + + stcopy2 = JuliaLowering.copy_ast(g, st; copy_source=false) + # Only nodes 1-3 should be copied + @test length(g.edge_ranges) === 21 + @test st._id != stcopy2._id + @test st ≈ stcopy2 + @test st.source === stcopy2.source + @test st.source[1] === stcopy2.source[1] + @test st.source[1][1] === stcopy2.source[1][1] + + # Copy into a new graph + new_g = ensure_attributes!(SyntaxGraph(); JuliaLowering.attrdefs(g)...) + stcopy3 = JuliaLowering.copy_ast(new_g, st) + @test length(new_g.edge_ranges) === 9 + @test st ≈ stcopy3 + + new_g = ensure_attributes!(SyntaxGraph(); JuliaLowering.attrdefs(g)...) + # Disallow for now, since we can't prevent dangling sourcerefs + @test_throws ErrorException JuliaLowering.copy_ast(new_g, st; copy_source=false) + end end