@jarenjs/json 0.9.2 → 0.34.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/ARCHITECTURE.md +86 -13
  2. package/README.md +248 -23
  3. package/dist/types/canonical.d.ts +37 -0
  4. package/dist/types/cow.d.ts +28 -0
  5. package/dist/types/errors.d.ts +45 -0
  6. package/dist/types/index.d.ts +3 -0
  7. package/dist/types/jslt/errors.d.ts +15 -8
  8. package/dist/types/jslt/index.d.ts +22 -0
  9. package/dist/types/jslt/packs/finance.d.ts +119 -0
  10. package/dist/types/jslt/packs/index.d.ts +310 -0
  11. package/dist/types/jslt/packs/math.d.ts +159 -0
  12. package/dist/types/jslt/packs/stats.d.ts +48 -0
  13. package/dist/types/jslt/registry.d.ts +65 -0
  14. package/dist/types/jtlt/errors.d.ts +3 -6
  15. package/dist/types/option-variants.d.ts +29 -0
  16. package/dist/types/patch.d.ts +214 -0
  17. package/dist/types/path.d.ts +139 -9
  18. package/dist/types/pointer.d.ts +100 -9
  19. package/dist/types/query/compile.d.ts +12 -0
  20. package/dist/types/query/errors.d.ts +72 -8
  21. package/dist/types/query/index.d.ts +317 -25
  22. package/dist/types/query/normalize.d.ts +24 -0
  23. package/dist/types/query/operators.d.ts +241 -1
  24. package/dist/types/query/runtime.d.ts +5 -8
  25. package/dist/types/query/types.d.ts +34 -0
  26. package/dist/types/segments.d.ts +31 -0
  27. package/dist/types/write.d.ts +204 -0
  28. package/dist/types/xquery/parse.d.ts +2 -3
  29. package/docs/JSLT-FORMAT.md +74 -3
  30. package/docs/JSLT-PRELUDE.md +1 -1
  31. package/docs/QUERY-FORMAT.md +695 -33
  32. package/package.json +18 -4
  33. package/schemas/geojson.draft-07.schema.json +323 -0
  34. package/schemas/geojson.jaren.schema.json +863 -0
  35. package/schemas/geojson.schema.json +172 -0
  36. package/schemas/jaren-jslt.authoring.schema.json +142 -0
  37. package/schemas/jaren-jslt.draft-07.schema.json +152 -11
  38. package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
  39. package/schemas/jaren-jslt.schema.json +152 -11
  40. package/schemas/jaren-query.draft-07.schema.json +152 -11
  41. package/schemas/jaren-query.llm-profile.schema.json +619 -0
  42. package/schemas/jaren-query.schema.json +82 -15
  43. package/src/basic.js +1 -1
  44. package/src/canonical.js +170 -0
  45. package/src/cow.js +106 -0
  46. package/src/errors.js +68 -0
  47. package/src/index.js +3 -0
  48. package/src/jslt/dispatch.js +178 -28
  49. package/src/jslt/errors.js +19 -14
  50. package/src/jslt/index.js +37 -29
  51. package/src/jslt/packs/finance.js +49 -0
  52. package/src/jslt/packs/index.js +18 -0
  53. package/src/jslt/packs/math.js +46 -0
  54. package/src/jslt/packs/stats.js +65 -0
  55. package/src/jslt/registry.js +200 -0
  56. package/src/jslt/stylesheet.js +14 -23
  57. package/src/jtlt/desugar.js +2 -3
  58. package/src/jtlt/errors.js +6 -12
  59. package/src/jtlt/index.js +12 -29
  60. package/src/jtlt/template.js +9 -18
  61. package/src/option-variants.js +54 -0
  62. package/src/patch.js +1052 -0
  63. package/src/path.js +319 -52
  64. package/src/pointer.js +225 -44
  65. package/src/query/compile.js +790 -75
  66. package/src/query/errors.js +72 -12
  67. package/src/query/index.js +274 -42
  68. package/src/query/normalize.js +489 -78
  69. package/src/query/operators.js +620 -23
  70. package/src/query/runtime.js +5 -19
  71. package/src/query/types.js +213 -0
  72. package/src/segments.js +409 -64
  73. package/src/write.js +660 -0
  74. package/src/xquery/parse.js +37 -53
package/ARCHITECTURE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @jarenjs/json Architecture
2
2
 
3
- This document describes the internals of `@jarenjs/json` for contributors: the JSON Pointer compiler, the RFC 9535 JSONPath compiler, the Jaren JSON Query engine with its XQuery text front-end, and the JSLT stylesheet dispatcher layered over the query compiler. The user-facing story is in the [README](./README.md); the language contracts are [docs/QUERY-FORMAT.md](./docs/QUERY-FORMAT.md) and [docs/JSLT-FORMAT.md](./docs/JSLT-FORMAT.md).
3
+ This document describes the internals of `@jarenjs/json` for contributors: the JSON Pointer compiler, the RFC 9535 JSONPath compiler, the Jaren JSON Query engine with its XQuery text front-end, the JSLT stylesheet dispatcher layered over the query compiler, and the JTLT text-template front-end that desugars onto JSLT. The user-facing story is in the [README](./README.md); the language contracts are [docs/QUERY-FORMAT.md](./docs/QUERY-FORMAT.md), [docs/JSLT-FORMAT.md](./docs/JSLT-FORMAT.md) and [docs/JTLT-FORMAT.md](./docs/JTLT-FORMAT.md).
4
4
 
5
5
  Everything here follows the house architecture of the schema validator (see [`packages/validate/ARCHITECTURE.md`](../validate/ARCHITECTURE.md)): **two-stage compilers** — parse and normalize once into an AST, then compile the AST into specialized closures with every decidable decision made at compile time. No `eval`, no `new Function` (CSP-safe), no allocation on hot paths, monomorphic closures wherever the engine can arrange it.
6
6
 
@@ -9,9 +9,13 @@ Everything here follows the house architecture of the schema validator (see [`pa
9
9
  | File | Purpose |
10
10
  |------|---------|
11
11
  | `src/basic.js` | string validation for JSON, JSON Pointer, JSONPath (`isValidJSON`, `isValidJSONPointer`, `isValidJSONPathStrict`, ...) |
12
- | `src/pointer.js` | the RFC 6901 + Relative JSON Pointer compiler (`compileJSONPointer`, `compileRelativeJSONPointer`, `compileDataRef`) |
13
- | `src/path.js` | the JSONPath compiler: parser, nodes-mode compilers (normalized paths), public API |
14
- | `src/segments.js` | package-internal runtime segment machinery shared by `path.js` and the query engine (not exported) |
12
+ | `src/canonical.js` | Canonical JSON (RFC 8785 / JCS): `canonicalizeJson` deterministic serialization for hashing and signing |
13
+ | `src/pointer.js` | the RFC 6901 + Relative JSON Pointer compiler (`compileJSONPointer`, `compileRelativeJSONPointer`, `compileDataRef`) and the reference-token codec (`encodeJSONPointerSegment`, `decodeJSONPointerSegment`, `formatJSONPointer`) |
14
+ | `src/cow.js` | package-internal copy-on-write core: owned-set state, spine cloning, step encoding (not exported) |
15
+ | `src/patch.js` | JSON Patch (RFC 6902) + JSON Merge Patch (RFC 7396): compiled copy-on-write appliers and the structural diffs |
16
+ | `src/write.js` | standalone write operations: set/insert/remove at pointers, normalized paths, or every JSONPath match |
17
+ | `src/path.js` | the JSONPath compiler: parser, function-extension registry, nodes-mode compilers (normalized paths), public API, the pointer bridge |
18
+ | `src/segments.js` | package-internal runtime segment machinery shared by `path.js` and the query engine — values, nodes and lazy modes (not exported) |
15
19
  | `src/query/errors.js` | `JsonQueryCompileError` / `JsonQueryRuntimeError` with `code` + `docPath` |
16
20
  | `src/query/runtime.js` | the tagged sequence representation (`EMPTY` / raw item / `Seq`), EBV, `stableKeyString` |
17
21
  | `src/query/normalize.js` | query document → frozen AST: all `JQ0xxx` checks, scopes, slots, externals, cardinality |
@@ -22,24 +26,58 @@ Everything here follows the house architecture of the schema validator (see [`pa
22
26
  | `src/jslt/dispatch.js` | match/body compilation, lazy path pre-passes, ranked dispatch, built-in rules |
23
27
  | `src/jslt/errors.js` | `JsltCompileError` / `JsltRuntimeError` with `code` + stylesheet `docPath` |
24
28
  | `src/jslt/index.js` | public API: `compileJsltStylesheet`, `transformJson`, result unwrapping, cache variants |
29
+ | `src/jtlt/template.js` | template envelope/rule normalization: closed shapes, the `output` method |
30
+ | `src/jtlt/desugar.js` | template → JSLT 0.1 stylesheet: rule bodies become array constructors of tagged segment pairs |
31
+ | `src/jtlt/writer.js` | serializes the desugared tagged-segment stream to `text`/`xml`, escaping interpolated data only |
32
+ | `src/jtlt/errors.js` | `JtltCompileError` / `JtltRuntimeError` with `code` + template `docPath` |
33
+ | `src/jtlt/index.js` | public API: `compileJtltStylesheet`, `renderText`, the exposed `render.stylesheet` |
25
34
  | `src/xquery/parse.js` | the XQuery text front-end: `parseXQuery(text)` → query document |
26
35
  | `src/xquery/index.js` | `parseXQuery`, `compileXQuery`, `XQuerySyntaxError` |
27
36
 
28
- Dependency direction: `basic.js` stands alone; `pointer.js` shares only the `NOTHING` sentinel from `segments.js`; `path.js` builds on `segments.js`; the query engine builds on `segments.js` (paths) and `runtime.js`; JSLT consumes the query normalizer/compiler through its package-internal extension point and the nodes-mode segment runner; the XQuery front-end emits query documents and depends only on the JSON format, never on engine internals. `@jarenjs/core` supplies char-code scanning, `equalsJson`, code-point helpers and I-Regexp compilation. JSON Schema remains an injected predicate hook: `@jarenjs/validate/query` may wire into query/JSLT, but this package never imports the validator.
37
+ Dependency direction: `basic.js` and `canonical.js` stand alone; `pointer.js` shares only the `NOTHING` sentinel and the array-index scanner from `segments.js`; `cow.js` stands alone as the package-internal copy-on-write core; `patch.js` builds on the pointer parser, the segment helpers and `cow.js`; `write.js` builds on `cow.js`, the pointer parser and the JSONPath compiler; `path.js` builds on `segments.js`; the query engine builds on `segments.js` (paths) and `runtime.js`; JSLT consumes the query normalizer/compiler through its package-internal extension point and the nodes-mode segment runner; JTLT is a front-end that desugars templates into ordinary JSLT stylesheets and serializes the dispatched result, depending only on the jslt module (through its public compiler) plus its own writer; the XQuery front-end emits query documents and depends only on the JSON format, never on engine internals. `@jarenjs/core` supplies char-code scanning, `equalsJson`, code-point helpers and I-Regexp compilation. JSON Schema remains an injected predicate hook: `@jarenjs/validate/query` may wire into query/JSLT, but this package never imports the validator.
29
38
 
30
39
  ## The two-stage pipeline
31
40
 
32
41
  All compilers in this package have the same shape:
33
42
 
34
- ```
35
- source ──[stage 1: parse / normalize]──► frozen AST ──[stage 2: compile]──► closure tree ──► run(data)
43
+ ```mermaid
44
+ flowchart LR
45
+ S["source"] -->|"stage 1<br/>parse / normalize"| A["frozen AST"]
46
+ A -->|"stage 2<br/>compile"| C["closure tree"]
47
+ C --> R["run(data)"]
48
+ N["every decidable decision<br/>is made before run()"]
49
+ C -.- N
50
+ class N note
36
51
  ```
37
52
 
38
53
  Stage 1 owns *all* static errors: the JSONPath parser is a single-pass, character-level recursive-descent parser (the `fail(message, position)` idiom, `JSONPathSyntaxError`), and the query normalizer raises every `JQ0xxx` compile error with a `docPath`. Stage 2 never re-checks structure; it specializes: selector kinds, slice bound arithmetic, comparison operators, literal regexes, operator arities and cardinality fast paths are all resolved before the first document is seen. A compiled query closes over nothing mutable and is reusable across documents and calls.
39
54
 
55
+ For the query engine, stage 1's output is a **published contract**, not an internal shape: `analyzeQuery` (the `./query` subpath) returns the frozen normalized tree, `NODE_KINDS` enumerates its twelve node kinds, and `AST_VERSION` versions the shape under the compatibility policy of QUERY-FORMAT.md Appendix C. Stage 2 — the closures — remains unpromised and changes freely. The reasoning: an exhaustive consumer of the normalized form (a translator, a planner) fails loudly when the language grows a construct, where a raw-document walker would silently degrade it; publishing stage 1 freezes the language's *resolved* form, not the implementation. An exhaustiveness gate in `test/json/query/` keeps `NODE_KINDS`, the appendix's table and the operator count in agreement with the code.
56
+
40
57
  ### JSON Pointer: segment-count specialization
41
58
 
42
- `pointer.js` follows the pipeline in miniature. The strict parsers (`parseJSONPointer`, `parseRelativeJSONPointer`) are single-pass char-code scanners with a lazy-decode fast path: an escape-free segment is a direct slice, and only segments containing `~` build a decoded string. The compilers pre-decode every member name and pre-parse every array index (one segment, two forms — RFC 6901 lets `"2"` address both a `"2"` member and array element 2), then specialize the getter by segment count (0 = identity, 1 and 2 = unrolled hops, N = a loop over parallel name/index arrays). A relative pointer trims its level count off the runtime location by scanning **backwards** for the N-th `/` — no split, no arrays — and resolution returns the `NOTHING` sentinel shared with `segments.js`, so pointer and JSONPath results compose. Nothing is allocated on any resolution path.
59
+ `pointer.js` follows the pipeline in miniature. The strict parsers (`parseJSONPointer`, `parseRelativeJSONPointer`) are single-pass char-code scanners with a lazy-decode fast path: an escape-free segment is a direct slice, and only segments containing `~` build a decoded string. The compilers pre-decode every member name and pre-parse every array index (one segment, two forms — RFC 6901 lets `"2"` address both a `"2"` member and array element 2), then specialize the getter by segment count (0 = identity, 1 to 4 = unrolled hops, N = a loop over parallel name/index arrays). A relative pointer trims its level count off the runtime location by scanning **backwards** for the N-th `/` — no split, no arrays — and resolution returns the `NOTHING` sentinel shared with `segments.js`, so pointer and JSONPath results compose. Nothing is allocated on any resolution path.
60
+
61
+ Four deliberate divergences are worth stating, because they are correct-but-surprising and load-bearing for the validator:
62
+
63
+ - **Own-property reads only.** A `hop` into an object uses `Object.hasOwn`, never a prototype walk, so `/toString` (and any other inherited name) resolves to `NOTHING` rather than the prototype method. RFC 6901 addresses members, and only own members are members.
64
+ - **Strict array-index parsing.** The one-token-two-forms array index goes through `scanArrayIndex` (`segments.js`): a leading-zero form (`01`), a non-digit tail (`1abc`), an exponent (`1e0`) and anything above `2³²−2` (`MAX_ARRAY_INDEX = 4294967294`) all reject as an index, falling back to the member-name form. Only a canonical non-negative integer addresses an array element.
65
+ - **The `0#` hash form yields the member *name* by default.** The `#` form resolves to the location's last segment **as a string**, a deliberate divergence from Relative JSON Pointer, which yields a *number* for an array position. The string form stays the default because the validator's `$data`/`data` keyword tests rely on it, and because it answers from the location string alone — no document access, tens of nanoseconds. The draft's answer is available per compile as `{ hashIndex: 'number' }`, which pays for a walk to the location's parent to find out whether it is an array (an object member named `"1"` is still the string `"1"` in that mode, since only the container decides); it falls back to the string when the parent is unreachable, and an unknown mode is a compile-time `TypeError`. Two related rules are *not* divergences: the root has no name, so `0#` there yields `NOTHING` rather than `''` (which would collide with the member a document can genuinely name `''`), and the `#` form answers from the location string alone without walking the document — it does not verify the location exists, which is what keeps it a string operation rather than a walk.
66
+ - **The relative-resolver `dataPath` contract.** A non-string location is coerced to `''` (the root); a non-empty location not starting with `/` returns `NOTHING`; and the lazy `~` decode (`decodeSegmentRange`) is *lax* — an invalid escape is kept literally rather than raising — because location paths are machine-generated by the validator through raw string concatenation, and the resolver must mirror exactly how they were built.
67
+
68
+ ### JSON Patch: compiled operations over copy-on-write
69
+
70
+ `patch.js` runs the pipeline over *two* documents: the patch compiles once, the data document is what varies per call. Stage 1 (`compileJSONPatch`) owns all `JP0xxx` errors — operation shapes, unknown ops, pointer syntax (wrapping the `JSONPointerSyntaxError` as `cause`), the move `from`-is-prefix-of-`path` rule — each with a `docPath` into the patch document. Every `path`/`from` is pre-parsed into decoded member names alongside pre-scanned array indexes (the pointer compiler's one-token-two-forms rule), with the last token split off for the mutating operations, and each operation becomes one closure with its error strings pre-bound.
71
+
72
+ Stage 2 is a copy-on-write interpreter of those closures. An application carries `{root, owned}` where `owned` is the set of nodes this application created: the first write along a path shallow-clones the spine from the root and registers the clones; later writes find the spine in the set and mutate in place. Consequences: the input is never touched (RFC 6902's atomic-application requirement costs nothing — a failing op just abandons the state), untouched subtrees are shared with the result (the JSLT `share` discipline), and k operations through one region cost one spine copy. `copy` deep-clones its source only when the subtree contains owned nodes (otherwise the inserted alias could be mutated through by a later operation); `mutate: true` sets `owned = null` (everything owned, nothing cloned) and then forces per-application deep copies of inserted values, since in-place results must not share structure with the patch document. Object member writes go through the `setMember`/`__proto__` discipline of the query engine, and the object shallow clone relies on spread's `CreateDataProperty` semantics for the same reason.
73
+
74
+ `test` and `copy`-reads never clone — they run a plain pre-compiled walk against the current root. The merge-patch side (`compileMergePatch`) pre-splits each patch level into remove/set/merge plans and applies them identity-preservingly: a level that changes nothing returns its target by reference, which makes a no-op merge return the input document itself. The structural diffs (`createJSONPatch`, `createMergePatch`) share `equalsJson`; the array diff trims the deep-equal common prefix/suffix and recurses index-wise over the overlap — linear and minimal for in-place edits and head/tail insertions, correct-but-larger for a mid-array insertion, which degrades to a run of per-index replaces. `arrayDiff: 'minimal'` aligns the changed middle instead. The cost model is the patch itself — a kept pair is free, a rewrite, an insert and a delete are one step each — so the alignment is **edit distance with substitutions, not a longest common subsequence**: LCS maximizes kept elements, a different objective that loses on a permutation, where it spends a delete plus an insert on what one rewrite covers. Because pairing every overlapping position is itself a valid alignment, the minimal mode never takes more steps than the default. It stays opt-in because it is O(m·n) in the length of the changed middle against the default's linear pass; elements are interned to integer ids (via `stableStringify`) so the inner loop compares integers and only confirms candidates with `equalsJson`, and a middle above a fixed cell budget falls back to the index-wise diff so the option can never turn a large diff quadratic.
75
+
76
+ The owned-set machinery itself lives in the package-internal `cow.js` (like `segments.js`, deliberately unexported), because `write.js` shares it. Its step encoding covers both addressing worlds in one pair of parallel arrays: the RFC 6901 form keeps every token in `names[i]` with its pre-scanned index form in `indexes[i]` (one token, two forms), while the JSONPath-derived typed form marks index selectors with `names[i] = null` — which also gives typed steps negative (from-the-end) indexes for free. `patch.js` and `write.js` keep their own ~20-line walk loops over these arrays so each raises its own error type with its own location fields.
77
+
78
+ ### Write operations: reverse document order
79
+
80
+ `write.js` publishes the standalone set/insert/remove writers. Pointer-addressed targets dispatch once at compile time (`$` → parse as JSONPath and require a singular query — which admits every RFC 9535 normalized path; otherwise the RFC 6901 parser), then compile to a closure over the pre-parsed steps. The query-selected writers (`compileJSONPathSetter` et al.) compile the query once; each application runs it in nodes mode and rewrites the matched normalized paths in **reverse document order** — descendants before ancestors, later siblings before earlier ones — so array-index shifts from inserts/removes never invalidate the remaining locations, nested matches compose, and an ancestor rewrite deterministically wins over rewrites inside it. Normalized paths are engine-generated, so the scanner that turns them back into typed steps (`scanNormalizedSteps`) is total for its input. The path↔pointer bridge (`jsonPointerFromJSONPath`, `jsonPathFromJSONPointer`) lives in `path.js` with the documented digit-token convention for the pointer→path direction.
43
81
 
44
82
  ### JSONPath: parser, segments, two output modes
45
83
 
@@ -47,6 +85,16 @@ Stage 1 owns *all* static errors: the JSONPath parser is a single-pass, characte
47
85
 
48
86
  Values mode is the default and the fast path; nodes mode (normalized paths for `query.nodes()`/`query.paths()`) is compiled **lazily on first use**, so value-only queries never pay for path-string production.
49
87
 
88
+ A third mode is pulled rather than pushed: `compileSegmentG`/`runSegmentsG` are the same selectors as values mode written as generators, so a consumer that stops early never visits the rest of the document. `query.iterate(data)` exposes it, and `query.first()`/`query.exists()` are one pull of it — a filter evaluates its predicate only until a node passes, a wildcard reads only the children pulled, a descendant segment abandons the walk mid-subtree. Nothing is buffered, so the laziness is per node rather than per segment. It is also compiled lazily, so a query that only calls `values()` never pays for it. The equivalence that makes this safe: a segment maps each input node to its outputs in order and concatenates, so pulling the chain depth-first yields exactly the sequence values mode builds breadth-first.
89
+
90
+ Laziness deliberately stops at the top of a query. The existence test inside a *filter* (`compileExists`) and `value()`'s one-node test still build their nodelists, because they run per candidate node where nodelists are a handful of items and generator setup costs more than the pushes it saves — the generator form benched about 9× slower on `$.items[?@.tags[*]]` over 2000 items.
91
+
92
+ A `$`-rooted comparable inside a filter (`[?@.price < $.config.max]`) is invariant across the candidates of one filter application, so it is memoized: computed at the first candidate, reused for the rest, and reset before each application. The reset hooks travel from the predicate tree to the selector that runs it (`compileFilterPredicate`), because a plain root-keyed memo would be wrong — a caller may mutate the document and re-run the query on the same root identity. A guard on root identity additionally covers a reentrant run against a different document. Hoisting the walk out of the loop measured ~2.9× on a three-segment comparable over 2000 candidates (~1.6× on a one-segment one); a filter with no absolute comparables compiles exactly as before.
93
+
94
+ Function extensions (RFC 9535 §2.4) are a registry rather than a fixed table. `options.pathFunctions` declares each extension's parameter and result types, so the parser applies the same well-typedness rules (§2.4.3) to a custom function as to a built-in, and rejects a name that would redefine one (§2.4.1). Because the declared types are known before an argument is parsed, argument parsing is type-directed: a `LogicalType` parameter takes the whole `logical-expr` production, which is how `!`, `&&` and comparisons become legal in that position and nowhere else. The registry is resolved once per registry object (memoized in a `WeakMap`) and the resulting table is null-prototype, so an inherited member name (`constructor`) is an unknown function rather than a half-formed descriptor. Threading reaches every entry point that embeds a path: `compileJSONPath`/`queryJSONPath`/`isValidJSONPathStrict`, the JSONPath-addressed writers in `write.js`, the query normalizer's `parsePathString` (both the absolute and the variable-rooted call sites), and the JSLT dispatcher's match paths and rule bodies. `queryJSONPath`'s compiled-query cache is keyed per registry, since one source compiles differently under different extensions.
95
+
96
+ The `json-path` string format resolves to the built-ins and nothing else, and so does `json-path-segments`: a format is a property of the string itself and must mean the same thing in every schema, independent of which extensions some host installed. A host that wants its own extensions asserted registers a tester bound to them.
97
+
50
98
  ## The query engine
51
99
 
52
100
  ### The sequence representation
@@ -73,11 +121,11 @@ Scoping is entirely compile-time: a linked chain of `{name, slot, card, parent}`
73
121
 
74
122
  ### FLWOR: streaming clauses, blocking clauses, liveness
75
123
 
76
- A FLWOR phrase compiles to a chain of nested `(frame, out) → void` closures. **There are no tuple objects**: a tuple *is* the current state of the frame slots. Clauses apply in the spec's fixed semantic order (`$for → $let → $where → $groupby → $orderby → $count → $return`) regardless of JSON key order.
124
+ A FLWOR phrase compiles to a chain of nested `(frame, out) → void` closures. **There are no tuple objects**: a tuple *is* the current state of the frame slots. Clauses apply in the spec's fixed semantic order (`$fold → $for → $let → $as → $where → $groupby → $orderby → $count → $return`) regardless of JSON key order.
77
125
 
78
126
  Streaming clauses never materialize the tuple stream:
79
127
 
80
- - `$for` iterates its source sequence (with the spec's D4 one-level array unpacking), rebinding its slot per tuple; multiple bindings nest left-to-right and may be correlated. The `$at` positional form keeps a per-activation 0-based counter.
128
+ - `$for` iterates its source sequence (with the spec's D4 one-level array unpacking), rebinding its slot per tuple; multiple bindings nest left-to-right and may be correlated. The `$at` positional form keeps a per-activation 0-based counter. `$allowing-empty` tests up front whether the source yields any tuple *after* unpacking and, if not, emits one with the slot bound to `EMPTY` (position `-1`); a `$window` binding materializes the unpacked item stream once and slices runs out of it.
81
129
  - `$let` writes its slot once per surrounding tuple.
82
130
  - `$where` gates the chain on the effective boolean value.
83
131
  - `$count` numbers surviving tuples through its own frame slot (reset per phrase evaluation — safe because a phrase cannot re-enter within one frame).
@@ -87,15 +135,35 @@ Streaming clauses never materialize the tuple stream:
87
135
  - `$orderby` runs a Schwartzian sort: each surviving tuple appends a `[key₁, ..., keyₙ, snapshot]` row, `Array.prototype.sort` (stable) compares precomputed keys, then the snapshots replay into the frame. Key type errors (`JQ2005`) are raised eagerly at key evaluation; empty keys order per `$empty` (least/greatest as ±∞ before direction).
88
136
  - `$groupby` accumulates a `Map` from a composite `stableKeyString` key to the group, in first-appearance order; grouping-key variables rebind to the key values, every other live variable rebinds to the *sequence* of its values across the group's tuples.
89
137
 
138
+ `$fold` needs no fifth driver pair. It replaces the collecting sink with one that assigns the accumulator slot, then wraps whichever of the four drivers was built: seed the slot, run the tuple stream unchanged, read the slot back. Both barriers, `$count` and `$where` therefore compose with it for free — a fold under `$orderby` reduces over sorted tuples because the replay loop calls the same sink.
139
+
140
+ ### FLWOR: the two compile-time rewrites
141
+
142
+ Both are pure specializations — they change the closures emitted, never the answer — and both are declined whenever the rewrite would be observable.
143
+
144
+ **Range iteration.** A `$for` or quantifier binding whose source is *statically* a `$range` compiles to a counting loop instead of materializing the sequence to walk it once. Memory drops from O(n) to O(1), so the `JQ2007` guard stops being the only thing between a query and the heap (a 50-million-item `$for` allocates nothing); a quantifier additionally stops at its witness. The loop is written out at each use site rather than shared through a callback, because an indirect call per iterated number costs more than the duplication saves. A `$range` that is genuinely materialized — bound by `$let`, handed to an aggregate — keeps the guard.
145
+
146
+ **Hash joins.** `$for a, $for b` with an equality `$where` is O(|a|·|b|) as nested loops. When the inner binding is *uncorrelated* (`collectReadSlots` proves its source reads no outer binding slot), the equality is answered from a `Map` built once over the inner side, making it O(|a| + |b|) — measured at 227 ms → 1.0 ms for 2000×2000, with identical rows.
147
+
148
+ The planner declines unless the rewrite is provably invisible: no `$as` and no `$let` in the phrase (both run per tuple *between* `$for` and `$where`, so forming fewer tuples would retract an assertion or skip a failure); no `$at`/`$allowing-empty`/`$window` on the probe binding; the equality is the whole `$where` or its **first** `$and` conjunct (so nothing that used to be evaluated first is skipped); both key expressions are paths or variable references, whose only failure is `JQ2006`, so moving *when* they are evaluated cannot move an error; and neither key is statically `MANY`, since `$eq` is existential over sequences. Buckets key on `stableKeyString`, which agrees with the `$eq` relation (`equalsJson`) on every JSON value except `NaN` — dropped on both sides, which is what `$eq` already does. The table is closure state refreshed once per phrase evaluation, which is sound because the language has no recursion: a compiled phrase can never be re-entered while it runs.
149
+
150
+ **Spatial joins.** The same rewrite for `$within` and `$bbox-intersects`: index the uncorrelated inner side's bounding boxes once (`createBboxIndex`, a static packed-Hilbert R-tree in `@jarenjs/core/geo`) and probe it per outer tuple. Measured 14× on 200 points against 800 regions, and the index itself is 538× faster than a scan at 100k boxes.
151
+
152
+ It differs from the hash join in one way that makes it simpler to prove correct: it does **not** consume the predicate. Box overlap is a *necessary* condition for both operators — a position inside a surface lies inside that surface's box — so the index can only remove candidates that would have failed anyway, and `$where` still runs unchanged on every survivor. The surviving tuples are decided by the same closure either way.
153
+
154
+ That leaves exactly one hazard: a tuple the index rejects never reaches the predicate, so an error the scan would have raised could be swallowed. Two guards close it. An inner item whose box cannot be computed goes into an always-check list rather than the tree, so a malformed operand still raises its `JQ2001`; and when the *outer* side has no box the probe falls back to the full scan. The planner also declines unless the predicate is the entire `$where` — an `$and` could throw in a conjunct the index skipped — and unless both operands are paths or variables, whose only failure is `JQ2006`.
155
+
156
+ `limits.steps` is the third compile-time switch. Instrumentation is opt-in at the `compileNode` dispatch point: with no step limit the compiler emits exactly the closures it always did, and with one it wraps every node in a counter check whose counter lives in its own frame slot. `limits.depth` needs no runtime support at all — with no recursion in the language, evaluation depth *is* the AST's static depth, so it is measured once at normalize time (`JQ0011`).
157
+
90
158
  Quantifier phrases (`$some`/`$every`) compile to early-exit loop nests over the same binding machinery — the first witnessing (or failing) tuple ends evaluation, and later runtime errors are never raised.
91
159
 
92
160
  ### `stableKeyString`
93
161
 
94
- Grouping, `$distinct` and the `$orderby` machinery need a total, deterministic equality that deep-equal JSON values agree on. `stableKeyString(value)` (runtime.js) serializes: strings via `JSON.stringify` (escape discipline guarantees no raw control characters, so `U+0000` safely separates composite keys), numbers bare with `-0` → `'0'` and `NaN`/`Infinity` by name, object keys sorted by code units, arrays and objects in JSON shape. Consequences worth knowing: `NaN` groups with `NaN` (the XQuery grouping rule; `$eq`'s relation — used by `$index-of` — matches nothing for `NaN`), and the two relations differ *only* there. This function is also the natural seed for the roadmap's canonical-JSON (RFC 8785) item.
162
+ Grouping, `$distinct` and the `$orderby` machinery need a total, deterministic equality that deep-equal JSON values agree on. `stableKeyString(value)` (runtime.js) serializes: strings via `JSON.stringify` (escape discipline guarantees no raw control characters, so `U+0000` safely separates composite keys), numbers bare with `-0` → `'0'` and `NaN`/`Infinity` by name, object keys sorted by code units, arrays and objects in JSON shape. Consequences worth knowing: `NaN` groups with `NaN` (the XQuery grouping rule; `$eq`'s relation — used by `$index-of` — matches nothing for `NaN`), and the two relations differ *only* there. It is deliberately **not** the canonical-JSON serializer: `canonicalizeJson` (canonical.js, RFC 8785) rejects `NaN`/`Infinity` outright, where grouping needs them representable so that `NaN` can group with `NaN`. The package now has three stable serializers with three jobs — this one is a grouping key, `stableStringify` (`@jarenjs/core/object`) is a cache fingerprint that follows `JSON.stringify` leniency, and `canonicalizeJson` is the interchange format that rejects every non-JSON input instead of coercing it, because a canonicalizer that quietly rewrote its input would sign a document nobody sent.
95
163
 
96
164
  ### The operator registry
97
165
 
98
- All 58 §8 operators live in one table in `operators.js` — the query-language analogue of `path.js`'s `FUNCTIONS` table:
166
+ All 93 §8 operators live in one table in `operators.js` — the query-language analogue of `path.js`'s `FUNCTIONS` table (the count is cross-checked against QUERY-FORMAT §8 by a test, so it cannot go stale again):
99
167
 
100
168
  ```javascript
101
169
  '$substring': {
@@ -123,6 +191,7 @@ The performance culture is the same as the JSONPath compiler's — specializatio
123
191
  - **Cardinality-specialized constructors**: map members and array elements that are statically `ONE` compile to direct assignment/push; only `MANY`/`OPT` positions pay for `appendItem` flattening.
124
192
  - **Item vs existential comparisons**: when both sides are statically `ONE`, `$eq`/`$lt`/... compile to a single item comparison — the O(n·m) existential double loop exists only where sequences are possible.
125
193
  - **Existence tests never materialize**: `$exists`/`$empty` over a path compile to `compileExists` (first hit wins), and `$where` over a bare path tests emptiness without building a nodelist.
194
+ - **Early exit at the top of a query**: `query.first()`/`query.exists()`/`query.iterate()` pull the lazy segment chain, so they stop at the first hit instead of materializing the nodelist — O(1) rather than O(document) on a wildcard, filter or descendant segment.
126
195
  - **Guard-free arithmetic**: `ONE`-card operands skip the empty-propagation branches.
127
196
  - **Literal regexes** (`$match`/`$search`/`$replace`, and path filters) are compiled to `RegExp` at query-compile time; dynamic patterns get a per-callsite monomorphic cache.
128
197
  - **Barriers snapshot only live slots** (`collectReadSlots` liveness), so a sort key that `$return` ignores is never copied per tuple.
@@ -150,7 +219,7 @@ That one entry is also the pruning index: a single hash lookup answers "does a r
150
219
 
151
220
  Rule bodies use the ordinary query normalizer/compiler with one injected extension entry, `$apply`; the core registry and published query schema remain unchanged. The extension normalizes the selector as an expression, freezes the static target mode, records every referenced mode for table construction, and compiles through the query engine's own cardinality/sequence machinery.
152
221
 
153
- Every compiled body extends the query frame by exactly three numeric slots after `normalized.frameSize`: current normalized location, dispatch depth, and the transform context — keeping every frame access an indexed element load. The transform context carries the root, per-rule external arrays and the lazy mode pre-passes without consuming query-visible slots; the all-unbound external resolution is precomputed at compile time, so calls without user bindings allocate nothing per transformation. Reserved externals `root` and `path` are detected among the normalizer's external records and populated per dispatch; user externals are deduplicated globally, resolved once per transformation, then projected into each rule's frame slots.
222
+ Every compiled body extends the query frame by exactly three numeric slots after `normalized.frameSize`: current normalized location, dispatch depth, and the transform context — keeping every frame access an indexed element load. The three slots are allocated **unconditionally**, even for bodies that never read a location or `$apply`: a conditional variant that grew the frame only when a body used those slots benched about 30% slower, because a single frame shape keeps the body closures monomorphic under V8 — the uniform frame is the faster shape even when some of it goes unused. The transform context carries the root, per-rule external arrays and the lazy mode pre-passes without consuming query-visible slots; the all-unbound external resolution is precomputed at compile time, so calls without user bindings allocate nothing per transformation. Reserved externals `root` and `path` are detected among the normalizer's external records and populated per dispatch; user externals are deduplicated globally, resolved once per transformation, then projected into each rule's frame slots.
154
223
 
155
224
  When an `$apply` selector is a path rooted at the current item or reserved `root`, its compiled AST exposes that fact. The extension runs the shared nodes-mode segments from the current normalized location, preserving paths into nested dispatch; every other selector evaluates through its ordinary getter and produces location-less items. Result sequences concatenate through `appendItem`/`seqOf`, exactly like every query operator.
156
225
 
@@ -160,6 +229,10 @@ The built-in `share`/`fresh` walkers implement query constructor semantics: empt
160
229
 
161
230
  Compilation wraps only errors crossing a language boundary: invalid match paths become JT0003 with the JSONPath cause, a missing type-test hook is JT0006 while hook rejection or a non-function result is JT0005, and `JsonQueryCompileError` from a body becomes JT0007 with the inner pointer composed under `/body`. At runtime, a rule body wraps `JsonQueryRuntimeError` once as JT2004; an existing `JsltRuntimeError` from nested `$apply` passes through unchanged. Dispatch depth is checked before each recursive call, and a host `RangeError` from a deep synchronous chain is converted to JT2001 so the public API never leaks an engine stack overflow.
162
231
 
232
+ ## The JTLT text front-end
233
+
234
+ `jtlt/` is a **front-end, not a second engine**, the same relationship `xquery/` has to the query engine. `template.js` validates only the envelope and rule shape (the `output` method, the closed rule vocabulary); everything deeper — match details, `$apply` modes, the query vocabulary inside segments — is left to the JSLT layer so the two vocabularies cannot drift. `desugar.js` compiles the template model down to an ordinary JSLT 0.1 stylesheet: each segment-list body becomes an array constructor of tagged segment pairs (`['r…', text]` raw literal, `['e…', …]` escaped interpolation, `['w…', …]` unescaped), and the built-in template rules are appended as matchless rules per mode. `writer.js`, chosen once per compiled template, serializes the dispatched tagged-segment stream to text or XML — escaping interpolated data only, literal template text passing through raw. The compiled JSLT stylesheet is exposed as `render.stylesheet`, and JSLT/query errors are remapped back to template `docPath`s (`errors.js`). Because dispatch, modes, conflict resolution and schema matching are inherited, JTLT adds zero operators to the query vocabulary and zero members to the JSLT vocabulary.
235
+
163
236
  ## The XQuery text front-end
164
237
 
165
238
  `xquery/parse.js` is a char-code recursive-descent parser for a defined subset of XQuery 3.1 *text* syntax that emits query documents — it is a **front-end, not a second engine**. Design rules: