@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,1047 @@
1
+ //#region Jaren JSON Query normalizer
2
+ // Single recursive pass over a query document (QUERY-FORMAT.md sections
3
+ // 3-9) producing the internal AST: plain frozen `{kind, card, docPath, ...}`
4
+ // nodes. All classification decisions - object partitioning, the closed
5
+ // phrase vocabulary, string forms, scope resolution - happen here, so the
6
+ // compiler (compile.js) only ever sees well-formed nodes.
7
+ //
8
+ // Static cardinality analysis: every node carries `card`, an upper
9
+ // approximation of its runtime sequence length (CARD_ONE means "always
10
+ // exactly one item"). compile.js uses it to emit singleton-mode closures
11
+ // that skip all sequence checks - the query-engine analogue of path.js's
12
+ // singular-query fast path.
13
+ //
14
+ // Scoping: lexical environments map variable names to frame slot indices.
15
+ // Each compiled query evaluates against one frame array; slot 0 is the
16
+ // input document, every binding site and every external parameter gets its
17
+ // own slot from a single allocator (nested FLWOR phrases simply keep
18
+ // allocating in the same frame). Free names become external parameters,
19
+ // collected in order of first appearance.
20
+
21
+ import { parseJSONPath, JSONPathSyntaxError } from '../path.js';
22
+ import { isSingularSegments } from '../segments.js';
23
+ import { JsonQueryCompileError } from './errors.js';
24
+ // The operator registry: `name -> { params, result, compile }`. Only
25
+ // referenced inside functions (never at module evaluation time), so the
26
+ // import cycle normalize.js <-> operators.js is initialization-safe.
27
+ import { OPERATORS } from './operators.js';
28
+
29
+ //#region cardinality
30
+
31
+ /** Statically empty (the node always evaluates to the empty sequence). */
32
+ export const CARD_ZERO = 0;
33
+ /** Always exactly one item; compile.js skips all sequence checks. */
34
+ export const CARD_ONE = 1;
35
+ /** Zero or one item. */
36
+ export const CARD_OPT = 2;
37
+ /** Any number of items (the analysis top). */
38
+ export const CARD_MANY = 3;
39
+
40
+ /**
41
+ * join = least upper bound over {ZERO, ONE, OPT, MANY}: the cardinality
42
+ * of "one of the two branches" ($if).
43
+ * @param {number} a - a CARD_* value
44
+ * @param {number} b - a CARD_* value
45
+ * @returns {number}
46
+ */
47
+ export function joinCard(a, b) {
48
+ if (a === b)
49
+ return a;
50
+ if (a === CARD_MANY || b === CARD_MANY)
51
+ return CARD_MANY;
52
+ return CARD_OPT;
53
+ }
54
+
55
+ /**
56
+ * sum = cardinality of two concatenated sequences ($seq); two non-empty
57
+ * contributions can exceed one item, which only MANY can express.
58
+ * @param {number} a - a CARD_* value
59
+ * @param {number} b - a CARD_* value
60
+ * @returns {number}
61
+ */
62
+ export function sumCard(a, b) {
63
+ if (a === CARD_ZERO)
64
+ return b;
65
+ if (b === CARD_ZERO)
66
+ return a;
67
+ return CARD_MANY;
68
+ }
69
+
70
+ //#endregion
71
+
72
+ //#region vocabulary tables
73
+
74
+ const hasOwn = Object.hasOwn;
75
+
76
+ const VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
77
+ const VAR_HEAD_RE = /^\$([A-Za-z_][A-Za-z0-9_]*)/;
78
+
79
+ // FLWOR clause keys (QUERY-FORMAT.md section 6.1) and quantifier keys
80
+ // (section 7). Clauses apply in the fixed semantic order of section 6.1
81
+ // regardless of JSON key order (D7).
82
+ const FLWOR_KEYS = new Set(['$for', '$let', '$as', '$where', '$groupby', '$orderby', '$count', '$return']);
83
+ const QUANTIFIER_KEYS = new Set(['$some', '$every', '$satisfies']);
84
+
85
+ // Keys of the explicit $orderby key-spec form (section 6.6). Contextual:
86
+ // they are not operators and stay outside the KNOWN_KEYS vocabulary.
87
+ const ORDERBY_SPEC_KEYS = new Set(['$key', '$dir', '$empty']);
88
+
89
+ // Escape hatches (QUERY-FORMAT.md section 3.5): structural forms with
90
+ // dedicated normalizer cases; every other operator lives in the registry.
91
+ const ESCAPE_KEYS = new Set(['$const', '$map']);
92
+
93
+ // The complete closed vocabulary decides JQ0002 (unknown key) versus
94
+ // JQ0003 (known keys in an invalid combination) for phrase objects.
95
+ // A function, not a precomputed set: the registry must never be read at
96
+ // module evaluation time (import cycle with operators.js). Host extension
97
+ // operators (ctx.extensions, see validateExtensions) count as vocabulary
98
+ // for the compile they were passed to.
99
+ function isVocabularyKey(key, ctx) {
100
+ return FLWOR_KEYS.has(key) || QUANTIFIER_KEYS.has(key)
101
+ || ESCAPE_KEYS.has(key) || hasOwn(OPERATORS, key)
102
+ || (ctx.extensions !== null && hasOwn(ctx.extensions, key));
103
+ }
104
+
105
+ //#endregion
106
+
107
+ //#region helpers
108
+
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');
118
+ }
119
+
120
+ function fail(code, message, docPath) {
121
+ throw new JsonQueryCompileError(code, message, docPath);
122
+ }
123
+
124
+ /**
125
+ * Deep-copy a JSON value and freeze every object/array in the copy.
126
+ * Used for `$const` values and the compiled query's `.doc` property, so
127
+ * the engine never freezes (or shares mutable state with) caller objects.
128
+ * @param {any} value - a JSON value
129
+ * @returns {any} an independent, deeply frozen copy
130
+ */
131
+ export function deepFreezeCopy(value) {
132
+ if (typeof value !== 'object' || value === null)
133
+ return value;
134
+ if (Array.isArray(value)) {
135
+ const out = new Array(value.length);
136
+ for (let i = 0; i < value.length; i++)
137
+ out[i] = deepFreezeCopy(value[i]);
138
+ return Object.freeze(out);
139
+ }
140
+ const out = {};
141
+ const keys = Object.keys(value);
142
+ for (let i = 0; i < keys.length; i++)
143
+ out[keys[i]] = deepFreezeCopy(value[keys[i]]);
144
+ return Object.freeze(out);
145
+ }
146
+
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
+ //#endregion
157
+
158
+ //#region strings (Rule 2)
159
+
160
+ function parsePathString(source, original, docPath) {
161
+ try {
162
+ return parseJSONPath(source);
163
+ }
164
+ catch (e) {
165
+ /* c8 ignore next 2 -- parseJSONPath only throws syntax errors */
166
+ if (!(e instanceof JSONPathSyntaxError))
167
+ throw e;
168
+ return fail('JQ0004', `'${original}' is not a valid path: ${e.message}`, docPath);
169
+ }
170
+ }
171
+
172
+ function makeLiteral(value, docPath) {
173
+ return Object.freeze({ kind: 'literal', card: CARD_ONE, docPath, value });
174
+ }
175
+
176
+ function makePathNode(name, rootSlot, external, rootCard, segments, docPath) {
177
+ const singular = isSingularSegments(segments);
178
+ const card = singular
179
+ ? (rootCard === CARD_MANY ? CARD_MANY : CARD_OPT)
180
+ : CARD_MANY;
181
+ return Object.freeze({
182
+ kind: 'path', card, docPath,
183
+ name, rootSlot, external, rootCard, segments: deepFreeze(segments), singular,
184
+ });
185
+ }
186
+
187
+ // resolve a variable reference: walk the lexical scope chain; a free name
188
+ // is an external parameter, allocated a slot on first appearance (spec
189
+ // section 9: use is the declaration)
190
+ function resolveVariable(name, scope, ctx) {
191
+ for (let sc = scope; sc !== null; sc = sc.parent) {
192
+ if (sc.name === name)
193
+ return { slot: sc.slot, card: sc.card, external: false };
194
+ }
195
+ let slot = ctx.externals.get(name);
196
+ if (slot === undefined) {
197
+ slot = ctx.nextSlot++;
198
+ ctx.externals.set(name, slot);
199
+ }
200
+ // an external is bound by the caller to one JSON value: exactly one item
201
+ return { slot, card: CARD_ONE, external: true };
202
+ }
203
+
204
+ function normalizeString(s, docPath, scope, ctx) {
205
+ if (s.charCodeAt(0) !== 0x24) // '$'
206
+ return makeLiteral(s, docPath);
207
+ if (s.length === 1) // '$' alone: the input document, frame slot 0
208
+ return Object.freeze({ kind: 'var', card: CARD_ONE, docPath, slot: 0, external: false, name: '$' });
209
+ const c1 = s.charCodeAt(1);
210
+ if (c1 === 0x24) // '$$' escape: drop exactly one leading '$'
211
+ return makeLiteral(s.slice(1), docPath);
212
+ if (c1 === 0x2E || c1 === 0x5B) { // '$.' | '$[' | '$..' - absolute path
213
+ const ast = parsePathString(s, s, docPath);
214
+ return makePathNode('$', 0, false, CARD_ONE, ast.segments, docPath);
215
+ }
216
+ const m = VAR_HEAD_RE.exec(s);
217
+ if (m === null)
218
+ return fail('JQ0004', `'${s}' is not a valid path or escape`, docPath);
219
+ const name = m[1];
220
+ const rest = s.slice(m[0].length);
221
+ const ref = resolveVariable(name, scope, ctx);
222
+ if (rest === '') // bare '$name': whole-variable reference
223
+ return Object.freeze({ kind: 'var', card: ref.card, docPath, slot: ref.slot, external: ref.external, name });
224
+ // variable-rooted path: the grammar is RFC 9535 with the root identifier
225
+ // replaced by the variable reference - parse with a substituted '$'
226
+ const ast = parsePathString('$' + rest, s, docPath);
227
+ return makePathNode(name, ref.slot, ref.external, ref.card, ast.segments, docPath);
228
+ }
229
+
230
+ //#endregion
231
+
232
+ //#region objects (Rule 1)
233
+
234
+ function normalizeObject(obj, docPath, scope, ctx) {
235
+ const keys = Object.keys(obj);
236
+ let dollarCount = 0;
237
+ for (let i = 0; i < keys.length; i++) {
238
+ if (keys[i].charCodeAt(0) === 0x24)
239
+ dollarCount++;
240
+ }
241
+ if (dollarCount === 0) { // map constructor ({} constructs the empty object)
242
+ const entries = new Array(keys.length);
243
+ for (let i = 0; i < keys.length; i++) {
244
+ const name = keys[i];
245
+ entries[i] = Object.freeze({
246
+ name,
247
+ expr: normalizeExpr(obj[name], docPath + '/' + escapeToken(name), scope, ctx),
248
+ });
249
+ }
250
+ return Object.freeze({ kind: 'object', card: CARD_ONE, docPath, entries: Object.freeze(entries) });
251
+ }
252
+ if (dollarCount !== keys.length)
253
+ return fail('JQ0001', 'an object cannot mix $-prefixed and plain keys', docPath);
254
+ return normalizePhrase(obj, keys, docPath, scope, ctx);
255
+ }
256
+
257
+ function normalizePhrase(obj, keys, docPath, scope, ctx) {
258
+ // FLWOR phrase shape: only FLWOR clause keys, $return plus $for and/or
259
+ // $let present (section 6.1). The degenerate {$let, $return} phrase
260
+ // keeps the direct 'let' node - it needs no tuple stream.
261
+ let allFlwor = true;
262
+ for (let i = 0; i < keys.length; i++) {
263
+ if (!FLWOR_KEYS.has(keys[i])) {
264
+ allFlwor = false;
265
+ break;
266
+ }
267
+ }
268
+ if (allFlwor && keys.length >= 2 && hasOwn(obj, '$return') && (hasOwn(obj, '$for') || hasOwn(obj, '$let'))) {
269
+ if (keys.length === 2 && hasOwn(obj, '$let'))
270
+ return normalizeLetPhrase(obj, docPath, scope, ctx);
271
+ return normalizeFlworPhrase(obj, docPath, scope, ctx);
272
+ }
273
+ // quantifier phrase shape (section 7): {$some|$every, $satisfies}
274
+ if (keys.length === 2 && hasOwn(obj, '$satisfies')
275
+ && (hasOwn(obj, '$some') || hasOwn(obj, '$every')))
276
+ return normalizeQuantifierPhrase(obj, docPath, scope, ctx);
277
+
278
+ if (keys.length === 1)
279
+ return normalizeOperator(keys[0], obj[keys[0]], docPath, scope, ctx);
280
+
281
+ // multi-key object matching no phrase shape
282
+ for (let i = 0; i < keys.length; i++) {
283
+ if (!isVocabularyKey(keys[i], ctx))
284
+ return failUnknownOperator(keys[i], docPath, ctx);
285
+ }
286
+ return fail('JQ0003', `invalid phrase key combination (${keys.join(', ')})`, docPath);
287
+ }
288
+
289
+ //#endregion
290
+
291
+ //#region unknown operators ("did you mean")
292
+
293
+ // Bounded Levenshtein distance (two-row DP); vocabulary keys are short,
294
+ // and this only ever runs on the JQ0002 error path.
295
+ function levenshtein(a, b) {
296
+ const alen = a.length;
297
+ const blen = b.length;
298
+ let prev = new Array(blen + 1);
299
+ let curr = new Array(blen + 1);
300
+ for (let j = 0; j <= blen; j++)
301
+ prev[j] = j;
302
+ for (let i = 1; i <= alen; i++) {
303
+ curr[0] = i;
304
+ const ca = a.charCodeAt(i - 1);
305
+ for (let j = 1; j <= blen; j++) {
306
+ const del = prev[j] + 1;
307
+ const ins = curr[j - 1] + 1;
308
+ const sub = prev[j - 1] + (ca === b.charCodeAt(j - 1) ? 0 : 1);
309
+ curr[j] = del < ins ? (del < sub ? del : sub) : (ins < sub ? ins : sub);
310
+ }
311
+ const t = prev;
312
+ prev = curr;
313
+ curr = t;
314
+ }
315
+ return prev[blen];
316
+ }
317
+
318
+ // suggestion candidates, built lazily (see the isVocabularyKey note)
319
+ let VOCABULARY_NAMES = null;
320
+
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.
325
+ function failUnknownOperator(key, docPath, ctx) {
326
+ if (VOCABULARY_NAMES === null) {
327
+ VOCABULARY_NAMES = [
328
+ ...FLWOR_KEYS, ...QUANTIFIER_KEYS, ...ESCAPE_KEYS, ...Object.keys(OPERATORS),
329
+ ];
330
+ }
331
+ const candidates = ctx.extensions === null
332
+ ? VOCABULARY_NAMES
333
+ : [...VOCABULARY_NAMES, ...Object.keys(ctx.extensions)];
334
+ let best = null;
335
+ let bestDist = 3;
336
+ for (let i = 0; i < candidates.length; i++) {
337
+ const name = candidates[i];
338
+ const lenDiff = key.length - name.length;
339
+ if (lenDiff > 2 || lenDiff < -2)
340
+ continue;
341
+ const d = levenshtein(key, name);
342
+ if (d < bestDist) {
343
+ bestDist = d;
344
+ best = name;
345
+ }
346
+ }
347
+ const hint = best === null ? '' : ` (did you mean '${best}'?)`;
348
+ return fail('JQ0002', `unknown operator '${key}'${hint}`, docPath);
349
+ }
350
+
351
+ //#endregion
352
+
353
+ //#region operators
354
+
355
+ function requireExprArray(op, arg, min, max, docPath) {
356
+ if (!Array.isArray(arg))
357
+ return fail('JQ0003', `'${op}' takes an array of expressions`, docPath);
358
+ if (arg.length < min || arg.length > max) {
359
+ const arity = min === max ? `exactly ${min}` : (max === Infinity ? `at least ${min}` : `${min} to ${max}`);
360
+ return fail('JQ0003', `'${op}' takes ${arity} operand(s), got ${arg.length}`, docPath);
361
+ }
362
+ return arg;
363
+ }
364
+
365
+ function normalizeElements(arg, docPath, scope, ctx) {
366
+ const out = new Array(arg.length);
367
+ for (let i = 0; i < arg.length; i++)
368
+ out[i] = normalizeExpr(arg[i], docPath + '/' + i, scope, ctx);
369
+ return Object.freeze(out);
370
+ }
371
+
372
+ // A JSON Schema literal (the raw schema argument of $valid/$assert and
373
+ // the $as clause, QUERY-FORMAT.md section 8.11): taken verbatim - never
374
+ // normalized as an expression, since JSON Schema keywords are $-prefixed
375
+ // ($ref, $defs) and must not collide with Rule 1 - deep-copied + frozen
376
+ // like $const, and compiled once, at query compile time, into a hot-path
377
+ // boolean predicate by the host-installed `options.compileTypeTest` hook.
378
+ // No hook installed is JQ0008; a hook rejection (invalid schema) is
379
+ // JQ0009, both at the owning operator's/clause's docPath.
380
+ function compileSchemaLiteral(value, schemaPath, opPath, ctx) {
381
+ if (ctx.compileTypeTest === null)
382
+ fail('JQ0008', 'schema operators require a type-test compiler (options.compileTypeTest)', opPath);
383
+ const schema = deepFreezeCopy(value);
384
+ let test;
385
+ try {
386
+ test = ctx.compileTypeTest(schema, schemaPath);
387
+ }
388
+ catch (e) {
389
+ fail('JQ0009', `invalid schema literal: ${e.message}`, opPath);
390
+ }
391
+ if (typeof test !== 'function')
392
+ fail('JQ0009', 'the type-test compiler did not return a predicate function', opPath);
393
+ return { schema, test };
394
+ }
395
+
396
+ // The inert raw node: a verbatim JSON value captured as compile-time
397
+ // data - deep-copied and frozen, walked by nothing, never compiled
398
+ // (compileOp hands 'raw' positions a null getter). Also exposed to host
399
+ // extension operators through the `normalize` override helpers.
400
+ function makeRaw(value, docPath) {
401
+ return Object.freeze({ kind: 'raw', card: CARD_ONE, docPath, value: deepFreezeCopy(value) });
402
+ }
403
+
404
+ // One argument position of a registry operator, per its declared kind:
405
+ // 'expr' normalizes an ordinary expression; 'raw' captures the value
406
+ // verbatim, unevaluated; 'schema' is 'raw' plus a compiled type-test
407
+ // predicate (the type-system work order's schema arguments); 'name'
408
+ // captures a validated variable name string. 'raw', 'schema' and 'name'
409
+ // produce inert `raw` nodes - compile-time data, never compiled.
410
+ function normalizeArg(kind, value, argPath, scope, ctx, opPath) {
411
+ if (kind === 'expr')
412
+ return normalizeExpr(value, argPath, scope, ctx);
413
+ if (kind === 'raw')
414
+ return makeRaw(value, argPath);
415
+ if (kind === 'schema') {
416
+ const { schema, test } = compileSchemaLiteral(value, argPath, opPath, ctx);
417
+ return Object.freeze({ kind: 'raw', card: CARD_ONE, docPath: argPath, value: schema, test });
418
+ }
419
+ // 'name'
420
+ if (typeof value !== 'string' || !VAR_NAME_RE.test(value))
421
+ fail('JQ0003', 'expected a variable name string', argPath);
422
+ return Object.freeze({ kind: 'raw', card: CARD_ONE, docPath: argPath, value });
423
+ }
424
+
425
+ // Argument list of an operator call, uniformly from its `params`
426
+ // descriptor (arity and shape violations are JQ0003).
427
+ function normalizeParams(key, params, arg, opPath, scope, ctx) {
428
+ if (typeof params === 'string') // the single form: the value IS the argument
429
+ return [normalizeArg(params, arg, opPath, scope, ctx, opPath)];
430
+ const kinds = params.kinds;
431
+ const max = params.variadic === true ? Infinity : kinds.length;
432
+ const list = requireExprArray(key, arg, params.min, max, opPath);
433
+ const args = new Array(list.length);
434
+ for (let i = 0; i < list.length; i++) {
435
+ const kind = kinds[i < kinds.length ? i : kinds.length - 1];
436
+ args[i] = normalizeArg(kind, list[i], opPath + '/' + i, scope, ctx, opPath);
437
+ }
438
+ return args;
439
+ }
440
+
441
+ function argCards(args) {
442
+ const cards = new Array(args.length);
443
+ for (let i = 0; i < args.length; i++)
444
+ cards[i] = args[i].card;
445
+ return cards;
446
+ }
447
+
448
+ // A registry operator call: arity and shape come uniformly from the
449
+ // table's `params` descriptor (JQ0003), the static cardinality from its
450
+ // `result` - individual operators never re-check structure.
451
+ function normalizeOperatorCall(key, entry, arg, docPath, opPath, scope, ctx) {
452
+ const args = normalizeParams(key, entry.params, arg, opPath, scope, ctx);
453
+ return Object.freeze({
454
+ kind: 'op', card: entry.result(argCards(args)), docPath, name: key, args: Object.freeze(args),
455
+ });
456
+ }
457
+
458
+ // Helpers handed to an extension entry's `normalize` override; see
459
+ // normalizeExtensionCall. Function declarations hoist, so freezing at
460
+ // module evaluation time is safe.
461
+ const EXTENSION_HELPERS = Object.freeze({ normalizeExpr, fail, makeRaw });
462
+
463
+ // A host extension operator call (options.extensions, package-internal):
464
+ // the registry contract plus an optional `normalize(arg, docPath, opPath,
465
+ // scope, ctx, helpers) -> { args, card? }` override for operators whose
466
+ // value shape the uniform `params` descriptor cannot express. `helpers`
467
+ // is `{ normalizeExpr, fail, makeRaw }`. The op node is built uniformly
468
+ // from the returned args - `card = card ?? entry.result(argCards)` - and
469
+ // carries the resolved entry so compileOp can dispatch without the table.
470
+ function normalizeExtensionCall(key, entry, arg, docPath, opPath, scope, ctx) {
471
+ let args;
472
+ let card = null;
473
+ if (typeof entry.normalize === 'function') {
474
+ const out = entry.normalize(arg, docPath, opPath, scope, ctx, EXTENSION_HELPERS);
475
+ args = Object.freeze(out.args.slice());
476
+ card = out.card ?? null;
477
+ }
478
+ else {
479
+ args = Object.freeze(normalizeParams(key, entry.params, arg, opPath, scope, ctx));
480
+ }
481
+ return Object.freeze({
482
+ kind: 'op', card: card ?? entry.result(argCards(args)), docPath, name: key, args, entry,
483
+ });
484
+ }
485
+
486
+ function normalizeOperator(key, arg, docPath, scope, ctx) {
487
+ const opPath = docPath + '/' + key;
488
+ switch (key) {
489
+ case '$const': // quote: verbatim single item, nothing inside evaluated
490
+ return Object.freeze({ kind: 'literal', card: CARD_ONE, docPath, value: deepFreezeCopy(arg) });
491
+
492
+ case '$map': { // general map constructor (section 3.5.2)
493
+ const list = requireExprArray(key, arg, 0, Infinity, opPath);
494
+ const pairs = new Array(list.length);
495
+ for (let i = 0; i < list.length; i++) {
496
+ const entry = list[i];
497
+ const entryPath = opPath + '/' + i;
498
+ if (!Array.isArray(entry) || entry.length !== 2)
499
+ return fail('JQ0003', 'a $map entry must be an array of exactly two expressions', entryPath);
500
+ pairs[i] = Object.freeze({
501
+ key: normalizeExpr(entry[0], entryPath + '/0', scope, ctx),
502
+ value: normalizeExpr(entry[1], entryPath + '/1', scope, ctx),
503
+ });
504
+ }
505
+ return Object.freeze({ kind: 'map', card: CARD_ONE, docPath, pairs: Object.freeze(pairs) });
506
+ }
507
+
508
+ default: {
509
+ // the registry vocabulary (section 8); note the section 6.7
510
+ // collision rule holds by construction: a single-key {"$count": e}
511
+ // object always reaches this lookup and is the operator
512
+ if (hasOwn(OPERATORS, key))
513
+ return normalizeOperatorCall(key, OPERATORS[key], arg, docPath, opPath, scope, ctx);
514
+ // host extension operators: after the core registry, before JQ0002
515
+ if (ctx.extensions !== null && hasOwn(ctx.extensions, key))
516
+ return normalizeExtensionCall(key, ctx.extensions[key], arg, docPath, opPath, scope, ctx);
517
+ if (FLWOR_KEYS.has(key) || QUANTIFIER_KEYS.has(key))
518
+ return fail('JQ0003', `'${key}' cannot form a phrase on its own`, docPath);
519
+ return failUnknownOperator(key, docPath, ctx);
520
+ }
521
+ }
522
+ }
523
+
524
+ //#endregion
525
+
526
+ //#region FLWOR and quantifier phrases
527
+
528
+ // One JQ0007 duplicate set spans all of a phrase's binding sites: $for
529
+ // names, $at names, $let names, $groupby key names, and the $count name
530
+ // (section 6.3). Rebinding a name from an enclosing phrase is ordinary
531
+ // shadowing and never hits this check.
532
+ function bindPhraseName(name, phraseNames, bindPath) {
533
+ if (!VAR_NAME_RE.test(name))
534
+ fail('JQ0003', `'${name}' is not a valid variable name`, bindPath);
535
+ if (phraseNames.has(name))
536
+ fail('JQ0007', `duplicate binding of variable '${name}' within one phrase`, bindPath);
537
+ phraseNames.add(name);
538
+ }
539
+
540
+ function requireBindingObject(clause, bindObj, clausePath) {
541
+ if (!isPlainObject(bindObj))
542
+ fail('JQ0003', `'${clause}' takes an object of variable bindings`, clausePath);
543
+ const names = Object.keys(bindObj);
544
+ if (names.length === 0)
545
+ fail('JQ0003', `'${clause}' requires at least one binding`, clausePath);
546
+ return names;
547
+ }
548
+
549
+ // $let bindings (section 6.3): each name binds the full sequence of its
550
+ // expression - no iteration, no array unpacking. Bindings evaluate
551
+ // sequentially in document key order; later sources see earlier names of
552
+ // the same object (correlation). Shared by the degenerate {$let, $return}
553
+ // phrase and the full FLWOR normalizer; returns the extended scope.
554
+ function normalizeLetBindings(letObj, letPath, scope, ctx, phraseNames, bindings) {
555
+ const names = requireBindingObject('$let', letObj, letPath);
556
+ let sc = scope;
557
+ for (let i = 0; i < names.length; i++) {
558
+ const name = names[i];
559
+ const bindPath = letPath + '/' + escapeToken(name);
560
+ bindPhraseName(name, phraseNames, bindPath);
561
+ 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);
564
+ const expr = normalizeExpr(source, bindPath, sc, ctx);
565
+ const slot = ctx.nextSlot++;
566
+ sc = { name, slot, card: expr.card, parent: sc };
567
+ bindings.push(Object.freeze({ name, slot, expr }));
568
+ }
569
+ return sc;
570
+ }
571
+
572
+ // The degenerate FLWOR phrase {$let, $return}: a straight-line binding
573
+ // chain, no tuple stream (the compiler keeps its direct fast path).
574
+ function normalizeLetPhrase(obj, docPath, scope, ctx) {
575
+ const bindings = [];
576
+ const sc = normalizeLetBindings(obj.$let, docPath + '/$let', scope, ctx, new Set(), bindings);
577
+ const ret = normalizeExpr(obj.$return, docPath + '/$return', sc, ctx);
578
+ return Object.freeze({
579
+ kind: 'let', card: ret.card, docPath,
580
+ bindings: Object.freeze(bindings), ret,
581
+ });
582
+ }
583
+
584
+ // $for bindings (section 6.2): each name iterates its source, one item
585
+ // per tuple (card ONE), with D4 array unpacking at runtime. The extended
586
+ // {"$in": expr, "$at": name} form additionally binds a 0-based position
587
+ // (D6). Multiple bindings nest left-to-right in document key order and
588
+ // may be correlated. Returns the extended scope.
589
+ function normalizeForBindings(forObj, forPath, scope, ctx, phraseNames, bindings, tupleSlots) {
590
+ const names = requireBindingObject('$for', forObj, forPath);
591
+ let sc = scope;
592
+ for (let i = 0; i < names.length; i++) {
593
+ const name = names[i];
594
+ const bindPath = forPath + '/' + escapeToken(name);
595
+ bindPhraseName(name, phraseNames, bindPath);
596
+ let source = forObj[name];
597
+ let sourcePath = bindPath;
598
+ 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');
605
+ source = source.$in;
606
+ sourcePath = bindPath + '/$in';
607
+ }
608
+ const expr = normalizeExpr(source, sourcePath, sc, ctx);
609
+ const slot = ctx.nextSlot++;
610
+ sc = { name, slot, card: CARD_ONE, parent: sc };
611
+ tupleSlots.push({ name, slot });
612
+ let atSlot = -1;
613
+ if (atName !== null) {
614
+ bindPhraseName(atName, phraseNames, bindPath + '/$at');
615
+ atSlot = ctx.nextSlot++;
616
+ sc = { name: atName, slot: atSlot, card: CARD_ONE, parent: sc };
617
+ tupleSlots.push({ name: atName, slot: atSlot });
618
+ }
619
+ bindings.push(Object.freeze({ name, slot, expr, atSlot }));
620
+ }
621
+ return sc;
622
+ }
623
+
624
+ // One $orderby key spec (section 6.6): an expression (ascending,
625
+ // empty-least) or the explicit {"$key", "$dir"?, "$empty"?} form.
626
+ function normalizeOrderbySpec(spec, specPath, scope, ctx) {
627
+ let key = spec;
628
+ let keyPath = specPath;
629
+ let desc = false;
630
+ let emptyGreatest = false;
631
+ if (isPlainObject(spec) && (hasOwn(spec, '$key') || hasOwn(spec, '$dir') || hasOwn(spec, '$empty'))) {
632
+ const specKeys = Object.keys(spec);
633
+ for (let i = 0; i < specKeys.length; i++) {
634
+ if (!ORDERBY_SPEC_KEYS.has(specKeys[i]))
635
+ return fail('JQ0003', `'${specKeys[i]}' is not a valid key of an $orderby key spec`, specPath);
636
+ }
637
+ if (!hasOwn(spec, '$key'))
638
+ return fail('JQ0003', "an explicit $orderby key spec requires '$key'", specPath);
639
+ if (hasOwn(spec, '$dir')) {
640
+ if (spec.$dir !== 'asc' && spec.$dir !== 'desc')
641
+ return fail('JQ0003', "'$dir' must be 'asc' or 'desc'", specPath + '/$dir');
642
+ desc = spec.$dir === 'desc';
643
+ }
644
+ if (hasOwn(spec, '$empty')) {
645
+ if (spec.$empty !== 'least' && spec.$empty !== 'greatest')
646
+ return fail('JQ0003', "'$empty' must be 'least' or 'greatest'", specPath + '/$empty');
647
+ emptyGreatest = spec.$empty === 'greatest';
648
+ }
649
+ key = spec.$key;
650
+ keyPath = specPath + '/$key';
651
+ }
652
+ return Object.freeze({
653
+ key: normalizeExpr(key, keyPath, scope, ctx),
654
+ desc, emptyGreatest, docPath: specPath,
655
+ });
656
+ }
657
+
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) {
664
+ switch (node.kind) {
665
+ case 'literal':
666
+ return;
667
+ case 'var':
668
+ out.add(node.slot);
669
+ return;
670
+ case 'path':
671
+ out.add(node.rootSlot);
672
+ return;
673
+ case 'object':
674
+ for (let i = 0; i < node.entries.length; i++)
675
+ collectReadSlots(node.entries[i].expr, out);
676
+ return;
677
+ case 'map':
678
+ for (let i = 0; i < node.pairs.length; i++) {
679
+ collectReadSlots(node.pairs[i].key, out);
680
+ collectReadSlots(node.pairs[i].value, out);
681
+ }
682
+ return;
683
+ case 'array':
684
+ for (let i = 0; i < node.elements.length; i++)
685
+ collectReadSlots(node.elements[i], out);
686
+ return;
687
+ case 'raw': // compile-time data of a registry operator, never evaluated
688
+ return;
689
+ case 'op':
690
+ for (let i = 0; i < node.args.length; i++)
691
+ collectReadSlots(node.args[i], out);
692
+ return;
693
+ case 'let':
694
+ for (let i = 0; i < node.bindings.length; i++)
695
+ collectReadSlots(node.bindings[i].expr, out);
696
+ collectReadSlots(node.ret, out);
697
+ return;
698
+ case 'quant':
699
+ for (let i = 0; i < node.bindings.length; i++)
700
+ collectReadSlots(node.bindings[i].expr, out);
701
+ collectReadSlots(node.satisfies, out);
702
+ return;
703
+ default: { // 'flwor'
704
+ for (let i = 0; i < node.forBindings.length; i++)
705
+ collectReadSlots(node.forBindings[i].expr, out);
706
+ for (let i = 0; i < node.letBindings.length; i++)
707
+ collectReadSlots(node.letBindings[i].expr, out);
708
+ if (node.asChecks !== null) { // reads its own binding slots per tuple
709
+ for (let i = 0; i < node.asChecks.length; i++)
710
+ out.add(node.asChecks[i].slot);
711
+ }
712
+ if (node.where !== null)
713
+ collectReadSlots(node.where, out);
714
+ if (node.groupby !== null) {
715
+ for (let i = 0; i < node.groupby.keys.length; i++)
716
+ collectReadSlots(node.groupby.keys[i].expr, out);
717
+ }
718
+ if (node.orderby !== null) {
719
+ for (let i = 0; i < node.orderby.specs.length; i++)
720
+ collectReadSlots(node.orderby.specs[i].key, out);
721
+ }
722
+ collectReadSlots(node.ret, out);
723
+ return;
724
+ }
725
+ }
726
+ }
727
+
728
+ // The full FLWOR phrase (section 6). Clauses normalize - and their
729
+ // bindings scope - in the fixed semantic order of section 6.1 (D7):
730
+ // $for -> $let -> $as -> $where -> $groupby -> $orderby -> $count -> $return.
731
+ // A $groupby rebinds the phrase's tuple variables for every later
732
+ // clause: key names become singletons (card OPT: a key may be the empty
733
+ // sequence), every other binding becomes the sequence of its values
734
+ // across the group's tuples (card MANY) - same slots, new static cards.
735
+ function normalizeFlworPhrase(obj, docPath, scope, ctx) {
736
+ const phraseNames = new Set();
737
+ const tupleSlots = []; // {name, slot} per pre-group binding site, in order
738
+ const forBindings = [];
739
+ const letBindings = [];
740
+ let sc = scope;
741
+
742
+ if (hasOwn(obj, '$for'))
743
+ sc = normalizeForBindings(obj.$for, docPath + '/$for', sc, ctx, phraseNames, forBindings, tupleSlots);
744
+ if (hasOwn(obj, '$let')) {
745
+ const before = letBindings.length;
746
+ sc = normalizeLetBindings(obj.$let, docPath + '/$let', sc, ctx, phraseNames, letBindings);
747
+ for (let i = before; i < letBindings.length; i++)
748
+ tupleSlots.push({ name: letBindings[i].name, slot: letBindings[i].slot });
749
+ }
750
+
751
+ // $as (section 6.4): schema assertions on the phrase's own bindings,
752
+ // applied per tuple after $for/$let and before $where. A $for/$at
753
+ // variable is validated as its one bound item; a $let variable per item
754
+ // of its bound sequence. The names must be binding sites of THIS phrase
755
+ // (JQ0005 otherwise); each schema compiles once via compileSchemaLiteral.
756
+ let asChecks = null;
757
+ if (hasOwn(obj, '$as')) {
758
+ const asPath = docPath + '/$as';
759
+ const asObj = obj.$as;
760
+ if (!isPlainObject(asObj))
761
+ return fail('JQ0003', "'$as' takes an object of variable-name to schema members", asPath);
762
+ const names = Object.keys(asObj);
763
+ if (names.length === 0)
764
+ return fail('JQ0003', "'$as' requires at least one member", asPath);
765
+ const checks = new Array(names.length);
766
+ for (let i = 0; i < names.length; i++) {
767
+ const name = names[i];
768
+ const checkPath = asPath + '/' + escapeToken(name);
769
+ let slot = -1;
770
+ for (let j = 0; j < tupleSlots.length; j++) {
771
+ if (tupleSlots[j].name === name) {
772
+ slot = tupleSlots[j].slot;
773
+ break;
774
+ }
775
+ }
776
+ if (slot < 0)
777
+ return fail('JQ0005', `'$as' names '${name}', which is not bound by this phrase's '$for'/'$let'`, checkPath);
778
+ let isLet = false;
779
+ for (let j = 0; j < letBindings.length; j++) {
780
+ if (letBindings[j].name === name) {
781
+ isLet = true;
782
+ break;
783
+ }
784
+ }
785
+ const { schema, test } = compileSchemaLiteral(asObj[name], checkPath, checkPath, ctx);
786
+ checks[i] = Object.freeze({ name, slot, isLet, schema, test, docPath: checkPath });
787
+ }
788
+ asChecks = Object.freeze(checks);
789
+ }
790
+
791
+ const where = hasOwn(obj, '$where')
792
+ ? normalizeExpr(obj.$where, docPath + '/$where', sc, ctx)
793
+ : null;
794
+
795
+ let groupby = null;
796
+ if (hasOwn(obj, '$groupby')) {
797
+ const groupPath = docPath + '/$groupby';
798
+ const names = requireBindingObject('$groupby', obj.$groupby, groupPath);
799
+ const keys = new Array(names.length);
800
+ for (let i = 0; i < names.length; i++) {
801
+ const name = names[i];
802
+ const bindPath = groupPath + '/' + escapeToken(name);
803
+ bindPhraseName(name, phraseNames, bindPath);
804
+ // key expressions evaluate per tuple, in the pre-group scope
805
+ const expr = normalizeExpr(obj.$groupby[name], bindPath, sc, ctx);
806
+ keys[i] = Object.freeze({ name, slot: ctx.nextSlot++, expr, docPath: bindPath });
807
+ }
808
+ // post-group scope: same slots, rebound cards
809
+ sc = scope;
810
+ for (let i = 0; i < tupleSlots.length; i++)
811
+ sc = { name: tupleSlots[i].name, slot: tupleSlots[i].slot, card: CARD_MANY, parent: sc };
812
+ for (let i = 0; i < keys.length; i++)
813
+ sc = { name: keys[i].name, slot: keys[i].slot, card: CARD_OPT, parent: sc };
814
+ groupby = { keys: Object.freeze(keys), docPath: groupPath, accSlots: null };
815
+ }
816
+
817
+ let orderby = null;
818
+ if (hasOwn(obj, '$orderby')) {
819
+ const orderPath = docPath + '/$orderby';
820
+ const raw = obj.$orderby;
821
+ let specs;
822
+ if (Array.isArray(raw)) { // always a list of key specs, major to minor
823
+ if (raw.length === 0)
824
+ return fail('JQ0003', "'$orderby' takes a key spec or a non-empty array of key specs", orderPath);
825
+ specs = new Array(raw.length);
826
+ for (let i = 0; i < raw.length; i++)
827
+ specs[i] = normalizeOrderbySpec(raw[i], orderPath + '/' + i, sc, ctx);
828
+ }
829
+ else {
830
+ specs = [normalizeOrderbySpec(raw, orderPath, sc, ctx)];
831
+ }
832
+ orderby = { specs: Object.freeze(specs), docPath: orderPath, liveSlots: null };
833
+ }
834
+
835
+ let count = null;
836
+ if (hasOwn(obj, '$count')) {
837
+ const countPath = docPath + '/$count';
838
+ const name = obj.$count;
839
+ if (typeof name !== 'string')
840
+ return fail('JQ0003', "'$count' takes a variable name string", countPath);
841
+ bindPhraseName(name, phraseNames, countPath);
842
+ const slot = ctx.nextSlot++;
843
+ sc = { name, slot, card: CARD_ONE, parent: sc };
844
+ count = Object.freeze({ name, slot });
845
+ }
846
+
847
+ const ret = normalizeExpr(obj.$return, docPath + '/$return', sc, ctx);
848
+
849
+ // barrier liveness (compile-time): $groupby accumulates - and $orderby
850
+ // snapshots - only the binding slots that later clauses actually read.
851
+ // The $count slot is written after both barriers and is never live.
852
+ const retReads = new Set();
853
+ collectReadSlots(ret, retReads);
854
+ if (groupby !== null) {
855
+ const laterReads = new Set(retReads);
856
+ if (orderby !== null) {
857
+ for (let i = 0; i < orderby.specs.length; i++)
858
+ collectReadSlots(orderby.specs[i].key, laterReads);
859
+ }
860
+ const accSlots = [];
861
+ for (let i = 0; i < tupleSlots.length; i++) {
862
+ if (laterReads.has(tupleSlots[i].slot))
863
+ accSlots.push(tupleSlots[i].slot);
864
+ }
865
+ groupby.accSlots = Object.freeze(accSlots);
866
+ }
867
+ if (orderby !== null) {
868
+ // slots that still vary per tuple at the $orderby barrier
869
+ const barrierSlots = [];
870
+ if (groupby !== null) {
871
+ for (let i = 0; i < groupby.keys.length; i++)
872
+ barrierSlots.push(groupby.keys[i].slot);
873
+ for (let i = 0; i < groupby.accSlots.length; i++)
874
+ barrierSlots.push(groupby.accSlots[i]);
875
+ }
876
+ else {
877
+ for (let i = 0; i < tupleSlots.length; i++)
878
+ barrierSlots.push(tupleSlots[i].slot);
879
+ }
880
+ const liveSlots = [];
881
+ for (let i = 0; i < barrierSlots.length; i++) {
882
+ if (retReads.has(barrierSlots[i]))
883
+ liveSlots.push(barrierSlots[i]);
884
+ }
885
+ orderby.liveSlots = Object.freeze(liveSlots);
886
+ }
887
+
888
+ // phrase cardinality: MANY unless provably otherwise - a $let-only
889
+ // phrase yields exactly one tuple ($where may still drop it)
890
+ let card;
891
+ if (forBindings.length !== 0 || groupby !== null)
892
+ card = CARD_MANY;
893
+ else
894
+ card = where !== null ? joinCard(ret.card, CARD_ZERO) : ret.card;
895
+
896
+ return Object.freeze({
897
+ kind: 'flwor', card, docPath,
898
+ forBindings: Object.freeze(forBindings),
899
+ letBindings: Object.freeze(letBindings),
900
+ asChecks, where,
901
+ groupby: groupby === null ? null : Object.freeze(groupby),
902
+ orderby: orderby === null ? null : Object.freeze(orderby),
903
+ count, ret,
904
+ });
905
+ }
906
+
907
+ // Quantifier phrases (section 7): {$some|$every, $satisfies}. The
908
+ // binding object follows $for rules (names, key-order nesting,
909
+ // correlation, D4 unpacking) except the extended $in/$at form (JQ0003).
910
+ function normalizeQuantifierPhrase(obj, docPath, scope, ctx) {
911
+ const some = hasOwn(obj, '$some');
912
+ const clause = some ? '$some' : '$every';
913
+ const clausePath = docPath + '/' + clause;
914
+ const bindObj = obj[clause];
915
+ const names = requireBindingObject(clause, bindObj, clausePath);
916
+ const phraseNames = new Set();
917
+ const bindings = new Array(names.length);
918
+ let sc = scope;
919
+ for (let i = 0; i < names.length; i++) {
920
+ const name = names[i];
921
+ const bindPath = clausePath + '/' + escapeToken(name);
922
+ bindPhraseName(name, phraseNames, bindPath);
923
+ 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);
926
+ const expr = normalizeExpr(source, bindPath, sc, ctx);
927
+ const slot = ctx.nextSlot++;
928
+ sc = { name, slot, card: CARD_ONE, parent: sc };
929
+ bindings[i] = Object.freeze({ name, slot, expr });
930
+ }
931
+ const satisfies = normalizeExpr(obj.$satisfies, docPath + '/$satisfies', sc, ctx);
932
+ return Object.freeze({
933
+ kind: 'quant', card: CARD_ONE, docPath, some,
934
+ bindings: Object.freeze(bindings), satisfies,
935
+ });
936
+ }
937
+
938
+ //#endregion
939
+
940
+ //#region entry points
941
+
942
+ // Validate `options.extensions` (package-internal, used by the JSLT
943
+ // layer; not a public contract): a plain object of `name -> entry`.
944
+ // Every name must start with '$' and must not collide with the core
945
+ // vocabulary - the closed format is unchanged, extensions are host
946
+ // machinery. Violations are host programming errors (TypeError), not
947
+ // JQ0xxx document errors.
948
+ function validateExtensions(extensions) {
949
+ if (!isPlainObject(extensions))
950
+ throw new TypeError('options.extensions must be a plain object of operator entries');
951
+ const names = Object.keys(extensions);
952
+ for (let i = 0; i < names.length; i++) {
953
+ const name = names[i];
954
+ if (name.charCodeAt(0) !== 0x24) // '$'
955
+ 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))
959
+ throw new TypeError(`extension operator '${name}' collides with the core vocabulary`);
960
+ }
961
+ return extensions;
962
+ }
963
+
964
+ function normalizeExpr(value, docPath, scope, ctx) {
965
+ switch (typeof value) {
966
+ case 'string':
967
+ return normalizeString(value, docPath, scope, ctx);
968
+ case 'number':
969
+ case 'boolean':
970
+ return makeLiteral(value, docPath);
971
+ case 'object': {
972
+ if (value === null)
973
+ return makeLiteral(null, docPath);
974
+ if (Array.isArray(value)) { // Rule 3: array constructor
975
+ return Object.freeze({
976
+ kind: 'array', card: CARD_ONE, docPath,
977
+ elements: normalizeElements(value, docPath, scope, ctx),
978
+ });
979
+ }
980
+ return normalizeObject(value, docPath, scope, ctx);
981
+ }
982
+ default:
983
+ return fail('JQ0003', `a query document cannot contain a ${typeof value}`, docPath);
984
+ }
985
+ }
986
+
987
+ /**
988
+ * Normalize a query document into the internal AST.
989
+ * @param {any} doc - the query document (any JSON value)
990
+ * @param {object} [options] - compile options
991
+ * @param {(schemaJson: any, docPath: string) => ((value: any) => boolean)}
992
+ * [options.compileTypeTest] - host hook compiling a JSON Schema literal
993
+ * into a boolean item predicate (QUERY-FORMAT.md section 8.11). Called
994
+ * once per schema literal, at query compile time. Without it, schema
995
+ * operators ($valid/$assert/$as) are compile error JQ0008.
996
+ * @param {object} [options.extensions] - package-internal operator
997
+ * extension point, the operator analogue of `compileTypeTest` (used by
998
+ * the JSLT layer; not a public contract). A plain object of
999
+ * `name -> entry`, where entry follows the operator registry contract
1000
+ * (`params`/`result`/`compile`, operators.js header) plus an optional
1001
+ * `normalize(arg, docPath, opPath, scope, ctx, helpers) -> {args, card?}`
1002
+ * override for polymorphic value shapes. Names must start with '$' and
1003
+ * must not collide with the core vocabulary (TypeError - a host
1004
+ * programming error, not a JQ0xxx document error). The published format
1005
+ * and its schema are unchanged: without extensions, the same documents
1006
+ * fail JQ0002.
1007
+ * @returns {{ root: object, frameSize: number, externals: {name: string, slot: number}[] }}
1008
+ * the AST root, the frame size, and the external parameters in order of
1009
+ * first appearance (slot order)
1010
+ * @throws {JsonQueryCompileError} on any JQ0xxx condition
1011
+ */
1012
+ export function normalizeQuery(doc, options = {}) {
1013
+ const compileTypeTest = typeof options.compileTypeTest === 'function'
1014
+ ? options.compileTypeTest
1015
+ : null;
1016
+ const extensions = options.extensions == null ? null : validateExtensions(options.extensions);
1017
+ const ctx = { nextSlot: 1, externals: new Map(), compileTypeTest, extensions };
1018
+ let expr = doc;
1019
+ let rootPath = '';
1020
+ // the version envelope is only recognized at the top level (section 4)
1021
+ if (isPlainObject(doc) && (hasOwn(doc, '$query') || hasOwn(doc, '$expr'))) {
1022
+ const keys = Object.keys(doc);
1023
+ let allDollar = true;
1024
+ for (let i = 0; i < keys.length; i++) {
1025
+ if (keys[i].charCodeAt(0) !== 0x24)
1026
+ allDollar = false;
1027
+ }
1028
+ if (allDollar) {
1029
+ if (hasOwn(doc, '$query') && doc.$query !== '0.1')
1030
+ fail('JQ0006', `unknown query format version ${JSON.stringify(doc.$query)}`, '/$query');
1031
+ if (!hasOwn(doc, '$query') || !hasOwn(doc, '$expr') || keys.length !== 2)
1032
+ fail('JQ0003', "the version envelope requires exactly the keys '$query' and '$expr'", '');
1033
+ expr = doc.$expr;
1034
+ rootPath = '/$expr';
1035
+ }
1036
+ }
1037
+ const root = normalizeExpr(expr, rootPath, null, ctx);
1038
+ const externals = new Array(ctx.externals.size);
1039
+ let i = 0;
1040
+ for (const [name, slot] of ctx.externals)
1041
+ externals[i++] = Object.freeze({ name, slot });
1042
+ return { root, frameSize: ctx.nextSlot, externals: Object.freeze(externals) };
1043
+ }
1044
+
1045
+ //#endregion
1046
+
1047
+ //#endregion