@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
@@ -18,9 +18,11 @@
18
18
  // allocating in the same frame). Free names become external parameters,
19
19
  // collected in order of first appearance.
20
20
 
21
- import { parseJSONPath, JSONPathSyntaxError } from '../path.js';
21
+ import { parseJSONPath, JSONPathSyntaxError, RE_JSONPATH_VARIABLE_HEAD } from '../path.js';
22
22
  import { isSingularSegments } from '../segments.js';
23
+ import { encodeJSONPointerSegment } from '../pointer.js';
23
24
  import { JsonQueryCompileError } from './errors.js';
25
+ import { deepFreeze, isJsonObject } from '@jarenjs/core/object';
24
26
  // The operator registry: `name -> { params, result, compile }`. Only
25
27
  // referenced inside functions (never at module evaluation time), so the
26
28
  // import cycle normalize.js <-> operators.js is initialization-safe.
@@ -28,6 +30,17 @@ import { OPERATORS } from './operators.js';
28
30
 
29
31
  //#region cardinality
30
32
 
33
+ /**
34
+ * The twelve node kinds of the normalized form, sorted — the published
35
+ * AST contract (QUERY-FORMAT.md Appendix C). An exhaustiveness gate
36
+ * asserts a corpus exercising every construct produces exactly this
37
+ * set, so a new kind cannot ship undocumented.
38
+ */
39
+ export const NODE_KINDS = Object.freeze([
40
+ 'array', 'call', 'flwor', 'let', 'literal', 'map',
41
+ 'object', 'op', 'path', 'quant', 'raw', 'var',
42
+ ]);
43
+
31
44
  /** Statically empty (the node always evaluates to the empty sequence). */
32
45
  export const CARD_ZERO = 0;
33
46
  /** Always exactly one item; compile.js skips all sequence checks. */
@@ -74,17 +87,38 @@ export function sumCard(a, b) {
74
87
  const hasOwn = Object.hasOwn;
75
88
 
76
89
  const VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
77
- const VAR_HEAD_RE = /^\$([A-Za-z_][A-Za-z0-9_]*)/;
90
+ // The '$name' head of a variable-rooted path string; shared with the
91
+ // json-path-segments format tester so both split at the same point.
92
+ const VAR_HEAD_RE = RE_JSONPATH_VARIABLE_HEAD;
78
93
 
79
94
  // FLWOR clause keys (QUERY-FORMAT.md section 6.1) and quantifier keys
80
95
  // (section 7). Clauses apply in the fixed semantic order of section 6.1
81
96
  // regardless of JSON key order (D7).
82
- const FLWOR_KEYS = new Set(['$for', '$let', '$as', '$where', '$groupby', '$orderby', '$count', '$return']);
97
+ const FLWOR_KEYS = new Set(['$fold', '$for', '$let', '$as', '$where', '$groupby', '$orderby', '$count', '$return']);
83
98
  const QUANTIFIER_KEYS = new Set(['$some', '$every', '$satisfies']);
84
99
 
85
100
  // Keys of the explicit $orderby key-spec form (section 6.6). Contextual:
86
101
  // they are not operators and stay outside the KNOWN_KEYS vocabulary.
87
- const ORDERBY_SPEC_KEYS = new Set(['$key', '$dir', '$empty']);
102
+ const ORDERBY_SPEC_KEYS = new Set(['$key', '$dir', '$empty', '$collation']);
103
+
104
+ // Keys of the extended $for binding form (sections 6.2, 6.10): the
105
+ // source, a positional variable, the outer-join switch, and the window
106
+ // specification. Also contextual - not operators.
107
+ const FOR_BINDING_KEYS = new Set(['$in', '$at', '$allowing-empty', '$window', '$size', '$step']);
108
+
109
+ // Whether a binding source uses the extended object form rather than
110
+ // being an ordinary expression. Any of its keys marks it, so a malformed
111
+ // combination is diagnosed as a bad binding instead of silently
112
+ // normalizing as a map constructor.
113
+ function isExtendedBinding(source) {
114
+ if (!isJsonObject(source))
115
+ return false;
116
+ for (const key of FOR_BINDING_KEYS) {
117
+ if (hasOwn(source, key))
118
+ return true;
119
+ }
120
+ return false;
121
+ }
88
122
 
89
123
  // Escape hatches (QUERY-FORMAT.md section 3.5): structural forms with
90
124
  // dedicated normalizer cases; every other operator lives in the registry.
@@ -106,19 +140,32 @@ function isVocabularyKey(key, ctx) {
106
140
 
107
141
  //#region helpers
108
142
 
109
- function isPlainObject(v) {
110
- return typeof v === 'object' && v !== null && !Array.isArray(v);
111
- }
112
-
113
- // RFC 6901 reference token escaping for docPath pointers
114
- function escapeToken(token) {
115
- if (token.indexOf('~') < 0 && token.indexOf('/') < 0)
116
- return token;
117
- return token.replace(/~/g, '~0').replace(/\//g, '~1');
143
+ function fail(code, message, docPath, options) {
144
+ throw new JsonQueryCompileError(code, message, docPath, options);
118
145
  }
119
146
 
120
- function fail(code, message, docPath) {
121
- throw new JsonQueryCompileError(code, message, docPath);
147
+ /**
148
+ * Project a safe diagnostic string from whatever a host hook threw:
149
+ * no `.message` read on a raw value, no user coercion, no
150
+ * proxy-observable reflection — the compiler must never fail while
151
+ * describing a host failure.
152
+ * @param {unknown} e
153
+ * @returns {string}
154
+ */
155
+ export function hostFailureText(e) {
156
+ try {
157
+ if (e instanceof Error) {
158
+ const message = e.message;
159
+ if (typeof message === 'string') return message;
160
+ return 'host error (message unavailable)';
161
+ }
162
+ }
163
+ catch { /* hostile classification or accessor */ }
164
+ if (e === null) return 'null';
165
+ const t = typeof e;
166
+ if (t === 'string') return e.length > 80 ? e.slice(0, 80) + '…' : e;
167
+ if (t === 'number' || t === 'boolean' || t === 'bigint' || t === 'undefined') return String(e);
168
+ return t === 'symbol' ? 'a symbol' : t === 'function' ? 'a function' : 'an object';
122
169
  }
123
170
 
124
171
  /**
@@ -144,22 +191,13 @@ export function deepFreezeCopy(value) {
144
191
  return Object.freeze(out);
145
192
  }
146
193
 
147
- function deepFreeze(value) {
148
- if (typeof value !== 'object' || value === null)
149
- return value;
150
- const keys = Object.keys(value);
151
- for (let i = 0; i < keys.length; i++)
152
- deepFreeze(value[keys[i]]);
153
- return Object.freeze(value);
154
- }
155
-
156
194
  //#endregion
157
195
 
158
196
  //#region strings (Rule 2)
159
197
 
160
- function parsePathString(source, original, docPath) {
198
+ function parsePathString(source, original, docPath, ctx) {
161
199
  try {
162
- return parseJSONPath(source);
200
+ return parseJSONPath(source, ctx.pathOptions);
163
201
  }
164
202
  catch (e) {
165
203
  /* c8 ignore next 2 -- parseJSONPath only throws syntax errors */
@@ -186,14 +224,23 @@ function makePathNode(name, rootSlot, external, rootCard, segments, docPath) {
186
224
 
187
225
  // resolve a variable reference: walk the lexical scope chain; a free name
188
226
  // is an external parameter, allocated a slot on first appearance (spec
189
- // section 9: use is the declaration)
190
- function resolveVariable(name, scope, ctx) {
227
+ // section 9: use is the declaration). Under a closed-world compilation
228
+ // (options.externals), only declared names may stay free - every other
229
+ // free name is JQ0005 at its own reference site.
230
+ function resolveVariable(name, scope, ctx, docPath) {
191
231
  for (let sc = scope; sc !== null; sc = sc.parent) {
192
232
  if (sc.name === name)
193
233
  return { slot: sc.slot, card: sc.card, external: false };
194
234
  }
195
235
  let slot = ctx.externals.get(name);
196
236
  if (slot === undefined) {
237
+ if (ctx.declaredExternals !== null && !ctx.declaredExternals.has(name)) {
238
+ const declared = [...ctx.declaredExternals];
239
+ fail('JQ0005', `'$${name}' is neither bound by an enclosing phrase nor a declared external`
240
+ + (declared.length === 0
241
+ ? ' (this query was compiled closed-world, declaring no externals)'
242
+ : ` (declared externals: ${declared.map((n) => "'" + n + "'").join(', ')})`), docPath);
243
+ }
197
244
  slot = ctx.nextSlot++;
198
245
  ctx.externals.set(name, slot);
199
246
  }
@@ -210,7 +257,7 @@ function normalizeString(s, docPath, scope, ctx) {
210
257
  if (c1 === 0x24) // '$$' escape: drop exactly one leading '$'
211
258
  return makeLiteral(s.slice(1), docPath);
212
259
  if (c1 === 0x2E || c1 === 0x5B) { // '$.' | '$[' | '$..' - absolute path
213
- const ast = parsePathString(s, s, docPath);
260
+ const ast = parsePathString(s, s, docPath, ctx);
214
261
  return makePathNode('$', 0, false, CARD_ONE, ast.segments, docPath);
215
262
  }
216
263
  const m = VAR_HEAD_RE.exec(s);
@@ -218,12 +265,12 @@ function normalizeString(s, docPath, scope, ctx) {
218
265
  return fail('JQ0004', `'${s}' is not a valid path or escape`, docPath);
219
266
  const name = m[1];
220
267
  const rest = s.slice(m[0].length);
221
- const ref = resolveVariable(name, scope, ctx);
268
+ const ref = resolveVariable(name, scope, ctx, docPath);
222
269
  if (rest === '') // bare '$name': whole-variable reference
223
270
  return Object.freeze({ kind: 'var', card: ref.card, docPath, slot: ref.slot, external: ref.external, name });
224
271
  // variable-rooted path: the grammar is RFC 9535 with the root identifier
225
272
  // replaced by the variable reference - parse with a substituted '$'
226
- const ast = parsePathString('$' + rest, s, docPath);
273
+ const ast = parsePathString('$' + rest, s, docPath, ctx);
227
274
  return makePathNode(name, ref.slot, ref.external, ref.card, ast.segments, docPath);
228
275
  }
229
276
 
@@ -244,7 +291,7 @@ function normalizeObject(obj, docPath, scope, ctx) {
244
291
  const name = keys[i];
245
292
  entries[i] = Object.freeze({
246
293
  name,
247
- expr: normalizeExpr(obj[name], docPath + '/' + escapeToken(name), scope, ctx),
294
+ expr: normalizeExpr(obj[name], docPath + '/' + encodeJSONPointerSegment(name), scope, ctx),
248
295
  });
249
296
  }
250
297
  return Object.freeze({ kind: 'object', card: CARD_ONE, docPath, entries: Object.freeze(entries) });
@@ -265,7 +312,8 @@ function normalizePhrase(obj, keys, docPath, scope, ctx) {
265
312
  break;
266
313
  }
267
314
  }
268
- if (allFlwor && keys.length >= 2 && hasOwn(obj, '$return') && (hasOwn(obj, '$for') || hasOwn(obj, '$let'))) {
315
+ if (allFlwor && keys.length >= 2 && hasOwn(obj, '$return')
316
+ && (hasOwn(obj, '$for') || hasOwn(obj, '$let') || hasOwn(obj, '$fold'))) {
269
317
  if (keys.length === 2 && hasOwn(obj, '$let'))
270
318
  return normalizeLetPhrase(obj, docPath, scope, ctx);
271
319
  return normalizeFlworPhrase(obj, docPath, scope, ctx);
@@ -318,11 +366,57 @@ function levenshtein(a, b) {
318
366
  // suggestion candidates, built lazily (see the isVocabularyKey note)
319
367
  let VOCABULARY_NAMES = null;
320
368
 
321
- // JQ0002 for an unknown $-key, with a "did you mean" suggestion when a
322
- // vocabulary key is within Levenshtein distance 2 (compile-time only).
323
- // With host extensions the candidate list is built per call (this is the
324
- // error path); the lazy global stays for the core-only case.
369
+ // SEMANTIC aliases the names a writer reaches for from other query
370
+ // languages (SQL, XPath, JSONata, JS array methods) or a plausible
371
+ // synonym, which Levenshtein cannot reach because they are lexically
372
+ // far from the real operator. This is the failure mode an LLM lands in
373
+ // most: it guesses `$first`/`$filter`/`$map` and, without a pointer,
374
+ // abandons the language. Each entry is the CLOSEST real spelling; a
375
+ // value of null means "no operator does this — here is how instead".
376
+ const OPERATOR_ALIASES = {
377
+ // sequence access
378
+ $first: "$head", $last: "$head of $reverse", $nth: "$get", $at: "$get",
379
+ $take: "$subsequence", $skip: "$subsequence", $slice: "$subsequence",
380
+ $drop: "$subsequence", $limit: "$subsequence",
381
+ // filtering / mapping / folding: these are FLWOR or a JSONPath filter,
382
+ // not operators ($where, $map, $fold, $orderby ARE real keys and never
383
+ // reach here — only the guesses that miss them are listed)
384
+ $filter: "a JSONPath filter like $[?(@.x > 1)] or $where in a $for",
385
+ $select: "a $for phrase with $return", $flatmap: "a $for phrase",
386
+ $reduce: "$fold", $foldl: "$fold", $aggregate: "$fold",
387
+ $group: "$groupby", "$group-by": "$groupby",
388
+ $sortby: "$sort (sorts scalars; order objects via a $for over a sorted key)",
389
+ "$sort-by": "$sort (scalars only)", $order: "$sort",
390
+ // aggregates / arithmetic
391
+ $size: "$count", $len: "$length", $abs: null, $round: null, $floor: "$idiv",
392
+ $ceil: null, $sqrt: null, $pow: null, $modulo: "$mod", $remainder: "$mod",
393
+ $subtract: "$sub", $multiply: "$mul", $divide: "$div", $minus: "$sub",
394
+ $times: "$mul", $negate: "$neg", $product: "$mul", $total: "$sum",
395
+ // strings / collections
396
+ $join: "$string-join", $split: null, $includes: "$contains",
397
+ $indexof: "$index-of", $find: "$index-of", $keys: "$entries",
398
+ $values: "$entries then $get", $has: "$exists", $tostring: "$string",
399
+ $tonumber: "$number", $len_str: "$string-length", $trim: "$normalize-space",
400
+ $lowercase: "$lower", $uppercase: "$upper", $startswith: "$starts-with",
401
+ $endswith: "$ends-with", $unique: "$distinct", $flatten: "a $for phrase",
402
+ // conditionals ($coalesce is a real operator; the guesses are here)
403
+ $case: "$if", $cond: "$if", $switch: "$if", $ternary: "$if",
404
+ $ifnull: "$default", $ifempty: "$default", $nvl: "$default",
405
+ };
406
+
407
+ // JQ0002 for an unknown $-key, with a "did you mean" suggestion. A
408
+ // curated SEMANTIC alias wins first (the cross-language guesses
409
+ // Levenshtein misses); otherwise the nearest vocabulary key within
410
+ // Levenshtein distance 2. Compile-time only; with host extensions the
411
+ // candidate list is built per call.
325
412
  function failUnknownOperator(key, docPath, ctx) {
413
+ if (hasOwn(OPERATOR_ALIASES, key)) {
414
+ const target = OPERATOR_ALIASES[key];
415
+ const hint = target === null
416
+ ? ' (no operator does this in jaren-query)'
417
+ : ` (use ${target.startsWith('$') && !target.includes(' ') ? `'${target}'` : target})`;
418
+ return fail('JQ0002', `unknown operator '${key}'${hint}`, docPath);
419
+ }
326
420
  if (VOCABULARY_NAMES === null) {
327
421
  VOCABULARY_NAMES = [
328
422
  ...FLWOR_KEYS, ...QUANTIFIER_KEYS, ...ESCAPE_KEYS, ...Object.keys(OPERATORS),
@@ -378,15 +472,22 @@ function normalizeElements(arg, docPath, scope, ctx) {
378
472
  // No hook installed is JQ0008; a hook rejection (invalid schema) is
379
473
  // JQ0009, both at the owning operator's/clause's docPath.
380
474
  function compileSchemaLiteral(value, schemaPath, opPath, ctx) {
381
- if (ctx.compileTypeTest === null)
475
+ if (ctx.compileTypeTest === null) {
476
+ // Analysis mode (QUERY-FORMAT.md Appendix C.1): the schema literal
477
+ // normalizes without a compiled predicate, so a consumer can analyse
478
+ // a document it could not execute. Strictly opt-in — compilation
479
+ // never sets ctx.analysis, so its behaviour is untouched.
480
+ if (ctx.analysis)
481
+ return { schema: deepFreezeCopy(value), test: null };
382
482
  fail('JQ0008', 'schema operators require a type-test compiler (options.compileTypeTest)', opPath);
483
+ }
383
484
  const schema = deepFreezeCopy(value);
384
485
  let test;
385
486
  try {
386
487
  test = ctx.compileTypeTest(schema, schemaPath);
387
488
  }
388
489
  catch (e) {
389
- fail('JQ0009', `invalid schema literal: ${e.message}`, opPath);
490
+ fail('JQ0009', `invalid schema literal: ${hostFailureText(e)}`, opPath, { cause: e });
390
491
  }
391
492
  if (typeof test !== 'function')
392
493
  fail('JQ0009', 'the type-test compiler did not return a predicate function', opPath);
@@ -404,7 +505,7 @@ function makeRaw(value, docPath) {
404
505
  // One argument position of a registry operator, per its declared kind:
405
506
  // 'expr' normalizes an ordinary expression; 'raw' captures the value
406
507
  // verbatim, unevaluated; 'schema' is 'raw' plus a compiled type-test
407
- // predicate (the type-system work order's schema arguments); 'name'
508
+ // predicate, for schema-literal arguments (`compileSchemaLiteral`); 'name'
408
509
  // captures a validated variable name string. 'raw', 'schema' and 'name'
409
510
  // produce inert `raw` nodes - compile-time data, never compiled.
410
511
  function normalizeArg(kind, value, argPath, scope, ctx, opPath) {
@@ -450,9 +551,15 @@ function argCards(args) {
450
551
  // `result` - individual operators never re-check structure.
451
552
  function normalizeOperatorCall(key, entry, arg, docPath, opPath, scope, ctx) {
452
553
  const args = normalizeParams(key, entry.params, arg, opPath, scope, ctx);
453
- return Object.freeze({
554
+ /** @type {any} */
555
+ const node = {
454
556
  kind: 'op', card: entry.result(argCards(args)), docPath, name: key, args: Object.freeze(args),
455
- });
557
+ };
558
+ // $range materializes its whole result: the resource guard becomes
559
+ // configurable through the compilation's limits (compileOp hands the
560
+ // node through to the entry's compile)
561
+ if (key === '$range' && ctx.limits !== null) node.limits = ctx.limits;
562
+ return Object.freeze(node);
456
563
  }
457
564
 
458
565
  // Helpers handed to an extension entry's `normalize` override; see
@@ -485,10 +592,30 @@ function normalizeExtensionCall(key, entry, arg, docPath, opPath, scope, ctx) {
485
592
 
486
593
  function normalizeOperator(key, arg, docPath, scope, ctx) {
487
594
  const opPath = docPath + '/' + key;
595
+ ctx.usedOps.add(key); // dependency reporting (query.explain)
488
596
  switch (key) {
489
597
  case '$const': // quote: verbatim single item, nothing inside evaluated
490
598
  return Object.freeze({ kind: 'literal', card: CARD_ONE, docPath, value: deepFreezeCopy(arg) });
491
599
 
600
+ case '$call': { // a registered trusted host function (options.functions)
601
+ if (!Array.isArray(arg) || arg.length < 1 || typeof arg[0] !== 'string')
602
+ return fail('JQ0010', "'$call' requires ['name', ...argument expressions]", opPath);
603
+ const name = arg[0];
604
+ const fn = ctx.functions !== null && hasOwn(ctx.functions, name)
605
+ ? ctx.functions[name]
606
+ : undefined;
607
+ if (fn === undefined)
608
+ return fail('JQ0010', `'$call' names no registered function '${name}'`, opPath);
609
+ ctx.usedFunctions.add(name);
610
+ const callArgs = new Array(arg.length - 1);
611
+ for (let i = 1; i < arg.length; i++)
612
+ callArgs[i - 1] = normalizeExpr(arg[i], opPath + '/' + i, scope, ctx);
613
+ return Object.freeze({
614
+ kind: 'call', card: CARD_OPT, docPath, name, fn,
615
+ args: Object.freeze(callArgs),
616
+ });
617
+ }
618
+
492
619
  case '$map': { // general map constructor (section 3.5.2)
493
620
  const list = requireExprArray(key, arg, 0, Infinity, opPath);
494
621
  const pairs = new Array(list.length);
@@ -538,7 +665,7 @@ function bindPhraseName(name, phraseNames, bindPath) {
538
665
  }
539
666
 
540
667
  function requireBindingObject(clause, bindObj, clausePath) {
541
- if (!isPlainObject(bindObj))
668
+ if (!isJsonObject(bindObj))
542
669
  fail('JQ0003', `'${clause}' takes an object of variable bindings`, clausePath);
543
670
  const names = Object.keys(bindObj);
544
671
  if (names.length === 0)
@@ -556,11 +683,11 @@ function normalizeLetBindings(letObj, letPath, scope, ctx, phraseNames, bindings
556
683
  let sc = scope;
557
684
  for (let i = 0; i < names.length; i++) {
558
685
  const name = names[i];
559
- const bindPath = letPath + '/' + escapeToken(name);
686
+ const bindPath = letPath + '/' + encodeJSONPointerSegment(name);
560
687
  bindPhraseName(name, phraseNames, bindPath);
561
688
  const source = letObj[name];
562
- if (isPlainObject(source) && (hasOwn(source, '$in') || hasOwn(source, '$at')))
563
- return fail('JQ0003', "the extended '$in'/'$at' binding form is not available in '$let'", bindPath);
689
+ if (isExtendedBinding(source))
690
+ return fail('JQ0003', "the extended binding form is not available in '$let'", bindPath);
564
691
  const expr = normalizeExpr(source, bindPath, sc, ctx);
565
692
  const slot = ctx.nextSlot++;
566
693
  sc = { name, slot, card: expr.card, parent: sc };
@@ -581,6 +708,34 @@ function normalizeLetPhrase(obj, docPath, scope, ctx) {
581
708
  });
582
709
  }
583
710
 
711
+ // The window specification of an extended $for binding (section 6.10).
712
+ // `$size`/`$step` are integer literals, not expressions: a window width
713
+ // that varied per tuple could not be compiled into a specialized loop,
714
+ // and no use has asked for one.
715
+ //
716
+ // A **tumbling** window partitions the item stream - every item lands in
717
+ // exactly one window - so its final window is kept even when short,
718
+ // because dropping it would silently lose data. A **sliding** window is
719
+ // a moving view of fixed width, so a short window is not one of them and
720
+ // the tail is not emitted. That is the whole rule.
721
+ function normalizeWindowSpec(source, bindPath) {
722
+ const kind = source.$window;
723
+ if (kind !== 'tumbling' && kind !== 'sliding')
724
+ return fail('JQ0003', "'$window' must be 'tumbling' or 'sliding'", bindPath + '/$window');
725
+ if (!hasOwn(source, '$size'))
726
+ return fail('JQ0003', "a '$window' binding requires '$size'", bindPath);
727
+ const size = source.$size;
728
+ if (!Number.isInteger(size) || size < 1)
729
+ return fail('JQ0003', "'$size' must be a positive integer", bindPath + '/$size');
730
+ let step = kind === 'tumbling' ? size : 1;
731
+ if (hasOwn(source, '$step')) {
732
+ step = source.$step;
733
+ if (!Number.isInteger(step) || step < 1)
734
+ return fail('JQ0003', "'$step' must be a positive integer", bindPath + '/$step');
735
+ }
736
+ return Object.freeze({ sliding: kind === 'sliding', size, step });
737
+ }
738
+
584
739
  // $for bindings (section 6.2): each name iterates its source, one item
585
740
  // per tuple (card ONE), with D4 array unpacking at runtime. The extended
586
741
  // {"$in": expr, "$at": name} form additionally binds a 0-based position
@@ -591,23 +746,45 @@ function normalizeForBindings(forObj, forPath, scope, ctx, phraseNames, bindings
591
746
  let sc = scope;
592
747
  for (let i = 0; i < names.length; i++) {
593
748
  const name = names[i];
594
- const bindPath = forPath + '/' + escapeToken(name);
749
+ const bindPath = forPath + '/' + encodeJSONPointerSegment(name);
595
750
  bindPhraseName(name, phraseNames, bindPath);
596
751
  let source = forObj[name];
597
752
  let sourcePath = bindPath;
598
753
  let atName = null;
599
- if (isPlainObject(source) && (hasOwn(source, '$in') || hasOwn(source, '$at'))) {
600
- if (Object.keys(source).length !== 2 || !hasOwn(source, '$in') || !hasOwn(source, '$at'))
601
- return fail('JQ0003', "the extended binding form takes exactly the keys '$in' and '$at'", bindPath);
602
- atName = source.$at;
603
- if (typeof atName !== 'string' || !VAR_NAME_RE.test(atName))
604
- return fail('JQ0003', "'$at' takes a variable name string", bindPath + '/$at');
754
+ let allowingEmpty = false;
755
+ let window = null;
756
+ if (isExtendedBinding(source)) {
757
+ const bindKeys = Object.keys(source);
758
+ for (let k = 0; k < bindKeys.length; k++) {
759
+ if (!FOR_BINDING_KEYS.has(bindKeys[k]))
760
+ return fail('JQ0003', `'${bindKeys[k]}' is not a valid key of an extended '$for' binding`, bindPath);
761
+ }
762
+ if (!hasOwn(source, '$in'))
763
+ return fail('JQ0003', "an extended '$for' binding requires '$in'", bindPath);
764
+ if (hasOwn(source, '$at')) {
765
+ atName = source.$at;
766
+ if (typeof atName !== 'string' || !VAR_NAME_RE.test(atName))
767
+ return fail('JQ0003', "'$at' takes a variable name string", bindPath + '/$at');
768
+ }
769
+ if (hasOwn(source, '$allowing-empty')) {
770
+ if (typeof source['$allowing-empty'] !== 'boolean')
771
+ return fail('JQ0003', "'$allowing-empty' takes a boolean", bindPath + '/$allowing-empty');
772
+ allowingEmpty = source['$allowing-empty'];
773
+ }
774
+ if (hasOwn(source, '$window'))
775
+ window = normalizeWindowSpec(source, bindPath);
776
+ else if (hasOwn(source, '$size') || hasOwn(source, '$step'))
777
+ return fail('JQ0003', "'$size'/'$step' require '$window'", bindPath);
605
778
  source = source.$in;
606
779
  sourcePath = bindPath + '/$in';
607
780
  }
608
781
  const expr = normalizeExpr(source, sourcePath, sc, ctx);
609
782
  const slot = ctx.nextSlot++;
610
- sc = { name, slot, card: CARD_ONE, parent: sc };
783
+ // a window variable holds the window's items, and an $allowing-empty
784
+ // binding may hold the empty sequence: neither is the plain
785
+ // exactly-one-item binding the compiler specializes for
786
+ const bindCard = window !== null ? CARD_MANY : (allowingEmpty ? CARD_OPT : CARD_ONE);
787
+ sc = { name, slot, card: bindCard, parent: sc };
611
788
  tupleSlots.push({ name, slot });
612
789
  let atSlot = -1;
613
790
  if (atName !== null) {
@@ -616,19 +793,25 @@ function normalizeForBindings(forObj, forPath, scope, ctx, phraseNames, bindings
616
793
  sc = { name: atName, slot: atSlot, card: CARD_ONE, parent: sc };
617
794
  tupleSlots.push({ name: atName, slot: atSlot });
618
795
  }
619
- bindings.push(Object.freeze({ name, slot, expr, atSlot }));
796
+ bindings.push(Object.freeze({ name, slot, expr, atSlot, allowingEmpty, window }));
620
797
  }
621
798
  return sc;
622
799
  }
623
800
 
624
801
  // One $orderby key spec (section 6.6): an expression (ascending,
625
- // empty-least) or the explicit {"$key", "$dir"?, "$empty"?} form.
802
+ // empty-least) or the explicit {"$key", "$dir"?, "$empty"?,
803
+ // "$collation"?} form. A $collation names a registered pure compare
804
+ // function (options.collations) applied to STRING keys; number keys
805
+ // keep numeric order.
626
806
  function normalizeOrderbySpec(spec, specPath, scope, ctx) {
627
807
  let key = spec;
628
808
  let keyPath = specPath;
629
809
  let desc = false;
630
810
  let emptyGreatest = false;
631
- if (isPlainObject(spec) && (hasOwn(spec, '$key') || hasOwn(spec, '$dir') || hasOwn(spec, '$empty'))) {
811
+ let collation = null;
812
+ let collationName = null;
813
+ if (isJsonObject(spec)
814
+ && (hasOwn(spec, '$key') || hasOwn(spec, '$dir') || hasOwn(spec, '$empty') || hasOwn(spec, '$collation'))) {
632
815
  const specKeys = Object.keys(spec);
633
816
  for (let i = 0; i < specKeys.length; i++) {
634
817
  if (!ORDERBY_SPEC_KEYS.has(specKeys[i]))
@@ -646,21 +829,37 @@ function normalizeOrderbySpec(spec, specPath, scope, ctx) {
646
829
  return fail('JQ0003', "'$empty' must be 'least' or 'greatest'", specPath + '/$empty');
647
830
  emptyGreatest = spec.$empty === 'greatest';
648
831
  }
832
+ if (hasOwn(spec, '$collation')) {
833
+ if (typeof spec.$collation !== 'string')
834
+ return fail('JQ0003', "'$collation' must be a registered collation name", specPath + '/$collation');
835
+ collationName = spec.$collation;
836
+ collation = ctx.collations !== null && hasOwn(ctx.collations, collationName)
837
+ ? ctx.collations[collationName]
838
+ : undefined;
839
+ if (collation === undefined)
840
+ return fail('JQ0010', `'$collation' names no registered collation '${collationName}'`, specPath + '/$collation');
841
+ ctx.usedCollations.add(collationName);
842
+ }
649
843
  key = spec.$key;
650
844
  keyPath = specPath + '/$key';
651
845
  }
652
846
  return Object.freeze({
653
847
  key: normalizeExpr(key, keyPath, scope, ctx),
654
- desc, emptyGreatest, docPath: specPath,
848
+ desc, emptyGreatest, collation, collationName, docPath: specPath,
655
849
  });
656
850
  }
657
851
 
658
- // Collect the frame slots an expression subtree reads (variable
659
- // references and path roots). Barrier liveness: $groupby/$orderby
660
- // materialize only the phrase binding slots that later clauses read.
661
- // Over-approximation is safe; slots written by nested phrases before
662
- // being read merely widen a snapshot harmlessly.
663
- function collectReadSlots(node, out) {
852
+ // Barrier liveness: $groupby/$orderby materialize only the phrase
853
+ // binding slots that later clauses read. Slots written by nested phrases
854
+ // before being read merely widen a snapshot harmlessly. The hash-join
855
+ // planner (compile.js) reuses this to prove a probe side uncorrelated.
856
+ /**
857
+ * Collect the frame slots an expression subtree reads (variable
858
+ * references and path roots) into `out`. Over-approximation is safe.
859
+ * @param {object} node - a frozen AST node
860
+ * @param {Set<number>} out - accumulator of slot indexes
861
+ */
862
+ export function collectReadSlots(node, out) {
664
863
  switch (node.kind) {
665
864
  case 'literal':
666
865
  return;
@@ -687,6 +886,7 @@ function collectReadSlots(node, out) {
687
886
  case 'raw': // compile-time data of a registry operator, never evaluated
688
887
  return;
689
888
  case 'op':
889
+ case 'call':
690
890
  for (let i = 0; i < node.args.length; i++)
691
891
  collectReadSlots(node.args[i], out);
692
892
  return;
@@ -701,6 +901,8 @@ function collectReadSlots(node, out) {
701
901
  collectReadSlots(node.satisfies, out);
702
902
  return;
703
903
  default: { // 'flwor'
904
+ if (node.fold !== null)
905
+ collectReadSlots(node.fold.expr, out);
704
906
  for (let i = 0; i < node.forBindings.length; i++)
705
907
  collectReadSlots(node.forBindings[i].expr, out);
706
908
  for (let i = 0; i < node.letBindings.length; i++)
@@ -739,8 +941,34 @@ function normalizeFlworPhrase(obj, docPath, scope, ctx) {
739
941
  const letBindings = [];
740
942
  let sc = scope;
741
943
 
944
+ // $fold (section 6.9): the accumulator clause. Its initial value is
945
+ // evaluated ONCE, in the enclosing scope, before the tuple stream
946
+ // starts; $return then yields the accumulator's next value per
947
+ // surviving tuple, and the phrase's value is the final accumulator
948
+ // instead of the collected $return sequence. This is what gives the
949
+ // language its fold without giving JSON a way to spell a function
950
+ // value: the accumulator is a binding, not a lambda parameter.
951
+ let fold = null;
952
+ if (hasOwn(obj, '$fold')) {
953
+ const foldPath = docPath + '/$fold';
954
+ const names = requireBindingObject('$fold', obj.$fold, foldPath);
955
+ if (names.length !== 1)
956
+ return fail('JQ0003', "'$fold' takes exactly one accumulator binding", foldPath);
957
+ const name = names[0];
958
+ const bindPath = foldPath + '/' + encodeJSONPointerSegment(name);
959
+ bindPhraseName(name, phraseNames, bindPath);
960
+ // the initial value cannot see this phrase's own bindings
961
+ const expr = normalizeExpr(obj.$fold[name], bindPath, scope, ctx);
962
+ fold = { name, slot: ctx.nextSlot++, expr, docPath: bindPath };
963
+ }
964
+
742
965
  if (hasOwn(obj, '$for'))
743
966
  sc = normalizeForBindings(obj.$for, docPath + '/$for', sc, ctx, phraseNames, forBindings, tupleSlots);
967
+ // the accumulator binds after $for - a $for source is iterated once and
968
+ // must not depend on a value that changes per tuple - and before $let,
969
+ // so every later clause sees the accumulation so far
970
+ if (fold !== null)
971
+ sc = { name: fold.name, slot: fold.slot, card: CARD_MANY, parent: sc };
744
972
  if (hasOwn(obj, '$let')) {
745
973
  const before = letBindings.length;
746
974
  sc = normalizeLetBindings(obj.$let, docPath + '/$let', sc, ctx, phraseNames, letBindings);
@@ -757,7 +985,7 @@ function normalizeFlworPhrase(obj, docPath, scope, ctx) {
757
985
  if (hasOwn(obj, '$as')) {
758
986
  const asPath = docPath + '/$as';
759
987
  const asObj = obj.$as;
760
- if (!isPlainObject(asObj))
988
+ if (!isJsonObject(asObj))
761
989
  return fail('JQ0003', "'$as' takes an object of variable-name to schema members", asPath);
762
990
  const names = Object.keys(asObj);
763
991
  if (names.length === 0)
@@ -765,7 +993,7 @@ function normalizeFlworPhrase(obj, docPath, scope, ctx) {
765
993
  const checks = new Array(names.length);
766
994
  for (let i = 0; i < names.length; i++) {
767
995
  const name = names[i];
768
- const checkPath = asPath + '/' + escapeToken(name);
996
+ const checkPath = asPath + '/' + encodeJSONPointerSegment(name);
769
997
  let slot = -1;
770
998
  for (let j = 0; j < tupleSlots.length; j++) {
771
999
  if (tupleSlots[j].name === name) {
@@ -799,7 +1027,7 @@ function normalizeFlworPhrase(obj, docPath, scope, ctx) {
799
1027
  const keys = new Array(names.length);
800
1028
  for (let i = 0; i < names.length; i++) {
801
1029
  const name = names[i];
802
- const bindPath = groupPath + '/' + escapeToken(name);
1030
+ const bindPath = groupPath + '/' + encodeJSONPointerSegment(name);
803
1031
  bindPhraseName(name, phraseNames, bindPath);
804
1032
  // key expressions evaluate per tuple, in the pre-group scope
805
1033
  const expr = normalizeExpr(obj.$groupby[name], bindPath, sc, ctx);
@@ -807,6 +1035,10 @@ function normalizeFlworPhrase(obj, docPath, scope, ctx) {
807
1035
  }
808
1036
  // post-group scope: same slots, rebound cards
809
1037
  sc = scope;
1038
+ // the accumulator is not a tuple variable - $groupby does not rebind
1039
+ // it, it keeps accumulating, now once per group
1040
+ if (fold !== null)
1041
+ sc = { name: fold.name, slot: fold.slot, card: CARD_MANY, parent: sc };
810
1042
  for (let i = 0; i < tupleSlots.length; i++)
811
1043
  sc = { name: tupleSlots[i].name, slot: tupleSlots[i].slot, card: CARD_MANY, parent: sc };
812
1044
  for (let i = 0; i < keys.length; i++)
@@ -888,19 +1120,25 @@ function normalizeFlworPhrase(obj, docPath, scope, ctx) {
888
1120
  // phrase cardinality: MANY unless provably otherwise - a $let-only
889
1121
  // phrase yields exactly one tuple ($where may still drop it)
890
1122
  let card;
891
- if (forBindings.length !== 0 || groupby !== null)
1123
+ if (fold !== null)
1124
+ // the phrase IS the accumulator: either it never updated (the initial
1125
+ // value) or it holds some $return result
1126
+ card = joinCard(fold.expr.card, ret.card);
1127
+ else if (forBindings.length !== 0 || groupby !== null)
892
1128
  card = CARD_MANY;
893
1129
  else
894
1130
  card = where !== null ? joinCard(ret.card, CARD_ZERO) : ret.card;
895
1131
 
896
1132
  return Object.freeze({
897
1133
  kind: 'flwor', card, docPath,
1134
+ fold: fold === null ? null : Object.freeze(fold),
898
1135
  forBindings: Object.freeze(forBindings),
899
1136
  letBindings: Object.freeze(letBindings),
900
1137
  asChecks, where,
901
1138
  groupby: groupby === null ? null : Object.freeze(groupby),
902
1139
  orderby: orderby === null ? null : Object.freeze(orderby),
903
1140
  count, ret,
1141
+ limits: ctx.limits,
904
1142
  });
905
1143
  }
906
1144
 
@@ -918,11 +1156,11 @@ function normalizeQuantifierPhrase(obj, docPath, scope, ctx) {
918
1156
  let sc = scope;
919
1157
  for (let i = 0; i < names.length; i++) {
920
1158
  const name = names[i];
921
- const bindPath = clausePath + '/' + escapeToken(name);
1159
+ const bindPath = clausePath + '/' + encodeJSONPointerSegment(name);
922
1160
  bindPhraseName(name, phraseNames, bindPath);
923
1161
  const source = bindObj[name];
924
- if (isPlainObject(source) && (hasOwn(source, '$in') || hasOwn(source, '$at')))
925
- return fail('JQ0003', "the extended '$in'/'$at' binding form is not available in quantifiers", bindPath);
1162
+ if (isExtendedBinding(source))
1163
+ return fail('JQ0003', 'the extended binding form is not available in quantifiers', bindPath);
926
1164
  const expr = normalizeExpr(source, bindPath, sc, ctx);
927
1165
  const slot = ctx.nextSlot++;
928
1166
  sc = { name, slot, card: CARD_ONE, parent: sc };
@@ -945,22 +1183,160 @@ function normalizeQuantifierPhrase(obj, docPath, scope, ctx) {
945
1183
  // vocabulary - the closed format is unchanged, extensions are host
946
1184
  // machinery. Violations are host programming errors (TypeError), not
947
1185
  // JQ0xxx document errors.
1186
+ // True when `name` is already part of the closed query vocabulary — a
1187
+ // FLWOR/quantifier/escape key, a reserved member, or a core operator.
1188
+ // A host extension (options.extensions) or a registry pack (the JSLT
1189
+ // operator registry) must not shadow any of these. Exported so the
1190
+ // registry builder can reject a colliding pack at `.use()` time with the
1191
+ // same rule the compiler enforces at normalize time.
1192
+ export function isReservedQueryName(name) {
1193
+ return FLWOR_KEYS.has(name) || QUANTIFIER_KEYS.has(name) || ESCAPE_KEYS.has(name)
1194
+ || ORDERBY_SPEC_KEYS.has(name) || name === '$in' || name === '$at'
1195
+ || name === '$query' || name === '$expr' || hasOwn(OPERATORS, name);
1196
+ }
1197
+
948
1198
  function validateExtensions(extensions) {
949
- if (!isPlainObject(extensions))
1199
+ if (!isJsonObject(extensions))
950
1200
  throw new TypeError('options.extensions must be a plain object of operator entries');
951
1201
  const names = Object.keys(extensions);
952
1202
  for (let i = 0; i < names.length; i++) {
953
1203
  const name = names[i];
954
1204
  if (name.charCodeAt(0) !== 0x24) // '$'
955
1205
  throw new TypeError(`extension operator '${name}' must start with '$'`);
956
- if (FLWOR_KEYS.has(name) || QUANTIFIER_KEYS.has(name) || ESCAPE_KEYS.has(name)
957
- || ORDERBY_SPEC_KEYS.has(name) || name === '$in' || name === '$at'
958
- || name === '$query' || name === '$expr' || hasOwn(OPERATORS, name))
1206
+ if (isReservedQueryName(name))
959
1207
  throw new TypeError(`extension operator '${name}' collides with the core vocabulary`);
960
1208
  }
961
1209
  return extensions;
962
1210
  }
963
1211
 
1212
+ // Validate a named registry of trusted pure host functions
1213
+ // (options.functions / options.collations): a plain object of
1214
+ // `name -> function`. Violations are host programming errors
1215
+ // (TypeError), like options.extensions.
1216
+ function validateNamedFunctions(value, what) {
1217
+ if (!isJsonObject(value))
1218
+ throw new TypeError(`options.${what} must be a plain object of named functions`);
1219
+ for (const name in value) {
1220
+ // the schema twins require non-empty names (minLength 1); the
1221
+ // registry enforces the same, so document and runtime agree
1222
+ if (name === '')
1223
+ throw new TypeError(`options.${what} must not register an empty-string name`);
1224
+ if (typeof value[name] !== 'function')
1225
+ throw new TypeError(`options.${what}['${name}'] must be a function`);
1226
+ }
1227
+ return value;
1228
+ }
1229
+
1230
+ // The known limits (section 8.12). Only limits the engine actually
1231
+ // enforces are accepted - an accepted-but-unenforced limit would be a
1232
+ // silent false guarantee.
1233
+ const LIMIT_NAMES = new Set(['sequenceItems', 'resultItems', 'steps', 'depth']);
1234
+
1235
+ function validateLimits(value) {
1236
+ if (!isJsonObject(value))
1237
+ throw new TypeError('options.limits must be a plain object');
1238
+ for (const name in value) {
1239
+ if (!LIMIT_NAMES.has(name))
1240
+ throw new TypeError(`options.limits.${name} is not a known limit`);
1241
+ const v = value[name];
1242
+ if (!Number.isInteger(v) || v < 1)
1243
+ throw new TypeError(`options.limits.${name} must be a positive integer`);
1244
+ }
1245
+ return Object.freeze({
1246
+ sequenceItems: value.sequenceItems ?? null,
1247
+ resultItems: value.resultItems ?? null,
1248
+ steps: value.steps ?? null,
1249
+ depth: value.depth ?? null,
1250
+ });
1251
+ }
1252
+
1253
+ // Validate `options.externals`: the closed-world declaration list. An
1254
+ // array of variable names (without the '$' sigil); `[]` declares none,
1255
+ // so every free variable is JQ0005. Absent means the open world, where
1256
+ // use is the declaration (section 9).
1257
+ function validateDeclaredExternals(value) {
1258
+ if (!Array.isArray(value))
1259
+ throw new TypeError('options.externals must be an array of variable names');
1260
+ const set = new Set();
1261
+ for (let i = 0; i < value.length; i++) {
1262
+ const name = value[i];
1263
+ if (typeof name !== 'string' || !VAR_NAME_RE.test(name))
1264
+ throw new TypeError(`options.externals[${i}] must be a variable name string (no '$' sigil)`);
1265
+ set.add(name);
1266
+ }
1267
+ return set;
1268
+ }
1269
+
1270
+ // Maximum expression nesting of a normalized AST. The language has no
1271
+ // recursion - no user-defined functions, no self-reference - so the
1272
+ // compiled closure tree's evaluation depth is exactly this static depth,
1273
+ // which is why `limits.depth` is a compile-time check (JQ0011) rather
1274
+ // than a runtime counter: exact, and free at evaluation time. Returns
1275
+ // the deepest node found alongside its depth, for the error position.
1276
+ function measureDepth(node, depth, worst) {
1277
+ if (depth > worst.depth) {
1278
+ worst.depth = depth;
1279
+ worst.docPath = node.docPath;
1280
+ }
1281
+ const next = depth + 1;
1282
+ switch (node.kind) {
1283
+ case 'literal':
1284
+ case 'var':
1285
+ case 'path':
1286
+ case 'raw':
1287
+ return;
1288
+ case 'object':
1289
+ for (let i = 0; i < node.entries.length; i++)
1290
+ measureDepth(node.entries[i].expr, next, worst);
1291
+ return;
1292
+ case 'map':
1293
+ for (let i = 0; i < node.pairs.length; i++) {
1294
+ measureDepth(node.pairs[i].key, next, worst);
1295
+ measureDepth(node.pairs[i].value, next, worst);
1296
+ }
1297
+ return;
1298
+ case 'array':
1299
+ for (let i = 0; i < node.elements.length; i++)
1300
+ measureDepth(node.elements[i], next, worst);
1301
+ return;
1302
+ case 'op':
1303
+ case 'call':
1304
+ for (let i = 0; i < node.args.length; i++)
1305
+ measureDepth(node.args[i], next, worst);
1306
+ return;
1307
+ case 'let':
1308
+ for (let i = 0; i < node.bindings.length; i++)
1309
+ measureDepth(node.bindings[i].expr, next, worst);
1310
+ measureDepth(node.ret, next, worst);
1311
+ return;
1312
+ case 'quant':
1313
+ for (let i = 0; i < node.bindings.length; i++)
1314
+ measureDepth(node.bindings[i].expr, next, worst);
1315
+ measureDepth(node.satisfies, next, worst);
1316
+ return;
1317
+ default: { // 'flwor'
1318
+ for (let i = 0; i < node.forBindings.length; i++)
1319
+ measureDepth(node.forBindings[i].expr, next, worst);
1320
+ for (let i = 0; i < node.letBindings.length; i++)
1321
+ measureDepth(node.letBindings[i].expr, next, worst);
1322
+ if (node.fold !== null)
1323
+ measureDepth(node.fold.expr, next, worst);
1324
+ if (node.where !== null)
1325
+ measureDepth(node.where, next, worst);
1326
+ if (node.groupby !== null) {
1327
+ for (let i = 0; i < node.groupby.keys.length; i++)
1328
+ measureDepth(node.groupby.keys[i].expr, next, worst);
1329
+ }
1330
+ if (node.orderby !== null) {
1331
+ for (let i = 0; i < node.orderby.specs.length; i++)
1332
+ measureDepth(node.orderby.specs[i].key, next, worst);
1333
+ }
1334
+ measureDepth(node.ret, next, worst);
1335
+ return;
1336
+ }
1337
+ }
1338
+ }
1339
+
964
1340
  function normalizeExpr(value, docPath, scope, ctx) {
965
1341
  switch (typeof value) {
966
1342
  case 'string':
@@ -1014,11 +1390,31 @@ export function normalizeQuery(doc, options = {}) {
1014
1390
  ? options.compileTypeTest
1015
1391
  : null;
1016
1392
  const extensions = options.extensions == null ? null : validateExtensions(options.extensions);
1017
- const ctx = { nextSlot: 1, externals: new Map(), compileTypeTest, extensions };
1393
+ const functions = options.functions == null ? null : validateNamedFunctions(options.functions, 'functions');
1394
+ const collations = options.collations == null ? null : validateNamedFunctions(options.collations, 'collations');
1395
+ const limits = options.limits == null ? null : validateLimits(options.limits);
1396
+ const declaredExternals = options.externals == null
1397
+ ? null
1398
+ : validateDeclaredExternals(options.externals);
1399
+ // JSONPath function extensions are a separate registry from
1400
+ // options.functions ($call's host functions): they extend the RFC 9535
1401
+ // grammar inside path strings, not the query vocabulary. Built once
1402
+ // per compile so parsePathString hands the parser the same object.
1403
+ const pathOptions = options.pathFunctions == null
1404
+ ? undefined
1405
+ : { pathFunctions: options.pathFunctions };
1406
+ const ctx = {
1407
+ nextSlot: 1, externals: new Map(), compileTypeTest, extensions,
1408
+ functions, collations, limits, pathOptions, declaredExternals,
1409
+ // package-internal: set only by analyzeQuery (Appendix C.1); the
1410
+ // compile entry point never passes it
1411
+ analysis: options.analysis === true,
1412
+ usedOps: new Set(), usedFunctions: new Set(), usedCollations: new Set(),
1413
+ };
1018
1414
  let expr = doc;
1019
1415
  let rootPath = '';
1020
1416
  // the version envelope is only recognized at the top level (section 4)
1021
- if (isPlainObject(doc) && (hasOwn(doc, '$query') || hasOwn(doc, '$expr'))) {
1417
+ if (isJsonObject(doc) && (hasOwn(doc, '$query') || hasOwn(doc, '$expr'))) {
1022
1418
  const keys = Object.keys(doc);
1023
1419
  let allDollar = true;
1024
1420
  for (let i = 0; i < keys.length; i++) {
@@ -1035,11 +1431,26 @@ export function normalizeQuery(doc, options = {}) {
1035
1431
  }
1036
1432
  }
1037
1433
  const root = normalizeExpr(expr, rootPath, null, ctx);
1434
+ if (limits !== null && limits.depth !== null) {
1435
+ const worst = { depth: 0, docPath: rootPath };
1436
+ measureDepth(root, 1, worst);
1437
+ if (worst.depth > limits.depth)
1438
+ fail('JQ0011', `the query nests ${worst.depth} expressions deep, more than limits.depth (${limits.depth})`, worst.docPath);
1439
+ }
1440
+ // limits.steps instruments every node evaluation; the counter lives in
1441
+ // its own frame slot, so nothing is threaded through the closures
1442
+ const stepSlot = limits !== null && limits.steps !== null ? ctx.nextSlot++ : -1;
1038
1443
  const externals = new Array(ctx.externals.size);
1039
1444
  let i = 0;
1040
1445
  for (const [name, slot] of ctx.externals)
1041
1446
  externals[i++] = Object.freeze({ name, slot });
1042
- return { root, frameSize: ctx.nextSlot, externals: Object.freeze(externals) };
1447
+ return {
1448
+ root, frameSize: ctx.nextSlot, externals: Object.freeze(externals),
1449
+ limits, stepSlot,
1450
+ usedOps: ctx.usedOps,
1451
+ usedFunctions: ctx.usedFunctions,
1452
+ usedCollations: ctx.usedCollations,
1453
+ };
1043
1454
  }
1044
1455
 
1045
1456
  //#endregion