Skip to content

Commit 9e6392e

Browse files
authored
Generate Expr(:incomplete) for errors which hit EOF (#102)
This allows REPL completion to work correctly. It works by pattern matching the parse tree, rather than hard coding incomplete expression detection into the parser itself. There's still more changes from #88 which would help make this nicer but for now it works. The tests here are somewhat derived from Base, but had to be reviewed and tweaked because they turn out to not really be consistent. For example, "begin;" and "begin" are both the prefix of a block construct, one of them shouldn't come out as `:other`.
1 parent b2372b7 commit 9e6392e

File tree

4 files changed

+163
-7
lines changed

4 files changed

+163
-7
lines changed

src/hooks.jl

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,76 @@
11
# This file provides an adaptor to match the API expected by the Julia runtime
22
# code in the binding Core._parse
33

4+
# Find the first error in a SyntaxNode tree, returning the index of the error
5+
# within its parent and the node itself.
6+
function _first_error(t::SyntaxNode)
7+
if is_error(t)
8+
return 0,t
9+
end
10+
if haschildren(t)
11+
for (i,c) in enumerate(children(t))
12+
if is_error(c)
13+
return i,c
14+
else
15+
x = _first_error(c)
16+
if x != (0,nothing)
17+
return x
18+
end
19+
end
20+
end
21+
end
22+
return 0,nothing
23+
end
24+
25+
# Classify an incomplete expression, returning a Symbol compatible with
26+
# Base.incomplete_tag().
27+
#
28+
# Roughly, the intention here is to classify which expression head is expected
29+
# next if the incomplete stream was to continue. (Though this is just rough. In
30+
# practice several categories are combined for the purposes of the REPL -
31+
# perhaps we can/should do something more precise in the future.)
32+
function _incomplete_tag(n::SyntaxNode)
33+
i,c = _first_error(n)
34+
if isnothing(c)
35+
return :none
36+
end
37+
# TODO: Check error hits last character
38+
if kind(c) == K"error" && begin
39+
cs = children(c)
40+
length(cs) > 0
41+
end
42+
k1 = kind(cs[1])
43+
if k1 == K"ErrorEofMultiComment"
44+
return :comment
45+
elseif k1 == K"ErrorEofChar"
46+
# TODO: Make this case into an internal node
47+
return :char
48+
end
49+
for cc in cs
50+
if kind(cc) == K"error"
51+
return :other
52+
end
53+
end
54+
end
55+
kp = kind(c.parent)
56+
if kp == K"string"
57+
return :string
58+
elseif kp == K"cmdstring"
59+
return :cmd
60+
elseif kp in KSet"block quote let try"
61+
return :block
62+
elseif kp in KSet"for while function if"
63+
return i == 1 ? :other : :block
64+
elseif kp in KSet"module struct"
65+
return i == 2 ? :other : :block
66+
elseif kp == K"do"
67+
return i < 3 ? :other : :block
68+
else
69+
return :other
70+
end
71+
end
72+
73+
#-------------------------------------------------------------------------------
474
@static if isdefined(Core, :_setparser!)
575
const _set_core_parse_hook = Core._setparser!
676
elseif isdefined(Core, :set_parser)
@@ -93,11 +163,29 @@ function _core_parser_hook(code, filename, lineno, offset, options)
93163
end
94164

95165
if any_error(stream)
96-
e = Expr(:error, ParseError(SourceFile(code, filename=filename), stream.diagnostics))
97-
ex = options === :all ? Expr(:toplevel, e) : e
166+
tree = build_tree(SyntaxNode, stream, wrap_toplevel_as_kind=K"None")
167+
_,err = _first_error(tree)
168+
# In the flisp parser errors are normally `Expr(:error, msg)` where
169+
# `msg` is a String. By using a ParseError for msg we can do fancy
170+
# error reporting instead.
171+
if last_byte(err) == lastindex(code)
172+
tag = _incomplete_tag(tree)
173+
# Here we replicate the particular messages
174+
msg =
175+
tag === :string ? "incomplete: invalid string syntax" :
176+
tag === :comment ? "incomplete: unterminated multi-line comment #= ... =#" :
177+
tag === :block ? "incomplete: construct requires end" :
178+
tag === :cmd ? "incomplete: invalid \"`\" syntax" :
179+
tag === :char ? "incomplete: invalid character literal" :
180+
"incomplete: premature end of input"
181+
error_ex = Expr(:incomplete, msg)
182+
else
183+
error_ex = Expr(:error, ParseError(stream, filename=filename))
184+
end
185+
ex = options === :all ? Expr(:toplevel, error_ex) : error_ex
98186
else
99187
# FIXME: Add support to lineno to this tree build (via SourceFile?)
100-
ex = build_tree(Expr, stream, filename=filename, wrap_toplevel_as_kind=K"None")
188+
ex = build_tree(Expr, stream; filename=filename, wrap_toplevel_as_kind=K"None")
101189
if Meta.isexpr(ex, :None)
102190
# The None wrapping is only to give somewhere for trivia to be
103191
# attached; unwrap!

src/parser_api.jl

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ struct ParseError <: Exception
4646
diagnostics::Vector{Diagnostic}
4747
end
4848

49+
function ParseError(stream::ParseStream; filename=nothing)
50+
source = SourceFile(sourcetext(stream), filename=filename)
51+
ParseError(source, stream.diagnostics)
52+
end
53+
4954
function Base.showerror(io::IO, err::ParseError, bt; backtrace=false)
5055
println(io, "ParseError:")
5156
show_diagnostics(io, err.diagnostics, err.source)
@@ -156,8 +161,7 @@ function parseall(::Type{T}, input...; rule=:toplevel, version=VERSION,
156161
emit_diagnostic(stream, error="unexpected text after parsing $rule")
157162
end
158163
if any_error(stream.diagnostics)
159-
source = SourceFile(sourcetext(stream, steal_textbuf=true), filename=filename)
160-
throw(ParseError(source, stream.diagnostics))
164+
throw(ParseError(stream, filename=filename))
161165
end
162166
# TODO: Figure out a more satisfying solution to the wrap_toplevel_as_kind
163167
# mess that we've got here.

src/syntax_tree.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ children(node::SyntaxNode) = haschildren(node) ? node.val::Vector{SyntaxNode} :
120120

121121
span(node::SyntaxNode) = span(node.raw)
122122

123+
first_byte(node::SyntaxNode) = node.position
124+
last_byte(node::SyntaxNode) = node.position + span(node) - 1
125+
123126
"""
124127
sourcetext(node)
125128
@@ -138,7 +141,7 @@ end
138141
function _show_syntax_node(io, current_filename, node::SyntaxNode, indent)
139142
fname = node.source.filename
140143
line, col = source_location(node.source, node.position)
141-
posstr = "$(lpad(line, 4)):$(rpad(col,3))$(lpad(node.position,6)):$(rpad(node.position+span(node)-1,6))"
144+
posstr = "$(lpad(line, 4)):$(rpad(col,3))$(lpad(first_byte(node),6)):$(rpad(last_byte(node),6))"
142145
val = node.val
143146
nodestr = haschildren(node) ? "[$(untokenize(head(node)))]" :
144147
isa(val, Symbol) ? string(val) : repr(val)

test/hooks.jl

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,69 @@
3131

3232
# Check that Meta.parse throws the JuliaSyntax.ParseError rather than
3333
# Meta.ParseError when Core integration is enabled.
34-
@test_throws JuliaSyntax.ParseError Meta.parse("[x")
34+
@test_throws JuliaSyntax.ParseError Meta.parse("[x)")
3535

3636
JuliaSyntax.enable_in_core!(false)
3737
end
38+
39+
@testset "Expr(:incomplete)" begin
40+
JuliaSyntax.enable_in_core!()
41+
42+
@test Meta.isexpr(Meta.parse("[x"), :incomplete)
43+
44+
for (str, tag) in [
45+
"" => :none
46+
"\"" => :string
47+
"\"\$foo" => :string
48+
"#=" => :comment
49+
"'" => :char
50+
"'a" => :char
51+
"`" => :cmd
52+
"(" => :other
53+
"[" => :other
54+
"begin" => :block
55+
"quote" => :block
56+
"let" => :block
57+
"let;" => :block
58+
"for" => :other
59+
"for x=xs" => :block
60+
"function" => :other
61+
"function f()" => :block
62+
"macro" => :other
63+
"macro f()" => :block
64+
"f() do" => :other
65+
"f() do x" => :block
66+
"module" => :other
67+
"module X" => :block
68+
"baremodule" => :other
69+
"baremodule X" => :block
70+
"mutable struct" => :other
71+
"mutable struct X" => :block
72+
"struct" => :other
73+
"struct X" => :block
74+
"if" => :other
75+
"if x" => :block
76+
"while" => :other
77+
"while x" => :block
78+
"try" => :block
79+
# could be `try x catch exc body end` or `try x catch ; body end`
80+
"try x catch" => :block
81+
"using" => :other
82+
"import" => :other
83+
"local" => :other
84+
"global" => :other
85+
86+
"1 == 2 ?" => :other
87+
"1 == 2 ? 3 :" => :other
88+
"1," => :other
89+
"1, " => :other
90+
"1,\n" => :other
91+
"1, \n" => :other
92+
]
93+
@testset "$(repr(str))" begin
94+
@test Base.incomplete_tag(Meta.parse(str, raise=false)) == tag
95+
end
96+
end
97+
JuliaSyntax.enable_in_core!(false)
98+
end
3899
end

0 commit comments

Comments
 (0)