@jarenjs/json 0.9.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 (57) hide show
  1. package/ARCHITECTURE.md +175 -0
  2. package/LICENSE +21 -0
  3. package/README.md +471 -0
  4. package/dist/types/basic.d.ts +32 -0
  5. package/dist/types/index.d.ts +4 -0
  6. package/dist/types/jslt/dispatch.d.ts +11 -0
  7. package/dist/types/jslt/errors.d.ts +18 -0
  8. package/dist/types/jslt/index.d.ts +53 -0
  9. package/dist/types/jslt/stylesheet.d.ts +8 -0
  10. package/dist/types/jtlt/desugar.d.ts +19 -0
  11. package/dist/types/jtlt/errors.d.ts +18 -0
  12. package/dist/types/jtlt/index.d.ts +57 -0
  13. package/dist/types/jtlt/template.d.ts +8 -0
  14. package/dist/types/jtlt/writer.d.ts +6 -0
  15. package/dist/types/path.d.ts +235 -0
  16. package/dist/types/pointer.d.ts +114 -0
  17. package/dist/types/query/compile.d.ts +21 -0
  18. package/dist/types/query/errors.d.ts +18 -0
  19. package/dist/types/query/index.d.ts +70 -0
  20. package/dist/types/query/normalize.d.ts +68 -0
  21. package/dist/types/query/operators.d.ts +424 -0
  22. package/dist/types/query/runtime.d.ts +93 -0
  23. package/dist/types/segments.d.ts +62 -0
  24. package/dist/types/xquery/index.d.ts +19 -0
  25. package/dist/types/xquery/parse.d.ts +20 -0
  26. package/docs/JSLT-FORMAT.md +861 -0
  27. package/docs/JSLT-PRELUDE.md +159 -0
  28. package/docs/JTLT-FORMAT.md +659 -0
  29. package/docs/QUERY-FORMAT.md +1221 -0
  30. package/docs/XQUERY-FRONTEND.md +321 -0
  31. package/package.json +81 -0
  32. package/schemas/jaren-jslt.draft-07.schema.json +776 -0
  33. package/schemas/jaren-jslt.schema.json +776 -0
  34. package/schemas/jaren-query.draft-07.schema.json +613 -0
  35. package/schemas/jaren-query.schema.json +375 -0
  36. package/src/basic.js +300 -0
  37. package/src/index.js +4 -0
  38. package/src/jslt/dispatch.js +934 -0
  39. package/src/jslt/errors.js +34 -0
  40. package/src/jslt/index.js +121 -0
  41. package/src/jslt/stylesheet.js +234 -0
  42. package/src/jtlt/desugar.js +231 -0
  43. package/src/jtlt/errors.js +34 -0
  44. package/src/jtlt/index.js +155 -0
  45. package/src/jtlt/template.js +130 -0
  46. package/src/jtlt/writer.js +110 -0
  47. package/src/path.js +977 -0
  48. package/src/pointer.js +453 -0
  49. package/src/query/compile.js +817 -0
  50. package/src/query/errors.js +33 -0
  51. package/src/query/index.js +150 -0
  52. package/src/query/normalize.js +1047 -0
  53. package/src/query/operators.js +1253 -0
  54. package/src/query/runtime.js +233 -0
  55. package/src/segments.js +627 -0
  56. package/src/xquery/index.js +35 -0
  57. package/src/xquery/parse.js +1647 -0
@@ -0,0 +1,817 @@
1
+ //#region Jaren JSON Query compiler
2
+ // Compiles the normalized AST (normalize.js) into a tree of specialized
3
+ // closures. Every node compiles to `get(frame) -> item | EMPTY | Seq`
4
+ // where `frame` is a plain array: slot 0 holds the input document,
5
+ // externals and $let bindings live in their allocated slots.
6
+ //
7
+ // All decisions are taken at compile time: operator dispatch, operand
8
+ // cardinality, singular-path detection. Nodes whose static cardinality is
9
+ // CARD_ONE compile to singleton-mode closures that skip every sequence
10
+ // check - the main reason compiled queries are fast.
11
+
12
+ import { compareCodePoints } from '@jarenjs/core/string';
13
+ import {
14
+ NOTHING,
15
+ compileSingularGetter,
16
+ compileSegmentV,
17
+ runSegmentsV,
18
+ } from '../segments.js';
19
+ import { JsonQueryRuntimeError } from './errors.js';
20
+ import { EMPTY, Seq, seqOf, appendItem, ebv, stableKeyString, describeItem } from './runtime.js';
21
+ import { CARD_ONE } from './normalize.js';
22
+ // The operator registry: every section-8 operator compiles through its
23
+ // table entry (compileOp). Only referenced inside functions, so the
24
+ // import cycle compile.js <-> operators.js is initialization-safe.
25
+ import { OPERATORS } from './operators.js';
26
+
27
+ /**
28
+ * Sentinel stored in the frame slot of an external parameter the caller
29
+ * did not bind; evaluating a reference to it raises JQ2006.
30
+ */
31
+ export const UNBOUND = Symbol('JsonQuery.Unbound');
32
+
33
+ //#region variables & paths
34
+
35
+ function compileVarGetter(slot, external, name, docPath) {
36
+ if (!external)
37
+ return (f) => f[slot];
38
+ return (f) => {
39
+ const v = f[slot];
40
+ if (v === UNBOUND)
41
+ throw new JsonQueryRuntimeError('JQ2006', `external parameter '${name}' was not bound`, docPath);
42
+ return v;
43
+ };
44
+ }
45
+
46
+ function compileVar(node) {
47
+ return compileVarGetter(node.slot, node.external, node.name, node.docPath);
48
+ }
49
+
50
+ // A path leaf: run the (pre-parsed) RFC 9535 segments against the root
51
+ // value. The root of an embedded `[?...]` filter's `$` is always frame
52
+ // slot 0, the input document (section 3.2). A variable-rooted path runs
53
+ // its segments against each item of the variable's bound sequence in
54
+ // order, concatenating results; an item that is an array flows as-is
55
+ // (one RFC 9535 node).
56
+ function compilePath(node) {
57
+ const base = compileVarGetter(node.rootSlot, node.external, node.name, node.docPath);
58
+ if (node.singular) {
59
+ const getter = compileSingularGetter(node.segments, true);
60
+ if (node.rootCard === CARD_ONE) {
61
+ // singleton root (input document, external, or ONE-card binding):
62
+ // a direct property walk, no sequence checks
63
+ return (f) => {
64
+ const v = getter(base(f), f[0]);
65
+ return v === NOTHING ? EMPTY : v;
66
+ };
67
+ }
68
+ return (f) => {
69
+ const b = base(f);
70
+ if (b === EMPTY)
71
+ return EMPTY;
72
+ if (b instanceof Seq) {
73
+ const items = b.items;
74
+ const acc = [];
75
+ for (let i = 0; i < items.length; i++) {
76
+ const v = getter(items[i], f[0]);
77
+ if (v !== NOTHING)
78
+ acc.push(v);
79
+ }
80
+ return seqOf(acc);
81
+ }
82
+ const v = getter(b, f[0]);
83
+ return v === NOTHING ? EMPTY : v;
84
+ };
85
+ }
86
+ const segs = node.segments.map(compileSegmentV);
87
+ if (node.rootCard === CARD_ONE)
88
+ return (f) => seqOf(runSegmentsV(segs, base(f), f[0]));
89
+ return (f) => {
90
+ const b = base(f);
91
+ if (b === EMPTY)
92
+ return EMPTY;
93
+ if (b instanceof Seq) {
94
+ const items = b.items;
95
+ const acc = [];
96
+ for (let i = 0; i < items.length; i++) {
97
+ const out = runSegmentsV(segs, items[i], f[0]);
98
+ for (let j = 0; j < out.length; j++)
99
+ acc.push(out[j]);
100
+ }
101
+ return seqOf(acc);
102
+ }
103
+ return seqOf(runSegmentsV(segs, b, f[0]));
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Existence-only compilation for `$exists`/`$empty` (and any future
109
+ * boolean context): paths never materialize a result sequence (the
110
+ * analogue of path.js's compileExists). Takes the operand AST node -
111
+ * this is why registry `compile` functions receive arg nodes, not just
112
+ * getters.
113
+ * @param {object} node - a frozen AST node from normalize.js
114
+ * @returns {(frame: any[]) => boolean}
115
+ */
116
+ export function compileExistsTest(node) {
117
+ if (node.kind === 'path') {
118
+ const base = compileVarGetter(node.rootSlot, node.external, node.name, node.docPath);
119
+ if (node.singular) {
120
+ const getter = compileSingularGetter(node.segments, true);
121
+ if (node.rootCard === CARD_ONE)
122
+ return (f) => getter(base(f), f[0]) !== NOTHING;
123
+ return (f) => {
124
+ const b = base(f);
125
+ if (b === EMPTY)
126
+ return false;
127
+ if (b instanceof Seq) {
128
+ const items = b.items;
129
+ for (let i = 0; i < items.length; i++) {
130
+ if (getter(items[i], f[0]) !== NOTHING)
131
+ return true;
132
+ }
133
+ return false;
134
+ }
135
+ return getter(b, f[0]) !== NOTHING;
136
+ };
137
+ }
138
+ const segs = node.segments.map(compileSegmentV);
139
+ if (node.rootCard === CARD_ONE)
140
+ return (f) => runSegmentsV(segs, base(f), f[0]).length !== 0;
141
+ return (f) => {
142
+ const b = base(f);
143
+ if (b === EMPTY)
144
+ return false;
145
+ if (b instanceof Seq) {
146
+ const items = b.items;
147
+ for (let i = 0; i < items.length; i++) {
148
+ if (runSegmentsV(segs, items[i], f[0]).length !== 0)
149
+ return true;
150
+ }
151
+ return false;
152
+ }
153
+ return runSegmentsV(segs, b, f[0]).length !== 0;
154
+ };
155
+ }
156
+ if (node.kind === 'var') {
157
+ const get = compileVar(node);
158
+ return (f) => get(f) !== EMPTY;
159
+ }
160
+ const get = compileNode(node);
161
+ return (f) => get(f) !== EMPTY;
162
+ }
163
+
164
+ //#endregion
165
+
166
+ //#region constructors
167
+
168
+ // member applier for map constructors and $map values: ONE-card values
169
+ // assign directly; otherwise an empty result omits the member and a
170
+ // multi-item result is JQ2001 (a JSON member holds exactly one value)
171
+ function memberValue(v, name, docPath) {
172
+ if (v instanceof Seq)
173
+ throw new JsonQueryRuntimeError('JQ2001',
174
+ `member '${name}' evaluated to ${v.items.length} items; an object member takes exactly one`, docPath);
175
+ return v;
176
+ }
177
+
178
+ function compileObject(node) {
179
+ const entries = node.entries;
180
+ if (entries.length === 0)
181
+ return () => ({});
182
+ const appliers = new Array(entries.length);
183
+ for (let i = 0; i < entries.length; i++) {
184
+ const { name, expr } = entries[i];
185
+ const get = compileNode(expr);
186
+ if (expr.card === CARD_ONE) {
187
+ appliers[i] = (f, out) => {
188
+ out[name] = get(f);
189
+ };
190
+ }
191
+ else {
192
+ const docPath = expr.docPath;
193
+ appliers[i] = (f, out) => {
194
+ const v = get(f);
195
+ if (v !== EMPTY)
196
+ out[name] = memberValue(v, name, docPath);
197
+ };
198
+ }
199
+ }
200
+ const alen = appliers.length;
201
+ return (f) => {
202
+ const out = {};
203
+ for (let i = 0; i < alen; i++)
204
+ appliers[i](f, out);
205
+ return out;
206
+ };
207
+ }
208
+
209
+ function compileMap(node) {
210
+ const pairs = node.pairs;
211
+ if (pairs.length === 0)
212
+ return () => ({});
213
+ const appliers = new Array(pairs.length);
214
+ for (let i = 0; i < pairs.length; i++) {
215
+ const { key, value } = pairs[i];
216
+ const keyGet = compileNode(key);
217
+ const valGet = compileNode(value);
218
+ const keyPath = key.docPath;
219
+ const valPath = value.docPath;
220
+ const valOne = value.card === CARD_ONE;
221
+ appliers[i] = (f, out) => {
222
+ const k = keyGet(f);
223
+ if (typeof k !== 'string')
224
+ throw new JsonQueryRuntimeError('JQ2004',
225
+ `a $map key must evaluate to a single string, got ${describeItem(k)}`, keyPath);
226
+ if (valOne) {
227
+ out[k] = valGet(f);
228
+ return;
229
+ }
230
+ const v = valGet(f);
231
+ if (v !== EMPTY)
232
+ out[k] = memberValue(v, k, valPath);
233
+ };
234
+ }
235
+ const alen = appliers.length;
236
+ return (f) => {
237
+ const out = {};
238
+ for (let i = 0; i < alen; i++)
239
+ appliers[i](f, out); // later pairs win on duplicate keys
240
+ return out;
241
+ };
242
+ }
243
+
244
+ // shared flattening accumulator of array constructors and $seq
245
+ function compileElementAppliers(elements) {
246
+ const appliers = new Array(elements.length);
247
+ for (let i = 0; i < elements.length; i++) {
248
+ const get = compileNode(elements[i]);
249
+ appliers[i] = elements[i].card === CARD_ONE
250
+ ? (f, acc) => acc.push(get(f))
251
+ : (f, acc) => appendItem(acc, get(f));
252
+ }
253
+ return appliers;
254
+ }
255
+
256
+ function compileArray(node) {
257
+ if (node.elements.length === 0)
258
+ return () => [];
259
+ const appliers = compileElementAppliers(node.elements);
260
+ const alen = appliers.length;
261
+ return (f) => {
262
+ const acc = [];
263
+ for (let i = 0; i < alen; i++)
264
+ appliers[i](f, acc);
265
+ return acc;
266
+ };
267
+ }
268
+
269
+ //#endregion
270
+
271
+ //#region operators
272
+
273
+ // A registry operator call (normalize.js `op` node): compile the argument
274
+ // getters, hand them - with the argument nodes, which carry `card` and
275
+ // `docPath` - to the table entry's `compile`. Arguments declared 'raw' or
276
+ // 'name' are compile-time data (`args[i].value`), not getters. Extension
277
+ // op nodes (options.extensions) carry their resolved entry themselves.
278
+ function compileOp(node) {
279
+ const entry = OPERATORS[node.name] ?? node.entry;
280
+ const args = node.args;
281
+ const gets = new Array(args.length);
282
+ for (let i = 0; i < args.length; i++)
283
+ gets[i] = args[i].kind === 'raw' ? null : compileNode(args[i]);
284
+ return entry.compile(gets, args, node.docPath + '/' + node.name);
285
+ }
286
+
287
+ //#endregion
288
+
289
+ //#region $let
290
+
291
+ function compileLet(node) {
292
+ const ret = compileNode(node.ret);
293
+ if (node.bindings.length === 1) {
294
+ const { slot, expr } = node.bindings[0];
295
+ const get = compileNode(expr);
296
+ return (f) => {
297
+ f[slot] = get(f);
298
+ return ret(f);
299
+ };
300
+ }
301
+ const blen = node.bindings.length;
302
+ const slots = new Array(blen);
303
+ const gets = new Array(blen);
304
+ for (let i = 0; i < blen; i++) {
305
+ slots[i] = node.bindings[i].slot;
306
+ gets[i] = compileNode(node.bindings[i].expr);
307
+ }
308
+ return (f) => {
309
+ for (let i = 0; i < blen; i++)
310
+ f[slots[i]] = gets[i](f);
311
+ return ret(f);
312
+ };
313
+ }
314
+
315
+ //#endregion
316
+
317
+ //#region FLWOR
318
+ // The tuple stream is a chain of nested closures, each of signature
319
+ // `(frame, out) -> void`: a $for clause iterates its source and calls the
320
+ // next stage per item, $let assigns and calls once, $as validates bound
321
+ // slots against compiled type tests, $where gates on EBV.
322
+ // A tuple IS the current state of the frame slots - no tuple objects, no
323
+ // intermediate arrays. `out` threads the current stage's collector
324
+ // through untouched: the result accumulator, or a barrier's state.
325
+ //
326
+ // $groupby and $orderby are barriers; they materialize the minimum via
327
+ // compile-time liveness (normalize.js): $groupby accumulates only the
328
+ // live binding slots per group, $orderby snapshots only the live slots
329
+ // per tuple next to its pre-evaluated key row (Schwartzian transform).
330
+ //
331
+ //#region roadmap: FLWOR optimizer
332
+ // The compiled form is the straightforward nested-loop pipeline: a join
333
+ // ($for x $for + $where equality) runs O(n*m). Hash joins (build a table
334
+ // on one side of an equijoin), filter hoisting into the deepest binding
335
+ // that covers the predicate's variables, and orderby/groupby fusion are
336
+ // future optimizer work orders.
337
+ //#endregion
338
+
339
+ // D4 iteration step: an item that is an array contributes its members
340
+ // (one level - nested arrays inside stay items); everything else is one
341
+ // tuple. Shared by $for and the quantifier loops.
342
+ function emitForItem(item, f, slot, next, out) {
343
+ if (Array.isArray(item)) {
344
+ for (let j = 0; j < item.length; j++) {
345
+ f[slot] = item[j];
346
+ next(f, out);
347
+ }
348
+ return;
349
+ }
350
+ f[slot] = item;
351
+ next(f, out);
352
+ }
353
+
354
+ // ... and the positional variant: `pos` is the 0-based (D6) position
355
+ // within the iterated (post-unpacking) sequence; returns the next one.
356
+ function emitForItemAt(item, f, slot, atSlot, pos, next, out) {
357
+ if (Array.isArray(item)) {
358
+ for (let j = 0; j < item.length; j++) {
359
+ f[slot] = item[j];
360
+ f[atSlot] = pos++;
361
+ next(f, out);
362
+ }
363
+ return pos;
364
+ }
365
+ f[slot] = item;
366
+ f[atSlot] = pos;
367
+ next(f, out);
368
+ return pos + 1;
369
+ }
370
+
371
+ function compileForClause(binding, next) {
372
+ const get = compileNode(binding.expr);
373
+ const slot = binding.slot;
374
+ const atSlot = binding.atSlot;
375
+ if (atSlot < 0) {
376
+ return (f, out) => {
377
+ const v = get(f);
378
+ if (v === EMPTY)
379
+ return;
380
+ if (v instanceof Seq) {
381
+ const items = v.items;
382
+ for (let i = 0; i < items.length; i++)
383
+ emitForItem(items[i], f, slot, next, out);
384
+ return;
385
+ }
386
+ emitForItem(v, f, slot, next, out);
387
+ };
388
+ }
389
+ // positional counter per binding activation
390
+ return (f, out) => {
391
+ const v = get(f);
392
+ if (v === EMPTY)
393
+ return;
394
+ if (v instanceof Seq) {
395
+ const items = v.items;
396
+ let pos = 0;
397
+ for (let i = 0; i < items.length; i++)
398
+ pos = emitForItemAt(items[i], f, slot, atSlot, pos, next, out);
399
+ return;
400
+ }
401
+ emitForItemAt(v, f, slot, atSlot, 0, next, out);
402
+ };
403
+ }
404
+
405
+ // One $as check (section 6.4): validate a phrase binding's frame slot per
406
+ // tuple against its compiled type-test predicate. A $for/$at variable is
407
+ // always exactly one item; a $let variable is validated per item of its
408
+ // bound sequence (the empty sequence passes vacuously). Failure is JQ2008,
409
+ // naming the variable.
410
+ function compileAsCheck(check, next) {
411
+ const { name, slot, test, docPath } = check;
412
+ if (!check.isLet) { // a $for/$at binding: one item per tuple
413
+ return (f, out) => {
414
+ const v = f[slot];
415
+ if (!test(v))
416
+ throw new JsonQueryRuntimeError('JQ2008',
417
+ `variable '${name}' failed its '$as' schema: ${describeItem(v)} does not satisfy it`, docPath);
418
+ next(f, out);
419
+ };
420
+ }
421
+ return (f, out) => {
422
+ const v = f[slot];
423
+ if (v !== EMPTY) {
424
+ if (v instanceof Seq) {
425
+ const items = v.items;
426
+ for (let i = 0; i < items.length; i++) {
427
+ if (!test(items[i]))
428
+ throw new JsonQueryRuntimeError('JQ2008',
429
+ `variable '${name}' failed its '$as' schema: item ${i} (${describeItem(items[i])}) does not satisfy it`, docPath);
430
+ }
431
+ }
432
+ else if (!test(v)) {
433
+ throw new JsonQueryRuntimeError('JQ2008',
434
+ `variable '${name}' failed its '$as' schema: ${describeItem(v)} does not satisfy it`, docPath);
435
+ }
436
+ }
437
+ next(f, out);
438
+ };
439
+ }
440
+
441
+ // $orderby tuple collector: evaluate the N key expressions once into a
442
+ // keys row, snapshot the live slots, push [keys..., snapshot]. A key
443
+ // value must be the empty sequence, one number, or one string (JQ2005).
444
+ function compileRowSink(specs, keyGets, keyPaths, liveSlots) {
445
+ const keyCount = specs.length;
446
+ const liveCount = liveSlots.length;
447
+ return (f, rows) => {
448
+ const row = new Array(keyCount + 1);
449
+ for (let i = 0; i < keyCount; i++) {
450
+ const v = keyGets[i](f);
451
+ if (v !== EMPTY && typeof v !== 'number' && typeof v !== 'string')
452
+ throw new JsonQueryRuntimeError('JQ2005',
453
+ `an $orderby key must be the empty sequence, a number, or a string, got ${describeItem(v)}`, keyPaths[i]);
454
+ row[i] = v;
455
+ }
456
+ const snap = new Array(liveCount);
457
+ for (let j = 0; j < liveCount; j++)
458
+ snap[j] = f[liveSlots[j]];
459
+ row[keyCount] = snap;
460
+ rows.push(row);
461
+ };
462
+ }
463
+
464
+ // Row comparator over the key specs (direction, $empty least/greatest,
465
+ // number/string type check per pair - JQ2005 on mismatch). Empty keys
466
+ // order as -Infinity under 'least' and +Infinity under 'greatest',
467
+ // before the direction applies (section 6.6). NaN keys order equal to
468
+ // themselves and less than every other number (the XQuery order-by
469
+ // rule). Ties fall through to the next key; JS sort is stable, so equal
470
+ // rows keep tuple order.
471
+ function compileRowComparator(specs, keyPaths) {
472
+ const keyCount = specs.length;
473
+ const descs = new Array(keyCount);
474
+ const emptyGreatests = new Array(keyCount);
475
+ for (let i = 0; i < keyCount; i++) {
476
+ descs[i] = specs[i].desc;
477
+ emptyGreatests[i] = specs[i].emptyGreatest;
478
+ }
479
+ return (a, b) => {
480
+ for (let i = 0; i < keyCount; i++) {
481
+ const x = a[i];
482
+ const y = b[i];
483
+ if (x === y) // also EMPTY vs EMPTY, and -0 vs 0 (mathematically equal)
484
+ continue;
485
+ let c;
486
+ if (x === EMPTY)
487
+ c = emptyGreatests[i] ? 1 : -1;
488
+ else if (y === EMPTY)
489
+ c = emptyGreatests[i] ? -1 : 1;
490
+ else if (typeof x === 'number') {
491
+ if (typeof y !== 'number')
492
+ throw new JsonQueryRuntimeError('JQ2005',
493
+ 'cannot order a number against a string in $orderby', keyPaths[i]);
494
+ if (x < y)
495
+ c = -1;
496
+ else if (x > y)
497
+ c = 1;
498
+ else if (x !== x) // x is NaN: equal to NaN, less than all others
499
+ c = y !== y ? 0 : -1;
500
+ else // y is NaN (x === y was false, so they are not both non-NaN equal)
501
+ c = 1;
502
+ }
503
+ else {
504
+ if (typeof y !== 'string')
505
+ throw new JsonQueryRuntimeError('JQ2005',
506
+ 'cannot order a string against a number in $orderby', keyPaths[i]);
507
+ c = compareCodePoints(x, y);
508
+ }
509
+ if (c !== 0)
510
+ return descs[i] ? -c : c;
511
+ }
512
+ return 0;
513
+ };
514
+ }
515
+
516
+ function compileFlwor(node) {
517
+ // final sink: $return collects into the accumulator; the $count clause
518
+ // numbers surviving tuples through its own frame slot (0-based, D6),
519
+ // reset once per phrase evaluation by the drivers below
520
+ const retGet = compileNode(node.ret);
521
+ let sink;
522
+ if (node.ret.card === CARD_ONE)
523
+ sink = (f, out) => out.push(retGet(f));
524
+ else
525
+ sink = (f, out) => appendItem(out, retGet(f));
526
+ const countSlot = node.count === null ? -1 : node.count.slot;
527
+ if (countSlot >= 0) {
528
+ const inner = sink;
529
+ sink = (f, out) => {
530
+ inner(f, out);
531
+ f[countSlot] += 1;
532
+ };
533
+ }
534
+
535
+ const groupby = node.groupby;
536
+ const orderby = node.orderby;
537
+
538
+ // $orderby machinery (Schwartzian rows + compiled comparator)
539
+ let rowSink = null;
540
+ let comparator = null;
541
+ let liveSlots = null;
542
+ let keyCount = 0;
543
+ if (orderby !== null) {
544
+ const specs = orderby.specs;
545
+ const keyGets = specs.map((s) => compileNode(s.key));
546
+ const keyPaths = specs.map((s) => s.docPath);
547
+ keyCount = specs.length;
548
+ liveSlots = orderby.liveSlots;
549
+ rowSink = compileRowSink(specs, keyGets, keyPaths, liveSlots);
550
+ comparator = compileRowComparator(specs, keyPaths);
551
+ }
552
+
553
+ // $groupby machinery: Map<stableKeyString composite, group>; the Map
554
+ // preserves first-appearance order. Key variables rebind as singletons,
555
+ // every other live binding as the concatenation of its values across
556
+ // the group's tuples (spec section 6.5).
557
+ let groupSink = null;
558
+ let writeGroup = null;
559
+ if (groupby !== null) {
560
+ const keys = groupby.keys;
561
+ const groupCount = keys.length;
562
+ const keyGets = keys.map((k) => compileNode(k.expr));
563
+ const keyPaths = keys.map((k) => k.docPath);
564
+ const keySlots = keys.map((k) => k.slot);
565
+ const accSlots = groupby.accSlots;
566
+ const accCount = accSlots.length;
567
+ groupSink = (f, map) => {
568
+ const keyValues = new Array(groupCount);
569
+ let composite = '';
570
+ for (let i = 0; i < groupCount; i++) {
571
+ const v = keyGets[i](f);
572
+ if (v instanceof Seq)
573
+ throw new JsonQueryRuntimeError('JQ2001',
574
+ `a $groupby key must be the empty sequence or a single item, got ${describeItem(v)}`, keyPaths[i]);
575
+ keyValues[i] = v;
576
+ // '\u0000' never occurs in stableKeyString output, '~' never
577
+ // starts one: the composite cannot collide across keys
578
+ composite += v === EMPTY ? '\u0000~' : '\u0000' + stableKeyString(v);
579
+ }
580
+ let group = map.get(composite);
581
+ if (group === undefined) {
582
+ const accs = new Array(accCount);
583
+ for (let j = 0; j < accCount; j++)
584
+ accs[j] = [];
585
+ group = { keyValues, accs };
586
+ map.set(composite, group);
587
+ }
588
+ const accs = group.accs;
589
+ for (let j = 0; j < accCount; j++)
590
+ appendItem(accs[j], f[accSlots[j]]);
591
+ };
592
+ writeGroup = (f, group) => {
593
+ const keyValues = group.keyValues;
594
+ for (let i = 0; i < groupCount; i++)
595
+ f[keySlots[i]] = keyValues[i];
596
+ const accs = group.accs;
597
+ for (let j = 0; j < accCount; j++)
598
+ f[accSlots[j]] = seqOf(accs[j]);
599
+ };
600
+ }
601
+
602
+ // the streaming prefix $for -> $let -> $as -> $where, feeding the first
603
+ // barrier's collector (or the final sink when there is none)
604
+ let emit = groupby !== null ? groupSink : (orderby !== null ? rowSink : sink);
605
+ if (node.where !== null) {
606
+ const cond = compileNode(node.where);
607
+ const condPath = node.where.docPath;
608
+ const next = emit;
609
+ emit = (f, out) => {
610
+ if (ebv(cond(f), condPath))
611
+ next(f, out);
612
+ };
613
+ }
614
+ if (node.asChecks !== null) {
615
+ for (let i = node.asChecks.length - 1; i >= 0; i--)
616
+ emit = compileAsCheck(node.asChecks[i], emit);
617
+ }
618
+ const lets = node.letBindings;
619
+ for (let i = lets.length - 1; i >= 0; i--) {
620
+ const slot = lets[i].slot;
621
+ const get = compileNode(lets[i].expr);
622
+ const next = emit;
623
+ emit = (f, out) => {
624
+ f[slot] = get(f);
625
+ next(f, out);
626
+ };
627
+ }
628
+ const fors = node.forBindings;
629
+ for (let i = fors.length - 1; i >= 0; i--)
630
+ emit = compileForClause(fors[i], emit);
631
+ const head = emit;
632
+
633
+ // drivers, one per barrier combination
634
+ if (groupby === null && orderby === null) {
635
+ if (countSlot < 0) {
636
+ return (f) => {
637
+ const out = [];
638
+ head(f, out);
639
+ return seqOf(out);
640
+ };
641
+ }
642
+ return (f) => {
643
+ const out = [];
644
+ f[countSlot] = 0;
645
+ head(f, out);
646
+ return seqOf(out);
647
+ };
648
+ }
649
+ if (groupby === null) { // $orderby only
650
+ return (f) => {
651
+ const rows = [];
652
+ head(f, rows);
653
+ rows.sort(comparator); // stable
654
+ const out = [];
655
+ if (countSlot >= 0)
656
+ f[countSlot] = 0;
657
+ const liveCount = liveSlots.length;
658
+ for (let i = 0; i < rows.length; i++) {
659
+ const snap = rows[i][keyCount];
660
+ for (let j = 0; j < liveCount; j++)
661
+ f[liveSlots[j]] = snap[j];
662
+ sink(f, out);
663
+ }
664
+ return seqOf(out);
665
+ };
666
+ }
667
+ if (orderby === null) { // $groupby only
668
+ return (f) => {
669
+ const map = new Map();
670
+ head(f, map);
671
+ const out = [];
672
+ if (countSlot >= 0)
673
+ f[countSlot] = 0;
674
+ for (const group of map.values()) { // first-appearance order
675
+ writeGroup(f, group);
676
+ sink(f, out);
677
+ }
678
+ return seqOf(out);
679
+ };
680
+ }
681
+ // $groupby then $orderby: sort the per-group tuples
682
+ return (f) => {
683
+ const map = new Map();
684
+ head(f, map);
685
+ const rows = [];
686
+ for (const group of map.values()) {
687
+ writeGroup(f, group);
688
+ rowSink(f, rows);
689
+ }
690
+ rows.sort(comparator);
691
+ const out = [];
692
+ if (countSlot >= 0)
693
+ f[countSlot] = 0;
694
+ const liveCount = liveSlots.length;
695
+ for (let i = 0; i < rows.length; i++) {
696
+ const snap = rows[i][keyCount];
697
+ for (let j = 0; j < liveCount; j++)
698
+ f[liveSlots[j]] = snap[j];
699
+ sink(f, out);
700
+ }
701
+ return seqOf(out);
702
+ };
703
+ }
704
+
705
+ //#endregion
706
+
707
+ //#region quantifiers
708
+
709
+ // $some/$every + $satisfies (section 7): a $for-style loop nest with
710
+ // early exit - $some stops at the first EBV-true tuple, $every at the
711
+ // first EBV-false one. D4 unpacking applies; nothing materializes.
712
+ function quantVisitSome(item, f, slot, next) {
713
+ if (Array.isArray(item)) { // D4
714
+ for (let j = 0; j < item.length; j++) {
715
+ f[slot] = item[j];
716
+ if (next(f))
717
+ return true;
718
+ }
719
+ return false;
720
+ }
721
+ f[slot] = item;
722
+ return next(f);
723
+ }
724
+
725
+ function quantVisitEvery(item, f, slot, next) {
726
+ if (Array.isArray(item)) { // D4
727
+ for (let j = 0; j < item.length; j++) {
728
+ f[slot] = item[j];
729
+ if (!next(f))
730
+ return false;
731
+ }
732
+ return true;
733
+ }
734
+ f[slot] = item;
735
+ return next(f);
736
+ }
737
+
738
+ function compileQuantLevel(binding, next, some) {
739
+ const get = compileNode(binding.expr);
740
+ const slot = binding.slot;
741
+ if (some) {
742
+ return (f) => {
743
+ const v = get(f);
744
+ if (v === EMPTY) // empty source: no witnessing tuple
745
+ return false;
746
+ if (v instanceof Seq) {
747
+ const items = v.items;
748
+ for (let i = 0; i < items.length; i++) {
749
+ if (quantVisitSome(items[i], f, slot, next))
750
+ return true;
751
+ }
752
+ return false;
753
+ }
754
+ return quantVisitSome(v, f, slot, next);
755
+ };
756
+ }
757
+ return (f) => {
758
+ const v = get(f);
759
+ if (v === EMPTY) // empty source: vacuously true
760
+ return true;
761
+ if (v instanceof Seq) {
762
+ const items = v.items;
763
+ for (let i = 0; i < items.length; i++) {
764
+ if (!quantVisitEvery(items[i], f, slot, next))
765
+ return false;
766
+ }
767
+ return true;
768
+ }
769
+ return quantVisitEvery(v, f, slot, next);
770
+ };
771
+ }
772
+
773
+ function compileQuant(node) {
774
+ const sat = compileNode(node.satisfies);
775
+ const satPath = node.satisfies.docPath;
776
+ const some = node.some;
777
+ let test = (f) => ebv(sat(f), satPath);
778
+ for (let i = node.bindings.length - 1; i >= 0; i--)
779
+ test = compileQuantLevel(node.bindings[i], test, some);
780
+ return test;
781
+ }
782
+
783
+ //#endregion
784
+
785
+ /**
786
+ * Compile a normalized AST node into its getter closure.
787
+ * @param {object} node - a frozen AST node from normalize.js
788
+ * @returns {(frame: any[]) => any} getter returning an item, EMPTY, or a Seq
789
+ */
790
+ export function compileNode(node) {
791
+ switch (node.kind) {
792
+ case 'literal': {
793
+ const value = node.value;
794
+ return () => value;
795
+ }
796
+ case 'var':
797
+ return compileVar(node);
798
+ case 'path':
799
+ return compilePath(node);
800
+ case 'object':
801
+ return compileObject(node);
802
+ case 'map':
803
+ return compileMap(node);
804
+ case 'array':
805
+ return compileArray(node);
806
+ case 'op':
807
+ return compileOp(node);
808
+ case 'let':
809
+ return compileLet(node);
810
+ case 'quant':
811
+ return compileQuant(node);
812
+ default: // 'flwor'
813
+ return compileFlwor(node);
814
+ }
815
+ }
816
+
817
+ //#endregion