volaro 0.1.0-alpha.13 → 0.1.0-alpha.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Volaro
2
2
 
3
- **Limited alpha** (this is `0.1.0-alpha.13`), published under both the
3
+ **Limited alpha** (this is `0.1.0-alpha.14`), published under both the
4
4
  `latest` and `alpha` dist-tags: `npm install volaro`.
5
5
 
6
6
  Volaro is an experimental application language intended for AI authoring and
@@ -1,6 +1,6 @@
1
1
  {
2
- "revision": "d6e19b4e656bee01bf90048117fb24b598abdf3b",
2
+ "revision": "e01dc1d92881e38143a73b21dfdf0660cb515906",
3
3
  "source": "git",
4
4
  "dirty": false,
5
- "content_sha256": "64c762006f1fceda3601d6c6b3a6de4ba99590d93aa14d04a749f318a840aa17"
5
+ "content_sha256": "6476761bc8a3508734fee9684b80e0efe0ae151a6a40b5f5586bf1595e3a960d"
6
6
  }
@@ -1 +1 @@
1
- d6e19b4e656bee01bf90048117fb24b598abdf3b
1
+ e01dc1d92881e38143a73b21dfdf0660cb515906
@@ -255,11 +255,14 @@ AUTH_ERROR_CODES = frozenset({
255
255
  "provider_error", "invalid_token", "expired_token", "invalid_code",
256
256
  "no_totp_enrolled",
257
257
  })
258
- # A route body's `catch <head>` tests the thrown error's code when <head> is
259
- # one of these (or a declared union variant); any other head binds the
260
- # caught error itself (`catch e`). resolve.py and vlbuild's `_route_try`
261
- # both read this set, so `err(e)` means the same thing to check and build.
262
- ROUTE_CATCH_CODES = (_ERROR_CODES | AUTH_ERROR_CODES
258
+ # `catch <head>` tests the thrown error's code when <head> is one of these
259
+ # (or a declared union variant), in a route body and in a view handler
260
+ # alike; any other head binds the caught error itself (`catch e`). The code
261
+ # matches when it equals <head> or starts with "<head>." (`catch decode`
262
+ # catches `decode.missing_field`). resolve.py, the W-CATCH-NO-FALLBACK
263
+ # check and vlbuild's `_route_try` / `_handler_try` all read this set, so a
264
+ # catch arm means the same thing to check and build, client and server.
265
+ CATCH_CODES = (_ERROR_CODES | AUTH_ERROR_CODES
263
266
  | frozenset({"bad_request", "unauthorized"}))
264
267
  _ERR_ROUTE_FIX = ("`return err(not_found(\"...\"))` in a `service` or "
265
268
  "page.server route body; a view reports a failure by "
@@ -414,6 +417,28 @@ def _walk(node):
414
417
  yield from _walk(getattr(node, f))
415
418
 
416
419
 
420
+ def _h1_path(nodes) -> list:
421
+ """The `h1` elements one render of `nodes` can show together, in source
422
+ order: siblings add up, while the branches of an `if` / `match` are
423
+ alternatives, so only the branch with the most `h1`s counts. Anything
424
+ else is searched whole, as before (a `list` body counts once)."""
425
+ out: list = []
426
+ for n in nodes or ():
427
+ if isinstance(n, A.Element):
428
+ out += ([n] if n.name == "h1" else []) + _h1_path(n.body)
429
+ elif isinstance(n, A.If):
430
+ branches = ([n.then] + [body for _c, body in n.elifs]
431
+ + [n.orelse])
432
+ out += max((_h1_path(b) for b in branches), key=len)
433
+ elif isinstance(n, A.Match):
434
+ out += max((_h1_path(arm[-1]) for arm in n.arms),
435
+ key=len, default=[])
436
+ else:
437
+ out += [x for x in _walk(n)
438
+ if isinstance(x, A.Element) and x.name == "h1"]
439
+ return out
440
+
441
+
417
442
  def _rendered_children(el):
418
443
  """Yield the element children `el` actually renders, in source order.
419
444
 
@@ -2398,7 +2423,6 @@ class Checker:
2398
2423
  if not isinstance(it, A.ViewDecl):
2399
2424
  continue
2400
2425
  prev = 0
2401
- first_h1 = None
2402
2426
  navs: list = []
2403
2427
  mains: list = []
2404
2428
  # action-45.3: `aside` (role=complementary) has the same problem
@@ -2416,14 +2440,6 @@ class Checker:
2416
2440
  "screen-reader users navigate by level; don't "
2417
2441
  "skip one" % (prev, lvl), node,
2418
2442
  "use h%d here, or restructure the section" % (prev + 1))
2419
- if lvl == 1:
2420
- if first_h1 is not None:
2421
- self._d("W-HEADING-MULTI-H1", Severity.WARNING,
2422
- "a view renders more than one `h1`; a page "
2423
- "usually has exactly one top-level heading",
2424
- node, "make the later one `h2`")
2425
- else:
2426
- first_h1 = node
2427
2443
  prev = lvl
2428
2444
  elif node.name == "nav":
2429
2445
  navs.append(node)
@@ -2432,6 +2448,14 @@ class Checker:
2432
2448
  mains.append(node)
2433
2449
  elif node.name in landmarks:
2434
2450
  landmarks[node.name].append(node)
2451
+ # Branches of an `if` / `match` render one at a time, so a view
2452
+ # with `h1 "Not found"` in one branch and `h1 found.title` in
2453
+ # the other renders one `h1`: count the branch with the most.
2454
+ for node in _h1_path(it.body)[1:]:
2455
+ self._d("W-HEADING-MULTI-H1", Severity.WARNING,
2456
+ "a view renders more than one `h1`; a page "
2457
+ "usually has exactly one top-level heading",
2458
+ node, "make the later one `h2`")
2435
2459
  for lname, nodes in landmarks.items():
2436
2460
  if lname == "nav" or len(nodes) < 2:
2437
2461
  continue # `nav` keeps its own E-NAV-NAME below
@@ -3530,6 +3554,19 @@ class Checker:
3530
3554
  "'?' inside 'try' is redundant -- the catch arms below "
3531
3555
  "already handle the error, so nothing is propagated", t.expr,
3532
3556
  "remove the trailing '?'")
3557
+ heads = [getattr(pat, "head", "") for pat, _body in t.catches]
3558
+ if (self._ctx == "view" and heads
3559
+ and all(h in CATCH_CODES or h in self._variants for h in heads)):
3560
+ # Every arm names a code, so any other error is rethrown out of
3561
+ # the handler. Through 0.1.0-alpha.13 a view handler's
3562
+ # `catch not_found` caught every error; code written against
3563
+ # that now lets the others through. A warning, not an error:
3564
+ # rethrowing can be what the author wants.
3565
+ self._d("W-CATCH-NO-FALLBACK", Severity.WARNING,
3566
+ "every `catch` arm here names an error code, so any other "
3567
+ "error (a 500, a network failure) is rethrown out of the "
3568
+ "handler", t,
3569
+ "add a final `catch e` arm to handle the rest")
3533
3570
  self._expr(t.expr, scopes, fallible)
3534
3571
  for pat, body in t.catches:
3535
3572
  inner = scopes + [_Scope(getattr(pat, "binds", []), "catch binding")]
@@ -50,7 +50,7 @@ import os
50
50
  import re
51
51
 
52
52
  from . import ast_nodes as A
53
- from .checks import (BUILTINS, CONTEXTUAL_WORDS, ROUTE_CATCH_CODES,
53
+ from .checks import (BUILTINS, CONTEXTUAL_WORDS, CATCH_CODES,
54
54
  STD_BUILT_FNS, _ERROR_CODES, _FN_NOT_BUILT, _PRIM_NAMES)
55
55
  from .diagnostics import Diagnostic, LineIndex, Severity
56
56
  from .elements import ELEMENT_NAMES
@@ -1057,7 +1057,7 @@ class Resolver:
1057
1057
  head = getattr(pat, "head", "")
1058
1058
  binds = list(getattr(pat, "binds", []) or [])
1059
1059
  sym = self.table.get(head)
1060
- if (head == "_" or head in ROUTE_CATCH_CODES
1060
+ if (head == "_" or head in CATCH_CODES
1061
1061
  or (sym is not None and sym.kind == "variant")
1062
1062
  or binds != [head]):
1063
1063
  return {b: None for b in binds}
@@ -59,7 +59,7 @@ from vlcheck.elements import (
59
59
  SPELLCHECK_HOMES, TABINDEX_ALLOWED, TEXT_STYLE_ATTRS,
60
60
  UNSAFE_ELEMENTS,
61
61
  )
62
- from vlcheck.checks import (AUTH_ERROR_CODES, ROUTE_CATCH_CODES, STD_BUILT_FNS,
62
+ from vlcheck.checks import (CATCH_CODES, STD_BUILT_FNS,
63
63
  _ERROR_CODES, _lit_int, _lit_text,
64
64
  _rendered_children)
65
65
  from vlcheck.lexer import Lexer
@@ -171,7 +171,16 @@ WHERE_PREDICATES = {
171
171
  "contains": 1,
172
172
  }
173
173
 
174
- _AUTH_ERROR_CODES = AUTH_ERROR_CODES
174
+
175
+
176
+ def _catch_code_test(head: str) -> str:
177
+ """The JS condition a `catch <code>` arm tests (spec section 6.2), in a
178
+ view handler and a route body alike: the caught error's `code` is
179
+ `head` or starts with `head.`, so `catch decode` also catches the
180
+ runtime's `decode.missing_field`."""
181
+ return ('_vlErr && typeof _vlErr.code === "string" && (_vlErr.code === %s'
182
+ ' || _vlErr.code.indexOf(%s) === 0)'
183
+ % (json.dumps(head), json.dumps(head + ".")))
175
184
 
176
185
 
177
186
  def _where_predicate_call(v):
@@ -3167,16 +3176,27 @@ class Emitter:
3167
3176
  return ["var %s = _vlErr;" % self._bind_local(binds[0])]
3168
3177
  return []
3169
3178
 
3179
+ def _is_catch_code(self, head: str) -> bool:
3180
+ """`catch <head>` tests the error's code (an error code, an auth
3181
+ error code or a declared union variant); any other head (`catch
3182
+ e`, `catch _`) catches every error. One rule for view handlers and
3183
+ route bodies: `vlcheck.checks.CATCH_CODES`."""
3184
+ return head in CATCH_CODES or head in self.variant_params
3185
+
3170
3186
  def _handler_try(self, st: A.Try, in_loop: bool = False) -> str:
3171
- """Lower Volaro's non-fall-through handled errors (§6.2)."""
3187
+ """Lower Volaro's non-fall-through handled errors (§6.2). An arm
3188
+ headed by an error code runs for that code only (it was a
3189
+ catch-all for every code but the auth ones and declared variants,
3190
+ so `catch not_found` swallowed every error); an error no arm
3191
+ matches is rethrown."""
3172
3192
  out = ["try { await %s; } catch (_vlErr) {" % self.expr(st.expr)]
3173
3193
  fallback = False
3174
3194
  for i, (pat, body) in enumerate(st.catches):
3175
3195
  head = getattr(pat, "head", "")
3176
- is_variant = head in _AUTH_ERROR_CODES or head in self.variant_params
3196
+ is_variant = self._is_catch_code(head)
3177
3197
  if is_variant:
3178
- out.append("%s (_vlErr && _vlErr.code === %s) {" %
3179
- ("if" if i == 0 else "else if", json.dumps(head)))
3198
+ out.append("%s (%s) {" % ("if" if i == 0 else "else if",
3199
+ _catch_code_test(head)))
3180
3200
  else:
3181
3201
  out.append("{" if i == 0 else "else {")
3182
3202
  fallback = True
@@ -3216,10 +3236,10 @@ class Emitter:
3216
3236
  fallback = False
3217
3237
  for i, (pat, body) in enumerate(try_node.catches):
3218
3238
  head = getattr(pat, "head", "")
3219
- is_variant = head in _AUTH_ERROR_CODES or head in self.variant_params
3239
+ is_variant = self._is_catch_code(head)
3220
3240
  if is_variant:
3221
- out.append("%s (_vlErr && _vlErr.code === %s) {" %
3222
- ("if" if i == 0 else "else if", json.dumps(head)))
3241
+ out.append("%s (%s) {" % ("if" if i == 0 else "else if",
3242
+ _catch_code_test(head)))
3223
3243
  else:
3224
3244
  out.append("{" if i == 0 else "else {")
3225
3245
  fallback = True
@@ -3629,11 +3649,11 @@ class Emitter:
3629
3649
  `catch e` / `return err(e)` example could not be built.
3630
3650
 
3631
3651
  - An arm headed by an error code (`catch not_found`), a declared
3632
- union variant or an auth error code runs when the thrown error has
3633
- that `code`, binding the variant's fields by name. Any other head
3634
- (`catch e`, `catch _`) catches every error and binds it to `e`;
3635
- arms after it are unreachable, as in a view handler. An error no
3636
- arm matches is rethrown.
3652
+ union variant or an auth error code runs when the thrown error's
3653
+ `code` is that head or starts with `head.`, binding the variant's
3654
+ fields by name. Any other head (`catch e`, `catch _`) catches
3655
+ every error and binds it to `e`; arms after it are unreachable,
3656
+ as in a view handler. An error no arm matches is rethrown.
3637
3657
  - Statement form: a handled error does not fall through. The arm
3638
3658
  ends the route: its last expression statement is the route's
3639
3659
  value (like a route body's own last line); otherwise the route
@@ -3643,7 +3663,6 @@ class Emitter:
3643
3663
  the `let` then run as usual.
3644
3664
  - `err(e)` inside an arm rethrows the caught error unchanged
3645
3665
  (`_ex_ServerErr`)."""
3646
- codes = set(ROUTE_CATCH_CODES) | set(self.variant_params)
3647
3666
  call_js = self.expr(t.expr)
3648
3667
  out: list[str] = []
3649
3668
  target = None
@@ -3656,10 +3675,10 @@ class Emitter:
3656
3675
  fallback = False
3657
3676
  for i, (pat, body) in enumerate(t.catches):
3658
3677
  head = getattr(pat, "head", "")
3659
- is_code = head in codes
3678
+ is_code = self._is_catch_code(head)
3660
3679
  if is_code:
3661
- out.append("%s (_vlErr && _vlErr.code === %s) {" %
3662
- ("if" if i == 0 else "else if", json.dumps(head)))
3680
+ out.append("%s (%s) {" % ("if" if i == 0 else "else if",
3681
+ _catch_code_test(head)))
3663
3682
  else:
3664
3683
  out.append("{" if i == 0 else "else {")
3665
3684
  fallback = True
package/language/crib.md CHANGED
@@ -49,6 +49,8 @@ run either way goes *before* the `try`; fire-and-forget uses `spawn`.
49
49
  try auth.sign_in(email: email, password: password)
50
50
  catch invalid_credentials
51
51
  error = "Wrong email or password." # no `return` needed
52
+ catch e
53
+ error = "Something went wrong."
52
54
  route.to("/") # reached only when nothing was caught
53
55
  ```
54
56
  `let v = try EXPR catch ...` above is different: it binds a value (whichever arm
@@ -59,6 +61,12 @@ fall-through bug this rule exists to prevent — put that logic in each `catch`
59
61
  arm (or in an `if`/`else` reading a flag the `catch` set, declared `let mut`)
60
62
  instead.
61
63
 
64
+ `catch not_found` (an error code, an auth code like `invalid_credentials`, or a
65
+ variant you declared) runs for that code only; `catch decode` also catches
66
+ `decode.missing_field`. Any other name (`catch e`) or `catch _` catches
67
+ everything, so it goes last. An error no arm matches is rethrown: in a view
68
+ handler, end the arms with `catch e` or check warns `W-CATCH-NO-FALLBACK`.
69
+
62
70
  `if C then else E` is also an expression (`derive x = if ok "a" else "b"`):
63
71
  `else` **required**, **binary only** (nest for 3+: `if a X else (if b Y else Z)`),
64
72
  each branch **one simple expression**, and the whole `derive` on **one line**
package/language/spec.md CHANGED
@@ -413,7 +413,9 @@ catch e
413
413
  return err(e)
414
414
  ```
415
415
 
416
- `?` requires the enclosing function to be fallible with a compatible error type. `??` takes a value of the success type. `try`/`catch` matches on the error union with the same exhaustiveness rules as `match`.
416
+ `?` requires the enclosing function to be fallible with a compatible error type. `??` takes a value of the success type. `try`/`catch` matches on the error union with the same exhaustiveness rules as `match`; `volaro check` does not enforce them yet.
417
+
418
+ A `catch` arm headed by an error code (`catch not_found`), an auth error code or a declared union variant runs when the error's `code` is that name or starts with that name and a dot, so `catch decode` also catches `decode.missing_field` (§9). Any other head (`catch e`, `catch _`) catches every error, and arms after it never run. An error no arm matches is rethrown, in a view handler and in a route body alike. In a view handler, a `try` whose arms all name codes is the warning `W-CATCH-NO-FALLBACK`, because a 500 or a network failure then leaves the handler: end it with `catch e` to handle the rest.
417
419
 
418
420
  **A handled error does not fall through.** When a `catch` arm runs, the statements after the `try` block do **not** execute — the `try` statement is complete. This is the opposite of the usual imperative reading and it is chosen deliberately: the fall-through version produced a login form that showed an error message *and* redirected to the success page, which was the single most common bug in early Volaro examples.
419
421
 
@@ -1063,7 +1065,7 @@ page title:"Weekly Report"
1063
1065
 
1064
1066
  `title:` is an expression evaluated at module scope (a literal, or something a module-level `fn` / config produces — not view state). It becomes the document `<title>` when it is a literal, and is assigned to `document.title` at boot regardless, so a full navigation to another page picks up that page's own title. `page` with no `title:`, or a statically empty one, is `E-PAGE-TITLE`; a build that renders a page with no `page` declaration at all is refused (`E-PAGE-TITLE` at build time). A `title:` expression that resolves to empty at load time raises `render.empty_page_title`. One `page` per module (`E-PAGE-DUP`).
1065
1067
 
1066
- **Headings.** `h1`…`h6` are `std.ui` primitives that emit the real HTML heading elements. The level is **semantic and independent of visual size** — `h2 "…" size:28` is a second-level heading that happens to be large. Within one view, a heading that jumps more than one level below the previous one is `W-HEADING-SKIP`, and a second `h1` is `W-HEADING-MULTI-H1` — warnings, not errors, and scoped to a single view so a reusable component that legitimately starts at `h2` or `h3` under some page's `h1` is never flagged.
1068
+ **Headings.** `h1`…`h6` are `std.ui` primitives that emit the real HTML heading elements. The level is **semantic and independent of visual size** — `h2 "…" size:28` is a second-level heading that happens to be large. Within one view, a heading that jumps more than one level below the previous one is `W-HEADING-SKIP`, and a second `h1` the view can render at the same time is `W-HEADING-MULTI-H1` (one `h1` in each branch of an `if` is one `h1`) — warnings, not errors, and scoped to a single view so a reusable component that legitimately starts at `h2` or `h3` under some page's `h1` is never flagged.
1067
1069
 
1068
1070
  **Landmarks.** `main` and `nav` are `std.ui` primitives emitting `<main>` / `<nav>`. There is one `main` region per page (`E-MAIN-DUP` for two in a view). A `nav` takes `aria_label:` (emitted as `aria-label`); when a view has more than one `nav`, each **must** carry a distinct name (`E-NAV-NAME`) so assistive tech can tell them apart. These rules hold on the **composed** document, not just within one view: a `main` in the page-root view and in a view it renders (or a view whose `main` is reused) is `E-MAIN-COMPOSED` — every `main` emits `id="vl-main"`, so a second one is also a duplicate id and an ambiguous skip target; two `<nav>`s assistive tech cannot tell apart — both unnamed, or sharing one `aria_label:` — from different composed views are likewise `E-NAV-NAME`; and one static `id:` rendered by more than one element is `E-ID-DUP`. The composed pass follows component instantiation within a module from the page root and treats `if` / `else` branches as mutually exclusive (a landmark in both branches counts once); it does not evaluate conditions, and cross-file `use` composition is not yet followed.
1069
1071
 
@@ -472,6 +472,13 @@ audio video source track picture map area
472
472
  prop or field (`on_retry()`) are not value methods and are unaffected.
473
473
  - `try call()?` is a warning (`W-TRY-PROPAGATE`): the `catch` arms already
474
474
  handle the error, so the `?` does nothing. Drop it.
475
+ - `catch not_found` in a view handler (an error code, an auth error code
476
+ or a declared variant) runs for that code only, the same as in a route
477
+ body; `catch decode` also catches `decode.missing_field`. `catch e` /
478
+ `catch _` catches anything. An error no arm matches is rethrown. Through
479
+ 0.1.0-alpha.13, `catch not_found` and the other standard codes caught
480
+ every error in a view handler. A handler `try` whose arms all name codes
481
+ is a warning (`W-CATCH-NO-FALLBACK`): add a final `catch e`.
475
482
 
476
483
  ### Data / services
477
484
  - `service` with `db.<model>` query builder (`where` / `order` / `limit` /
@@ -484,8 +491,9 @@ audio video source track picture map area
484
491
  value. Any other statement (`match` as a statement, `par`, `spawn`) fails
485
492
  the build, at any nesting depth. Use `?` to propagate errors.
486
493
  - `try`/`catch` in a route body, both forms. `catch not_found` (an error
487
- code, or a variant of a union you declare) runs for that code only and
488
- binds the variant's fields; `catch e` catches anything. An error no arm
494
+ code, or a variant of a union you declare) runs for that code (or a
495
+ `not_found.` code) only and binds the variant's fields; `catch e`
496
+ catches anything. An error no arm
489
497
  matches propagates. The bare statement form does not fall through: the
490
498
  arm ends the route, and its last expression is the route's value.
491
499
  `let x = try ...` gives `x` the arm's last expression and carries on.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volaro",
3
- "version": "0.1.0-alpha.13",
3
+ "version": "0.1.0-alpha.14",
4
4
  "description": "Volaro \u2014 an application language written to be authored by an AI agent and read by a person. Ships the language reference and a working compiler for a supported subset (volaro check / build / dev). CLI: `volaro` (alias `vl`).",
5
5
  "repository": {
6
6
  "type": "git",