@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,1647 @@
1
+ //#region XQuery text front-end (XQUERY-FRONTEND.md)
2
+ // parseXQuery: a strict, single-pass recursive-descent parser (char-code
3
+ // level, the path.js discipline) for a defined subset of XQuery 3.1 text
4
+ // syntax, emitting Jaren JSON Query documents (QUERY-FORMAT.md). This is
5
+ // a front-end only: the JSON query document stays the canonical language
6
+ // and compileJsonQuery the only engine - the parser never evaluates
7
+ // anything and never emits an invalid document.
8
+ //
9
+ // Everything outside the subset fails with a named, positioned
10
+ // XQuerySyntaxError. The `unsupported ...` message prefixes are the
11
+ // classification signal for compliance tooling (the QT3 harness) - keep
12
+ // them stable:
13
+ //
14
+ // - `unsupported construct '<name>'` - a recognized XQuery construct
15
+ // outside the subset (instance of, path expressions, typeswitch, ...)
16
+ // - `unsupported function '<name>'` / `'<name>#<arity>'` - a function
17
+ // (or an arity of one) outside the mapping table
18
+ // - `unsupported clause order: ...` - a FLWOR clause sequence that
19
+ // cannot be expressed by mechanically nesting phrases (section below)
20
+ // - `unsupported variable name '<name>'` / `unsupported lookup index 0`
21
+ //
22
+ // 1-based/0-based rule (D6, documented in XQUERY-FRONTEND.md): positional
23
+ // *inputs* are adjusted at parse time so XQuery text means what it says
24
+ // (`?N` lookups, substring/subsequence starts, array:get indexes emit
25
+ // N-1); positional *outputs* keep the JSON format's 0-based convention
26
+ // (`at $i`, `count $c`, fn:index-of results).
27
+
28
+ //#region imports & shared tables
29
+
30
+ import {
31
+ CC_TAB,
32
+ CC_LF,
33
+ CC_CR,
34
+ CC_SPACE,
35
+ CC_BANG,
36
+ CC_DQUOTE,
37
+ CC_DOLLAR,
38
+ CC_AMP,
39
+ CC_SQUOTE,
40
+ CC_LPAREN,
41
+ CC_RPAREN,
42
+ CC_STAR,
43
+ CC_COMMA,
44
+ CC_MINUS,
45
+ CC_DOT,
46
+ CC_SLASH,
47
+ CC_COLON,
48
+ CC_LT,
49
+ CC_EQ,
50
+ CC_GT,
51
+ CC_QUESTION,
52
+ CC_AT,
53
+ CC_LBRACKET,
54
+ CC_RBRACKET,
55
+ CC_UNDERSCORE,
56
+ CC_PIPE,
57
+ isDigitCode,
58
+ } from '@jarenjs/core/scan';
59
+
60
+ const CC_HASH = 0x23;
61
+ const CC_PERCENT = 0x25;
62
+ const CC_PLUS = 0x2B;
63
+ const CC_SEMICOLON = 0x3B;
64
+ const CC_BACKTICK = 0x60;
65
+ const CC_LBRACE = 0x7B;
66
+ const CC_RBRACE = 0x7D;
67
+
68
+ const hasOwn = Object.hasOwn;
69
+
70
+ /**
71
+ * Error thrown when XQuery text is not valid for the supported subset -
72
+ * either invalid XQuery 3.1 syntax or a recognized construct outside the
73
+ * subset (mirrors JSONPathSyntaxError: `source` and `position` locate the
74
+ * offending token).
75
+ */
76
+ export class XQuerySyntaxError extends SyntaxError {
77
+ constructor(message, source, position) {
78
+ super(`Invalid XQuery: ${message} at position ${position} in '${source}'`);
79
+ this.name = 'XQuerySyntaxError';
80
+ this.source = source;
81
+ this.position = position;
82
+ }
83
+ }
84
+
85
+ // value comparisons map onto the same operators as general comparisons -
86
+ // a documented front-end approximation (singleton inputs behave
87
+ // identically; see XQUERY-FRONTEND.md)
88
+ const VALUE_COMPS = Object.freeze({
89
+ eq: '$eq', ne: '$ne', lt: '$lt', le: '$le', gt: '$gt', ge: '$ge',
90
+ });
91
+
92
+ // keywords that can never start an expression in the subset; produces a
93
+ // clearer error than the generic path-expression one
94
+ const EXPR_KEYWORDS = new Set([
95
+ 'for', 'let', 'some', 'every', 'if', 'then', 'else', 'return', 'where',
96
+ 'order', 'group', 'stable', 'count', 'satisfies', 'in', 'at', 'to',
97
+ 'and', 'or', 'div', 'idiv', 'mod', 'union', 'intersect', 'except',
98
+ 'instance', 'treat', 'castable', 'cast', 'is', 'case', 'default',
99
+ 'ascending', 'descending', 'least', 'greatest', 'collation', 'empty',
100
+ 'allowing', 'by', 'declare', 'import', 'module', 'external', 'variable',
101
+ ]);
102
+
103
+ // computed node constructor keywords (keyword followed by '{')
104
+ const NODE_CTOR_KEYWORDS = new Set([
105
+ 'element', 'attribute', 'text', 'comment', 'document', 'namespace',
106
+ 'processing-instruction',
107
+ ]);
108
+
109
+ // FLWOR clause positions in the fixed semantic order of the JSON format
110
+ // (QUERY-FORMAT.md section 6.1, D7)
111
+ const CLAUSE_SLOT = Object.freeze({
112
+ for: 0, let: 1, where: 2, groupby: 3, orderby: 4, count: 5,
113
+ });
114
+
115
+ const CLAUSE_LABEL = Object.freeze({
116
+ for: 'for', let: 'let', where: 'where', groupby: 'group by',
117
+ orderby: 'order by', count: 'count',
118
+ });
119
+
120
+ // sentinel for the '?*' wildcard lookup key
121
+ const STAR_KEY = Object.freeze({ star: true });
122
+
123
+ //#endregion
124
+
125
+ //#region function mapping table
126
+
127
+ // 1-based -> 0-based (D6) for positional *input* arguments: number
128
+ // literals fold at parse time, everything else subtracts at runtime.
129
+ // An integral shift commutes with the F&O round() the engine applies.
130
+ function toZeroBased(e) {
131
+ return typeof e === 'number' ? e - 1 : { '$sub': [e, 1] };
132
+ }
133
+
134
+ function fnUnary(op) {
135
+ return { min: 1, max: 1, emit: (args) => ({ [op]: args[0] }) };
136
+ }
137
+
138
+ function fnPair(op) {
139
+ return { min: 2, max: 2, emit: (args) => ({ [op]: args }) };
140
+ }
141
+
142
+ // substring/subsequence: F&O's 1-based start becomes 0-based (D6);
143
+ // the optional length argument is a count and passes through unchanged
144
+ function fnAdjustedStart(op) {
145
+ return {
146
+ min: 2, max: 3,
147
+ emit: (args) => {
148
+ const out = [args[0], toZeroBased(args[1])];
149
+ if (args.length === 3)
150
+ out.push(args[2]);
151
+ return { [op]: out };
152
+ },
153
+ };
154
+ }
155
+
156
+ // The supported built-in functions (bare or `fn:`-prefixed), one entry
157
+ // per name: `{ min, max, emit(args) }` over already-emitted argument
158
+ // expressions. Arities outside [min, max] - including F&O arities that
159
+ // exist but are not in the subset, like fn:sum#2 or the 0-argument
160
+ // context-item forms - fail as `unsupported function 'name#arity'`.
161
+ const FN_TABLE = Object.freeze({
162
+ 'count': fnUnary('$count'),
163
+ 'sum': fnUnary('$sum'),
164
+ 'avg': fnUnary('$avg'),
165
+ 'min': fnUnary('$min'),
166
+ 'max': fnUnary('$max'),
167
+ 'exists': fnUnary('$exists'),
168
+ 'empty': fnUnary('$empty'),
169
+ 'not': fnUnary('$not'),
170
+ 'boolean': fnUnary('$boolean'),
171
+ 'string': fnUnary('$string'),
172
+ 'number': fnUnary('$number'),
173
+ 'reverse': fnUnary('$reverse'),
174
+ 'head': fnUnary('$head'),
175
+ 'tail': fnUnary('$tail'),
176
+ 'distinct-values': fnUnary('$distinct'),
177
+ 'upper-case': fnUnary('$upper'),
178
+ 'lower-case': fnUnary('$lower'),
179
+ 'string-length': fnUnary('$string-length'),
180
+ 'normalize-space': fnUnary('$normalize-space'),
181
+ 'contains': fnPair('$contains'),
182
+ 'starts-with': fnPair('$starts-with'),
183
+ 'ends-with': fnPair('$ends-with'),
184
+ // fn:matches tests a *substring* match (F&O), which is `$search`;
185
+ // `$match` is the anchored RFC 9535 match() - see XQUERY-FRONTEND.md
186
+ 'matches': fnPair('$search'),
187
+ // fn:index-of *results* stay 0-based (D6) - the positional-output rule
188
+ 'index-of': fnPair('$index-of'),
189
+ 'concat': { min: 2, max: Infinity, emit: (args) => ({ '$concat': args }) },
190
+ 'string-join': { min: 1, max: 2, emit: (args) => ({ '$string-join': args }) },
191
+ 'replace': { min: 3, max: 3, emit: (args) => ({ '$replace': args }) },
192
+ 'substring': fnAdjustedStart('$substring'),
193
+ 'subsequence': fnAdjustedStart('$subsequence'),
194
+ // XQuery has no boolean literals; fn:true()/fn:false() are the spelling
195
+ 'true': { min: 0, max: 0, emit: () => true },
196
+ 'false': { min: 0, max: 0, emit: () => false },
197
+ });
198
+
199
+ const MAP_FN_TABLE = Object.freeze({
200
+ 'get': { min: 2, max: 2, emit: (args) => ({ '$get': args }) },
201
+ });
202
+
203
+ const ARRAY_FN_TABLE = Object.freeze({
204
+ // array:get's 1-based position becomes a 0-based (D6) $get index
205
+ 'get': { min: 2, max: 2, emit: (args) => ({ '$get': [args[0], toZeroBased(args[1])] }) },
206
+ });
207
+
208
+ //#endregion
209
+
210
+ //#region emission helpers
211
+
212
+ // Rule-2 escaping (QUERY-FORMAT.md section 3.2): a literal string that
213
+ // starts with '$' must be emitted with one extra leading '$'
214
+ function emitString(s) {
215
+ return s.charCodeAt(0) === CC_DOLLAR ? '$' + s : s;
216
+ }
217
+
218
+ // own-property assignment for emitted objects keyed by user-controlled
219
+ // names: a plain `obj[name] =` would follow the prototype chain for
220
+ // '__proto__' and silently drop the member
221
+ function setMember(obj, name, value) {
222
+ if (name === '__proto__')
223
+ Object.defineProperty(obj, name, { value, enumerable: true, writable: true, configurable: true });
224
+ else
225
+ obj[name] = value;
226
+ }
227
+
228
+ // member names foldable into RFC 9535 dot shorthand; everything else
229
+ // (NCNames with '-', '.', or non-ASCII) uses the bracketed name selector.
230
+ // NCNames can never contain a quote or backslash, so no escaping needed.
231
+ const SHORTHAND_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
232
+
233
+ const VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
234
+
235
+ // `{"$seq": []}` - the emitted form of '()' (used to fold `else ()`)
236
+ function isEmptySeqExpr(e) {
237
+ if (typeof e !== 'object' || e === null || Array.isArray(e))
238
+ return false;
239
+ const keys = Object.keys(e);
240
+ return keys.length === 1 && keys[0] === '$seq'
241
+ && Array.isArray(e.$seq) && e.$seq.length === 0;
242
+ }
243
+
244
+ //#endregion
245
+
246
+ //#region parser
247
+
248
+ function isNameFirstCode(c) {
249
+ return (c >= 0x41 && c <= 0x5A) // A-Z
250
+ || (c >= 0x61 && c <= 0x7A) // a-z
251
+ || c === CC_UNDERSCORE
252
+ || c >= 0x80; // any non-ASCII code unit
253
+ }
254
+
255
+ // XML NameChar, pragmatically: name start, digits, '-' and '.'
256
+ function isNameCharCode(c) {
257
+ return isNameFirstCode(c) || isDigitCode(c) || c === CC_MINUS || c === CC_DOT;
258
+ }
259
+
260
+ /**
261
+ * Parse XQuery 3.1 text (the supported subset, see XQUERY-FRONTEND.md)
262
+ * into a Jaren JSON Query document.
263
+ * @param {string} source - the XQuery text (e.g. `for $b in $doc?store?book?* return $b?title`)
264
+ * @returns {any} a valid query document for `compileJsonQuery` (envelope omitted)
265
+ * @throws {XQuerySyntaxError} when the text is invalid or uses a construct
266
+ * outside the subset (named `unsupported ...` messages)
267
+ */
268
+ export function parseXQuery(source) {
269
+ if (typeof source !== 'string')
270
+ throw new XQuerySyntaxError('query must be a string', String(source), 0);
271
+
272
+ const len = source.length;
273
+ let pos = 0;
274
+
275
+ function fail(message, at = pos) {
276
+ throw new XQuerySyntaxError(message, source, at);
277
+ }
278
+
279
+ function cc(at) {
280
+ return at < len ? source.charCodeAt(at) : -1;
281
+ }
282
+
283
+ // whitespace and nestable (: ... :) comments (XQuery A.2.1 ws:explicit
284
+ // does not occur in the subset, so comments are legal between any two
285
+ // tokens and are handled uniformly here)
286
+ function skipWS() {
287
+ while (pos < len) {
288
+ const c = source.charCodeAt(pos);
289
+ if (c === CC_SPACE || c === CC_TAB || c === CC_LF || c === CC_CR) {
290
+ pos++;
291
+ continue;
292
+ }
293
+ if (c === CC_LPAREN && cc(pos + 1) === CC_COLON) {
294
+ const start = pos;
295
+ pos += 2;
296
+ let depth = 1;
297
+ while (depth > 0) {
298
+ if (pos >= len)
299
+ fail('unterminated comment', start);
300
+ const d = source.charCodeAt(pos);
301
+ if (d === CC_LPAREN && cc(pos + 1) === CC_COLON) {
302
+ depth++;
303
+ pos += 2;
304
+ }
305
+ else if (d === CC_COLON && cc(pos + 1) === CC_RPAREN) {
306
+ depth--;
307
+ pos += 2;
308
+ }
309
+ else {
310
+ pos++;
311
+ }
312
+ }
313
+ continue;
314
+ }
315
+ break;
316
+ }
317
+ }
318
+
319
+ //#region scalar tokens
320
+
321
+ function parseNCName() {
322
+ const start = pos;
323
+ if (!isNameFirstCode(cc(pos)))
324
+ fail('expected a name');
325
+ while (pos < len) {
326
+ const c = source.charCodeAt(pos);
327
+ if (c >= 0xD800 && c <= 0xDFFF) {
328
+ // raw surrogates must form a well-formed pair
329
+ const d = cc(pos + 1);
330
+ if (c >= 0xDC00 || d < 0xDC00 || d > 0xDFFF)
331
+ fail('lone surrogate in name');
332
+ pos += 2;
333
+ continue;
334
+ }
335
+ if (!isNameCharCode(c))
336
+ break;
337
+ pos++;
338
+ }
339
+ return source.slice(start, pos);
340
+ }
341
+
342
+ // reads the NCName at pos when it equals `word` (consuming it),
343
+ // returning its start position; -1 otherwise (nothing consumed)
344
+ function tryKeyword(word) {
345
+ const save = pos;
346
+ skipWS();
347
+ if (isNameFirstCode(cc(pos))) {
348
+ const at = pos;
349
+ if (parseNCName() === word)
350
+ return at;
351
+ }
352
+ pos = save;
353
+ return -1;
354
+ }
355
+
356
+ function expectKeyword(word) {
357
+ skipWS();
358
+ const at = pos;
359
+ if (!isNameFirstCode(cc(pos)) || parseNCName() !== word)
360
+ fail(`expected '${word}'`, at);
361
+ }
362
+
363
+ // variable name after a consumed '$' ('$' is a delimiting terminal, so
364
+ // whitespace and comments may separate it from the name): an NCName
365
+ // restricted to the JSON format's variable lexeme [A-Za-z_][A-Za-z0-9_]*
366
+ // (JQ0003 rule) - an NCName like `foo-bar` or `a.b` is valid XQuery but
367
+ // cannot map
368
+ function parseVarName() {
369
+ skipWS();
370
+ const at = pos;
371
+ const name = parseNCName();
372
+ if (name === 'Q' && cc(pos) === CC_LBRACE)
373
+ fail("unsupported construct 'URI-qualified name'", at);
374
+ if (cc(pos) === CC_COLON && isNameFirstCode(cc(pos + 1)))
375
+ fail("unsupported construct 'namespaced variable'", at);
376
+ if (!VAR_NAME_RE.test(name))
377
+ fail(`unsupported variable name '${name}'`, at);
378
+ return name;
379
+ }
380
+
381
+ // string literal, both quote kinds, doubled-quote escape; `&` character
382
+ // and entity references are outside the subset
383
+ function parseStringLiteral() {
384
+ const quote = source.charCodeAt(pos);
385
+ const start = pos;
386
+ pos++;
387
+ let out = '';
388
+ let chunk = pos;
389
+ while (pos < len) {
390
+ const c = source.charCodeAt(pos);
391
+ if (c === quote) {
392
+ if (cc(pos + 1) === quote) { // '' / "" escape
393
+ out += source.slice(chunk, pos + 1);
394
+ pos += 2;
395
+ chunk = pos;
396
+ continue;
397
+ }
398
+ out += source.slice(chunk, pos);
399
+ pos++;
400
+ return out;
401
+ }
402
+ if (c === CC_AMP)
403
+ fail("unsupported construct 'character reference'", pos);
404
+ if (c >= 0xD800 && c <= 0xDFFF) {
405
+ const d = cc(pos + 1);
406
+ if (c >= 0xDC00 || d < 0xDC00 || d > 0xDFFF)
407
+ fail('lone surrogate in string literal');
408
+ pos += 2;
409
+ continue;
410
+ }
411
+ pos++;
412
+ }
413
+ return fail('unterminated string literal', start);
414
+ }
415
+
416
+ // IntegerLiteral | DecimalLiteral | DoubleLiteral (XQuery A.2 - unlike
417
+ // JSON: leading zeros, '1.' and '.5' are all legal; no sign). The value
418
+ // is emitted as a JSON number, so only the double's value survives.
419
+ function parseNumberLiteral() {
420
+ const start = pos;
421
+ if (cc(pos) === CC_DOT) {
422
+ pos++; // callers guarantee a digit follows
423
+ while (isDigitCode(cc(pos)))
424
+ pos++;
425
+ }
426
+ else {
427
+ while (isDigitCode(cc(pos)))
428
+ pos++;
429
+ if (cc(pos) === CC_DOT) {
430
+ pos++;
431
+ while (isDigitCode(cc(pos)))
432
+ pos++;
433
+ }
434
+ }
435
+ const e = cc(pos);
436
+ if (e === 0x65 || e === 0x45) { // e | E
437
+ pos++;
438
+ const s = cc(pos);
439
+ if (s === CC_PLUS || s === CC_MINUS)
440
+ pos++;
441
+ if (!isDigitCode(cc(pos)))
442
+ fail('expected a digit in the exponent');
443
+ while (isDigitCode(cc(pos)))
444
+ pos++;
445
+ }
446
+ // XQuery A.2.2: a numeric literal must not be followed directly by
447
+ // '.' or a name start character (e.g. `10div 3` is not `10 div 3`)
448
+ const n = cc(pos);
449
+ if (n === CC_DOT || isNameFirstCode(n))
450
+ fail('a numeric literal must be followed by a delimiter');
451
+ const value = Number(source.slice(start, pos));
452
+ // an overflowing DoubleLiteral is a valid XQuery double (INF) but has
453
+ // no JSON literal form - the parser must never emit an invalid document
454
+ if (!Number.isFinite(value))
455
+ fail("unsupported construct 'non-finite number literal'", start);
456
+ return value;
457
+ }
458
+
459
+ //#endregion
460
+
461
+ //#region expressions by precedence
462
+
463
+ // Expr ::= ExprSingle ("," ExprSingle)* - the XQuery comma is $seq
464
+ function parseExpr() {
465
+ const first = parseExprSingle();
466
+ skipWS();
467
+ if (cc(pos) !== CC_COMMA)
468
+ return first;
469
+ const items = [first];
470
+ while (cc(pos) === CC_COMMA) {
471
+ pos++;
472
+ items.push(parseExprSingle());
473
+ skipWS();
474
+ }
475
+ return { '$seq': items };
476
+ }
477
+
478
+ // ExprSingle ::= FLWORExpr | QuantifiedExpr | IfExpr | OrExpr
479
+ // (SwitchExpr, TypeswitchExpr and TryCatchExpr are recognized and
480
+ // rejected by name)
481
+ function parseExprSingle() {
482
+ skipWS();
483
+ const at = pos;
484
+ if (isNameFirstCode(cc(pos))) {
485
+ const save = pos;
486
+ const ident = parseNCName();
487
+ skipWS();
488
+ const next = cc(pos);
489
+ if (ident === 'for' || ident === 'let') {
490
+ if (next === CC_DOLLAR)
491
+ return parseFlwor(ident);
492
+ if (ident === 'for' && isNameFirstCode(next)) {
493
+ const save2 = pos;
494
+ const w = parseNCName();
495
+ pos = save2;
496
+ if (w === 'sliding' || w === 'tumbling')
497
+ fail("unsupported construct 'window clause'", at);
498
+ }
499
+ }
500
+ else if ((ident === 'some' || ident === 'every') && next === CC_DOLLAR) {
501
+ return parseQuantified(ident);
502
+ }
503
+ else if (ident === 'if' && next === CC_LPAREN) {
504
+ return parseIf();
505
+ }
506
+ else if ((ident === 'switch' || ident === 'typeswitch') && next === CC_LPAREN) {
507
+ fail(`unsupported construct '${ident} expression'`, at);
508
+ }
509
+ else if (ident === 'try' && next === CC_LBRACE) {
510
+ fail("unsupported construct 'try/catch expression'", at);
511
+ }
512
+ pos = save;
513
+ }
514
+ return parseOr();
515
+ }
516
+
517
+ function parseOr() {
518
+ const first = parseAnd();
519
+ let items = null;
520
+ for (;;) {
521
+ const save = pos;
522
+ skipWS();
523
+ if (isNameFirstCode(cc(pos)) && parseNCName() === 'or') {
524
+ if (items === null)
525
+ items = [first];
526
+ items.push(parseAnd());
527
+ continue;
528
+ }
529
+ pos = save;
530
+ return items === null ? first : { '$or': items };
531
+ }
532
+ }
533
+
534
+ function parseAnd() {
535
+ const first = parseComparison();
536
+ let items = null;
537
+ for (;;) {
538
+ const save = pos;
539
+ skipWS();
540
+ if (isNameFirstCode(cc(pos)) && parseNCName() === 'and') {
541
+ if (items === null)
542
+ items = [first];
543
+ items.push(parseComparison());
544
+ continue;
545
+ }
546
+ pos = save;
547
+ return items === null ? first : { '$and': items };
548
+ }
549
+ }
550
+
551
+ // ComparisonExpr ::= StringConcatExpr ((ValueComp | GeneralComp) StringConcatExpr)?
552
+ // Value comparisons map onto the general-comparison operators (front-end
553
+ // approximation); node comparisons (`is`, `<<`, `>>`) are unsupported.
554
+ function parseComparison() {
555
+ const left = parseStringConcat();
556
+ const save = pos;
557
+ skipWS();
558
+ const at = pos;
559
+ let op = null;
560
+ const c = cc(pos);
561
+ if (c === CC_EQ) {
562
+ if (cc(pos + 1) === CC_GT)
563
+ fail("unsupported construct 'arrow expression'", at);
564
+ pos++;
565
+ op = '$eq';
566
+ }
567
+ else if (c === CC_BANG) {
568
+ if (cc(pos + 1) !== CC_EQ)
569
+ fail("unsupported construct 'simple map operator'", at);
570
+ pos += 2;
571
+ op = '$ne';
572
+ }
573
+ else if (c === CC_LT) {
574
+ if (cc(pos + 1) === CC_LT)
575
+ fail("unsupported construct 'node comparison'", at);
576
+ if (cc(pos + 1) === CC_EQ) {
577
+ pos += 2;
578
+ op = '$le';
579
+ }
580
+ else {
581
+ pos++;
582
+ op = '$lt';
583
+ }
584
+ }
585
+ else if (c === CC_GT) {
586
+ if (cc(pos + 1) === CC_GT)
587
+ fail("unsupported construct 'node comparison'", at);
588
+ if (cc(pos + 1) === CC_EQ) {
589
+ pos += 2;
590
+ op = '$ge';
591
+ }
592
+ else {
593
+ pos++;
594
+ op = '$gt';
595
+ }
596
+ }
597
+ else if (isNameFirstCode(c)) {
598
+ const ident = parseNCName();
599
+ if (hasOwn(VALUE_COMPS, ident))
600
+ op = VALUE_COMPS[ident];
601
+ else if (ident === 'is')
602
+ fail("unsupported construct 'node comparison'", at);
603
+ else {
604
+ pos = save;
605
+ return left;
606
+ }
607
+ }
608
+ else {
609
+ pos = save;
610
+ return left;
611
+ }
612
+ skipWS();
613
+ return { [op]: [left, parseStringConcat()] };
614
+ }
615
+
616
+ // StringConcatExpr ::= RangeExpr ("||" RangeExpr)* - variadic $concat.
617
+ // A single '|' is the (unsupported) union operator.
618
+ function parseStringConcat() {
619
+ const first = parseRange();
620
+ let items = null;
621
+ for (;;) {
622
+ const save = pos;
623
+ skipWS();
624
+ if (cc(pos) === CC_PIPE) {
625
+ if (cc(pos + 1) === CC_PIPE) {
626
+ pos += 2;
627
+ if (items === null)
628
+ items = [first];
629
+ items.push(parseRange());
630
+ continue;
631
+ }
632
+ fail("unsupported construct 'union expression'");
633
+ }
634
+ pos = save;
635
+ return items === null ? first : { '$concat': items };
636
+ }
637
+ }
638
+
639
+ // RangeExpr ::= AdditiveExpr ("to" AdditiveExpr)?
640
+ function parseRange() {
641
+ const first = parseAdditive();
642
+ const save = pos;
643
+ skipWS();
644
+ if (isNameFirstCode(cc(pos)) && parseNCName() === 'to')
645
+ return { '$range': [first, parseAdditive()] };
646
+ pos = save;
647
+ return first;
648
+ }
649
+
650
+ function parseAdditive() {
651
+ let expr = parseMultiplicative();
652
+ for (;;) {
653
+ const save = pos;
654
+ skipWS();
655
+ const c = cc(pos);
656
+ if (c === CC_PLUS) {
657
+ pos++;
658
+ expr = { '$add': [expr, parseMultiplicative()] };
659
+ continue;
660
+ }
661
+ if (c === CC_MINUS) {
662
+ pos++;
663
+ expr = { '$sub': [expr, parseMultiplicative()] };
664
+ continue;
665
+ }
666
+ pos = save;
667
+ return expr;
668
+ }
669
+ }
670
+
671
+ // MultiplicativeExpr ::= UnaryExpr (("*" | "div" | "idiv" | "mod") UnaryExpr)*
672
+ // The grammar levels between multiplicative and unary (union, intersect,
673
+ // except, instance of, treat as, castable as, cast as, arrow) are all
674
+ // outside the subset and rejected here by name.
675
+ function parseMultiplicative() {
676
+ let expr = parseUnary();
677
+ for (;;) {
678
+ const save = pos;
679
+ skipWS();
680
+ const at = pos;
681
+ const c = cc(pos);
682
+ if (c === CC_STAR) {
683
+ pos++;
684
+ expr = { '$mul': [expr, parseUnary()] };
685
+ continue;
686
+ }
687
+ if (isNameFirstCode(c)) {
688
+ const ident = parseNCName();
689
+ if (ident === 'div') {
690
+ expr = { '$div': [expr, parseUnary()] };
691
+ continue;
692
+ }
693
+ if (ident === 'idiv') {
694
+ expr = { '$idiv': [expr, parseUnary()] };
695
+ continue;
696
+ }
697
+ if (ident === 'mod') {
698
+ expr = { '$mod': [expr, parseUnary()] };
699
+ continue;
700
+ }
701
+ if (ident === 'instance')
702
+ fail("unsupported construct 'instance of'", at);
703
+ if (ident === 'treat')
704
+ fail("unsupported construct 'treat as'", at);
705
+ if (ident === 'castable')
706
+ fail("unsupported construct 'castable as'", at);
707
+ if (ident === 'cast')
708
+ fail("unsupported construct 'cast as'", at);
709
+ if (ident === 'union' || ident === 'intersect' || ident === 'except')
710
+ fail(`unsupported construct '${ident} expression'`, at);
711
+ }
712
+ pos = save;
713
+ return expr;
714
+ }
715
+ }
716
+
717
+ // UnaryExpr ::= ("-" | "+")* ValueExpr. Unary '+' is a no-op in this
718
+ // mapping; each '-' emits $neg, folding into number literals directly.
719
+ function parseUnary() {
720
+ skipWS();
721
+ let c = cc(pos);
722
+ if (c === CC_MINUS || c === CC_PLUS) {
723
+ let negs = 0;
724
+ while (c === CC_MINUS || c === CC_PLUS) {
725
+ if (c === CC_MINUS)
726
+ negs++;
727
+ pos++;
728
+ skipWS();
729
+ c = cc(pos);
730
+ }
731
+ let expr = parsePostfix();
732
+ for (let i = 0; i < negs; i++)
733
+ expr = typeof expr === 'number' ? -expr : { '$neg': expr };
734
+ return expr;
735
+ }
736
+ return parsePostfix();
737
+ }
738
+
739
+ //#endregion
740
+
741
+ //#region postfix lookups & path folding
742
+
743
+ // postfix operators outside the subset, each with a named error
744
+ function checkUnsupportedPostfix(c) {
745
+ if (c === CC_LPAREN)
746
+ fail("unsupported construct 'dynamic function call'");
747
+ if (c === CC_LBRACKET)
748
+ fail("unsupported construct 'predicate'");
749
+ if (c === CC_HASH)
750
+ fail("unsupported construct 'named function reference'");
751
+ if (c === CC_SLASH)
752
+ fail("unsupported construct 'path expression'");
753
+ }
754
+
755
+ // '?' consumed: KeySpecifier ::= NCName | IntegerLiteral | "*"
756
+ // (a ParenthesizedExpr key is outside the subset)
757
+ function parseLookupKey() {
758
+ skipWS();
759
+ const c = cc(pos);
760
+ if (c === CC_STAR) {
761
+ pos++;
762
+ return STAR_KEY;
763
+ }
764
+ if (isDigitCode(c))
765
+ return { index: parseLookupIndex() };
766
+ if (isNameFirstCode(c))
767
+ return { name: parseNCName() };
768
+ if (c === CC_LPAREN)
769
+ fail("unsupported construct 'parenthesized lookup key'");
770
+ return fail('expected a lookup key');
771
+ }
772
+
773
+ function parseLookupIndex() {
774
+ const at = pos;
775
+ while (isDigitCode(cc(pos)))
776
+ pos++;
777
+ if (cc(pos) === CC_DOT || isNameFirstCode(cc(pos)))
778
+ fail('expected an integer lookup key', at);
779
+ const n = Number(source.slice(at, pos));
780
+ if (!Number.isSafeInteger(n))
781
+ fail('integer out of interoperable range', at);
782
+ if (n === 0)
783
+ fail('unsupported lookup index 0 (XQuery arrays are 1-based)', at);
784
+ return n - 1; // 1-based XQuery -> 0-based RFC 9535 (D6)
785
+ }
786
+
787
+ // PostfixExpr with the variable-rooted fold: `$b?price?1` becomes the
788
+ // path string "$b.price[0]" (per-item lookup over the variable's
789
+ // sequence, exactly the XQuery postfix-lookup rule); lookups on any
790
+ // other base map to $get, which addresses a *single* item (documented
791
+ // approximation). Unary lookup (`?name` on the context item) is
792
+ // outside the subset via parsePrimary.
793
+ function parsePostfix() {
794
+ skipWS();
795
+ if (cc(pos) === CC_DOLLAR) {
796
+ pos++;
797
+ const name = parseVarName();
798
+ let segs = '';
799
+ for (;;) {
800
+ const save = pos;
801
+ skipWS();
802
+ const c = cc(pos);
803
+ if (c === CC_QUESTION) {
804
+ pos++;
805
+ const key = parseLookupKey();
806
+ if (key === STAR_KEY)
807
+ segs += '[*]';
808
+ else if (hasOwn(key, 'index'))
809
+ segs += '[' + key.index + ']';
810
+ else
811
+ segs += SHORTHAND_RE.test(key.name) ? '.' + key.name : "['" + key.name + "']";
812
+ continue;
813
+ }
814
+ checkUnsupportedPostfix(c);
815
+ pos = save;
816
+ return '$' + name + segs;
817
+ }
818
+ }
819
+ let expr = parsePrimary();
820
+ for (;;) {
821
+ const save = pos;
822
+ skipWS();
823
+ const c = cc(pos);
824
+ if (c === CC_QUESTION) {
825
+ const at = pos;
826
+ pos++;
827
+ const key = parseLookupKey();
828
+ if (key === STAR_KEY)
829
+ fail("unsupported construct 'wildcard lookup on a non-variable expression'", at);
830
+ expr = { '$get': [expr, hasOwn(key, 'index') ? key.index : key.name] };
831
+ continue;
832
+ }
833
+ checkUnsupportedPostfix(c);
834
+ pos = save;
835
+ return expr;
836
+ }
837
+ }
838
+
839
+ //#endregion
840
+
841
+ //#region primary expressions
842
+
843
+ function parsePrimary() {
844
+ skipWS();
845
+ const at = pos;
846
+ const c = cc(pos);
847
+ if (c === CC_SQUOTE || c === CC_DQUOTE)
848
+ return emitString(parseStringLiteral());
849
+ if (isDigitCode(c))
850
+ return parseNumberLiteral();
851
+ if (c === CC_DOT) {
852
+ if (isDigitCode(cc(pos + 1)))
853
+ return parseNumberLiteral();
854
+ if (cc(pos + 1) === CC_DOT)
855
+ fail("unsupported construct 'path expression'", at);
856
+ fail("unsupported construct 'context item expression'", at);
857
+ }
858
+ if (c === CC_LPAREN)
859
+ return parseParenthesized();
860
+ if (c === CC_LBRACKET)
861
+ return parseSquareArray();
862
+ if (c === CC_QUESTION)
863
+ fail("unsupported construct 'unary lookup'", at);
864
+ if (c === CC_SLASH || c === CC_AT || c === CC_STAR)
865
+ fail("unsupported construct 'path expression'", at);
866
+ if (c === CC_LT)
867
+ fail("unsupported construct 'node constructor'", at);
868
+ if (c === CC_BACKTICK)
869
+ fail("unsupported construct 'string constructor'", at);
870
+ if (isNameFirstCode(c))
871
+ return parseNamedPrimary(at);
872
+ return fail('expected an expression');
873
+ }
874
+
875
+ function parseParenthesized() {
876
+ pos++; // consume '('
877
+ skipWS();
878
+ if (cc(pos) === CC_HASH)
879
+ fail("unsupported construct 'pragma'", pos - 1);
880
+ if (cc(pos) === CC_RPAREN) {
881
+ pos++;
882
+ return { '$seq': [] }; // '()' - the empty sequence
883
+ }
884
+ const expr = parseExpr();
885
+ skipWS();
886
+ if (cc(pos) !== CC_RPAREN)
887
+ fail("expected ')'");
888
+ pos++;
889
+ return expr;
890
+ }
891
+
892
+ // SquareArrayConstructor `[a, b]`: each member expression becomes one
893
+ // array-constructor element. The JSON array constructor flattens each
894
+ // element's sequence, so a non-singleton member deviates from XQuery's
895
+ // sequence-valued members (which JSON cannot hold) - documented.
896
+ function parseSquareArray() {
897
+ pos++; // consume '['
898
+ skipWS();
899
+ if (cc(pos) === CC_RBRACKET) {
900
+ pos++;
901
+ return [];
902
+ }
903
+ const items = [];
904
+ for (;;) {
905
+ items.push(parseExprSingle());
906
+ skipWS();
907
+ const c = cc(pos);
908
+ if (c === CC_COMMA) {
909
+ pos++;
910
+ continue;
911
+ }
912
+ if (c === CC_RBRACKET) {
913
+ pos++;
914
+ return items;
915
+ }
916
+ fail("expected ',' or ']'");
917
+ }
918
+ }
919
+
920
+ // CurlyArrayConstructor `array { E }`: the enclosed expression's
921
+ // sequence flattens into the members - exactly the JSON array
922
+ // constructor's behavior.
923
+ function parseCurlyArray() {
924
+ pos++; // consume '{'
925
+ skipWS();
926
+ if (cc(pos) === CC_RBRACE) {
927
+ pos++;
928
+ return [];
929
+ }
930
+ const items = [parseExprSingle()];
931
+ for (;;) {
932
+ skipWS();
933
+ const c = cc(pos);
934
+ if (c === CC_COMMA) {
935
+ pos++;
936
+ items.push(parseExprSingle());
937
+ continue;
938
+ }
939
+ if (c === CC_RBRACE) {
940
+ pos++;
941
+ return items;
942
+ }
943
+ fail("expected ',' or '}'");
944
+ }
945
+ }
946
+
947
+ // MapConstructor `map { K : V, ... }`: a plain-object map constructor
948
+ // when every key is a string literal without a leading '$', else the
949
+ // general `$map` form (computed keys evaluate at runtime, JQ2004).
950
+ // Static keys that can never be strings, and duplicate literal keys
951
+ // (XQDY0137), are rejected at parse time.
952
+ function parseMapConstructor() {
953
+ pos++; // consume '{'
954
+ skipWS();
955
+ if (cc(pos) === CC_RBRACE) {
956
+ pos++;
957
+ return {};
958
+ }
959
+ const keys = [];
960
+ const values = [];
961
+ const literals = []; // the literal string value per key, or null
962
+ const seen = new Set();
963
+ for (;;) {
964
+ skipWS();
965
+ const keyAt = pos;
966
+ const literal = (cc(pos) === CC_SQUOTE || cc(pos) === CC_DQUOTE);
967
+ const key = parseExprSingle();
968
+ const scalarKey = typeof key === 'number' || typeof key === 'boolean' || key === null;
969
+ if (scalarKey || Array.isArray(key))
970
+ fail("unsupported construct 'non-string map key'", keyAt);
971
+ if (literal && typeof key === 'string') {
972
+ // a quoted key that parsed to a bare string is a literal (a
973
+ // leading '$' would have been '$$'-escaped by emitString)
974
+ const raw = key.charCodeAt(0) === CC_DOLLAR ? key.slice(1) : key;
975
+ if (seen.has(raw))
976
+ fail(`duplicate map key '${raw}'`, keyAt);
977
+ seen.add(raw);
978
+ literals.push(raw);
979
+ }
980
+ else {
981
+ literals.push(null);
982
+ }
983
+ skipWS();
984
+ if (cc(pos) !== CC_COLON)
985
+ fail("expected ':'");
986
+ pos++;
987
+ keys.push(key);
988
+ values.push(parseExprSingle());
989
+ skipWS();
990
+ const c = cc(pos);
991
+ if (c === CC_COMMA) {
992
+ pos++;
993
+ continue;
994
+ }
995
+ if (c === CC_RBRACE) {
996
+ pos++;
997
+ break;
998
+ }
999
+ fail("expected ',' or '}'");
1000
+ }
1001
+ let plain = true;
1002
+ for (let i = 0; i < keys.length; i++) {
1003
+ if (literals[i] === null || literals[i].charCodeAt(0) === CC_DOLLAR) {
1004
+ plain = false;
1005
+ break;
1006
+ }
1007
+ }
1008
+ if (plain) {
1009
+ const out = {};
1010
+ for (let i = 0; i < keys.length; i++)
1011
+ setMember(out, keys[i], values[i]);
1012
+ return out;
1013
+ }
1014
+ const pairs = new Array(keys.length);
1015
+ for (let i = 0; i < keys.length; i++)
1016
+ pairs[i] = [keys[i], values[i]];
1017
+ return { '$map': pairs };
1018
+ }
1019
+
1020
+ function parseNamedPrimary(at) {
1021
+ let name = parseNCName();
1022
+ let written = name;
1023
+ let prefix = null;
1024
+ if (cc(pos) === CC_COLON && isNameFirstCode(cc(pos + 1))) {
1025
+ pos++;
1026
+ prefix = name;
1027
+ name = parseNCName();
1028
+ written = prefix + ':' + name;
1029
+ }
1030
+ skipWS();
1031
+ const next = cc(pos);
1032
+ if (next === CC_LPAREN) {
1033
+ if (prefix === null && name === 'function')
1034
+ fail("unsupported construct 'inline function expression'", at);
1035
+ pos++; // consume '('
1036
+ const args = parseArguments();
1037
+ return emitFunctionCall(prefix, name, written, args, at);
1038
+ }
1039
+ if (next === CC_HASH)
1040
+ fail("unsupported construct 'named function reference'", at);
1041
+ if (next === CC_LBRACE && prefix === null) {
1042
+ if (name === 'map')
1043
+ return parseMapConstructor();
1044
+ if (name === 'array')
1045
+ return parseCurlyArray();
1046
+ if (NODE_CTOR_KEYWORDS.has(name))
1047
+ fail("unsupported construct 'computed node constructor'", at);
1048
+ if (name === 'ordered' || name === 'unordered')
1049
+ fail(`unsupported construct '${name} expression'`, at);
1050
+ if (name === 'validate')
1051
+ fail("unsupported construct 'validate expression'", at);
1052
+ }
1053
+ if (prefix === null && name === 'function')
1054
+ fail("unsupported construct 'inline function expression'", at);
1055
+ if (prefix === null && EXPR_KEYWORDS.has(name))
1056
+ fail(`unexpected keyword '${name}'`, at);
1057
+ // a bare (or prefixed) name in expression position is an axis step
1058
+ return fail("unsupported construct 'path expression'", at);
1059
+ }
1060
+
1061
+ function parseArguments() {
1062
+ skipWS();
1063
+ if (cc(pos) === CC_RPAREN) {
1064
+ pos++;
1065
+ return [];
1066
+ }
1067
+ const args = [];
1068
+ for (;;) {
1069
+ skipWS();
1070
+ if (cc(pos) === CC_QUESTION) {
1071
+ // a bare '?' argument is a partial-application placeholder
1072
+ const at = pos;
1073
+ const save = pos;
1074
+ pos++;
1075
+ skipWS();
1076
+ const c = cc(pos);
1077
+ pos = save;
1078
+ if (c === CC_COMMA || c === CC_RPAREN)
1079
+ fail("unsupported construct 'argument placeholder'", at);
1080
+ fail("unsupported construct 'unary lookup'", at);
1081
+ }
1082
+ args.push(parseExprSingle());
1083
+ skipWS();
1084
+ const c = cc(pos);
1085
+ if (c === CC_COMMA) {
1086
+ pos++;
1087
+ continue;
1088
+ }
1089
+ if (c === CC_RPAREN) {
1090
+ pos++;
1091
+ return args;
1092
+ }
1093
+ fail("expected ',' or ')'");
1094
+ }
1095
+ }
1096
+
1097
+ function emitFunctionCall(prefix, local, written, args, at) {
1098
+ let table;
1099
+ if (prefix === null || prefix === 'fn')
1100
+ table = FN_TABLE;
1101
+ else if (prefix === 'map')
1102
+ table = MAP_FN_TABLE;
1103
+ else if (prefix === 'array')
1104
+ table = ARRAY_FN_TABLE;
1105
+ else
1106
+ return fail(`unsupported function '${written}'`, at);
1107
+ if (!hasOwn(table, local))
1108
+ return fail(`unsupported function '${written}'`, at);
1109
+ const def = table[local];
1110
+ if (args.length < def.min || args.length > def.max)
1111
+ return fail(`unsupported function '${written}#${args.length}'`, at);
1112
+ return def.emit(args);
1113
+ }
1114
+
1115
+ //#endregion
1116
+
1117
+ //#region if, quantifiers
1118
+
1119
+ // IfExpr ::= "if" "(" Expr ")" "then" ExprSingle "else" ExprSingle
1120
+ // (`else ()` folds to the two-argument $if form)
1121
+ function parseIf() {
1122
+ pos++; // consume '('
1123
+ const cond = parseExpr();
1124
+ skipWS();
1125
+ if (cc(pos) !== CC_RPAREN)
1126
+ fail("expected ')'");
1127
+ pos++;
1128
+ expectKeyword('then');
1129
+ const thenExpr = parseExprSingle();
1130
+ expectKeyword('else');
1131
+ const elseExpr = parseExprSingle();
1132
+ if (isEmptySeqExpr(elseExpr))
1133
+ return { '$if': [cond, thenExpr] };
1134
+ return { '$if': [cond, thenExpr, elseExpr] };
1135
+ }
1136
+
1137
+ // QuantifiedExpr ::= ("some" | "every") "$" VarName "in" ExprSingle
1138
+ // ("," "$" VarName "in" ExprSingle)* "satisfies" ExprSingle
1139
+ // Distinct names merge into one binding object; a repeated name starts
1140
+ // a nested quantifier phrase (XQuery shadowing; one binding object
1141
+ // cannot bind a name twice - JQ0007). Nesting quantifiers of the same
1142
+ // polarity is equivalent to one quantifier over the combined tuples.
1143
+ function parseQuantified(kw) {
1144
+ const key = kw === 'some' ? '$some' : '$every';
1145
+ const bindings = [];
1146
+ for (;;) {
1147
+ skipWS();
1148
+ if (cc(pos) !== CC_DOLLAR)
1149
+ fail(`expected '$' after '${kw}'`);
1150
+ pos++;
1151
+ const name = parseVarName();
1152
+ checkTypeDeclaration();
1153
+ expectKeyword('in');
1154
+ bindings.push([name, parseExprSingle()]);
1155
+ skipWS();
1156
+ if (cc(pos) === CC_COMMA) {
1157
+ pos++;
1158
+ continue;
1159
+ }
1160
+ break;
1161
+ }
1162
+ expectKeyword('satisfies');
1163
+ const cond = parseExprSingle();
1164
+ const groups = [];
1165
+ let group = {};
1166
+ for (let i = 0; i < bindings.length; i++) {
1167
+ const name = bindings[i][0];
1168
+ if (hasOwn(group, name)) {
1169
+ groups.push(group);
1170
+ group = {};
1171
+ }
1172
+ setMember(group, name, bindings[i][1]);
1173
+ }
1174
+ groups.push(group);
1175
+ let out = cond;
1176
+ for (let i = groups.length - 1; i >= 0; i--)
1177
+ out = { [key]: groups[i], '$satisfies': out };
1178
+ return out;
1179
+ }
1180
+
1181
+ // 'as SequenceType' declarations are outside the subset everywhere
1182
+ function checkTypeDeclaration() {
1183
+ const at = tryKeyword('as');
1184
+ if (at >= 0)
1185
+ fail("unsupported construct 'type declaration'", at);
1186
+ }
1187
+
1188
+ //#endregion
1189
+
1190
+ //#region FLWOR
1191
+
1192
+ // FLWORExpr ::= (ForClause | LetClause) IntermediateClause* ReturnClause.
1193
+ // Clauses parse into a flat list in source order; assembleFlwor packs
1194
+ // them into (possibly nested) JSON FLWOR phrases.
1195
+ function parseFlwor(firstKw) {
1196
+ const clauses = [];
1197
+ parseBindingClause(firstKw, clauses);
1198
+ for (;;) {
1199
+ skipWS();
1200
+ const at = pos;
1201
+ if (!isNameFirstCode(cc(pos)))
1202
+ fail('expected a FLWOR clause');
1203
+ const kw = parseNCName();
1204
+ if (kw === 'for' || kw === 'let') {
1205
+ requireDollar(kw, at);
1206
+ parseBindingClause(kw, clauses);
1207
+ continue;
1208
+ }
1209
+ if (kw === 'where') {
1210
+ clauses.push({ kind: 'where', expr: parseExprSingle(), at });
1211
+ continue;
1212
+ }
1213
+ if (kw === 'group') {
1214
+ expectKeyword('by');
1215
+ parseGroupByClause(clauses, at);
1216
+ continue;
1217
+ }
1218
+ if (kw === 'stable') {
1219
+ expectKeyword('order');
1220
+ expectKeyword('by');
1221
+ parseOrderByClause(clauses, at);
1222
+ continue;
1223
+ }
1224
+ if (kw === 'order') {
1225
+ expectKeyword('by');
1226
+ parseOrderByClause(clauses, at);
1227
+ continue;
1228
+ }
1229
+ if (kw === 'count') {
1230
+ skipWS();
1231
+ if (cc(pos) !== CC_DOLLAR)
1232
+ fail("expected '$' after 'count'");
1233
+ pos++;
1234
+ clauses.push({ kind: 'count', name: parseVarName(), at });
1235
+ continue;
1236
+ }
1237
+ if (kw === 'return')
1238
+ return assembleFlwor(clauses, parseExprSingle());
1239
+ return fail(`expected a FLWOR clause, got '${kw}'`, at);
1240
+ }
1241
+ }
1242
+
1243
+ function requireDollar(kw, clauseAt) {
1244
+ skipWS();
1245
+ if (cc(pos) === CC_DOLLAR)
1246
+ return;
1247
+ if (kw === 'for' && isNameFirstCode(cc(pos))) {
1248
+ const save = pos;
1249
+ const w = parseNCName();
1250
+ pos = save;
1251
+ if (w === 'sliding' || w === 'tumbling')
1252
+ fail("unsupported construct 'window clause'", clauseAt);
1253
+ }
1254
+ fail(`expected '$' after '${kw}'`);
1255
+ }
1256
+
1257
+ // ForClause / LetClause with comma-separated bindings; each binding
1258
+ // becomes one clause entry (a multi-binding clause and consecutive
1259
+ // clauses of the same kind are equivalent - both merge when packed)
1260
+ function parseBindingClause(kind, clauses) {
1261
+ for (;;) {
1262
+ skipWS();
1263
+ const at = pos;
1264
+ if (cc(pos) !== CC_DOLLAR)
1265
+ fail(`expected '$' after '${kind}'`);
1266
+ pos++;
1267
+ const name = parseVarName();
1268
+ checkTypeDeclaration();
1269
+ if (kind === 'for') {
1270
+ const allowingAt = tryKeyword('allowing');
1271
+ if (allowingAt >= 0)
1272
+ fail("unsupported construct 'allowing empty'", allowingAt);
1273
+ let atName = null;
1274
+ if (tryKeyword('at') >= 0) {
1275
+ skipWS();
1276
+ if (cc(pos) !== CC_DOLLAR)
1277
+ fail("expected '$' after 'at'");
1278
+ pos++;
1279
+ skipWS();
1280
+ const atNameAt = pos;
1281
+ atName = parseVarName();
1282
+ if (atName === name)
1283
+ fail(`duplicate variable '$${name}'`, atNameAt);
1284
+ }
1285
+ expectKeyword('in');
1286
+ clauses.push({ kind: 'for', name, atName, expr: parseExprSingle(), at });
1287
+ }
1288
+ else {
1289
+ skipWS();
1290
+ if (cc(pos) !== CC_COLON || cc(pos + 1) !== CC_EQ)
1291
+ fail("expected ':='");
1292
+ pos += 2;
1293
+ clauses.push({ kind: 'let', name, expr: parseExprSingle(), at });
1294
+ }
1295
+ skipWS();
1296
+ if (cc(pos) === CC_COMMA) {
1297
+ pos++;
1298
+ continue;
1299
+ }
1300
+ return;
1301
+ }
1302
+ }
1303
+
1304
+ // GroupByClause: only the `group by $new := expr` form maps - the JSON
1305
+ // $groupby key is always a fresh grouping variable (JQ0007 forbids
1306
+ // rebinding); the bare `group by $x` form is outside the subset
1307
+ function parseGroupByClause(clauses, at) {
1308
+ const keys = [];
1309
+ for (;;) {
1310
+ skipWS();
1311
+ const bindAt = pos;
1312
+ if (cc(pos) !== CC_DOLLAR)
1313
+ fail("expected '$' after 'group by'");
1314
+ pos++;
1315
+ const name = parseVarName();
1316
+ checkTypeDeclaration();
1317
+ skipWS();
1318
+ if (cc(pos) !== CC_COLON || cc(pos + 1) !== CC_EQ)
1319
+ fail("unsupported construct 'group by' binding without ':='", bindAt);
1320
+ pos += 2;
1321
+ const expr = parseExprSingle();
1322
+ const collationAt = tryKeyword('collation');
1323
+ if (collationAt >= 0)
1324
+ fail("unsupported construct 'collation'", collationAt);
1325
+ keys.push({ name, expr, at: bindAt });
1326
+ skipWS();
1327
+ if (cc(pos) === CC_COMMA) {
1328
+ pos++;
1329
+ continue;
1330
+ }
1331
+ break;
1332
+ }
1333
+ clauses.push({ kind: 'groupby', keys, at });
1334
+ }
1335
+
1336
+ // OrderByClause: OrderSpec ::= ExprSingle ("ascending" | "descending")?
1337
+ // ("empty" ("least" | "greatest"))? - collation is outside the subset
1338
+ function parseOrderByClause(clauses, at) {
1339
+ const specs = [];
1340
+ for (;;) {
1341
+ const key = parseExprSingle();
1342
+ let desc = false;
1343
+ let greatest = false;
1344
+ if (tryKeyword('ascending') < 0 && tryKeyword('descending') >= 0)
1345
+ desc = true;
1346
+ if (tryKeyword('empty') >= 0) {
1347
+ if (tryKeyword('greatest') >= 0)
1348
+ greatest = true;
1349
+ else if (tryKeyword('least') < 0)
1350
+ fail("expected 'least' or 'greatest'");
1351
+ }
1352
+ const collationAt = tryKeyword('collation');
1353
+ if (collationAt >= 0)
1354
+ fail("unsupported construct 'collation'", collationAt);
1355
+ specs.push({ key, desc, greatest });
1356
+ skipWS();
1357
+ if (cc(pos) === CC_COMMA) {
1358
+ pos++;
1359
+ continue;
1360
+ }
1361
+ break;
1362
+ }
1363
+ clauses.push({ kind: 'orderby', specs, at });
1364
+ }
1365
+
1366
+ // A key spec in default form is the bare key expression; explicit
1367
+ // modifiers use the {$key, $dir, $empty} object with only non-default
1368
+ // members. An array-constructor key always takes the explicit form: a
1369
+ // bare array in $orderby position reads as a list of key specs.
1370
+ function emitOrderBySpec(spec) {
1371
+ if (!spec.desc && !spec.greatest && !Array.isArray(spec.key))
1372
+ return spec.key;
1373
+ const out = { '$key': spec.key };
1374
+ if (spec.desc)
1375
+ out.$dir = 'desc';
1376
+ if (spec.greatest)
1377
+ out.$empty = 'greatest';
1378
+ return out;
1379
+ }
1380
+
1381
+ function emitOrderBy(specs) {
1382
+ if (specs.length === 1)
1383
+ return emitOrderBySpec(specs[0]);
1384
+ const out = new Array(specs.length);
1385
+ for (let i = 0; i < specs.length; i++)
1386
+ out[i] = emitOrderBySpec(specs[i]);
1387
+ return out;
1388
+ }
1389
+
1390
+ //#region clause packing
1391
+ // The JSON FLWOR phrase holds at most one clause per kind, applied in
1392
+ // the fixed semantic order $for -> $let -> $where -> $groupby ->
1393
+ // $orderby -> $count -> $return (D7). The parser packs the source
1394
+ // clause list greedily into that order and expresses everything else
1395
+ // by nesting - which is only sound for the per-tuple clauses:
1396
+ //
1397
+ // - for/let/where nest into $return freely (a nested phrase runs
1398
+ // once per surviving tuple, exactly XQuery's tuple-stream
1399
+ // semantics); a bare leading `where` nests as `$if`.
1400
+ // - group by / order by / count operate on the *whole* tuple stream.
1401
+ // Nesting them under a phrase that iterates ($for or $groupby)
1402
+ // would wrongly scope them to one tuple, so that clause order is
1403
+ // rejected: `unsupported clause order: '<kw>' after '<kw>'`.
1404
+ //
1405
+ // A name collision (XQuery shadowing, JQ0007 in one phrase) also forces
1406
+ // a split - the nested phrase then shadows, exactly XQuery semantics.
1407
+
1408
+ function assembleFlwor(clauses, ret) {
1409
+ return buildChain(clauses, 0, ret, false);
1410
+ }
1411
+
1412
+ function buildChain(clauses, i, ret, enclosingMulti) {
1413
+ if (i >= clauses.length)
1414
+ return ret;
1415
+ const c = clauses[i];
1416
+ if (c.kind === 'where') // a leading tuple filter is $if
1417
+ return { '$if': [c.expr, buildChain(clauses, i + 1, ret, enclosingMulti)] };
1418
+ if (c.kind === 'groupby' || c.kind === 'orderby' || c.kind === 'count')
1419
+ fail(`unsupported clause order: '${CLAUSE_LABEL[c.kind]}' after '${CLAUSE_LABEL[clauses[i - 1].kind]}'`, c.at);
1420
+ return buildPhrase(clauses, i, ret, enclosingMulti);
1421
+ }
1422
+
1423
+ function buildPhrase(clauses, i, ret, enclosingMulti) {
1424
+ const names = new Set();
1425
+ let forObj = null;
1426
+ let letObj = null;
1427
+ let whereExpr = null;
1428
+ let groupObj = null;
1429
+ let orderSpecs = null;
1430
+ let countName = null;
1431
+ let lastSlot = -1;
1432
+ let multi = false; // this phrase iterates: $for or $groupby present
1433
+
1434
+ for (; i < clauses.length; i++) {
1435
+ const c = clauses[i];
1436
+ const slot = CLAUSE_SLOT[c.kind];
1437
+ if (c.kind === 'for' || c.kind === 'let') {
1438
+ // equal slots merge (consecutive bindings of one kind); a slot
1439
+ // regression or a name collision closes the phrase
1440
+ if (slot < lastSlot)
1441
+ break;
1442
+ if (names.has(c.name) || (c.kind === 'for' && c.atName !== null && names.has(c.atName)))
1443
+ break;
1444
+ if (c.kind === 'for') {
1445
+ if (forObj === null)
1446
+ forObj = {};
1447
+ setMember(forObj, c.name, c.atName === null ? c.expr : { '$in': c.expr, '$at': c.atName });
1448
+ names.add(c.name);
1449
+ if (c.atName !== null)
1450
+ names.add(c.atName);
1451
+ multi = true;
1452
+ }
1453
+ else {
1454
+ if (letObj === null)
1455
+ letObj = {};
1456
+ setMember(letObj, c.name, c.expr);
1457
+ names.add(c.name);
1458
+ }
1459
+ lastSlot = slot;
1460
+ continue;
1461
+ }
1462
+ if (slot <= lastSlot)
1463
+ break;
1464
+ if (c.kind === 'where') {
1465
+ whereExpr = c.expr;
1466
+ lastSlot = slot;
1467
+ continue;
1468
+ }
1469
+ // whole-stream clauses cannot nest under an iterating phrase
1470
+ if (enclosingMulti)
1471
+ fail(`unsupported clause order: '${CLAUSE_LABEL[c.kind]}' after '${CLAUSE_LABEL[clauses[i - 1].kind]}'`, c.at);
1472
+ if (c.kind === 'groupby') {
1473
+ groupObj = {};
1474
+ for (let k = 0; k < c.keys.length; k++) {
1475
+ const g = c.keys[k];
1476
+ if (hasOwn(groupObj, g.name))
1477
+ fail(`duplicate variable '$${g.name}'`, g.at);
1478
+ if (names.has(g.name))
1479
+ fail(`unsupported construct 'group by' rebinding variable '$${g.name}'`, g.at);
1480
+ setMember(groupObj, g.name, g.expr);
1481
+ }
1482
+ for (let k = 0; k < c.keys.length; k++)
1483
+ names.add(c.keys[k].name);
1484
+ multi = true;
1485
+ lastSlot = slot;
1486
+ continue;
1487
+ }
1488
+ if (c.kind === 'orderby') {
1489
+ orderSpecs = c.specs;
1490
+ lastSlot = slot;
1491
+ continue;
1492
+ }
1493
+ // count
1494
+ if (names.has(c.name))
1495
+ fail(`unsupported construct 'count' rebinding variable '$${c.name}'`, c.at);
1496
+ countName = c.name;
1497
+ names.add(c.name);
1498
+ lastSlot = slot;
1499
+ }
1500
+
1501
+ const retExpr = i < clauses.length
1502
+ ? buildChain(clauses, i, ret, enclosingMulti || multi)
1503
+ : ret;
1504
+
1505
+ const phrase = {};
1506
+ if (forObj !== null)
1507
+ phrase.$for = forObj;
1508
+ if (letObj !== null)
1509
+ phrase.$let = letObj;
1510
+ if (whereExpr !== null)
1511
+ phrase.$where = whereExpr;
1512
+ if (groupObj !== null)
1513
+ phrase.$groupby = groupObj;
1514
+ if (orderSpecs !== null)
1515
+ phrase.$orderby = emitOrderBy(orderSpecs);
1516
+ if (countName !== null)
1517
+ phrase.$count = countName;
1518
+ phrase.$return = retExpr;
1519
+ return phrase;
1520
+ }
1521
+
1522
+ //#endregion
1523
+
1524
+ //#endregion
1525
+
1526
+ //#region prolog
1527
+
1528
+ const declared = new Set();
1529
+
1530
+ // `xquery version "..." (encoding "...")? ;` - parsed, ignored
1531
+ function parseVersionDecl() {
1532
+ const save = pos;
1533
+ if (tryKeyword('xquery') < 0)
1534
+ return;
1535
+ skipWS();
1536
+ if (!isNameFirstCode(cc(pos))) {
1537
+ pos = save;
1538
+ return;
1539
+ }
1540
+ const w = parseNCName();
1541
+ if (w !== 'version' && w !== 'encoding') {
1542
+ pos = save;
1543
+ return;
1544
+ }
1545
+ skipWS();
1546
+ if (cc(pos) !== CC_SQUOTE && cc(pos) !== CC_DQUOTE)
1547
+ fail(`expected a string literal after '${w}'`);
1548
+ parseStringLiteral();
1549
+ if (w === 'version') {
1550
+ const save2 = pos;
1551
+ if (tryKeyword('encoding') >= 0) {
1552
+ skipWS();
1553
+ if (cc(pos) !== CC_SQUOTE && cc(pos) !== CC_DQUOTE)
1554
+ fail("expected a string literal after 'encoding'");
1555
+ parseStringLiteral();
1556
+ }
1557
+ else {
1558
+ pos = save2;
1559
+ }
1560
+ }
1561
+ skipWS();
1562
+ if (cc(pos) !== CC_SEMICOLON)
1563
+ fail("expected ';'");
1564
+ pos++;
1565
+ }
1566
+
1567
+ // `declare variable $name external;` - validated and ignored: in the
1568
+ // JSON format use is the declaration (spec section 9); the engine
1569
+ // collects free variables as externals. Everything else in a prolog is
1570
+ // outside the subset, each with a named error.
1571
+ function parseVariableDecl(declAt) {
1572
+ skipWS();
1573
+ if (cc(pos) !== CC_DOLLAR)
1574
+ fail("expected '$' after 'declare variable'");
1575
+ pos++;
1576
+ skipWS();
1577
+ const nameAt = pos;
1578
+ const name = parseVarName();
1579
+ if (declared.has(name))
1580
+ fail(`duplicate variable declaration '$${name}'`, nameAt);
1581
+ checkTypeDeclaration();
1582
+ skipWS();
1583
+ if (cc(pos) === CC_COLON && cc(pos + 1) === CC_EQ)
1584
+ fail("unsupported construct 'variable declaration with default value'", declAt);
1585
+ expectKeyword('external');
1586
+ skipWS();
1587
+ if (cc(pos) === CC_COLON && cc(pos + 1) === CC_EQ)
1588
+ fail("unsupported construct 'variable declaration with default value'", declAt);
1589
+ if (cc(pos) !== CC_SEMICOLON)
1590
+ fail("expected ';'");
1591
+ pos++;
1592
+ declared.add(name);
1593
+ }
1594
+
1595
+ function parseProlog() {
1596
+ parseVersionDecl();
1597
+ for (;;) {
1598
+ const save = pos;
1599
+ skipWS();
1600
+ const at = pos;
1601
+ if (!isNameFirstCode(cc(pos))) {
1602
+ pos = save;
1603
+ return;
1604
+ }
1605
+ const w = parseNCName();
1606
+ if (w === 'declare') {
1607
+ skipWS();
1608
+ if (cc(pos) === CC_PERCENT)
1609
+ fail("unsupported construct 'annotation'");
1610
+ if (!isNameFirstCode(cc(pos)))
1611
+ fail('expected a declaration keyword');
1612
+ const kind = parseNCName();
1613
+ if (kind === 'variable') {
1614
+ parseVariableDecl(at);
1615
+ continue;
1616
+ }
1617
+ fail(`unsupported construct 'declare ${kind}'`, at);
1618
+ }
1619
+ if (w === 'import') {
1620
+ skipWS();
1621
+ const kind = isNameFirstCode(cc(pos)) ? parseNCName() : 'declaration';
1622
+ fail(`unsupported construct 'import ${kind}'`, at);
1623
+ }
1624
+ if (w === 'module')
1625
+ fail("unsupported construct 'library module'", at);
1626
+ pos = save;
1627
+ return;
1628
+ }
1629
+ }
1630
+
1631
+ //#endregion
1632
+
1633
+ // Module ::= VersionDecl? Prolog QueryBody
1634
+ parseProlog();
1635
+ skipWS();
1636
+ if (pos >= len)
1637
+ fail('empty query', 0);
1638
+ const body = parseExpr();
1639
+ skipWS();
1640
+ if (pos !== len)
1641
+ fail('unexpected token');
1642
+ return body;
1643
+ }
1644
+
1645
+ //#endregion
1646
+
1647
+ //#endregion