Skip to content

Commit 0a51f3d

Browse files
committed
Clean up and document syntax tree child access API
Here I commit to a more consistent but simpler child access API for syntax trees, as informed by the JuliaLowering work so far: * `is_leaf(node)` is given a precise definition (previously `!haschildren()` - but that had issues - see #483) * `children(node)` returns the child list, or `nothing` if there are no children. The `nothing` might be seen as inconvenient, but mapping across the children of a leaf node is probably an error and one should probably branch on `is_leaf` first. * `numchildren(node)` is documented * `node[i]`, `node[i:j]` are documented to index into the child list We distinguish `GreenNode` and its implementation of `span` from `SyntaxNode` and its implementation of `byte_range` and `sourcetext` - these seem to just have very different APIs, at least as of now. I've deleted the questionable overloads of multidimensional `getindex` and the `child` function in favor of single dimensional getindex. I don't know whether anyone ever ended up using these. But I didn't and they didn't seem useful+consistent enough to keep the complexity. I've kept setindex! for now, to set a child of a `SyntaxNode`. Though I'm not sure this is a good idea to support by default.
1 parent 6532515 commit 0a51f3d

File tree

8 files changed

+149
-118
lines changed

8 files changed

+149
-118
lines changed

docs/src/api.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,46 @@ JuliaSyntax.SHORT_FORM_FUNCTION_FLAG
124124

125125
## Syntax trees
126126

127-
Syntax tree types:
127+
Access to the children of a tree node is provided by the functions
128+
129+
```@docs
130+
JuliaSyntax.is_leaf
131+
JuliaSyntax.numchildren
132+
JuliaSyntax.children
133+
```
134+
135+
For convenient access to the children, we also provide `node[i]`, `node[i:j]`
136+
and `node[begin:end]` by implementing `Base.getindex()`, `Base.firstindex()` and
137+
`Base.lastindex()`. We choose to return a view from `node[i:j]` to make it
138+
non-allocating.
139+
140+
Tree traversal is supported by using these functions along with the predicates
141+
such as [`kind`](@ref) listed above.
142+
143+
### Trees referencing the source
128144

129145
```@docs
130146
JuliaSyntax.SyntaxNode
131-
JuliaSyntax.GreenNode
132147
```
133148

134-
Functions applicable to syntax trees include everything in the sections on
149+
Functions applicable to `SyntaxNode` include everything in the sections on
135150
heads/kinds as well as the accessor functions in the source code handling
136151
section.
152+
153+
### Relocatable syntax trees
154+
155+
[`GreenNode`](@ref) is a special low level syntax tree: it's "relocatable" in
156+
the sense that it doesn't carry an absolute position in the source code or even
157+
a reference to the source text. This allows it to be reused for incremental
158+
parsing, but does make it a pain to work with directly!
159+
160+
```@docs
161+
JuliaSyntax.GreenNode
162+
```
163+
164+
Green nodes only have a relative position so implement `span()` instead of
165+
`byte_range()`:
166+
167+
```@docs
168+
JuliaSyntax.span
169+
```

src/green_tree.jl

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,58 @@ As implementation choices, we choose that:
2323
struct GreenNode{Head}
2424
head::Head
2525
span::UInt32
26-
args::Union{Nothing,Vector{GreenNode{Head}}}
26+
children::Union{Nothing,Vector{GreenNode{Head}}}
2727
end
2828

29-
function GreenNode(head::Head, span::Integer, args=nothing) where {Head}
30-
GreenNode{Head}(head, span, args)
29+
function GreenNode(head::Head, span::Integer, children=nothing) where {Head}
30+
GreenNode{Head}(head, span, children)
3131
end
3232

3333
# Accessors / predicates
34-
is_leaf(node::GreenNode) = isnothing(node.args)
35-
children(node::GreenNode) = isnothing(node.args) ? () : node.args
36-
span(node::GreenNode) = node.span
34+
is_leaf(node::GreenNode) = isnothing(node.children)
35+
children(node::GreenNode) = node.children
36+
numchildren(node::GreenNode) = isnothing(node.children) ? 0 : length(node.children)
3737
head(node::GreenNode) = node.head
3838

39+
"""
40+
span(node)
41+
42+
Get the number of bytes this node covers in the source text.
43+
"""
44+
span(node::GreenNode) = node.span
45+
46+
Base.getindex(node::GreenNode, i::Int) = children(node)[i]
47+
Base.getindex(node::GreenNode, rng::UnitRange) = view(children(node), rng)
48+
Base.firstindex(node::GreenNode) = 1
49+
Base.lastindex(node::GreenNode) = length(children(node))
50+
51+
"""
52+
Get absolute position and span of the child of `node` at the given tree `path`.
53+
"""
54+
function child_position_span(node::GreenNode, path::Int...)
55+
n = node
56+
p = 1
57+
for index in path
58+
cs = children(n)
59+
for i = 1:index-1
60+
p += span(cs[i])
61+
end
62+
n = cs[index]
63+
end
64+
return n, p, n.span
65+
end
66+
67+
function highlight(io::IO, source::SourceFile, node::GreenNode, path::Int...; kws...)
68+
_, p, span = child_position_span(node, path...)
69+
q = p + span - 1
70+
highlight(io, source, p:q; kws...)
71+
end
72+
3973
Base.summary(node::GreenNode) = summary(node.head)
4074

41-
Base.hash(node::GreenNode, h::UInt) = hash((node.head, node.span, node.args), h)
75+
Base.hash(node::GreenNode, h::UInt) = hash((node.head, node.span, node.children), h)
4276
function Base.:(==)(n1::GreenNode, n2::GreenNode)
43-
n1.head == n2.head && n1.span == n2.span && n1.args == n2.args
77+
n1.head == n2.head && n1.span == n2.span && n1.children == n2.children
4478
end
4579

4680
# Pretty printing

src/hooks.jl

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,8 @@ function _incomplete_tag(n::SyntaxNode, codelen)
4545
return :none
4646
end
4747
end
48-
if kind(c) == K"error" && begin
49-
cs = children(c)
50-
length(cs) > 0
51-
end
52-
for cc in cs
48+
if kind(c) == K"error" && numchildren(c) > 0
49+
for cc in children(c)
5350
if kind(cc) == K"error"
5451
return :other
5552
end

src/syntax_tree.jl

Lines changed: 34 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,36 @@ function _to_SyntaxNode(source::SourceFile, txtbuf::Vector{UInt8}, offset::Int,
106106
end
107107
end
108108

109+
"""
110+
is_leaf(node)
111+
112+
Determine whether the node is a leaf of the tree. In our trees a "leaf"
113+
corresponds to a single token in the source text.
114+
"""
109115
is_leaf(node::TreeNode) = node.children === nothing
110-
children(node::TreeNode) = (c = node.children; return c === nothing ? () : c)
116+
117+
"""
118+
children(node)
119+
120+
Return an iterable list of children for the node. For leaves, return `nothing`.
121+
"""
122+
children(node::TreeNode) = node.children
123+
124+
"""
125+
numchildren(node)
126+
127+
Return `length(children(node))` but possibly computed in a more efficient way.
128+
"""
111129
numchildren(node::TreeNode) = (isnothing(node.children) ? 0 : length(node.children))
112130

131+
Base.getindex(node::AbstractSyntaxNode, i::Int) = children(node)[i]
132+
Base.getindex(node::AbstractSyntaxNode, rng::UnitRange) = view(children(node), rng)
133+
Base.firstindex(node::AbstractSyntaxNode) = 1
134+
Base.lastindex(node::AbstractSyntaxNode) = length(children(node))
135+
136+
function Base.setindex!(node::SN, x::SN, i::Int) where {SN<:AbstractSyntaxNode}
137+
children(node)[i] = x
138+
end
113139

114140
"""
115141
head(x)
@@ -217,10 +243,12 @@ function Base.copy(node::TreeNode)
217243
# copy the container but not the data (ie, deep copy the tree, shallow copy the data). copy(::Expr) is similar
218244
# copy "un-parents" the top-level `node` that you're copying
219245
newnode = typeof(node)(nothing, is_leaf(node) ? nothing : typeof(node)[], copy(node.data))
220-
for child in children(node)
221-
newchild = copy(child)
222-
newchild.parent = newnode
223-
push!(newnode, newchild)
246+
if !is_leaf(node)
247+
for child in children(node)
248+
newchild = copy(child)
249+
newchild.parent = newnode
250+
push!(newnode, newchild)
251+
end
224252
end
225253
return newnode
226254
end
@@ -235,71 +263,4 @@ function build_tree(::Type{SyntaxNode}, stream::ParseStream;
235263
SyntaxNode(source, green_tree, position=first_byte(stream), keep_parens=keep_parens)
236264
end
237265

238-
#-------------------------------------------------------------------------------
239-
# Tree utilities
240-
241-
"""
242-
child(node, i1, i2, ...)
243-
244-
Get child at a tree path. If indexing accessed children, it would be
245-
`node[i1][i2][...]`
246-
"""
247-
function child(node, path::Integer...)
248-
n = node
249-
for index in path
250-
n = children(n)[index]
251-
end
252-
return n
253-
end
254-
255-
function setchild!(node::SyntaxNode, path, x)
256-
n1 = child(node, path[1:end-1]...)
257-
n1.children[path[end]] = x
258-
end
259-
260-
# We can overload multidimensional Base.getindex / Base.setindex! for node
261-
# types.
262-
#
263-
# The justification for this is to view a tree as a multidimensional ragged
264-
# array, where descending depthwise into the tree corresponds to dimensions of
265-
# the array.
266-
#
267-
# However... this analogy is only good for complete trees at a given depth (=
268-
# dimension). But the syntax is oh-so-handy!
269-
function Base.getindex(node::Union{SyntaxNode,GreenNode}, path::Int...)
270-
child(node, path...)
271-
end
272-
function Base.lastindex(node::Union{SyntaxNode,GreenNode})
273-
length(children(node))
274-
end
275-
276-
function Base.setindex!(node::SyntaxNode, x::SyntaxNode, path::Int...)
277-
setchild!(node, path, x)
278-
end
279-
280-
"""
281-
Get absolute position and span of the child of `node` at the given tree `path`.
282-
"""
283-
function child_position_span(node::GreenNode, path::Int...)
284-
n = node
285-
p = 1
286-
for index in path
287-
cs = children(n)
288-
for i = 1:index-1
289-
p += span(cs[i])
290-
end
291-
n = cs[index]
292-
end
293-
return n, p, n.span
294-
end
295-
296-
function child_position_span(node::SyntaxNode, path::Int...)
297-
n = child(node, path...)
298-
n, n.position, span(n)
299-
end
300-
301-
function highlight(io::IO, source::SourceFile, node::GreenNode, path::Int...; kws...)
302-
_, p, span = child_position_span(node, path...)
303-
q = p + span - 1
304-
highlight(io, source, p:q; kws...)
305-
end
266+
@deprecate haschildren(x) !is_leaf(x) false

test/green_node.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@
1313
SyntaxHead(K"Identifier", 0x0000)
1414
]
1515

16+
@test numchildren(t) == 5
17+
@test !is_leaf(t)
18+
@test is_leaf(t[1])
19+
20+
@test t[1] === children(t)[1]
21+
@test t[2:4] == [t[2],t[3],t[4]]
22+
@test firstindex(t) == 1
23+
@test lastindex(t) == 5
24+
1625
t2 = parsestmt(GreenNode, "aa + b")
1726
@test t == t2
1827
@test t !== t2

test/runtests.jl

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
using JuliaSyntax
22
using Test
33

4-
using JuliaSyntax: SourceFile
5-
6-
using JuliaSyntax: GreenNode, SyntaxNode,
7-
flags, EMPTY_FLAGS, TRIVIA_FLAG, INFIX_FLAG,
8-
children, child, setchild!, SyntaxHead
9-
104
include("test_utils.jl")
115
include("test_utils_tests.jl")
126
include("fuzz_test.jl")

test/syntax_tree.jl

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,35 @@
33
tt = "a*b + c"
44
t = parsestmt(SyntaxNode, tt)
55

6-
@test sourcetext(child(t, 1)) == "a*b"
7-
@test sourcetext(child(t, 1, 1)) == "a"
8-
@test sourcetext(child(t, 1, 2)) == "*"
9-
@test sourcetext(child(t, 1, 3)) == "b"
10-
@test sourcetext(child(t, 2)) == "+"
11-
@test sourcetext(child(t, 3)) == "c"
6+
@test sourcetext(t[1]) == "a*b"
7+
@test sourcetext(t[1][1]) == "a"
8+
@test sourcetext(t[1][2]) == "*"
9+
@test sourcetext(t[1][3]) == "b"
10+
@test sourcetext(t[2]) == "+"
11+
@test sourcetext(t[3]) == "c"
1212

13-
@test JuliaSyntax.first_byte(child(t, 2)) == findfirst(==('+'), tt)
14-
@test JuliaSyntax.source_line(child(t, 3)) == 1
15-
@test source_location(child(t, 3)) == (1, 7)
13+
@test JuliaSyntax.first_byte(t[2]) == findfirst(==('+'), tt)
14+
@test JuliaSyntax.source_line(t[3]) == 1
15+
@test source_location(t[3]) == (1, 7)
1616

1717
# Child indexing
18-
@test t[1] === child(t, 1)
19-
@test t[1, 1] === child(t, 1, 1)
20-
@test t[end] === child(t, 3)
21-
# Unfortunately, can't make t[1, end] work
22-
# as `lastindex(t, 2)` isn't well defined
18+
@test t[end] === t[3]
19+
@test sourcetext.(t[2:3]) == ["+", "c"]
20+
@test sourcetext.(t[2:end]) == ["+", "c"]
21+
@test firstindex(t) == 1
22+
@test lastindex(t) == 3
23+
@test !is_leaf(t)
24+
@test is_leaf(t[3])
2325

2426
@test sprint(show, t) == "(call-i (call-i a * b) + c)"
2527
@test sprint(io->show(io, MIME("text/x.sexpression"), t, show_kind=true)) ==
2628
"(call-i (call-i a::Identifier *::* b::Identifier) +::+ c::Identifier)"
2729

28-
@test sprint(highlight, child(t, 1, 3)) == "a*b + c\n# ╙"
30+
@test sprint(highlight, t[1][3]) == "a*b + c\n# ╙"
2931
@test sprint(highlight, t.source, t.raw, 1, 3) == "a*b + c\n# ╙"
3032

3133
# Pass-through field access
32-
node = child(t, 1, 1)
34+
node = t[1][1]
3335
@test node.val === :a
3436
# The specific error text has evolved over Julia versions. Check that it involves `SyntaxData` and immutability
3537
e = try node.val = :q catch e e end
@@ -40,20 +42,20 @@
4042
ct = copy(t)
4143
ct.data = nothing
4244
@test ct.data === nothing && t.data !== nothing
43-
@test child(ct, 1).parent === ct
44-
@test child(ct, 1) !== child(t, 1)
45+
@test ct[1].parent === ct
46+
@test ct[1] !== t[1]
4547

4648
node = parsestmt(SyntaxNode, "f()")
4749
push!(node, parsestmt(SyntaxNode, "x"))
4850
@test length(children(node)) == 2
4951
node[2] = parsestmt(SyntaxNode, "y")
50-
@test sourcetext(child(node, 2)) == "y"
52+
@test sourcetext(node[2]) == "y"
5153

5254
# SyntaxNode with offsets
5355
t,_ = parsestmt(SyntaxNode, "begin a end\nbegin b end", 13)
5456
@test t.position == 13
55-
@test child(t,1).position == 19
56-
@test child(t,1).val == :b
57+
@test t[1].position == 19
58+
@test t[1].val == :b
5759

5860
# Unicode character ranges
5961
src = "ab + αβ"

test/test_utils.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using Test
22

3-
# We need a relative include here as JuliaSyntax my come from Base.
3+
# We need a relative include here as JuliaSyntax may come from Base.
44
using .JuliaSyntax:
55
# Parsing
66
ParseStream,
@@ -23,14 +23,15 @@ using .JuliaSyntax:
2323
# Node inspection
2424
kind,
2525
flags,
26+
EMPTY_FLAGS, TRIVIA_FLAG, INFIX_FLAG,
2627
head,
2728
span,
2829
SyntaxHead,
2930
is_trivia,
3031
sourcetext,
3132
is_leaf,
33+
numchildren,
3234
children,
33-
child,
3435
fl_parseall,
3536
fl_parse,
3637
highlight,

0 commit comments

Comments
 (0)