Skip to content
← Back

src/syntax/parsers/statements/list.ghul

1
namespace Syntax.Parsers.Statements is
2
use System.Exception
3
4
use IO.Std
5
6
use Source
7
8
use Ghul.Pipes
9
10
class LIST(
11
terminators: Collections.LIST[Lexical.TOKEN],
12
statement_parser: Parser[Trees.Statements.Statement]
13
): Base[Trees.Statements.LIST] is
14
terminators: Collections.List[Lexical.TOKEN]
15
description: string => "statement list"
16
17
super()
18
19
parse(context: CONTEXT) -> Trees.Statements.LIST is
20
self.terminators = terminators
21
22
let start = context.location
23
let end mut = context.location
24
let statements = Collections.LIST[Trees.Statements.Statement]()
25
26
let want_semicolon mut = false
27
28
// Whether the final statement of the list was terminated with
29
// a written `;`. Compound statements never demand one, so a
30
// `;` after them is consumed here and recorded - at end-of-body
31
// the fact distinguishes a discarded statement from an
32
// implicit tail return.
33
let last_terminated mut = true
34
35
// Set while a stray clause keyword skipped as debris has not yet
36
// met its closer: a pasted fragment such as `else ... esac` is
37
// one mistake, so the closer that ends it is skipped unreported.
38
let in_debris_fragment mut = false
39
40
while !context.is_end_of_file /\ !at_terminator(context) do
41
if context.at_debris_closer then
42
let is_clause = context.is_clause_keyword(context.current_token)
43
44
if is_clause \/ !in_debris_fragment then
45
context.error(context.location, "syntax error: unexpected {context.current_token_name}")
46
fi
47
48
in_debris_fragment = is_clause
49
50
context.next_token()
51
elif context.current.token == Lexical.TOKEN.SEMICOLON then
52
let semicolon = context.current
53
context.next_token()
54
context.note_written_terminator(semicolon)
55
56
want_semicolon = false
57
last_terminated = true
58
elif want_semicolon then
59
case context.accept_terminator()
60
when Parsers.TERMINATOR.WRITTEN then
61
last_terminated = true
62
when Parsers.TERMINATOR.INFERRED then
63
last_terminated = false
64
when Parsers.TERMINATOR.MISSING then
65
// reported; recovery continues below as before
66
esac
67
68
want_semicolon = false
69
else
70
try
71
let statement = statement_parser.parse(context)
72
73
if statement? then
74
statements.add(statement)
75
76
end = statement.location
77
78
if statement.expects_semicolon then
79
want_semicolon = true
80
last_terminated = false
81
elif context.current.token == Lexical.TOKEN.SEMICOLON then
82
let semicolon = context.current
83
context.next_token()
84
context.note_written_terminator(semicolon)
85
last_terminated = true
86
else
87
// A compound statement demands no terminator,
88
// so its boundary never reaches
89
// accept_terminator - but a `;` written here
90
// is redundant, so the boundary is one the
91
// inlay marks.
92
context.note_inferred_terminator()
93
94
last_terminated = false
95
fi
96
else
97
// the statement parser reported its errors and gave up:
98
// skip ahead so the unparsed tokens don't produce a
99
// cascade of follow-on errors
100
end = recover_to_terminator(context, start.start_column)
101
fi
102
catch e: UnwindException
103
throw e
104
catch e: Exception
105
end = recover_to_terminator(context, start.start_column)
106
yrt
107
fi
108
od
109
110
// The last statement of the list falls out of the loop before
111
// reaching accept_terminator, so its boundary is recorded here.
112
if want_semicolon then
113
context.note_inferred_terminator()
114
fi
115
116
let result = Trees.Statements.LIST(start::end, statements)
117
result.last_was_terminated = last_terminated
118
119
return result
120
si
121
122
at_terminator(context: CONTEXT) -> bool =>
123
terminators |> any(t => t == context.current_token) /\ !context.at_debris_closer
124
125
// Resynchronise after a statement the parser could not build. The
126
// tokens from here to the next statement boundary are unparsed, so
127
// skipping them stops each producing an error of its own - but
128
// skipping too many is far worse than skipping too few. A keyword
129
// consumed here that some enclosing construct was waiting for
130
// leaves that construct hunting an ender that no longer exists, and
131
// one unparseable statement takes the rest of the body with it.
132
//
133
// Three things stop the skip:
134
//
135
// a token some construct still being parsed would end at, which
136
// is read from those parsers rather than guessed at - see
137
// CONTEXT._awaited_enders. A closing keyword that ends nothing
138
// open belongs to a construct the parser never entered, so it
139
// is debris and the skip goes past it;
140
// a line indented no further than the list itself;
141
// a keyword that can only begin a statement.
142
//
143
// Nothing is counted. An earlier version tracked how deep in
144
// brackets the skip had gone, which fails for the reason such
145
// counting always fails here: the code being recovered from is
146
// incomplete, so its brackets do not balance, and a count taken
147
// from them is a count of the mistake. Asking the parsers what they
148
// are waiting for needs no such assumption.
149
//
150
// The skip always consumes at least one token once it has started,
151
// or the caller re-attempts the same failing parse forever.
152
recover_to_terminator(context: CONTEXT, indent: int) -> LOCATION is
153
let end mut = context.previous_end
154
let consumed mut = false
155
156
while !context.is_end_of_file do
157
if context.is_awaited_ender(context.current_token) then
158
break
159
fi
160
161
if consumed /\ _at_resync_point(context, indent) then
162
break
163
fi
164
165
end = context.current.location
166
167
context.next_token()
168
169
consumed = true
170
od
171
172
return end
173
si
174
175
// Where the skip can hand back to the statement parser. A keyword
176
// that can begin nothing but a statement is one wherever it stands,
177
// including on the line the skip started on - a construct written
178
// after a stray token is still a construct, and stopping at its
179
// first keyword parses it rather than discarding it. An identifier
180
// or a literal is not, since either is far more often the tail of
181
// whatever is being skipped; those resynchronise only once a line
182
// has dedented back to the list's own level, which starts something
183
// new whatever it opens with.
184
_at_resync_point(context: CONTEXT, indent: int) -> bool =>
185
_starts_statement(context.current_token) \/
186
(context.current.first_on_line /\ context.current.location.start_column <= indent)
187
188
_starts_statement(token: Lexical.TOKEN) -> bool =>
189
token == Lexical.TOKEN.LET \/
190
token == Lexical.TOKEN.IF \/
191
token == Lexical.TOKEN.CASE \/
192
token == Lexical.TOKEN.WHILE \/
193
token == Lexical.TOKEN.FOR \/
194
token == Lexical.TOKEN.TRY \/
195
token == Lexical.TOKEN.RETURN \/
196
token == Lexical.TOKEN.YIELD \/
197
token == Lexical.TOKEN.THROW \/
198
token == Lexical.TOKEN.BREAK \/
199
token == Lexical.TOKEN.CONTINUE \/
200
token == Lexical.TOKEN.ASSERT
201
202
203
si
204
si