Skip to content

Commit 2d2d677

Browse files
mlechuaviateskc42f
authored
SyntaxGraph: Usability and performance tweaks (#48)
* `copy_ast`: Add option to not recurse on `.source`, clarify docs, fix over-recursion * `ensure_attributes!`: throw instead of attempting to mutate NamedTuple * Add graph utils: `unfreeze_attrs`, `attrdefs` * Print more information when node does not have attribute * Fix printing for identifier-like kinds `String`/`Cmd` `MacroName` * 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!` * Remove ineffective call to `freeze_attrs` converting from SyntaxNode * Test `ensure`, `delete` attrs, `attrtypes`, `copy_ast` --------- Co-authored-by: Shuhei Kadowaki <[email protected]> Co-authored-by: Claire Foster <[email protected]>
1 parent 66dac41 commit 2d2d677

File tree

3 files changed

+167
-27
lines changed

3 files changed

+167
-27
lines changed

src/ast.jl

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -428,27 +428,47 @@ end
428428

429429

430430
"""
431-
Copy AST `ex` into `ctx`
431+
Recursively copy AST `ex` into `ctx`.
432+
433+
Special provenance handling: If `copy_source` is true, treat the `.source`
434+
attribute as a reference and recurse on its contents. Otherwise, treat it like
435+
any other attribute.
432436
"""
433-
function copy_ast(ctx, ex)
434-
# TODO: Do we need to keep a mapping of node IDs to ensure we don't
435-
# double-copy here in the case when some tree nodes are pointed to by
436-
# multiple parents? (How much does this actually happen in practice?)
437-
s = ex.source
438-
# TODO: Figure out how to use provenance() here?
439-
srcref = s isa NodeId ? copy_ast(ctx, SyntaxTree(ex._graph, s)) :
440-
s isa Tuple ? map(i->copy_ast(ctx, SyntaxTree(ex._graph, i)), s) :
441-
s
442-
if !is_leaf(ex)
443-
cs = SyntaxList(ctx)
444-
for e in children(ex)
445-
push!(cs, copy_ast(ctx, e))
446-
end
447-
ex2 = makenode(ctx, srcref, ex, cs)
437+
function copy_ast(ctx, ex::SyntaxTree; copy_source=true)
438+
graph1 = syntax_graph(ex)
439+
graph2 = syntax_graph(ctx)
440+
!copy_source && check_same_graph(graph1, graph2)
441+
id2 = _copy_ast(graph2, graph1, ex._id, Dict{NodeId, NodeId}(), copy_source)
442+
return SyntaxTree(graph2, id2)
443+
end
444+
445+
function _copy_ast(graph2::SyntaxGraph, graph1::SyntaxGraph,
446+
id1::NodeId, seen, copy_source)
447+
let copied = get(seen, id1, nothing)
448+
isnothing(copied) || return copied
449+
end
450+
id2 = newnode!(graph2)
451+
seen[id1] = id2
452+
src1 = get(SyntaxTree(graph1, id1), :source, nothing)
453+
src2 = if !copy_source
454+
src1
455+
elseif src1 isa NodeId
456+
_copy_ast(graph2, graph1, src1, seen, copy_source)
457+
elseif src1 isa Tuple
458+
map(i->_copy_ast(graph2, graph1, i, seen, copy_source), src1)
448459
else
449-
ex2 = makeleaf(ctx, srcref, ex)
460+
src1
450461
end
451-
return ex2
462+
copy_attrs!(SyntaxTree(graph2, id2), SyntaxTree(graph1, id1), true)
463+
setattr!(graph2, id2; source=src2)
464+
if !is_leaf(graph1, id1)
465+
cs = NodeId[]
466+
for cid in children(graph1, id1)
467+
push!(cs, _copy_ast(graph2, graph1, cid, seen, copy_source))
468+
end
469+
setchildren!(graph2, id2, cs)
470+
end
471+
return id2
452472
end
453473

454474
#-------------------------------------------------------------------------------

src/syntax_graph.jl

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ function freeze_attrs(graph::SyntaxGraph)
2222
SyntaxGraph(graph.edge_ranges, graph.edges, frozen_attrs)
2323
end
2424

25+
# Create a copy of `graph` where the attribute list is mutable
26+
function unfreeze_attrs(graph::SyntaxGraph)
27+
unfrozen_attrs = Dict{Symbol,Any}(pairs(graph.attributes)...)
28+
SyntaxGraph(graph.edge_ranges, graph.edges, unfrozen_attrs)
29+
end
30+
2531
function _show_attrs(io, attributes::Dict)
2632
show(io, MIME("text/plain"), attributes)
2733
end
@@ -33,6 +39,10 @@ function attrnames(graph::SyntaxGraph)
3339
keys(graph.attributes)
3440
end
3541

42+
function attrdefs(graph::SyntaxGraph)
43+
[(k=>typeof(v).parameters[2]) for (k, v) in pairs(graph.attributes)]
44+
end
45+
3646
function Base.show(io::IO, ::MIME"text/plain", graph::SyntaxGraph)
3747
print(io, typeof(graph),
3848
" with $(length(graph.edge_ranges)) vertices, $(length(graph.edges)) edges, and attributes:\n")
@@ -46,25 +56,42 @@ function ensure_attributes!(graph::SyntaxGraph; kws...)
4656
if haskey(graph.attributes, k)
4757
v0 = valtype(graph.attributes[k])
4858
v == v0 || throw(ErrorException("Attribute type mismatch $v != $v0"))
59+
elseif graph.attributes isa NamedTuple
60+
throw(ErrorException("""
61+
ensure_attributes!: $k is not an existing attribute, and the graph's attributes are frozen. \
62+
Consider calling non-mutating `ensure_attributes` instead."""))
4963
else
5064
graph.attributes[k] = Dict{NodeId,v}()
5165
end
5266
end
5367
graph
5468
end
5569

56-
function ensure_attributes(graph::SyntaxGraph; kws...)
57-
g = SyntaxGraph(graph.edge_ranges, graph.edges, Dict(pairs(graph.attributes)...))
70+
function ensure_attributes(graph::SyntaxGraph{<:Dict}; kws...)
71+
g = unfreeze_attrs(graph)
72+
ensure_attributes!(g; kws...)
73+
end
74+
75+
function ensure_attributes(graph::SyntaxGraph{<:NamedTuple}; kws...)
76+
g = unfreeze_attrs(graph)
5877
ensure_attributes!(g; kws...)
5978
freeze_attrs(g)
6079
end
6180

62-
function delete_attributes(graph::SyntaxGraph, attr_names...)
63-
attributes = Dict(pairs(graph.attributes)...)
81+
function delete_attributes!(graph::SyntaxGraph{<:Dict}, attr_names::Symbol...)
6482
for name in attr_names
65-
delete!(attributes, name)
83+
delete!(graph.attributes, name)
6684
end
67-
SyntaxGraph(graph.edge_ranges, graph.edges, (; pairs(attributes)...))
85+
graph
86+
end
87+
88+
function delete_attributes(graph::SyntaxGraph{<:Dict}, attr_names::Symbol...)
89+
delete_attributes!(unfreeze_attrs(graph), attr_names...)
90+
end
91+
92+
function delete_attributes(graph::SyntaxGraph{<:NamedTuple}, attr_names::Symbol...)
93+
g = delete_attributes!(unfreeze_attrs(graph), attr_names...)
94+
freeze_attrs(g)
6895
end
6996

7097
function newnode!(graph::SyntaxGraph)
@@ -205,7 +232,9 @@ function Base.getproperty(ex::SyntaxTree, name::Symbol)
205232
name === :_id && return getfield(ex, :_id)
206233
_id = getfield(ex, :_id)
207234
return get(getproperty(getfield(ex, :_graph), name), _id) do
208-
error("Property `$name[$_id]` not found")
235+
attrstr = join(["\n $n = $(getproperty(ex, n))"
236+
for n in attrnames(ex)], ",")
237+
error("Property `$name[$_id]` not found. Available attributes:$attrstr")
209238
end
210239
end
211240

@@ -415,7 +444,7 @@ const SourceAttrType = Union{SourceRef,LineNumberNode,NodeId,Tuple}
415444
function SyntaxTree(graph::SyntaxGraph, node::SyntaxNode)
416445
ensure_attributes!(graph, kind=Kind, syntax_flags=UInt16, source=SourceAttrType,
417446
value=Any, name_val=String)
418-
id = _convert_nodes(freeze_attrs(graph), node)
447+
id = _convert_nodes(graph, node)
419448
return SyntaxTree(graph, id)
420449
end
421450

@@ -428,7 +457,7 @@ attrsummary(name, value::Number) = "$name=$value"
428457

429458
function _value_string(ex)
430459
k = kind(ex)
431-
str = k == K"Identifier" || k == K"MacroName" || is_operator(k) ? ex.name_val :
460+
str = k in KSet"Identifier MacroName StringMacroName CmdMacroName" || is_operator(k) ? ex.name_val :
432461
k == K"Placeholder" ? ex.name_val :
433462
k == K"SSAValue" ? "%" :
434463
k == K"BindingId" ? "#" :

test/syntax_graph.jl

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,45 @@
1+
@testset "SyntaxGraph attrs" begin
2+
st = parsestmt(SyntaxTree, "function foo end")
3+
g_init = JuliaLowering.unfreeze_attrs(st._graph)
4+
gf1 = JuliaLowering.freeze_attrs(g_init)
5+
gu1 = JuliaLowering.unfreeze_attrs(gf1)
6+
7+
# Check that freeze/unfreeze do their jobs
8+
@test gf1.attributes isa NamedTuple
9+
@test gu1.attributes isa Dict
10+
@test Set(keys(gf1.attributes)) == Set(keys(gu1.attributes))
11+
12+
# ensure_attributes
13+
gf2 = JuliaLowering.ensure_attributes(gf1, test_attr=Symbol, foo=Type)
14+
gu2 = JuliaLowering.ensure_attributes(gu1, test_attr=Symbol, foo=Type)
15+
# returns a graph with the same attribute storage
16+
@test gf2.attributes isa NamedTuple
17+
@test gu2.attributes isa Dict
18+
# does its job
19+
@test (:test_attr=>Symbol) in JuliaLowering.attrdefs(gf2)
20+
@test (:foo=>Type) in JuliaLowering.attrdefs(gf2)
21+
@test Set(keys(gf2.attributes)) == Set(keys(gu2.attributes))
22+
# no mutation
23+
@test !((:test_attr=>Symbol) in JuliaLowering.attrdefs(gf1))
24+
@test !((:foo=>Type) in JuliaLowering.attrdefs(gf1))
25+
@test Set(keys(gf1.attributes)) == Set(keys(gu1.attributes))
26+
27+
# delete_attributes
28+
gf3 = JuliaLowering.delete_attributes(gf2, :test_attr, :foo)
29+
gu3 = JuliaLowering.delete_attributes(gu2, :test_attr, :foo)
30+
# returns a graph with the same attribute storage
31+
@test gf3.attributes isa NamedTuple
32+
@test gu3.attributes isa Dict
33+
# does its job
34+
@test !((:test_attr=>Symbol) in JuliaLowering.attrdefs(gf3))
35+
@test !((:foo=>Type) in JuliaLowering.attrdefs(gf3))
36+
@test Set(keys(gf3.attributes)) == Set(keys(gu3.attributes))
37+
# no mutation
38+
@test (:test_attr=>Symbol) in JuliaLowering.attrdefs(gf2)
39+
@test (:foo=>Type) in JuliaLowering.attrdefs(gf2)
40+
@test Set(keys(gf2.attributes)) == Set(keys(gu2.attributes))
41+
end
42+
143
@testset "SyntaxTree" begin
244
# Expr conversion
345
@test Expr(parsestmt(SyntaxTree, "begin a + b ; c end", filename="none")) ==
@@ -16,4 +58,53 @@
1658
@test kind(tree2) == K"block"
1759
@test kind(tree2[1]) == K"Identifier" && tree2[1].name_val == "x"
1860
@test kind(tree2[2]) == K"Identifier" && tree2[2].name_val == "some_unique_identifier"
61+
62+
"For filling required attrs in graphs created by hand"
63+
function testgraph(edge_ranges, edges, more_attrs...)
64+
kinds = Dict(map(i->(i=>K"block"), eachindex(edge_ranges)))
65+
sources = Dict(map(i->(i=>LineNumberNode(i)), eachindex(edge_ranges)))
66+
SyntaxGraph(
67+
edge_ranges,
68+
edges,
69+
Dict(:kind => kinds, :source => sources, more_attrs...))
70+
end
71+
72+
@testset "copy_ast" begin
73+
# 1 --> 2 --> 3 src(7-9) = line 7-9
74+
# 4 --> 5 --> 6 src(i) = i + 3
75+
# 7 --> 8 --> 9
76+
g = testgraph([1:1, 2:2, 0:-1, 3:3, 4:4, 0:-1, 5:5, 6:6, 0:-1],
77+
[2, 3, 5, 6, 8, 9],
78+
:source => Dict(enumerate([
79+
map(i->i+3, 1:6)...
80+
map(LineNumberNode, 7:9)...])))
81+
st = SyntaxTree(g, 1)
82+
stcopy = JuliaLowering.copy_ast(g, st)
83+
# Each node should be copied once
84+
@test length(g.edge_ranges) === 18
85+
@test st._id != stcopy._id
86+
@test st stcopy
87+
@test st.source !== stcopy.source
88+
@test st.source[1] !== stcopy.source[1]
89+
@test st.source[1][1] !== stcopy.source[1][1]
90+
91+
stcopy2 = JuliaLowering.copy_ast(g, st; copy_source=false)
92+
# Only nodes 1-3 should be copied
93+
@test length(g.edge_ranges) === 21
94+
@test st._id != stcopy2._id
95+
@test st stcopy2
96+
@test st.source === stcopy2.source
97+
@test st.source[1] === stcopy2.source[1]
98+
@test st.source[1][1] === stcopy2.source[1][1]
99+
100+
# Copy into a new graph
101+
new_g = ensure_attributes!(SyntaxGraph(); JuliaLowering.attrdefs(g)...)
102+
stcopy3 = JuliaLowering.copy_ast(new_g, st)
103+
@test length(new_g.edge_ranges) === 9
104+
@test st stcopy3
105+
106+
new_g = ensure_attributes!(SyntaxGraph(); JuliaLowering.attrdefs(g)...)
107+
# Disallow for now, since we can't prevent dangling sourcerefs
108+
@test_throws ErrorException JuliaLowering.copy_ast(new_g, st; copy_source=false)
109+
end
19110
end

0 commit comments

Comments
 (0)