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.
Files changed (34) hide show
  1. package/README.md +88 -22
  2. package/bin/vl.js +437 -28
  3. package/compiler/SOURCE_REV +1 -0
  4. package/compiler/validator/vlcheck/__init__.py +10 -0
  5. package/compiler/validator/vlcheck/__main__.py +128 -0
  6. package/compiler/validator/vlcheck/ast_nodes.py +397 -0
  7. package/compiler/validator/vlcheck/checks.py +1075 -0
  8. package/compiler/validator/vlcheck/diagnostics.py +88 -0
  9. package/compiler/validator/vlcheck/lexer.py +311 -0
  10. package/compiler/validator/vlcheck/parser.py +1432 -0
  11. package/compiler/validator/vlcheck/project_config.py +97 -0
  12. package/compiler/validator/vlcheck/resolve.py +848 -0
  13. package/compiler/validator/vlcheck/test_ids.py +98 -0
  14. package/compiler/vlbuild/styling/README.md +39 -0
  15. package/compiler/vlbuild/styling/build-css.mjs +103 -0
  16. package/compiler/vlbuild/styling/package-lock.json +1254 -0
  17. package/compiler/vlbuild/styling/package.json +15 -0
  18. package/compiler/vlbuild/styling/test-build-css.mjs +69 -0
  19. package/compiler/vlbuild/vlbuild/__init__.py +16 -0
  20. package/compiler/vlbuild/vlbuild/__main__.py +243 -0
  21. package/compiler/vlbuild/vlbuild/assets/vlrt.css +165 -0
  22. package/compiler/vlbuild/vlbuild/assets/vlrt.js +1151 -0
  23. package/compiler/vlbuild/vlbuild/emit.py +1839 -0
  24. package/compiler/vlbuild/vlbuild/server_emit.py +1549 -0
  25. package/compiler/vlbuild/vlbuild/style_config.py +347 -0
  26. package/compiler/vlbuild/vlbuild/styling.py +39 -0
  27. package/examples/station.vl +2 -2
  28. package/language/crib.md +51 -4
  29. package/language/spec.md +20 -1
  30. package/language/supported.md +117 -0
  31. package/lib/env.js +107 -0
  32. package/package.json +19 -2
  33. package/scripts/selftest.mjs +54 -0
  34. 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