@omgbase/oqx 0.15.0 → 0.18.0
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/CHANGELOG.md +36 -0
- package/README.md +170 -17
- package/dist/ast.d.ts +76 -22
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +21 -6
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +37 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +54 -0
- package/dist/build.js.map +1 -0
- package/dist/context.d.ts +13 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +7 -3
- package/dist/context.js.map +1 -1
- package/dist/engine.d.ts +1 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +52 -23
- package/dist/engine.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +19 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -12
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts +15 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +104 -51
- package/dist/lexer.js.map +1 -1
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +5 -3
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/optimize/ir.d.ts +2 -2
- package/dist/optimize/ir.d.ts.map +1 -1
- package/dist/optimize/ir.js +3 -3
- package/dist/optimize/ir.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +616 -251
- package/dist/parser.js.map +1 -1
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +4 -2
- package/dist/plan.js.map +1 -1
- package/dist/print.d.ts +17 -0
- package/dist/print.d.ts.map +1 -0
- package/dist/print.js +266 -0
- package/dist/print.js.map +1 -0
- package/dist/resolve.d.ts +23 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +330 -0
- package/dist/resolve.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/walk.d.ts +54 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +139 -0
- package/dist/walk.js.map +1 -0
- package/package.json +1 -1
- package/src/ast.ts +104 -35
- package/src/build.ts +72 -0
- package/src/context.ts +16 -3
- package/src/engine.ts +55 -26
- package/src/errors.ts +3 -2
- package/src/index.ts +38 -13
- package/src/lexer.ts +102 -51
- package/src/optimize/analysis.ts +4 -3
- package/src/optimize/ir.ts +4 -4
- package/src/parser.ts +573 -225
- package/src/plan.ts +3 -2
- package/src/print.ts +274 -0
- package/src/resolve.ts +305 -0
- package/src/version.ts +4 -0
- package/src/walk.ts +180 -0
package/dist/parser.js
CHANGED
|
@@ -25,12 +25,37 @@
|
|
|
25
25
|
// receiver-plus-consumer form `<receiver> <consumer> { block }` supplies the
|
|
26
26
|
// source itself, so its block's `from` is an optional re-projection). Only
|
|
27
27
|
// `select` may drop its keyword, and only when it is the first clause written
|
|
28
|
-
// (`name, age from people`); every other clause always carries its keyword
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// body
|
|
32
|
-
//
|
|
33
|
-
//
|
|
28
|
+
// (`name, age from people`); every other clause always carries its keyword. A
|
|
29
|
+
// BLOCK may instead lead with a predicate (since 0.17): a leading expression that
|
|
30
|
+
// is syntactically a predicate — not a bare name, a dotted navigation or a lift —
|
|
31
|
+
// opens a `where`-first body (`people exists { age > 50 }`); at the top level a
|
|
32
|
+
// leading predicate is still an error, since `where` would precede `from`. An
|
|
33
|
+
// out-of-order clause is a parse error naming the order.
|
|
34
|
+
//
|
|
35
|
+
// Sugar (0.17) desugars HERE, to existing nodes: `x { … }` is `x collect { … }`
|
|
36
|
+
// (not in `follow` destination position, where a brace is the options block);
|
|
37
|
+
// `x[p]` is `x first { where p }`, `x[n]` is `x first { offset n }`, with a
|
|
38
|
+
// trailing `!` making the first a `single` (predicate) or a `required` (index);
|
|
39
|
+
// `is x` is `!!x`, `not x` is `!x`. Two shapes are new: the postfix `required`
|
|
40
|
+
// (`x!`, tightest) and the identity operators `is` / `is not` (comparison
|
|
41
|
+
// level). A directive may stand in value position (`jobs first { }.pay`), which
|
|
42
|
+
// is how the canonical printer writes a bracket chain back. `where` may reference the same
|
|
43
|
+
// body's `select` aliases: the parser VALIDATES those references (a cycle or a
|
|
44
|
+
// block alias inside an expression is a parse error) but keeps the surface
|
|
45
|
+
// form; `resolveAliases` (resolve.ts) substitutes them before evaluation.
|
|
46
|
+
//
|
|
47
|
+
// Absolute scope references (0.18) desugar HERE too: `N^name` — an unsigned
|
|
48
|
+
// integer literal immediately followed by `^` — is the `outer` reference (or the
|
|
49
|
+
// lift) reaching scope N, i.e. `levels = depth − N`, where `depth` is the
|
|
50
|
+
// syntactic scope depth of the position being parsed (SEMANTICS §2: the root is
|
|
51
|
+
// 0, a top-level row 1, a block's body one deeper than its receiver, a follow
|
|
52
|
+
// `where` one deeper than the frontier row). The parser tracks that depth
|
|
53
|
+
// (`this.depth`) exactly as the `visit` walk computes it; `N >= depth` is a
|
|
54
|
+
// parse error. There is no new node: `print` writes the carets back.
|
|
55
|
+
//
|
|
56
|
+
// Every node carries its `span` — `[start, end)` in code points over the raw
|
|
57
|
+
// source, from the first token that produced it to the end of the last; a
|
|
58
|
+
// parenthesized operand's span includes its parentheses (spec/oqx/AST.md).
|
|
34
59
|
//
|
|
35
60
|
// Principle of least surprise, applied to the grammar: a rule a careful user
|
|
36
61
|
// would not predict is a bug. Hence `!` binds tighter than comparison in every
|
|
@@ -42,6 +67,8 @@
|
|
|
42
67
|
// misreads.
|
|
43
68
|
import { lexTemplate, lexString } from "./lexer.js";
|
|
44
69
|
import { OqxError } from "./errors.js";
|
|
70
|
+
import { resolveSelect, resolveWhere } from "./resolve.js";
|
|
71
|
+
import { printTemplate } from "./print.js";
|
|
45
72
|
const CONSUMERS = new Set(["collect", "exists", "none", "count", "first", "single"]);
|
|
46
73
|
// The fixed clause order of a body (ADR-020). Each clause appears at most once.
|
|
47
74
|
const CLAUSE_ORDER = ["select", "from", "where", "follow", "order by", "limit", "offset"];
|
|
@@ -68,12 +95,38 @@ export function parseTemplate(fragments, values) {
|
|
|
68
95
|
export function parseString(src) {
|
|
69
96
|
return new Parser(lexString(src)).parseQuery();
|
|
70
97
|
}
|
|
98
|
+
// A copy of a node with another span (a parenthesized operand takes the span of
|
|
99
|
+
// its parentheses).
|
|
100
|
+
function withSpan(node, span) {
|
|
101
|
+
return { ...node, span };
|
|
102
|
+
}
|
|
71
103
|
class Parser {
|
|
72
104
|
tokens;
|
|
73
105
|
pos = 0;
|
|
106
|
+
/** While a block's leading run is read speculatively as a predicate, a bare
|
|
107
|
+
* `first`/`single` leaf is not rejected on sight: the body decides after the
|
|
108
|
+
* run is classified (`rejectBareLookups`). */
|
|
109
|
+
deferLookup = false;
|
|
110
|
+
/** The scope depth (SEMANTICS §2) at which the construct being parsed is
|
|
111
|
+
* evaluated: 0 at the root (the top-level source and bounds), 1 for a
|
|
112
|
+
* top-level row's clauses, one deeper per block body and per follow `where`.
|
|
113
|
+
* An absolute reference `N^name` reads `depth − N` scopes out. */
|
|
114
|
+
depth = 0;
|
|
74
115
|
constructor(tokens) {
|
|
75
116
|
this.tokens = tokens;
|
|
76
117
|
}
|
|
118
|
+
/** Run `f` with the scope depth set to `d`, restoring it afterwards (also on
|
|
119
|
+
* the error path, so a speculative read that fails leaves the depth intact). */
|
|
120
|
+
atDepth(d, f) {
|
|
121
|
+
const saved = this.depth;
|
|
122
|
+
this.depth = d;
|
|
123
|
+
try {
|
|
124
|
+
return f();
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
this.depth = saved;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
77
130
|
// ---- cursor helpers -------------------------------------------------------
|
|
78
131
|
peek() { return this.tokens[this.pos]; }
|
|
79
132
|
peekAt(n) { return this.tokens[this.pos + n]; }
|
|
@@ -89,26 +142,54 @@ class Parser {
|
|
|
89
142
|
fail(msg) {
|
|
90
143
|
throw new OqxError(`${msg} (at offset ${this.peek().pos})`, "parse");
|
|
91
144
|
}
|
|
145
|
+
/** Code-point offset of the next token (the start of a node about to be parsed). */
|
|
146
|
+
start() { return this.peek().pos; }
|
|
147
|
+
/** Code-point offset just past the last consumed token (the end of a node just parsed). */
|
|
148
|
+
end() { return this.pos === 0 ? 0 : this.tokens[this.pos - 1].end; }
|
|
149
|
+
sp(start) { return [start, this.end()]; }
|
|
92
150
|
// ---- top level ------------------------------------------------------------
|
|
93
151
|
parseQuery() {
|
|
152
|
+
const start = this.start();
|
|
94
153
|
// Directive form: `<receiver> <consumer> { … }` consuming the whole query.
|
|
95
|
-
|
|
96
|
-
if (directive && this.at("eof")) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
154
|
+
let directive = this.tryOp(false);
|
|
155
|
+
if (directive && !this.at("eof") && (this.at("lbracket") || this.atValueDirective())) {
|
|
156
|
+
// A longer chain whose outermost node is still a directive
|
|
157
|
+
// (`people first { } first { }` — how `people[p][0]` prints back).
|
|
158
|
+
const save = this.pos;
|
|
159
|
+
try {
|
|
160
|
+
const e = this.parsePostfix(directive);
|
|
161
|
+
if (e.kind === "op" && this.at("eof"))
|
|
162
|
+
directive = e;
|
|
163
|
+
else
|
|
164
|
+
this.pos = save;
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
if (!(err instanceof OqxError))
|
|
168
|
+
throw err;
|
|
169
|
+
this.pos = save;
|
|
170
|
+
}
|
|
109
171
|
}
|
|
172
|
+
if (directive && this.at("eof"))
|
|
173
|
+
return this.queryFromOp(directive, start);
|
|
110
174
|
if (directive)
|
|
111
175
|
this.fail(`unexpected ${this.tokDesc()} after the top-level directive`);
|
|
176
|
+
// A bracket chain whose outermost node is a directive is the whole query too
|
|
177
|
+
// (`people[age > 40]` ≡ `people first { where age > 40 }`). Read
|
|
178
|
+
// speculatively: anything else (a parse error, a longer expression) is the
|
|
179
|
+
// body form's business.
|
|
180
|
+
if (this.at("ident") || this.at("caret") || this.at("binding")) {
|
|
181
|
+
const save = this.pos;
|
|
182
|
+
try {
|
|
183
|
+
const e = this.parsePostfix();
|
|
184
|
+
if (e.kind === "op" && this.at("eof"))
|
|
185
|
+
return this.queryFromOp(e, start);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
if (!(err instanceof OqxError))
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
this.pos = save;
|
|
192
|
+
}
|
|
112
193
|
// Body form: the fixed clause list; its `from` is the source.
|
|
113
194
|
const body = this.parseBody(TOP_CTX);
|
|
114
195
|
if (!this.at("eof"))
|
|
@@ -117,6 +198,8 @@ class Parser {
|
|
|
117
198
|
this.fail("a query must name its source with `from <collection>` (or be `<collection> <consumer> { … }`)");
|
|
118
199
|
}
|
|
119
200
|
return {
|
|
201
|
+
kind: "query",
|
|
202
|
+
span: this.sp(start),
|
|
120
203
|
source: body.froms[0],
|
|
121
204
|
from: body.froms.slice(1),
|
|
122
205
|
where: body.where,
|
|
@@ -126,7 +209,17 @@ class Parser {
|
|
|
126
209
|
follow: body.follow,
|
|
127
210
|
distinct: body.distinct,
|
|
128
211
|
values: body.values,
|
|
129
|
-
|
|
212
|
+
limit: body.limit,
|
|
213
|
+
offset: body.offset,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// The whole-query form of a directive: its receiver is the source, its block
|
|
217
|
+
// the body, its consumer the query's.
|
|
218
|
+
queryFromOp(e, start) {
|
|
219
|
+
return {
|
|
220
|
+
kind: "query", span: this.sp(start), source: e.receiver, from: e.sub.from, where: e.sub.where, select: e.sub.select,
|
|
221
|
+
orderBy: e.sub.orderBy, consumer: e.op, follow: e.sub.follow, distinct: e.distinct, values: e.sub.values,
|
|
222
|
+
limit: e.sub.limit, offset: e.sub.offset,
|
|
130
223
|
};
|
|
131
224
|
}
|
|
132
225
|
tokDesc() {
|
|
@@ -149,6 +242,10 @@ class Parser {
|
|
|
149
242
|
let limit = null;
|
|
150
243
|
let offset = null;
|
|
151
244
|
let stage = -1;
|
|
245
|
+
// A top-level body's row clauses (select/where/follow/order by) are read at
|
|
246
|
+
// depth 1, its source and bounds at the root (0); a block's clauses are all
|
|
247
|
+
// at the block's depth, which `parseConsumerBlock` set.
|
|
248
|
+
const rowDepth = ctx.top ? 1 : this.depth;
|
|
152
249
|
const enter = (clause) => {
|
|
153
250
|
const idx = CLAUSE_ORDER.indexOf(clause);
|
|
154
251
|
if (idx === stage)
|
|
@@ -166,7 +263,7 @@ class Parser {
|
|
|
166
263
|
this.next();
|
|
167
264
|
distinct = true;
|
|
168
265
|
}
|
|
169
|
-
({ items: select, values } = this.parseProjection(ctx));
|
|
266
|
+
({ items: select, values } = this.atDepth(rowDepth, () => this.parseProjection(ctx)));
|
|
170
267
|
continue;
|
|
171
268
|
}
|
|
172
269
|
if (this.at("kw", "from")) {
|
|
@@ -178,19 +275,19 @@ class Parser {
|
|
|
178
275
|
if (this.at("kw", "where")) {
|
|
179
276
|
enter("where");
|
|
180
277
|
this.next();
|
|
181
|
-
where = this.parseWhere();
|
|
278
|
+
where = this.atDepth(rowDepth, () => this.parseWhere());
|
|
182
279
|
continue;
|
|
183
280
|
}
|
|
184
281
|
if (this.atFollow()) {
|
|
185
282
|
enter("follow");
|
|
186
|
-
follow = this.parseFollow();
|
|
283
|
+
follow = this.atDepth(rowDepth, () => this.parseFollow());
|
|
187
284
|
continue;
|
|
188
285
|
}
|
|
189
286
|
if (this.atOrderBy()) {
|
|
190
287
|
enter("order by");
|
|
191
288
|
this.next();
|
|
192
289
|
this.next(); // `order` `by`
|
|
193
|
-
orderBy = this.parseOrderSpecs();
|
|
290
|
+
orderBy = this.atDepth(rowDepth, () => this.parseOrderSpecs());
|
|
194
291
|
continue;
|
|
195
292
|
}
|
|
196
293
|
if (this.atBound()) {
|
|
@@ -210,18 +307,81 @@ class Parser {
|
|
|
210
307
|
continue;
|
|
211
308
|
}
|
|
212
309
|
// A keyword-less run. Before any clause it is the projection (`select` is
|
|
213
|
-
// the one keyword that may be dropped, and only in first position)
|
|
214
|
-
//
|
|
310
|
+
// the one keyword that may be dropped, and only in first position) — or,
|
|
311
|
+
// in a block, a leading predicate that opens a `where`-first body. After
|
|
312
|
+
// any clause it is an error.
|
|
215
313
|
if (stage === -1 && (this.at("ident") || this.at("binding") || this.at("caret") || this.canStartValue())) {
|
|
216
|
-
|
|
217
|
-
(
|
|
314
|
+
const lead = this.atDepth(rowDepth, () => this.parseLeadingRun(ctx));
|
|
315
|
+
if (lead.where) {
|
|
316
|
+
enter("where");
|
|
317
|
+
where = lead.where;
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
enter("select");
|
|
321
|
+
({ items: select, values } = lead.projection);
|
|
322
|
+
}
|
|
218
323
|
continue;
|
|
219
324
|
}
|
|
220
325
|
this.failUnexpectedInBody(stage, ctx);
|
|
221
326
|
}
|
|
222
|
-
|
|
327
|
+
// Validate the alias references of this body (cycles, a block alias inside a
|
|
328
|
+
// `where` expression, a `select` item naming an item to its right); the
|
|
329
|
+
// surface form is kept. `where` first: its cycle report follows its own path.
|
|
330
|
+
if (where)
|
|
331
|
+
resolveWhere(select, where, (msg) => this.fail(msg));
|
|
332
|
+
resolveSelect(select, (msg) => this.fail(msg));
|
|
223
333
|
return { froms, where, select, orderBy, follow, distinct, values, limit, offset };
|
|
224
334
|
}
|
|
335
|
+
// The keyword-less run that opens a body. At the top level it is always the
|
|
336
|
+
// projection. In a block it is a `where`-first body when the leading expression
|
|
337
|
+
// is SYNTACTICALLY a predicate (GRAMMAR §2): the run is read as a where tree
|
|
338
|
+
// first; if that tree is predicate-shaped — anything but a bare name, a dotted
|
|
339
|
+
// navigation or a lift, with a parenthesized expression counting as a
|
|
340
|
+
// predicate — and is not the item of a `values` projection, it is the `where`.
|
|
341
|
+
// Otherwise the tokens are re-read as the projection. When both readings fail,
|
|
342
|
+
// the error that got further wins (the farthest-failure rule), so a mistake
|
|
343
|
+
// deep inside `{ jobs exists { … } }` is reported where it is.
|
|
344
|
+
parseLeadingRun(ctx) {
|
|
345
|
+
if (!ctx.top) {
|
|
346
|
+
const save = this.pos;
|
|
347
|
+
const first = this.peek();
|
|
348
|
+
let w = null;
|
|
349
|
+
let specErr = null;
|
|
350
|
+
let specPos = -1;
|
|
351
|
+
this.deferLookup = true;
|
|
352
|
+
try {
|
|
353
|
+
w = this.parseWhere();
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
if (!(e instanceof OqxError))
|
|
357
|
+
throw e;
|
|
358
|
+
specErr = e;
|
|
359
|
+
specPos = this.pos;
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
this.deferLookup = false;
|
|
363
|
+
}
|
|
364
|
+
if (w && predicateShaped(w, first) && !this.at("ident", "values")) {
|
|
365
|
+
if (this.at("comma")) {
|
|
366
|
+
this.fail(`a leading predicate starts a \`where\`-first body, which has no projection — write the projection first: \`{ <projection> where ${describe(w)} }\``);
|
|
367
|
+
}
|
|
368
|
+
this.rejectBareLookups(w);
|
|
369
|
+
return { where: w, projection: null };
|
|
370
|
+
}
|
|
371
|
+
this.pos = save;
|
|
372
|
+
if (specErr) {
|
|
373
|
+
try {
|
|
374
|
+
return { where: null, projection: this.parseProjection(ctx) };
|
|
375
|
+
}
|
|
376
|
+
catch (e) {
|
|
377
|
+
if (e instanceof OqxError && this.pos < specPos)
|
|
378
|
+
throw specErr;
|
|
379
|
+
throw e;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return { where: null, projection: this.parseProjection(ctx) };
|
|
384
|
+
}
|
|
225
385
|
// The error for a token that starts no clause, phrased for the mistake it most
|
|
226
386
|
// likely is: a bare run right after `from` (the old implicit `where`, or a
|
|
227
387
|
// consumer word where a whole-query directive was meant), or a stray token.
|
|
@@ -255,10 +415,10 @@ class Parser {
|
|
|
255
415
|
if (last === "from") {
|
|
256
416
|
this.fail(`unexpected ${this.tokDesc()} after \`from\` — a predicate needs \`where\` (there is no implicit where), and a projection goes before \`from\` (\`select … from …\`); expected ${remaining}`);
|
|
257
417
|
}
|
|
258
|
-
// `{ rel exists { … } }
|
|
259
|
-
//
|
|
418
|
+
// `{ name, rel exists { … } }`: the leading items were read as the projection,
|
|
419
|
+
// so the consumer word is where the mistake shows.
|
|
260
420
|
if (last === "select" && t.type === "ident" && CONSUMERS.has(t.value)) {
|
|
261
|
-
this.fail(`unexpected \`${t.value}\` after a projection — a consumer test is a predicate: write \`where <relation> ${t.value} { … }\`
|
|
421
|
+
this.fail(`unexpected \`${t.value}\` after a projection — a consumer test is a predicate: write \`where <relation> ${t.value} { … }\` after the projection (a block may lead with a predicate only when nothing is projected); a nested block in a projection needs a name (\`name: <relation> collect { … }\`)`);
|
|
262
422
|
}
|
|
263
423
|
if (last === null)
|
|
264
424
|
this.fail(`unexpected ${this.tokDesc()} — expected a projection or ${remaining}`);
|
|
@@ -274,83 +434,37 @@ class Parser {
|
|
|
274
434
|
const nx = this.peekAt(1);
|
|
275
435
|
return !!nx && (nx.type === "number" || nx.type === "binding" || nx.type === "caret");
|
|
276
436
|
}
|
|
277
|
-
//
|
|
278
|
-
// `
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
if (aliases.size === 0)
|
|
304
|
-
return where;
|
|
305
|
-
const resolving = [];
|
|
306
|
-
const subst = (e) => {
|
|
307
|
-
switch (e.kind) {
|
|
308
|
-
case "ident": {
|
|
309
|
-
const a = aliases.get(e.name);
|
|
310
|
-
if (!a)
|
|
311
|
-
return e;
|
|
312
|
-
if (resolving[resolving.length - 1] === e.name)
|
|
313
|
-
return e; // its own name inside its own expression: the row field
|
|
314
|
-
if (resolving.includes(e.name)) {
|
|
315
|
-
const cycle = [...resolving.slice(resolving.indexOf(e.name)), e.name].join(" → ");
|
|
316
|
-
this.fail(`select aliases form a cycle: ${cycle} — an alias used in \`where\` cannot depend on itself`);
|
|
317
|
-
}
|
|
318
|
-
if (a.kind === "collect") {
|
|
319
|
-
this.fail(`select alias '${e.name}' is a ${a.op.op} { … } block — in \`where\` it can only stand alone as a non-empty test, not inside an expression`);
|
|
320
|
-
}
|
|
321
|
-
resolving.push(e.name);
|
|
322
|
-
const out = subst(a.expr);
|
|
323
|
-
resolving.pop();
|
|
324
|
-
return out;
|
|
325
|
-
}
|
|
326
|
-
case "member": return { ...e, recv: subst(e.recv) };
|
|
327
|
-
case "index": return { ...e, recv: subst(e.recv), index: subst(e.index) };
|
|
328
|
-
case "call": return { ...e, recv: e.recv ? subst(e.recv) : null, args: e.args.map(subst) };
|
|
329
|
-
case "unary": return { ...e, expr: subst(e.expr) };
|
|
330
|
-
case "binary":
|
|
331
|
-
case "logical":
|
|
332
|
-
case "in": return { ...e, left: subst(e.left), right: subst(e.right) };
|
|
333
|
-
case "range": return { ...e, lo: e.lo ? subst(e.lo) : null, hi: e.hi ? subst(e.hi) : null };
|
|
334
|
-
default: return e; // lit, binding, outer (`^name` reads an enclosing row, never an alias)
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
const walk = (w) => {
|
|
338
|
-
switch (w.kind) {
|
|
339
|
-
case "and": return { kind: "and", parts: w.parts.map(walk) };
|
|
340
|
-
case "or": return { kind: "or", parts: w.parts.map(walk) };
|
|
341
|
-
case "not": return { kind: "not", expr: walk(w.expr) };
|
|
342
|
-
case "scalar": {
|
|
343
|
-
if (w.expr.kind === "ident") {
|
|
344
|
-
const a = aliases.get(w.expr.name);
|
|
345
|
-
if (a?.kind === "collect")
|
|
346
|
-
return a.op; // a collection in predicate position: non-empty
|
|
347
|
-
}
|
|
348
|
-
return { kind: "scalar", expr: subst(w.expr) };
|
|
349
|
-
}
|
|
350
|
-
case "op": return { ...w, receiver: subst(w.receiver) }; // the receiver is read in this scope; the block is its own scope
|
|
437
|
+
// Whether the token `n` ahead can head a scope reference: a `^` run, or the
|
|
438
|
+
// `N^` of an absolute reference.
|
|
439
|
+
atScopeRefAt(n) {
|
|
440
|
+
return this.peekAt(n)?.type === "caret" || this.atAbsoluteRefAt(n);
|
|
441
|
+
}
|
|
442
|
+
// `N^` — an unsigned integer literal (digits only) immediately followed by a
|
|
443
|
+
// caret, no whitespace between: an absolute scope reference (GRAMMAR §4).
|
|
444
|
+
// `1 ^x` is not one (the `^` then fails as a stray token), nor is `1.0^x`.
|
|
445
|
+
atAbsoluteRef() { return this.atAbsoluteRefAt(0); }
|
|
446
|
+
atAbsoluteRefAt(n) {
|
|
447
|
+
const t = this.peekAt(n);
|
|
448
|
+
const nx = this.peekAt(n + 1);
|
|
449
|
+
return t?.type === "number" && /^\d+$/.test(t.value) && nx?.type === "caret" && nx.pos === t.end;
|
|
450
|
+
}
|
|
451
|
+
// The head of an outer reference or a lift: a run of carets (one scope out
|
|
452
|
+
// per caret), or an absolute reference `N^` (scope N, GRAMMAR §4), both as the
|
|
453
|
+
// number of scopes out from the current depth. Returns 0 when there is none.
|
|
454
|
+
// The absolute form is checked before anything is consumed, so a failing
|
|
455
|
+
// lookahead (`tryOp`) leaves the cursor on the integer.
|
|
456
|
+
parseScopeRef() {
|
|
457
|
+
if (this.atAbsoluteRef()) {
|
|
458
|
+
const n = this.peek();
|
|
459
|
+
const target = Number(n.value);
|
|
460
|
+
if (target >= this.depth) {
|
|
461
|
+
this.fail(`scope ${n.value} does not enclose this block (the current scope is depth ${this.depth})`);
|
|
351
462
|
}
|
|
352
|
-
|
|
353
|
-
|
|
463
|
+
this.next(); // the integer
|
|
464
|
+
this.next(); // the caret
|
|
465
|
+
return this.depth - target;
|
|
466
|
+
}
|
|
467
|
+
return this.parseCarets();
|
|
354
468
|
}
|
|
355
469
|
atOrderBy() {
|
|
356
470
|
const t = this.peek();
|
|
@@ -363,10 +477,15 @@ class Parser {
|
|
|
363
477
|
if (!this.at("ident", "follow"))
|
|
364
478
|
return false;
|
|
365
479
|
const nx = this.peekAt(1);
|
|
366
|
-
return !!nx && (nx.type === "ident" || nx.type === "binding" ||
|
|
480
|
+
return !!nx && (nx.type === "ident" || nx.type === "binding" || this.atScopeRefAt(1));
|
|
481
|
+
}
|
|
482
|
+
// Whether the cursor is at something that can begin a `follow` destination.
|
|
483
|
+
atDestinationStart() {
|
|
484
|
+
return this.at("ident") || this.at("binding") || this.atScopeRefAt(0);
|
|
367
485
|
}
|
|
368
486
|
// ---- follow ---------------------------------------------------------------
|
|
369
487
|
parseFollow() {
|
|
488
|
+
const start = this.start();
|
|
370
489
|
this.next(); // `follow`
|
|
371
490
|
let distinct = false;
|
|
372
491
|
// After `follow`, `distinct` is a keyword (a relation literally named
|
|
@@ -374,19 +493,19 @@ class Parser {
|
|
|
374
493
|
if (this.at("ident", "distinct")) {
|
|
375
494
|
this.next();
|
|
376
495
|
distinct = true;
|
|
377
|
-
if (!this.
|
|
496
|
+
if (!this.atDestinationStart()) {
|
|
378
497
|
this.fail("expected a relation after `follow distinct` (`follow distinct <relation>`)");
|
|
379
498
|
}
|
|
380
499
|
}
|
|
381
500
|
const destinations = [this.parseFollowDestination()];
|
|
382
501
|
while (this.at("comma")) {
|
|
383
502
|
this.next();
|
|
384
|
-
if (!this.
|
|
503
|
+
if (!this.atDestinationStart()) {
|
|
385
504
|
this.fail("expected a relation after ',' (`follow <relation>, <relation>`)");
|
|
386
505
|
}
|
|
387
506
|
destinations.push(this.parseFollowDestination());
|
|
388
507
|
}
|
|
389
|
-
const follow = { destinations, distinct, where: null, frontier: null, depth: null, by: null };
|
|
508
|
+
const follow = { kind: "follow", span: this.sp(start), destinations, distinct, where: null, frontier: null, depth: null, by: null };
|
|
390
509
|
if (!this.at("lbrace"))
|
|
391
510
|
return follow;
|
|
392
511
|
this.next(); // '{'
|
|
@@ -395,7 +514,8 @@ class Parser {
|
|
|
395
514
|
if (follow.where)
|
|
396
515
|
this.fail("duplicate `where` in follow clause");
|
|
397
516
|
this.next();
|
|
398
|
-
|
|
517
|
+
// Read in the successor's scope, one deeper than the frontier row.
|
|
518
|
+
follow.where = this.atDepth(this.depth + 1, () => this.parseValueExpr());
|
|
399
519
|
}
|
|
400
520
|
else if (this.at("ident", "frontier")) {
|
|
401
521
|
if (follow.frontier)
|
|
@@ -421,12 +541,13 @@ class Parser {
|
|
|
421
541
|
follow.depth = v;
|
|
422
542
|
}
|
|
423
543
|
else {
|
|
424
|
-
this.fail(`unexpected ${this.tokDesc()} in follow block — expected where/frontier/depth/by`);
|
|
544
|
+
this.fail(`unexpected ${this.tokDesc()} in follow block — expected where/frontier/depth/by (a brace after the last destination is the options block; a destination block needs its consumer: \`follow <relation> collect { … }\`)`);
|
|
425
545
|
}
|
|
426
546
|
}
|
|
427
547
|
if (!this.at("rbrace"))
|
|
428
548
|
this.fail("expected '}' to close the follow block");
|
|
429
549
|
this.next();
|
|
550
|
+
follow.span = this.sp(start);
|
|
430
551
|
return follow;
|
|
431
552
|
}
|
|
432
553
|
// One `follow` destination. The lookahead mirrors `tryOp`: a receiver (binding,
|
|
@@ -444,10 +565,11 @@ class Parser {
|
|
|
444
565
|
this.next();
|
|
445
566
|
isReceiver = true;
|
|
446
567
|
}
|
|
447
|
-
else if (this.at("ident") || this.
|
|
448
|
-
this.
|
|
568
|
+
else if (this.at("ident") || this.atScopeRefAt(0)) {
|
|
569
|
+
const startPos = this.start();
|
|
570
|
+
this.parseScopeRef();
|
|
449
571
|
if (this.at("ident")) {
|
|
450
|
-
this.parseNavFrom(this.next());
|
|
572
|
+
this.parseNavFrom(this.next(), 0, startPos);
|
|
451
573
|
isReceiver = true;
|
|
452
574
|
}
|
|
453
575
|
}
|
|
@@ -468,7 +590,7 @@ class Parser {
|
|
|
468
590
|
}
|
|
469
591
|
}
|
|
470
592
|
this.pos = start;
|
|
471
|
-
if (this.
|
|
593
|
+
if (this.atScopeRefAt(0)) {
|
|
472
594
|
this.fail("`follow` takes a relation of the current row (`follow <relation>`); an outer reference `^name` is not allowed there — it may only head a destination block (`follow ^name collect { … }`)");
|
|
473
595
|
}
|
|
474
596
|
return this.parseReceiver();
|
|
@@ -480,13 +602,17 @@ class Parser {
|
|
|
480
602
|
// call (`entries(prefs)`), so a computed collection can be consumed directly.
|
|
481
603
|
parseReceiver() {
|
|
482
604
|
if (this.at("binding"))
|
|
483
|
-
return
|
|
484
|
-
const
|
|
605
|
+
return this.bindingNode(this.next());
|
|
606
|
+
const start = this.start();
|
|
607
|
+
const levels = this.parseScopeRef();
|
|
485
608
|
if (!this.at("ident"))
|
|
486
609
|
this.fail("expected a collection navigation (a property/relation name)");
|
|
487
610
|
if (LITERAL_WORDS.has(this.peek().value))
|
|
488
611
|
this.fail(`\`${this.peek().value}\` is a literal, not a collection`);
|
|
489
|
-
return this.parseNavFrom(this.next(), levels).expr;
|
|
612
|
+
return this.parseNavFrom(this.next(), levels, start).expr;
|
|
613
|
+
}
|
|
614
|
+
bindingNode(t) {
|
|
615
|
+
return { kind: "binding", span: [t.pos, t.end], index: t.index };
|
|
490
616
|
}
|
|
491
617
|
// Consume a run of `^` and return its length (0 when there is none).
|
|
492
618
|
parseCarets() {
|
|
@@ -498,19 +624,24 @@ class Parser {
|
|
|
498
624
|
return levels;
|
|
499
625
|
}
|
|
500
626
|
// A dotted navigation chain from `head`; `levels` > 0 makes the head an outer
|
|
501
|
-
// reference read exactly that many scopes out
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
627
|
+
// reference read exactly that many scopes out (`start` is then the position
|
|
628
|
+
// of the first caret). A bare head followed by `(` is a free-function call
|
|
629
|
+
// (`entries(x)`), which may then be navigated further.
|
|
630
|
+
parseNavFrom(head, levels = 0, start = head.pos) {
|
|
631
|
+
let expr = levels > 0
|
|
632
|
+
? { kind: "outer", span: [start, head.end], levels, name: head.value }
|
|
633
|
+
: { kind: "ident", span: [start, head.end], name: head.value };
|
|
505
634
|
let name = head.value;
|
|
506
|
-
if (levels === 0 && this.at("lparen"))
|
|
507
|
-
|
|
635
|
+
if (levels === 0 && this.at("lparen")) {
|
|
636
|
+
const args = this.parseArgs();
|
|
637
|
+
expr = { kind: "call", span: this.sp(start), recv: null, name: head.value, args };
|
|
638
|
+
}
|
|
508
639
|
while (this.at("dot")) {
|
|
509
640
|
this.next();
|
|
510
641
|
if (!this.at("ident"))
|
|
511
642
|
this.fail("expected a property name after '.'");
|
|
512
643
|
name = this.next().value;
|
|
513
|
-
expr = { kind: "member", recv: expr, name };
|
|
644
|
+
expr = { kind: "member", span: this.sp(start), recv: expr, name };
|
|
514
645
|
}
|
|
515
646
|
return { expr, name };
|
|
516
647
|
}
|
|
@@ -543,8 +674,13 @@ class Parser {
|
|
|
543
674
|
// `^^x` bind different rows), so the lift depth is part of the key.
|
|
544
675
|
const seen = new Set();
|
|
545
676
|
for (const it of items) {
|
|
546
|
-
if (it.name === "")
|
|
547
|
-
|
|
677
|
+
if (it.name === "") {
|
|
678
|
+
if (it.kind === "collect")
|
|
679
|
+
this.fail("a nested block in a projection needs a name (`name: <relation> collect { … }`); as a predicate, a consumer test is written `where <relation> exists { … }`");
|
|
680
|
+
if (ctx.top)
|
|
681
|
+
this.fail("a leading expression is a projection (select): an item that is not a plain name needs an alias (`name: expr`) or `values`; to filter by it write `where …` after `from` — at the top level a predicate is never implicit (the clause order puts `from` first)");
|
|
682
|
+
this.fail("an item that is not a plain name needs an alias (`name: expr`) or `values`; to filter by it write `where …` after the projection (a block may lead with a predicate only when nothing is projected: `{ age > 18 }`)");
|
|
683
|
+
}
|
|
548
684
|
const key = `${"^".repeat(it.kind === "field" ? it.lift : 0)}${it.name}`;
|
|
549
685
|
if (seen.has(key))
|
|
550
686
|
this.fail(`duplicate projection name '${it.name}' — each projected item needs its own name (alias one: \`other: expr\`)`);
|
|
@@ -554,12 +690,13 @@ class Parser {
|
|
|
554
690
|
return { items, values };
|
|
555
691
|
}
|
|
556
692
|
parseSelectItem(ctx) {
|
|
693
|
+
const start = this.start();
|
|
557
694
|
// Leading `^`s mark a lift; the count is how many scopes out it binds. A
|
|
558
695
|
// lift is bound by a `collect { … }` in where position and nowhere else — at
|
|
559
696
|
// the top level, in a select-position block, or in an exists/none/count
|
|
560
697
|
// block it would silently do nothing (or act as a plain field), so it is an
|
|
561
|
-
// error there.
|
|
562
|
-
const lift = this.
|
|
698
|
+
// error there. `N^name:` is the lift into scope N (GRAMMAR §4).
|
|
699
|
+
const lift = this.parseScopeRef();
|
|
563
700
|
if (!this.at("ident") && !this.canStartValue())
|
|
564
701
|
this.fail("expected a projection name");
|
|
565
702
|
if (lift > 0 && !ctx.liftsAllowed) {
|
|
@@ -571,21 +708,31 @@ class Parser {
|
|
|
571
708
|
this.next(); // ':'
|
|
572
709
|
const op = this.tryOp(false);
|
|
573
710
|
if (op) {
|
|
711
|
+
// `name: jobs first { }.pay`: the directive is the head of a longer value.
|
|
712
|
+
if (this.atScalarContinuation() || this.atValueDirective())
|
|
713
|
+
return this.finishItem(name, this.parseOr(op), lift, start);
|
|
574
714
|
if (op.op !== "collect" && op.op !== "first" && op.op !== "single") {
|
|
575
715
|
this.fail(`projection '${name}' must use collect/first/single, not ${op.op} (exists/none/count are where-position tests)`);
|
|
576
716
|
}
|
|
577
|
-
|
|
578
|
-
this.fail(`a lift (^${name}) value must be a scalar expression, not ${op.op} { … }`);
|
|
579
|
-
return { kind: "collect", name, op };
|
|
717
|
+
return this.finishItem(name, op, lift, start);
|
|
580
718
|
}
|
|
581
|
-
|
|
582
|
-
return { kind: "field", name, expr, lift };
|
|
719
|
+
return this.finishItem(name, this.parseValueExpr(), lift, start);
|
|
583
720
|
}
|
|
584
|
-
// Unaliased item: a bare/dotted navigation keys by its last
|
|
585
|
-
// other expression is unnamed ("") — legal only under `values`
|
|
586
|
-
// parseProjection, which sees the whole list).
|
|
721
|
+
// Unaliased item: a bare/dotted navigation (under any `!`) keys by its last
|
|
722
|
+
// segment; any other expression is unnamed ("") — legal only under `values`
|
|
723
|
+
// (checked by parseProjection, which sees the whole list).
|
|
587
724
|
const expr = this.parseValueExpr();
|
|
588
|
-
return
|
|
725
|
+
return this.finishItem(navKey(expr) ?? "", expr, lift, start);
|
|
726
|
+
}
|
|
727
|
+
// An item from its value: exactly a directive (written out, or the bracket
|
|
728
|
+
// sugar) is the `collect` item kind; anything else is a `field`.
|
|
729
|
+
finishItem(name, expr, lift, start) {
|
|
730
|
+
if (expr.kind === "op") {
|
|
731
|
+
if (lift)
|
|
732
|
+
this.fail(`a lift (^${name}) value must be a scalar expression, not ${expr.op} { … }`);
|
|
733
|
+
return { kind: "collect", span: this.sp(start), name, op: expr };
|
|
734
|
+
}
|
|
735
|
+
return { kind: "field", span: this.sp(start), name, expr, lift };
|
|
589
736
|
}
|
|
590
737
|
// ---- order by -------------------------------------------------------------
|
|
591
738
|
parseOrderSpecs() {
|
|
@@ -597,6 +744,7 @@ class Parser {
|
|
|
597
744
|
return specs;
|
|
598
745
|
}
|
|
599
746
|
parseOrderSpec() {
|
|
747
|
+
const start = this.start();
|
|
600
748
|
const expr = this.parseValueExpr();
|
|
601
749
|
let desc = false;
|
|
602
750
|
if (this.at("ident", "asc"))
|
|
@@ -605,7 +753,7 @@ class Parser {
|
|
|
605
753
|
this.next();
|
|
606
754
|
desc = true;
|
|
607
755
|
}
|
|
608
|
-
return { expr, desc };
|
|
756
|
+
return { kind: "order", span: this.sp(start), expr, desc };
|
|
609
757
|
}
|
|
610
758
|
// ---- where boolean tree: or → and → primary --------------------------------
|
|
611
759
|
// `!` is handled in `parseWherePrimary`: it applies to the operand right after
|
|
@@ -613,27 +761,33 @@ class Parser {
|
|
|
613
761
|
// whole comparison — `!a == b` is `(!a) == b`, exactly as in value position.
|
|
614
762
|
parseWhere() { return this.parseWhereOr(); }
|
|
615
763
|
parseWhereOr() {
|
|
764
|
+
const start = this.start();
|
|
616
765
|
const left = this.parseWhereAnd();
|
|
617
|
-
if (!this.
|
|
766
|
+
if (!this.atOr())
|
|
618
767
|
return left;
|
|
619
768
|
const parts = [left];
|
|
620
|
-
while (this.
|
|
769
|
+
while (this.atOr()) {
|
|
621
770
|
this.next();
|
|
622
771
|
parts.push(this.parseWhereAnd());
|
|
623
772
|
}
|
|
624
|
-
return { kind: "or", parts };
|
|
773
|
+
return { kind: "or", span: this.sp(start), parts };
|
|
625
774
|
}
|
|
626
775
|
parseWhereAnd() {
|
|
776
|
+
const start = this.start();
|
|
627
777
|
const left = this.parseWherePrimary();
|
|
628
|
-
if (!this.
|
|
778
|
+
if (!this.atAnd())
|
|
629
779
|
return left;
|
|
630
780
|
const parts = [left];
|
|
631
|
-
while (this.
|
|
781
|
+
while (this.atAnd()) {
|
|
632
782
|
this.next();
|
|
633
783
|
parts.push(this.parseWherePrimary());
|
|
634
784
|
}
|
|
635
|
-
return { kind: "and", parts };
|
|
785
|
+
return { kind: "and", span: this.sp(start), parts };
|
|
636
786
|
}
|
|
787
|
+
// `&&` / `and` and `||` / `or` are one operator each (0.17): the words are
|
|
788
|
+
// exact synonyms of the symbols, parsed to the same nodes.
|
|
789
|
+
atAnd() { return this.atOp("&&") || this.at("kw", "and"); }
|
|
790
|
+
atOr() { return this.atOp("||") || this.at("kw", "or"); }
|
|
637
791
|
// A where operand: `[!…] ( where )`, `[!…] <receiver> <consumer> { … }`, or a
|
|
638
792
|
// scalar leaf (a cmp-level expression, which handles its own `!`).
|
|
639
793
|
//
|
|
@@ -645,51 +799,100 @@ class Parser {
|
|
|
645
799
|
// predicate.
|
|
646
800
|
parseWherePrimary() {
|
|
647
801
|
const start = this.pos;
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
802
|
+
// Leading negations: `!`, `not` (one each) and `is` (two — `is x` ≡ `!!x`).
|
|
803
|
+
const notStarts = [];
|
|
804
|
+
for (;;) {
|
|
805
|
+
if (this.atOp("!") || this.at("kw", "not"))
|
|
806
|
+
notStarts.push(this.next().pos);
|
|
807
|
+
else if (this.at("kw", "is")) {
|
|
808
|
+
const p = this.next().pos;
|
|
809
|
+
notStarts.push(p, p);
|
|
810
|
+
}
|
|
811
|
+
else
|
|
812
|
+
break;
|
|
652
813
|
}
|
|
653
814
|
if (this.at("lparen")) {
|
|
654
|
-
this.next();
|
|
815
|
+
const lparen = this.next().pos;
|
|
655
816
|
const inner = this.parseWhere();
|
|
656
817
|
if (!this.at("rparen"))
|
|
657
818
|
this.fail("expected ')' to close a grouped where expression");
|
|
658
819
|
this.next();
|
|
820
|
+
const group = [lparen, this.end()];
|
|
659
821
|
if (this.atScalarContinuation()) {
|
|
660
|
-
let e = this.parsePostfix(this.whereToExpr(inner));
|
|
661
|
-
for (let i =
|
|
662
|
-
e = { kind: "unary", op: "!", expr: e };
|
|
663
|
-
|
|
822
|
+
let e = this.parsePostfix(withSpan(this.whereToExpr(inner), group));
|
|
823
|
+
for (let i = notStarts.length - 1; i >= 0; i--)
|
|
824
|
+
e = { kind: "unary", span: [notStarts[i], e.span[1]], op: "!", expr: e };
|
|
825
|
+
const expr = this.parseCmp(e);
|
|
826
|
+
return { kind: "scalar", span: expr.span, expr };
|
|
664
827
|
}
|
|
665
|
-
return wrapNot(inner,
|
|
828
|
+
return wrapNot(withSpan(inner, group), notStarts);
|
|
666
829
|
}
|
|
667
830
|
const op = this.tryOp(true);
|
|
668
|
-
|
|
669
|
-
|
|
831
|
+
// A `first`/`single` directive is a value, never a test: the scalar grammar
|
|
832
|
+
// reads it (as a value-position directive) with whatever follows it, and
|
|
833
|
+
// `rejectBareLookup` turns a bare one into the lookup error.
|
|
834
|
+
if (op && op.op !== "first" && op.op !== "single")
|
|
835
|
+
return wrapNot(this.finishWhereOp(op), notStarts);
|
|
670
836
|
// A scalar leaf, re-read from the first `!` so the scalar grammar gives `!`
|
|
671
837
|
// its one precedence (tighter than comparison). A leaf whose whole value is
|
|
672
838
|
// a negation keeps the `not` node shape (`where !active`).
|
|
673
839
|
this.pos = start;
|
|
674
|
-
|
|
840
|
+
const leaf = scalarLeaf(this.parseCmp());
|
|
841
|
+
if (!this.deferLookup)
|
|
842
|
+
this.rejectBareLookup(leaf);
|
|
843
|
+
return leaf;
|
|
844
|
+
}
|
|
845
|
+
// A where leaf that is exactly a `first`/`single` directive (under any `!`) is
|
|
846
|
+
// the select-position lookup error: a lookup is a value, and alone in `where`
|
|
847
|
+
// the writer almost certainly meant `exists`.
|
|
848
|
+
rejectBareLookup(w) {
|
|
849
|
+
let core = w;
|
|
850
|
+
while (core.kind === "not")
|
|
851
|
+
core = core.expr;
|
|
852
|
+
if (core.kind !== "scalar")
|
|
853
|
+
return;
|
|
854
|
+
let e = core.expr;
|
|
855
|
+
while (e.kind === "unary" && e.op === "!")
|
|
856
|
+
e = e.expr;
|
|
857
|
+
if (e.kind === "op" && (e.op === "first" || e.op === "single")) {
|
|
858
|
+
this.fail(`${e.op} { … } is a select-position lookup; in where use exists { … } / none { … } or count { … } <op> N (\`x[p]\` is \`x first { where p }\`; a lookup is a value — compare it, navigate it or require it: \`x[p].name == …\`, \`x[0]!\`)`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// `rejectBareLookup` over every leaf of a tree read with `deferLookup` on.
|
|
862
|
+
rejectBareLookups(w) {
|
|
863
|
+
switch (w.kind) {
|
|
864
|
+
case "and":
|
|
865
|
+
case "or":
|
|
866
|
+
w.parts.forEach((p) => this.rejectBareLookups(p));
|
|
867
|
+
return;
|
|
868
|
+
case "not":
|
|
869
|
+
case "scalar":
|
|
870
|
+
this.rejectBareLookup(w);
|
|
871
|
+
return;
|
|
872
|
+
case "op": return;
|
|
873
|
+
}
|
|
675
874
|
}
|
|
676
|
-
// Whether the token after a `)` continues a scalar
|
|
875
|
+
// Whether the token after a `)` (or a directive) continues a scalar
|
|
876
|
+
// expression: an operator, `.`-navigation, a bracket, a postfix `!`, a range,
|
|
877
|
+
// `in`, or `is`.
|
|
677
878
|
atScalarContinuation() {
|
|
678
879
|
const t = this.peek();
|
|
679
880
|
if (t.type === "op")
|
|
680
|
-
return CMP_OPS.has(t.value) || ADD_OPS.has(t.value) || MUL_OPS.has(t.value);
|
|
681
|
-
|
|
881
|
+
return CMP_OPS.has(t.value) || ADD_OPS.has(t.value) || MUL_OPS.has(t.value) || t.value === "!";
|
|
882
|
+
if (t.type === "kw")
|
|
883
|
+
return t.value === "is";
|
|
884
|
+
return t.type === "dot" || t.type === "range" || t.type === "lbracket" || (t.type === "ident" && t.value === "in");
|
|
682
885
|
}
|
|
683
886
|
// A parenthesized where-group that turned out to be a scalar operand, as an
|
|
684
887
|
// expression. A consumer test has no scalar value, so it cannot be operated on.
|
|
685
888
|
whereToExpr(w) {
|
|
686
889
|
switch (w.kind) {
|
|
687
890
|
case "scalar": return w.expr;
|
|
688
|
-
case "not": return { kind: "unary", op: "!", expr: this.whereToExpr(w.expr) };
|
|
891
|
+
case "not": return { kind: "unary", span: w.span, op: "!", expr: this.whereToExpr(w.expr) };
|
|
689
892
|
case "and":
|
|
690
893
|
case "or": {
|
|
691
894
|
const op = w.kind === "and" ? "&&" : "||";
|
|
692
|
-
return w.parts.map((p) => this.whereToExpr(p)).reduce((l, r) => ({ kind: "logical", op, left: l, right: r }));
|
|
895
|
+
return w.parts.map((p) => this.whereToExpr(p)).reduce((l, r) => ({ kind: "logical", span: [l.span[0], r.span[1]], op, left: l, right: r }));
|
|
693
896
|
}
|
|
694
897
|
case "op": {
|
|
695
898
|
const hint = w.op === "count" ? ` — write \`<relation> count { … } ${this.peek().value} N\` without the parentheses` : "";
|
|
@@ -717,74 +920,175 @@ class Parser {
|
|
|
717
920
|
if (!Number.isInteger(v))
|
|
718
921
|
this.fail("count comparison takes an integer");
|
|
719
922
|
op.countCmp = { op: relop, value: v };
|
|
923
|
+
op.span = [op.span[0], this.end()];
|
|
720
924
|
}
|
|
721
925
|
return op;
|
|
722
926
|
}
|
|
723
|
-
// Detect + parse a postfix consumer op `<receiver> <consumer> { <sub> }
|
|
724
|
-
//
|
|
927
|
+
// Detect + parse a postfix consumer op `<receiver> <consumer> { <sub> }` — or
|
|
928
|
+
// its implicit-`collect` form `<receiver> { <sub> }`. Returns null (rewinding)
|
|
929
|
+
// when the lookahead is not a directive.
|
|
725
930
|
tryOp(inWhere) {
|
|
726
931
|
const start = this.pos;
|
|
932
|
+
const startPos = this.start();
|
|
727
933
|
let receiver;
|
|
934
|
+
// An absolute reference that does not enclose this scope is an error only
|
|
935
|
+
// if the tokens do form a directive (`0^docs collect { }` at the root);
|
|
936
|
+
// otherwise the expression grammar gets to read them (a top-level `0^name`
|
|
937
|
+
// item is a lift, with the lift's own error), so the failure is held back
|
|
938
|
+
// until the lookahead has decided.
|
|
939
|
+
let scopeErr = null;
|
|
728
940
|
if (this.at("binding"))
|
|
729
|
-
receiver =
|
|
730
|
-
else if (this.at("ident") || this.
|
|
731
|
-
|
|
941
|
+
receiver = this.bindingNode(this.next());
|
|
942
|
+
else if (this.at("ident") || this.atScopeRefAt(0)) {
|
|
943
|
+
let levels = 0;
|
|
944
|
+
try {
|
|
945
|
+
levels = this.parseScopeRef();
|
|
946
|
+
}
|
|
947
|
+
catch (err) {
|
|
948
|
+
if (!(err instanceof OqxError))
|
|
949
|
+
throw err;
|
|
950
|
+
scopeErr = err;
|
|
951
|
+
this.next();
|
|
952
|
+
this.next(); // the integer and the caret, as if it had parsed
|
|
953
|
+
levels = 1;
|
|
954
|
+
}
|
|
732
955
|
if (!this.at("ident")) {
|
|
733
956
|
this.pos = start;
|
|
734
957
|
return null;
|
|
735
958
|
}
|
|
736
|
-
receiver = this.parseNavFrom(this.next(), levels).expr;
|
|
959
|
+
receiver = this.parseNavFrom(this.next(), levels, startPos).expr;
|
|
737
960
|
}
|
|
738
961
|
else
|
|
739
962
|
return null;
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
let distinct = false;
|
|
750
|
-
if (this.at("ident", "distinct")) {
|
|
751
|
-
this.next();
|
|
752
|
-
distinct = true;
|
|
753
|
-
}
|
|
754
|
-
this.next(); // '{'
|
|
755
|
-
const { sub, distinct: bodyDistinct } = this.parseSubquery({ top: false, op, liftsAllowed: inWhere && op === "collect" });
|
|
756
|
-
if (!this.at("rbrace"))
|
|
757
|
-
this.fail(`expected '}' to close the ${op} { … } block`);
|
|
758
|
-
this.next();
|
|
759
|
-
return { kind: "op", receiver, op, sub, distinct: distinct || bodyDistinct };
|
|
963
|
+
const op = this.at("lbrace") ? "collect" : this.atConsumerBlock();
|
|
964
|
+
if (op) {
|
|
965
|
+
if (scopeErr)
|
|
966
|
+
throw scopeErr;
|
|
967
|
+
if (receiver.kind === "ident" && LITERAL_WORDS.has(receiver.name))
|
|
968
|
+
this.fail(`\`${receiver.name}\` is a literal, not a collection`);
|
|
969
|
+
// `count { }` with nothing before it: the consumer word is not a receiver.
|
|
970
|
+
if (op === "collect" && receiver.kind === "ident" && CONSUMERS.has(receiver.name)) {
|
|
971
|
+
this.fail(`unexpected '{' after \`${receiver.name}\` — a consumer needs a receiver: \`<collection> ${receiver.name} { … }\``);
|
|
760
972
|
}
|
|
973
|
+
return this.parseConsumerBlock(receiver, op, inWhere);
|
|
761
974
|
}
|
|
762
975
|
this.pos = start;
|
|
763
976
|
return null;
|
|
764
977
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
978
|
+
// The cursor is at a consumer word that opens a block — `<op> {` or
|
|
979
|
+
// `<op> distinct {`. Consumes nothing.
|
|
980
|
+
atConsumerBlock() {
|
|
981
|
+
if (!this.at("ident") || !CONSUMERS.has(this.peek().value))
|
|
982
|
+
return null;
|
|
983
|
+
const after = this.peekAt(1);
|
|
984
|
+
const opThenBrace = after?.type === "lbrace";
|
|
985
|
+
const opDistinctBrace = after?.type === "ident" && after.value === "distinct" && this.peekAt(2)?.type === "lbrace";
|
|
986
|
+
return opThenBrace || opDistinctBrace ? this.peek().value : null;
|
|
987
|
+
}
|
|
988
|
+
// `[<op>] [distinct] { <sub> }` over an already-parsed receiver; the cursor is
|
|
989
|
+
// at the consumer word, or at the `{` of the implicit `collect`.
|
|
990
|
+
parseConsumerBlock(receiver, op, inWhere) {
|
|
991
|
+
const startPos = receiver.span[0];
|
|
992
|
+
if (this.at("ident"))
|
|
993
|
+
this.next(); // the consumer word (absent for `x { … }`)
|
|
994
|
+
let distinct = false;
|
|
995
|
+
if (this.at("ident", "distinct")) {
|
|
996
|
+
this.next();
|
|
997
|
+
distinct = true;
|
|
998
|
+
}
|
|
999
|
+
const lbrace = this.next().pos; // '{'
|
|
1000
|
+
const defer = this.deferLookup;
|
|
1001
|
+
this.deferLookup = false;
|
|
1002
|
+
// The receiver was read at the enclosing depth; the block's rows are one deeper.
|
|
1003
|
+
const body = this.atDepth(this.depth + 1, () => this.parseBody({ top: false, op, liftsAllowed: inWhere && op === "collect" }));
|
|
1004
|
+
this.deferLookup = defer;
|
|
1005
|
+
if (!this.at("rbrace"))
|
|
1006
|
+
this.fail(`expected '}' to close the ${op} { … } block`);
|
|
1007
|
+
this.next();
|
|
1008
|
+
const sub = {
|
|
1009
|
+
kind: "subquery",
|
|
1010
|
+
span: this.sp(lbrace),
|
|
1011
|
+
from: body.froms,
|
|
1012
|
+
where: body.where,
|
|
1013
|
+
select: body.select,
|
|
1014
|
+
orderBy: body.orderBy,
|
|
1015
|
+
follow: body.follow,
|
|
1016
|
+
values: body.values,
|
|
1017
|
+
limit: body.limit,
|
|
1018
|
+
offset: body.offset,
|
|
770
1019
|
};
|
|
1020
|
+
return { kind: "op", span: this.sp(startPos), receiver, op, sub, countCmp: null, distinct: distinct || body.distinct };
|
|
1021
|
+
}
|
|
1022
|
+
// A bracket suffix on `recv` (GRAMMAR §4): `[n]` / `[${…}]` is positional —
|
|
1023
|
+
// `recv first { offset n }` — and anything else a predicate —
|
|
1024
|
+
// `recv first { where p }`; a trailing `!` makes the index required and the
|
|
1025
|
+
// predicate a `single` (required too, so that zero matches is an error).
|
|
1026
|
+
parseBracket(recv) {
|
|
1027
|
+
if (recv.kind === "lit")
|
|
1028
|
+
this.fail(`\`${describe(recv)}\` is a literal, not a collection`);
|
|
1029
|
+
const start = recv.span[0];
|
|
1030
|
+
const lbracket = this.next().pos; // '['
|
|
1031
|
+
let index = null;
|
|
1032
|
+
let where = null;
|
|
1033
|
+
if (this.at("number") && this.peekAt(1)?.type === "rbracket") {
|
|
1034
|
+
const t = this.next();
|
|
1035
|
+
const v = Number(t.value);
|
|
1036
|
+
if (!Number.isInteger(v))
|
|
1037
|
+
this.fail(`an index is a non-negative integer literal or a binding (got ${t.value})`);
|
|
1038
|
+
index = { kind: "lit", span: [t.pos, t.end], value: v };
|
|
1039
|
+
}
|
|
1040
|
+
else if (this.at("binding") && this.peekAt(1)?.type === "rbracket") {
|
|
1041
|
+
index = this.bindingNode(this.next());
|
|
1042
|
+
}
|
|
1043
|
+
else if (this.atOp("-") && this.peekAt(1)?.type === "number" && this.peekAt(2)?.type === "rbracket") {
|
|
1044
|
+
this.fail("negative indices are not supported (`x[-1]`): an index is a non-negative integer literal or a binding");
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
const defer = this.deferLookup;
|
|
1048
|
+
this.deferLookup = false;
|
|
1049
|
+
// The predicate is the block's `where`: read in the rows' scope, one deeper.
|
|
1050
|
+
where = this.atDepth(this.depth + 1, () => this.parseWhere());
|
|
1051
|
+
this.deferLookup = defer;
|
|
1052
|
+
}
|
|
1053
|
+
if (!this.at("rbracket"))
|
|
1054
|
+
this.fail("expected ']' to close the bracket");
|
|
1055
|
+
this.next();
|
|
1056
|
+
const subSpan = [lbracket, this.end()];
|
|
1057
|
+
const sub = (parts) => ({
|
|
1058
|
+
kind: "subquery", span: subSpan, from: [], where: null, select: [], orderBy: null, follow: null, values: false, limit: null, offset: null, ...parts,
|
|
1059
|
+
});
|
|
1060
|
+
const bang = this.atOp("!") ? this.next() : null;
|
|
1061
|
+
if (index) {
|
|
1062
|
+
const op = { kind: "op", span: [start, subSpan[1]], receiver: recv, op: "first", sub: sub({ offset: index }), countCmp: null, distinct: false };
|
|
1063
|
+
return bang ? { kind: "required", span: [start, bang.end], expr: op } : op;
|
|
1064
|
+
}
|
|
1065
|
+
if (bang) {
|
|
1066
|
+
const op = { kind: "op", span: [start, bang.end], receiver: recv, op: "single", sub: sub({ where }), countCmp: null, distinct: false };
|
|
1067
|
+
return { kind: "required", span: [start, bang.end], expr: op };
|
|
1068
|
+
}
|
|
1069
|
+
return { kind: "op", span: [start, subSpan[1]], receiver: recv, op: "first", sub: sub({ where }), countCmp: null, distinct: false };
|
|
771
1070
|
}
|
|
772
1071
|
// ---- expression Pratt parser ----------------------------------------------
|
|
773
|
-
// Value position (select/order/follow/source): full boolean+arithmetic.
|
|
1072
|
+
// Value position (select/order/follow/source): full boolean+arithmetic. Each
|
|
1073
|
+
// level takes an optional already-parsed `left` operand (a directive read by
|
|
1074
|
+
// `tryOp`, a promoted where-group), so it continues into the operator tail
|
|
1075
|
+
// without re-reading its tokens.
|
|
774
1076
|
parseValueExpr() { return this.parseOr(); }
|
|
775
|
-
parseOr() {
|
|
776
|
-
|
|
777
|
-
while (this.
|
|
1077
|
+
parseOr(left) {
|
|
1078
|
+
left = this.parseAnd(left);
|
|
1079
|
+
while (this.atOr()) {
|
|
778
1080
|
this.next();
|
|
779
|
-
|
|
1081
|
+
const right = this.parseAnd();
|
|
1082
|
+
left = { kind: "logical", span: [left.span[0], right.span[1]], op: "||", left, right };
|
|
780
1083
|
}
|
|
781
1084
|
return left;
|
|
782
1085
|
}
|
|
783
|
-
parseAnd() {
|
|
784
|
-
|
|
785
|
-
while (this.
|
|
1086
|
+
parseAnd(left) {
|
|
1087
|
+
left = this.parseCmp(left);
|
|
1088
|
+
while (this.atAnd()) {
|
|
786
1089
|
this.next();
|
|
787
|
-
|
|
1090
|
+
const right = this.parseCmp();
|
|
1091
|
+
left = { kind: "logical", span: [left.span[0], right.span[1]], op: "&&", left, right };
|
|
788
1092
|
}
|
|
789
1093
|
return left;
|
|
790
1094
|
}
|
|
@@ -798,12 +1102,20 @@ class Parser {
|
|
|
798
1102
|
parseCmp(left) {
|
|
799
1103
|
const lhs = this.parseRange(left);
|
|
800
1104
|
const t = this.peek();
|
|
801
|
-
const isCmp = () => (this.peek().type === "op" && CMP_OPS.has(this.peek().value)) || this.at("ident", "in");
|
|
1105
|
+
const isCmp = () => (this.peek().type === "op" && CMP_OPS.has(this.peek().value)) || this.at("ident", "in") || this.at("kw", "is");
|
|
802
1106
|
if (!isCmp())
|
|
803
1107
|
return lhs;
|
|
804
|
-
|
|
1108
|
+
let op = this.next().value;
|
|
1109
|
+
// `is` / `is not` — identity (SEMANTICS §5b); `is not` is the compound.
|
|
1110
|
+
if (op === "is" && this.at("kw", "not")) {
|
|
1111
|
+
this.next();
|
|
1112
|
+
op = "is not";
|
|
1113
|
+
}
|
|
805
1114
|
const rhs = this.parseRange();
|
|
806
|
-
const
|
|
1115
|
+
const span = [lhs.span[0], rhs.span[1]];
|
|
1116
|
+
const result = op === "in"
|
|
1117
|
+
? { kind: "in", span, left: lhs, right: rhs }
|
|
1118
|
+
: { kind: "binary", span, op: op, left: lhs, right: rhs };
|
|
807
1119
|
if (isCmp()) {
|
|
808
1120
|
this.fail(`comparisons do not chain: \`a ${t.value} b ${this.peek().value} c\` — write two comparisons joined with \`&&\``);
|
|
809
1121
|
}
|
|
@@ -816,16 +1128,19 @@ class Parser {
|
|
|
816
1128
|
// opens the high end.
|
|
817
1129
|
parseRange(left) {
|
|
818
1130
|
if (left === undefined && this.at("range")) {
|
|
819
|
-
const
|
|
1131
|
+
const tok = this.next();
|
|
1132
|
+
const exclusiveEnd = tok.value === "...";
|
|
820
1133
|
if (!this.canStartValue())
|
|
821
1134
|
this.fail("a range needs at least one bound: `lo..hi`, `lo..`, or `..hi`");
|
|
822
|
-
|
|
1135
|
+
const hi = this.parseAdd();
|
|
1136
|
+
return { kind: "range", span: [tok.pos, hi.span[1]], lo: null, hi, exclusiveEnd };
|
|
823
1137
|
}
|
|
824
1138
|
const lo = this.parseAdd(left);
|
|
825
1139
|
if (this.at("range")) {
|
|
826
|
-
const
|
|
1140
|
+
const tok = this.next();
|
|
1141
|
+
const exclusiveEnd = tok.value === "...";
|
|
827
1142
|
const hi = this.canStartValue() ? this.parseAdd() : null;
|
|
828
|
-
return { kind: "range", lo, hi, exclusiveEnd };
|
|
1143
|
+
return { kind: "range", span: [lo.span[0], hi ? hi.span[1] : tok.end], lo, hi, exclusiveEnd };
|
|
829
1144
|
}
|
|
830
1145
|
return lo;
|
|
831
1146
|
}
|
|
@@ -838,6 +1153,8 @@ class Parser {
|
|
|
838
1153
|
const t = this.peek();
|
|
839
1154
|
if (t.type === "ident")
|
|
840
1155
|
return !CLAUSE_WORDS.has(t.value);
|
|
1156
|
+
if (t.type === "kw")
|
|
1157
|
+
return t.value === "is" || t.value === "not"; // the prefix operators
|
|
841
1158
|
if (t.type === "number" || t.type === "string" ||
|
|
842
1159
|
t.type === "binding" || t.type === "lparen" || t.type === "caret")
|
|
843
1160
|
return true;
|
|
@@ -847,7 +1164,8 @@ class Parser {
|
|
|
847
1164
|
left = this.parseMul(left);
|
|
848
1165
|
while (this.peek().type === "op" && ADD_OPS.has(this.peek().value)) {
|
|
849
1166
|
const op = this.next().value;
|
|
850
|
-
|
|
1167
|
+
const right = this.parseMul();
|
|
1168
|
+
left = { kind: "binary", span: [left.span[0], right.span[1]], op, left, right };
|
|
851
1169
|
}
|
|
852
1170
|
return left;
|
|
853
1171
|
}
|
|
@@ -855,25 +1173,30 @@ class Parser {
|
|
|
855
1173
|
left = this.parseUnary(left);
|
|
856
1174
|
while (this.peek().type === "op" && MUL_OPS.has(this.peek().value)) {
|
|
857
1175
|
const op = this.next().value;
|
|
858
|
-
|
|
1176
|
+
const right = this.parseUnary();
|
|
1177
|
+
left = { kind: "binary", span: [left.span[0], right.span[1]], op, left, right };
|
|
859
1178
|
}
|
|
860
1179
|
return left;
|
|
861
1180
|
}
|
|
862
1181
|
parseUnary(left) {
|
|
863
1182
|
if (left !== undefined)
|
|
864
1183
|
return this.parsePostfix(left);
|
|
865
|
-
if (this.atOp("!")) {
|
|
866
|
-
this.next();
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1184
|
+
if (this.atOp("!") || this.atOp("-") || this.at("kw", "is") || this.at("kw", "not")) {
|
|
1185
|
+
const tok = this.next();
|
|
1186
|
+
const expr = this.parseUnary();
|
|
1187
|
+
const span = [tok.pos, expr.span[1]];
|
|
1188
|
+
// `is x` ≡ `!!x`, `not x` ≡ `!x`: pure sugar, both nodes spanning the sugar.
|
|
1189
|
+
if (tok.type === "kw") {
|
|
1190
|
+
const not = { kind: "unary", span, op: "!", expr };
|
|
1191
|
+
return tok.value === "is" ? { kind: "unary", span, op: "!", expr: not } : not;
|
|
1192
|
+
}
|
|
1193
|
+
return { kind: "unary", span, op: tok.value, expr };
|
|
872
1194
|
}
|
|
873
1195
|
return this.parsePostfix();
|
|
874
1196
|
}
|
|
875
1197
|
parsePostfix(left) {
|
|
876
1198
|
let expr = left ?? this.parsePrimary();
|
|
1199
|
+
const start = expr.span[0];
|
|
877
1200
|
for (;;) {
|
|
878
1201
|
if (this.at("dot")) {
|
|
879
1202
|
this.next();
|
|
@@ -881,25 +1204,45 @@ class Parser {
|
|
|
881
1204
|
this.fail("expected a property name after '.'");
|
|
882
1205
|
const name = this.next().value;
|
|
883
1206
|
if (this.at("lparen")) {
|
|
884
|
-
|
|
1207
|
+
const args = this.parseArgs();
|
|
1208
|
+
expr = { kind: "call", span: this.sp(start), recv: expr, name, args };
|
|
885
1209
|
}
|
|
886
1210
|
else {
|
|
887
|
-
expr = { kind: "member", recv: expr, name };
|
|
1211
|
+
expr = { kind: "member", span: this.sp(start), recv: expr, name };
|
|
888
1212
|
}
|
|
889
1213
|
}
|
|
890
1214
|
else if (this.at("lparen") && left === undefined && expr.kind === "ident") {
|
|
891
1215
|
// free function call: name(args) — a bare name read here, not `(f)(x)`
|
|
892
|
-
|
|
1216
|
+
const args = this.parseArgs();
|
|
1217
|
+
expr = { kind: "call", span: this.sp(start), recv: null, name: expr.name, args };
|
|
1218
|
+
}
|
|
1219
|
+
else if (this.at("lbracket")) {
|
|
1220
|
+
expr = this.parseBracket(expr);
|
|
893
1221
|
}
|
|
894
|
-
else if (this.
|
|
895
|
-
|
|
1222
|
+
else if (this.atOp("!")) {
|
|
1223
|
+
// Postfix `!`: a `!` right after a complete chain is `required` — OQX
|
|
1224
|
+
// never places two operands side by side, so it cannot begin one here.
|
|
1225
|
+
const bang = this.next();
|
|
1226
|
+
expr = { kind: "required", span: [start, bang.end], expr };
|
|
1227
|
+
}
|
|
1228
|
+
else if (this.atValueDirective()) {
|
|
1229
|
+
// A value-position directive, `recv first { … }` — how a bracket chain
|
|
1230
|
+
// prints back; exists/none/count have no value form and are left to
|
|
1231
|
+
// the body's own error.
|
|
1232
|
+
if (expr.kind === "lit")
|
|
1233
|
+
this.fail(`\`${describe(expr)}\` is a literal, not a collection`);
|
|
1234
|
+
expr = this.parseConsumerBlock(expr, this.atConsumerBlock(), false);
|
|
896
1235
|
}
|
|
897
1236
|
else {
|
|
898
|
-
break;
|
|
1237
|
+
break; // a bare `{` is a consumer block boundary — not part of a value expression; anything else ends the chain too
|
|
899
1238
|
}
|
|
900
1239
|
}
|
|
901
1240
|
return expr;
|
|
902
1241
|
}
|
|
1242
|
+
atValueDirective() {
|
|
1243
|
+
const op = this.atConsumerBlock();
|
|
1244
|
+
return op === "collect" || op === "first" || op === "single";
|
|
1245
|
+
}
|
|
903
1246
|
parseArgs() {
|
|
904
1247
|
this.next(); // '('
|
|
905
1248
|
const args = [];
|
|
@@ -921,23 +1264,24 @@ class Parser {
|
|
|
921
1264
|
// select-item head `^name:` is a lift, handled in parseSelectItem; here, in
|
|
922
1265
|
// expression position, `^` reads an enclosing row's field even when the
|
|
923
1266
|
// current row shadows the name.)
|
|
924
|
-
if (t.type === "caret") {
|
|
925
|
-
const levels = this.
|
|
1267
|
+
if (t.type === "caret" || this.atAbsoluteRef()) {
|
|
1268
|
+
const levels = this.parseScopeRef();
|
|
926
1269
|
if (!this.at("ident"))
|
|
927
1270
|
this.fail("expected an identifier after '^' (an outer reference)");
|
|
928
|
-
|
|
1271
|
+
const name = this.next();
|
|
1272
|
+
return { kind: "outer", span: [t.pos, name.end], levels, name: name.value };
|
|
929
1273
|
}
|
|
930
1274
|
if (t.type === "number") {
|
|
931
1275
|
this.next();
|
|
932
|
-
return { kind: "lit", value: Number(t.value) };
|
|
1276
|
+
return { kind: "lit", span: [t.pos, t.end], value: Number(t.value) };
|
|
933
1277
|
}
|
|
934
1278
|
if (t.type === "string") {
|
|
935
1279
|
this.next();
|
|
936
|
-
return { kind: "lit", value: t.value };
|
|
1280
|
+
return { kind: "lit", span: [t.pos, t.end], value: t.value };
|
|
937
1281
|
}
|
|
938
1282
|
if (t.type === "binding") {
|
|
939
1283
|
this.next();
|
|
940
|
-
return
|
|
1284
|
+
return this.bindingNode(t);
|
|
941
1285
|
}
|
|
942
1286
|
if (t.type === "lparen") {
|
|
943
1287
|
this.next();
|
|
@@ -945,17 +1289,18 @@ class Parser {
|
|
|
945
1289
|
if (!this.at("rparen"))
|
|
946
1290
|
this.fail("expected ')'");
|
|
947
1291
|
this.next();
|
|
948
|
-
return e;
|
|
1292
|
+
return withSpan(e, [t.pos, this.end()]);
|
|
949
1293
|
}
|
|
950
1294
|
if (t.type === "ident") {
|
|
951
1295
|
this.next();
|
|
1296
|
+
const span = [t.pos, t.end];
|
|
952
1297
|
if (t.value === "true")
|
|
953
|
-
return { kind: "lit", value: true };
|
|
1298
|
+
return { kind: "lit", span, value: true };
|
|
954
1299
|
if (t.value === "false")
|
|
955
|
-
return { kind: "lit", value: false };
|
|
1300
|
+
return { kind: "lit", span, value: false };
|
|
956
1301
|
if (t.value === "null")
|
|
957
|
-
return { kind: "lit", value: null };
|
|
958
|
-
return { kind: "ident", name: t.value };
|
|
1302
|
+
return { kind: "lit", span, value: null };
|
|
1303
|
+
return { kind: "ident", span, name: t.value };
|
|
959
1304
|
}
|
|
960
1305
|
this.fail(`unexpected ${this.tokDesc()} — expected a value`);
|
|
961
1306
|
}
|
|
@@ -967,25 +1312,45 @@ function navKey(e) {
|
|
|
967
1312
|
case "ident": return e.name;
|
|
968
1313
|
case "outer": return e.name;
|
|
969
1314
|
case "member": return e.name;
|
|
1315
|
+
case "required": return navKey(e.expr);
|
|
970
1316
|
default: return null;
|
|
971
1317
|
}
|
|
972
1318
|
}
|
|
973
|
-
//
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1319
|
+
// Whether a block's leading run, read as a where tree, is syntactically a
|
|
1320
|
+
// predicate (GRAMMAR §2): anything but a bare name, a dotted navigation or a lift
|
|
1321
|
+
// (an `outer`), with a parenthesized leading expression always a predicate.
|
|
1322
|
+
function predicateShaped(w, first) {
|
|
1323
|
+
if (first.type === "lparen")
|
|
1324
|
+
return true;
|
|
1325
|
+
if (w.kind !== "scalar")
|
|
1326
|
+
return true;
|
|
1327
|
+
return !isNavigation(w.expr);
|
|
1328
|
+
}
|
|
1329
|
+
function isNavigation(e) {
|
|
1330
|
+
switch (e.kind) {
|
|
1331
|
+
case "ident":
|
|
1332
|
+
case "outer": return true;
|
|
1333
|
+
case "member": return isNavigation(e.recv);
|
|
1334
|
+
default: return false;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
// The canonical text of a node for an error message; a binding shows as its
|
|
1338
|
+
// `${n}` marker (the printer refuses to turn a bound value into source).
|
|
1339
|
+
function describe(node) {
|
|
1340
|
+
const t = printTemplate(node);
|
|
1341
|
+
return t.strings.map((s, i) => (i < t.count ? `${s}\${${t.indices[i]}}` : s)).join("");
|
|
1342
|
+
}
|
|
1343
|
+
// `!` applied once per recorded `!` position to a where node, innermost first.
|
|
1344
|
+
function wrapNot(w, notStarts) {
|
|
1345
|
+
for (let i = notStarts.length - 1; i >= 0; i--)
|
|
1346
|
+
w = { kind: "not", span: [notStarts[i], w.span[1]], expr: w };
|
|
977
1347
|
return w;
|
|
978
1348
|
}
|
|
979
1349
|
// A scalar where leaf. A leading `!` on the whole leaf becomes a `not` node (so
|
|
980
1350
|
// `where !active` keeps its shape); inside a comparison it stays a unary `!`.
|
|
981
1351
|
function scalarLeaf(e) {
|
|
982
1352
|
if (e.kind === "unary" && e.op === "!")
|
|
983
|
-
return { kind: "not", expr: scalarLeaf(e.expr) };
|
|
984
|
-
return { kind: "scalar", expr: e };
|
|
985
|
-
}
|
|
986
|
-
// The optional `limit`/`offset` fields of a Query/Subquery, present only when set
|
|
987
|
-
// (so a Query built without them is unchanged).
|
|
988
|
-
function bounds(b) {
|
|
989
|
-
return { ...(b.limit ? { limit: b.limit } : {}), ...(b.offset ? { offset: b.offset } : {}) };
|
|
1353
|
+
return { kind: "not", span: e.span, expr: scalarLeaf(e.expr) };
|
|
1354
|
+
return { kind: "scalar", span: e.span, expr: e };
|
|
990
1355
|
}
|
|
991
1356
|
//# sourceMappingURL=parser.js.map
|