volaro 0.1.0-alpha.11 → 0.1.0-alpha.12

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.11`), published under both the
3
+ **Limited alpha** (this is `0.1.0-alpha.12`), 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": "5f9c32d19082250fc91ab3664ae6726aa2cfd0a0",
2
+ "revision": "92972bae88b0cd71b621a8c21dbeae052dc8a3c9",
3
3
  "source": "git",
4
4
  "dirty": false,
5
- "content_sha256": "53474ecb82ebaf7ff2861f647b7796dc9c84cce73d7061ee64a65c90365a80ae"
5
+ "content_sha256": "09ae1f5782b061471ba112a8820cd9a08f4519ecff84eb418a5062f8ed23c467"
6
6
  }
@@ -1 +1 @@
1
- 5f9c32d19082250fc91ab3664ae6726aa2cfd0a0
1
+ 92972bae88b0cd71b621a8c21dbeae052dc8a3c9
@@ -124,64 +124,186 @@ _METHOD_OTHER_FIX = {
124
124
  # Names a page-server / service body has in scope that are ambient objects,
125
125
  # not user values.
126
126
  _AMBIENT_RECEIVERS = {"session", "request"}
127
- # Free functions the spec shows but no emitter lowers as a bare call (not in
128
- # vlbuild's STD_FNS). Only an error where the build emits the call -- a view
129
- # or a service route; a module-level `fn` body is never emitted into either.
127
+ # The std free-function surface, in one place. Every std function name the
128
+ # checker knows is in exactly one of the two tables below:
129
+ #
130
+ # STD_BUILT_FNS -- vlbuild lowers `name(...)` to `VL.std.name(...)`.
131
+ # vlbuild's `emit.STD_FNS` IS this set (imported, not
132
+ # copied), and `VL.std` in `assets/vlrt.js` defines each
133
+ # one; server.js embeds that same `VL.std` block, so a
134
+ # route body gets the identical function.
135
+ # _FN_NOT_BUILT -- a name the spec, the corpus or an agent uses that this
136
+ # build has no function for. A bare call where the build
137
+ # emits it (a view or a service route) is E-FN-NOT-BUILT
138
+ # with this fix line; a module-level `fn` body is never
139
+ # emitted into either, so it is not judged there.
140
+ #
141
+ # resolve.py's known-name list is derived from these two, so a name cannot be
142
+ # "known" without being classified. `test_std_fn_parity` in vlbuild's
143
+ # tests/test_build.py calls every known name through the real checker and
144
+ # requires each accepted one to exist in the emitter, `vlrt.js`, its minified
145
+ # copy and server.js. This replaced five hand-synced tables: `unique` was
146
+ # known to the resolver, missing from the other four, and built to a bare
147
+ # `unique(...)` that crashed every page (Testing Lab search brief, Example
148
+ # Demo 2.3).
149
+ STD_BUILT_FNS = frozenset({
150
+ # std.list
151
+ "filter", "map", "take", "count", "sum", "sort_by", "group_by", "chunk",
152
+ # list growth (spec section 20.9): the only way to combine or grow a list
153
+ "concat", "append",
154
+ # `unique(xs)`: first occurrence of each distinct value, in order
155
+ "unique",
156
+ # std.str (spec section 13: split, join, trim, case, replace, contains,
157
+ # regex -- `matches`)
158
+ "join", "split", "first_char", "trim", "upper", "lower", "replace",
159
+ "contains", "matches",
160
+ # std.map
161
+ "keys", "values", "get_or",
162
+ # numeric conversion
163
+ "int_to_float", "float_to_int",
164
+ })
165
+
166
+ _FOLD_FIX = "a `for` loop over a `let mut` accumulator"
130
167
  _FN_NOT_BUILT = {
131
- "sorted": "sort_by(xs, fn(x) x)", "first": "xs[0] (check xs.len() > 0 first)",
132
- "fold": "a `for` loop over a `let mut` accumulator",
168
+ # in the spec (section 13 or its examples), not built
169
+ "fold": _FOLD_FIX,
133
170
  "merge": "a record literal naming every field",
134
171
  "to_int": "no string-to-int conversion is built yet; keep the value an int",
172
+ "now": ("a `model` field default (`created_at time = now()`); a view or "
173
+ "route cannot read the clock in this build"),
174
+ # names the checker has accepted, not built
175
+ "sorted": "sort_by(xs, fn(x) x)",
176
+ "first": "xs[0] (check xs.len() > 0 first)",
177
+ "reduce": _FOLD_FIX,
178
+ "flatten": _FOLD_FIX + " and `concat(acc, x)`",
179
+ "reverse": _FOLD_FIX + " and `concat([x], acc)`",
180
+ "zip": "a `for i in 0..xs.len()` loop reading `xs[i]` and `ys[i]`",
181
+ "enumerate": "a `for i in 0..xs.len()` loop reading `xs[i]`",
182
+ "index_of": "a `for i in 0..xs.len()` loop that compares `xs[i]`",
183
+ "slice": "take(xs, n) for a prefix, or a `for i in a..b` loop",
184
+ "range": "a `for i in 0..n` loop",
185
+ "any": "count(filter(xs, f)) > 0",
186
+ "all": "count(filter(xs, f)) == count(xs)",
187
+ "none": "count(filter(xs, f)) == 0",
188
+ "avg": "sum(xs) / count(xs) (check count(xs) > 0 first)",
189
+ "min": "if (a < b) a else b",
190
+ "max": "if (a > b) a else b",
191
+ "abs": "if (x < 0) -x else x",
192
+ "round": "float_to_int(x + 0.5) for a non-negative x",
193
+ "floor": "float_to_int(x) for a non-negative x",
194
+ "ceil": "float_to_int(x) + 1 when x has a fraction, for a non-negative x",
195
+ "starts_with": "matches(s, \"^prefix\")",
196
+ "ends_with": "matches(s, \"suffix$\")",
197
+ "pad_start": "string interpolation: `\"{pad}{s}\"`",
198
+ "pad_end": "string interpolation: `\"{s}{pad}\"`",
199
+ "repeat": _FOLD_FIX + " building the string",
200
+ "uuid4": "let the database assign the id (`id uuid pk`)",
201
+ "random": "no random source is built yet",
135
202
  }
136
-
137
- # `use std.X { name, ... }` names `vlbuild` actually lowers to a working call
138
- # -- NOT the same list as `BUILTINS` above (that one is a generous, deliberately
139
- # loose allowlist for the W-UNDEF-NAME heuristic, and includes several names,
140
- # e.g. `merge`/`fold`/`first`/`to_int`, the spec shows but this build's emitter
141
- # has no lowering for). Duplicated from `vlbuild/emit.py`'s `STD_FNS` (plus
142
- # `now`, whose one working lowering — a `model` field default — lives in
143
- # `server_emit.py`'s `_sql_default`) rather than imported: `vlcheck` has no
144
- # dependency on `vlbuild` (the dependency runs the other way), so the two
145
- # packages cannot share this table directly. Keep in sync by hand; a mismatch
146
- # only makes this check too strict or too loose, never silently wrong, since
147
- # `vlbuild` is the actual authority on what it lowers.
203
+ assert not (STD_BUILT_FNS & set(_FN_NOT_BUILT))
204
+ BUILTINS.update(STD_BUILT_FNS, _FN_NOT_BUILT)
205
+ # The names `use std.X { ... }` may import: every built function, plus `now`,
206
+ # whose one working lowering -- a `model` field default -- is server_emit.py's
207
+ # `_sql_default`.
148
208
  #
149
209
  # Found reproducing the login-page benchmark's Bug 3 (`test-only`
150
- # `login-page/volaro/NOTES.md`): `use std.http { post }` passes `vlcheck`
151
- # clean and compiles to a bare, undefined `post(...)` call in the emitted JS
152
- # (`emit.py`'s `_ex_Call` has no special case for an unrecognized `A.Name`
153
- # callee, so it falls through to emitting the call verbatim) -- a silent
154
- # ReferenceError at runtime, not a build-time diagnostic. `std.http` has no
155
- # member in either list below; nothing under it can pass this check.
156
- _STD_SUPPORTED_NAMES = {
157
- "filter", "map", "take", "count", "sum", "sort_by", "group_by", "chunk",
158
- "join", "split", "first_char", "int_to_float", "float_to_int",
159
- "trim", "upper", "lower", "keys", "values", "get_or", "concat", "append",
160
- "now",
161
- # std.str's `regex` (spec section 13): `matches(s, pattern)`, added
162
- # alongside its `vlbuild/emit.py` `STD_FNS` entry and `VL.std.matches`
163
- # (assets/vlrt.js) -- keep all three in sync by hand.
164
- "matches",
165
- # std.str's `replace` (spec section 13's own std.str row already names
166
- # it, alongside split/join/trim/case/regex, as part of the documented
167
- # surface -- this build just had not implemented it yet). Added for
168
- # feature 17 (export): CSV field escaping (RFC 4180 -- double an
169
- # embedded '"') needs a literal substring replace-all; reproduced
170
- # missing first (`use std.str { replace }` failed vlcheck with
171
- # E-UNKNOWN-STD-IMPORT before this entry existed). Replace-ALL,
172
- # literal-substring semantics (not regex, not first-occurrence-only) --
173
- # keep this entry, `vlbuild/emit.py`'s `STD_FNS`, `assets/vlrt.js`'s
174
- # `VL.std.replace`, and `server_emit.py`'s server-side `VL.std.replace`
175
- # shim in sync by hand, same as `matches`/`split` above.
176
- "replace",
177
- # std.str's `contains(s, part) bool` (case-sensitive substring test).
178
- # Also a `where()` predicate name (BUILTINS above); vlbuild lowers a
179
- # `where(col: contains(x))` value as the predicate and every other call
180
- # as `VL.std.contains`. Keep in sync with `vlbuild/emit.py`'s `STD_FNS`.
181
- "contains",
210
+ # `login-page/volaro/NOTES.md`): `use std.http { post }` passed `vlcheck`
211
+ # clean and compiled to a bare, undefined `post(...)` call. `std.http` has no
212
+ # member here; nothing under it can pass this check.
213
+ _STD_SUPPORTED_NAMES = STD_BUILT_FNS | {"now"}
214
+
215
+ # Bare words the spec uses in argument position (`.order(created_at: desc)`,
216
+ # `auth.start_oauth(google)`). They resolve as names; resolve.py reads this
217
+ # set.
218
+ CONTEXTUAL_WORDS = frozenset({"asc", "desc", "google", "github"})
219
+
220
+ # Every other name the checker knows is not a function the build lowers, so
221
+ # a bare call to one compiles to an undefined (or non-function) JavaScript
222
+ # callee and throws when the page or route runs. Each is classified here by
223
+ # the one place it may be called, if any; `_check_builtin_call` rejects the
224
+ # call everywhere else in emitted code. `test_bare_call_parity` (vlbuild
225
+ # tests/test_build.py, gate 2) calls every known name in every emitted
226
+ # context and requires the checker to reject it or the build to lower it,
227
+ # and `_unclassified_names()` must stay empty, so a name added to BUILTINS
228
+ # cannot slip through unjudged. (Testing Lab, Example Demo 2.3: `unique`
229
+ # was the std case; these are the rest of the class.)
230
+ #
231
+ # `where()` predicates (spec section 13.1): a predicate is only a
232
+ # `.where(field: pred(x))` value (or a branch of an `if` that is one). The
233
+ # fix is the operator that tests the same thing on a value. `contains` is
234
+ # also a std.str function, so it is built everywhere.
235
+ _WHERE_PREDICATE_FIX = {
236
+ "lt": "`a < b`", "lte": "`a <= b`", "gt": "`a > b`", "gte": "`a >= b`",
237
+ "ne": "`a != b`", "between": "`lo <= x and x <= hi`",
238
+ "any_of": "`count(filter(xs, fn(v) v == x)) > 0`",
239
+ }
240
+ # Error codes (spec section 6): a code is constructed only as the argument of
241
+ # `err(...)` in a service or page-server route body, where the build turns
242
+ # it into the route's error response. `catch not_found` and a match arm name
243
+ # a code without calling it.
244
+ _ERROR_CODES = frozenset({
245
+ "not_found", "forbidden", "conflict", "rejected", "unexpected",
246
+ "out_of_range", "decode", "invalid_credentials", "unverified_email",
247
+ "totp_required", "rate_limited",
248
+ })
249
+ _ERR_ROUTE_FIX = ("`return err(not_found(\"...\"))` in a `service` or "
250
+ "page.server route body; a view reports a failure by "
251
+ "setting its own state")
252
+ # Names that are never called: namespaces and ambient objects (call a member
253
+ # instead), type names (no conversion function by that name), contextual
254
+ # words, and names the spec's examples use that no build provides.
255
+ _NO_BUILD_NAMESPACE = "no `%s` namespace is built yet"
256
+ _NOT_CALLABLE = {
257
+ # namespaces / ambient objects
258
+ "env": "read a member: `env.NAME`",
259
+ "session": "read a member: `session.user`",
260
+ "request": "read a member: `request.headers`",
261
+ "route": "`route.to(\"/path\")`",
262
+ "auth": "a section 11.3 call: `auth.sign_in(email: e, password: p)`",
263
+ "db": "a query: `db.posts.where(...).all()?`",
264
+ "log": "`log.info(\"message\", value)`",
265
+ "api": "an `api` block endpoint: `client.endpoint(...)`",
266
+ "analytics": _NO_BUILD_NAMESPACE % "analytics",
267
+ "events": _NO_BUILD_NAMESPACE % "events",
268
+ "ws": _NO_BUILD_NAMESPACE % "ws",
269
+ "mailer": _NO_BUILD_NAMESPACE % "mailer",
270
+ "billing": _NO_BUILD_NAMESPACE % "billing",
271
+ # result constructor: a `T!` success is the plain value (spec section 6)
272
+ "ok": "return the value itself; a successful `T!` is the plain value",
273
+ # names from the spec's examples, not built-in functions
274
+ "create_guest": "no `create_guest` is built in; build the value inline",
275
+ "guest_user": "no `guest_user` is built in; build the value inline",
276
+ # contextual words
277
+ "asc": "an `.order(field: asc)` value, not a call",
278
+ "desc": "an `.order(field: desc)` value, not a call",
279
+ "google": "an `auth.start_oauth(google)` argument, not a call",
280
+ "github": "an `auth.start_oauth(github)` argument, not a call",
281
+ # type names
282
+ "str": "string interpolation: `\"{x}\"`",
283
+ "int": "float_to_int(x)",
284
+ "float": "int_to_float(x)",
285
+ "bool": "a comparison: `x != 0`, `s != \"\"`",
286
+ "bytes": "no conversion to `bytes` is built",
287
+ "time": "a `time` field value; no conversion to `time` is built",
288
+ "dur": "no conversion to `dur` is built",
289
+ "uuid": "let the database assign the id (`id uuid pk`)",
290
+ "json": "no conversion to `json` is built",
291
+ "nil": "the value `nil`, not a call",
182
292
  }
183
293
 
184
294
 
295
+ def _unclassified_names() -> set[str]:
296
+ """Known names with no bare-call rule: must stay empty (asserted below
297
+ and by test_bare_call_parity)."""
298
+ classified = (set(STD_BUILT_FNS) | set(_FN_NOT_BUILT) | set(_METHOD_ONLY)
299
+ | set(_WHERE_PREDICATE_FIX) | _ERROR_CODES | {"err"}
300
+ | set(_NOT_CALLABLE) | set(ELEMENT_NAMES))
301
+ return (set(BUILTINS) | set(_PRIM_NAMES) | CONTEXTUAL_WORDS) - classified
302
+
303
+
304
+ assert not _unclassified_names(), _unclassified_names()
305
+
306
+
185
307
  def _lit_text(v):
186
308
  """The literal text of a plain string literal, or None when `v` is not one
187
309
  (a bare word, an expression, or an *interpolated* string -- whose runtime
@@ -324,6 +446,17 @@ class Checker:
324
446
  # (module-level fn bodies)
325
447
  self._module_names: set[str] = set()
326
448
  self._emitted = True
449
+ # which emitted code is being walked: "view" (a view's state,
450
+ # derives, render tree and handlers) or "route" (a service route,
451
+ # a page.server handler, a _server.vl provider). `err(...)` builds
452
+ # only in a route.
453
+ self._ctx = "route"
454
+ # payload-carrying union variants this module declares, and the
455
+ # calls whose position makes them legal (a `where()` predicate
456
+ # value, the error constructor inside `err(...)`)
457
+ self._variants: set[str] = set()
458
+ self._views: set[str] = set()
459
+ self._exempt_calls: set[int] = set()
327
460
  # names of `fn`-typed record fields in this module: `x.on_pick(id)`
328
461
  # calls one, which E-METHOD-UNKNOWN must not report
329
462
  self._fn_fields: set[str] = set()
@@ -2973,10 +3106,15 @@ class Checker:
2973
3106
  if nm:
2974
3107
  top.add(nm)
2975
3108
  self._module_names.add(nm)
3109
+ if isinstance(it, A.ViewDecl):
3110
+ self._views.add(nm)
2976
3111
  if isinstance(it, A.Use):
2977
3112
  for n in it.names:
2978
3113
  top.add(n)
2979
- self._module_names.add(n)
3114
+ # `use std.time { now }` names the std function, not an
3115
+ # author's own: its call is still judged below
3116
+ if not it.path.startswith("std."):
3117
+ self._module_names.add(n)
2980
3118
  base = it.path.replace("./", "").replace("../", "")
2981
3119
  base = base.split("/")[-1].split(".")[-1]
2982
3120
  if base:
@@ -2987,6 +3125,7 @@ class Checker:
2987
3125
  elif isinstance(it, A.TypeDecl) and it.kind == "union":
2988
3126
  for v in it.variants:
2989
3127
  top.add(v.name)
3128
+ self._variants.add(v.name)
2990
3129
  elif isinstance(it, (A.TypeDecl, A.ModelDecl)):
2991
3130
  pass
2992
3131
 
@@ -3006,7 +3145,9 @@ class Checker:
3006
3145
  scope.bind(s.name, "derive")
3007
3146
  elif isinstance(s, A.LoadDecl):
3008
3147
  scope.bind(s.name, "load result")
3148
+ self._ctx = "view"
3009
3149
  self._scoped(it.body, [top, scope], fallible=True)
3150
+ self._ctx = "route"
3010
3151
  elif isinstance(it, A.ServiceDecl):
3011
3152
  for r in it.routes:
3012
3153
  scope = _Scope((p.name for p in r.params), "route parameter")
@@ -3121,23 +3262,100 @@ class Checker:
3121
3262
  def _check_builtin_call(self, call, scopes: list[set]) -> None:
3122
3263
  """A bare call to an allowlisted name the build cannot lower compiles
3123
3264
  to an undefined JS function (action-48). Skipped when the name is the
3124
- author's own: a local binding or a module declaration or import."""
3265
+ author's own: a local binding or a module declaration or a
3266
+ non-`std` import."""
3125
3267
  callee = call.callee
3126
- if not isinstance(callee, A.Name):
3268
+ if not isinstance(callee, A.Name) or id(call) in self._exempt_calls:
3127
3269
  return
3128
3270
  nm = callee.id
3129
- if nm in self._module_names or any(nm in s for s in scopes[1:]):
3271
+ if nm in STD_BUILT_FNS:
3272
+ return
3273
+ # `session` / `request` are bound in a view's or handler's scope, but
3274
+ # they are the ambient objects, never the author's own function
3275
+ ambient = nm in _AMBIENT_RECEIVERS
3276
+ throws = "compiles to a call the built page or route cannot make, and throws when it runs"
3277
+ if (nm in self._views and self._emitted
3278
+ and not any(nm in s for s in scopes[1:])):
3279
+ # a view is rendered by a line of its own (an element); as a
3280
+ # value, `Row(1)` calls the emitted render function with no
3281
+ # context and throws
3282
+ self._d("E-NOT-A-FUNCTION", Severity.ERROR,
3283
+ "'%s' is a view, not a function -- as a value `%s(...)` %s"
3284
+ % (nm, nm, throws), call,
3285
+ "render it as its own line in the view body: `%s(...)`" % nm)
3286
+ return
3287
+ if not ambient and (nm in self._module_names
3288
+ or any(nm in s for s in scopes[1:])):
3130
3289
  return
3131
3290
  if nm in _METHOD_ONLY:
3132
3291
  self._d("E-METHOD-AS-FN", Severity.ERROR,
3133
3292
  "'%s' is a method, not a function -- `%s(...)` compiles to "
3134
3293
  "an undefined function and throws when the page runs"
3135
3294
  % (nm, nm), call, "call it on the value: `%s`" % _METHOD_ONLY[nm])
3136
- elif nm in _FN_NOT_BUILT and self._emitted:
3295
+ elif not self._emitted:
3296
+ # a module-level `fn` body: the build refuses module functions
3297
+ # (E-VIEW-MODULE-FN), so nothing here is emitted
3298
+ return
3299
+ elif nm in _FN_NOT_BUILT:
3137
3300
  self._d("E-FN-NOT-BUILT", Severity.ERROR,
3138
- "'%s' is in the spec but this build has no working `%s` -- "
3139
- "the call compiles and throws when it runs" % (nm, nm),
3301
+ "'%s' is a std name this build of Volaro has no function "
3302
+ "for -- the call compiles to an undefined function and "
3303
+ "throws when it runs" % nm,
3140
3304
  call, "use %s" % _FN_NOT_BUILT[nm])
3305
+ elif nm == "err":
3306
+ if self._ctx == "view":
3307
+ self._d("E-ERR-CONTEXT", Severity.ERROR,
3308
+ "`err(...)` fails a route; a view has no route to fail "
3309
+ "-- in a view it %s" % throws, call,
3310
+ "use " + _ERR_ROUTE_FIX)
3311
+ elif nm in _ERROR_CODES or nm in self._variants:
3312
+ what = ("the error code '%s'" % nm if nm in _ERROR_CODES
3313
+ and nm not in self._variants
3314
+ else "the union variant '%s'" % nm)
3315
+ self._d("E-ERR-CONTEXT", Severity.ERROR,
3316
+ "%s is built only as the argument of `err(...)` in a route "
3317
+ "body -- here `%s(...)` %s" % (what, nm, throws), call,
3318
+ "use " + _ERR_ROUTE_FIX + ("" if nm in _ERROR_CODES else
3319
+ "; for data, a record type with the same fields"))
3320
+ elif nm in _WHERE_PREDICATE_FIX:
3321
+ self._d("E-WHERE-PREDICATE", Severity.ERROR,
3322
+ "'%s' is a `where()` predicate (spec section 13.1), built "
3323
+ "only as a `.where(field: %s(...))` value -- here it %s"
3324
+ % (nm, nm, throws), call,
3325
+ "compare the values with %s, or move it into "
3326
+ "`.where(field: %s(x))`" % (_WHERE_PREDICATE_FIX[nm], nm))
3327
+ elif nm in _NOT_CALLABLE:
3328
+ self._d("E-NOT-A-FUNCTION", Severity.ERROR,
3329
+ "'%s' is not a function -- `%s(...)` %s" % (nm, nm, throws),
3330
+ call, "use %s" % _NOT_CALLABLE[nm])
3331
+ elif nm in ELEMENT_NAMES:
3332
+ self._d("E-NOT-A-FUNCTION", Severity.ERROR,
3333
+ "'%s' is an element, not a function -- as a value "
3334
+ "`%s(...)` %s" % (nm, nm, throws), call,
3335
+ "write the element as its own line in the view body: `%s ...`"
3336
+ % nm)
3337
+
3338
+ def _mark_positional_calls(self, call) -> None:
3339
+ """Exempt the calls whose position gives them meaning: a predicate as
3340
+ a `.where(field: ...)` value (either branch of an `if` value too), and
3341
+ the error constructor that is the argument of `err(...)`."""
3342
+ callee = call.callee
3343
+ if isinstance(callee, A.Member) and callee.name == "where":
3344
+ stack = [v for _, v in call.named]
3345
+ while stack:
3346
+ v = stack.pop()
3347
+ if isinstance(v, A.IfExpr):
3348
+ stack += [v.then, v.orelse]
3349
+ elif (isinstance(v, A.Call) and isinstance(v.callee, A.Name)
3350
+ and v.callee.id in _WHERE_PREDICATE_FIX):
3351
+ self._exempt_calls.add(id(v))
3352
+ elif isinstance(callee, A.Name) and callee.id == "err":
3353
+ # in a view the `err(...)` itself is the one error reported
3354
+ for a in call.args:
3355
+ if (isinstance(a, A.Call) and isinstance(a.callee, A.Name)
3356
+ and (a.callee.id in _ERROR_CODES
3357
+ or a.callee.id in self._variants)):
3358
+ self._exempt_calls.add(id(a))
3141
3359
 
3142
3360
  @staticmethod
3143
3361
  def _value_receiver(obj, scopes: list[set]) -> bool:
@@ -3331,6 +3549,7 @@ class Checker:
3331
3549
  self._expr(ex.obj, scopes, fallible)
3332
3550
  self._expr(ex.index, scopes, fallible)
3333
3551
  elif isinstance(ex, A.Call):
3552
+ self._mark_positional_calls(ex)
3334
3553
  self._check_builtin_call(ex, scopes)
3335
3554
  self._check_value_method(ex, scopes)
3336
3555
  callee = ex.callee
@@ -50,30 +50,25 @@ import os
50
50
  import re
51
51
 
52
52
  from . import ast_nodes as A
53
- from .checks import BUILTINS, _PRIM_NAMES
53
+ from .checks import (BUILTINS, CONTEXTUAL_WORDS, STD_BUILT_FNS, _FN_NOT_BUILT,
54
+ _PRIM_NAMES)
54
55
  from .diagnostics import Diagnostic, LineIndex, Severity
55
56
  from .elements import ELEMENT_NAMES
56
57
  from .lexer import Lexer
57
58
  from .parser import Parser
58
59
 
59
- # std.list / std.map / std math names referenced bare in the spec + corpus.
60
- # Names only — no arity is checked for these (no signature to check against).
61
- _STD_EXTRA = {
62
- "sum", "count", "min", "max", "abs", "round", "floor", "ceil", "avg",
63
- "reverse", "sorted", "unique", "flatten", "concat", "zip", "enumerate",
64
- "range", "any", "all", "none", "contains", "index_of", "reduce", "slice",
65
- "float_to_int", "trim", "upper", "lower", "starts_with", "ends_with",
66
- "replace", "pad_start", "pad_end", "repeat", "now", "uuid4", "random",
67
- "matches",
68
- }
60
+ # std free-function names: the ones this build lowers and the ones it rejects
61
+ # with E-FN-NOT-BUILT (checks.py owns both tables, so a name can only become
62
+ # known here by being classified there). Names only -- no arity is checked.
63
+ _STD_EXTRA = set(STD_BUILT_FNS) | set(_FN_NOT_BUILT)
69
64
  # std.list / std.str functions whose result is a list (E-LIST-OPERATOR);
70
65
  # only when the name is not the module's own declaration or a local binding
71
66
  _LIST_STD_FNS = {"concat", "append", "filter", "map", "take", "sort_by",
72
- "chunk", "keys", "values", "split"}
67
+ "chunk", "keys", "values", "split", "unique"}
73
68
  _ARITH = {"+", "-", "*", "/", "%"}
74
69
  _COMPOUND_ARITH = {"+=", "-=", "*=", "/="}
75
70
  # bare-word identifiers the spec uses in argument position (not bindings)
76
- _CONTEXTUAL = {"asc", "desc", "google", "github"}
71
+ _CONTEXTUAL = set(CONTEXTUAL_WORDS)
77
72
  # Spec §3.4: a component's case carries meaning.
78
73
  # lowercase -> a std.ui *primitive element*, exactly this set (also in
79
74
  # `checks.BUILTINS`, which is how a bare lowercase element resolves);
@@ -78,7 +78,29 @@
78
78
  };
79
79
 
80
80
  // -- std helpers referenced by transpiled `derive` / pipelines ------------
81
+ // @vl-std-begin
82
+ // server_emit.py copies the lines between the two marker lines into
83
+ // server.js, so a route body runs these same functions.
84
+ // Keep the block self-contained: JavaScript built-ins and names defined
85
+ // inside it only. Every name in vlcheck's STD_BUILT_FNS must be a member
86
+ // of VL.std (test_std_fn_parity in tests/test_build.py checks this file,
87
+ // the minified copy and server.js).
81
88
  function asArray(x) { return x == null ? [] : x; }
89
+ // Volaro equality for `unique`: scalars as `==` compares them (`===`), and
90
+ // records and lists structurally, equal when their fields or items are
91
+ // (spec section 4.3: no identity or reference semantics).
92
+ function stdEq(a, b) {
93
+ if (a === b) return true;
94
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object"
95
+ || Array.isArray(a) !== Array.isArray(b)) return false;
96
+ var ka = Object.keys(a);
97
+ if (ka.length !== Object.keys(b).length) return false;
98
+ for (var i = 0; i < ka.length; i++) {
99
+ if (!Object.prototype.hasOwnProperty.call(b, ka[i])
100
+ || !stdEq(a[ka[i]], b[ka[i]])) return false;
101
+ }
102
+ return true;
103
+ }
82
104
  VL.std = {
83
105
  filter: function (xs, f) { return asArray(xs).filter(function (x) { return f(x); }); },
84
106
  map: function (xs, f) { return asArray(xs).map(function (x) { return f(x); }); },
@@ -137,6 +159,24 @@
137
159
  // `state` reassignment style elsewhere (`title = ""`, not a mutating call).
138
160
  concat: function (a, b) { return asArray(a).concat(asArray(b)); },
139
161
  append: function (xs, x) { return asArray(xs).concat([x]); },
162
+ // `unique(xs)`: the first occurrence of each distinct value, in list
163
+ // order, as a new list. Scalars go through a Set (same verdicts as `===`,
164
+ // except NaN, which never equals itself and so is always kept); records
165
+ // and lists are compared structurally against the ones already kept.
166
+ unique: function (xs) {
167
+ var out = [], seen = new Set(), objs = [];
168
+ asArray(xs).forEach(function (x) {
169
+ if (x !== null && typeof x === "object") {
170
+ for (var i = 0; i < objs.length; i++) if (stdEq(objs[i], x)) return;
171
+ objs.push(x);
172
+ } else if (x === x) {
173
+ if (seen.has(x)) return;
174
+ seen.add(x);
175
+ }
176
+ out.push(x);
177
+ });
178
+ return out;
179
+ },
140
180
  // std.str's `regex` (spec section 13's stdlib table) as a whole-purpose
141
181
  // string-vs-pattern test -- the general capability the reconstructed
142
182
  // `.split()`/`.len()`/indexing email-shape check (found needed by a
@@ -155,6 +195,7 @@
155
195
  catch (e) { return false; }
156
196
  }
157
197
  };
198
+ // @vl-std-end
158
199
  VL.len = function (x) { return x == null ? 0 : x.length; };
159
200
 
160
201
  // A failed T! carries a stable `code` (spec section 6.3).
@@ -1,2 +1,2 @@
1
1
  /* minified vlrt.js; readable source in the volaro package */
2
- "use strict";!function(e){var t={},n=null;function r(e){return null==e?[]:e}t.fetchData=function(){return n||(n=e.fetch("data.json",{cache:"no-store"}).then(function(e){if(!e.ok)throw new Error("HTTP "+e.status+" loading data.json");return e.json()})),n},t.db={},t.qb=function(e){var t=(e||[]).slice(),n={where:function(e){var r={lt:function(e,t){return e<t},lte:function(e,t){return e<=t},gt:function(e,t){return e>t},gte:function(e,t){return e>=t},ne:function(e,t){return e!==t}};return Object.keys(e||{}).forEach(function(n){var i=e[n];if(null!=i&&""!==i)if(i&&"object"==typeof i&&"string"==typeof i.op)if("between"===i.op)t=t.filter(function(e){return e[n]>=i.v[0]&&e[n]<=i.v[1]});else if("any_of"===i.op){var o=i.v||[];t=t.filter(function(e){return-1!==o.indexOf(e[n])})}else{var a=r[i.op];t=t.filter(function(e){return a(e[n],i.v)})}else t=t.filter(function(e){return e[n]===i})}),n},order:function(e){var r=Object.keys(e||{})[0];if(r){var i="desc"===e[r]?-1:1;t.sort(function(e,t){return e[r]<t[r]?-i:e[r]>t[r]?i:0})}return n},limit:function(e){return t=t.slice(0,e),n},all:function(){return t},first:function(){return t.length?t[0]:null}};return n},t.std={filter:function(e,t){return r(e).filter(function(e){return t(e)})},map:function(e,t){return r(e).map(function(e){return t(e)})},take:function(e,t){return r(e).slice(0,t)},chunk:function(e,t){for(var n=r(e),i=[],o=0;o<n.length;o+=t)i.push(n.slice(o,o+t));return i},count:function(e){return r(e).length},sum:function(e){return r(e).reduce(function(e,t){return e+(t||0)},0)},sort_by:function(e,t){return r(e).slice().sort(function(e,n){var r=t(e),i=t(n);return r<i?-1:r>i?1:0})},group_by:function(e,t){var n={};return r(e).forEach(function(e){var r=t(e);(n[r]=n[r]||[]).push(e)}),n},join:function(e,t){return r(e).join(void 0===t?"":t)},split:function(e,t){return String(e).split(t)},replace:function(e,t,n){return String(e).split(String(t)).join(String(n))},contains:function(e,t){return-1!==String(e).indexOf(String(t))},first_char:function(e){return String(e).slice(0,1)},trim:function(e){return String(e).trim()},upper:function(e){return String(e).toUpperCase()},lower:function(e){return String(e).toLowerCase()},keys:function(e){return Object.keys(e||{})},values:function(e){return Object.values(e||{})},get_or:function(e,t,n){return e&&t in e?e[t]:n},int_to_float:function(e){return e},float_to_int:function(e){return Math.trunc(e)},concat:function(e,t){return r(e).concat(r(t))},append:function(e,t){return r(e).concat([t])},matches:function(e,t){try{return new RegExp(String(t)).test(String(e))}catch(e){return!1}}},t.len=function(e){return null==e?0:e.length},t.err=function(e,t){var n=new Error(t||e);return n.code=e,n},t.decode=function(e,n,r){return function e(n,i,o){if(null==n||"object"!=typeof n)throw t.err("decode.missing_field",(r||"response")+": expected an object at "+(o||"/"));for(var a in i)if(Object.prototype.hasOwnProperty.call(i,a)){if(void 0===n[a])throw t.err("decode.missing_field",(r||"response")+": missing "+o+"/"+a);i[a]&&"object"==typeof i[a]&&e(n[a],i[a],o+"/"+a)}return n}(e,n||{},"")},t.api=async function(n,r,i,o,a){var l,u,s,c={method:n,headers:{},credentials:"same-origin"};if(void 0!==i&&(c.headers["Content-Type"]="application/json",c.body=JSON.stringify(i)),/^(POST|PUT|PATCH|DELETE)$/.test(n)&&e.document){var d=(l=new RegExp("(?:^|;\\s*)"+(o||"vl_csrf").replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]+)"),(u=(e.document&&e.document.cookie||"").match(l))?decodeURIComponent(u[1]):null);d&&(c.headers["X-CSRF-Token"]=d)}try{s=await e.fetch(r,c)}catch(e){if(!a)throw e;var f=t.err("transport.network",n+" "+r+" -> network failure");throw f.kind="transport",f}if(!s.ok){var v=null;try{v=await s.json()}catch(e){}if(a){if(!v||"object"!=typeof v.error||null===v.error||"string"!=typeof v.error.code){var h=t.err("transport.malformed",n+" "+r+" -> "+s.status);throw h.kind="transport",h}var p=v.error.code,y=t.err(p,n+" "+r+" -> "+s.status),g=v.error.fields;throw y.fields=g&&"object"==typeof g?g:{},y.kind=a.indexOf(p)>=0?"domain":s.status>=500?"internal":"framework",y}var m=v&&v.error?v.error:404===s.status?"not_found":"http."+s.status,b=t.err(m,n+" "+r+" -> "+s.status);throw v&&"object"==typeof v&&Object.keys(v).forEach(function(e){"error"!==e&&(b[e]=v[e])}),b}return 204===s.status?null:s.json()},t.authApi=async function(n,r,i){async function o(){var n=await e.fetch("/__vl/auth/csrf",{method:"GET",credentials:"same-origin",cache:"no-store"});if(!n.ok)throw t.err("http."+n.status,"could not establish CSRF token")}await o();try{return await t.api(n,r,i,"vl_csrf_pre")}catch(e){if(e&&"forbidden"===e.code)return await o(),t.api(n,r,i,"vl_csrf_pre");throw e}};var i=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function o(e,n){if(null!=e&&!0!==e&&!1!==e){if(Array.isArray(e))throw t.err("render.multi_root_row","a keyed list row must be a single element; key "+JSON.stringify(n)+" maps to "+e.length+" sibling nodes -- wrap the row in one `col` / `row`");e.nodeType&&(e._vlKey=n)}}function a(e,t){null!=t&&!1!==t&&!0!==t&&(Array.isArray(t)?t.forEach(function(t){a(e,t)}):"undefined"!=typeof Node&&t instanceof Node||t&&t.nodeType?e.appendChild(t):e.appendChild(document.createTextNode(String(t))))}function l(){return t._appEl||e.document&&e.document.getElementById("vl-root")||null}function u(e){for(var n=0;n<t._overlays.length;n++)if(t._overlays[n].id===e)return n;return-1}function s(e,t){if(e)try{t?(e.inert=!0,e.setAttribute("aria-hidden","true")):(e.inert=!1,e.removeAttribute("aria-hidden"))}catch(e){}}function c(e,t){if(!e||!t)return!1;if("boolean"==typeof e.isConnected)return e.isConnected;if("function"==typeof t.contains)return t.contains(e);for(var n=e;n.parentNode;)n=n.parentNode;return n===t||n===t.body||n===t.documentElement}function d(e){if(!e||!e.querySelectorAll)return[];for(var t=e.querySelectorAll("a[href],button,input,select,textarea,[tabindex]"),n=[],r=0;r<t.length;r++){var i=t[r];!i.disabled&&(null==i.tabIndex||i.tabIndex>=0||"-1"!==i.getAttribute("tabindex"))&&n.push(i)}return n}function f(e){var n=t.overlay._dismiss[e];"function"==typeof n&&n()}function v(e,t){if(!e||e.nodeType!==t.nodeType||e.nodeName!==t.nodeName)return t;if(3===t.nodeType)return e.nodeValue!==t.nodeValue&&(e.nodeValue=t.nodeValue),e;if(1!==t.nodeType||!e.attributes||!t.attributes)return t;if(e.getAttribute("data-vl-bind")!==t.getAttribute("data-vl-bind"))return t;if(!t._vlHandlers||!e._vlHandlers)return t;Object.keys(e._vlHandlers).forEach(function(t){e.removeEventListener(t,e._vlHandlers[t])}),Object.keys(t._vlHandlers).forEach(function(n){e.addEventListener(n,t._vlHandlers[n])}),e._vlHandlers=t._vlHandlers;var n="DETAILS"===e.nodeName?"open":null;return Array.from(e.attributes).forEach(function(r){r.name!==n&&(t.hasAttribute(r.name)||e.removeAttribute(r.name))}),Array.from(t.attributes).forEach(function(t){t.name!==n&&e.getAttribute(t.name)!==t.value&&e.setAttribute(t.name,t.value)}),h(e,Array.from(t.childNodes)),t.hasAttribute&&t.hasAttribute("value")&&e.value!==t.value&&(e.value=t.value),t.hasAttribute&&t.hasAttribute("data-vl-bind")&&e.checked!==t.checked&&(e.checked=t.checked),e}function h(e,t){for(var n=0;n<t.length;n++)if(t[n]&&null!=t[n]._vlKey)return p(e,t);for(t.forEach(function(t,n){var r=e.childNodes[n],i=v(r,t);r?i!==r&&e.replaceChild(i,r):e.appendChild(i)});e.childNodes.length>t.length;)e.removeChild(e.lastChild)}function p(n,r){var i=Array.prototype.slice.call(n.childNodes),o=Object.create(null),a=[];i.forEach(function(e){e&&null!=e._vlKey?e._vlKey in o||(o[e._vlKey]=e):a.push(e)});var l=Object.create(null);r.forEach(function(e){e&&null!=e._vlKey&&(l[e._vlKey]=!0)}),Object.keys(o).forEach(function(e){if(!l[e]){var n=o[e];n&&n._vlPfx&&t.pruneInstPrefix(n._vlHost,n._vlPfx)}});var u=e.document,s=u&&u.activeElement,c=null;s&&"number"==typeof s.selectionStart&&(c={start:s.selectionStart,end:s.selectionEnd,dir:s.selectionDirection});for(var f=[],h=0;h<t._overlays.length;h++){var p=t._overlays[h];if(p.restoreFocus)for(var y=0;y<i.length;y++){var g=i[y];if(g&&null!=g._vlKey&&!l[g._vlKey]&&g.contains&&g.contains(p.restoreFocus)){f.push({rec:p,index:y});break}}}var m=-1;if(s)for(var b=0;b<i.length;b++){var _=i[b];if(_&&null!=_._vlKey&&!l[_._vlKey]&&_.contains&&_.contains(s)){m=b;break}}var S=0,k=Object.create(null),w=[];r.forEach(function(e){if(e&&null!=e._vlKey){var n=e._vlKey;if(k[n])throw t.err("render.duplicate_key","list key "+JSON.stringify(n)+" is used by more than one row; keys must be unique within a list");k[n]=!0;var r=o[n],i=r?v(r,e):e;i._vlKey=n,w.push(i)}else{var l=a[S++];w.push(l?v(l,e):e)}});for(var E=0;E<w.length;E++)n.childNodes[E]!==w[E]&&n.insertBefore(w[E],n.childNodes[E]||null);for(;n.childNodes.length>w.length;)n.removeChild(n.lastChild);if(s&&u&&u.contains&&u.contains(s)&&u.activeElement!==s&&"function"==typeof s.focus){try{s.focus({preventScroll:!0})}catch(e){}if(c&&"function"==typeof s.setSelectionRange)try{s.setSelectionRange(c.start,c.end,c.dir)}catch(e){}}function A(e){for(var t=[],r=0;r<n.childNodes.length;r++)n.childNodes[r]&&null!=n.childNodes[r]._vlKey&&t.push(n.childNodes[r]);if(!t.length)return null;for(var o=0,a=0;a<e;a++){var u=i[a];u&&null!=u._vlKey&&l[u._vlKey]&&o++}var s=t[Math.min(o,t.length-1)];return d(s)[0]||s}if(m>=0&&u&&(!u.contains||!u.contains(s))){var C=A(m);if(C&&"function"==typeof C.focus)try{C.focus({preventScroll:!0})}catch(e){}}for(var x=0;x<f.length;x++)f[x].rec.restoreFallback=A(f[x].index)}t.fmt=function(e,t){var n=e instanceof Date?e:new Date(e);return isNaN(n.getTime())?String(e):String(t||"yyyy-MM-dd").replace(/yyyy/g,n.getFullYear()).replace(/MMM/g,i[n.getMonth()]).replace(/MM/g,("0"+(n.getMonth()+1)).slice(-2)).replace(/dd/g,("0"+n.getDate()).slice(-2)).replace(/\bd\b/g,n.getDate())},t.range=function(e,t,n){for(var r=[],i=e;n?i<=t:i<t;i++)r.push(i);return r},t.each=function(e,t){return r(e).map(function(e,n){return t(e,n)})},t.keyed=function(e,n,i){for(var a=r(e),l=[],u=Object.create(null),s=0;s<a.length;s++){var c=i(a[s],s);if(null==c)throw t.err("render.missing_key","list key expression produced "+String(c)+" for item "+s+"; a keyed list needs a stable, defined key per row");if(u[c=String(c)])throw t.err("render.duplicate_key","list key "+JSON.stringify(c)+" is used by more than one row; keys must be unique within a list");u[c]=!0;var d=n(a[s],s);o(d,c),l.push(d)}return l},t.escKey=function(e){return String(e).replace(/\\/g,"\\\\").replace(/\//g,"\\/")},t.tagInst=function(e,t,n){return n&&n.nodeType&&(n._vlPfx=e,n._vlHost=t),n},t.pruneInstPrefix=function(e,t){e&&t&&["_state","_mounted","_loads"].forEach(function(n){var r=e[n];r&&Object.keys(r).forEach(function(e){0===e.indexOf(t)&&delete r[e]})})},t.appendKids=a,t.testIdsEnabled=!1,t._testFrame=null,t._testScopes=[],t.withTestScope=function(e,n){if("string"!=typeof e||!/^[a-z][a-z0-9-]{0,79}$/.test(e))throw t.err("render.invalid_test_scope","test_scope must be a public lowercase slug");t._testScopes.push(e);try{return n()}finally{t._testScopes.pop()}},t.testId=function(e){var n=t._testScopes.concat([e]).join("/");if(t._testFrame){if(t._testFrame.ids.has(n))throw t.err("render.duplicate_test_id","duplicate automation ID in the next rendered document");t._testFrame.ids.add(n)}return n},t.linkSafeSchemes={http:1,https:1,mailto:1,tel:1},t.linkHref=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.linkSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked link scheme "+i[1].toLowerCase()+":"),"about:blank#blocked"):n},t.fetchSafeSchemes={http:1,https:1},t.fetchUrl=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.fetchSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked resource scheme "+i[1].toLowerCase()+":"),""):n},t.fetchSrcset=function(e){if(null==e)return"";for(var n=String(e),r=n.split(","),i=0;i<r.length;i++){var o=r[i].trim().split(/\s+/)[0];if(o&&""===t.fetchUrl(o))return""}return n},t.detailsToggle=function(e){return function(t){var n=t.currentTarget;if(n&&n.isConnected){if(!0===n._vlToggleSeeded)return e.call(this,t);n._vlToggleSeeded=!0}}},t.el=function(e,t,n){var r=document.createElement(e);return(t=t||{}).class&&(r.className=t.class),t.style&&Object.keys(t.style).forEach(function(e){r.style[e]=t.style[e]}),t.attrs&&Object.keys(t.attrs).forEach(function(e){var n=t.attrs[e];return"checked"===e?(r.checked=!!n,void(n?r.setAttribute("checked",""):r.removeAttribute("checked"))):"muted"===e?(r.muted=!!n,r.defaultMuted=!!n,void(n?r.setAttribute("muted",""):r.removeAttribute("muted"))):void(null!=n&&!1!==n&&r.setAttribute(e,!0===n?"":String(n)))}),void 0!==t.text&&null!==t.text&&(r.textContent=String(t.text)),"details"===e&&(r._vlToggleSeeded=!r.hasAttribute("open")),t.on&&Object.keys(t.on).forEach(function(e){r.addEventListener(e,t.on[e])}),r._vlHandlers=t.on||{},a(r,n),t.attrs&&"value"in t.attrs&&null!==t.attrs.value&&void 0!==t.attrs.value&&!1!==t.attrs.value&&(r.value=t.attrs.value),r},t.Spinner=function(){return t.el("div",{class:"vl-spinner",text:"loading…"},[])},t.live=function(e,n,r){if(null!=e&&"string"!=typeof e)throw t.err("render.invalid_status","status/error message must be a string or nil");r=Object.assign({},r||{},{role:"status","aria-live":"polite","aria-atomic":"true"});var i=t.el("span",{class:n||"vl-status",attrs:r},[]);return i.appendChild(document.createTextNode(e||"")),i},t._fieldSeq=0,t.field=function(e){var n=e.name;if(null==n||""===String(n).trim())throw t.err("render.empty_input_name","an input's accessible name ("+(e.aria?"aria_label:":"label:")+") is "+(null==n?String(n):JSON.stringify(String(n)))+" at render time; a control must always have a non-empty name");n=String(n);var r="vlf"+ ++t._fieldSeq,i=r+"c",o=r+"h",a=r+"e",l=e.input||{},u=l.attrs=l.attrs||{};u.id=i;var s=void 0!==e.help&&null!==e.help,c=e.hasError?e.error:void 0,d=null!=c&&""!==c,f=[];s&&f.push(o),d&&f.push(a),f.length?u["aria-describedby"]=f.join(" "):delete u["aria-describedby"],e.hasError&&(u["aria-invalid"]=d?"true":null),e.aria&&(u["aria-label"]=n);var v=[];return e.aria||v.push(t.el("label",{class:"vl-label",attrs:{for:i},text:n},[])),v.push(t.el(e.tag||"input",l,e.children||[])),s&&v.push(t.el("span",{class:"vl-help",attrs:{id:o},text:e.help},[])),e.hasError&&v.push(t.live(c,"vl-error",{id:a})),t.el("div",{class:"vl-field"},v)},t.reqAlt=function(e){if(null==e||""===String(e).trim())throw t.err("render.empty_alt","an img `alt:` resolved to "+(null==e?String(e):JSON.stringify(String(e)))+" at render time; use `decorative` for a presentational image or give real alternative text");return String(e)},t.setTitle=function(n){if(null==n||""===String(n).trim())throw t.err("render.empty_page_title","the page `title:` resolved to "+(null==n?String(n):JSON.stringify(String(n)))+" at load time; a page must have a non-empty title");var r=e.document;return r&&(r.title=String(n)),String(n)},t.wireSkipLink=function(){var n=e.document;if(n&&n.querySelector&&n.body){var r=n.querySelector("nav.vl-skip-nav"),i=n.querySelector("a.vl-skip"),o=n.querySelector("main");if(o){o.id||(o.id="vl-main"),o.getAttribute&&null==o.getAttribute("tabindex")&&o.setAttribute("tabindex","-1");var a=t._mainLabel;if(a&&o.getAttribute){var l=null!=o.getAttribute("data-vl-route-label");(null==o.getAttribute("aria-label")||l)&&(o.getAttribute("aria-label")!==a&&o.setAttribute("aria-label",a),l||o.setAttribute("data-vl-route-label",""))}if(i)i.getAttribute("href")!=="#"+o.id&&i.setAttribute("href","#"+o.id);else{var u=t.el("a",{class:"vl-skip",attrs:{href:"#"+o.id},text:"Skip to main content"},[]);u.addEventListener("click",function(t){t.preventDefault&&t.preventDefault();var r=n.querySelector("main")||n.getElementById("vl-main");r&&r.focus&&r.focus();try{r&&r.id&&e.history&&e.history.replaceState&&e.history.replaceState(null,"","#"+r.id)}catch(e){}}),r||(r=t.el("nav",{class:"vl-skip-nav",attrs:{"aria-label":"Skip navigation"}},[]),n.body.insertBefore(r,n.body.firstChild)),r.appendChild(u)}}else{var s=r||i;s&&s.parentNode&&s.parentNode.removeChild(s)}}},t.queryProps=function(t){var n={};try{var r=e.location,i=new URLSearchParams(r&&r.search||"");(t||[]).forEach(function(e){i.has(e)&&(n[e]=i.get(e))})}catch(e){}return n},t._overlays=[],t._appEl=null,t.overlay={open:function(n){var r=u(n);if(r>=0)return t._overlays[r].el;var i=e.document,o=i.createElement("div");o.className="vl-overlay",o.setAttribute("data-overlay",String(n));var a=i.body||l()&&l().parentNode||null;a&&a.appendChild(o);var c=i.activeElement;return t._overlays.push({id:n,el:o,restoreFocus:c&&c!==i.body?c:null}),1===t._overlays.length&&s(l(),!0),o},close:function(n){var r=u(n);if(!(r<0)){var i=t._overlays.splice(r,1)[0],o=e.document;i._onClick&&i.el.removeEventListener&&i.el.removeEventListener("click",i._onClick),i._onTab&&i.el.removeEventListener&&i.el.removeEventListener("keydown",i._onTab),i._onKey&&o&&o.removeEventListener&&o.removeEventListener("keydown",i._onKey),delete t.overlay._dismiss[n],i.el&&i.el.parentNode&&i.el.parentNode.removeChild(i.el),0===t._overlays.length&&(s(l(),!1),function(e,t){for(var n=[e.restoreFocus,e.restoreFallback,t&&t.getElementById&&t.getElementById("vl-main")],r=0;r<n.length;r++){var i=n[r];if(i&&c(i,t)&&"function"==typeof i.focus){try{i.focus()}catch(e){}if(!t||t.activeElement===i)return}}var o=e.restoreFocus;if(o&&"function"==typeof o.focus)try{o.focus()}catch(e){}}(i,o))}},focusInto:function(e){if(e){var t=d(e)[0]||e;if(t===e&&(null==e.tabIndex||e.tabIndex<0)&&(e.tabIndex=-1),"function"==typeof t.focus)try{t.focus()}catch(e){}}},count:function(){return t._overlays.length},_dismiss:Object.create(null),_live:null,beginFrame:function(){this._live=Object.create(null)},endFrame:function(){var e=this._live;if(this._live=null,e)for(var n=t._overlays.length-1;n>=0;n--){var r=t._overlays[n].id;r in e||this.close(r)}},paint:function(n,r,i,o){var a;if(t._testFrame)return t._testFrame.portals.push(function(){t.overlay.paint(n,r,function(){return a},o)}),a=i(),null;var l=u(n)<0,s=this.open(n);s.className="vl-overlay vl-overlay-dialog",this._live&&(this._live[n]=!0),this._dismiss[n]=o;var c=t._overlays[u(n)],h="vl-dlg-title-"+String(n).replace(/[^\w-]/g,"_"),p=t.el("div",{class:"vl-dialog",attrs:{role:"dialog","aria-modal":"true","aria-labelledby":h,tabindex:"-1"}},[t.el("h2",{class:"vl-dialog-title",attrs:{id:h},text:r},[]),i()]);if(c.panel&&c.panel.parentNode===s&&s.replaceChild){var y=v(c.panel,p);y!==c.panel&&(s.replaceChild(y,c.panel),c.panel=y)}else if(s.replaceChildren)s.replaceChildren(p),c.panel=p;else{for(;s.firstChild;)s.removeChild(s.firstChild);s.appendChild(p),c.panel=p}return l&&(c._onTab=function(t){if("Tab"===(t.key||"")||9===t.keyCode){var n=c.panel,r=d(n);if(r.length){var i=r[0],o=r[r.length-1],a=e.document.activeElement;t.shiftKey&&a===i?(t.preventDefault&&t.preventDefault(),o.focus()):t.shiftKey||a!==o&&n.contains(a)||(t.preventDefault&&t.preventDefault(),i.focus())}else t.preventDefault&&t.preventDefault()}},s.addEventListener&&s.addEventListener("keydown",c._onTab),c._onClick=function(e){e.target===s&&f(n)},s.addEventListener&&s.addEventListener("click",c._onClick),c._onKey=function(e){"Escape"!==(e.key||"")&&27!==e.keyCode||f(n)},e.document.addEventListener&&e.document.addEventListener("keydown",c._onKey),this.focusInto(c.panel)),null}},t.route={to:function(t){if("string"==typeof t&&t)try{if("?"===t[0]||"#"===t[0]){var n=e.history;return void(n&&n.replaceState&&n.replaceState(null,"",t))}var r=e.location;r&&r.assign&&r.assign(t)}catch(e){}}},t.tabs=function(e){for(var n=(e=e||{}).tabs||[],r=0,i=0;i<n.length;i++)if(n[i].id===e.selected){r=i;break}var o=n.length?n[r].id:null;function a(t){n.length&&e.onSelect&&e.onSelect(n[(t+n.length)%n.length].id)}var l=t.el("div",{class:"vl-tablist",attrs:{role:"tablist"},on:{keydown:function(e){var t=e.key||"",i=function(){e.preventDefault&&e.preventDefault()};"ArrowRight"===t||"ArrowDown"===t?(i(),a(r+1)):"ArrowLeft"===t||"ArrowUp"===t?(i(),a(r-1)):"Home"===t?(i(),a(0)):"End"===t&&(i(),a(n.length-1))}}},n.map(function(n){var r=n.id===o;return t.el("button",{class:"vl-tab",attrs:{role:"tab",id:"vl-tab-"+n.id,"aria-selected":r?"true":"false","aria-controls":"vl-panel-"+n.id,tabindex:r?"0":"-1"},text:n.label,on:{click:function(){e.onSelect&&e.onSelect(n.id)}}},[])})),u=n[r],s=t.el("div",{class:"vl-tabpanel",attrs:{role:"tabpanel",id:"vl-panel-"+o,"aria-labelledby":"vl-tab-"+o,tabindex:"0"}},u?[u.panel()]:[]);return t.el("div",{class:"vl-tabs"},[l,s])},t.subctx=function(e,t){var n=e._host,r=(e._pfx||"")+(t||"");return{_pfx:r,_host:n,signal:n.signal,state:function(e,t){return n.state(r+e,t)},setState:function(e,t){return n.setState(r+e,t)},load:function(e,t,i,o){return n.load(r+e,t,i,o)},reload:function(e){return n.reload(r+e)},loading:function(e){return n.loading(r+e)},mountOnce:function(e,t){return n.mountOnce(r+e,t)}}},t.mount=function(n,r,i){var o=i||{};t._appEl=r;var l="undefined"!=typeof AbortController?new AbortController:null,u={_state:Object.create(null),_loads:Object.create(null),_mounted:Object.create(null),_scheduled:!1,_composing:!1,_abortController:l,signal:l?l.signal:void 0,state:function(e,t){return e in this._state||(this._state[e]=t),this._state[e]},mountOnce:function(e,t){this._mounted[e]||(this._mounted[e]=!0,t())},setState:function(e,t){return this._state[e]=t,this.schedule(),t},load:function(e,n,r,i){var o=JSON.stringify(void 0===n?null:n),a=this._loads[e];if(!a||a.key!==o){a=this._loads[e]={key:o,pending:!0,error:null,value:void 0};var l=this;Promise.resolve().then(r).then(function(n){l._loads[e]===a&&(a.pending=!1,a.value=n,l.schedule(),t._onLoadSettled&&t._onLoadSettled(l))},function(n){l._loads[e]===a&&(a.pending=!1,a.error=n,l.schedule(),!i&&t._onUnhandledLoadError&&t._onUnhandledLoadError(l,e,n),t._onLoadSettled&&t._onLoadSettled(l))})}return a},reload:function(e){delete this._loads[e],this.schedule()},loading:function(e){var t=this._loads[e];return!t||t.pending},schedule:function(){var e=this;e._scheduled||(e._scheduled=!0,queueMicrotask(function(){e._scheduled=!1,e.render()}))},render:function(){if(!this._composing){var i,l=e.document,u=l&&l.activeElement,c=null;u&&u.getAttribute&&u.getAttribute("data-vl-bind")&&(!l.contains||l.contains(u))&&(c={node:u,key:u.getAttribute("data-vl-bind"),start:u.selectionStart,end:u.selectionEnd,direction:u.selectionDirection,scrollLeft:u.scrollLeft,scrollTop:u.scrollTop}),t.overlay.beginFrame(),t.testIdsEnabled&&(t._testFrame={ids:new Set,portals:[]});try{i=n(o,s);var d=t._testFrame;t._testFrame=null,d&&d.portals.forEach(function(e){e()})}catch(e){if(t._testFrame=null,t._testScopes=[],e&&/^render\.(duplicate_test_id|invalid_test_scope)$/.test(e.code||""))throw t.overlay._live=null,e;throw r.replaceChildren(t.el("pre",{class:"vl-error",text:String(e&&e.stack||e)},[])),e}if(Array.isArray(i)){var f=document.createDocumentFragment();a(f,i),r.replaceChild?h(r,Array.from(f.childNodes)):r.replaceChildren(f)}else r.replaceChild?h(r,[i]):r.replaceChildren(i);if(t.overlay.endFrame(),t.wireSkipLink(),c&&l){var v=l.contains&&l.contains(c.node)?c.node:null;if(!v&&l.querySelectorAll){var p=Array.prototype.filter.call(l.querySelectorAll("[data-vl-bind]"),function(e){return e.getAttribute("data-vl-bind")===c.key});1===p.length&&(v=p[0])}if(v&&l.activeElement!==v){if(v.focus({preventScroll:!0}),null!=c.start&&"function"==typeof v.setSelectionRange)try{v.setSelectionRange(c.start,c.end,c.direction)}catch(e){}v.scrollLeft=c.scrollLeft,v.scrollTop=c.scrollTop}}}}},s=t.subctx({_pfx:"",_host:u},""),c=e.document;return c&&c.addEventListener&&(c.addEventListener("compositionstart",function(){u._composing=!0}),c.addEventListener("compositionend",function(){u._composing=!1,u.schedule()})),u.render(),u},t._onUnhandledLoadError=null,t._onLoadSettled=null,t._mainLabel=null,t._modules=Object.create(null);var y=Object.create(null);t.registerModule=function(e,n){t._modules[e]=n},t.loadModule=function(n,r){if(t._modules[n])return Promise.resolve(t._modules[n]);if(y[n])return y[n];var i=e.document,o=new Promise(function(e,o){var a=i.createElement("script");a.src=r,a.onload=function(){t._modules[n]?e(t._modules[n]):o(new Error("module '"+n+"' did not register itself (loaded "+r+")"))},a.onerror=function(){o(new Error("failed to load module '"+n+"' from "+r))},i.head.appendChild(a)});return y[n]=o,o},e.VL=t}("undefined"!=typeof window?window:globalThis);
2
+ "use strict";!function(e){var t={},n=null;function r(e){return null==e?[]:e}function i(e,t){if(e===t)return!0;if(null===e||null===t||"object"!=typeof e||"object"!=typeof t||Array.isArray(e)!==Array.isArray(t))return!1;var n=Object.keys(e);if(n.length!==Object.keys(t).length)return!1;for(var r=0;r<n.length;r++)if(!Object.prototype.hasOwnProperty.call(t,n[r])||!i(e[n[r]],t[n[r]]))return!1;return!0}t.fetchData=function(){return n||(n=e.fetch("data.json",{cache:"no-store"}).then(function(e){if(!e.ok)throw new Error("HTTP "+e.status+" loading data.json");return e.json()})),n},t.db={},t.qb=function(e){var t=(e||[]).slice(),n={where:function(e){var r={lt:function(e,t){return e<t},lte:function(e,t){return e<=t},gt:function(e,t){return e>t},gte:function(e,t){return e>=t},ne:function(e,t){return e!==t}};return Object.keys(e||{}).forEach(function(n){var i=e[n];if(null!=i&&""!==i)if(i&&"object"==typeof i&&"string"==typeof i.op)if("between"===i.op)t=t.filter(function(e){return e[n]>=i.v[0]&&e[n]<=i.v[1]});else if("any_of"===i.op){var o=i.v||[];t=t.filter(function(e){return-1!==o.indexOf(e[n])})}else{var a=r[i.op];t=t.filter(function(e){return a(e[n],i.v)})}else t=t.filter(function(e){return e[n]===i})}),n},order:function(e){var r=Object.keys(e||{})[0];if(r){var i="desc"===e[r]?-1:1;t.sort(function(e,t){return e[r]<t[r]?-i:e[r]>t[r]?i:0})}return n},limit:function(e){return t=t.slice(0,e),n},all:function(){return t},first:function(){return t.length?t[0]:null}};return n},t.std={filter:function(e,t){return r(e).filter(function(e){return t(e)})},map:function(e,t){return r(e).map(function(e){return t(e)})},take:function(e,t){return r(e).slice(0,t)},chunk:function(e,t){for(var n=r(e),i=[],o=0;o<n.length;o+=t)i.push(n.slice(o,o+t));return i},count:function(e){return r(e).length},sum:function(e){return r(e).reduce(function(e,t){return e+(t||0)},0)},sort_by:function(e,t){return r(e).slice().sort(function(e,n){var r=t(e),i=t(n);return r<i?-1:r>i?1:0})},group_by:function(e,t){var n={};return r(e).forEach(function(e){var r=t(e);(n[r]=n[r]||[]).push(e)}),n},join:function(e,t){return r(e).join(void 0===t?"":t)},split:function(e,t){return String(e).split(t)},replace:function(e,t,n){return String(e).split(String(t)).join(String(n))},contains:function(e,t){return-1!==String(e).indexOf(String(t))},first_char:function(e){return String(e).slice(0,1)},trim:function(e){return String(e).trim()},upper:function(e){return String(e).toUpperCase()},lower:function(e){return String(e).toLowerCase()},keys:function(e){return Object.keys(e||{})},values:function(e){return Object.values(e||{})},get_or:function(e,t,n){return e&&t in e?e[t]:n},int_to_float:function(e){return e},float_to_int:function(e){return Math.trunc(e)},concat:function(e,t){return r(e).concat(r(t))},append:function(e,t){return r(e).concat([t])},unique:function(e){var t=[],n=new Set,o=[];return r(e).forEach(function(e){if(null!==e&&"object"==typeof e){for(var r=0;r<o.length;r++)if(i(o[r],e))return;o.push(e)}else if(e==e){if(n.has(e))return;n.add(e)}t.push(e)}),t},matches:function(e,t){try{return new RegExp(String(t)).test(String(e))}catch(e){return!1}}},t.len=function(e){return null==e?0:e.length},t.err=function(e,t){var n=new Error(t||e);return n.code=e,n},t.decode=function(e,n,r){return function e(n,i,o){if(null==n||"object"!=typeof n)throw t.err("decode.missing_field",(r||"response")+": expected an object at "+(o||"/"));for(var a in i)if(Object.prototype.hasOwnProperty.call(i,a)){if(void 0===n[a])throw t.err("decode.missing_field",(r||"response")+": missing "+o+"/"+a);i[a]&&"object"==typeof i[a]&&e(n[a],i[a],o+"/"+a)}return n}(e,n||{},"")},t.api=async function(n,r,i,o,a){var l,u,s,c={method:n,headers:{},credentials:"same-origin"};if(void 0!==i&&(c.headers["Content-Type"]="application/json",c.body=JSON.stringify(i)),/^(POST|PUT|PATCH|DELETE)$/.test(n)&&e.document){var d=(l=new RegExp("(?:^|;\\s*)"+(o||"vl_csrf").replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]+)"),(u=(e.document&&e.document.cookie||"").match(l))?decodeURIComponent(u[1]):null);d&&(c.headers["X-CSRF-Token"]=d)}try{s=await e.fetch(r,c)}catch(e){if(!a)throw e;var f=t.err("transport.network",n+" "+r+" -> network failure");throw f.kind="transport",f}if(!s.ok){var v=null;try{v=await s.json()}catch(e){}if(a){if(!v||"object"!=typeof v.error||null===v.error||"string"!=typeof v.error.code){var h=t.err("transport.malformed",n+" "+r+" -> "+s.status);throw h.kind="transport",h}var p=v.error.code,y=t.err(p,n+" "+r+" -> "+s.status),g=v.error.fields;throw y.fields=g&&"object"==typeof g?g:{},y.kind=a.indexOf(p)>=0?"domain":s.status>=500?"internal":"framework",y}var m=v&&v.error?v.error:404===s.status?"not_found":"http."+s.status,b=t.err(m,n+" "+r+" -> "+s.status);throw v&&"object"==typeof v&&Object.keys(v).forEach(function(e){"error"!==e&&(b[e]=v[e])}),b}return 204===s.status?null:s.json()},t.authApi=async function(n,r,i){async function o(){var n=await e.fetch("/__vl/auth/csrf",{method:"GET",credentials:"same-origin",cache:"no-store"});if(!n.ok)throw t.err("http."+n.status,"could not establish CSRF token")}await o();try{return await t.api(n,r,i,"vl_csrf_pre")}catch(e){if(e&&"forbidden"===e.code)return await o(),t.api(n,r,i,"vl_csrf_pre");throw e}};var o=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function a(e,n){if(null!=e&&!0!==e&&!1!==e){if(Array.isArray(e))throw t.err("render.multi_root_row","a keyed list row must be a single element; key "+JSON.stringify(n)+" maps to "+e.length+" sibling nodes -- wrap the row in one `col` / `row`");e.nodeType&&(e._vlKey=n)}}function l(e,t){null!=t&&!1!==t&&!0!==t&&(Array.isArray(t)?t.forEach(function(t){l(e,t)}):"undefined"!=typeof Node&&t instanceof Node||t&&t.nodeType?e.appendChild(t):e.appendChild(document.createTextNode(String(t))))}function u(){return t._appEl||e.document&&e.document.getElementById("vl-root")||null}function s(e){for(var n=0;n<t._overlays.length;n++)if(t._overlays[n].id===e)return n;return-1}function c(e,t){if(e)try{t?(e.inert=!0,e.setAttribute("aria-hidden","true")):(e.inert=!1,e.removeAttribute("aria-hidden"))}catch(e){}}function d(e,t){if(!e||!t)return!1;if("boolean"==typeof e.isConnected)return e.isConnected;if("function"==typeof t.contains)return t.contains(e);for(var n=e;n.parentNode;)n=n.parentNode;return n===t||n===t.body||n===t.documentElement}function f(e){if(!e||!e.querySelectorAll)return[];for(var t=e.querySelectorAll("a[href],button,input,select,textarea,[tabindex]"),n=[],r=0;r<t.length;r++){var i=t[r];!i.disabled&&(null==i.tabIndex||i.tabIndex>=0||"-1"!==i.getAttribute("tabindex"))&&n.push(i)}return n}function v(e){var n=t.overlay._dismiss[e];"function"==typeof n&&n()}function h(e,t){if(!e||e.nodeType!==t.nodeType||e.nodeName!==t.nodeName)return t;if(3===t.nodeType)return e.nodeValue!==t.nodeValue&&(e.nodeValue=t.nodeValue),e;if(1!==t.nodeType||!e.attributes||!t.attributes)return t;if(e.getAttribute("data-vl-bind")!==t.getAttribute("data-vl-bind"))return t;if(!t._vlHandlers||!e._vlHandlers)return t;Object.keys(e._vlHandlers).forEach(function(t){e.removeEventListener(t,e._vlHandlers[t])}),Object.keys(t._vlHandlers).forEach(function(n){e.addEventListener(n,t._vlHandlers[n])}),e._vlHandlers=t._vlHandlers;var n="DETAILS"===e.nodeName?"open":null;return Array.from(e.attributes).forEach(function(r){r.name!==n&&(t.hasAttribute(r.name)||e.removeAttribute(r.name))}),Array.from(t.attributes).forEach(function(t){t.name!==n&&e.getAttribute(t.name)!==t.value&&e.setAttribute(t.name,t.value)}),p(e,Array.from(t.childNodes)),t.hasAttribute&&t.hasAttribute("value")&&e.value!==t.value&&(e.value=t.value),t.hasAttribute&&t.hasAttribute("data-vl-bind")&&e.checked!==t.checked&&(e.checked=t.checked),e}function p(e,t){for(var n=0;n<t.length;n++)if(t[n]&&null!=t[n]._vlKey)return y(e,t);for(t.forEach(function(t,n){var r=e.childNodes[n],i=h(r,t);r?i!==r&&e.replaceChild(i,r):e.appendChild(i)});e.childNodes.length>t.length;)e.removeChild(e.lastChild)}function y(n,r){var i=Array.prototype.slice.call(n.childNodes),o=Object.create(null),a=[];i.forEach(function(e){e&&null!=e._vlKey?e._vlKey in o||(o[e._vlKey]=e):a.push(e)});var l=Object.create(null);r.forEach(function(e){e&&null!=e._vlKey&&(l[e._vlKey]=!0)}),Object.keys(o).forEach(function(e){if(!l[e]){var n=o[e];n&&n._vlPfx&&t.pruneInstPrefix(n._vlHost,n._vlPfx)}});var u=e.document,s=u&&u.activeElement,c=null;s&&"number"==typeof s.selectionStart&&(c={start:s.selectionStart,end:s.selectionEnd,dir:s.selectionDirection});for(var d=[],v=0;v<t._overlays.length;v++){var p=t._overlays[v];if(p.restoreFocus)for(var y=0;y<i.length;y++){var g=i[y];if(g&&null!=g._vlKey&&!l[g._vlKey]&&g.contains&&g.contains(p.restoreFocus)){d.push({rec:p,index:y});break}}}var m=-1;if(s)for(var b=0;b<i.length;b++){var _=i[b];if(_&&null!=_._vlKey&&!l[_._vlKey]&&_.contains&&_.contains(s)){m=b;break}}var S=0,k=Object.create(null),w=[];r.forEach(function(e){if(e&&null!=e._vlKey){var n=e._vlKey;if(k[n])throw t.err("render.duplicate_key","list key "+JSON.stringify(n)+" is used by more than one row; keys must be unique within a list");k[n]=!0;var r=o[n],i=r?h(r,e):e;i._vlKey=n,w.push(i)}else{var l=a[S++];w.push(l?h(l,e):e)}});for(var E=0;E<w.length;E++)n.childNodes[E]!==w[E]&&n.insertBefore(w[E],n.childNodes[E]||null);for(;n.childNodes.length>w.length;)n.removeChild(n.lastChild);if(s&&u&&u.contains&&u.contains(s)&&u.activeElement!==s&&"function"==typeof s.focus){try{s.focus({preventScroll:!0})}catch(e){}if(c&&"function"==typeof s.setSelectionRange)try{s.setSelectionRange(c.start,c.end,c.dir)}catch(e){}}function A(e){for(var t=[],r=0;r<n.childNodes.length;r++)n.childNodes[r]&&null!=n.childNodes[r]._vlKey&&t.push(n.childNodes[r]);if(!t.length)return null;for(var o=0,a=0;a<e;a++){var u=i[a];u&&null!=u._vlKey&&l[u._vlKey]&&o++}var s=t[Math.min(o,t.length-1)];return f(s)[0]||s}if(m>=0&&u&&(!u.contains||!u.contains(s))){var C=A(m);if(C&&"function"==typeof C.focus)try{C.focus({preventScroll:!0})}catch(e){}}for(var x=0;x<d.length;x++)d[x].rec.restoreFallback=A(d[x].index)}t.fmt=function(e,t){var n=e instanceof Date?e:new Date(e);return isNaN(n.getTime())?String(e):String(t||"yyyy-MM-dd").replace(/yyyy/g,n.getFullYear()).replace(/MMM/g,o[n.getMonth()]).replace(/MM/g,("0"+(n.getMonth()+1)).slice(-2)).replace(/dd/g,("0"+n.getDate()).slice(-2)).replace(/\bd\b/g,n.getDate())},t.range=function(e,t,n){for(var r=[],i=e;n?i<=t:i<t;i++)r.push(i);return r},t.each=function(e,t){return r(e).map(function(e,n){return t(e,n)})},t.keyed=function(e,n,i){for(var o=r(e),l=[],u=Object.create(null),s=0;s<o.length;s++){var c=i(o[s],s);if(null==c)throw t.err("render.missing_key","list key expression produced "+String(c)+" for item "+s+"; a keyed list needs a stable, defined key per row");if(u[c=String(c)])throw t.err("render.duplicate_key","list key "+JSON.stringify(c)+" is used by more than one row; keys must be unique within a list");u[c]=!0;var d=n(o[s],s);a(d,c),l.push(d)}return l},t.escKey=function(e){return String(e).replace(/\\/g,"\\\\").replace(/\//g,"\\/")},t.tagInst=function(e,t,n){return n&&n.nodeType&&(n._vlPfx=e,n._vlHost=t),n},t.pruneInstPrefix=function(e,t){e&&t&&["_state","_mounted","_loads"].forEach(function(n){var r=e[n];r&&Object.keys(r).forEach(function(e){0===e.indexOf(t)&&delete r[e]})})},t.appendKids=l,t.testIdsEnabled=!1,t._testFrame=null,t._testScopes=[],t.withTestScope=function(e,n){if("string"!=typeof e||!/^[a-z][a-z0-9-]{0,79}$/.test(e))throw t.err("render.invalid_test_scope","test_scope must be a public lowercase slug");t._testScopes.push(e);try{return n()}finally{t._testScopes.pop()}},t.testId=function(e){var n=t._testScopes.concat([e]).join("/");if(t._testFrame){if(t._testFrame.ids.has(n))throw t.err("render.duplicate_test_id","duplicate automation ID in the next rendered document");t._testFrame.ids.add(n)}return n},t.linkSafeSchemes={http:1,https:1,mailto:1,tel:1},t.linkHref=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.linkSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked link scheme "+i[1].toLowerCase()+":"),"about:blank#blocked"):n},t.fetchSafeSchemes={http:1,https:1},t.fetchUrl=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.fetchSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked resource scheme "+i[1].toLowerCase()+":"),""):n},t.fetchSrcset=function(e){if(null==e)return"";for(var n=String(e),r=n.split(","),i=0;i<r.length;i++){var o=r[i].trim().split(/\s+/)[0];if(o&&""===t.fetchUrl(o))return""}return n},t.detailsToggle=function(e){return function(t){var n=t.currentTarget;if(n&&n.isConnected){if(!0===n._vlToggleSeeded)return e.call(this,t);n._vlToggleSeeded=!0}}},t.el=function(e,t,n){var r=document.createElement(e);return(t=t||{}).class&&(r.className=t.class),t.style&&Object.keys(t.style).forEach(function(e){r.style[e]=t.style[e]}),t.attrs&&Object.keys(t.attrs).forEach(function(e){var n=t.attrs[e];return"checked"===e?(r.checked=!!n,void(n?r.setAttribute("checked",""):r.removeAttribute("checked"))):"muted"===e?(r.muted=!!n,r.defaultMuted=!!n,void(n?r.setAttribute("muted",""):r.removeAttribute("muted"))):void(null!=n&&!1!==n&&r.setAttribute(e,!0===n?"":String(n)))}),void 0!==t.text&&null!==t.text&&(r.textContent=String(t.text)),"details"===e&&(r._vlToggleSeeded=!r.hasAttribute("open")),t.on&&Object.keys(t.on).forEach(function(e){r.addEventListener(e,t.on[e])}),r._vlHandlers=t.on||{},l(r,n),t.attrs&&"value"in t.attrs&&null!==t.attrs.value&&void 0!==t.attrs.value&&!1!==t.attrs.value&&(r.value=t.attrs.value),r},t.Spinner=function(){return t.el("div",{class:"vl-spinner",text:"loading…"},[])},t.live=function(e,n,r){if(null!=e&&"string"!=typeof e)throw t.err("render.invalid_status","status/error message must be a string or nil");r=Object.assign({},r||{},{role:"status","aria-live":"polite","aria-atomic":"true"});var i=t.el("span",{class:n||"vl-status",attrs:r},[]);return i.appendChild(document.createTextNode(e||"")),i},t._fieldSeq=0,t.field=function(e){var n=e.name;if(null==n||""===String(n).trim())throw t.err("render.empty_input_name","an input's accessible name ("+(e.aria?"aria_label:":"label:")+") is "+(null==n?String(n):JSON.stringify(String(n)))+" at render time; a control must always have a non-empty name");n=String(n);var r="vlf"+ ++t._fieldSeq,i=r+"c",o=r+"h",a=r+"e",l=e.input||{},u=l.attrs=l.attrs||{};u.id=i;var s=void 0!==e.help&&null!==e.help,c=e.hasError?e.error:void 0,d=null!=c&&""!==c,f=[];s&&f.push(o),d&&f.push(a),f.length?u["aria-describedby"]=f.join(" "):delete u["aria-describedby"],e.hasError&&(u["aria-invalid"]=d?"true":null),e.aria&&(u["aria-label"]=n);var v=[];return e.aria||v.push(t.el("label",{class:"vl-label",attrs:{for:i},text:n},[])),v.push(t.el(e.tag||"input",l,e.children||[])),s&&v.push(t.el("span",{class:"vl-help",attrs:{id:o},text:e.help},[])),e.hasError&&v.push(t.live(c,"vl-error",{id:a})),t.el("div",{class:"vl-field"},v)},t.reqAlt=function(e){if(null==e||""===String(e).trim())throw t.err("render.empty_alt","an img `alt:` resolved to "+(null==e?String(e):JSON.stringify(String(e)))+" at render time; use `decorative` for a presentational image or give real alternative text");return String(e)},t.setTitle=function(n){if(null==n||""===String(n).trim())throw t.err("render.empty_page_title","the page `title:` resolved to "+(null==n?String(n):JSON.stringify(String(n)))+" at load time; a page must have a non-empty title");var r=e.document;return r&&(r.title=String(n)),String(n)},t.wireSkipLink=function(){var n=e.document;if(n&&n.querySelector&&n.body){var r=n.querySelector("nav.vl-skip-nav"),i=n.querySelector("a.vl-skip"),o=n.querySelector("main");if(o){o.id||(o.id="vl-main"),o.getAttribute&&null==o.getAttribute("tabindex")&&o.setAttribute("tabindex","-1");var a=t._mainLabel;if(a&&o.getAttribute){var l=null!=o.getAttribute("data-vl-route-label");(null==o.getAttribute("aria-label")||l)&&(o.getAttribute("aria-label")!==a&&o.setAttribute("aria-label",a),l||o.setAttribute("data-vl-route-label",""))}if(i)i.getAttribute("href")!=="#"+o.id&&i.setAttribute("href","#"+o.id);else{var u=t.el("a",{class:"vl-skip",attrs:{href:"#"+o.id},text:"Skip to main content"},[]);u.addEventListener("click",function(t){t.preventDefault&&t.preventDefault();var r=n.querySelector("main")||n.getElementById("vl-main");r&&r.focus&&r.focus();try{r&&r.id&&e.history&&e.history.replaceState&&e.history.replaceState(null,"","#"+r.id)}catch(e){}}),r||(r=t.el("nav",{class:"vl-skip-nav",attrs:{"aria-label":"Skip navigation"}},[]),n.body.insertBefore(r,n.body.firstChild)),r.appendChild(u)}}else{var s=r||i;s&&s.parentNode&&s.parentNode.removeChild(s)}}},t.queryProps=function(t){var n={};try{var r=e.location,i=new URLSearchParams(r&&r.search||"");(t||[]).forEach(function(e){i.has(e)&&(n[e]=i.get(e))})}catch(e){}return n},t._overlays=[],t._appEl=null,t.overlay={open:function(n){var r=s(n);if(r>=0)return t._overlays[r].el;var i=e.document,o=i.createElement("div");o.className="vl-overlay",o.setAttribute("data-overlay",String(n));var a=i.body||u()&&u().parentNode||null;a&&a.appendChild(o);var l=i.activeElement;return t._overlays.push({id:n,el:o,restoreFocus:l&&l!==i.body?l:null}),1===t._overlays.length&&c(u(),!0),o},close:function(n){var r=s(n);if(!(r<0)){var i=t._overlays.splice(r,1)[0],o=e.document;i._onClick&&i.el.removeEventListener&&i.el.removeEventListener("click",i._onClick),i._onTab&&i.el.removeEventListener&&i.el.removeEventListener("keydown",i._onTab),i._onKey&&o&&o.removeEventListener&&o.removeEventListener("keydown",i._onKey),delete t.overlay._dismiss[n],i.el&&i.el.parentNode&&i.el.parentNode.removeChild(i.el),0===t._overlays.length&&(c(u(),!1),function(e,t){for(var n=[e.restoreFocus,e.restoreFallback,t&&t.getElementById&&t.getElementById("vl-main")],r=0;r<n.length;r++){var i=n[r];if(i&&d(i,t)&&"function"==typeof i.focus){try{i.focus()}catch(e){}if(!t||t.activeElement===i)return}}var o=e.restoreFocus;if(o&&"function"==typeof o.focus)try{o.focus()}catch(e){}}(i,o))}},focusInto:function(e){if(e){var t=f(e)[0]||e;if(t===e&&(null==e.tabIndex||e.tabIndex<0)&&(e.tabIndex=-1),"function"==typeof t.focus)try{t.focus()}catch(e){}}},count:function(){return t._overlays.length},_dismiss:Object.create(null),_live:null,beginFrame:function(){this._live=Object.create(null)},endFrame:function(){var e=this._live;if(this._live=null,e)for(var n=t._overlays.length-1;n>=0;n--){var r=t._overlays[n].id;r in e||this.close(r)}},paint:function(n,r,i,o){var a;if(t._testFrame)return t._testFrame.portals.push(function(){t.overlay.paint(n,r,function(){return a},o)}),a=i(),null;var l=s(n)<0,u=this.open(n);u.className="vl-overlay vl-overlay-dialog",this._live&&(this._live[n]=!0),this._dismiss[n]=o;var c=t._overlays[s(n)],d="vl-dlg-title-"+String(n).replace(/[^\w-]/g,"_"),p=t.el("div",{class:"vl-dialog",attrs:{role:"dialog","aria-modal":"true","aria-labelledby":d,tabindex:"-1"}},[t.el("h2",{class:"vl-dialog-title",attrs:{id:d},text:r},[]),i()]);if(c.panel&&c.panel.parentNode===u&&u.replaceChild){var y=h(c.panel,p);y!==c.panel&&(u.replaceChild(y,c.panel),c.panel=y)}else if(u.replaceChildren)u.replaceChildren(p),c.panel=p;else{for(;u.firstChild;)u.removeChild(u.firstChild);u.appendChild(p),c.panel=p}return l&&(c._onTab=function(t){if("Tab"===(t.key||"")||9===t.keyCode){var n=c.panel,r=f(n);if(r.length){var i=r[0],o=r[r.length-1],a=e.document.activeElement;t.shiftKey&&a===i?(t.preventDefault&&t.preventDefault(),o.focus()):t.shiftKey||a!==o&&n.contains(a)||(t.preventDefault&&t.preventDefault(),i.focus())}else t.preventDefault&&t.preventDefault()}},u.addEventListener&&u.addEventListener("keydown",c._onTab),c._onClick=function(e){e.target===u&&v(n)},u.addEventListener&&u.addEventListener("click",c._onClick),c._onKey=function(e){"Escape"!==(e.key||"")&&27!==e.keyCode||v(n)},e.document.addEventListener&&e.document.addEventListener("keydown",c._onKey),this.focusInto(c.panel)),null}},t.route={to:function(t){if("string"==typeof t&&t)try{if("?"===t[0]||"#"===t[0]){var n=e.history;return void(n&&n.replaceState&&n.replaceState(null,"",t))}var r=e.location;r&&r.assign&&r.assign(t)}catch(e){}}},t.tabs=function(e){for(var n=(e=e||{}).tabs||[],r=0,i=0;i<n.length;i++)if(n[i].id===e.selected){r=i;break}var o=n.length?n[r].id:null;function a(t){n.length&&e.onSelect&&e.onSelect(n[(t+n.length)%n.length].id)}var l=t.el("div",{class:"vl-tablist",attrs:{role:"tablist"},on:{keydown:function(e){var t=e.key||"",i=function(){e.preventDefault&&e.preventDefault()};"ArrowRight"===t||"ArrowDown"===t?(i(),a(r+1)):"ArrowLeft"===t||"ArrowUp"===t?(i(),a(r-1)):"Home"===t?(i(),a(0)):"End"===t&&(i(),a(n.length-1))}}},n.map(function(n){var r=n.id===o;return t.el("button",{class:"vl-tab",attrs:{role:"tab",id:"vl-tab-"+n.id,"aria-selected":r?"true":"false","aria-controls":"vl-panel-"+n.id,tabindex:r?"0":"-1"},text:n.label,on:{click:function(){e.onSelect&&e.onSelect(n.id)}}},[])})),u=n[r],s=t.el("div",{class:"vl-tabpanel",attrs:{role:"tabpanel",id:"vl-panel-"+o,"aria-labelledby":"vl-tab-"+o,tabindex:"0"}},u?[u.panel()]:[]);return t.el("div",{class:"vl-tabs"},[l,s])},t.subctx=function(e,t){var n=e._host,r=(e._pfx||"")+(t||"");return{_pfx:r,_host:n,signal:n.signal,state:function(e,t){return n.state(r+e,t)},setState:function(e,t){return n.setState(r+e,t)},load:function(e,t,i,o){return n.load(r+e,t,i,o)},reload:function(e){return n.reload(r+e)},loading:function(e){return n.loading(r+e)},mountOnce:function(e,t){return n.mountOnce(r+e,t)}}},t.mount=function(n,r,i){var o=i||{};t._appEl=r;var a="undefined"!=typeof AbortController?new AbortController:null,u={_state:Object.create(null),_loads:Object.create(null),_mounted:Object.create(null),_scheduled:!1,_composing:!1,_abortController:a,signal:a?a.signal:void 0,state:function(e,t){return e in this._state||(this._state[e]=t),this._state[e]},mountOnce:function(e,t){this._mounted[e]||(this._mounted[e]=!0,t())},setState:function(e,t){return this._state[e]=t,this.schedule(),t},load:function(e,n,r,i){var o=JSON.stringify(void 0===n?null:n),a=this._loads[e];if(!a||a.key!==o){a=this._loads[e]={key:o,pending:!0,error:null,value:void 0};var l=this;Promise.resolve().then(r).then(function(n){l._loads[e]===a&&(a.pending=!1,a.value=n,l.schedule(),t._onLoadSettled&&t._onLoadSettled(l))},function(n){l._loads[e]===a&&(a.pending=!1,a.error=n,l.schedule(),!i&&t._onUnhandledLoadError&&t._onUnhandledLoadError(l,e,n),t._onLoadSettled&&t._onLoadSettled(l))})}return a},reload:function(e){delete this._loads[e],this.schedule()},loading:function(e){var t=this._loads[e];return!t||t.pending},schedule:function(){var e=this;e._scheduled||(e._scheduled=!0,queueMicrotask(function(){e._scheduled=!1,e.render()}))},render:function(){if(!this._composing){var i,a=e.document,u=a&&a.activeElement,c=null;u&&u.getAttribute&&u.getAttribute("data-vl-bind")&&(!a.contains||a.contains(u))&&(c={node:u,key:u.getAttribute("data-vl-bind"),start:u.selectionStart,end:u.selectionEnd,direction:u.selectionDirection,scrollLeft:u.scrollLeft,scrollTop:u.scrollTop}),t.overlay.beginFrame(),t.testIdsEnabled&&(t._testFrame={ids:new Set,portals:[]});try{i=n(o,s);var d=t._testFrame;t._testFrame=null,d&&d.portals.forEach(function(e){e()})}catch(e){if(t._testFrame=null,t._testScopes=[],e&&/^render\.(duplicate_test_id|invalid_test_scope)$/.test(e.code||""))throw t.overlay._live=null,e;throw r.replaceChildren(t.el("pre",{class:"vl-error",text:String(e&&e.stack||e)},[])),e}if(Array.isArray(i)){var f=document.createDocumentFragment();l(f,i),r.replaceChild?p(r,Array.from(f.childNodes)):r.replaceChildren(f)}else r.replaceChild?p(r,[i]):r.replaceChildren(i);if(t.overlay.endFrame(),t.wireSkipLink(),c&&a){var v=a.contains&&a.contains(c.node)?c.node:null;if(!v&&a.querySelectorAll){var h=Array.prototype.filter.call(a.querySelectorAll("[data-vl-bind]"),function(e){return e.getAttribute("data-vl-bind")===c.key});1===h.length&&(v=h[0])}if(v&&a.activeElement!==v){if(v.focus({preventScroll:!0}),null!=c.start&&"function"==typeof v.setSelectionRange)try{v.setSelectionRange(c.start,c.end,c.direction)}catch(e){}v.scrollLeft=c.scrollLeft,v.scrollTop=c.scrollTop}}}}},s=t.subctx({_pfx:"",_host:u},""),c=e.document;return c&&c.addEventListener&&(c.addEventListener("compositionstart",function(){u._composing=!0}),c.addEventListener("compositionend",function(){u._composing=!1,u.schedule()})),u.render(),u},t._onUnhandledLoadError=null,t._onLoadSettled=null,t._mainLabel=null,t._modules=Object.create(null);var g=Object.create(null);t.registerModule=function(e,n){t._modules[e]=n},t.loadModule=function(n,r){if(t._modules[n])return Promise.resolve(t._modules[n]);if(g[n])return g[n];var i=e.document,o=new Promise(function(e,o){var a=i.createElement("script");a.src=r,a.onload=function(){t._modules[n]?e(t._modules[n]):o(new Error("module '"+n+"' did not register itself (loaded "+r+")"))},a.onerror=function(){o(new Error("failed to load module '"+n+"' from "+r))},i.head.appendChild(a)});return g[n]=o,o},e.VL=t}("undefined"!=typeof window?window:globalThis);
@@ -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 _lit_int, _lit_text, _rendered_children
62
+ from vlcheck.checks import STD_BUILT_FNS, _lit_int, _lit_text, _rendered_children
63
63
  from vlcheck.lexer import Lexer
64
64
  from vlcheck.parser import Parser
65
65
  from vlcheck.resolve import (_AUTH_API, AUTH_BUILT, _route_name,
@@ -117,44 +117,16 @@ _ATTR_TARGETS = {
117
117
  "alt": frozenset({"img", "area"}),
118
118
  }
119
119
 
120
- # std free functions that map straight onto the runtime's VL.std.*
121
- STD_FNS = {
122
- "filter", "map", "take", "count", "sum", "sort_by", "group_by", "chunk",
123
- "join", "split", "first_char", "int_to_float", "float_to_int",
124
- "trim", "upper", "lower", "keys", "values", "get_or",
125
- # list growth (spec section 20.9): concat(a, b) and append(xs, x) are the
126
- # only way to combine or grow a list. Decided over a `+` overload (would
127
- # make one symbol mean two things, and `xs + x` vs `xs + ys` is genuinely
128
- # ambiguous for a generating model) and spread syntax (new grammar for
129
- # one operation). Both work through the pipeline like any other std fn:
130
- # `xs |> append(x)`.
131
- "concat", "append",
132
- # std.str's `regex` (spec section 13): a whole-purpose string-vs-pattern
133
- # test, `matches(s, pattern)` -- see `VL.std.matches` (assets/vlrt.js)
134
- # for the exact semantics (never throws; caller supplies its own
135
- # anchors). The smallest general capability that permits exact
136
- # validation against an arbitrary frozen pattern, found needed when a
137
- # hand-rolled `.split()`-based email-shape check only caught a literal
138
- # space, not the full whitespace class a real pattern's `\s` covers.
139
- "matches",
140
- # std.str's `replace(s, target, replacement)` (spec section 13 already
141
- # names it as part of std.str's row; this build had not implemented it
142
- # until feature 17 (export) needed literal-substring replace-all for
143
- # RFC 4180 CSV field escaping -- see checks.py's matching entry for the
144
- # full reproduce-then-fix note. Keep in sync with checks.py,
145
- # assets/vlrt.js, and server_emit.py's VL.std shim.
146
- "replace",
147
- # std.str's `contains(s, part) bool`: case-sensitive substring test (an
148
- # empty `part` is true). Before this, a view had no way to ask "does
149
- # this text contain that text": `contains(...)` existed only as a
150
- # `where()` predicate, so a bare `contains(a, b)` in a derive built
151
- # clean and threw `ReferenceError: contains is not defined` (Testing
152
- # Lab search brief, Example Demo 2.3). A `where(col: contains(x))`
153
- # value never reaches this table: `_where_value_js` lowers predicate
154
- # calls to their `{op, v}` marker before generic call lowering. Keep in
155
- # sync with checks.py, assets/vlrt.js and server_emit.py's VL.std shim.
156
- "contains",
157
- }
120
+ # std free functions that map straight onto the runtime's VL.std.*. The set is
121
+ # vlcheck's `STD_BUILT_FNS`, imported rather than copied, so the checker can
122
+ # never accept a std call this emitter does not lower. Add a function there
123
+ # and to `VL.std` in assets/vlrt.js (server.js embeds the same block); the
124
+ # parity test in tests/test_build.py (`test_std_fn_parity`) fails until both
125
+ # exist. Per-function history (why `matches`, `replace`, `contains`,
126
+ # `concat`/`append` were added) is in `git log -S` on this file. A
127
+ # `where(col: contains(x))` value never reaches this table: `_where_value_js`
128
+ # lowers predicate calls to their `{op, v}` marker first.
129
+ STD_FNS = STD_BUILT_FNS
158
130
 
159
131
  # attr name -> CSS property, for attrs whose value is a length in px
160
132
  PX_KEYS = {
@@ -460,6 +432,7 @@ class Emitter:
460
432
  self.page: A.PageDecl | None = None # spec §8.11 `page title:` declaration
461
433
  self.service: A.ServiceDecl | None = None
462
434
  self.server_mode = False # a model backs a service route
435
+ self._route_depth = 0 # >0 inside route_block (a route body)
463
436
  self.state_names: set[str] = set()
464
437
  self.load_names: set[str] = set()
465
438
  self._counter = 0
@@ -3553,9 +3526,11 @@ class Emitter:
3553
3526
  Assignment is plain: a route has no view `state`, so `_emit_assign`
3554
3527
  (which turns a state name into `ctx.setState`) must not be used."""
3555
3528
  self._push_scope()
3529
+ self._route_depth += 1
3556
3530
  try:
3557
3531
  return self._route_block(stmts, decl, in_loop, tail)
3558
3532
  finally:
3533
+ self._route_depth -= 1
3559
3534
  self._pop_scope()
3560
3535
 
3561
3536
  def _route_block(self, stmts, decl: str, in_loop: bool, tail: bool) -> list[str]:
@@ -3693,6 +3668,11 @@ class Emitter:
3693
3668
  return segments
3694
3669
 
3695
3670
  def _parse_sub_expr(self, text: str):
3671
+ # The `{expr}` text is still inside the outer string literal, so a
3672
+ # string argument in it is written with escaped quotes:
3673
+ # `"{join(cats, \",\")}"`. Undo that one level (`\"` and `\\`)
3674
+ # before lexing; lexed raw, `\",\"` became the separator `",")`.
3675
+ text = re.sub(r'\\(["\\])', r"\1", text)
3696
3676
  toks = Lexer(text, self.filename).lex()
3697
3677
  return Parser(toks, text, self.filename).parse_expr()
3698
3678
 
@@ -3866,8 +3846,13 @@ class Emitter:
3866
3846
  if isinstance(callee, A.Name) and callee.id in STD_FNS:
3867
3847
  return "VL.std.%s(%s)" % (callee.id, ", ".join(self.expr(a) for a in e.args))
3868
3848
 
3869
- if self.server_mode and isinstance(callee, A.Name) and callee.id == "err" \
3870
- and len(e.args) == 1 and not e.named:
3849
+ # `err(...)` in a route body, on either target: the real server, or
3850
+ # the browser stub a service with no backing model runs as (there it
3851
+ # was emitted as a bare `err(...)` call that threw ReferenceError
3852
+ # instead of failing the call). vlcheck's E-ERR-CONTEXT rejects it
3853
+ # in a view.
3854
+ if ((self.server_mode or self._route_depth) and isinstance(callee, A.Name)
3855
+ and callee.id == "err" and len(e.args) == 1 and not e.named):
3871
3856
  return self._ex_ServerErr(e.args[0])
3872
3857
 
3873
3858
  args = [self.expr(a) for a in e.args]
@@ -37,6 +37,7 @@ from __future__ import annotations
37
37
 
38
38
  import json
39
39
  import re
40
+ from pathlib import Path
40
41
 
41
42
  from vlcheck import ast_nodes as A
42
43
 
@@ -124,6 +125,25 @@ def _sql_default(node):
124
125
  return None
125
126
 
126
127
 
128
+ _VLRT = Path(__file__).resolve().parent / "assets" / "vlrt.js"
129
+
130
+
131
+ def vl_std_js() -> str:
132
+ """The browser runtime's `VL.std` block (assets/vlrt.js, the lines between
133
+ its `// @vl-std-begin` and `// @vl-std-end` marker lines), for server.js.
134
+ One source for both targets, so a std function a view can call is never
135
+ missing from a route body. A missing or duplicated marker is a broken
136
+ compiler install, not a user error."""
137
+ lines = _VLRT.read_text(encoding="utf-8").splitlines()
138
+ marks = [i for i, ln in enumerate(lines)
139
+ if ln.strip() in ("// @vl-std-begin", "// @vl-std-end")]
140
+ if (len(marks) != 2 or lines[marks[0]].strip() != "// @vl-std-begin"
141
+ or lines[marks[1]].strip() != "// @vl-std-end"):
142
+ raise BuildError("internal: assets/vlrt.js must have exactly one "
143
+ "`// @vl-std-begin` / `// @vl-std-end` pair")
144
+ return "\n".join(lines[marks[0] + 1:marks[1]])
145
+
146
+
127
147
  class ServerEmitter:
128
148
  def __init__(self, module: A.Module, models: dict, service: A.ServiceDecl | None,
129
149
  filename: str = "<input>", auth: A.AuthDecl | None = None,
@@ -801,6 +821,7 @@ class ServerEmitter:
801
821
  self._auth_session_setting("idle_timeout"), 12 * 3600),
802
822
  "session_same_site": "Lax" if oauth_declared else "Strict",
803
823
  "spa_fallback": "true" if self.spa_fallback else "false",
824
+ "vl_std": vl_std_js(),
804
825
  }
805
826
 
806
827
 
@@ -1300,76 +1321,22 @@ function readBody(req) {
1300
1321
  function err(code, message) { const e = new Error(message || code); e.code = code; return e; }
1301
1322
 
1302
1323
  // Route bodies are lowered by the same Emitter as the browser target (see
1303
- // module docstring), so a call like `.len()` emits the same `VL.len(...)` on
1304
- // both targets -- but server.js is a standalone Node script with no VL
1305
- // runtime loaded. Found completing feature 10 (`page.len()` in a route body
1306
- // threw `ReferenceError: VL is not defined` at request time, past both
1307
- // vlcheck and precheck, neither of which inspects *which* std calls a route
1308
- // body makes). Minimal, proven-necessary compatibility shim, not a general
1309
- // port of vlrt.js: add the next VL.* function here only when a feature
1310
- // actually calls it server-side, the same discipline as std.ui and the
1311
- // where() predicates.
1324
+ // module docstring), so a route body emits the same `VL.len(...)` and
1325
+ // `VL.std.<name>(...)` calls a view does -- but server.js is a standalone
1326
+ // Node script with no VL runtime loaded. `VL.std` below is the browser
1327
+ // runtime's own block (assets/vlrt.js, between its @vl-std-begin/end
1328
+ // markers), copied in at build time, so every std function a view can call
1329
+ // exists here too. It used to be a hand-ported subset that fell behind
1330
+ // `STD_FNS` one function at a time (`matches`, `split`, `filter`, `count`,
1331
+ // `replace`, `contains`, `lower`, `trim`, `map`, `concat` were each found
1332
+ // missing by a route that called them).
1312
1333
  const VL = {
1313
1334
  len: (x) => (x == null ? 0 : x.length),
1314
- // Found completing feature 14 (pagination): a route body's `matches(s,
1315
- // pattern)` (std.str, spec section 13) compiles clean -- vlcheck's
1316
- // import check and precheck's construct check both pass -- but throws
1317
- // `TypeError: Cannot read properties of undefined (reading 'matches')`
1318
- // at request time, because this shim had no `.std` at all. Same
1319
- // pre-existing gap this file's own comment above already names ("add the
1320
- // next VL.* function here only when a feature actually calls it
1321
- // server-side"); ported verbatim from vlrt.js's `VL.std.matches` (never
1322
- // throws -- a malformed pattern fails closed, same contract both
1323
- // targets share).
1324
- std: {
1325
- matches: (s, pattern) => {
1326
- try { return new RegExp(String(pattern)).test(String(s)); }
1327
- catch (e) { return false; }
1328
- },
1329
- // action-43: a route body splitting a compound (created_at, id) cursor
1330
- // string (`cursor |> split("_")`) hit the same class of gap `matches`
1331
- // did above -- ported verbatim from vlrt.js's `VL.std.split`.
1332
- split: (s, sep) => String(s).split(sep),
1333
- // feature 16 (bulk operation): a route body computing `missing_ids`
1334
- // (which requested ids a scoped update actually matched) hit the same
1335
- // class of gap `matches`/`split` did above -- `VL.std.filter`/
1336
- // `VL.std.count` compile clean but threw `TypeError: VL.std.filter is
1337
- // not a function` at request time (a generic try/catch upstream
1338
- // flattened it to an opaque 500 "internal error", reproduced directly
1339
- // against the unmodified shim before this fix). Ported verbatim from
1340
- // vlrt.js's own `filter`/`count` (same null-safe `asArray` contract).
1341
- filter: (xs, f) => (xs == null ? [] : xs).filter((x) => f(x)),
1342
- count: (xs) => (xs == null ? [] : xs).length,
1343
- // feature 17 (export): a route body building a CSV line needs
1344
- // literal-substring replace-ALL to double an embedded '"' per RFC
1345
- // 4180 -- same pre-existing shim gap `matches`/`split`/`filter`/
1346
- // `count` above were each added for in turn. Ported verbatim from
1347
- // vlrt.js's `VL.std.replace` (split+join, not regex).
1348
- replace: (s, target, repl) => String(s).split(String(target)).join(String(repl)),
1349
- // std.str's substring test, ported verbatim from vlrt.js's
1350
- // `VL.std.contains`. A `where(col: contains(x))` value is the SQL LIKE
1351
- // predicate instead and never calls this.
1352
- contains: (s, part) => String(s).indexOf(String(part)) !== -1,
1353
- // The case-insensitive search the crib teaches
1354
- // (`contains(lower(a.title), lower(trim(q)))`) threw `VL.std.lower is
1355
- // not a function` in a route body. Ported verbatim from vlrt.js.
1356
- lower: (s) => String(s).toLowerCase(),
1357
- trim: (s) => String(s).trim(),
1358
- // feature 17 (export): the same route body maps each matched Article
1359
- // to its own escaped CSV line (`matching |> map(fn(a) ...)`) -- built
1360
- // clean (vlcheck/precheck do not check the shim's contents, same gap
1361
- // class as every entry above), then threw `TypeError: VL.std.map is
1362
- // not a function` at request time on the real running server,
1363
- // reproduced directly before this fix. Ported verbatim from vlrt.js's
1364
- // own `map` (same null-safe `asArray` contract as `filter`).
1365
- map: (xs, f) => (xs == null ? [] : xs).map((x) => f(x)),
1366
- // feature 17 (export): prepending the CSV header line to the mapped
1367
- // data rows (`concat([header], data_rows)`) hit the same shim gap --
1368
- // reproduced directly (`TypeError: VL.std.concat is not a function`)
1369
- // before this entry existed. Ported verbatim from vlrt.js's own
1370
- // `concat`.
1371
- concat: (a, b) => (a == null ? [] : a).concat(b == null ? [] : b),
1372
- },
1335
+ std: (function () {
1336
+ const VL = {};
1337
+ %(vl_std)s
1338
+ return VL.std;
1339
+ })(),
1373
1340
  };
1374
1341
 
1375
1342
  // Coerce one `?name=raw` query-string value or one decoded path segment to
package/language/spec.md CHANGED
@@ -1702,7 +1702,7 @@ The library is small on purpose. A large surface is a large thing to remember an
1702
1702
  | `std.db` | query builder, migrations, transactions |
1703
1703
  | `std.time` | `now`, formatting, parsing, arithmetic on `time` and `dur` |
1704
1704
  | `std.str` | split, join, trim, case, replace, contains (`contains(s, part) bool` — case-sensitive substring test, an empty `part` is `true`; distinct from the §13.1 `where()` predicate of the same name), regex (`matches(s, pattern) bool` — an unanchored whole-`RegExp`-source test, never throws; §6.2's model of "the compiler-owned surface" — see `volaro supported` for exactly what this build ships of this row) |
1705
- | `std.list` | map, filter, fold, sort_by, group_by, take, chunk, concat, append |
1705
+ | `std.list` | map, filter, fold, sort_by, group_by, take, chunk, concat, append, unique (`unique(xs) [T]` — the first occurrence of each distinct value, in list order; values compare by §4.3 equality, so two records are the same when their fields are equal) |
1706
1706
  | `std.map` | keys, values, merge, get_or |
1707
1707
  | `std.json` | encode, decode, pointer access |
1708
1708
  | `std.crypto` | random bytes, hashing, timing-safe compare |
@@ -422,9 +422,35 @@ audio video source track picture map area
422
422
  bare call is `E-METHOD-AS-FN`. `reload()` and `loading()` act on a
423
423
  `load`, so they are valid only on a name the view's own `load` binds
424
424
  (`E-LOAD-METHOD`); `loading()` is true while its fetch is in flight.
425
- The spec's `sorted`, `first`, `fold`, `merge` and `to_int` free
426
- functions are not built; a call to one in a view or route is
427
- `E-FN-NOT-BUILT`.
425
+ The std free functions this build has are the ones listed under
426
+ "Data / services" below (`concat`, `append`, `unique`, `matches`,
427
+ `contains`, `replace`), plus `filter`, `map`, `take`, `count`, `sum`,
428
+ `sort_by`, `group_by`, `chunk`, `join`, `split`, `first_char`, `trim`,
429
+ `upper`, `lower`, `keys`, `values`, `get_or`, `int_to_float` and
430
+ `float_to_int`. Any other std name (`sorted`, `first`, `fold`, `merge`,
431
+ `to_int`, `now()` outside a `model` default, `reverse`, `min`, `max`,
432
+ `starts_with`, …) is `E-FN-NOT-BUILT` in a view or route, and its
433
+ `fix:` line gives the working form.
434
+ - Other names the checker knows are callable only in one position, or not
435
+ at all. Each rejection has a `fix:` line.
436
+ - `where()` predicates (`lt`, `lte`, `gt`, `gte`, `ne`, `between`,
437
+ `any_of`) are only `.where(field: gt(x))` values, including either
438
+ branch of an `if` value. Anywhere else, `gt(x)` is
439
+ `E-WHERE-PREDICATE`; compare with the operator (`a > b`).
440
+ `contains` is also a std.str function, so it works everywhere.
441
+ - `err(...)` works only in a `service` or page.server route body. In a
442
+ view it is `E-ERR-CONTEXT`. An error code (`not_found`, `forbidden`,
443
+ `conflict`, …) or a payload variant of a union the file declares is
444
+ only the argument of `err(...)`. `not_found("x")` anywhere else is
445
+ `E-ERR-CONTEXT`. A service with no `model` runs its routes in the
446
+ browser, and `err(...)` fails the call there too.
447
+ - A namespace or ambient object (`db`, `env`, `log`, `route`, `auth`,
448
+ `session`, `request`, `api`, and the unbuilt `analytics`, `events`,
449
+ `ws`, `mailer`, `billing`), a type name (`str(x)`, `int(x)`, …), `ok`,
450
+ `asc`/`desc`, `google`/`github`, the spec example names `create_guest`
451
+ and `guest_user`, and an element or view used as a value
452
+ (`derive y = span(xs)`, `derive r = Row(1)`) are `E-NOT-A-FUNCTION`. A successful `T!` is the plain
453
+ value, so write `xs`, not `ok(xs)`. `str(x)` is `"{x}"`.
428
454
  - `len`, `format`, `reload` and `loading` are the only methods a value
429
455
  has. String and list operations are
430
456
  std functions: `lower(x)`, `trim(x)`, `xs |> filter(f)`,
@@ -481,6 +507,13 @@ audio video source track picture map area
481
507
  - `std.list` `concat` / `append`; `xs |> map|filter(fn(x) …)`. (`fold` is in
482
508
  `volaro spec`'s stdlib table but is **not implemented** in this build —
483
509
  found stale while documenting `matches` below; corrected here, not fixed.)
510
+ - `std.list` `unique(xs)` — the first occurrence of each distinct value, in
511
+ list order, as a new list. Scalars compare as `==` does (`1` and `"1"`
512
+ differ); records and lists compare by their fields and items. Works in
513
+ views, handlers and route bodies, and through the pipeline:
514
+ ```vl
515
+ derive cats = pub |> map(fn(a) a.category) |> unique()
516
+ ```
484
517
  - `std.str` `matches(s, pattern)` — a whole-string-vs-regex test, `std.str`'s
485
518
  `regex` entry in `volaro spec`'s stdlib table (section 13). `pattern` is a
486
519
  plain regex source string, no delimiters, no flags; the caller supplies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volaro",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.12",
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",