@jarenjs/json 0.9.2 → 0.34.0

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/src/segments.js CHANGED
@@ -7,8 +7,8 @@
7
7
  // (value, normalized-path) pairs. This module is package-internal and is
8
8
  // deliberately not listed in the package exports.
9
9
 
10
- import { equalsJson } from '@jarenjs/core/object';
11
- import { countCodePoints, compareCodePoints } from '@jarenjs/core/string';
10
+ import { equalsJson, compareJsonScalarLt } from '@jarenjs/core/object';
11
+ import { countCodePoints } from '@jarenjs/core/string';
12
12
  import { compileIRegexp } from '@jarenjs/core/text/iregexp';
13
13
  import {
14
14
  CC_TAB,
@@ -17,6 +17,8 @@ import {
17
17
  CC_SPACE,
18
18
  CC_SQUOTE,
19
19
  CC_BACKSLASH,
20
+ CC_0,
21
+ isDigitCode,
20
22
  } from '@jarenjs/core/scan';
21
23
 
22
24
  /**
@@ -28,6 +30,35 @@ export const NOTHING = Symbol('JSONPath.Nothing');
28
30
 
29
31
  const hasOwn = Object.hasOwn;
30
32
 
33
+ // Array indexes are bounded by the maximum array length (2^32 - 1), so a
34
+ // valid index has at most 10 digits and is strictly below 2^32 - 1.
35
+ const MAX_ARRAY_INDEX = 4294967294;
36
+
37
+ /**
38
+ * Scan `source[start..end)` as an RFC 6901 array index: `0`, or a digit
39
+ * sequence without leading zeros. Returns -1 when the range is not a
40
+ * valid index (`-` is never a valid read index). Shared by the JSON
41
+ * Pointer compiler (pointer.js) and the JSON Patch engine (patch.js).
42
+ */
43
+ export function scanArrayIndex(source, start, end) {
44
+ const digits = end - start;
45
+ if (digits === 0 || digits > 10)
46
+ return -1;
47
+ const first = source.charCodeAt(start);
48
+ if (!isDigitCode(first))
49
+ return -1;
50
+ if (first === CC_0)
51
+ return digits === 1 ? 0 : -1;
52
+ let index = first - CC_0;
53
+ for (let i = start + 1; i < end; i++) {
54
+ const c = source.charCodeAt(i);
55
+ if (!isDigitCode(c))
56
+ return -1;
57
+ index = index * 10 + (c - CC_0);
58
+ }
59
+ return index <= MAX_ARRAY_INDEX ? index : -1;
60
+ }
61
+
31
62
  /**
32
63
  * Returns true when every segment is a child segment with exactly one
33
64
  * name or index selector (a "singular query", RFC 9535 section 2.3.5.1).
@@ -56,16 +87,6 @@ function cmpEquals(a, b) {
56
87
  return equalsJson(a, b);
57
88
  }
58
89
 
59
- // numbers by value, strings by Unicode scalar values (RFC 9535
60
- // section 2.3.5.2.2); other types do not order
61
- function cmpLess(a, b) {
62
- if (typeof a === 'number')
63
- return typeof b === 'number' && a < b;
64
- if (typeof a === 'string')
65
- return typeof b === 'string' && compareCodePoints(a, b) < 0;
66
- return false;
67
- }
68
-
69
90
  function countOwnKeys(obj) {
70
91
  let count = 0;
71
92
  for (const key in obj) {
@@ -129,6 +150,13 @@ export function runSegmentsV(segs, start, root) {
129
150
  /**
130
151
  * Compile an existence test for a filter query; singular queries never
131
152
  * materialize nodelists.
153
+ *
154
+ * The non-singular case builds its nodelist rather than pulling the
155
+ * lazy chain (compileSegmentG), even though it only needs one node: a
156
+ * filter runs this per candidate node, where nodelists are a handful of
157
+ * items and generator setup costs more than the pushes it saves - it
158
+ * benched ~9x slower on `$.items[?@.tags[*]]` over 2000 items. Laziness
159
+ * pays at the top of a query, not inside a filter.
132
160
  * @returns {(current: any, root: any) => boolean}
133
161
  */
134
162
  export function compileExists(query) {
@@ -141,17 +169,86 @@ export function compileExists(query) {
141
169
  return (current, root) => runSegmentsV(segs, relative ? current : root, root).length > 0;
142
170
  }
143
171
 
172
+ // Collector for the reset hooks of `$`-rooted comparable memos, active
173
+ // while compileFilterPredicate compiles a filter's predicate tree; null
174
+ // outside a filter, where a memo would have no reset and go stale.
175
+ let pendingResets = null;
176
+
177
+ // Hand-back from compileFilterPredicate to the filter selector that ran
178
+ // it - module scope like sliceFrom/sliceTo: read immediately, never kept.
179
+ let filterReset = null;
180
+
181
+ /**
182
+ * Compile a filter's predicate expression, collecting the reset hooks of
183
+ * the `$`-rooted comparable memos inside it (see compileComparable) and
184
+ * handing their combined reset - or null when the tree has none - back
185
+ * through `filterReset`. The filter selector runs the reset before each
186
+ * application. A nested filter collects into its own frame, so an inner
187
+ * filter's memos reset per inner application.
188
+ * @returns {(current: any, root: any) => boolean}
189
+ */
190
+ function compileFilterPredicate(expr) {
191
+ const saved = pendingResets;
192
+ pendingResets = [];
193
+ const pred = compileLogicalExpr(expr);
194
+ const resets = pendingResets;
195
+ pendingResets = saved;
196
+ if (resets.length === 0) {
197
+ filterReset = null;
198
+ }
199
+ else if (resets.length === 1) {
200
+ filterReset = resets[0];
201
+ }
202
+ else {
203
+ filterReset = () => {
204
+ for (let i = 0; i < resets.length; i++)
205
+ resets[i]();
206
+ };
207
+ }
208
+ return pred;
209
+ }
210
+
144
211
  // getter producing a ValueType result (a JSON value or NOTHING)
145
212
  function compileComparable(node) {
146
213
  if (node.kind === 'literal') {
147
214
  const value = node.value;
148
215
  return () => value;
149
216
  }
150
- if (node.kind === 'query')
151
- return compileSingularGetter(node.query.segments, node.query.relative);
217
+ if (node.kind === 'query') {
218
+ const getter = compileSingularGetter(node.query.segments, node.query.relative);
219
+ if (node.query.relative || pendingResets === null)
220
+ return getter;
221
+ // A `$`-rooted comparable is invariant for one filter application,
222
+ // so it is computed at the first candidate and reused for the rest
223
+ // instead of re-walking from the root per candidate. The filter
224
+ // resets the memo before each application (a caller may mutate the
225
+ // document and re-run the query on the same root identity); the
226
+ // root check covers a reentrant run against a different document.
227
+ let cachedRoot = NOTHING;
228
+ let cachedValue = NOTHING;
229
+ pendingResets.push(() => { cachedRoot = NOTHING; });
230
+ return (current, root) => {
231
+ if (root !== cachedRoot) {
232
+ cachedValue = getter(current, root);
233
+ cachedRoot = root;
234
+ }
235
+ return cachedValue;
236
+ };
237
+ }
152
238
  return compileValueFunction(node);
153
239
  }
154
240
 
241
+ // getter producing a NodesType result (an array of the selected values).
242
+ // The argument is a filter query or a nodes-returning extension.
243
+ function compileNodesGetter(arg) {
244
+ if (arg.kind !== 'query')
245
+ return compileUserFunction(arg);
246
+ const query = arg.query;
247
+ const segs = query.segments.map(compileSegmentV);
248
+ const relative = query.relative;
249
+ return (current, root) => runSegmentsV(segs, relative ? current : root, root);
250
+ }
251
+
155
252
  function compileValueFunction(func) {
156
253
  switch (func.name) {
157
254
  case 'length': {
@@ -168,32 +265,97 @@ function compileValueFunction(func) {
168
265
  };
169
266
  }
170
267
  case 'count': {
171
- const query = func.args[0].query;
172
- if (isSingularSegments(query.segments)) {
173
- const getter = compileSingularGetter(query.segments, query.relative);
268
+ const arg = func.args[0];
269
+ if (arg.kind === 'query' && isSingularSegments(arg.query.segments)) {
270
+ const getter = compileSingularGetter(arg.query.segments, arg.query.relative);
174
271
  return (current, root) => (getter(current, root) === NOTHING ? 0 : 1);
175
272
  }
176
- const segs = query.segments.map(compileSegmentV);
177
- const relative = query.relative;
178
- return (current, root) => runSegmentsV(segs, relative ? current : root, root).length;
273
+ const get = compileNodesGetter(arg);
274
+ return (current, root) => get(current, root).length;
179
275
  }
180
276
  case 'value': {
181
- const query = func.args[0].query;
182
- if (isSingularSegments(query.segments))
183
- return compileSingularGetter(query.segments, query.relative);
184
- const segs = query.segments.map(compileSegmentV);
185
- const relative = query.relative;
277
+ const arg = func.args[0];
278
+ if (arg.kind === 'query' && isSingularSegments(arg.query.segments))
279
+ return compileSingularGetter(arg.query.segments, arg.query.relative);
280
+ // eager for the same reason as compileExists: this runs per
281
+ // candidate node inside a filter
282
+ const get = compileNodesGetter(arg);
186
283
  return (current, root) => {
187
- const result = runSegmentsV(segs, relative ? current : root, root);
284
+ const result = get(current, root);
188
285
  return result.length === 1 ? result[0] : NOTHING;
189
286
  };
190
287
  }
191
- /* c8 ignore next 2 -- guarded by the parser's well-typedness checks */
192
288
  default:
193
- throw new Error(`JSONPath: function '${func.name}' does not return ValueType`);
289
+ return compileUserFunction(func);
194
290
  }
195
291
  }
196
292
 
293
+ // Compile a registered function extension (RFC 9535 section 2.4).
294
+ // Arguments are marshalled to their declared types - ValueType is a
295
+ // JSON value or NOTHING, NodesType an array of values, LogicalType a
296
+ // boolean - and the result is checked against the declared return type,
297
+ // because a registry entry that lies about its type would otherwise
298
+ // corrupt the comparison rules further up.
299
+ function compileUserFunction(func) {
300
+ /* c8 ignore next 2 -- guarded by the parser's well-typedness checks */
301
+ if (typeof func.evaluate !== 'function')
302
+ throw new Error(`JSONPath: function '${func.name}' has no implementation`);
303
+ const evaluate = func.evaluate;
304
+ const name = func.name;
305
+ const params = func.params;
306
+ const gets = new Array(func.args.length);
307
+ for (let i = 0; i < gets.length; i++) {
308
+ const arg = func.args[i];
309
+ gets[i] = params[i] === 'value'
310
+ ? compileComparable(arg)
311
+ : params[i] === 'nodes'
312
+ ? compileNodesGetter(arg)
313
+ : compileLogicalExpr(arg.expr);
314
+ }
315
+
316
+ let call;
317
+ switch (gets.length) {
318
+ case 0:
319
+ call = () => evaluate();
320
+ break;
321
+ case 1: {
322
+ const a = gets[0];
323
+ call = (c, r) => evaluate(a(c, r));
324
+ break;
325
+ }
326
+ case 2: {
327
+ const a = gets[0];
328
+ const b = gets[1];
329
+ call = (c, r) => evaluate(a(c, r), b(c, r));
330
+ break;
331
+ }
332
+ default:
333
+ call = (c, r) => {
334
+ const argv = new Array(gets.length);
335
+ for (let i = 0; i < gets.length; i++)
336
+ argv[i] = gets[i](c, r);
337
+ return evaluate(...argv);
338
+ };
339
+ }
340
+
341
+ if (func.returns === 'logical')
342
+ return (c, r) => call(c, r) === true;
343
+ if (func.returns === 'value') {
344
+ // a function may legitimately return Nothing; `undefined` is not a
345
+ // JSON value, so it is the natural spelling of it
346
+ return (c, r) => {
347
+ const v = call(c, r);
348
+ return v === undefined ? NOTHING : v;
349
+ };
350
+ }
351
+ return (c, r) => {
352
+ const v = call(c, r);
353
+ if (!Array.isArray(v))
354
+ throw new TypeError(`JSONPath: function '${name}' must return an array of nodes (NodesType)`);
355
+ return v;
356
+ };
357
+ }
358
+
197
359
  function compileRegexTest(func, fullMatch) {
198
360
  const inputGet = compileComparable(func.args[0]);
199
361
  const patternArg = func.args[1];
@@ -236,20 +398,20 @@ function compileComparison(expr) {
236
398
  case '!=':
237
399
  return (c, r) => !cmpEquals(left(c, r), right(c, r));
238
400
  case '<':
239
- return (c, r) => cmpLess(left(c, r), right(c, r));
401
+ return (c, r) => compareJsonScalarLt(left(c, r), right(c, r));
240
402
  case '>':
241
- return (c, r) => cmpLess(right(c, r), left(c, r));
403
+ return (c, r) => compareJsonScalarLt(right(c, r), left(c, r));
242
404
  case '<=':
243
405
  return (c, r) => {
244
406
  const a = left(c, r);
245
407
  const b = right(c, r);
246
- return cmpLess(a, b) || cmpEquals(a, b);
408
+ return compareJsonScalarLt(a, b) || cmpEquals(a, b);
247
409
  };
248
410
  default: // '>='
249
411
  return (c, r) => {
250
412
  const a = left(c, r);
251
413
  const b = right(c, r);
252
- return cmpLess(b, a) || cmpEquals(a, b);
414
+ return compareJsonScalarLt(b, a) || cmpEquals(a, b);
253
415
  };
254
416
  }
255
417
  }
@@ -288,8 +450,17 @@ export function compileLogicalExpr(expr) {
288
450
  }
289
451
  case 'exists':
290
452
  return compileExists(expr.query);
291
- case 'ftest':
292
- return compileRegexTest(expr.func, expr.func.name === 'match');
453
+ case 'ftest': {
454
+ const func = expr.func;
455
+ if (func.name === 'match' || func.name === 'search')
456
+ return compileRegexTest(func, func.name === 'match');
457
+ // a registered extension as a test expression: LogicalType is the
458
+ // answer itself, NodesType is true for a non-empty nodelist
459
+ const fn = compileUserFunction(func);
460
+ if (func.returns === 'logical')
461
+ return fn;
462
+ return (c, r) => fn(c, r).length > 0;
463
+ }
293
464
  default: // 'cmp'
294
465
  return compileComparison(expr);
295
466
  }
@@ -299,6 +470,29 @@ export function compileLogicalExpr(expr) {
299
470
 
300
471
  //#region segment compilation (values mode)
301
472
 
473
+ // Resolve a slice selector's bounds against an array length (RFC 9535
474
+ // section 2.3.4.2.2) for a `for (i = sliceFrom; i != sliceTo; i += step)`
475
+ // walk - exclusive at `sliceTo` in both directions. The two results are
476
+ // handed back through module scope rather than an object or a pair,
477
+ // because this runs once per input node on every slice selector in all
478
+ // three segment modes and must not allocate. Read them immediately; no
479
+ // slice applier calls anything in between.
480
+ let sliceFrom = 0;
481
+ let sliceTo = 0;
482
+
483
+ function sliceBounds(start, end, step, len) {
484
+ const s = start === null ? (step > 0 ? 0 : len - 1) : (start < 0 ? len + start : start);
485
+ const e = end === null ? (step > 0 ? len : -1) : (end < 0 ? len + end : end);
486
+ if (step > 0) {
487
+ sliceFrom = s < 0 ? 0 : (s > len ? len : s);
488
+ sliceTo = e < 0 ? 0 : (e > len ? len : e);
489
+ }
490
+ else {
491
+ sliceFrom = s < -1 ? -1 : (s > len - 1 ? len - 1 : s);
492
+ sliceTo = e < -1 ? -1 : (e > len - 1 ? len - 1 : e);
493
+ }
494
+ }
495
+
302
496
  // selector-node functions: (value, output, root) => void
303
497
 
304
498
  export function compileSelectorNodeV(sel) {
@@ -351,26 +545,23 @@ export function compileSelectorNodeV(sel) {
351
545
  const len = v.length;
352
546
  if (len === 0)
353
547
  return;
354
- // bounds per RFC 9535 section 2.3.4.2.2
355
- const s = start === null ? (step > 0 ? 0 : len - 1) : (start < 0 ? len + start : start);
356
- const e = end === null ? (step > 0 ? len : -1) : (end < 0 ? len + end : end);
548
+ sliceBounds(start, end, step, len);
357
549
  if (step > 0) {
358
- const lower = s < 0 ? 0 : (s > len ? len : s);
359
- const upper = e < 0 ? 0 : (e > len ? len : e);
360
- for (let i = lower; i < upper; i += step)
550
+ for (let i = sliceFrom; i < sliceTo; i += step)
361
551
  out.push(v[i]);
362
552
  }
363
553
  else {
364
- const upper = s < -1 ? -1 : (s > len - 1 ? len - 1 : s);
365
- const lower = e < -1 ? -1 : (e > len - 1 ? len - 1 : e);
366
- for (let i = upper; i > lower; i += step)
554
+ for (let i = sliceFrom; i > sliceTo; i += step)
367
555
  out.push(v[i]);
368
556
  }
369
557
  };
370
558
  }
371
559
  default: { // 'filter'
372
- const pred = compileLogicalExpr(sel.expr);
560
+ const pred = compileFilterPredicate(sel.expr);
561
+ const reset = filterReset;
373
562
  return (v, out, root) => {
563
+ if (reset !== null)
564
+ reset();
374
565
  if (Array.isArray(v)) {
375
566
  for (let i = 0; i < v.length; i++) {
376
567
  if (pred(v[i], root))
@@ -402,15 +593,21 @@ export function descendV(v, output, root, apply) {
402
593
  }
403
594
  }
404
595
 
596
+ // Combine a segment's selectors into one (value, output, root) => void
597
+ // applier; a single selector is its own applier.
598
+ function compileSelectorsV(seg) {
599
+ const fns = seg.selectors.map(compileSelectorNodeV);
600
+ if (fns.length === 1)
601
+ return fns[0];
602
+ return (v, out, root) => {
603
+ for (let i = 0; i < fns.length; i++)
604
+ fns[i](v, out, root);
605
+ };
606
+ }
607
+
405
608
  // segment functions: (input, output, root) => void
406
609
  export function compileSegmentV(seg) {
407
- const fns = seg.selectors.map(compileSelectorNodeV);
408
- const apply = fns.length === 1
409
- ? fns[0]
410
- : (v, out, root) => {
411
- for (let i = 0; i < fns.length; i++)
412
- fns[i](v, out, root);
413
- };
610
+ const apply = compileSelectorsV(seg);
414
611
  if (seg.descendant) {
415
612
  return (input, output, root) => {
416
613
  for (let i = 0; i < input.length; i++)
@@ -425,11 +622,161 @@ export function compileSegmentV(seg) {
425
622
 
426
623
  //#endregion
427
624
 
625
+ //#region segment compilation (lazy values mode)
626
+ // The same selectors as values mode, pulled one node at a time instead
627
+ // of pushed into a nodelist. A consumer that stops early (`first`,
628
+ // `exists`, `value()`'s two-node test) never visits the rest of the
629
+ // document: a filter evaluates its predicate only until a node passes,
630
+ // a wildcard reads only the children actually pulled, and a descendant
631
+ // segment abandons the walk mid-subtree.
632
+ //
633
+ // Nothing here is buffered - every applier yields directly - so the
634
+ // laziness is per node, not per segment. Enumeration order is identical
635
+ // to runSegmentsV: a segment maps each input node to its outputs in
636
+ // order and concatenates them, so pulling the chain depth-first visits
637
+ // exactly the sequence values mode builds breadth-first.
638
+
639
+ // selector-node functions: (value, root) => Generator<any>
640
+
641
+ function compileSelectorNodeG(sel) {
642
+ switch (sel.kind) {
643
+ case 'name': {
644
+ const name = sel.name;
645
+ return function* nameG(v) {
646
+ if (typeof v === 'object' && v !== null && !Array.isArray(v) && hasOwn(v, name))
647
+ yield v[name];
648
+ };
649
+ }
650
+ case 'index': {
651
+ const index = sel.index;
652
+ return function* indexG(v) {
653
+ if (!Array.isArray(v))
654
+ return;
655
+ const idx = index < 0 ? v.length + index : index;
656
+ if (idx >= 0 && idx < v.length)
657
+ yield v[idx];
658
+ };
659
+ }
660
+ case 'wildcard':
661
+ return function* wildcardG(v) {
662
+ if (Array.isArray(v)) {
663
+ yield* v;
664
+ }
665
+ else if (typeof v === 'object' && v !== null) {
666
+ for (const key in v) {
667
+ if (hasOwn(v, key))
668
+ yield v[key];
669
+ }
670
+ }
671
+ };
672
+ case 'slice': {
673
+ const start = sel.start;
674
+ const end = sel.end;
675
+ const step = sel.step === null ? 1 : sel.step;
676
+ if (step === 0)
677
+ return function* emptySliceG() { };
678
+ return function* sliceG(v) {
679
+ if (!Array.isArray(v))
680
+ return;
681
+ const len = v.length;
682
+ if (len === 0)
683
+ return;
684
+ sliceBounds(start, end, step, len);
685
+ // read the bounds out before the first yield: a suspended
686
+ // generator must not depend on the shared scratch surviving
687
+ const from = sliceFrom;
688
+ const to = sliceTo;
689
+ if (step > 0) {
690
+ for (let i = from; i < to; i += step)
691
+ yield v[i];
692
+ }
693
+ else {
694
+ for (let i = from; i > to; i += step)
695
+ yield v[i];
696
+ }
697
+ };
698
+ }
699
+ default: { // 'filter'
700
+ const pred = compileFilterPredicate(sel.expr);
701
+ const reset = filterReset;
702
+ return function* filterG(v, root) {
703
+ if (reset !== null)
704
+ reset();
705
+ if (Array.isArray(v)) {
706
+ for (let i = 0; i < v.length; i++) {
707
+ if (pred(v[i], root))
708
+ yield v[i];
709
+ }
710
+ }
711
+ else if (typeof v === 'object' && v !== null) {
712
+ for (const key in v) {
713
+ if (hasOwn(v, key) && pred(v[key], root))
714
+ yield v[key];
715
+ }
716
+ }
717
+ };
718
+ }
719
+ }
720
+ }
721
+
722
+ function* descendG(v, root, apply) {
723
+ yield* apply(v, root);
724
+ if (Array.isArray(v)) {
725
+ for (let i = 0; i < v.length; i++)
726
+ yield* descendG(v[i], root, apply);
727
+ }
728
+ else if (typeof v === 'object' && v !== null) {
729
+ for (const key in v) {
730
+ if (hasOwn(v, key))
731
+ yield* descendG(v[key], root, apply);
732
+ }
733
+ }
734
+ }
735
+
736
+ /**
737
+ * Compile a segment into its lazy form: a generator function yielding
738
+ * the nodes one input value contributes, in document order.
739
+ * @param {object} seg - a parsed query segment
740
+ * @returns {(value: any, root: any) => Generator<any>}
741
+ */
742
+ export function compileSegmentG(seg) {
743
+ const fns = seg.selectors.map(compileSelectorNodeG);
744
+ const apply = fns.length === 1
745
+ ? fns[0]
746
+ : function* applyG(v, root) {
747
+ for (let i = 0; i < fns.length; i++)
748
+ yield* fns[i](v, root);
749
+ };
750
+ if (seg.descendant)
751
+ return function* segmentDescendantG(v, root) { yield* descendG(v, root, apply); };
752
+ return apply;
753
+ }
754
+
755
+ /**
756
+ * Lazily enumerate the nodelist a compiled segment chain selects,
757
+ * yielding values in document order.
758
+ * @param {Function[]} gens - segment generators from compileSegmentG
759
+ * @param {number} i - the segment to apply (0 to start the chain)
760
+ * @param {any} value - the value this segment applies to
761
+ * @param {any} root - the query root (`$` inside embedded filters)
762
+ * @returns {Generator<any>}
763
+ */
764
+ export function* runSegmentsG(gens, i, value, root) {
765
+ if (i === gens.length) {
766
+ yield value;
767
+ return;
768
+ }
769
+ for (const v of gens[i](value, root))
770
+ yield* runSegmentsG(gens, i + 1, v, root);
771
+ }
772
+
773
+ //#endregion
774
+
428
775
  //#region segment compilation (nodes mode, normalized paths)
429
- // Moved verbatim from path.js: the selector/segment compilers producing
430
- // (value, normalized-path) pairs per RFC 9535 section 2.7. path.js
431
- // imports them back for `query.nodes()`/`query.paths()`; nodes mode
432
- // stays lazily compiled there, so value-only queries never pay for it.
776
+ // The selector/segment compilers producing (value, normalized-path)
777
+ // pairs per RFC 9535 section 2.7. path.js imports them for
778
+ // `query.nodes()`/`query.paths()`; nodes mode stays lazily compiled
779
+ // there, so value-only queries never pay for it.
433
780
 
434
781
  // eslint-disable-next-line no-control-regex
435
782
  export const RE_NAME_NEEDS_ESCAPE = /['\\\u0000-\u001f]/;
@@ -516,20 +863,15 @@ export function compileSelectorNodeP(sel) {
516
863
  const len = v.length;
517
864
  if (len === 0)
518
865
  return;
519
- const s = start === null ? (step > 0 ? 0 : len - 1) : (start < 0 ? len + start : start);
520
- const e = end === null ? (step > 0 ? len : -1) : (end < 0 ? len + end : end);
866
+ sliceBounds(start, end, step, len);
521
867
  if (step > 0) {
522
- const lower = s < 0 ? 0 : (s > len ? len : s);
523
- const upper = e < 0 ? 0 : (e > len ? len : e);
524
- for (let i = lower; i < upper; i += step) {
868
+ for (let i = sliceFrom; i < sliceTo; i += step) {
525
869
  outV.push(v[i]);
526
870
  outP.push(p + '[' + i + ']');
527
871
  }
528
872
  }
529
873
  else {
530
- const upper = s < -1 ? -1 : (s > len - 1 ? len - 1 : s);
531
- const lower = e < -1 ? -1 : (e > len - 1 ? len - 1 : e);
532
- for (let i = upper; i > lower; i += step) {
874
+ for (let i = sliceFrom; i > sliceTo; i += step) {
533
875
  outV.push(v[i]);
534
876
  outP.push(p + '[' + i + ']');
535
877
  }
@@ -537,8 +879,11 @@ export function compileSelectorNodeP(sel) {
537
879
  };
538
880
  }
539
881
  default: { // 'filter'
540
- const pred = compileLogicalExpr(sel.expr);
882
+ const pred = compileFilterPredicate(sel.expr);
883
+ const reset = filterReset;
541
884
  return (v, p, outV, outP, root) => {
885
+ if (reset !== null)
886
+ reset();
542
887
  if (Array.isArray(v)) {
543
888
  for (let i = 0; i < v.length; i++) {
544
889
  if (pred(v[i], root)) {