volaro 0.0.2 → 0.1.0-alpha.1
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 +88 -22
- package/bin/vl.js +437 -28
- 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 +1075 -0
- package/compiler/validator/vlcheck/diagnostics.py +88 -0
- package/compiler/validator/vlcheck/lexer.py +311 -0
- package/compiler/validator/vlcheck/parser.py +1432 -0
- package/compiler/validator/vlcheck/project_config.py +97 -0
- package/compiler/validator/vlcheck/resolve.py +848 -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 +243 -0
- package/compiler/vlbuild/vlbuild/assets/vlrt.css +165 -0
- package/compiler/vlbuild/vlbuild/assets/vlrt.js +1151 -0
- package/compiler/vlbuild/vlbuild/emit.py +1839 -0
- package/compiler/vlbuild/vlbuild/server_emit.py +1549 -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 +51 -4
- package/language/spec.md +20 -1
- package/language/supported.md +117 -0
- package/lib/env.js +107 -0
- package/package.json +19 -2
- package/scripts/selftest.mjs +54 -0
- package/scripts/sync-compiler.sh +52 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""CLI entry point.
|
|
2
|
+
|
|
3
|
+
python -m vlcheck <file-or-dir>... [--json] [--quiet] [--strict-names] [--resolve] [--release]
|
|
4
|
+
|
|
5
|
+
`--release` promotes "unresolved development placeholder" warnings (e.g. an
|
|
6
|
+
`img alt_todo:`) to errors -- the ship gate for unfinished markers.
|
|
7
|
+
|
|
8
|
+
`--resolve` adds the L1 resolution pass (`resolve.py`) on top of the tolerant
|
|
9
|
+
Stage-1 checks: unknown names / fields / routes, wrong arity, unknown or missing
|
|
10
|
+
named arguments, non-exhaustive `match` over a declared union. It follows `use`
|
|
11
|
+
imports across files. This is the Stage-3 G2 grader for the Volaro arm.
|
|
12
|
+
|
|
13
|
+
Exit code is non-zero if any error-severity diagnostic is produced.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import glob
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
from .checks import check
|
|
24
|
+
from .diagnostics import LineIndex, Severity
|
|
25
|
+
from .lexer import Lexer
|
|
26
|
+
from .parser import Parser
|
|
27
|
+
from .resolve import resolve
|
|
28
|
+
from .project_config import load_config, ConfigError
|
|
29
|
+
from .diagnostics import Diagnostic
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _iter_paths(args: list[str]) -> list[str]:
|
|
33
|
+
out: list[str] = []
|
|
34
|
+
for a in args:
|
|
35
|
+
if os.path.isdir(a):
|
|
36
|
+
out += sorted(glob.glob(os.path.join(a, "**", "*.vl"), recursive=True))
|
|
37
|
+
elif any(ch in a for ch in "*?["):
|
|
38
|
+
out += sorted(glob.glob(a, recursive=True))
|
|
39
|
+
else:
|
|
40
|
+
out.append(a)
|
|
41
|
+
return out
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check_source(src: str, filename: str, strict_names: bool = False,
|
|
45
|
+
do_resolve: bool = False, release: bool = False):
|
|
46
|
+
li = LineIndex(src)
|
|
47
|
+
lx = Lexer(src, filename)
|
|
48
|
+
tokens = lx.lex()
|
|
49
|
+
parser = Parser(tokens, src, filename)
|
|
50
|
+
mod = parser.parse_module()
|
|
51
|
+
diags = (list(lx.diags) + list(parser.diags)
|
|
52
|
+
+ check(mod, filename, li, strict_names=strict_names, release=release))
|
|
53
|
+
if do_resolve:
|
|
54
|
+
diags += resolve(mod, filename, li)
|
|
55
|
+
diags.sort(key=lambda d: (d.start, 0 if d.severity == Severity.ERROR else 1, d.code))
|
|
56
|
+
return li, diags
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main(argv: list[str]) -> int:
|
|
60
|
+
argv = list(argv)
|
|
61
|
+
argv = [part for arg in argv for part in (['--config', arg.split('=', 1)[1]]
|
|
62
|
+
if arg.startswith('--config=') else [arg])]
|
|
63
|
+
config = None
|
|
64
|
+
if argv.count('--config') > 1:
|
|
65
|
+
print('E-CONFIG-OPTION: --config may be supplied only once', file=sys.stderr)
|
|
66
|
+
return 2
|
|
67
|
+
if '--config' in argv:
|
|
68
|
+
i = argv.index('--config')
|
|
69
|
+
if i + 1 >= len(argv) or argv[i + 1].startswith('--'):
|
|
70
|
+
print('E-CONFIG-READ: --config requires a path', file=sys.stderr)
|
|
71
|
+
return 2
|
|
72
|
+
config = argv[i + 1]
|
|
73
|
+
del argv[i:i + 2]
|
|
74
|
+
flags = {a for a in argv if a.startswith("--")}
|
|
75
|
+
args = [a for a in argv if not a.startswith("--")]
|
|
76
|
+
as_json = "--json" in flags
|
|
77
|
+
quiet = "--quiet" in flags
|
|
78
|
+
strict_names = "--strict-names" in flags
|
|
79
|
+
do_resolve = "--resolve" in flags
|
|
80
|
+
release = "--release" in flags
|
|
81
|
+
|
|
82
|
+
paths = _iter_paths(args)
|
|
83
|
+
if not paths:
|
|
84
|
+
print("usage: python -m vlcheck <file-or-dir>... "
|
|
85
|
+
"[--json] [--quiet] [--strict-names] [--resolve] [--release] [--config PATH]",
|
|
86
|
+
file=sys.stderr)
|
|
87
|
+
return 2
|
|
88
|
+
|
|
89
|
+
total_err = total_warn = 0
|
|
90
|
+
json_rows: list[dict] = []
|
|
91
|
+
|
|
92
|
+
for path in paths:
|
|
93
|
+
try:
|
|
94
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
95
|
+
src = fh.read()
|
|
96
|
+
except (FileNotFoundError, IsADirectoryError):
|
|
97
|
+
print(f"error: cannot read {path}", file=sys.stderr)
|
|
98
|
+
total_err += 1
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
li, diags = check_source(src, path, strict_names=strict_names,
|
|
102
|
+
do_resolve=do_resolve, release=release)
|
|
103
|
+
try:
|
|
104
|
+
load_config(path, config)
|
|
105
|
+
except ConfigError as exc:
|
|
106
|
+
diags.append(Diagnostic(exc.code, Severity.ERROR, str(exc), 0, 0, file=path))
|
|
107
|
+
for d in diags:
|
|
108
|
+
if d.severity == Severity.ERROR:
|
|
109
|
+
total_err += 1
|
|
110
|
+
elif d.severity == Severity.WARNING:
|
|
111
|
+
total_warn += 1
|
|
112
|
+
if as_json:
|
|
113
|
+
json_rows.append(d.to_json(li))
|
|
114
|
+
elif not quiet or d.severity == Severity.ERROR:
|
|
115
|
+
print(d.render_human(li))
|
|
116
|
+
print()
|
|
117
|
+
|
|
118
|
+
if as_json:
|
|
119
|
+
print(json.dumps({"diagnostics": json_rows}, indent=2))
|
|
120
|
+
else:
|
|
121
|
+
n = len(paths)
|
|
122
|
+
print(f"checked {n} file{'s' if n != 1 else ''}: "
|
|
123
|
+
f"{total_err} error(s), {total_warn} warning(s)")
|
|
124
|
+
return 1 if total_err else 0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""A plain dataclass AST. Every node carries byte offsets `start` / `end`.
|
|
2
|
+
|
|
3
|
+
This is intentionally lossy compared to the spec's rowan CST: the validator does
|
|
4
|
+
not format or rewrite source, it only inspects structure. Every field has a
|
|
5
|
+
default so nodes can be built incrementally by the tolerant parser.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Node:
|
|
16
|
+
start: int = 0
|
|
17
|
+
end: int = 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# --- items --------------------------------------------------------------
|
|
21
|
+
@dataclass
|
|
22
|
+
class Module(Node):
|
|
23
|
+
items: list = field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Use(Node):
|
|
28
|
+
path: str = ""
|
|
29
|
+
names: list = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class TypeDecl(Node):
|
|
34
|
+
name: str = ""
|
|
35
|
+
kind: str = "" # record | union | alias
|
|
36
|
+
fields: list = field(default_factory=list)
|
|
37
|
+
variants: list = field(default_factory=list)
|
|
38
|
+
alias_of: Any = None
|
|
39
|
+
distinct: bool = False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Field(Node):
|
|
44
|
+
name: str = ""
|
|
45
|
+
type: Any = None
|
|
46
|
+
default: Any = None
|
|
47
|
+
attrs: list = field(default_factory=list) # model field attrs: pk, unique, ref:User.id
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Variant(Node):
|
|
52
|
+
name: str = ""
|
|
53
|
+
params: list = field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ModelDecl(Node):
|
|
58
|
+
name: str = ""
|
|
59
|
+
fields: list = field(default_factory=list)
|
|
60
|
+
indexes: list = field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Param(Node):
|
|
65
|
+
name: str = ""
|
|
66
|
+
type: Any = None
|
|
67
|
+
default: Any = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class FnDecl(Node):
|
|
72
|
+
name: str = ""
|
|
73
|
+
params: list = field(default_factory=list)
|
|
74
|
+
ret: Any = None
|
|
75
|
+
body: list = field(default_factory=list)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class TypeRef(Node):
|
|
80
|
+
name: str = ""
|
|
81
|
+
args: list = field(default_factory=list)
|
|
82
|
+
optional: bool = False
|
|
83
|
+
fallible: bool = False
|
|
84
|
+
err_type: Any = None
|
|
85
|
+
is_fn: bool = False
|
|
86
|
+
fn_params: list = field(default_factory=list)
|
|
87
|
+
fn_ret: Any = None
|
|
88
|
+
opt_nested: bool = False # T?? seen
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# --- views ------------------------------------------------------------
|
|
92
|
+
@dataclass
|
|
93
|
+
class ViewDecl(Node):
|
|
94
|
+
name: str = ""
|
|
95
|
+
params: list = field(default_factory=list)
|
|
96
|
+
body: list = field(default_factory=list)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class PageDecl(Node):
|
|
101
|
+
"""Module-level page declaration (spec §8.11). Carries the page title and
|
|
102
|
+
(later) other document-scope properties. `attrs` is [(name, expr)], like an
|
|
103
|
+
element's — `title:` is required and evaluated at module scope."""
|
|
104
|
+
attrs: list = field(default_factory=list)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class RecipeDecl(Node):
|
|
109
|
+
"""Project styling recipe. Rows remain directives so validation can stay
|
|
110
|
+
tolerant while preserving the compact, indentation-based surface."""
|
|
111
|
+
primitive: str = ""
|
|
112
|
+
body: list = field(default_factory=list)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class StateDecl(Node):
|
|
117
|
+
name: str = ""
|
|
118
|
+
type: Any = None
|
|
119
|
+
init: Any = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class DeriveDecl(Node):
|
|
124
|
+
name: str = ""
|
|
125
|
+
expr: Any = None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class LoadDecl(Node):
|
|
130
|
+
name: str = ""
|
|
131
|
+
expr: Any = None
|
|
132
|
+
pending: list = field(default_factory=list)
|
|
133
|
+
error_name: str | None = None
|
|
134
|
+
error_body: list = field(default_factory=list)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class OnBlock(Node):
|
|
139
|
+
event: str = ""
|
|
140
|
+
args: list = field(default_factory=list)
|
|
141
|
+
body: list = field(default_factory=list)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class ListBlock(Node):
|
|
146
|
+
src: Any = None
|
|
147
|
+
var: str = ""
|
|
148
|
+
key: Any = None
|
|
149
|
+
has_key: bool = False
|
|
150
|
+
body: list = field(default_factory=list)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class Element(Node):
|
|
155
|
+
name: str = ""
|
|
156
|
+
args: list = field(default_factory=list) # positional exprs
|
|
157
|
+
attrs: list = field(default_factory=list) # [(name, expr)]
|
|
158
|
+
body: list = field(default_factory=list)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --- api / service / auth -------------------------------------------------
|
|
162
|
+
@dataclass
|
|
163
|
+
class ApiDecl(Node):
|
|
164
|
+
name: str = ""
|
|
165
|
+
from_expr: Any = None
|
|
166
|
+
settings: list = field(default_factory=list)
|
|
167
|
+
endpoints: list = field(default_factory=list)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass
|
|
171
|
+
class Endpoint(Node):
|
|
172
|
+
method: str = ""
|
|
173
|
+
name: str = ""
|
|
174
|
+
params: list = field(default_factory=list)
|
|
175
|
+
ret: Any = None
|
|
176
|
+
settings: list = field(default_factory=list)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class ServiceDecl(Node):
|
|
181
|
+
name: str = ""
|
|
182
|
+
is_public: bool = False # `service X public` — every route public unless it overrides
|
|
183
|
+
routes: list = field(default_factory=list)
|
|
184
|
+
settings: list = field(default_factory=list)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class Route(Node):
|
|
189
|
+
method: str = ""
|
|
190
|
+
path: str = ""
|
|
191
|
+
params: list = field(default_factory=list)
|
|
192
|
+
ret: Any = None
|
|
193
|
+
guard: Any = None
|
|
194
|
+
alias: str | None = None # `... as name` — overrides the generated client name
|
|
195
|
+
body: list = field(default_factory=list)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class Guard(Node):
|
|
200
|
+
kind: str = "" # required | optional | role | owner
|
|
201
|
+
detail: Any = None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass
|
|
205
|
+
class AuthDecl(Node):
|
|
206
|
+
settings: list = field(default_factory=list)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@dataclass
|
|
210
|
+
class Directive(Node):
|
|
211
|
+
"""Generic setting line: a head word, raw token texts, and an optional block.
|
|
212
|
+
|
|
213
|
+
Used for the many free-form configuration lines (api settings, auth providers,
|
|
214
|
+
service `errors` / `before` / `after`, theme) that the validator records but
|
|
215
|
+
does not deeply check.
|
|
216
|
+
"""
|
|
217
|
+
head: str = ""
|
|
218
|
+
tokens: list = field(default_factory=list)
|
|
219
|
+
body: list = field(default_factory=list)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# --- statements -------------------------------------------------------
|
|
223
|
+
@dataclass
|
|
224
|
+
class Let(Node):
|
|
225
|
+
name: str = ""
|
|
226
|
+
mut: bool = False
|
|
227
|
+
type: Any = None
|
|
228
|
+
init: Any = None
|
|
229
|
+
empty_literal_no_type: bool = False
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dataclass
|
|
233
|
+
class Assign(Node):
|
|
234
|
+
target: Any = None
|
|
235
|
+
op: str = "="
|
|
236
|
+
value: Any = None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass
|
|
240
|
+
class If(Node):
|
|
241
|
+
cond: Any = None
|
|
242
|
+
then: list = field(default_factory=list)
|
|
243
|
+
elifs: list = field(default_factory=list) # [(cond, body)]
|
|
244
|
+
orelse: list = field(default_factory=list)
|
|
245
|
+
has_else: bool = False
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@dataclass
|
|
249
|
+
class Match(Node):
|
|
250
|
+
subject: Any = None
|
|
251
|
+
arms: list = field(default_factory=list) # [(pattern, guard, body)]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@dataclass
|
|
255
|
+
class For(Node):
|
|
256
|
+
vars: list = field(default_factory=list)
|
|
257
|
+
iter: Any = None
|
|
258
|
+
body: list = field(default_factory=list)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass
|
|
262
|
+
class While(Node):
|
|
263
|
+
cond: Any = None
|
|
264
|
+
body: list = field(default_factory=list)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@dataclass
|
|
268
|
+
class Par(Node):
|
|
269
|
+
lets: list = field(default_factory=list)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass
|
|
273
|
+
class Spawn(Node):
|
|
274
|
+
body: list = field(default_factory=list)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@dataclass
|
|
278
|
+
class Try(Node):
|
|
279
|
+
expr: Any = None
|
|
280
|
+
catches: list = field(default_factory=list) # [(pattern, body)]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@dataclass
|
|
284
|
+
class Return(Node):
|
|
285
|
+
value: Any = None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@dataclass
|
|
289
|
+
class Break(Node):
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@dataclass
|
|
294
|
+
class Skip(Node):
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@dataclass
|
|
299
|
+
class ExprStmt(Node):
|
|
300
|
+
expr: Any = None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# --- expressions -----------------------------------------------------
|
|
304
|
+
@dataclass
|
|
305
|
+
class Name(Node):
|
|
306
|
+
id: str = ""
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@dataclass
|
|
310
|
+
class Literal(Node):
|
|
311
|
+
kind: str = "" # int float str bool nil duration
|
|
312
|
+
raw: str = ""
|
|
313
|
+
interp_names: list = field(default_factory=list)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@dataclass
|
|
317
|
+
class ListLit(Node):
|
|
318
|
+
items: list = field(default_factory=list)
|
|
319
|
+
empty: bool = False
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@dataclass
|
|
323
|
+
class RecordLit(Node):
|
|
324
|
+
type_name: str | None = None
|
|
325
|
+
fields: list = field(default_factory=list) # [(name_or_None, expr)]
|
|
326
|
+
empty: bool = False
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@dataclass
|
|
330
|
+
class Call(Node):
|
|
331
|
+
callee: Any = None
|
|
332
|
+
args: list = field(default_factory=list)
|
|
333
|
+
named: list = field(default_factory=list) # [(name, expr)]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@dataclass
|
|
337
|
+
class Member(Node):
|
|
338
|
+
obj: Any = None
|
|
339
|
+
name: str = ""
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@dataclass
|
|
343
|
+
class Index(Node):
|
|
344
|
+
obj: Any = None
|
|
345
|
+
index: Any = None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@dataclass
|
|
349
|
+
class Unary(Node):
|
|
350
|
+
op: str = ""
|
|
351
|
+
operand: Any = None
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@dataclass
|
|
355
|
+
class Binary(Node):
|
|
356
|
+
op: str = ""
|
|
357
|
+
left: Any = None
|
|
358
|
+
right: Any = None
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@dataclass
|
|
362
|
+
class TryOp(Node): # postfix ?
|
|
363
|
+
operand: Any = None
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@dataclass
|
|
367
|
+
class IfExpr(Node):
|
|
368
|
+
cond: Any = None
|
|
369
|
+
then: Any = None
|
|
370
|
+
orelse: Any = None
|
|
371
|
+
has_else: bool = False
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@dataclass
|
|
375
|
+
class Closure(Node):
|
|
376
|
+
params: list = field(default_factory=list)
|
|
377
|
+
ret: Any = None
|
|
378
|
+
body: Any = None # expr, or list[stmt]
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@dataclass
|
|
382
|
+
class MatchExpr(Node):
|
|
383
|
+
subject: Any = None
|
|
384
|
+
arms: list = field(default_factory=list)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@dataclass
|
|
388
|
+
class Range(Node):
|
|
389
|
+
lo: Any = None
|
|
390
|
+
hi: Any = None
|
|
391
|
+
inclusive: bool = False
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@dataclass
|
|
395
|
+
class Pattern(Node):
|
|
396
|
+
binds: list = field(default_factory=list)
|
|
397
|
+
head: str = "" # constructor / variant / "_" ; "" for literal & structural patterns
|