Skip to content

Commit 0a0aa04

Browse files
authored
Merge pull request #484 from JuliaLang/caf/tree-API-cleanup
Clean up and document syntax tree child access API + mark public API
2 parents 992dc07 + b644c87 commit 0a0aa04

File tree

9 files changed

+220
-131
lines changed

9 files changed

+220
-131
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/JuliaSyntax.jl

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,81 @@
11
module JuliaSyntax
22

3-
# Conservative list of exports - only export the most common/useful things
4-
# here.
3+
macro _public(syms)
4+
if VERSION >= v"1.11"
5+
names = syms isa Symbol ? [syms] : syms.args
6+
esc(Expr(:public, names...))
7+
else
8+
nothing
9+
end
10+
end
11+
12+
# Public API, in the order of docs/src/api.md
13+
14+
# Parsing.
15+
export parsestmt,
16+
parseall,
17+
parseatom
18+
19+
@_public parse!,
20+
ParseStream,
21+
build_tree
522

6-
# Parsing. See also
7-
# parse!(), ParseStream
8-
export parsestmt, parseall, parseatom
923
# Tokenization
10-
export tokenize, Token, untokenize
11-
# Source file handling. See also
12-
# highlight() sourcetext() source_line() source_location() char_range()
24+
export tokenize,
25+
Token,
26+
untokenize
27+
28+
# Source file handling
29+
@_public sourcefile,
30+
byte_range,
31+
char_range,
32+
first_byte,
33+
last_byte,
34+
filename,
35+
source_line,
36+
source_location,
37+
sourcetext,
38+
highlight
39+
1340
export SourceFile
14-
# Expression heads/kinds. See also
15-
# flags() and related predicates.
16-
export @K_str, kind, head
17-
# Syntax tree types. See also
18-
# GreenNode
41+
@_public source_line_range
42+
43+
# Expression predicates, kinds and flags
44+
export @K_str, kind
45+
@_public Kind
46+
47+
@_public flags,
48+
SyntaxHead,
49+
head,
50+
is_trivia,
51+
is_prefix_call,
52+
is_infix_op_call,
53+
is_prefix_op_call,
54+
is_postfix_op_call,
55+
is_dotted,
56+
is_suffixed,
57+
is_decorated,
58+
numeric_flags,
59+
has_flags,
60+
TRIPLE_STRING_FLAG,
61+
RAW_STRING_FLAG,
62+
PARENS_FLAG,
63+
COLON_QUOTE,
64+
TOPLEVEL_SEMICOLONS_FLAG,
65+
MUTABLE_FLAG,
66+
BARE_MODULE_FLAG,
67+
SHORT_FORM_FUNCTION_FLAG
68+
69+
# Syntax trees
70+
@_public is_leaf,
71+
numchildren,
72+
children
73+
1974
export SyntaxNode
2075

76+
@_public GreenNode,
77+
span
78+
2179
# Helper utilities
2280
include("utils.jl")
2381

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")

0 commit comments

Comments
 (0)