@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.
- package/ARCHITECTURE.md +175 -0
- package/LICENSE +21 -0
- package/README.md +471 -0
- package/dist/types/basic.d.ts +32 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/jslt/dispatch.d.ts +11 -0
- package/dist/types/jslt/errors.d.ts +18 -0
- package/dist/types/jslt/index.d.ts +53 -0
- package/dist/types/jslt/stylesheet.d.ts +8 -0
- package/dist/types/jtlt/desugar.d.ts +19 -0
- package/dist/types/jtlt/errors.d.ts +18 -0
- package/dist/types/jtlt/index.d.ts +57 -0
- package/dist/types/jtlt/template.d.ts +8 -0
- package/dist/types/jtlt/writer.d.ts +6 -0
- package/dist/types/path.d.ts +235 -0
- package/dist/types/pointer.d.ts +114 -0
- package/dist/types/query/compile.d.ts +21 -0
- package/dist/types/query/errors.d.ts +18 -0
- package/dist/types/query/index.d.ts +70 -0
- package/dist/types/query/normalize.d.ts +68 -0
- package/dist/types/query/operators.d.ts +424 -0
- package/dist/types/query/runtime.d.ts +93 -0
- package/dist/types/segments.d.ts +62 -0
- package/dist/types/xquery/index.d.ts +19 -0
- package/dist/types/xquery/parse.d.ts +20 -0
- package/docs/JSLT-FORMAT.md +861 -0
- package/docs/JSLT-PRELUDE.md +159 -0
- package/docs/JTLT-FORMAT.md +659 -0
- package/docs/QUERY-FORMAT.md +1221 -0
- package/docs/XQUERY-FRONTEND.md +321 -0
- package/package.json +81 -0
- package/schemas/jaren-jslt.draft-07.schema.json +776 -0
- package/schemas/jaren-jslt.schema.json +776 -0
- package/schemas/jaren-query.draft-07.schema.json +613 -0
- package/schemas/jaren-query.schema.json +375 -0
- package/src/basic.js +300 -0
- package/src/index.js +4 -0
- package/src/jslt/dispatch.js +934 -0
- package/src/jslt/errors.js +34 -0
- package/src/jslt/index.js +121 -0
- package/src/jslt/stylesheet.js +234 -0
- package/src/jtlt/desugar.js +231 -0
- package/src/jtlt/errors.js +34 -0
- package/src/jtlt/index.js +155 -0
- package/src/jtlt/template.js +130 -0
- package/src/jtlt/writer.js +110 -0
- package/src/path.js +977 -0
- package/src/pointer.js +453 -0
- package/src/query/compile.js +817 -0
- package/src/query/errors.js +33 -0
- package/src/query/index.js +150 -0
- package/src/query/normalize.js +1047 -0
- package/src/query/operators.js +1253 -0
- package/src/query/runtime.js +233 -0
- package/src/segments.js +627 -0
- package/src/xquery/index.js +35 -0
- 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
|