@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,1253 @@
1
+ //#region Jaren JSON Query operator registry
2
+ // The single table of the section-8 operator vocabulary (QUERY-FORMAT.md):
3
+ // every operator declares its signature once - `name -> { params, result,
4
+ // compile }` - and gets uniform compile-time checking from it. The
5
+ // query-language analogue of the `FUNCTIONS` table in path.js.
6
+ //
7
+ // - `params` describes the argument shape. The string 'expr' is the
8
+ // single form (`{"$op": operand}`: the value IS the one argument);
9
+ // `{ kinds, min, variadic? }` is the array form (`{"$op": [a, b]}`),
10
+ // where `kinds[i]` is the expectation of position i - 'expr' (an
11
+ // ordinary expression), 'raw' (a verbatim JSON value, not evaluated),
12
+ // 'schema' (a verbatim JSON Schema literal, compiled at query compile
13
+ // time into an item predicate at `args[i].test` - the schema
14
+ // arguments of $valid/$assert) or
15
+ // 'name' (a variable name string). `min` is the minimum argument
16
+ // count; the maximum is `kinds.length`, or unbounded when `variadic`
17
+ // (the last kind repeats). normalize.js validates arity and shape
18
+ // from this descriptor (JQ0003), so compile functions never re-check
19
+ // structure.
20
+ // - `result(cards)` is the operator's static cardinality contribution:
21
+ // given the argument cardinalities it returns the result cardinality
22
+ // (an upper approximation; CARD_ONE means "always exactly one item").
23
+ // - `compile(gets, args, docPath)` returns the getter closure.
24
+ // `gets[i]` is the compiled getter of argument i (`null` for
25
+ // 'raw'/'name' positions - their value is compile-time data at
26
+ // `args[i].value`); `args[i]` is the frozen argument AST node
27
+ // carrying `card` and `docPath`; `docPath` locates the operator key
28
+ // itself (e.g. `/$idiv`).
29
+ //
30
+ // All runtime error conditions of the operator library (JQ2xxx,
31
+ // QUERY-FORMAT.md section 10.3) are raised here.
32
+
33
+ import { equalsJson } from '@jarenjs/core/object';
34
+ import { countCodePoints, compareCodePoints } from '@jarenjs/core/string';
35
+ import { compileIRegexp } from '@jarenjs/core/text/iregexp';
36
+ import { JsonQueryRuntimeError } from './errors.js';
37
+ import {
38
+ EMPTY, Seq, seqOf, appendItem, ebv, itemCount, firstItem,
39
+ stableKeyString, describeItem,
40
+ } from './runtime.js';
41
+ import {
42
+ CARD_ZERO, CARD_ONE, CARD_OPT, CARD_MANY, joinCard, sumCard,
43
+ } from './normalize.js';
44
+ import { compileExistsTest } from './compile.js';
45
+
46
+ const hasOwn = Object.hasOwn;
47
+
48
+ //#region parameter and result descriptors
49
+
50
+ // the single form: the operator's value is its one operand expression
51
+ const UNARY = 'expr';
52
+ // array forms; frozen and shared across entries
53
+ const ARGS_2 = Object.freeze({ kinds: Object.freeze(['expr', 'expr']), min: 2 });
54
+ const ARGS_3 = Object.freeze({ kinds: Object.freeze(['expr', 'expr', 'expr']), min: 3 });
55
+ const ARGS_1_2 = Object.freeze({ kinds: Object.freeze(['expr', 'expr']), min: 1 });
56
+ const ARGS_2_3 = Object.freeze({ kinds: Object.freeze(['expr', 'expr', 'expr']), min: 2 });
57
+ const ARGS_0N = Object.freeze({ kinds: Object.freeze(['expr']), min: 0, variadic: true });
58
+ const ARGS_1N = Object.freeze({ kinds: Object.freeze(['expr']), min: 1, variadic: true });
59
+ // the schema operators $valid/$assert: [expr, schema] (section 8.11)
60
+ const ARGS_EXPR_SCHEMA = Object.freeze({ kinds: Object.freeze(['expr', 'schema']), min: 2 });
61
+
62
+ const RESULT_ONE = () => CARD_ONE;
63
+ const RESULT_OPT = () => CARD_OPT;
64
+ const RESULT_MANY = () => CARD_MANY;
65
+ // the operand's cardinality is preserved ($distinct, $reverse, $sort)
66
+ const resultOfOperand = (cards) => cards[0];
67
+ // a singleton operand stays a singleton; anything else may come out empty
68
+ const resultEmptyPropagates = (cards) => (cards[0] === CARD_ONE ? CARD_ONE : CARD_OPT);
69
+
70
+ //#endregion
71
+
72
+ //#region runtime argument helpers
73
+
74
+ function runtimeError(code, message, docPath) {
75
+ return new JsonQueryRuntimeError(code, message, docPath);
76
+ }
77
+
78
+ // string parameter rule (section 8.7): the empty sequence reads as '',
79
+ // a single string is itself, anything else is JQ2001
80
+ function stringArg(v, docPath) {
81
+ if (typeof v === 'string')
82
+ return v;
83
+ if (v === EMPTY)
84
+ return '';
85
+ throw runtimeError('JQ2001', `expected a string, got ${describeItem(v)}`, docPath);
86
+ }
87
+
88
+ // numeric position/length parameter: a single number, everything else -
89
+ // including the empty sequence - is JQ2001
90
+ function numberArg(v, docPath) {
91
+ if (typeof v !== 'number')
92
+ throw runtimeError('JQ2001', `expected a number, got ${describeItem(v)}`, docPath);
93
+ return v;
94
+ }
95
+
96
+ // the $string cast table (section 8.10); callers handle the empty sequence
97
+ function castString(v, docPath) {
98
+ switch (typeof v) {
99
+ case 'string':
100
+ return v;
101
+ case 'number':
102
+ return String(v);
103
+ case 'boolean':
104
+ return v ? 'true' : 'false';
105
+ default:
106
+ if (v === null)
107
+ return 'null';
108
+ throw runtimeError('JQ2001', `cannot cast ${describeItem(v)} to a string`, docPath);
109
+ }
110
+ }
111
+
112
+ // the $number cast table (section 8.10): strings must be JSON numbers
113
+ // (strict RFC 8259 grammar - no leading '+', no bare '.', no whitespace)
114
+ const JSON_NUMBER_RE = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
115
+
116
+ function castNumber(v, docPath) {
117
+ switch (typeof v) {
118
+ case 'number':
119
+ return v;
120
+ case 'boolean':
121
+ return v ? 1 : 0;
122
+ case 'string':
123
+ if (JSON_NUMBER_RE.test(v))
124
+ return Number(v);
125
+ throw runtimeError('JQ2001', `'${v}' is not a JSON number`, docPath);
126
+ default:
127
+ throw runtimeError('JQ2001', `cannot cast ${describeItem(v)} to a number`, docPath);
128
+ }
129
+ }
130
+
131
+ //#endregion
132
+
133
+ //#region shared compile helpers
134
+
135
+ // per-element appliers of $seq: CARD_ONE elements push directly,
136
+ // everything else flattens through appendItem (same discipline as the
137
+ // array constructor in compile.js)
138
+ function compileSeqOp(gets, args) {
139
+ if (gets.length === 0)
140
+ return () => EMPTY;
141
+ if (gets.length === 1) // {"$seq": [e]} is e
142
+ return gets[0];
143
+ const alen = gets.length;
144
+ const appliers = new Array(alen);
145
+ for (let i = 0; i < alen; i++) {
146
+ const get = gets[i];
147
+ appliers[i] = args[i].card === CARD_ONE
148
+ ? (f, acc) => acc.push(get(f))
149
+ : (f, acc) => appendItem(acc, get(f));
150
+ }
151
+ return (f) => {
152
+ const acc = [];
153
+ for (let i = 0; i < alen; i++)
154
+ appliers[i](f, acc);
155
+ return seqOf(acc);
156
+ };
157
+ }
158
+
159
+ // item comparison rules (section 8.4): $eq/$ne deep structural JSON
160
+ // equality (D2); ordering only between two numbers or two strings, any
161
+ // other pair is simply false (no witness, no error)
162
+ function itemEq(a, b) {
163
+ return equalsJson(a, b);
164
+ }
165
+ function itemNe(a, b) {
166
+ return !equalsJson(a, b);
167
+ }
168
+ function itemLt(a, b) {
169
+ if (typeof a === 'number')
170
+ return typeof b === 'number' && a < b;
171
+ if (typeof a === 'string')
172
+ return typeof b === 'string' && compareCodePoints(a, b) < 0;
173
+ return false;
174
+ }
175
+ function itemLe(a, b) {
176
+ if (typeof a === 'number')
177
+ return typeof b === 'number' && a <= b;
178
+ if (typeof a === 'string')
179
+ return typeof b === 'string' && compareCodePoints(a, b) <= 0;
180
+ return false;
181
+ }
182
+ function itemGt(a, b) {
183
+ return itemLt(b, a);
184
+ }
185
+ function itemGe(a, b) {
186
+ return itemLe(b, a);
187
+ }
188
+
189
+ // existential general comparison (section 8.4): true iff some pair of
190
+ // items compares true; either side empty means no witnessing pair
191
+ // (contrast the path filter dialect where Nothing == Nothing holds -
192
+ // section 5.2)
193
+ function comparisonEntry(itemCmp) {
194
+ return {
195
+ params: ARGS_2,
196
+ result: RESULT_ONE,
197
+ compile: (gets, args) => {
198
+ const left = gets[0];
199
+ const right = gets[1];
200
+ if (args[0].card === CARD_ONE && args[1].card === CARD_ONE)
201
+ // the common case: two singletons, one direct item comparison
202
+ return (f) => itemCmp(left(f), right(f));
203
+ return (f) => {
204
+ const lv = left(f);
205
+ if (lv === EMPTY)
206
+ return false;
207
+ const rv = right(f);
208
+ if (rv === EMPTY)
209
+ return false;
210
+ if (lv instanceof Seq) {
211
+ const li = lv.items;
212
+ if (rv instanceof Seq) {
213
+ const ri = rv.items;
214
+ for (let i = 0; i < li.length; i++) {
215
+ for (let j = 0; j < ri.length; j++) {
216
+ if (itemCmp(li[i], ri[j]))
217
+ return true;
218
+ }
219
+ }
220
+ return false;
221
+ }
222
+ for (let i = 0; i < li.length; i++) {
223
+ if (itemCmp(li[i], rv))
224
+ return true;
225
+ }
226
+ return false;
227
+ }
228
+ if (rv instanceof Seq) {
229
+ const ri = rv.items;
230
+ for (let j = 0; j < ri.length; j++) {
231
+ if (itemCmp(lv, ri[j]))
232
+ return true;
233
+ }
234
+ return false;
235
+ }
236
+ return itemCmp(lv, rv);
237
+ };
238
+ },
239
+ };
240
+ }
241
+
242
+ function arithOperandError(v, docPath) {
243
+ return runtimeError('JQ2001', `arithmetic requires a number operand, got ${describeItem(v)}`, docPath);
244
+ }
245
+
246
+ // IEEE double arithmetic (section 8.5, D1): empty operands propagate,
247
+ // non-number singletons and longer sequences are JQ2001. `makeApply`
248
+ // builds the two-number kernel (it closes over the operator docPath for
249
+ // the JQ2002 zero-divisor errors of $idiv/$mod).
250
+ function arithmeticEntry(makeApply) {
251
+ return {
252
+ params: ARGS_2,
253
+ result: (cards) => (cards[0] === CARD_ONE && cards[1] === CARD_ONE ? CARD_ONE : CARD_OPT),
254
+ compile: (gets, args, docPath) => {
255
+ const apply = makeApply(docPath);
256
+ const left = gets[0];
257
+ const right = gets[1];
258
+ const leftPath = args[0].docPath;
259
+ const rightPath = args[1].docPath;
260
+ if (args[0].card === CARD_ONE && args[1].card === CARD_ONE) {
261
+ // singleton operands: type guard only, no sequence checks
262
+ return (f) => {
263
+ const a = left(f);
264
+ if (typeof a !== 'number')
265
+ throw arithOperandError(a, leftPath);
266
+ const b = right(f);
267
+ if (typeof b !== 'number')
268
+ throw arithOperandError(b, rightPath);
269
+ return apply(a, b);
270
+ };
271
+ }
272
+ return (f) => {
273
+ const a = left(f);
274
+ if (a === EMPTY) // empty propagation (XQuery)
275
+ return EMPTY;
276
+ if (typeof a !== 'number')
277
+ throw arithOperandError(a, leftPath);
278
+ const b = right(f);
279
+ if (b === EMPTY)
280
+ return EMPTY;
281
+ if (typeof b !== 'number')
282
+ throw arithOperandError(b, rightPath);
283
+ return apply(a, b);
284
+ };
285
+ },
286
+ };
287
+ }
288
+
289
+ // variadic EBV logic (section 8.6): left-to-right with short-circuit -
290
+ // operands after the deciding one are not evaluated
291
+ function logicEntry(stopOn) {
292
+ return {
293
+ params: ARGS_1N,
294
+ result: RESULT_ONE,
295
+ compile: (gets, args) => {
296
+ const paths = args.map((a) => a.docPath);
297
+ const flen = gets.length;
298
+ return (f) => {
299
+ for (let i = 0; i < flen; i++) {
300
+ if (ebv(gets[i](f), paths[i]) === stopOn)
301
+ return stopOn;
302
+ }
303
+ return !stopOn;
304
+ };
305
+ },
306
+ };
307
+ }
308
+
309
+ //#endregion
310
+
311
+ //#region string operators
312
+
313
+ // unary string operator: coerce per stringArg, apply the kernel
314
+ function stringUnaryEntry(apply) {
315
+ return {
316
+ params: UNARY,
317
+ result: RESULT_ONE,
318
+ compile: (gets, args) => {
319
+ const get = gets[0];
320
+ const docPath = args[0].docPath;
321
+ return (f) => apply(stringArg(get(f), docPath));
322
+ },
323
+ };
324
+ }
325
+
326
+ // binary string operator over two coerced string arguments
327
+ function stringPairEntry(test) {
328
+ return {
329
+ params: ARGS_2,
330
+ result: RESULT_ONE,
331
+ compile: (gets, args) => {
332
+ const aGet = gets[0];
333
+ const aPath = args[0].docPath;
334
+ const bGet = gets[1];
335
+ const bPath = args[1].docPath;
336
+ return (f) => test(stringArg(aGet(f), aPath), stringArg(bGet(f), bPath));
337
+ },
338
+ };
339
+ }
340
+
341
+ // fn:normalize-space: strip leading/trailing XML whitespace (space, tab,
342
+ // LF, CR) and collapse internal runs to a single space
343
+ function normalizeSpace(s) {
344
+ let out = '';
345
+ let pending = false;
346
+ for (let i = 0; i < s.length; i++) {
347
+ const c = s.charCodeAt(i);
348
+ if (c === 0x20 || c === 0x09 || c === 0x0A || c === 0x0D) {
349
+ pending = out !== '';
350
+ continue;
351
+ }
352
+ if (pending) {
353
+ out += ' ';
354
+ pending = false;
355
+ }
356
+ out += s[i];
357
+ }
358
+ return out;
359
+ }
360
+
361
+ // $match (anchored) / $search (free): I-Regexp (RFC 9485, D5). Literal
362
+ // patterns compile at query-compile time; dynamic patterns get the
363
+ // per-callsite monomorphic cache (the compileRegexTest approach in
364
+ // segments.js). A syntactically invalid I-Regexp yields false, the
365
+ // RFC 9535 match()/search() behavior.
366
+ function regexTestEntry(fullMatch) {
367
+ return {
368
+ params: ARGS_2,
369
+ result: RESULT_ONE,
370
+ compile: (gets, args) => {
371
+ const inGet = gets[0];
372
+ const inPath = args[0].docPath;
373
+ const patNode = args[1];
374
+ if (patNode.kind === 'literal' && typeof patNode.value === 'string') {
375
+ const re = compileIRegexp(patNode.value, fullMatch);
376
+ if (re === null)
377
+ return (f) => {
378
+ stringArg(inGet(f), inPath); // the input type rule still applies
379
+ return false;
380
+ };
381
+ return (f) => re.test(stringArg(inGet(f), inPath));
382
+ }
383
+ const patGet = gets[1];
384
+ const patPath = patNode.docPath;
385
+ let lastPattern = null;
386
+ let lastRegExp = null;
387
+ return (f) => {
388
+ const s = stringArg(inGet(f), inPath);
389
+ const p = stringArg(patGet(f), patPath);
390
+ if (p !== lastPattern) {
391
+ lastPattern = p;
392
+ lastRegExp = compileIRegexp(p, fullMatch);
393
+ }
394
+ return lastRegExp !== null && lastRegExp.test(s);
395
+ };
396
+ },
397
+ };
398
+ }
399
+
400
+ // $replace pattern compilation: null marks an invalid I-Regexp, false a
401
+ // pattern that matches the zero-length string (an error per F&O
402
+ // fn:replace, err:FORX0002/FORX0003 -> JQ2001), otherwise the global
403
+ // RegExp used for replace-all
404
+ function replaceRegexp(pattern) {
405
+ const re = compileIRegexp(pattern, false);
406
+ if (re === null)
407
+ return null;
408
+ if (re.test(''))
409
+ return false;
410
+ return new RegExp(re.source, 'gu');
411
+ }
412
+
413
+ function replaceRegexpError(gre, pattern, docPath) {
414
+ return runtimeError('JQ2001', gre === null
415
+ ? `'${pattern}' is not a valid I-Regexp pattern`
416
+ : `'$replace' pattern '${pattern}' matches the zero-length string`, docPath);
417
+ }
418
+
419
+ function compileReplace(gets, args) {
420
+ const sGet = gets[0];
421
+ const sPath = args[0].docPath;
422
+ const patNode = args[1];
423
+ const patPath = patNode.docPath;
424
+ const repGet = gets[2];
425
+ const repPath = args[2].docPath;
426
+ if (patNode.kind === 'literal' && typeof patNode.value === 'string') {
427
+ const gre = replaceRegexp(patNode.value);
428
+ return (f) => {
429
+ const s = stringArg(sGet(f), sPath);
430
+ const rep = stringArg(repGet(f), repPath);
431
+ if (gre === null || gre === false)
432
+ throw replaceRegexpError(gre, patNode.value, patPath);
433
+ // function replacement: `rep` is inserted literally, no `$`-group
434
+ // references (I-Regexp guarantees no capture semantics - D5)
435
+ return s.replace(gre, () => rep);
436
+ };
437
+ }
438
+ const patGet = gets[1];
439
+ let lastPattern = null;
440
+ let lastGre = null;
441
+ return (f) => {
442
+ const s = stringArg(sGet(f), sPath);
443
+ const p = stringArg(patGet(f), patPath);
444
+ const rep = stringArg(repGet(f), repPath);
445
+ if (p !== lastPattern) {
446
+ lastPattern = p;
447
+ lastGre = replaceRegexp(p);
448
+ }
449
+ if (lastGre === null || lastGre === false)
450
+ throw replaceRegexpError(lastGre, p, patPath);
451
+ return s.replace(lastGre, () => rep);
452
+ };
453
+ }
454
+
455
+ //#endregion
456
+
457
+ //#region aggregate operators
458
+
459
+ function aggregateNumber(v, docPath) {
460
+ if (typeof v !== 'number')
461
+ throw runtimeError('JQ2001', `aggregate items must be numbers, got ${describeItem(v)}`, docPath);
462
+ return v;
463
+ }
464
+
465
+ function minmaxError(v, docPath) {
466
+ return runtimeError('JQ2001', `'$min'/'$max' items must be all numbers or all strings, got ${describeItem(v)}`, docPath);
467
+ }
468
+
469
+ // $min/$max (section 8.8): empty -> empty; items all numbers or all
470
+ // strings (Unicode scalar value order), mixed or anything else JQ2001;
471
+ // any NaN item makes a numeric result NaN (F&O fn:min/fn:max)
472
+ function minmaxEntry(isMax) {
473
+ return {
474
+ params: UNARY,
475
+ result: resultEmptyPropagates,
476
+ compile: (gets, args) => {
477
+ const get = gets[0];
478
+ const docPath = args[0].docPath;
479
+ return (f) => {
480
+ const v = get(f);
481
+ if (v === EMPTY)
482
+ return EMPTY;
483
+ if (!(v instanceof Seq)) {
484
+ const t = typeof v;
485
+ if (t !== 'number' && t !== 'string')
486
+ throw minmaxError(v, docPath);
487
+ return v;
488
+ }
489
+ const items = v.items;
490
+ let best = items[0];
491
+ const numeric = typeof best === 'number';
492
+ if (!numeric && typeof best !== 'string')
493
+ throw minmaxError(best, docPath);
494
+ let sawNaN = numeric && best !== best;
495
+ for (let i = 1; i < items.length; i++) {
496
+ const item = items[i];
497
+ if (typeof item === 'number') {
498
+ if (!numeric)
499
+ throw minmaxError(item, docPath);
500
+ if (item !== item)
501
+ sawNaN = true;
502
+ else if (isMax ? item > best : item < best)
503
+ best = item;
504
+ }
505
+ else if (typeof item === 'string') {
506
+ if (numeric)
507
+ throw minmaxError(item, docPath);
508
+ const c = compareCodePoints(item, best);
509
+ if (isMax ? c > 0 : c < 0)
510
+ best = item;
511
+ }
512
+ else {
513
+ throw minmaxError(item, docPath);
514
+ }
515
+ }
516
+ return sawNaN ? NaN : best;
517
+ };
518
+ },
519
+ };
520
+ }
521
+
522
+ //#endregion
523
+
524
+ //#region sequence operators
525
+
526
+ function sortError(v, docPath) {
527
+ return runtimeError('JQ2005', `'$sort' items must be all numbers or all strings, got ${describeItem(v)}`, docPath);
528
+ }
529
+
530
+ // number order for $sort: NaN equal to itself and less than every other
531
+ // number (the section 6.6 order-by rule)
532
+ function compareNumberKeys(a, b) {
533
+ if (a < b)
534
+ return -1;
535
+ if (a > b)
536
+ return 1;
537
+ if (a !== a)
538
+ return b !== b ? 0 : -1;
539
+ return b !== b ? 1 : 0;
540
+ }
541
+
542
+ function rangeBoundError(v, docPath) {
543
+ return runtimeError('JQ2001', `'$range' bounds must be integral numbers, got ${describeItem(v)}`, docPath);
544
+ }
545
+
546
+ // the resource guard of $range (section 10.3, JQ2007)
547
+ const RANGE_LIMIT = 4294967296; // 2^32
548
+
549
+ //#endregion
550
+
551
+ //#region the registry
552
+
553
+ /**
554
+ * The complete v0.1 operator vocabulary (QUERY-FORMAT.md section 8), one
555
+ * entry per operator: `{ params, result, compile }` (see the module
556
+ * comment for the descriptor contract). Frozen; the closed vocabulary is
557
+ * exactly the keys of this table plus the phrase keys and escape hatches
558
+ * handled by normalize.js.
559
+ */
560
+ export const OPERATORS = Object.freeze({
561
+
562
+ //#region section 8.2 - sequences
563
+
564
+ '$seq': {
565
+ params: ARGS_0N,
566
+ result: (cards) => {
567
+ let card = CARD_ZERO;
568
+ for (let i = 0; i < cards.length; i++)
569
+ card = sumCard(card, cards[i]);
570
+ return card;
571
+ },
572
+ compile: compileSeqOp,
573
+ },
574
+
575
+ '$exists': {
576
+ params: UNARY,
577
+ result: RESULT_ONE,
578
+ // existence never materializes a result sequence (the path.js
579
+ // compileExists analogue lives in compile.js, keyed on the node)
580
+ compile: (gets, args) => compileExistsTest(args[0]),
581
+ },
582
+
583
+ '$empty': {
584
+ params: UNARY,
585
+ result: RESULT_ONE,
586
+ compile: (gets, args) => {
587
+ const test = compileExistsTest(args[0]);
588
+ return (f) => !test(f);
589
+ },
590
+ },
591
+
592
+ //#endregion
593
+
594
+ //#region section 8.3 - conditional
595
+
596
+ '$if': {
597
+ params: ARGS_2_3,
598
+ result: (cards) => joinCard(cards[1], cards.length === 3 ? cards[2] : CARD_ZERO),
599
+ compile: (gets, args) => {
600
+ const cond = gets[0];
601
+ const condPath = args[0].docPath;
602
+ const then = gets[1];
603
+ if (gets.length === 2) // missing else means the empty sequence
604
+ return (f) => (ebv(cond(f), condPath) ? then(f) : EMPTY);
605
+ const alt = gets[2];
606
+ return (f) => (ebv(cond(f), condPath) ? then(f) : alt(f));
607
+ },
608
+ },
609
+
610
+ //#endregion
611
+
612
+ //#region section 8.4 - comparisons
613
+
614
+ '$eq': comparisonEntry(itemEq),
615
+ '$ne': comparisonEntry(itemNe),
616
+ '$lt': comparisonEntry(itemLt),
617
+ '$le': comparisonEntry(itemLe),
618
+ '$gt': comparisonEntry(itemGt),
619
+ '$ge': comparisonEntry(itemGe),
620
+
621
+ //#endregion
622
+
623
+ //#region section 8.5 - arithmetic
624
+
625
+ '$add': arithmeticEntry(() => (a, b) => a + b),
626
+ '$sub': arithmeticEntry(() => (a, b) => a - b),
627
+ '$mul': arithmeticEntry(() => (a, b) => a * b),
628
+ // IEEE 754 double division: /0 is +-Infinity or NaN (D1)
629
+ '$div': arithmeticEntry(() => (a, b) => a / b),
630
+ // truncating division; zero divisor errors (section 8.5)
631
+ '$idiv': arithmeticEntry((docPath) => (a, b) => {
632
+ if (b === 0)
633
+ throw runtimeError('JQ2002', "'$idiv' by zero", docPath);
634
+ return Math.trunc(a / b);
635
+ }),
636
+ // XQuery double mod takes the sign of the dividend = JS %
637
+ '$mod': arithmeticEntry((docPath) => (a, b) => {
638
+ if (b === 0)
639
+ throw runtimeError('JQ2002', "'$mod' by zero", docPath);
640
+ return a % b;
641
+ }),
642
+
643
+ '$neg': {
644
+ params: UNARY,
645
+ result: resultEmptyPropagates,
646
+ compile: (gets, args) => {
647
+ const get = gets[0];
648
+ const docPath = args[0].docPath;
649
+ if (args[0].card === CARD_ONE) {
650
+ return (f) => {
651
+ const a = get(f);
652
+ if (typeof a !== 'number')
653
+ throw arithOperandError(a, docPath);
654
+ return -a;
655
+ };
656
+ }
657
+ return (f) => {
658
+ const a = get(f);
659
+ if (a === EMPTY)
660
+ return EMPTY;
661
+ if (typeof a !== 'number')
662
+ throw arithOperandError(a, docPath);
663
+ return -a;
664
+ };
665
+ },
666
+ },
667
+
668
+ //#endregion
669
+
670
+ //#region section 8.6 - logic
671
+
672
+ '$and': logicEntry(false),
673
+ '$or': logicEntry(true),
674
+
675
+ '$not': {
676
+ params: UNARY,
677
+ result: RESULT_ONE,
678
+ compile: (gets, args) => {
679
+ const get = gets[0];
680
+ const docPath = args[0].docPath;
681
+ return (f) => !ebv(get(f), docPath);
682
+ },
683
+ },
684
+
685
+ //#endregion
686
+
687
+ //#region section 8.7 - strings
688
+
689
+ '$concat': {
690
+ params: ARGS_0N,
691
+ result: RESULT_ONE,
692
+ compile: (gets, args) => {
693
+ if (gets.length === 0)
694
+ return () => '';
695
+ const paths = args.map((a) => a.docPath);
696
+ const flen = gets.length;
697
+ return (f) => {
698
+ let s = '';
699
+ for (let i = 0; i < flen; i++) {
700
+ const v = gets[i](f);
701
+ // an empty operand contributes '' (section 8.7)
702
+ s += v === EMPTY ? '' : castString(v, paths[i]);
703
+ }
704
+ return s;
705
+ };
706
+ },
707
+ },
708
+
709
+ '$string-join': {
710
+ params: ARGS_1_2,
711
+ result: RESULT_ONE,
712
+ compile: (gets, args) => {
713
+ const seqGet = gets[0];
714
+ const seqPath = args[0].docPath;
715
+ const sepGet = gets.length === 2 ? gets[1] : null;
716
+ const sepPath = sepGet === null ? '' : args[1].docPath;
717
+ return (f) => {
718
+ const v = seqGet(f);
719
+ const sep = sepGet === null ? '' : stringArg(sepGet(f), sepPath);
720
+ if (v === EMPTY)
721
+ return '';
722
+ if (!(v instanceof Seq)) // items cast per $string, like $concat
723
+ return castString(v, seqPath);
724
+ const items = v.items;
725
+ let s = castString(items[0], seqPath);
726
+ for (let i = 1; i < items.length; i++)
727
+ s += sep + castString(items[i], seqPath);
728
+ return s;
729
+ };
730
+ },
731
+ },
732
+
733
+ '$substring': {
734
+ params: ARGS_2_3,
735
+ result: RESULT_ONE,
736
+ compile: (gets, args) => {
737
+ const sGet = gets[0];
738
+ const sPath = args[0].docPath;
739
+ const startGet = gets[1];
740
+ const startPath = args[1].docPath;
741
+ const lenGet = gets.length === 3 ? gets[2] : null;
742
+ const lenPath = lenGet === null ? '' : args[2].docPath;
743
+ return (f) => {
744
+ const s = stringArg(sGet(f), sPath);
745
+ // F&O fn:substring bounds (fn:round = Math.round), 0-based (D6):
746
+ // code points at positions p with round(start) <= p, and
747
+ // p < round(start) + round(len) when len is given. NaN bounds
748
+ // satisfy no comparison and select nothing.
749
+ const from = Math.round(numberArg(startGet(f), startPath));
750
+ const to = lenGet === null ? Infinity : from + Math.round(numberArg(lenGet(f), lenPath));
751
+ if (from !== from || to !== to) // a NaN bound selects nothing
752
+ return '';
753
+ if (from <= 0 && to === Infinity)
754
+ return s;
755
+ let out = '';
756
+ let p = 0;
757
+ for (const ch of s) {
758
+ if (p >= to)
759
+ break;
760
+ if (p >= from)
761
+ out += ch;
762
+ p++;
763
+ }
764
+ return out;
765
+ };
766
+ },
767
+ },
768
+
769
+ '$contains': stringPairEntry((s, sub) => s.includes(sub)),
770
+ '$starts-with': stringPairEntry((s, prefix) => s.startsWith(prefix)),
771
+ '$ends-with': stringPairEntry((s, suffix) => s.endsWith(suffix)),
772
+
773
+ '$upper': stringUnaryEntry((s) => s.toUpperCase()),
774
+ '$lower': stringUnaryEntry((s) => s.toLowerCase()),
775
+ '$string-length': stringUnaryEntry(countCodePoints),
776
+ '$normalize-space': stringUnaryEntry(normalizeSpace),
777
+
778
+ '$match': regexTestEntry(true),
779
+ '$search': regexTestEntry(false),
780
+
781
+ '$replace': {
782
+ params: ARGS_3,
783
+ result: RESULT_ONE,
784
+ compile: compileReplace,
785
+ },
786
+
787
+ //#endregion
788
+
789
+ //#region section 8.8 - aggregates
790
+ // Aggregates see their operand sequence as-is: an array item is one
791
+ // item - D4 unpacking is a $for/quantifier rule, not a sequence rule.
792
+
793
+ '$count': {
794
+ params: UNARY,
795
+ result: RESULT_ONE,
796
+ compile: (gets) => {
797
+ const get = gets[0];
798
+ return (f) => itemCount(get(f));
799
+ },
800
+ },
801
+
802
+ '$sum': {
803
+ params: UNARY,
804
+ result: RESULT_ONE,
805
+ compile: (gets, args) => {
806
+ const get = gets[0];
807
+ const docPath = args[0].docPath;
808
+ return (f) => {
809
+ const v = get(f);
810
+ if (v === EMPTY) // $sum of the empty sequence is 0 (F&O)
811
+ return 0;
812
+ if (v instanceof Seq) {
813
+ const items = v.items;
814
+ let sum = 0;
815
+ for (let i = 0; i < items.length; i++)
816
+ sum += aggregateNumber(items[i], docPath);
817
+ return sum;
818
+ }
819
+ return aggregateNumber(v, docPath);
820
+ };
821
+ },
822
+ },
823
+
824
+ '$avg': {
825
+ params: UNARY,
826
+ result: resultEmptyPropagates,
827
+ compile: (gets, args) => {
828
+ const get = gets[0];
829
+ const docPath = args[0].docPath;
830
+ return (f) => {
831
+ const v = get(f);
832
+ if (v === EMPTY) // $avg of the empty sequence is empty (F&O)
833
+ return EMPTY;
834
+ if (v instanceof Seq) {
835
+ const items = v.items;
836
+ let sum = 0;
837
+ for (let i = 0; i < items.length; i++)
838
+ sum += aggregateNumber(items[i], docPath);
839
+ return sum / items.length;
840
+ }
841
+ return aggregateNumber(v, docPath);
842
+ };
843
+ },
844
+ },
845
+
846
+ '$min': minmaxEntry(false),
847
+ '$max': minmaxEntry(true),
848
+
849
+ //#endregion
850
+
851
+ //#region section 8.9 - sequence operators
852
+
853
+ '$distinct': {
854
+ params: UNARY,
855
+ result: resultOfOperand,
856
+ compile: (gets) => {
857
+ const get = gets[0];
858
+ return (f) => {
859
+ const v = get(f);
860
+ if (!(v instanceof Seq)) // zero or one item: already distinct
861
+ return v;
862
+ // deep equality via the grouping key relation (section 6.5):
863
+ // NaN is one distinct value, -0 deduplicates with 0
864
+ const items = v.items;
865
+ const seen = new Set();
866
+ const out = [];
867
+ for (let i = 0; i < items.length; i++) {
868
+ const key = stableKeyString(items[i]);
869
+ if (!seen.has(key)) {
870
+ seen.add(key);
871
+ out.push(items[i]);
872
+ }
873
+ }
874
+ return seqOf(out);
875
+ };
876
+ },
877
+ },
878
+
879
+ '$reverse': {
880
+ params: UNARY,
881
+ result: resultOfOperand,
882
+ compile: (gets) => {
883
+ const get = gets[0];
884
+ return (f) => {
885
+ const v = get(f);
886
+ if (!(v instanceof Seq))
887
+ return v;
888
+ const items = v.items;
889
+ const out = new Array(items.length);
890
+ for (let i = 0; i < items.length; i++)
891
+ out[i] = items[items.length - 1 - i];
892
+ return seqOf(out);
893
+ };
894
+ },
895
+ },
896
+
897
+ '$sort': {
898
+ params: UNARY,
899
+ result: resultOfOperand,
900
+ compile: (gets, args) => {
901
+ const get = gets[0];
902
+ const docPath = args[0].docPath;
903
+ return (f) => {
904
+ const v = get(f);
905
+ if (v === EMPTY)
906
+ return EMPTY;
907
+ if (!(v instanceof Seq)) {
908
+ const t = typeof v;
909
+ if (t !== 'number' && t !== 'string')
910
+ throw sortError(v, docPath);
911
+ return v;
912
+ }
913
+ const items = v.items;
914
+ const numeric = typeof items[0] === 'number';
915
+ const expected = numeric ? 'number' : 'string';
916
+ for (let i = 0; i < items.length; i++) {
917
+ if (typeof items[i] !== expected)
918
+ throw sortError(items[i], docPath);
919
+ }
920
+ const out = items.slice();
921
+ out.sort(numeric ? compareNumberKeys : compareCodePoints); // stable
922
+ return seqOf(out);
923
+ };
924
+ },
925
+ },
926
+
927
+ '$head': {
928
+ params: UNARY,
929
+ result: (cards) => (cards[0] === CARD_MANY ? CARD_OPT : cards[0]),
930
+ compile: (gets) => {
931
+ const get = gets[0];
932
+ return (f) => firstItem(get(f));
933
+ },
934
+ },
935
+
936
+ '$tail': {
937
+ params: UNARY,
938
+ result: (cards) => (cards[0] === CARD_MANY ? CARD_MANY : CARD_ZERO),
939
+ compile: (gets) => {
940
+ const get = gets[0];
941
+ return (f) => {
942
+ const v = get(f);
943
+ if (!(v instanceof Seq)) // zero or one item: nothing after the head
944
+ return EMPTY;
945
+ return seqOf(v.items.slice(1));
946
+ };
947
+ },
948
+ },
949
+
950
+ '$subsequence': {
951
+ params: ARGS_2_3,
952
+ result: (cards) => (cards[0] === CARD_MANY ? CARD_MANY : (cards[0] === CARD_ZERO ? CARD_ZERO : CARD_OPT)),
953
+ compile: (gets, args) => {
954
+ const seqGet = gets[0];
955
+ const startGet = gets[1];
956
+ const startPath = args[1].docPath;
957
+ const lenGet = gets.length === 3 ? gets[2] : null;
958
+ const lenPath = lenGet === null ? '' : args[2].docPath;
959
+ return (f) => {
960
+ const v = seqGet(f);
961
+ // same F&O bounds as $substring, over item positions (D6)
962
+ const from = Math.round(numberArg(startGet(f), startPath));
963
+ const to = lenGet === null ? Infinity : from + Math.round(numberArg(lenGet(f), lenPath));
964
+ if (v === EMPTY || from !== from || to !== to)
965
+ return EMPTY;
966
+ if (!(v instanceof Seq))
967
+ return (from <= 0 && to > 0) ? v : EMPTY;
968
+ const items = v.items;
969
+ const lo = from > 0 ? from : 0;
970
+ const hi = to < items.length ? to : items.length;
971
+ if (lo >= hi)
972
+ return EMPTY;
973
+ return seqOf(items.slice(lo, hi));
974
+ };
975
+ },
976
+ },
977
+
978
+ '$index-of': {
979
+ params: ARGS_2,
980
+ result: (cards) => (cards[0] === CARD_MANY ? CARD_MANY : (cards[0] === CARD_ZERO ? CARD_ZERO : CARD_OPT)),
981
+ compile: (gets, args) => {
982
+ const seqGet = gets[0];
983
+ const itemGet = gets[1];
984
+ const itemPath = args[1].docPath;
985
+ return (f) => {
986
+ const v = seqGet(f);
987
+ const target = itemGet(f);
988
+ if (target === EMPTY || target instanceof Seq)
989
+ throw runtimeError('JQ2001',
990
+ `'$index-of' takes a single search item, got ${describeItem(target)}`, itemPath);
991
+ // deep equality per D2 (the $eq relation: NaN matches nothing),
992
+ // 0-based positions (D6)
993
+ if (v === EMPTY)
994
+ return EMPTY;
995
+ if (!(v instanceof Seq))
996
+ return equalsJson(v, target) ? 0 : EMPTY;
997
+ const items = v.items;
998
+ const out = [];
999
+ for (let i = 0; i < items.length; i++) {
1000
+ if (equalsJson(items[i], target))
1001
+ out.push(i);
1002
+ }
1003
+ return seqOf(out);
1004
+ };
1005
+ },
1006
+ },
1007
+
1008
+ '$range': {
1009
+ params: ARGS_2,
1010
+ result: RESULT_MANY,
1011
+ compile: (gets, args, docPath) => {
1012
+ const fromGet = gets[0];
1013
+ const fromPath = args[0].docPath;
1014
+ const toGet = gets[1];
1015
+ const toPath = args[1].docPath;
1016
+ return (f) => {
1017
+ const a = fromGet(f);
1018
+ const b = toGet(f);
1019
+ if (a === EMPTY || b === EMPTY) // XQuery `to`: empty operand, empty range
1020
+ return EMPTY;
1021
+ if (typeof a !== 'number' || !Number.isInteger(a))
1022
+ throw rangeBoundError(a, fromPath);
1023
+ if (typeof b !== 'number' || !Number.isInteger(b))
1024
+ throw rangeBoundError(b, toPath);
1025
+ if (a > b)
1026
+ return EMPTY;
1027
+ const n = b - a + 1;
1028
+ if (n > RANGE_LIMIT)
1029
+ throw runtimeError('JQ2007', `'$range' of ${n} items exceeds the 2^32-item resource guard`, docPath);
1030
+ const out = new Array(n);
1031
+ for (let i = 0; i < n; i++)
1032
+ out[i] = a + i;
1033
+ return seqOf(out);
1034
+ };
1035
+ },
1036
+ },
1037
+
1038
+ '$get': {
1039
+ params: ARGS_2,
1040
+ result: RESULT_OPT,
1041
+ compile: (gets) => {
1042
+ const targetGet = gets[0];
1043
+ const keyGet = gets[1];
1044
+ // the dynamic counterpart of path leaves: object + string key,
1045
+ // array + integer index (negative counts from the end, like the
1046
+ // RFC 9535 index selector); anything else is simply empty
1047
+ return (f) => {
1048
+ const target = targetGet(f);
1049
+ const key = keyGet(f);
1050
+ if (target === EMPTY || target instanceof Seq || key === EMPTY || key instanceof Seq)
1051
+ return EMPTY;
1052
+ if (typeof key === 'string') {
1053
+ if (typeof target !== 'object' || target === null || Array.isArray(target))
1054
+ return EMPTY;
1055
+ return hasOwn(target, key) ? target[key] : EMPTY;
1056
+ }
1057
+ if (typeof key === 'number' && Array.isArray(target) && Number.isInteger(key)) {
1058
+ const idx = key < 0 ? target.length + key : key;
1059
+ return (idx >= 0 && idx < target.length) ? target[idx] : EMPTY;
1060
+ }
1061
+ return EMPTY;
1062
+ };
1063
+ },
1064
+ },
1065
+
1066
+ //#endregion
1067
+
1068
+ //#region section 8.10 - types and casts
1069
+
1070
+ '$is-string': isTypeEntry((v) => typeof v === 'string'),
1071
+ '$is-number': isTypeEntry((v) => typeof v === 'number'),
1072
+ '$is-boolean': isTypeEntry((v) => typeof v === 'boolean'),
1073
+ '$is-null': isTypeEntry((v) => v === null),
1074
+ '$is-array': isTypeEntry(Array.isArray),
1075
+ '$is-object': isTypeEntry((v) =>
1076
+ typeof v === 'object' && v !== null && !Array.isArray(v) && !(v instanceof Seq)),
1077
+
1078
+ '$string': {
1079
+ params: UNARY,
1080
+ result: resultEmptyPropagates,
1081
+ compile: (gets, args) => {
1082
+ const get = gets[0];
1083
+ const docPath = args[0].docPath;
1084
+ return (f) => {
1085
+ const v = get(f);
1086
+ return v === EMPTY ? EMPTY : castString(v, docPath);
1087
+ };
1088
+ },
1089
+ },
1090
+
1091
+ '$number': {
1092
+ params: UNARY,
1093
+ result: resultEmptyPropagates,
1094
+ compile: (gets, args) => {
1095
+ const get = gets[0];
1096
+ const docPath = args[0].docPath;
1097
+ return (f) => {
1098
+ const v = get(f);
1099
+ return v === EMPTY ? EMPTY : castNumber(v, docPath);
1100
+ };
1101
+ },
1102
+ },
1103
+
1104
+ '$boolean': { // the EBV as an operator: a 2+ item operand is JQ2003
1105
+ params: UNARY,
1106
+ result: RESULT_ONE,
1107
+ compile: (gets, args) => {
1108
+ const get = gets[0];
1109
+ const docPath = args[0].docPath;
1110
+ return (f) => ebv(get(f), docPath);
1111
+ },
1112
+ },
1113
+
1114
+ '$coalesce': {
1115
+ params: ARGS_1N,
1116
+ result: coalesceCard,
1117
+ compile: compileCoalesce,
1118
+ },
1119
+
1120
+ '$default': { // sugar for $coalesce of exactly two
1121
+ params: ARGS_2,
1122
+ result: coalesceCard,
1123
+ compile: compileCoalesce,
1124
+ },
1125
+
1126
+ //#endregion
1127
+
1128
+ //#region section 8.11 - schema operators
1129
+ // JSON Schema as the type system: the schema argument is a verbatim
1130
+ // literal ('schema' kind), compiled once at query compile time into the
1131
+ // predicate at `args[1].test` by the host's compileTypeTest hook
1132
+ // (JQ0008 without one). Validation is per ITEM of the operand's result
1133
+ // sequence - the schema sees items, never the sequence itself.
1134
+
1135
+ '$valid': {
1136
+ params: ARGS_EXPR_SCHEMA,
1137
+ result: RESULT_ONE,
1138
+ compile: (gets, args) => {
1139
+ const get = gets[0];
1140
+ const test = args[1].test;
1141
+ if (args[0].card === CARD_ONE)
1142
+ return (f) => test(get(f));
1143
+ return (f) => {
1144
+ const v = get(f);
1145
+ if (v === EMPTY) // vacuously true, like $every
1146
+ return true;
1147
+ if (v instanceof Seq) {
1148
+ const items = v.items;
1149
+ for (let i = 0; i < items.length; i++) {
1150
+ if (!test(items[i]))
1151
+ return false;
1152
+ }
1153
+ return true;
1154
+ }
1155
+ return test(v);
1156
+ };
1157
+ },
1158
+ },
1159
+
1160
+ '$assert': {
1161
+ params: ARGS_EXPR_SCHEMA,
1162
+ result: resultOfOperand,
1163
+ compile: (gets, args, docPath) => {
1164
+ const get = gets[0];
1165
+ const test = args[1].test;
1166
+ if (args[0].card === CARD_ONE) {
1167
+ return (f) => {
1168
+ const v = get(f);
1169
+ if (!test(v))
1170
+ throw runtimeError('JQ2008', `'$assert' failed: ${describeItem(v)} does not satisfy the schema`, docPath);
1171
+ return v;
1172
+ };
1173
+ }
1174
+ return (f) => {
1175
+ const v = get(f);
1176
+ if (v === EMPTY) // no items, nothing to fail
1177
+ return v;
1178
+ if (v instanceof Seq) {
1179
+ const items = v.items;
1180
+ for (let i = 0; i < items.length; i++) {
1181
+ if (!test(items[i]))
1182
+ throw runtimeError('JQ2008', `'$assert' failed: item ${i} (${describeItem(items[i])}) does not satisfy the schema`, docPath);
1183
+ }
1184
+ return v;
1185
+ }
1186
+ if (!test(v))
1187
+ throw runtimeError('JQ2008', `'$assert' failed: ${describeItem(v)} does not satisfy the schema`, docPath);
1188
+ return v;
1189
+ };
1190
+ },
1191
+ },
1192
+
1193
+ //#endregion
1194
+ });
1195
+
1196
+ // singleton type predicates (section 8.10): true iff the operand is one
1197
+ // item of the type - the empty sequence and 2+ item sequences are false,
1198
+ // not errors (EMPTY is a symbol and Seq is excluded per test)
1199
+ function isTypeEntry(test) {
1200
+ return {
1201
+ params: UNARY,
1202
+ result: RESULT_ONE,
1203
+ compile: (gets) => {
1204
+ const get = gets[0];
1205
+ return (f) => test(get(f));
1206
+ },
1207
+ };
1208
+ }
1209
+
1210
+ // $coalesce cardinality: operands past the first CARD_ONE one are never
1211
+ // reached, statically empty operands contribute nothing
1212
+ function coalesceCard(cards) {
1213
+ let seen = false;
1214
+ let many = false;
1215
+ let terminal = false;
1216
+ for (let i = 0; i < cards.length; i++) {
1217
+ const c = cards[i];
1218
+ if (c === CARD_ZERO)
1219
+ continue;
1220
+ seen = true;
1221
+ if (c === CARD_MANY)
1222
+ many = true;
1223
+ if (c === CARD_ONE) {
1224
+ terminal = true;
1225
+ break;
1226
+ }
1227
+ }
1228
+ if (!seen)
1229
+ return CARD_ZERO;
1230
+ if (many)
1231
+ return CARD_MANY;
1232
+ return terminal ? CARD_ONE : CARD_OPT;
1233
+ }
1234
+
1235
+ // first operand whose result is non-empty, lazily: operands after it are
1236
+ // not evaluated and cannot raise errors
1237
+ function compileCoalesce(gets) {
1238
+ if (gets.length === 1)
1239
+ return gets[0];
1240
+ const flen = gets.length;
1241
+ return (f) => {
1242
+ for (let i = 0; i < flen; i++) {
1243
+ const v = gets[i](f);
1244
+ if (v !== EMPTY)
1245
+ return v;
1246
+ }
1247
+ return EMPTY;
1248
+ };
1249
+ }
1250
+
1251
+ //#endregion
1252
+
1253
+ //#endregion