volaro 0.0.2 → 0.1.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -22
- package/bin/vl.js +466 -28
- package/compiler/SOURCE_INFO.json +6 -0
- package/compiler/SOURCE_REV +1 -0
- package/compiler/validator/vlcheck/__init__.py +10 -0
- package/compiler/validator/vlcheck/__main__.py +128 -0
- package/compiler/validator/vlcheck/ast_nodes.py +397 -0
- package/compiler/validator/vlcheck/checks.py +1227 -0
- package/compiler/validator/vlcheck/diagnostics.py +88 -0
- package/compiler/validator/vlcheck/lexer.py +343 -0
- package/compiler/validator/vlcheck/parser.py +1638 -0
- package/compiler/validator/vlcheck/project_config.py +97 -0
- package/compiler/validator/vlcheck/resolve.py +849 -0
- package/compiler/validator/vlcheck/test_ids.py +98 -0
- package/compiler/vlbuild/styling/README.md +39 -0
- package/compiler/vlbuild/styling/build-css.mjs +103 -0
- package/compiler/vlbuild/styling/package-lock.json +1254 -0
- package/compiler/vlbuild/styling/package.json +15 -0
- package/compiler/vlbuild/styling/test-build-css.mjs +69 -0
- package/compiler/vlbuild/vlbuild/__init__.py +16 -0
- package/compiler/vlbuild/vlbuild/__main__.py +246 -0
- package/compiler/vlbuild/vlbuild/assets/vlrt.css +165 -0
- package/compiler/vlbuild/vlbuild/assets/vlrt.js +1291 -0
- package/compiler/vlbuild/vlbuild/emit.py +2023 -0
- package/compiler/vlbuild/vlbuild/server_emit.py +1551 -0
- package/compiler/vlbuild/vlbuild/static_assets.py +83 -0
- package/compiler/vlbuild/vlbuild/style_config.py +347 -0
- package/compiler/vlbuild/vlbuild/styling.py +39 -0
- package/examples/station.vl +2 -2
- package/language/crib.md +131 -10
- package/language/spec.md +209 -7
- package/language/supported.md +185 -0
- package/lib/env.js +107 -0
- package/package.json +19 -2
- package/scripts/record-provenance.mjs +51 -0
- package/scripts/selftest.mjs +54 -0
- package/scripts/sync-compiler.sh +52 -0
|
@@ -0,0 +1,1638 @@
|
|
|
1
|
+
"""A tolerant recursive-descent parser with a Pratt expression sub-parser.
|
|
2
|
+
|
|
3
|
+
Design choices for a Stage 1 validator:
|
|
4
|
+
- Never abort. On an unexpected token, record E-PARSE and resynchronize on the
|
|
5
|
+
next NEWLINE / DEDENT so the rest of the file still gets checked.
|
|
6
|
+
- Parse types, `fn` signatures, and the skeleton of views / services precisely,
|
|
7
|
+
because that is where the checks live.
|
|
8
|
+
- Parse the many free-form configuration lines (api settings, auth providers,
|
|
9
|
+
theme, service errors/before/after) permissively as `Directive` nodes.
|
|
10
|
+
- Binding powers follow spec section 5.2 ordering (range binds tighter than
|
|
11
|
+
comparison, looser than additive). See PROJECT.md section 5.3 for the spec's
|
|
12
|
+
internal disagreement here.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
from . import ast_nodes as A
|
|
20
|
+
from .diagnostics import Diagnostic, Severity
|
|
21
|
+
from .lexer import Token
|
|
22
|
+
|
|
23
|
+
_METHODS = ("get", "post", "put", "patch", "delete")
|
|
24
|
+
_ASSIGN_OPS = ("=", "+=", "-=", "*=", "/=")
|
|
25
|
+
_IDENT_RE = re.compile(r"(?<![.\w])([A-Za-z_][A-Za-z0-9_]*)")
|
|
26
|
+
|
|
27
|
+
# binop -> (left_bp, right_bp); equal bp marks non-associative
|
|
28
|
+
_BINOPS = {
|
|
29
|
+
"|>": (1, 2),
|
|
30
|
+
"??": (4, 3), # right associative
|
|
31
|
+
"or": (5, 6),
|
|
32
|
+
"and": (7, 8),
|
|
33
|
+
"==": (9, 9), "!=": (9, 9),
|
|
34
|
+
"<": (11, 11), "<=": (11, 11), ">": (11, 11), ">=": (11, 11),
|
|
35
|
+
"..": (12, 12), "..=": (12, 12),
|
|
36
|
+
"+": (13, 14), "-": (13, 14),
|
|
37
|
+
"*": (15, 16), "/": (15, 16), "%": (15, 16),
|
|
38
|
+
}
|
|
39
|
+
_PREFIX_BP = 17
|
|
40
|
+
_CMP = {"<", "<=", ">", ">=", "==", "!="}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Parser:
|
|
44
|
+
def __init__(self, tokens: list[Token], src: str, filename: str = "<input>") -> None:
|
|
45
|
+
self.toks = tokens
|
|
46
|
+
self.src = src
|
|
47
|
+
self.filename = filename
|
|
48
|
+
self.pos = 0
|
|
49
|
+
self.diags: list[Diagnostic] = []
|
|
50
|
+
# Mirrors the lexer's own `bracket_depth` at the token-stream level
|
|
51
|
+
# (same characters, so always in lockstep) — used by `_end_line` to
|
|
52
|
+
# tolerate a statement ending at a closing bracket / comma instead of
|
|
53
|
+
# a real NEWLINE, the shape a block-bodied closure's last statement
|
|
54
|
+
# takes when it is itself a call argument (see
|
|
55
|
+
# `_parse_bracketed_closure_body`, B2 in the callback-props decision).
|
|
56
|
+
self.bracket_depth = 0
|
|
57
|
+
# One-shot: set by `_parse_bracketed_closure_body` when it ends a
|
|
58
|
+
# block-bodied closure's block by finding a sibling call argument at
|
|
59
|
+
# a shallower column, having already consumed (inside that last
|
|
60
|
+
# statement's own `_end_line()`) the real NEWLINE token that would
|
|
61
|
+
# otherwise have told the enclosing call-argument loop a separator
|
|
62
|
+
# occurred. Read via `_take_closure_ate_sep`, which resets it.
|
|
63
|
+
self._closure_ate_sep = False
|
|
64
|
+
|
|
65
|
+
def _take_closure_ate_sep(self) -> bool:
|
|
66
|
+
v = self._closure_ate_sep
|
|
67
|
+
self._closure_ate_sep = False
|
|
68
|
+
return v
|
|
69
|
+
|
|
70
|
+
# -- cursor ----------------------------------------------------------
|
|
71
|
+
def peek(self, k: int = 0) -> Token:
|
|
72
|
+
j = self.pos + k
|
|
73
|
+
return self.toks[j] if j < len(self.toks) else self.toks[-1]
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def cur(self) -> Token:
|
|
77
|
+
return self.peek()
|
|
78
|
+
|
|
79
|
+
def at(self, kind: str) -> bool:
|
|
80
|
+
return self.cur.kind == kind
|
|
81
|
+
|
|
82
|
+
def at_kw(self, *words: str) -> bool:
|
|
83
|
+
return self.cur.kind == "KW" and (not words or self.cur.value in words)
|
|
84
|
+
|
|
85
|
+
def at_soft(self, *words: str) -> bool:
|
|
86
|
+
return self.cur.kind == "NAME" and self.cur.value in words
|
|
87
|
+
|
|
88
|
+
def at_op(self, *ops: str) -> bool:
|
|
89
|
+
return self.cur.kind == "OP" and self.cur.value in ops
|
|
90
|
+
|
|
91
|
+
def advance(self) -> Token:
|
|
92
|
+
t = self.cur
|
|
93
|
+
if t.kind == "OP" and t.value in "([{":
|
|
94
|
+
self.bracket_depth += 1
|
|
95
|
+
elif t.kind == "OP" and t.value in ")]}":
|
|
96
|
+
self.bracket_depth = max(0, self.bracket_depth - 1)
|
|
97
|
+
if self.pos < len(self.toks) - 1:
|
|
98
|
+
self.pos += 1
|
|
99
|
+
return t
|
|
100
|
+
|
|
101
|
+
def expect_op(self, op: str, ctx: str = "") -> Token | None:
|
|
102
|
+
if self.at_op(op):
|
|
103
|
+
return self.advance()
|
|
104
|
+
self.error(f"expected {op!r}{(' ' + ctx) if ctx else ''}, found {self._desc(self.cur)}",
|
|
105
|
+
self.cur)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def error(self, msg: str, tok: Token, code: str = "E-PARSE", fix: str | None = None) -> None:
|
|
109
|
+
self.diags.append(Diagnostic(code, Severity.ERROR, msg, tok.start, max(tok.end, tok.start + 1),
|
|
110
|
+
fix, self.filename))
|
|
111
|
+
|
|
112
|
+
def _desc(self, t: Token) -> str:
|
|
113
|
+
if t.kind == "EOF":
|
|
114
|
+
return "end of file"
|
|
115
|
+
if t.kind == "NEWLINE":
|
|
116
|
+
return "end of line"
|
|
117
|
+
if t.kind in ("INDENT", "DEDENT"):
|
|
118
|
+
return t.kind.lower()
|
|
119
|
+
return f"{t.value!r}" if t.value else t.kind
|
|
120
|
+
|
|
121
|
+
def skip_newlines(self) -> None:
|
|
122
|
+
while self.at("NEWLINE"):
|
|
123
|
+
self.advance()
|
|
124
|
+
|
|
125
|
+
def _at_line_end(self) -> bool:
|
|
126
|
+
return self.cur.kind in ("NEWLINE", "DEDENT", "EOF")
|
|
127
|
+
|
|
128
|
+
def _end_line(self) -> None:
|
|
129
|
+
if self.at("NEWLINE"):
|
|
130
|
+
self.advance()
|
|
131
|
+
elif self.cur.kind in ("DEDENT", "EOF"):
|
|
132
|
+
return
|
|
133
|
+
elif self.bracket_depth > 0 and self.at_op(")", "]", "}", ","):
|
|
134
|
+
# The statement ended because the enclosing bracket closes (or the
|
|
135
|
+
# next comma-separated item starts) right here, not because of a
|
|
136
|
+
# real newline — exactly the shape of the last statement in a
|
|
137
|
+
# block-bodied closure passed as a call argument (see
|
|
138
|
+
# `_parse_bracketed_closure_body`): the lexer suppresses
|
|
139
|
+
# INDENT/DEDENT inside any open bracket, so there is no DEDENT to
|
|
140
|
+
# end it with either; `parse_stmt` still calls `_end_line`
|
|
141
|
+
# unconditionally, so this has to be tolerated here.
|
|
142
|
+
return
|
|
143
|
+
else:
|
|
144
|
+
self.error(f"expected end of line, found {self._desc(self.cur)}", self.cur)
|
|
145
|
+
self._recover_to_line()
|
|
146
|
+
|
|
147
|
+
def _recover_to_line(self) -> None:
|
|
148
|
+
while self.cur.kind not in ("EOF",):
|
|
149
|
+
if self.at("NEWLINE"):
|
|
150
|
+
self.advance()
|
|
151
|
+
return
|
|
152
|
+
if self.at("DEDENT"):
|
|
153
|
+
return
|
|
154
|
+
self.advance()
|
|
155
|
+
|
|
156
|
+
# -- blocks ----------------------------------------------------------
|
|
157
|
+
def parse_block(self, parse_item) -> list:
|
|
158
|
+
items: list = []
|
|
159
|
+
self.skip_newlines()
|
|
160
|
+
if not self.at("INDENT"):
|
|
161
|
+
if self.bracket_depth > 0:
|
|
162
|
+
# `_parse_bracketed_closure_body` (a flat statement sequence,
|
|
163
|
+
# tracked by source column) is the only block form available
|
|
164
|
+
# inside an open bracket -- the lexer never emits INDENT/
|
|
165
|
+
# DEDENT there, so a *nested* block (this `if`/`match`/`for`/
|
|
166
|
+
# `while`/`try`/`par`/`spawn` trying to open one) has nothing
|
|
167
|
+
# to parse. Name the real cause instead of the generic
|
|
168
|
+
# message, which otherwise reads as unrelated to the actual
|
|
169
|
+
# `fn(...)` closure a few tokens up.
|
|
170
|
+
self.error(
|
|
171
|
+
"a nested block (if / match / for / while / try / par / "
|
|
172
|
+
"spawn) is not supported inside a closure written "
|
|
173
|
+
"directly as a call argument -- only a flat sequence of "
|
|
174
|
+
"statements is. Bind the closure with `let` above the "
|
|
175
|
+
"call instead, then pass it by name.",
|
|
176
|
+
self.cur, code="E-CLOSURE-NESTED-BLOCK")
|
|
177
|
+
else:
|
|
178
|
+
self.error("expected an indented block", self.cur)
|
|
179
|
+
return items
|
|
180
|
+
self.advance() # INDENT
|
|
181
|
+
while not self.at("DEDENT") and not self.at("EOF"):
|
|
182
|
+
self.skip_newlines()
|
|
183
|
+
if self.at("DEDENT") or self.at("EOF"):
|
|
184
|
+
break
|
|
185
|
+
before = self.pos
|
|
186
|
+
node = parse_item()
|
|
187
|
+
if node is not None:
|
|
188
|
+
if isinstance(node, list):
|
|
189
|
+
items.extend(node)
|
|
190
|
+
else:
|
|
191
|
+
items.append(node)
|
|
192
|
+
self.skip_newlines()
|
|
193
|
+
if self.pos == before:
|
|
194
|
+
self._recover_to_line()
|
|
195
|
+
if self.at("DEDENT"):
|
|
196
|
+
self.advance()
|
|
197
|
+
return items
|
|
198
|
+
|
|
199
|
+
# -- module / items ----------------------------------------------------
|
|
200
|
+
def parse_module(self) -> A.Module:
|
|
201
|
+
mod = A.Module(start=0)
|
|
202
|
+
self.skip_newlines()
|
|
203
|
+
while not self.at("EOF"):
|
|
204
|
+
before = self.pos
|
|
205
|
+
item = self.parse_item()
|
|
206
|
+
if item is not None:
|
|
207
|
+
mod.items.append(item)
|
|
208
|
+
self.skip_newlines()
|
|
209
|
+
if self.pos == before:
|
|
210
|
+
self.advance()
|
|
211
|
+
mod.end = self.cur.end
|
|
212
|
+
return mod
|
|
213
|
+
|
|
214
|
+
def parse_item(self):
|
|
215
|
+
t = self.cur
|
|
216
|
+
if self.at_kw("use"):
|
|
217
|
+
return self.parse_use()
|
|
218
|
+
if self.at_kw("type"):
|
|
219
|
+
return self.parse_type_decl()
|
|
220
|
+
if self.at_kw("fn"):
|
|
221
|
+
return self.parse_fn_decl()
|
|
222
|
+
if self.at_soft("view"):
|
|
223
|
+
return self.parse_view_decl()
|
|
224
|
+
if self.at_soft("api"):
|
|
225
|
+
return self.parse_api_decl()
|
|
226
|
+
if self.at_soft("service"):
|
|
227
|
+
return self.parse_service_decl()
|
|
228
|
+
if self.at_kw("auth"):
|
|
229
|
+
return self.parse_auth_decl()
|
|
230
|
+
if self.at_soft("model"):
|
|
231
|
+
return self.parse_model_decl()
|
|
232
|
+
if self.at_soft("theme"):
|
|
233
|
+
return self.parse_theme_decl()
|
|
234
|
+
if self.at_soft("recipe"):
|
|
235
|
+
return self.parse_recipe_decl()
|
|
236
|
+
if self.at_soft("page"):
|
|
237
|
+
return self.parse_page_decl()
|
|
238
|
+
if self.at_kw("let") or self.at_soft("state") or self.at_soft("derive"):
|
|
239
|
+
self.error(
|
|
240
|
+
f"'{t.value}' binds a value and a module holds declarations only",
|
|
241
|
+
t, fix="move it inside a view or fn; for a shared value use a "
|
|
242
|
+
"zero-parameter fn, and for config use volaro.toml / env")
|
|
243
|
+
self._recover_to_line()
|
|
244
|
+
return None
|
|
245
|
+
self.error(f"unexpected {self._desc(t)} at top level", t)
|
|
246
|
+
self._recover_to_line()
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
def parse_use(self) -> A.Use:
|
|
250
|
+
start = self.advance().start
|
|
251
|
+
parts: list[str] = []
|
|
252
|
+
# A module filename may coincide with a language keyword (`auth.vl` is
|
|
253
|
+
# the project's own shared authentication declaration). Once `use` has
|
|
254
|
+
# started, keyword tokens are path text too; punctuation/newline still
|
|
255
|
+
# delimit the path unambiguously.
|
|
256
|
+
while (self.cur.kind in ("NAME", "TYPENAME", "PRIM", "KW")
|
|
257
|
+
or self.at_op(".", "..", "/")):
|
|
258
|
+
parts.append(self.advance().value)
|
|
259
|
+
names: list[str] = []
|
|
260
|
+
if self.at_op("{"):
|
|
261
|
+
self.advance()
|
|
262
|
+
while not self.at_op("}") and not self.at("EOF"):
|
|
263
|
+
if self.cur.kind in ("NAME", "TYPENAME"):
|
|
264
|
+
names.append(self.advance().value)
|
|
265
|
+
if self.at_op(","):
|
|
266
|
+
self.advance()
|
|
267
|
+
else:
|
|
268
|
+
break
|
|
269
|
+
self.expect_op("}")
|
|
270
|
+
end = self.cur.end
|
|
271
|
+
self._end_line()
|
|
272
|
+
return A.Use(start=start, end=end, path="".join(parts), names=names)
|
|
273
|
+
|
|
274
|
+
# -- type / model ----------------------------------------------------
|
|
275
|
+
def parse_type_decl(self) -> A.TypeDecl:
|
|
276
|
+
start = self.advance().start
|
|
277
|
+
name = self.advance().value if self.at("TYPENAME") else "?"
|
|
278
|
+
node = A.TypeDecl(start=start, name=name)
|
|
279
|
+
if self.at_op("="):
|
|
280
|
+
self.advance()
|
|
281
|
+
if self.at("NEWLINE"):
|
|
282
|
+
node.kind = "union"
|
|
283
|
+
node.variants = self.parse_block(self.parse_variant_block)
|
|
284
|
+
return node
|
|
285
|
+
tref = self.parse_type_ref()
|
|
286
|
+
if self.at_op("|"):
|
|
287
|
+
node.kind = "union"
|
|
288
|
+
node.variants.append(A.Variant(start=tref.start, name=tref.name))
|
|
289
|
+
while self.at_op("|"):
|
|
290
|
+
self.advance()
|
|
291
|
+
node.variants.append(self.parse_variant_inline())
|
|
292
|
+
self._end_line()
|
|
293
|
+
else:
|
|
294
|
+
node.kind = "alias"
|
|
295
|
+
node.alias_of = tref
|
|
296
|
+
if self.at_soft("distinct"):
|
|
297
|
+
self.advance()
|
|
298
|
+
node.distinct = True
|
|
299
|
+
self._end_line()
|
|
300
|
+
return node
|
|
301
|
+
node.kind = "record"
|
|
302
|
+
node.fields = self.parse_block(self.parse_field)
|
|
303
|
+
node.end = self.cur.end
|
|
304
|
+
return node
|
|
305
|
+
|
|
306
|
+
def parse_variant_block(self):
|
|
307
|
+
if self.cur.kind not in ("NAME", "TYPENAME"):
|
|
308
|
+
self.error("expected a variant name", self.cur)
|
|
309
|
+
self._recover_to_line()
|
|
310
|
+
return None
|
|
311
|
+
v = A.Variant(start=self.cur.start, name=self.advance().value)
|
|
312
|
+
if self.at_op("("):
|
|
313
|
+
self.advance()
|
|
314
|
+
v.params = self._param_list_until(")")
|
|
315
|
+
self.expect_op(")")
|
|
316
|
+
self._end_line()
|
|
317
|
+
return v
|
|
318
|
+
|
|
319
|
+
def parse_variant_inline(self) -> A.Variant:
|
|
320
|
+
v = A.Variant(start=self.cur.start,
|
|
321
|
+
name=self.advance().value if self.cur.kind in ("NAME", "TYPENAME") else "?")
|
|
322
|
+
if self.at_op("("):
|
|
323
|
+
self.advance()
|
|
324
|
+
v.params = self._param_list_until(")")
|
|
325
|
+
self.expect_op(")")
|
|
326
|
+
return v
|
|
327
|
+
|
|
328
|
+
def parse_field(self):
|
|
329
|
+
# field / param names may be keywords (e.g. `role`, `type`) per spec section 4.3
|
|
330
|
+
if self.cur.kind not in ("NAME", "KW"):
|
|
331
|
+
self.error("expected a field name", self.cur)
|
|
332
|
+
self._recover_to_line()
|
|
333
|
+
return None
|
|
334
|
+
f = A.Field(start=self.cur.start, name=self.advance().value)
|
|
335
|
+
if not self._at_line_end() and not self.at_op("="):
|
|
336
|
+
f.type = self.parse_type_ref()
|
|
337
|
+
if self.at_op("="):
|
|
338
|
+
self.advance()
|
|
339
|
+
f.default = self.parse_expr()
|
|
340
|
+
self._end_line()
|
|
341
|
+
return f
|
|
342
|
+
|
|
343
|
+
def parse_model_decl(self) -> A.ModelDecl:
|
|
344
|
+
start = self.advance().start
|
|
345
|
+
name = self.advance().value if self.at("TYPENAME") else "?"
|
|
346
|
+
node = A.ModelDecl(start=start, name=name)
|
|
347
|
+
|
|
348
|
+
def item():
|
|
349
|
+
if self.at_soft("index"):
|
|
350
|
+
self.advance()
|
|
351
|
+
cols: list[str] = []
|
|
352
|
+
if self.at_op("("):
|
|
353
|
+
self.advance()
|
|
354
|
+
while not self.at_op(")") and not self.at("EOF"):
|
|
355
|
+
if self.at("NAME"):
|
|
356
|
+
cols.append(self.advance().value)
|
|
357
|
+
if self.at_soft("desc"):
|
|
358
|
+
self.advance()
|
|
359
|
+
if self.at_op(","):
|
|
360
|
+
self.advance()
|
|
361
|
+
else:
|
|
362
|
+
break
|
|
363
|
+
self.expect_op(")")
|
|
364
|
+
self._end_line()
|
|
365
|
+
node.indexes.append(cols)
|
|
366
|
+
return None
|
|
367
|
+
if self.cur.kind not in ("NAME", "KW"):
|
|
368
|
+
self.error("expected a model field", self.cur)
|
|
369
|
+
self._recover_to_line()
|
|
370
|
+
return None
|
|
371
|
+
f = A.Field(start=self.cur.start, name=self.advance().value)
|
|
372
|
+
if not self._at_line_end() and not self.at_op("="):
|
|
373
|
+
f.type = self.parse_type_ref()
|
|
374
|
+
while not self._at_line_end():
|
|
375
|
+
if self.at_op("="):
|
|
376
|
+
self.advance()
|
|
377
|
+
f.default = self.parse_expr()
|
|
378
|
+
continue
|
|
379
|
+
if self.at("NAME"):
|
|
380
|
+
a = self.advance().value
|
|
381
|
+
if self.at_op(":"):
|
|
382
|
+
self.advance()
|
|
383
|
+
val: list[str] = []
|
|
384
|
+
while self.cur.kind in ("NAME", "TYPENAME") or self.at_op("."):
|
|
385
|
+
val.append(self.advance().value)
|
|
386
|
+
f.attrs.append(f"{a}:{''.join(val)}")
|
|
387
|
+
else:
|
|
388
|
+
f.attrs.append(a)
|
|
389
|
+
continue
|
|
390
|
+
self.error(f"unexpected {self._desc(self.cur)} in model field", self.cur)
|
|
391
|
+
self._recover_to_line()
|
|
392
|
+
break
|
|
393
|
+
self._end_line()
|
|
394
|
+
node.fields.append(f)
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
self.parse_block(item)
|
|
398
|
+
node.end = self.cur.end
|
|
399
|
+
return node
|
|
400
|
+
|
|
401
|
+
# -- type refs -----------------------------------------------------
|
|
402
|
+
def parse_type_ref(self) -> A.TypeRef:
|
|
403
|
+
start = self.cur.start
|
|
404
|
+
if self.at_kw("fn"):
|
|
405
|
+
self.advance()
|
|
406
|
+
self.expect_op("(")
|
|
407
|
+
fps = []
|
|
408
|
+
while not self.at_op(")") and not self.at("EOF") and not self.at("NEWLINE"):
|
|
409
|
+
fps.append(self.parse_type_ref())
|
|
410
|
+
if self.at_op(","):
|
|
411
|
+
self.advance()
|
|
412
|
+
else:
|
|
413
|
+
break
|
|
414
|
+
self.expect_op(")")
|
|
415
|
+
ret = self.parse_type_ref()
|
|
416
|
+
return A.TypeRef(start=start, end=self.cur.end, is_fn=True, fn_params=fps, fn_ret=ret)
|
|
417
|
+
|
|
418
|
+
if self.cur.kind in ("PRIM", "TYPENAME", "NAME"):
|
|
419
|
+
tr = A.TypeRef(start=start, name=self.advance().value)
|
|
420
|
+
if self.at_op("<"):
|
|
421
|
+
self.advance()
|
|
422
|
+
while not self.at_op(">") and not self.at("EOF") and not self.at("NEWLINE"):
|
|
423
|
+
tr.args.append(self.parse_type_ref())
|
|
424
|
+
if self.at_op(","):
|
|
425
|
+
self.advance()
|
|
426
|
+
else:
|
|
427
|
+
break
|
|
428
|
+
self.expect_op(">")
|
|
429
|
+
elif self.at("NIL"):
|
|
430
|
+
self.advance()
|
|
431
|
+
tr = A.TypeRef(start=start, name="nil")
|
|
432
|
+
else:
|
|
433
|
+
self.error(f"expected a type, found {self._desc(self.cur)}", self.cur)
|
|
434
|
+
return A.TypeRef(start=start, end=self.cur.end, name="?")
|
|
435
|
+
|
|
436
|
+
while True:
|
|
437
|
+
if self.at_op("??"): # lexer glues '?' '?' into one token
|
|
438
|
+
self.advance()
|
|
439
|
+
tr.optional = True
|
|
440
|
+
tr.opt_nested = True
|
|
441
|
+
continue
|
|
442
|
+
if self.at_op("?"):
|
|
443
|
+
self.advance()
|
|
444
|
+
if tr.optional:
|
|
445
|
+
tr.opt_nested = True
|
|
446
|
+
tr.optional = True
|
|
447
|
+
if self.at_op("?"):
|
|
448
|
+
tr.opt_nested = True
|
|
449
|
+
self.advance()
|
|
450
|
+
continue
|
|
451
|
+
if self.at_op("!"):
|
|
452
|
+
self.advance()
|
|
453
|
+
tr.fallible = True
|
|
454
|
+
if self.cur.kind in ("TYPENAME", "PRIM"):
|
|
455
|
+
tr.err_type = self.parse_type_ref()
|
|
456
|
+
continue
|
|
457
|
+
break
|
|
458
|
+
tr.end = self.cur.end
|
|
459
|
+
return tr
|
|
460
|
+
|
|
461
|
+
# -- params ------------------------------------------------------------
|
|
462
|
+
def _param_list_until(self, closer: str) -> list:
|
|
463
|
+
params = []
|
|
464
|
+
self.skip_newlines() # a param list may span lines inside ( )
|
|
465
|
+
while not self.at_op(closer) and not self.at("EOF"):
|
|
466
|
+
if not self.at("NAME"):
|
|
467
|
+
self.error(f"expected a parameter name, found {self._desc(self.cur)}", self.cur)
|
|
468
|
+
break
|
|
469
|
+
p = A.Param(start=self.cur.start, name=self.advance().value)
|
|
470
|
+
if not self.at_op(",", closer, "="):
|
|
471
|
+
p.type = self.parse_type_ref()
|
|
472
|
+
if self.at_op("="):
|
|
473
|
+
self.advance()
|
|
474
|
+
p.default = self.parse_expr()
|
|
475
|
+
params.append(p)
|
|
476
|
+
self.skip_newlines()
|
|
477
|
+
if self.at_op(","):
|
|
478
|
+
self.advance()
|
|
479
|
+
self.skip_newlines()
|
|
480
|
+
else:
|
|
481
|
+
break
|
|
482
|
+
return params
|
|
483
|
+
|
|
484
|
+
# -- functions -----------------------------------------------------
|
|
485
|
+
def parse_fn_decl(self) -> A.FnDecl:
|
|
486
|
+
start = self.advance().start
|
|
487
|
+
name = self.advance().value if self.at("NAME") else "?"
|
|
488
|
+
fn = A.FnDecl(start=start, name=name)
|
|
489
|
+
self.expect_op("(")
|
|
490
|
+
fn.params = self._param_list_until(")")
|
|
491
|
+
self.expect_op(")")
|
|
492
|
+
if not self._at_line_end():
|
|
493
|
+
fn.ret = self.parse_type_ref()
|
|
494
|
+
fn.body = self.parse_block(self.parse_stmt)
|
|
495
|
+
fn.end = self.cur.end
|
|
496
|
+
return fn
|
|
497
|
+
|
|
498
|
+
# -- views -------------------------------------------------------
|
|
499
|
+
def parse_view_decl(self) -> A.ViewDecl:
|
|
500
|
+
start = self.advance().start
|
|
501
|
+
name = self.advance().value if self.at("TYPENAME") else "?"
|
|
502
|
+
v = A.ViewDecl(start=start, name=name)
|
|
503
|
+
self.expect_op("(")
|
|
504
|
+
v.params = self._param_list_until(")")
|
|
505
|
+
self.expect_op(")")
|
|
506
|
+
v.body = self.parse_block(self.parse_view_stmt)
|
|
507
|
+
v.end = self.cur.end
|
|
508
|
+
return v
|
|
509
|
+
|
|
510
|
+
def parse_view_stmt(self):
|
|
511
|
+
t = self.cur
|
|
512
|
+
# `state` / `load` are contextual: keywords only as the head of a view
|
|
513
|
+
# statement (`state x = ...`), plain identifiers otherwise (`state = x`,
|
|
514
|
+
# `load(id)`).
|
|
515
|
+
if self.at_soft("state") and self.peek(1).kind == "NAME":
|
|
516
|
+
self.advance()
|
|
517
|
+
n = self.advance().value if self.at("NAME") else "?"
|
|
518
|
+
typ = None
|
|
519
|
+
if not self.at_op("="):
|
|
520
|
+
typ = self.parse_type_ref()
|
|
521
|
+
self.expect_op("=")
|
|
522
|
+
init = self.parse_expr()
|
|
523
|
+
self._end_line()
|
|
524
|
+
return A.StateDecl(start=t.start, name=n, type=typ, init=init)
|
|
525
|
+
if self.at_kw("derive"):
|
|
526
|
+
self.advance()
|
|
527
|
+
n = self.advance().value if self.at("NAME") else "?"
|
|
528
|
+
self.expect_op("=")
|
|
529
|
+
e = self.parse_expr()
|
|
530
|
+
self._end_line()
|
|
531
|
+
return A.DeriveDecl(start=t.start, name=n, expr=e)
|
|
532
|
+
if self.at_soft("load") and self.peek(1).kind == "NAME":
|
|
533
|
+
return self.parse_load()
|
|
534
|
+
if self.at_kw("on"):
|
|
535
|
+
return self.parse_on_block()
|
|
536
|
+
if self.at_soft("list") and self._is_list_block():
|
|
537
|
+
return self.parse_list_block()
|
|
538
|
+
if self.at_kw("if"):
|
|
539
|
+
return self.parse_if(self.parse_view_stmt)
|
|
540
|
+
if self.at_kw("match"):
|
|
541
|
+
return self.parse_match_stmt(self.parse_view_stmt)
|
|
542
|
+
if self.cur.kind in ("NAME", "TYPENAME") and not self.at_kw():
|
|
543
|
+
if self.at("NAME") and self.peek(1).kind == "OP" and self.peek(1).value in _ASSIGN_OPS:
|
|
544
|
+
return self.parse_stmt()
|
|
545
|
+
return self.parse_element()
|
|
546
|
+
return self.parse_stmt()
|
|
547
|
+
|
|
548
|
+
def _is_list_block(self) -> bool:
|
|
549
|
+
j = self.pos + 1
|
|
550
|
+
depth = 0
|
|
551
|
+
while j < len(self.toks):
|
|
552
|
+
tk = self.toks[j]
|
|
553
|
+
if tk.kind in ("NEWLINE", "EOF", "INDENT", "DEDENT"):
|
|
554
|
+
return False
|
|
555
|
+
if tk.kind == "OP" and tk.value in "([{":
|
|
556
|
+
depth += 1
|
|
557
|
+
elif tk.kind == "OP" and tk.value in ")]}":
|
|
558
|
+
depth -= 1
|
|
559
|
+
elif depth == 0 and tk.kind == "KW" and tk.value == "as":
|
|
560
|
+
return True
|
|
561
|
+
j += 1
|
|
562
|
+
return False
|
|
563
|
+
|
|
564
|
+
def parse_list_block(self) -> A.ListBlock:
|
|
565
|
+
start = self.advance().start # 'list'
|
|
566
|
+
src = self.parse_expr()
|
|
567
|
+
if self.at_kw("as"):
|
|
568
|
+
self.advance()
|
|
569
|
+
else:
|
|
570
|
+
self.error("expected 'as' in list block", self.cur)
|
|
571
|
+
var = self.advance().value if self.at("NAME") else "?"
|
|
572
|
+
node = A.ListBlock(start=start, src=src, var=var)
|
|
573
|
+
if self.at_soft("key"):
|
|
574
|
+
self.advance()
|
|
575
|
+
if self.at_op(":"):
|
|
576
|
+
self.advance()
|
|
577
|
+
node.key = self.parse_expr()
|
|
578
|
+
node.has_key = True
|
|
579
|
+
else:
|
|
580
|
+
self.error("expected ':' after 'key'", self.cur)
|
|
581
|
+
node.end = self.cur.end
|
|
582
|
+
self.skip_newlines()
|
|
583
|
+
if self.at("INDENT"):
|
|
584
|
+
node.body = self.parse_block(self.parse_view_stmt)
|
|
585
|
+
else:
|
|
586
|
+
self._end_line()
|
|
587
|
+
return node
|
|
588
|
+
|
|
589
|
+
def parse_load(self) -> A.LoadDecl:
|
|
590
|
+
start = self.advance().start
|
|
591
|
+
n = self.advance().value if self.at("NAME") else "?"
|
|
592
|
+
self.expect_op("=")
|
|
593
|
+
e = self.parse_expr()
|
|
594
|
+
node = A.LoadDecl(start=start, name=n, expr=e)
|
|
595
|
+
node.end = self.cur.end
|
|
596
|
+
self.skip_newlines()
|
|
597
|
+
if self.at("INDENT"):
|
|
598
|
+
self.advance()
|
|
599
|
+
while not self.at("DEDENT") and not self.at("EOF"):
|
|
600
|
+
self.skip_newlines()
|
|
601
|
+
if self.at("DEDENT"):
|
|
602
|
+
break
|
|
603
|
+
if self.at_soft("pending"):
|
|
604
|
+
self.advance()
|
|
605
|
+
node.pending = self.parse_block(self.parse_view_stmt)
|
|
606
|
+
elif self.at_soft("error"):
|
|
607
|
+
self.advance()
|
|
608
|
+
if self.at("NAME"):
|
|
609
|
+
node.error_name = self.advance().value
|
|
610
|
+
node.error_body = self.parse_block(self.parse_view_stmt)
|
|
611
|
+
else:
|
|
612
|
+
self.error("expected 'pending' or 'error' in load block", self.cur)
|
|
613
|
+
self._recover_to_line()
|
|
614
|
+
self.skip_newlines()
|
|
615
|
+
if self.at("DEDENT"):
|
|
616
|
+
self.advance()
|
|
617
|
+
else:
|
|
618
|
+
self._end_line()
|
|
619
|
+
return node
|
|
620
|
+
|
|
621
|
+
def parse_on_block(self) -> A.OnBlock:
|
|
622
|
+
start = self.advance().start # 'on'
|
|
623
|
+
ev = self.advance().value if self.cur.kind in ("NAME", "KW") else "?"
|
|
624
|
+
args: list[str] = []
|
|
625
|
+
while self.at("NAME"):
|
|
626
|
+
args.append(self.advance().value)
|
|
627
|
+
if self.at_op(","):
|
|
628
|
+
self.advance()
|
|
629
|
+
else:
|
|
630
|
+
break
|
|
631
|
+
node = A.OnBlock(start=start, event=ev, args=args)
|
|
632
|
+
if self.at_op(":"): # inline form: `on tap: <stmt>`
|
|
633
|
+
self.advance()
|
|
634
|
+
node.body = self._inline_handler_body()
|
|
635
|
+
self._end_line()
|
|
636
|
+
else:
|
|
637
|
+
node.body = self.parse_block(self.parse_stmt)
|
|
638
|
+
node.end = self.cur.end
|
|
639
|
+
return node
|
|
640
|
+
|
|
641
|
+
def _inline_handler_body(self) -> list:
|
|
642
|
+
"""One statement after `on <event>:` — an expression or an assignment
|
|
643
|
+
(`on tap: open = false`)."""
|
|
644
|
+
start = self.cur.start
|
|
645
|
+
expr = self.parse_expr()
|
|
646
|
+
if self.at_op(*_ASSIGN_OPS):
|
|
647
|
+
op = self.advance().value
|
|
648
|
+
return [A.Assign(start=start, target=expr, op=op, value=self.parse_expr())]
|
|
649
|
+
return [A.ExprStmt(start=start, expr=expr)]
|
|
650
|
+
|
|
651
|
+
def parse_element(self) -> A.Element:
|
|
652
|
+
start = self.cur.start
|
|
653
|
+
name = self.advance().value
|
|
654
|
+
el = A.Element(start=start, name=name)
|
|
655
|
+
|
|
656
|
+
def is_attr() -> bool:
|
|
657
|
+
# `name:` where name is an identifier or a keyword like `type` / `on`
|
|
658
|
+
return (self.cur.kind in ("NAME", "KW")
|
|
659
|
+
and not self.at_kw("on", "if", "match")
|
|
660
|
+
and self.peek(1).kind == "OP" and self.peek(1).value == ":")
|
|
661
|
+
|
|
662
|
+
if self.at_op("("):
|
|
663
|
+
self.advance()
|
|
664
|
+
self.skip_newlines() # attrs may open onto their own lines
|
|
665
|
+
while not self.at_op(")") and not self.at("EOF"):
|
|
666
|
+
if is_attr():
|
|
667
|
+
an = self.advance().value
|
|
668
|
+
self.advance()
|
|
669
|
+
el.attrs.append((an, self.parse_expr()))
|
|
670
|
+
else:
|
|
671
|
+
el.args.append(self.parse_expr())
|
|
672
|
+
# spec 3.7: a bare newline also separates items inside an open
|
|
673
|
+
# bracket, matching `_parse_postfix`'s own call-argument loop
|
|
674
|
+
# -- needed for a component with more than one callback prop
|
|
675
|
+
# (DESIGN-01), each written as its own multi-line closure, with
|
|
676
|
+
# no comma between them. `_take_closure_ate_sep` reads (and
|
|
677
|
+
# resets) the one-shot flag `_parse_bracketed_closure_body`
|
|
678
|
+
# sets when a closure attr's own last statement already
|
|
679
|
+
# consumed the newline that would otherwise separate it here.
|
|
680
|
+
had_nl = self.at("NEWLINE") or self._take_closure_ate_sep()
|
|
681
|
+
self.skip_newlines()
|
|
682
|
+
if self.at_op(","):
|
|
683
|
+
self.advance()
|
|
684
|
+
self.skip_newlines()
|
|
685
|
+
elif not had_nl and not self.at_op(")"):
|
|
686
|
+
break
|
|
687
|
+
self.expect_op(")")
|
|
688
|
+
|
|
689
|
+
while not self._at_line_end():
|
|
690
|
+
if is_attr():
|
|
691
|
+
an = self.advance().value
|
|
692
|
+
self.advance()
|
|
693
|
+
el.attrs.append((an, self.parse_expr()))
|
|
694
|
+
continue
|
|
695
|
+
if self.at_kw("on"):
|
|
696
|
+
self.advance()
|
|
697
|
+
ev = self.advance().value if self.cur.kind in ("NAME", "KW") else "?"
|
|
698
|
+
on_args: list[str] = []
|
|
699
|
+
while self.at("NAME"):
|
|
700
|
+
on_args.append(self.advance().value)
|
|
701
|
+
if self.at_op(","):
|
|
702
|
+
self.advance()
|
|
703
|
+
else:
|
|
704
|
+
break
|
|
705
|
+
if self.at_op(":"): # `... on tap: <stmt>`
|
|
706
|
+
self.advance()
|
|
707
|
+
el.body.append(A.OnBlock(start=start, event=ev, args=on_args,
|
|
708
|
+
body=self._inline_handler_body()))
|
|
709
|
+
continue
|
|
710
|
+
# `... on tap` then an indented block — terminates the element line
|
|
711
|
+
ob = A.OnBlock(start=start, event=ev, args=on_args)
|
|
712
|
+
if self.at("NEWLINE"):
|
|
713
|
+
self.advance()
|
|
714
|
+
ob.body = self.parse_block(self.parse_stmt) if self.at("INDENT") else []
|
|
715
|
+
el.body.append(ob)
|
|
716
|
+
el.end = self.cur.end
|
|
717
|
+
return el
|
|
718
|
+
if self.at_kw("if", "match"):
|
|
719
|
+
break
|
|
720
|
+
before = self.pos
|
|
721
|
+
el.args.append(self.parse_expr())
|
|
722
|
+
if self.pos == before:
|
|
723
|
+
self.advance()
|
|
724
|
+
break
|
|
725
|
+
|
|
726
|
+
el.end = self.cur.end
|
|
727
|
+
if self.at("NEWLINE"):
|
|
728
|
+
self.advance()
|
|
729
|
+
if self.at("INDENT"):
|
|
730
|
+
el.body = list(el.body) + self.parse_block(self.parse_view_stmt)
|
|
731
|
+
return el
|
|
732
|
+
|
|
733
|
+
# -- api / service / auth / theme ----------------------------------------
|
|
734
|
+
def parse_api_decl(self) -> A.ApiDecl:
|
|
735
|
+
start = self.advance().start
|
|
736
|
+
name = self.advance().value if self.cur.kind in ("NAME", "KW") else "?"
|
|
737
|
+
node = A.ApiDecl(start=start, name=name)
|
|
738
|
+
if self.at_soft("from"):
|
|
739
|
+
self.advance()
|
|
740
|
+
node.from_expr = self.parse_expr()
|
|
741
|
+
|
|
742
|
+
def item():
|
|
743
|
+
if self.at("NAME") and self.cur.value in _METHODS:
|
|
744
|
+
return self.parse_endpoint()
|
|
745
|
+
return self.parse_directive()
|
|
746
|
+
|
|
747
|
+
for r in self.parse_block(item):
|
|
748
|
+
if isinstance(r, A.Endpoint):
|
|
749
|
+
node.endpoints.append(r)
|
|
750
|
+
elif r is not None:
|
|
751
|
+
node.settings.append(r)
|
|
752
|
+
node.end = self.cur.end
|
|
753
|
+
return node
|
|
754
|
+
|
|
755
|
+
def parse_endpoint(self) -> A.Endpoint:
|
|
756
|
+
start = self.cur.start
|
|
757
|
+
method = self.advance().value
|
|
758
|
+
name = self.advance().value if self.at("NAME") else "?"
|
|
759
|
+
ep = A.Endpoint(start=start, method=method, name=name)
|
|
760
|
+
self.expect_op("(")
|
|
761
|
+
ep.params = self._param_list_until(")")
|
|
762
|
+
self.expect_op(")")
|
|
763
|
+
if not self._at_line_end():
|
|
764
|
+
ep.ret = self.parse_type_ref()
|
|
765
|
+
self.skip_newlines()
|
|
766
|
+
if self.at("INDENT"):
|
|
767
|
+
ep.settings = self.parse_block(self.parse_directive)
|
|
768
|
+
else:
|
|
769
|
+
self._end_line()
|
|
770
|
+
ep.end = self.cur.end
|
|
771
|
+
return ep
|
|
772
|
+
|
|
773
|
+
def parse_service_decl(self) -> A.ServiceDecl:
|
|
774
|
+
start = self.advance().start
|
|
775
|
+
name = self.advance().value if self.cur.kind in ("NAME", "KW") else "?"
|
|
776
|
+
node = A.ServiceDecl(start=start, name=name)
|
|
777
|
+
if self.at_soft("public"):
|
|
778
|
+
self.advance()
|
|
779
|
+
node.is_public = True
|
|
780
|
+
|
|
781
|
+
def item():
|
|
782
|
+
if self.at("NAME") and self.cur.value in _METHODS:
|
|
783
|
+
return self.parse_route()
|
|
784
|
+
return self.parse_directive()
|
|
785
|
+
|
|
786
|
+
for r in self.parse_block(item):
|
|
787
|
+
if isinstance(r, A.Route):
|
|
788
|
+
node.routes.append(r)
|
|
789
|
+
elif r is not None:
|
|
790
|
+
node.settings.append(r)
|
|
791
|
+
node.end = self.cur.end
|
|
792
|
+
return node
|
|
793
|
+
|
|
794
|
+
def parse_route(self) -> A.Route:
|
|
795
|
+
start = self.cur.start
|
|
796
|
+
method = self.advance().value
|
|
797
|
+
parts: list[str] = []
|
|
798
|
+
while self.at_op("/"):
|
|
799
|
+
parts.append(self.advance().value) # '/'
|
|
800
|
+
if self.at_op(":"):
|
|
801
|
+
parts.append(self.advance().value) # ':'
|
|
802
|
+
if self.at("NAME"):
|
|
803
|
+
parts.append(self.advance().value) # segment name
|
|
804
|
+
rt = A.Route(start=start, method=method, path="".join(parts))
|
|
805
|
+
if self.at_op("("):
|
|
806
|
+
self.advance()
|
|
807
|
+
rt.params = self._param_list_until(")")
|
|
808
|
+
self.expect_op(")")
|
|
809
|
+
if not self._at_line_end() and not self.at_kw("as"):
|
|
810
|
+
rt.ret = self.parse_type_ref()
|
|
811
|
+
if self.at_kw("as"):
|
|
812
|
+
self.advance()
|
|
813
|
+
if self.cur.kind in ("NAME", "TYPENAME"):
|
|
814
|
+
rt.alias = self.advance().value
|
|
815
|
+
# end the header line, then look for an optional indented body. A bodyless
|
|
816
|
+
# route (`get /posts/:id Post!`) is valid and may be followed by a sibling.
|
|
817
|
+
if self.at("NEWLINE"):
|
|
818
|
+
self.advance()
|
|
819
|
+
elif not self.at("INDENT") and self.cur.kind not in ("DEDENT", "EOF"):
|
|
820
|
+
self.error(f"expected end of line, found {self._desc(self.cur)}", self.cur)
|
|
821
|
+
self._recover_to_line()
|
|
822
|
+
self.skip_newlines()
|
|
823
|
+
if self.at("INDENT"):
|
|
824
|
+
self.advance()
|
|
825
|
+
body = []
|
|
826
|
+
while not self.at("DEDENT") and not self.at("EOF"):
|
|
827
|
+
self.skip_newlines()
|
|
828
|
+
if self.at("DEDENT"):
|
|
829
|
+
break
|
|
830
|
+
if self.at_kw("auth"):
|
|
831
|
+
rt.guard = self.parse_guard()
|
|
832
|
+
else:
|
|
833
|
+
before = self.pos
|
|
834
|
+
body.append(self.parse_stmt())
|
|
835
|
+
if self.pos == before:
|
|
836
|
+
self._recover_to_line()
|
|
837
|
+
self.skip_newlines()
|
|
838
|
+
if self.at("DEDENT"):
|
|
839
|
+
self.advance()
|
|
840
|
+
rt.body = body
|
|
841
|
+
rt.end = self.cur.end
|
|
842
|
+
return rt
|
|
843
|
+
|
|
844
|
+
def parse_guard(self) -> A.Guard:
|
|
845
|
+
start = self.advance().start # 'auth'
|
|
846
|
+
g = A.Guard(start=start)
|
|
847
|
+
# The natural spelling for the open §20.8 gap is otherwise especially
|
|
848
|
+
# unhelpful: `auth role:admin or owner:...` falls through into an
|
|
849
|
+
# ordinary end-of-line parse error. Recognise the attempted composition
|
|
850
|
+
# before parsing either side and turn it into a repair instruction.
|
|
851
|
+
line = []
|
|
852
|
+
i = self.pos
|
|
853
|
+
while i < len(self.toks) and self.toks[i].kind not in ("NEWLINE", "DEDENT", "EOF"):
|
|
854
|
+
line.append(self.toks[i])
|
|
855
|
+
i += 1
|
|
856
|
+
joiner = next((t for t in line if t.kind == "KW" and t.value in ("or", "and")), None)
|
|
857
|
+
if joiner is not None and any(t.value in ("role", "owner") for t in line):
|
|
858
|
+
self.error(
|
|
859
|
+
"authorization guards do not compose in Volaro v0.2 (§20.8)",
|
|
860
|
+
joiner,
|
|
861
|
+
code="E-AUTH-COMPOSE",
|
|
862
|
+
fix=("use `auth required`, perform the combined check in the handler, "
|
|
863
|
+
"and mark it `# LANGUAGE GAP (§20.8)`; do not present the "
|
|
864
|
+
"workaround as idiomatic Volaro"),
|
|
865
|
+
)
|
|
866
|
+
g.kind = "compose"
|
|
867
|
+
self._recover_to_line()
|
|
868
|
+
g.end = self.cur.end
|
|
869
|
+
return g
|
|
870
|
+
if self.at("NAME") and self.cur.value in ("required", "optional", "none"):
|
|
871
|
+
g.kind = self.advance().value
|
|
872
|
+
elif self.at_soft("role"):
|
|
873
|
+
self.advance()
|
|
874
|
+
g.kind = "role"
|
|
875
|
+
if self.at_op(":"):
|
|
876
|
+
self.advance()
|
|
877
|
+
g.detail = self._role_list()
|
|
878
|
+
elif self.at_soft("owner"):
|
|
879
|
+
self.advance()
|
|
880
|
+
g.kind = "owner"
|
|
881
|
+
if self.at_op(":"):
|
|
882
|
+
self.advance()
|
|
883
|
+
g.detail = self.parse_expr()
|
|
884
|
+
else:
|
|
885
|
+
g.kind = self.advance().value if self.cur.kind in ("NAME", "KW") else "?"
|
|
886
|
+
g.end = self.cur.end
|
|
887
|
+
self._end_line()
|
|
888
|
+
return g
|
|
889
|
+
|
|
890
|
+
def _role_list(self) -> list:
|
|
891
|
+
roles: list[str] = []
|
|
892
|
+
if self.at_op("["):
|
|
893
|
+
self.advance()
|
|
894
|
+
while not self.at_op("]") and not self.at("EOF"):
|
|
895
|
+
if self.cur.kind in ("NAME", "KW"):
|
|
896
|
+
roles.append(self.advance().value)
|
|
897
|
+
if self.at_op(","):
|
|
898
|
+
self.advance()
|
|
899
|
+
else:
|
|
900
|
+
break
|
|
901
|
+
self.expect_op("]")
|
|
902
|
+
elif self.cur.kind in ("NAME", "KW"):
|
|
903
|
+
roles.append(self.advance().value)
|
|
904
|
+
return roles
|
|
905
|
+
|
|
906
|
+
def parse_auth_decl(self) -> A.AuthDecl:
|
|
907
|
+
start = self.advance().start
|
|
908
|
+
node = A.AuthDecl(start=start)
|
|
909
|
+
node.settings = self.parse_block(self.parse_directive)
|
|
910
|
+
node.end = self.cur.end
|
|
911
|
+
return node
|
|
912
|
+
|
|
913
|
+
def parse_theme_decl(self) -> A.Directive:
|
|
914
|
+
start = self.advance().start
|
|
915
|
+
node = A.Directive(start=start, head="theme")
|
|
916
|
+
node.body = self.parse_block(self.parse_directive)
|
|
917
|
+
node.end = self.cur.end
|
|
918
|
+
return node
|
|
919
|
+
|
|
920
|
+
def parse_recipe_decl(self) -> A.RecipeDecl:
|
|
921
|
+
start = self.advance().start
|
|
922
|
+
primitive = (self.advance().value
|
|
923
|
+
if self.cur.kind in ("NAME", "KW") else "?")
|
|
924
|
+
node = A.RecipeDecl(start=start, primitive=primitive)
|
|
925
|
+
node.body = self.parse_block(self.parse_directive)
|
|
926
|
+
node.end = self.cur.end
|
|
927
|
+
return node
|
|
928
|
+
|
|
929
|
+
def parse_page_decl(self) -> A.PageDecl:
|
|
930
|
+
start = self.advance().start # 'page'
|
|
931
|
+
node = A.PageDecl(start=start)
|
|
932
|
+
# inline `name:<expr>` attributes on the same line, element-style
|
|
933
|
+
while not self._at_line_end():
|
|
934
|
+
if (self.cur.kind in ("NAME", "KW") and self.peek(1).kind == "OP"
|
|
935
|
+
and self.peek(1).value == ":"):
|
|
936
|
+
an = self.advance().value
|
|
937
|
+
self.advance() # ':'
|
|
938
|
+
node.attrs.append((an, self.parse_expr()))
|
|
939
|
+
continue
|
|
940
|
+
self.error("`page` takes `name:<expr>` settings (e.g. `page "
|
|
941
|
+
"title:\"…\"`)", self.cur)
|
|
942
|
+
self._recover_to_line()
|
|
943
|
+
break
|
|
944
|
+
node.end = self.cur.end
|
|
945
|
+
if self.at("NEWLINE"):
|
|
946
|
+
self.advance()
|
|
947
|
+
return node
|
|
948
|
+
|
|
949
|
+
def parse_directive(self):
|
|
950
|
+
if self.cur.kind in ("DEDENT", "EOF"):
|
|
951
|
+
return None
|
|
952
|
+
start = self.cur.start
|
|
953
|
+
head = self.cur.value if self.cur.kind in ("NAME", "KW", "TYPENAME") else self._desc(self.cur)
|
|
954
|
+
toks: list[str] = []
|
|
955
|
+
while not self.at("NEWLINE") and self.cur.kind not in ("DEDENT", "EOF"):
|
|
956
|
+
toks.append(self.cur.value)
|
|
957
|
+
self.advance()
|
|
958
|
+
node = A.Directive(start=start, end=self.cur.end, head=head, tokens=toks)
|
|
959
|
+
self.skip_newlines()
|
|
960
|
+
if self.at("INDENT"):
|
|
961
|
+
node.body = self.parse_block(self.parse_directive)
|
|
962
|
+
return node
|
|
963
|
+
|
|
964
|
+
# -- statements ----------------------------------------------------
|
|
965
|
+
def parse_stmt(self):
|
|
966
|
+
t = self.cur
|
|
967
|
+
if self.at_kw("let"):
|
|
968
|
+
return self.parse_let()
|
|
969
|
+
if self.at_kw("if"):
|
|
970
|
+
return self.parse_if(self.parse_stmt)
|
|
971
|
+
if self.at_kw("match"):
|
|
972
|
+
return self.parse_match_stmt(self.parse_stmt)
|
|
973
|
+
if self.at_kw("for"):
|
|
974
|
+
return self.parse_for()
|
|
975
|
+
if self.at_kw("while"):
|
|
976
|
+
return self.parse_while()
|
|
977
|
+
if self.at_kw("par"):
|
|
978
|
+
return self.parse_par()
|
|
979
|
+
if self.at_kw("spawn"):
|
|
980
|
+
return self.parse_spawn()
|
|
981
|
+
if self.at_kw("try"):
|
|
982
|
+
return self.parse_try()
|
|
983
|
+
if self.at_kw("return"):
|
|
984
|
+
self.advance()
|
|
985
|
+
val = None if self._at_line_end() else self.parse_expr()
|
|
986
|
+
self._end_line()
|
|
987
|
+
return A.Return(start=t.start, value=val)
|
|
988
|
+
if self.at_kw("break"):
|
|
989
|
+
self.advance()
|
|
990
|
+
self._end_line()
|
|
991
|
+
return A.Break(start=t.start)
|
|
992
|
+
if self.at_kw("skip"):
|
|
993
|
+
self.advance()
|
|
994
|
+
self._end_line()
|
|
995
|
+
return A.Skip(start=t.start)
|
|
996
|
+
|
|
997
|
+
expr = self.parse_expr()
|
|
998
|
+
if self.at_op(*_ASSIGN_OPS):
|
|
999
|
+
op = self.advance().value
|
|
1000
|
+
val = self.parse_expr()
|
|
1001
|
+
if not self._closure_block_body(val):
|
|
1002
|
+
self._end_line()
|
|
1003
|
+
return A.Assign(start=t.start, target=expr, op=op, value=val)
|
|
1004
|
+
if not self._closure_block_body(expr):
|
|
1005
|
+
self._end_line()
|
|
1006
|
+
return A.ExprStmt(start=t.start, expr=expr)
|
|
1007
|
+
|
|
1008
|
+
@staticmethod
|
|
1009
|
+
def _closure_block_body(node) -> bool:
|
|
1010
|
+
"""True when `node` is a closure whose body is a statement block that
|
|
1011
|
+
has already been fully parsed through its own terminator (a DEDENT
|
|
1012
|
+
via `parse_block`, or the bracket-tracked end of
|
|
1013
|
+
`_parse_bracketed_closure_body`) — so the caller must NOT also call
|
|
1014
|
+
`_end_line()` for it: there is no pending NEWLINE/DEDENT left to
|
|
1015
|
+
consume, only the next statement (B3 in the callback-props
|
|
1016
|
+
decision — `let x = fn(...)\\n <block>` followed by a sibling
|
|
1017
|
+
statement broke here before this check existed)."""
|
|
1018
|
+
return isinstance(node, A.Closure) and isinstance(node.body, list)
|
|
1019
|
+
|
|
1020
|
+
def parse_let(self) -> A.Let:
|
|
1021
|
+
start = self.advance().start
|
|
1022
|
+
mut = False
|
|
1023
|
+
if self.at_kw("mut"):
|
|
1024
|
+
self.advance()
|
|
1025
|
+
mut = True
|
|
1026
|
+
name = self.advance().value if self.at("NAME") else "?"
|
|
1027
|
+
node = A.Let(start=start, name=name, mut=mut)
|
|
1028
|
+
if not self.at_op("="):
|
|
1029
|
+
node.type = self.parse_type_ref()
|
|
1030
|
+
self.expect_op("=")
|
|
1031
|
+
# `let x = try EXPR \n catch pat block(stmt)` (spec section 6.2,
|
|
1032
|
+
# expression position) -- `try` is not an ordinary expression prefix
|
|
1033
|
+
# (parse_expr doesn't know it), so it's special-cased here the same
|
|
1034
|
+
# way parse_stmt special-cases the statement form. Reuses parse_try
|
|
1035
|
+
# verbatim: the AST shape (A.Try: expr + catches) is identical either
|
|
1036
|
+
# way: emit.py tells them apart by where the A.Try node ends up.
|
|
1037
|
+
if self.at_kw("try"):
|
|
1038
|
+
node.init = self.parse_try()
|
|
1039
|
+
node.end = self.cur.end
|
|
1040
|
+
return node
|
|
1041
|
+
node.init = self.parse_expr()
|
|
1042
|
+
if (node.type is None and isinstance(node.init, (A.ListLit, A.RecordLit))
|
|
1043
|
+
and getattr(node.init, "empty", False)):
|
|
1044
|
+
node.empty_literal_no_type = True
|
|
1045
|
+
if self._closure_block_body(node.init):
|
|
1046
|
+
node.end = node.init.end
|
|
1047
|
+
return node
|
|
1048
|
+
self._end_line()
|
|
1049
|
+
node.end = self.cur.end
|
|
1050
|
+
return node
|
|
1051
|
+
|
|
1052
|
+
def parse_if(self, sub) -> A.If:
|
|
1053
|
+
start = self.advance().start
|
|
1054
|
+
cond = self.parse_expr()
|
|
1055
|
+
node = A.If(start=start, cond=cond)
|
|
1056
|
+
node.then = self.parse_block(sub)
|
|
1057
|
+
while (self.at_kw("else") and self.peek(1).kind == "KW" and self.peek(1).value == "if"):
|
|
1058
|
+
self.advance()
|
|
1059
|
+
self.advance()
|
|
1060
|
+
c = self.parse_expr()
|
|
1061
|
+
node.elifs.append((c, self.parse_block(sub)))
|
|
1062
|
+
if self.at_kw("else"):
|
|
1063
|
+
self.advance()
|
|
1064
|
+
node.has_else = True
|
|
1065
|
+
node.orelse = self.parse_block(sub)
|
|
1066
|
+
node.end = self.cur.end
|
|
1067
|
+
return node
|
|
1068
|
+
|
|
1069
|
+
def parse_match_stmt(self, sub) -> A.Match:
|
|
1070
|
+
start = self.advance().start
|
|
1071
|
+
node = A.Match(start=start, subject=self.parse_expr())
|
|
1072
|
+
self.skip_newlines()
|
|
1073
|
+
if not self.at("INDENT"):
|
|
1074
|
+
self.error("expected indented match arms", self.cur)
|
|
1075
|
+
return node
|
|
1076
|
+
self.advance()
|
|
1077
|
+
while not self.at("DEDENT") and not self.at("EOF"):
|
|
1078
|
+
self.skip_newlines()
|
|
1079
|
+
if self.at("DEDENT"):
|
|
1080
|
+
break
|
|
1081
|
+
arm = self._match_arm(sub)
|
|
1082
|
+
if arm is not None:
|
|
1083
|
+
node.arms.append(arm)
|
|
1084
|
+
self.skip_newlines()
|
|
1085
|
+
if self.at("DEDENT"):
|
|
1086
|
+
self.advance()
|
|
1087
|
+
node.end = self.cur.end
|
|
1088
|
+
return node
|
|
1089
|
+
|
|
1090
|
+
def _match_arm(self, sub):
|
|
1091
|
+
before = self.pos
|
|
1092
|
+
pat = self.parse_pattern()
|
|
1093
|
+
guard = None
|
|
1094
|
+
if self.at_kw("if"):
|
|
1095
|
+
self.advance()
|
|
1096
|
+
guard = self.parse_expr()
|
|
1097
|
+
if self.at_op("->"):
|
|
1098
|
+
self.advance()
|
|
1099
|
+
else:
|
|
1100
|
+
self.error(f"expected '->' in match arm, found {self._desc(self.cur)}", self.cur)
|
|
1101
|
+
if self.at("NEWLINE"):
|
|
1102
|
+
body = self.parse_block(sub)
|
|
1103
|
+
else:
|
|
1104
|
+
body = [A.ExprStmt(expr=self.parse_expr())]
|
|
1105
|
+
self._end_line()
|
|
1106
|
+
if self.pos == before:
|
|
1107
|
+
self.advance()
|
|
1108
|
+
return (pat, guard, body)
|
|
1109
|
+
|
|
1110
|
+
def parse_for(self) -> A.For:
|
|
1111
|
+
start = self.advance().start
|
|
1112
|
+
node = A.For(start=start)
|
|
1113
|
+
if self.at("NAME"):
|
|
1114
|
+
node.vars.append(self.advance().value)
|
|
1115
|
+
if self.at_op(","):
|
|
1116
|
+
self.advance()
|
|
1117
|
+
if self.at("NAME"):
|
|
1118
|
+
node.vars.append(self.advance().value)
|
|
1119
|
+
if self.at_kw("in"):
|
|
1120
|
+
self.advance()
|
|
1121
|
+
else:
|
|
1122
|
+
self.error("expected 'in' in for loop", self.cur)
|
|
1123
|
+
node.iter = self.parse_expr()
|
|
1124
|
+
node.body = self.parse_block(self.parse_stmt)
|
|
1125
|
+
node.end = self.cur.end
|
|
1126
|
+
return node
|
|
1127
|
+
|
|
1128
|
+
def parse_while(self) -> A.While:
|
|
1129
|
+
start = self.advance().start
|
|
1130
|
+
node = A.While(start=start, cond=self.parse_expr())
|
|
1131
|
+
node.body = self.parse_block(self.parse_stmt)
|
|
1132
|
+
node.end = self.cur.end
|
|
1133
|
+
return node
|
|
1134
|
+
|
|
1135
|
+
def parse_par(self) -> A.Par:
|
|
1136
|
+
start = self.advance().start
|
|
1137
|
+
node = A.Par(start=start)
|
|
1138
|
+
node.lets = self.parse_block(self.parse_stmt)
|
|
1139
|
+
node.end = self.cur.end
|
|
1140
|
+
return node
|
|
1141
|
+
|
|
1142
|
+
def parse_spawn(self) -> A.Spawn:
|
|
1143
|
+
start = self.advance().start
|
|
1144
|
+
node = A.Spawn(start=start)
|
|
1145
|
+
node.body = self.parse_block(self.parse_stmt)
|
|
1146
|
+
node.end = self.cur.end
|
|
1147
|
+
return node
|
|
1148
|
+
|
|
1149
|
+
def parse_try(self) -> A.Try:
|
|
1150
|
+
start = self.advance().start # 'try'
|
|
1151
|
+
if self._at_line_end():
|
|
1152
|
+
# `try` with nothing else on its line -- e.g. `try` followed by
|
|
1153
|
+
# an indented block of statements, treating it as a general
|
|
1154
|
+
# block the way `try { ... }` works in other languages. Volaro's
|
|
1155
|
+
# `try` wraps exactly one fallible EXPRESSION on its own line;
|
|
1156
|
+
# it is not a block construct. Name the actual mistake instead
|
|
1157
|
+
# of letting the indented block that follows cascade into
|
|
1158
|
+
# unrelated "expected end of line" / "unexpected dedent" noise.
|
|
1159
|
+
self.error(
|
|
1160
|
+
"`try` needs its fallible expression on the SAME line -- "
|
|
1161
|
+
"`try EXPR`, with `catch` starting its own dedented line "
|
|
1162
|
+
"after it. `try` is not a block and cannot wrap multiple "
|
|
1163
|
+
"statements the way `try { ... }` does in other languages",
|
|
1164
|
+
self.cur, fix="try some_call(...)\ncatch e\n fallback_value")
|
|
1165
|
+
self._skip_block_for_recovery()
|
|
1166
|
+
node = A.Try(start=start,
|
|
1167
|
+
expr=A.Literal(start=start, end=self.cur.end, kind="nil", raw="nil"))
|
|
1168
|
+
node.end = self.cur.end
|
|
1169
|
+
return node
|
|
1170
|
+
node = A.Try(start=start, expr=self.parse_expr())
|
|
1171
|
+
if self.at_kw("catch"):
|
|
1172
|
+
# `catch` right after the expression, on the SAME line -- e.g.
|
|
1173
|
+
# `let r = try f() catch e fallback`. `catch` must start its own
|
|
1174
|
+
# line, dedented to align with `try` (or the `let` it's part
|
|
1175
|
+
# of). Named directly instead of the generic "expected end of
|
|
1176
|
+
# line" this would otherwise produce.
|
|
1177
|
+
self.error(
|
|
1178
|
+
"`catch` must start its own line, dedented to align with "
|
|
1179
|
+
"`try` -- `try EXPR catch ...` all on one line is not valid",
|
|
1180
|
+
self.cur, fix="try f()\ncatch e\n fallback_value")
|
|
1181
|
+
self._recover_to_line()
|
|
1182
|
+
else:
|
|
1183
|
+
self._end_line()
|
|
1184
|
+
self.skip_newlines()
|
|
1185
|
+
while self.at_kw("catch"):
|
|
1186
|
+
self.advance()
|
|
1187
|
+
pat = self.parse_pattern()
|
|
1188
|
+
body = self.parse_block(self.parse_stmt)
|
|
1189
|
+
node.catches.append((pat, body))
|
|
1190
|
+
self.skip_newlines()
|
|
1191
|
+
node.end = self.cur.end
|
|
1192
|
+
return node
|
|
1193
|
+
|
|
1194
|
+
def _skip_block_for_recovery(self) -> None:
|
|
1195
|
+
"""Best-effort error recovery: consume an entire INDENT...DEDENT
|
|
1196
|
+
block without building AST for it, so one malformed `try` (used as
|
|
1197
|
+
a block) doesn't cascade into a wall of unrelated follow-on parse
|
|
1198
|
+
errors on every statement inside it."""
|
|
1199
|
+
self.skip_newlines()
|
|
1200
|
+
if not self.at("INDENT"):
|
|
1201
|
+
return
|
|
1202
|
+
depth = 0
|
|
1203
|
+
while self.cur.kind != "EOF":
|
|
1204
|
+
if self.at("INDENT"):
|
|
1205
|
+
depth += 1
|
|
1206
|
+
elif self.at("DEDENT"):
|
|
1207
|
+
depth -= 1
|
|
1208
|
+
if depth == 0:
|
|
1209
|
+
self.advance()
|
|
1210
|
+
return
|
|
1211
|
+
self.advance()
|
|
1212
|
+
|
|
1213
|
+
def parse_pattern(self) -> A.Pattern:
|
|
1214
|
+
start = self.cur.start
|
|
1215
|
+
binds: list[str] = []
|
|
1216
|
+
head = ""
|
|
1217
|
+
|
|
1218
|
+
def ctor_args():
|
|
1219
|
+
self.advance() # (
|
|
1220
|
+
while not self.at_op(")") and not self.at("EOF"):
|
|
1221
|
+
if self.at("NAME"):
|
|
1222
|
+
binds.append(self.advance().value)
|
|
1223
|
+
else:
|
|
1224
|
+
self.advance()
|
|
1225
|
+
if self.at_op(","):
|
|
1226
|
+
self.advance()
|
|
1227
|
+
else:
|
|
1228
|
+
break
|
|
1229
|
+
self.expect_op(")")
|
|
1230
|
+
|
|
1231
|
+
if self.at("NAME") and self.cur.value == "_":
|
|
1232
|
+
self.advance()
|
|
1233
|
+
head = "_"
|
|
1234
|
+
elif self.at_op("_"):
|
|
1235
|
+
self.advance()
|
|
1236
|
+
head = "_"
|
|
1237
|
+
elif self.at("NAME"):
|
|
1238
|
+
nm = self.advance().value
|
|
1239
|
+
head = nm
|
|
1240
|
+
if self.at_op("("):
|
|
1241
|
+
ctor_args()
|
|
1242
|
+
else:
|
|
1243
|
+
binds.append(nm)
|
|
1244
|
+
elif self.at("TYPENAME"):
|
|
1245
|
+
head = self.cur.value
|
|
1246
|
+
self.advance()
|
|
1247
|
+
if self.at_op("("):
|
|
1248
|
+
ctor_args()
|
|
1249
|
+
elif self.at_op("{"):
|
|
1250
|
+
self.advance()
|
|
1251
|
+
while not self.at_op("}") and not self.at("EOF"):
|
|
1252
|
+
if self.at("NAME"):
|
|
1253
|
+
fn = self.advance().value
|
|
1254
|
+
if self.at_op(":"):
|
|
1255
|
+
self.advance()
|
|
1256
|
+
if self.at("NAME"):
|
|
1257
|
+
binds.append(self.advance().value)
|
|
1258
|
+
else:
|
|
1259
|
+
self.parse_expr()
|
|
1260
|
+
else:
|
|
1261
|
+
binds.append(fn)
|
|
1262
|
+
if self.at_op(","):
|
|
1263
|
+
self.advance()
|
|
1264
|
+
else:
|
|
1265
|
+
break
|
|
1266
|
+
self.expect_op("}")
|
|
1267
|
+
elif self.cur.kind in ("INT", "FLOAT", "STRING", "BOOL", "NIL", "DURATION"):
|
|
1268
|
+
self.advance()
|
|
1269
|
+
if self.at_op("..", "..="):
|
|
1270
|
+
self.advance()
|
|
1271
|
+
if self.cur.kind in ("INT", "FLOAT", "STRING", "DURATION"):
|
|
1272
|
+
self.advance()
|
|
1273
|
+
elif self.at_op("["):
|
|
1274
|
+
self.advance()
|
|
1275
|
+
while not self.at_op("]") and not self.at("EOF"):
|
|
1276
|
+
if self.at("NAME"):
|
|
1277
|
+
binds.append(self.advance().value)
|
|
1278
|
+
else:
|
|
1279
|
+
self.advance()
|
|
1280
|
+
if self.at_op(","):
|
|
1281
|
+
self.advance()
|
|
1282
|
+
else:
|
|
1283
|
+
break
|
|
1284
|
+
self.expect_op("]")
|
|
1285
|
+
else:
|
|
1286
|
+
self.advance()
|
|
1287
|
+
|
|
1288
|
+
return A.Pattern(start=start, end=self.cur.end, binds=binds, head=head)
|
|
1289
|
+
|
|
1290
|
+
# -- expressions (Pratt) ---------------------------------------------
|
|
1291
|
+
def parse_expr(self, min_bp: int = 0):
|
|
1292
|
+
left = self._parse_prefix()
|
|
1293
|
+
while True:
|
|
1294
|
+
op = self._peek_binop()
|
|
1295
|
+
if op is None:
|
|
1296
|
+
break
|
|
1297
|
+
text, lbp, rbp = op
|
|
1298
|
+
if lbp < min_bp:
|
|
1299
|
+
break
|
|
1300
|
+
self.advance()
|
|
1301
|
+
if text in ("..", "..="):
|
|
1302
|
+
right = self.parse_expr(rbp)
|
|
1303
|
+
left = A.Range(start=getattr(left, "start", 0), end=getattr(right, "end", 0),
|
|
1304
|
+
lo=left, hi=right, inclusive=(text == "..="))
|
|
1305
|
+
continue
|
|
1306
|
+
right = self.parse_expr(rbp)
|
|
1307
|
+
node = A.Binary(start=getattr(left, "start", 0), end=getattr(right, "end", 0),
|
|
1308
|
+
op=text, left=left, right=right)
|
|
1309
|
+
if text in _CMP:
|
|
1310
|
+
nxt = self._peek_binop()
|
|
1311
|
+
inner = right.op if isinstance(right, A.Binary) else None
|
|
1312
|
+
if (nxt and nxt[0] in _CMP) or (inner in _CMP):
|
|
1313
|
+
self.diags.append(Diagnostic(
|
|
1314
|
+
"E-CHAIN-CMP", Severity.ERROR,
|
|
1315
|
+
"comparison operators do not chain",
|
|
1316
|
+
node.start, node.end,
|
|
1317
|
+
"split into two comparisons joined by 'and'", self.filename))
|
|
1318
|
+
left = node
|
|
1319
|
+
return left
|
|
1320
|
+
|
|
1321
|
+
def _peek_binop(self):
|
|
1322
|
+
t = self.cur
|
|
1323
|
+
if t.kind == "OP" and t.value in _BINOPS:
|
|
1324
|
+
lbp, rbp = _BINOPS[t.value]
|
|
1325
|
+
return (t.value, lbp, rbp)
|
|
1326
|
+
if t.kind == "KW" and t.value in ("and", "or"):
|
|
1327
|
+
lbp, rbp = _BINOPS[t.value]
|
|
1328
|
+
return (t.value, lbp, rbp)
|
|
1329
|
+
return None
|
|
1330
|
+
|
|
1331
|
+
def _parse_prefix(self):
|
|
1332
|
+
t = self.cur
|
|
1333
|
+
if self.at_kw("not"):
|
|
1334
|
+
self.advance()
|
|
1335
|
+
operand = self.parse_expr(_PREFIX_BP)
|
|
1336
|
+
return A.Unary(start=t.start, end=getattr(operand, "end", t.end), op="not", operand=operand)
|
|
1337
|
+
if self.at_op("-"):
|
|
1338
|
+
self.advance()
|
|
1339
|
+
operand = self.parse_expr(_PREFIX_BP)
|
|
1340
|
+
return A.Unary(start=t.start, end=getattr(operand, "end", t.end), op="-", operand=operand)
|
|
1341
|
+
return self._parse_postfix(self._parse_primary())
|
|
1342
|
+
|
|
1343
|
+
def _parse_postfix(self, node):
|
|
1344
|
+
while True:
|
|
1345
|
+
if self.at_op("."):
|
|
1346
|
+
self.advance()
|
|
1347
|
+
nm = self.advance().value if self.cur.kind in ("NAME", "TYPENAME", "KW") else "?"
|
|
1348
|
+
node = A.Member(start=getattr(node, "start", 0), end=self.cur.end, obj=node, name=nm)
|
|
1349
|
+
elif self.at_op("("):
|
|
1350
|
+
# In `if (condition) (then_expr) else other`, the closing
|
|
1351
|
+
# condition parenthesis is a boundary, not a callable value.
|
|
1352
|
+
if self.pos in getattr(self, "_if_condition_ends", ()):
|
|
1353
|
+
break
|
|
1354
|
+
self.advance()
|
|
1355
|
+
args, named = [], []
|
|
1356
|
+
self.skip_newlines() # arguments may span lines inside ( )
|
|
1357
|
+
while not self.at_op(")") and not self.at("EOF"):
|
|
1358
|
+
if self.at("NAME") and self.peek(1).kind == "OP" and self.peek(1).value == ":":
|
|
1359
|
+
an = self.advance().value
|
|
1360
|
+
self.advance()
|
|
1361
|
+
named.append((an, self.parse_expr()))
|
|
1362
|
+
else:
|
|
1363
|
+
args.append(self.parse_expr())
|
|
1364
|
+
had_nl = self.at("NEWLINE") or self._take_closure_ate_sep()
|
|
1365
|
+
self.skip_newlines()
|
|
1366
|
+
if self.at_op(","):
|
|
1367
|
+
self.advance()
|
|
1368
|
+
self.skip_newlines()
|
|
1369
|
+
elif not had_nl and not self.at_op(")"):
|
|
1370
|
+
break # spec 3.7: a bare newline also separates args
|
|
1371
|
+
self.expect_op(")")
|
|
1372
|
+
node = A.Call(start=getattr(node, "start", 0), end=self.cur.end,
|
|
1373
|
+
callee=node, args=args, named=named)
|
|
1374
|
+
elif self.at_op("["):
|
|
1375
|
+
self.advance()
|
|
1376
|
+
idx = self.parse_expr()
|
|
1377
|
+
self.expect_op("]")
|
|
1378
|
+
node = A.Index(start=getattr(node, "start", 0), end=self.cur.end, obj=node, index=idx)
|
|
1379
|
+
elif self.at_op("?"):
|
|
1380
|
+
self.advance()
|
|
1381
|
+
node = A.TryOp(start=getattr(node, "start", 0), end=self.cur.end, operand=node)
|
|
1382
|
+
else:
|
|
1383
|
+
break
|
|
1384
|
+
return node
|
|
1385
|
+
|
|
1386
|
+
def _parse_primary(self):
|
|
1387
|
+
t = self.cur
|
|
1388
|
+
if t.kind == "INT":
|
|
1389
|
+
self.advance()
|
|
1390
|
+
return A.Literal(start=t.start, end=t.end, kind="int", raw=t.value)
|
|
1391
|
+
if t.kind == "FLOAT":
|
|
1392
|
+
self.advance()
|
|
1393
|
+
return A.Literal(start=t.start, end=t.end, kind="float", raw=t.value)
|
|
1394
|
+
if t.kind == "DURATION":
|
|
1395
|
+
self.advance()
|
|
1396
|
+
return A.Literal(start=t.start, end=t.end, kind="duration", raw=t.value)
|
|
1397
|
+
if t.kind == "BOOL":
|
|
1398
|
+
self.advance()
|
|
1399
|
+
return A.Literal(start=t.start, end=t.end, kind="bool", raw=t.value)
|
|
1400
|
+
if t.kind == "NIL":
|
|
1401
|
+
self.advance()
|
|
1402
|
+
return A.Literal(start=t.start, end=t.end, kind="nil", raw="nil")
|
|
1403
|
+
if t.kind == "STRING":
|
|
1404
|
+
self.advance()
|
|
1405
|
+
lit = A.Literal(start=t.start, end=t.end, kind="str", raw=t.value)
|
|
1406
|
+
lit.interp_names = self._interp_names(t)
|
|
1407
|
+
return lit
|
|
1408
|
+
if self.at_kw("if"):
|
|
1409
|
+
return self._parse_if_expr()
|
|
1410
|
+
if self.at_kw("match"):
|
|
1411
|
+
return self.parse_match_expr()
|
|
1412
|
+
if self.at_kw("fn"):
|
|
1413
|
+
return self._parse_closure()
|
|
1414
|
+
if self.at_op("("):
|
|
1415
|
+
self.advance()
|
|
1416
|
+
e = self.parse_expr()
|
|
1417
|
+
self.expect_op(")")
|
|
1418
|
+
return e
|
|
1419
|
+
if self.at_op("["):
|
|
1420
|
+
return self._parse_list_lit()
|
|
1421
|
+
if self.at_op("{"):
|
|
1422
|
+
return self._parse_record_lit(None)
|
|
1423
|
+
if t.kind == "TYPENAME":
|
|
1424
|
+
self.advance()
|
|
1425
|
+
if self.at_op("{"):
|
|
1426
|
+
return self._parse_record_lit(t.value)
|
|
1427
|
+
return A.Name(start=t.start, end=t.end, id=t.value)
|
|
1428
|
+
# `err` and `auth` are reserved but read as values here; `api` / `session`
|
|
1429
|
+
# are contextual and already arrive as NAME tokens.
|
|
1430
|
+
if t.kind in ("NAME", "PRIM") or self.at_kw("err", "auth"):
|
|
1431
|
+
self.advance()
|
|
1432
|
+
return A.Name(start=t.start, end=t.end, id=t.value)
|
|
1433
|
+
self.error(f"expected an expression, found {self._desc(t)}", t)
|
|
1434
|
+
self.advance()
|
|
1435
|
+
return A.Name(start=t.start, end=t.end, id="?")
|
|
1436
|
+
|
|
1437
|
+
def _parse_if_expr(self) -> A.IfExpr:
|
|
1438
|
+
start = self.advance().start
|
|
1439
|
+
boundary = None
|
|
1440
|
+
if self.at_op("("):
|
|
1441
|
+
depth = 0
|
|
1442
|
+
for i in range(self.pos, len(self.toks)):
|
|
1443
|
+
t = self.toks[i]
|
|
1444
|
+
if t.kind == "OP" and t.value == "(":
|
|
1445
|
+
depth += 1
|
|
1446
|
+
elif t.kind == "OP" and t.value == ")":
|
|
1447
|
+
depth -= 1
|
|
1448
|
+
if depth == 0:
|
|
1449
|
+
boundary = i + 1
|
|
1450
|
+
break
|
|
1451
|
+
previous = getattr(self, "_if_condition_ends", set())
|
|
1452
|
+
self._if_condition_ends = previous | ({boundary} if boundary is not None else set())
|
|
1453
|
+
try:
|
|
1454
|
+
cond = self.parse_expr()
|
|
1455
|
+
finally:
|
|
1456
|
+
self._if_condition_ends = previous
|
|
1457
|
+
node = A.IfExpr(start=start, cond=cond, then=self.parse_expr())
|
|
1458
|
+
if self.at_kw("else"):
|
|
1459
|
+
self.advance()
|
|
1460
|
+
node.orelse = self.parse_expr()
|
|
1461
|
+
node.has_else = True
|
|
1462
|
+
else:
|
|
1463
|
+
self.diags.append(Diagnostic(
|
|
1464
|
+
"E-IF-EXPR-ELSE", Severity.ERROR,
|
|
1465
|
+
"an `if` expression is `if <cond> <then> else <else>` -- `else` "
|
|
1466
|
+
"is required, each branch is one expression, and it is binary "
|
|
1467
|
+
"(nest for 3+, no `else if` / `match`). If this looks complete, "
|
|
1468
|
+
"a `(` right after a value was read as a call -- wrap the "
|
|
1469
|
+
"condition: `if (cond) a else b`.",
|
|
1470
|
+
start, self.cur.end,
|
|
1471
|
+
"if (cond) a else b # binary; nest for more cases",
|
|
1472
|
+
self.filename))
|
|
1473
|
+
node.end = self.cur.end
|
|
1474
|
+
return node
|
|
1475
|
+
|
|
1476
|
+
def parse_match_expr(self) -> A.MatchExpr:
|
|
1477
|
+
start = self.advance().start
|
|
1478
|
+
node = A.MatchExpr(start=start, subject=self.parse_expr())
|
|
1479
|
+
self.skip_newlines()
|
|
1480
|
+
if self.at("INDENT"):
|
|
1481
|
+
self.advance()
|
|
1482
|
+
while not self.at("DEDENT") and not self.at("EOF"):
|
|
1483
|
+
self.skip_newlines()
|
|
1484
|
+
if self.at("DEDENT"):
|
|
1485
|
+
break
|
|
1486
|
+
arm = self._match_arm(self.parse_stmt)
|
|
1487
|
+
if arm is not None:
|
|
1488
|
+
node.arms.append(arm)
|
|
1489
|
+
self.skip_newlines()
|
|
1490
|
+
if self.at("DEDENT"):
|
|
1491
|
+
self.advance()
|
|
1492
|
+
node.end = self.cur.end
|
|
1493
|
+
return node
|
|
1494
|
+
|
|
1495
|
+
def _parse_closure(self) -> A.Closure:
|
|
1496
|
+
start = self.advance().start # 'fn'
|
|
1497
|
+
self.expect_op("(")
|
|
1498
|
+
params = self._param_list_until(")")
|
|
1499
|
+
self.expect_op(")")
|
|
1500
|
+
node = A.Closure(start=start, params=params)
|
|
1501
|
+
if self.cur.kind in ("TYPENAME", "PRIM") and self.peek(1).kind == "NEWLINE":
|
|
1502
|
+
node.ret = self.parse_type_ref()
|
|
1503
|
+
if self.at("NEWLINE"):
|
|
1504
|
+
self.skip_newlines()
|
|
1505
|
+
if self.at("INDENT"):
|
|
1506
|
+
node.body = self.parse_block(self.parse_stmt)
|
|
1507
|
+
elif self.bracket_depth > 0:
|
|
1508
|
+
# A block-bodied closure written as a call argument (e.g. a
|
|
1509
|
+
# callback prop's closure at its most natural call site,
|
|
1510
|
+
# `Child(onPick: fn(item)\n selected = item.id)`): the lexer
|
|
1511
|
+
# suppresses INDENT/DEDENT inside any open bracket (spec 3.7
|
|
1512
|
+
# keeps NEWLINE as the item separator there), so there is no
|
|
1513
|
+
# INDENT token to find the block by. Track it by source
|
|
1514
|
+
# column instead, entirely inside the parser.
|
|
1515
|
+
node.body = self._parse_bracketed_closure_body()
|
|
1516
|
+
node.end = self.cur.end
|
|
1517
|
+
return node
|
|
1518
|
+
node.body = self.parse_expr()
|
|
1519
|
+
node.end = getattr(node.body, "end", self.cur.end)
|
|
1520
|
+
return node
|
|
1521
|
+
|
|
1522
|
+
def _col(self, tok: Token) -> int:
|
|
1523
|
+
"""0-based column of `tok` on its own source line."""
|
|
1524
|
+
return tok.start - (self.src.rfind("\n", 0, tok.start) + 1)
|
|
1525
|
+
|
|
1526
|
+
def _parse_bracketed_closure_body(self) -> list:
|
|
1527
|
+
"""The statement-block form of `_parse_closure`, for when it is
|
|
1528
|
+
reached inside an open bracket (see the call site). `parse_block`
|
|
1529
|
+
can't be reused here — it waits for a DEDENT token, which the lexer
|
|
1530
|
+
never produces while any bracket is open — so this parses statements
|
|
1531
|
+
directly and decides where the block ends by comparing each
|
|
1532
|
+
statement's column to the first one's, stopping at a shallower
|
|
1533
|
+
column or at whatever closes/continues the enclosing bracket
|
|
1534
|
+
(`)` / `]` / `}` / `,`) — the same tokens `_end_line`'s matching
|
|
1535
|
+
tolerance (above) lets each statement end on."""
|
|
1536
|
+
self.skip_newlines()
|
|
1537
|
+
if self.cur.kind in ("DEDENT", "EOF") or self.at_op(")", "]", "}", ","):
|
|
1538
|
+
return []
|
|
1539
|
+
body_col = self._col(self.cur)
|
|
1540
|
+
stmts: list = []
|
|
1541
|
+
while True:
|
|
1542
|
+
before = self.pos
|
|
1543
|
+
stmts.append(self.parse_stmt())
|
|
1544
|
+
if self.pos == before:
|
|
1545
|
+
self.advance()
|
|
1546
|
+
break
|
|
1547
|
+
self.skip_newlines()
|
|
1548
|
+
if self.cur.kind in ("DEDENT", "EOF") or self.at_op(")", "]", "}", ","):
|
|
1549
|
+
break
|
|
1550
|
+
if self._col(self.cur) != body_col:
|
|
1551
|
+
# A sibling at a shallower column follows (e.g. the next
|
|
1552
|
+
# named call argument) -- the block ends here. Getting here
|
|
1553
|
+
# required a real NEWLINE, already consumed inside the last
|
|
1554
|
+
# statement's own `_end_line()`; tell the call-argument loop
|
|
1555
|
+
# that resumes once `_parse_closure` returns, since it can no
|
|
1556
|
+
# longer see that token to know a separator occurred.
|
|
1557
|
+
self._closure_ate_sep = True
|
|
1558
|
+
break
|
|
1559
|
+
return stmts
|
|
1560
|
+
|
|
1561
|
+
def _parse_list_lit(self) -> A.ListLit:
|
|
1562
|
+
start = self.advance().start # [
|
|
1563
|
+
node = A.ListLit(start=start)
|
|
1564
|
+
self.skip_newlines() # spec 3.7: a literal may open onto its own lines
|
|
1565
|
+
while not self.at_op("]") and not self.at("EOF"):
|
|
1566
|
+
before = self.pos
|
|
1567
|
+
node.items.append(self.parse_expr())
|
|
1568
|
+
# This loop has no separator-required break (unlike a call's
|
|
1569
|
+
# argument list), so it needs no `_closure_ate_sep` reading --
|
|
1570
|
+
# but drain it regardless, so a block-bodied closure item here
|
|
1571
|
+
# can never leave a stale flag for some later, unrelated call's
|
|
1572
|
+
# argument loop to misread.
|
|
1573
|
+
self._closure_ate_sep = False
|
|
1574
|
+
if self.at_op(","):
|
|
1575
|
+
comma = self.advance()
|
|
1576
|
+
self.skip_newlines() # `,` then a newline before the next item / `]`
|
|
1577
|
+
if self.at_op("]"):
|
|
1578
|
+
self.diags.append(Diagnostic(
|
|
1579
|
+
"E-TRAILING-COMMA", Severity.ERROR,
|
|
1580
|
+
"trailing commas are not allowed",
|
|
1581
|
+
comma.start, comma.end, "remove the trailing comma", self.filename))
|
|
1582
|
+
else:
|
|
1583
|
+
self.skip_newlines() # spec 3.7: a newline separates items in block position
|
|
1584
|
+
if self.pos == before:
|
|
1585
|
+
self.advance()
|
|
1586
|
+
self.expect_op("]")
|
|
1587
|
+
node.empty = not node.items
|
|
1588
|
+
node.end = self.cur.end
|
|
1589
|
+
return node
|
|
1590
|
+
|
|
1591
|
+
def _parse_record_lit(self, type_name) -> A.RecordLit:
|
|
1592
|
+
start = self.cur.start
|
|
1593
|
+
self.expect_op("{")
|
|
1594
|
+
node = A.RecordLit(start=start, type_name=type_name)
|
|
1595
|
+
self.skip_newlines() # spec 3.7: a literal may open onto its own lines
|
|
1596
|
+
while not self.at_op("}") and not self.at("EOF"):
|
|
1597
|
+
before = self.pos
|
|
1598
|
+
if self.at("NAME") and self.peek(1).kind == "OP" and self.peek(1).value == ":":
|
|
1599
|
+
fn = self.advance().value
|
|
1600
|
+
self.advance()
|
|
1601
|
+
node.fields.append((fn, self.parse_expr()))
|
|
1602
|
+
else:
|
|
1603
|
+
key = self.parse_expr()
|
|
1604
|
+
if self.at_op(":"):
|
|
1605
|
+
self.advance()
|
|
1606
|
+
node.fields.append((None, self.parse_expr()))
|
|
1607
|
+
else:
|
|
1608
|
+
node.fields.append((None, key))
|
|
1609
|
+
self._closure_ate_sep = False # see the matching drain in _parse_list_lit
|
|
1610
|
+
if self.at_op(","):
|
|
1611
|
+
comma = self.advance()
|
|
1612
|
+
self.skip_newlines() # `,` then a newline before the next field / `}`
|
|
1613
|
+
if self.at_op("}"):
|
|
1614
|
+
self.diags.append(Diagnostic(
|
|
1615
|
+
"E-TRAILING-COMMA", Severity.ERROR,
|
|
1616
|
+
"trailing commas are not allowed",
|
|
1617
|
+
comma.start, comma.end, "remove the trailing comma", self.filename))
|
|
1618
|
+
else:
|
|
1619
|
+
self.skip_newlines() # spec 3.7: a newline separates fields in block position
|
|
1620
|
+
if self.pos == before:
|
|
1621
|
+
self.advance()
|
|
1622
|
+
self.expect_op("}")
|
|
1623
|
+
node.empty = not node.fields
|
|
1624
|
+
node.end = self.cur.end
|
|
1625
|
+
return node
|
|
1626
|
+
|
|
1627
|
+
def _interp_names(self, tok: Token) -> list:
|
|
1628
|
+
out: list[str] = []
|
|
1629
|
+
for (s, e) in getattr(tok, "interps", []):
|
|
1630
|
+
frag = self.src[s:e]
|
|
1631
|
+
for m in _IDENT_RE.finditer(frag):
|
|
1632
|
+
out.append(m.group(1))
|
|
1633
|
+
return out
|
|
1634
|
+
|
|
1635
|
+
|
|
1636
|
+
def parse(tokens: list[Token], src: str, filename: str = "<input>"):
|
|
1637
|
+
p = Parser(tokens, src, filename)
|
|
1638
|
+
return p.parse_module(), p.diags
|