@omgbase/oqx 0.14.1 → 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 +41 -0
- package/README.md +192 -34
- package/dist/ast.d.ts +77 -23
- 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 +55 -26
- 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 +1 -1
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +7 -5
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/optimize/ir.d.ts +3 -3
- 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 +5 -3
- 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/semantics.js +1 -1
- package/dist/semantics.js.map +1 -1
- 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 +105 -36
- package/src/build.ts +72 -0
- package/src/context.ts +16 -3
- package/src/engine.ts +57 -28
- package/src/errors.ts +3 -2
- package/src/index.ts +38 -13
- package/src/lexer.ts +102 -51
- package/src/optimize/analysis.ts +6 -5
- package/src/optimize/ir.ts +5 -5
- package/src/parser.ts +573 -225
- package/src/plan.ts +4 -3
- package/src/print.ts +274 -0
- package/src/resolve.ts +305 -0
- package/src/semantics.ts +1 -1
- package/src/version.ts +4 -0
- package/src/walk.ts +180 -0
package/src/parser.ts
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
|
|
@@ -46,7 +71,10 @@ import { lexTemplate, lexString } from "./lexer.ts";
|
|
|
46
71
|
import { OqxError } from "./errors.ts";
|
|
47
72
|
import type {
|
|
48
73
|
Query, Where, Expr, OpNode, Subquery, SelectItem, OrderSpec, Follow, FollowDestination, Consumer, RelOp,
|
|
74
|
+
BinaryOp, Span,
|
|
49
75
|
} from "./ast.ts";
|
|
76
|
+
import { resolveSelect, resolveWhere } from "./resolve.ts";
|
|
77
|
+
import { printTemplate } from "./print.ts";
|
|
50
78
|
|
|
51
79
|
const CONSUMERS = new Set<string>(["collect", "exists", "none", "count", "first", "single"]);
|
|
52
80
|
// The fixed clause order of a body (ADR-020). Each clause appears at most once.
|
|
@@ -100,14 +128,37 @@ interface BodyClauses {
|
|
|
100
128
|
offset: Expr | null;
|
|
101
129
|
}
|
|
102
130
|
|
|
131
|
+
// A copy of a node with another span (a parenthesized operand takes the span of
|
|
132
|
+
// its parentheses).
|
|
133
|
+
function withSpan<T extends { span: Span }>(node: T, span: Span): T {
|
|
134
|
+
return { ...node, span };
|
|
135
|
+
}
|
|
136
|
+
|
|
103
137
|
class Parser {
|
|
104
138
|
private tokens: Token[];
|
|
105
139
|
private pos = 0;
|
|
140
|
+
/** While a block's leading run is read speculatively as a predicate, a bare
|
|
141
|
+
* `first`/`single` leaf is not rejected on sight: the body decides after the
|
|
142
|
+
* run is classified (`rejectBareLookups`). */
|
|
143
|
+
private deferLookup = false;
|
|
144
|
+
/** The scope depth (SEMANTICS §2) at which the construct being parsed is
|
|
145
|
+
* evaluated: 0 at the root (the top-level source and bounds), 1 for a
|
|
146
|
+
* top-level row's clauses, one deeper per block body and per follow `where`.
|
|
147
|
+
* An absolute reference `N^name` reads `depth − N` scopes out. */
|
|
148
|
+
private depth = 0;
|
|
106
149
|
|
|
107
150
|
constructor(tokens: Token[]) {
|
|
108
151
|
this.tokens = tokens;
|
|
109
152
|
}
|
|
110
153
|
|
|
154
|
+
/** Run `f` with the scope depth set to `d`, restoring it afterwards (also on
|
|
155
|
+
* the error path, so a speculative read that fails leaves the depth intact). */
|
|
156
|
+
private atDepth<T>(d: number, f: () => T): T {
|
|
157
|
+
const saved = this.depth;
|
|
158
|
+
this.depth = d;
|
|
159
|
+
try { return f(); } finally { this.depth = saved; }
|
|
160
|
+
}
|
|
161
|
+
|
|
111
162
|
// ---- cursor helpers -------------------------------------------------------
|
|
112
163
|
private peek(): Token { return this.tokens[this.pos]!; }
|
|
113
164
|
private peekAt(n: number): Token | undefined { return this.tokens[this.pos + n]; }
|
|
@@ -123,26 +174,46 @@ class Parser {
|
|
|
123
174
|
private fail(msg: string): never {
|
|
124
175
|
throw new OqxError(`${msg} (at offset ${this.peek().pos})`, "parse");
|
|
125
176
|
}
|
|
177
|
+
/** Code-point offset of the next token (the start of a node about to be parsed). */
|
|
178
|
+
private start(): number { return this.peek().pos; }
|
|
179
|
+
/** Code-point offset just past the last consumed token (the end of a node just parsed). */
|
|
180
|
+
private end(): number { return this.pos === 0 ? 0 : this.tokens[this.pos - 1]!.end; }
|
|
181
|
+
private sp(start: number): Span { return [start, this.end()]; }
|
|
126
182
|
|
|
127
183
|
// ---- top level ------------------------------------------------------------
|
|
128
184
|
parseQuery(): Query {
|
|
185
|
+
const start = this.start();
|
|
129
186
|
// Directive form: `<receiver> <consumer> { … }` consuming the whole query.
|
|
130
|
-
|
|
131
|
-
if (directive && this.at("eof")) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
};
|
|
187
|
+
let directive = this.tryOp(false);
|
|
188
|
+
if (directive && !this.at("eof") && (this.at("lbracket") || this.atValueDirective())) {
|
|
189
|
+
// A longer chain whose outermost node is still a directive
|
|
190
|
+
// (`people first { } first { }` — how `people[p][0]` prints back).
|
|
191
|
+
const save = this.pos;
|
|
192
|
+
try {
|
|
193
|
+
const e = this.parsePostfix(directive);
|
|
194
|
+
if (e.kind === "op" && this.at("eof")) directive = e;
|
|
195
|
+
else this.pos = save;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (!(err instanceof OqxError)) throw err;
|
|
198
|
+
this.pos = save;
|
|
199
|
+
}
|
|
144
200
|
}
|
|
201
|
+
if (directive && this.at("eof")) return this.queryFromOp(directive, start);
|
|
145
202
|
if (directive) this.fail(`unexpected ${this.tokDesc()} after the top-level directive`);
|
|
203
|
+
// A bracket chain whose outermost node is a directive is the whole query too
|
|
204
|
+
// (`people[age > 40]` ≡ `people first { where age > 40 }`). Read
|
|
205
|
+
// speculatively: anything else (a parse error, a longer expression) is the
|
|
206
|
+
// body form's business.
|
|
207
|
+
if (this.at("ident") || this.at("caret") || this.at("binding")) {
|
|
208
|
+
const save = this.pos;
|
|
209
|
+
try {
|
|
210
|
+
const e = this.parsePostfix();
|
|
211
|
+
if (e.kind === "op" && this.at("eof")) return this.queryFromOp(e, start);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
if (!(err instanceof OqxError)) throw err;
|
|
214
|
+
}
|
|
215
|
+
this.pos = save;
|
|
216
|
+
}
|
|
146
217
|
|
|
147
218
|
// Body form: the fixed clause list; its `from` is the source.
|
|
148
219
|
const body = this.parseBody(TOP_CTX);
|
|
@@ -151,6 +222,8 @@ class Parser {
|
|
|
151
222
|
this.fail("a query must name its source with `from <collection>` (or be `<collection> <consumer> { … }`)");
|
|
152
223
|
}
|
|
153
224
|
return {
|
|
225
|
+
kind: "query",
|
|
226
|
+
span: this.sp(start),
|
|
154
227
|
source: body.froms[0]!,
|
|
155
228
|
from: body.froms.slice(1),
|
|
156
229
|
where: body.where,
|
|
@@ -160,7 +233,18 @@ class Parser {
|
|
|
160
233
|
follow: body.follow,
|
|
161
234
|
distinct: body.distinct,
|
|
162
235
|
values: body.values,
|
|
163
|
-
|
|
236
|
+
limit: body.limit,
|
|
237
|
+
offset: body.offset,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// The whole-query form of a directive: its receiver is the source, its block
|
|
242
|
+
// the body, its consumer the query's.
|
|
243
|
+
private queryFromOp(e: OpNode, start: number): Query {
|
|
244
|
+
return {
|
|
245
|
+
kind: "query", span: this.sp(start), source: e.receiver, from: e.sub.from, where: e.sub.where, select: e.sub.select,
|
|
246
|
+
orderBy: e.sub.orderBy, consumer: e.op, follow: e.sub.follow, distinct: e.distinct, values: e.sub.values,
|
|
247
|
+
limit: e.sub.limit, offset: e.sub.offset,
|
|
164
248
|
};
|
|
165
249
|
}
|
|
166
250
|
|
|
@@ -185,6 +269,10 @@ class Parser {
|
|
|
185
269
|
let limit: Expr | null = null;
|
|
186
270
|
let offset: Expr | null = null;
|
|
187
271
|
let stage = -1;
|
|
272
|
+
// A top-level body's row clauses (select/where/follow/order by) are read at
|
|
273
|
+
// depth 1, its source and bounds at the root (0); a block's clauses are all
|
|
274
|
+
// at the block's depth, which `parseConsumerBlock` set.
|
|
275
|
+
const rowDepth = ctx.top ? 1 : this.depth;
|
|
188
276
|
|
|
189
277
|
const enter = (clause: Clause): void => {
|
|
190
278
|
const idx = CLAUSE_ORDER.indexOf(clause);
|
|
@@ -200,7 +288,7 @@ class Parser {
|
|
|
200
288
|
enter("select");
|
|
201
289
|
this.next();
|
|
202
290
|
if (this.at("ident", "distinct")) { this.next(); distinct = true; }
|
|
203
|
-
({ items: select, values } = this.parseProjection(ctx));
|
|
291
|
+
({ items: select, values } = this.atDepth(rowDepth, () => this.parseProjection(ctx)));
|
|
204
292
|
continue;
|
|
205
293
|
}
|
|
206
294
|
if (this.at("kw", "from")) {
|
|
@@ -212,18 +300,18 @@ class Parser {
|
|
|
212
300
|
if (this.at("kw", "where")) {
|
|
213
301
|
enter("where");
|
|
214
302
|
this.next();
|
|
215
|
-
where = this.parseWhere();
|
|
303
|
+
where = this.atDepth(rowDepth, () => this.parseWhere());
|
|
216
304
|
continue;
|
|
217
305
|
}
|
|
218
306
|
if (this.atFollow()) {
|
|
219
307
|
enter("follow");
|
|
220
|
-
follow = this.parseFollow();
|
|
308
|
+
follow = this.atDepth(rowDepth, () => this.parseFollow());
|
|
221
309
|
continue;
|
|
222
310
|
}
|
|
223
311
|
if (this.atOrderBy()) {
|
|
224
312
|
enter("order by");
|
|
225
313
|
this.next(); this.next(); // `order` `by`
|
|
226
|
-
orderBy = this.parseOrderSpecs();
|
|
314
|
+
orderBy = this.atDepth(rowDepth, () => this.parseOrderSpecs());
|
|
227
315
|
continue;
|
|
228
316
|
}
|
|
229
317
|
if (this.atBound()) {
|
|
@@ -240,19 +328,76 @@ class Parser {
|
|
|
240
328
|
continue;
|
|
241
329
|
}
|
|
242
330
|
// A keyword-less run. Before any clause it is the projection (`select` is
|
|
243
|
-
// the one keyword that may be dropped, and only in first position)
|
|
244
|
-
//
|
|
331
|
+
// the one keyword that may be dropped, and only in first position) — or,
|
|
332
|
+
// in a block, a leading predicate that opens a `where`-first body. After
|
|
333
|
+
// any clause it is an error.
|
|
245
334
|
if (stage === -1 && (this.at("ident") || this.at("binding") || this.at("caret") || this.canStartValue())) {
|
|
246
|
-
|
|
247
|
-
(
|
|
335
|
+
const lead = this.atDepth(rowDepth, () => this.parseLeadingRun(ctx));
|
|
336
|
+
if (lead.where) {
|
|
337
|
+
enter("where");
|
|
338
|
+
where = lead.where;
|
|
339
|
+
} else {
|
|
340
|
+
enter("select");
|
|
341
|
+
({ items: select, values } = lead.projection!);
|
|
342
|
+
}
|
|
248
343
|
continue;
|
|
249
344
|
}
|
|
250
345
|
this.failUnexpectedInBody(stage, ctx);
|
|
251
346
|
}
|
|
252
|
-
|
|
347
|
+
// Validate the alias references of this body (cycles, a block alias inside a
|
|
348
|
+
// `where` expression, a `select` item naming an item to its right); the
|
|
349
|
+
// surface form is kept. `where` first: its cycle report follows its own path.
|
|
350
|
+
if (where) resolveWhere(select, where, (msg) => this.fail(msg));
|
|
351
|
+
resolveSelect(select, (msg) => this.fail(msg));
|
|
253
352
|
return { froms, where, select, orderBy, follow, distinct, values, limit, offset };
|
|
254
353
|
}
|
|
255
354
|
|
|
355
|
+
// The keyword-less run that opens a body. At the top level it is always the
|
|
356
|
+
// projection. In a block it is a `where`-first body when the leading expression
|
|
357
|
+
// is SYNTACTICALLY a predicate (GRAMMAR §2): the run is read as a where tree
|
|
358
|
+
// first; if that tree is predicate-shaped — anything but a bare name, a dotted
|
|
359
|
+
// navigation or a lift, with a parenthesized expression counting as a
|
|
360
|
+
// predicate — and is not the item of a `values` projection, it is the `where`.
|
|
361
|
+
// Otherwise the tokens are re-read as the projection. When both readings fail,
|
|
362
|
+
// the error that got further wins (the farthest-failure rule), so a mistake
|
|
363
|
+
// deep inside `{ jobs exists { … } }` is reported where it is.
|
|
364
|
+
private parseLeadingRun(ctx: BodyCtx): { where: Where | null; projection: { items: SelectItem[]; values: boolean } | null } {
|
|
365
|
+
if (!ctx.top) {
|
|
366
|
+
const save = this.pos;
|
|
367
|
+
const first = this.peek();
|
|
368
|
+
let w: Where | null = null;
|
|
369
|
+
let specErr: OqxError | null = null;
|
|
370
|
+
let specPos = -1;
|
|
371
|
+
this.deferLookup = true;
|
|
372
|
+
try {
|
|
373
|
+
w = this.parseWhere();
|
|
374
|
+
} catch (e) {
|
|
375
|
+
if (!(e instanceof OqxError)) throw e;
|
|
376
|
+
specErr = e;
|
|
377
|
+
specPos = this.pos;
|
|
378
|
+
} finally {
|
|
379
|
+
this.deferLookup = false;
|
|
380
|
+
}
|
|
381
|
+
if (w && predicateShaped(w, first) && !this.at("ident", "values")) {
|
|
382
|
+
if (this.at("comma")) {
|
|
383
|
+
this.fail(`a leading predicate starts a \`where\`-first body, which has no projection — write the projection first: \`{ <projection> where ${describe(w)} }\``);
|
|
384
|
+
}
|
|
385
|
+
this.rejectBareLookups(w);
|
|
386
|
+
return { where: w, projection: null };
|
|
387
|
+
}
|
|
388
|
+
this.pos = save;
|
|
389
|
+
if (specErr) {
|
|
390
|
+
try {
|
|
391
|
+
return { where: null, projection: this.parseProjection(ctx) };
|
|
392
|
+
} catch (e) {
|
|
393
|
+
if (e instanceof OqxError && this.pos < specPos) throw specErr;
|
|
394
|
+
throw e;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return { where: null, projection: this.parseProjection(ctx) };
|
|
399
|
+
}
|
|
400
|
+
|
|
256
401
|
// The error for a token that starts no clause, phrased for the mistake it most
|
|
257
402
|
// likely is: a bare run right after `from` (the old implicit `where`, or a
|
|
258
403
|
// consumer word where a whole-query directive was meant), or a stray token.
|
|
@@ -283,10 +428,10 @@ class Parser {
|
|
|
283
428
|
if (last === "from") {
|
|
284
429
|
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}`);
|
|
285
430
|
}
|
|
286
|
-
// `{ rel exists { … } }
|
|
287
|
-
//
|
|
431
|
+
// `{ name, rel exists { … } }`: the leading items were read as the projection,
|
|
432
|
+
// so the consumer word is where the mistake shows.
|
|
288
433
|
if (last === "select" && t.type === "ident" && CONSUMERS.has(t.value)) {
|
|
289
|
-
this.fail(`unexpected \`${t.value}\` after a projection — a consumer test is a predicate: write \`where <relation> ${t.value} { … }\`
|
|
434
|
+
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 { … }\`)`);
|
|
290
435
|
}
|
|
291
436
|
if (last === null) this.fail(`unexpected ${this.tokDesc()} — expected a projection or ${remaining}`);
|
|
292
437
|
this.fail(`unexpected ${this.tokDesc()} after \`${last}\` — expected ${remaining}`);
|
|
@@ -302,74 +447,39 @@ class Parser {
|
|
|
302
447
|
return !!nx && (nx.type === "number" || nx.type === "binding" || nx.type === "caret");
|
|
303
448
|
}
|
|
304
449
|
|
|
305
|
-
//
|
|
306
|
-
// `
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
case "ident": {
|
|
333
|
-
const a = aliases.get(e.name);
|
|
334
|
-
if (!a) return e;
|
|
335
|
-
if (resolving[resolving.length - 1] === e.name) return e; // its own name inside its own expression: the row field
|
|
336
|
-
if (resolving.includes(e.name)) {
|
|
337
|
-
const cycle = [...resolving.slice(resolving.indexOf(e.name)), e.name].join(" → ");
|
|
338
|
-
this.fail(`select aliases form a cycle: ${cycle} — an alias used in \`where\` cannot depend on itself`);
|
|
339
|
-
}
|
|
340
|
-
if (a.kind === "collect") {
|
|
341
|
-
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`);
|
|
342
|
-
}
|
|
343
|
-
resolving.push(e.name);
|
|
344
|
-
const out = subst(a.expr);
|
|
345
|
-
resolving.pop();
|
|
346
|
-
return out;
|
|
347
|
-
}
|
|
348
|
-
case "member": return { ...e, recv: subst(e.recv) };
|
|
349
|
-
case "index": return { ...e, recv: subst(e.recv), index: subst(e.index) };
|
|
350
|
-
case "call": return { ...e, recv: e.recv ? subst(e.recv) : null, args: e.args.map(subst) };
|
|
351
|
-
case "unary": return { ...e, expr: subst(e.expr) };
|
|
352
|
-
case "binary": case "logical": case "in": return { ...e, left: subst(e.left), right: subst(e.right) };
|
|
353
|
-
case "range": return { ...e, lo: e.lo ? subst(e.lo) : null, hi: e.hi ? subst(e.hi) : null };
|
|
354
|
-
default: return e; // lit, binding, outer (`^name` reads an enclosing row, never an alias)
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
const walk = (w: Where): Where => {
|
|
358
|
-
switch (w.kind) {
|
|
359
|
-
case "and": return { kind: "and", parts: w.parts.map(walk) };
|
|
360
|
-
case "or": return { kind: "or", parts: w.parts.map(walk) };
|
|
361
|
-
case "not": return { kind: "not", expr: walk(w.expr) };
|
|
362
|
-
case "scalar": {
|
|
363
|
-
if (w.expr.kind === "ident") {
|
|
364
|
-
const a = aliases.get(w.expr.name);
|
|
365
|
-
if (a?.kind === "collect") return a.op; // a collection in predicate position: non-empty
|
|
366
|
-
}
|
|
367
|
-
return { kind: "scalar", expr: subst(w.expr) };
|
|
368
|
-
}
|
|
369
|
-
case "op": return { ...w, receiver: subst(w.receiver) }; // the receiver is read in this scope; the block is its own scope
|
|
450
|
+
// Whether the token `n` ahead can head a scope reference: a `^` run, or the
|
|
451
|
+
// `N^` of an absolute reference.
|
|
452
|
+
private atScopeRefAt(n: number): boolean {
|
|
453
|
+
return this.peekAt(n)?.type === "caret" || this.atAbsoluteRefAt(n);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// `N^` — an unsigned integer literal (digits only) immediately followed by a
|
|
457
|
+
// caret, no whitespace between: an absolute scope reference (GRAMMAR §4).
|
|
458
|
+
// `1 ^x` is not one (the `^` then fails as a stray token), nor is `1.0^x`.
|
|
459
|
+
private atAbsoluteRef(): boolean { return this.atAbsoluteRefAt(0); }
|
|
460
|
+
private atAbsoluteRefAt(n: number): boolean {
|
|
461
|
+
const t = this.peekAt(n);
|
|
462
|
+
const nx = this.peekAt(n + 1);
|
|
463
|
+
return t?.type === "number" && /^\d+$/.test(t.value) && nx?.type === "caret" && nx.pos === t.end;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// The head of an outer reference or a lift: a run of carets (one scope out
|
|
467
|
+
// per caret), or an absolute reference `N^` (scope N, GRAMMAR §4), both as the
|
|
468
|
+
// number of scopes out from the current depth. Returns 0 when there is none.
|
|
469
|
+
// The absolute form is checked before anything is consumed, so a failing
|
|
470
|
+
// lookahead (`tryOp`) leaves the cursor on the integer.
|
|
471
|
+
private parseScopeRef(): number {
|
|
472
|
+
if (this.atAbsoluteRef()) {
|
|
473
|
+
const n = this.peek();
|
|
474
|
+
const target = Number(n.value);
|
|
475
|
+
if (target >= this.depth) {
|
|
476
|
+
this.fail(`scope ${n.value} does not enclose this block (the current scope is depth ${this.depth})`);
|
|
370
477
|
}
|
|
371
|
-
|
|
372
|
-
|
|
478
|
+
this.next(); // the integer
|
|
479
|
+
this.next(); // the caret
|
|
480
|
+
return this.depth - target;
|
|
481
|
+
}
|
|
482
|
+
return this.parseCarets();
|
|
373
483
|
}
|
|
374
484
|
|
|
375
485
|
private atOrderBy(): boolean {
|
|
@@ -383,11 +493,17 @@ class Parser {
|
|
|
383
493
|
private atFollow(): boolean {
|
|
384
494
|
if (!this.at("ident", "follow")) return false;
|
|
385
495
|
const nx = this.peekAt(1);
|
|
386
|
-
return !!nx && (nx.type === "ident" || nx.type === "binding" ||
|
|
496
|
+
return !!nx && (nx.type === "ident" || nx.type === "binding" || this.atScopeRefAt(1));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Whether the cursor is at something that can begin a `follow` destination.
|
|
500
|
+
private atDestinationStart(): boolean {
|
|
501
|
+
return this.at("ident") || this.at("binding") || this.atScopeRefAt(0);
|
|
387
502
|
}
|
|
388
503
|
|
|
389
504
|
// ---- follow ---------------------------------------------------------------
|
|
390
505
|
private parseFollow(): Follow {
|
|
506
|
+
const start = this.start();
|
|
391
507
|
this.next(); // `follow`
|
|
392
508
|
let distinct = false;
|
|
393
509
|
// After `follow`, `distinct` is a keyword (a relation literally named
|
|
@@ -395,26 +511,27 @@ class Parser {
|
|
|
395
511
|
if (this.at("ident", "distinct")) {
|
|
396
512
|
this.next();
|
|
397
513
|
distinct = true;
|
|
398
|
-
if (!this.
|
|
514
|
+
if (!this.atDestinationStart()) {
|
|
399
515
|
this.fail("expected a relation after `follow distinct` (`follow distinct <relation>`)");
|
|
400
516
|
}
|
|
401
517
|
}
|
|
402
518
|
const destinations: FollowDestination[] = [this.parseFollowDestination()];
|
|
403
519
|
while (this.at("comma")) {
|
|
404
520
|
this.next();
|
|
405
|
-
if (!this.
|
|
521
|
+
if (!this.atDestinationStart()) {
|
|
406
522
|
this.fail("expected a relation after ',' (`follow <relation>, <relation>`)");
|
|
407
523
|
}
|
|
408
524
|
destinations.push(this.parseFollowDestination());
|
|
409
525
|
}
|
|
410
|
-
const follow: Follow = { destinations, distinct, where: null, frontier: null, depth: null, by: null };
|
|
526
|
+
const follow: Follow = { kind: "follow", span: this.sp(start), destinations, distinct, where: null, frontier: null, depth: null, by: null };
|
|
411
527
|
if (!this.at("lbrace")) return follow;
|
|
412
528
|
this.next(); // '{'
|
|
413
529
|
while (!this.at("eof") && !this.at("rbrace")) {
|
|
414
530
|
if (this.at("kw", "where")) {
|
|
415
531
|
if (follow.where) this.fail("duplicate `where` in follow clause");
|
|
416
532
|
this.next();
|
|
417
|
-
|
|
533
|
+
// Read in the successor's scope, one deeper than the frontier row.
|
|
534
|
+
follow.where = this.atDepth(this.depth + 1, () => this.parseValueExpr());
|
|
418
535
|
} else if (this.at("ident", "frontier")) {
|
|
419
536
|
if (follow.frontier) this.fail("duplicate `frontier` in follow clause");
|
|
420
537
|
this.next();
|
|
@@ -431,11 +548,12 @@ class Parser {
|
|
|
431
548
|
if (!Number.isInteger(v) || v < 1 || v > 8) this.fail("follow depth must be an integer between 1 and 8");
|
|
432
549
|
follow.depth = v;
|
|
433
550
|
} else {
|
|
434
|
-
this.fail(`unexpected ${this.tokDesc()} in follow block — expected where/frontier/depth/by`);
|
|
551
|
+
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 { … }\`)`);
|
|
435
552
|
}
|
|
436
553
|
}
|
|
437
554
|
if (!this.at("rbrace")) this.fail("expected '}' to close the follow block");
|
|
438
555
|
this.next();
|
|
556
|
+
follow.span = this.sp(start);
|
|
439
557
|
return follow;
|
|
440
558
|
}
|
|
441
559
|
|
|
@@ -451,9 +569,10 @@ class Parser {
|
|
|
451
569
|
const start = this.pos;
|
|
452
570
|
let isReceiver = false;
|
|
453
571
|
if (this.at("binding")) { this.next(); isReceiver = true; }
|
|
454
|
-
else if (this.at("ident") || this.
|
|
455
|
-
this.
|
|
456
|
-
|
|
572
|
+
else if (this.at("ident") || this.atScopeRefAt(0)) {
|
|
573
|
+
const startPos = this.start();
|
|
574
|
+
this.parseScopeRef();
|
|
575
|
+
if (this.at("ident")) { this.parseNavFrom(this.next(), 0, startPos); isReceiver = true; }
|
|
457
576
|
}
|
|
458
577
|
if (isReceiver && this.at("ident") && CONSUMERS.has(this.peek().value)) {
|
|
459
578
|
const op = this.peek().value;
|
|
@@ -471,7 +590,7 @@ class Parser {
|
|
|
471
590
|
}
|
|
472
591
|
}
|
|
473
592
|
this.pos = start;
|
|
474
|
-
if (this.
|
|
593
|
+
if (this.atScopeRefAt(0)) {
|
|
475
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 { … }`)");
|
|
476
595
|
}
|
|
477
596
|
return this.parseReceiver();
|
|
@@ -483,11 +602,16 @@ class Parser {
|
|
|
483
602
|
// named root is only reachable as a receiver through `^` — or a free-function
|
|
484
603
|
// call (`entries(prefs)`), so a computed collection can be consumed directly.
|
|
485
604
|
private parseReceiver(): Expr {
|
|
486
|
-
if (this.at("binding")) return
|
|
487
|
-
const
|
|
605
|
+
if (this.at("binding")) return this.bindingNode(this.next());
|
|
606
|
+
const start = this.start();
|
|
607
|
+
const levels = this.parseScopeRef();
|
|
488
608
|
if (!this.at("ident")) this.fail("expected a collection navigation (a property/relation name)");
|
|
489
609
|
if (LITERAL_WORDS.has(this.peek().value)) this.fail(`\`${this.peek().value}\` is a literal, not a collection`);
|
|
490
|
-
return this.parseNavFrom(this.next(), levels).expr;
|
|
610
|
+
return this.parseNavFrom(this.next(), levels, start).expr;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private bindingNode(t: Token): Expr {
|
|
614
|
+
return { kind: "binding", span: [t.pos, t.end], index: t.index! };
|
|
491
615
|
}
|
|
492
616
|
|
|
493
617
|
// Consume a run of `^` and return its length (0 when there is none).
|
|
@@ -498,17 +622,23 @@ class Parser {
|
|
|
498
622
|
}
|
|
499
623
|
|
|
500
624
|
// A dotted navigation chain from `head`; `levels` > 0 makes the head an outer
|
|
501
|
-
// reference read exactly that many scopes out
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
625
|
+
// reference read exactly that many scopes out (`start` is then the position
|
|
626
|
+
// of the first caret). A bare head followed by `(` is a free-function call
|
|
627
|
+
// (`entries(x)`), which may then be navigated further.
|
|
628
|
+
private parseNavFrom(head: Token, levels = 0, start = head.pos): { expr: Expr; name: string } {
|
|
629
|
+
let expr: Expr = levels > 0
|
|
630
|
+
? { kind: "outer", span: [start, head.end], levels, name: head.value }
|
|
631
|
+
: { kind: "ident", span: [start, head.end], name: head.value };
|
|
505
632
|
let name = head.value;
|
|
506
|
-
if (levels === 0 && this.at("lparen"))
|
|
633
|
+
if (levels === 0 && this.at("lparen")) {
|
|
634
|
+
const args = this.parseArgs();
|
|
635
|
+
expr = { kind: "call", span: this.sp(start), recv: null, name: head.value, args };
|
|
636
|
+
}
|
|
507
637
|
while (this.at("dot")) {
|
|
508
638
|
this.next();
|
|
509
639
|
if (!this.at("ident")) this.fail("expected a property name after '.'");
|
|
510
640
|
name = this.next().value;
|
|
511
|
-
expr = { kind: "member", recv: expr, name };
|
|
641
|
+
expr = { kind: "member", span: this.sp(start), recv: expr, name };
|
|
512
642
|
}
|
|
513
643
|
return { expr, name };
|
|
514
644
|
}
|
|
@@ -536,7 +666,11 @@ class Parser {
|
|
|
536
666
|
// `^^x` bind different rows), so the lift depth is part of the key.
|
|
537
667
|
const seen = new Set<string>();
|
|
538
668
|
for (const it of items) {
|
|
539
|
-
if (it.name === "")
|
|
669
|
+
if (it.name === "") {
|
|
670
|
+
if (it.kind === "collect") 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 { … }`");
|
|
671
|
+
if (ctx.top) 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)");
|
|
672
|
+
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 }`)");
|
|
673
|
+
}
|
|
540
674
|
const key = `${"^".repeat(it.kind === "field" ? it.lift : 0)}${it.name}`;
|
|
541
675
|
if (seen.has(key)) this.fail(`duplicate projection name '${it.name}' — each projected item needs its own name (alias one: \`other: expr\`)`);
|
|
542
676
|
seen.add(key);
|
|
@@ -546,12 +680,13 @@ class Parser {
|
|
|
546
680
|
}
|
|
547
681
|
|
|
548
682
|
private parseSelectItem(ctx: BodyCtx): SelectItem {
|
|
683
|
+
const start = this.start();
|
|
549
684
|
// Leading `^`s mark a lift; the count is how many scopes out it binds. A
|
|
550
685
|
// lift is bound by a `collect { … }` in where position and nowhere else — at
|
|
551
686
|
// the top level, in a select-position block, or in an exists/none/count
|
|
552
687
|
// block it would silently do nothing (or act as a plain field), so it is an
|
|
553
|
-
// error there.
|
|
554
|
-
const lift = this.
|
|
688
|
+
// error there. `N^name:` is the lift into scope N (GRAMMAR §4).
|
|
689
|
+
const lift = this.parseScopeRef();
|
|
555
690
|
if (!this.at("ident") && !this.canStartValue()) this.fail("expected a projection name");
|
|
556
691
|
if (lift > 0 && !ctx.liftsAllowed) {
|
|
557
692
|
const what = this.at("ident") ? `^${this.peek().value}` : "^name";
|
|
@@ -562,20 +697,30 @@ class Parser {
|
|
|
562
697
|
this.next(); // ':'
|
|
563
698
|
const op = this.tryOp(false);
|
|
564
699
|
if (op) {
|
|
700
|
+
// `name: jobs first { }.pay`: the directive is the head of a longer value.
|
|
701
|
+
if (this.atScalarContinuation() || this.atValueDirective()) return this.finishItem(name, this.parseOr(op), lift, start);
|
|
565
702
|
if (op.op !== "collect" && op.op !== "first" && op.op !== "single") {
|
|
566
703
|
this.fail(`projection '${name}' must use collect/first/single, not ${op.op} (exists/none/count are where-position tests)`);
|
|
567
704
|
}
|
|
568
|
-
|
|
569
|
-
return { kind: "collect", name, op };
|
|
705
|
+
return this.finishItem(name, op, lift, start);
|
|
570
706
|
}
|
|
571
|
-
|
|
572
|
-
return { kind: "field", name, expr, lift };
|
|
707
|
+
return this.finishItem(name, this.parseValueExpr(), lift, start);
|
|
573
708
|
}
|
|
574
|
-
// Unaliased item: a bare/dotted navigation keys by its last
|
|
575
|
-
// other expression is unnamed ("") — legal only under `values`
|
|
576
|
-
// parseProjection, which sees the whole list).
|
|
709
|
+
// Unaliased item: a bare/dotted navigation (under any `!`) keys by its last
|
|
710
|
+
// segment; any other expression is unnamed ("") — legal only under `values`
|
|
711
|
+
// (checked by parseProjection, which sees the whole list).
|
|
577
712
|
const expr = this.parseValueExpr();
|
|
578
|
-
return
|
|
713
|
+
return this.finishItem(navKey(expr) ?? "", expr, lift, start);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// An item from its value: exactly a directive (written out, or the bracket
|
|
717
|
+
// sugar) is the `collect` item kind; anything else is a `field`.
|
|
718
|
+
private finishItem(name: string, expr: Expr, lift: number, start: number): SelectItem {
|
|
719
|
+
if (expr.kind === "op") {
|
|
720
|
+
if (lift) this.fail(`a lift (^${name}) value must be a scalar expression, not ${expr.op} { … }`);
|
|
721
|
+
return { kind: "collect", span: this.sp(start), name, op: expr };
|
|
722
|
+
}
|
|
723
|
+
return { kind: "field", span: this.sp(start), name, expr, lift };
|
|
579
724
|
}
|
|
580
725
|
|
|
581
726
|
// ---- order by -------------------------------------------------------------
|
|
@@ -586,11 +731,12 @@ class Parser {
|
|
|
586
731
|
}
|
|
587
732
|
|
|
588
733
|
private parseOrderSpec(): OrderSpec {
|
|
734
|
+
const start = this.start();
|
|
589
735
|
const expr = this.parseValueExpr();
|
|
590
736
|
let desc = false;
|
|
591
737
|
if (this.at("ident", "asc")) this.next();
|
|
592
738
|
else if (this.at("ident", "desc")) { this.next(); desc = true; }
|
|
593
|
-
return { expr, desc };
|
|
739
|
+
return { kind: "order", span: this.sp(start), expr, desc };
|
|
594
740
|
}
|
|
595
741
|
|
|
596
742
|
// ---- where boolean tree: or → and → primary --------------------------------
|
|
@@ -600,21 +746,28 @@ class Parser {
|
|
|
600
746
|
private parseWhere(): Where { return this.parseWhereOr(); }
|
|
601
747
|
|
|
602
748
|
private parseWhereOr(): Where {
|
|
749
|
+
const start = this.start();
|
|
603
750
|
const left = this.parseWhereAnd();
|
|
604
|
-
if (!this.
|
|
751
|
+
if (!this.atOr()) return left;
|
|
605
752
|
const parts = [left];
|
|
606
|
-
while (this.
|
|
607
|
-
return { kind: "or", parts };
|
|
753
|
+
while (this.atOr()) { this.next(); parts.push(this.parseWhereAnd()); }
|
|
754
|
+
return { kind: "or", span: this.sp(start), parts };
|
|
608
755
|
}
|
|
609
756
|
|
|
610
757
|
private parseWhereAnd(): Where {
|
|
758
|
+
const start = this.start();
|
|
611
759
|
const left = this.parseWherePrimary();
|
|
612
|
-
if (!this.
|
|
760
|
+
if (!this.atAnd()) return left;
|
|
613
761
|
const parts = [left];
|
|
614
|
-
while (this.
|
|
615
|
-
return { kind: "and", parts };
|
|
762
|
+
while (this.atAnd()) { this.next(); parts.push(this.parseWherePrimary()); }
|
|
763
|
+
return { kind: "and", span: this.sp(start), parts };
|
|
616
764
|
}
|
|
617
765
|
|
|
766
|
+
// `&&` / `and` and `||` / `or` are one operator each (0.17): the words are
|
|
767
|
+
// exact synonyms of the symbols, parsed to the same nodes.
|
|
768
|
+
private atAnd(): boolean { return this.atOp("&&") || this.at("kw", "and"); }
|
|
769
|
+
private atOr(): boolean { return this.atOp("||") || this.at("kw", "or"); }
|
|
770
|
+
|
|
618
771
|
// A where operand: `[!…] ( where )`, `[!…] <receiver> <consumer> { … }`, or a
|
|
619
772
|
// scalar leaf (a cmp-level expression, which handles its own `!`).
|
|
620
773
|
//
|
|
@@ -626,34 +779,72 @@ class Parser {
|
|
|
626
779
|
// predicate.
|
|
627
780
|
private parseWherePrimary(): Where {
|
|
628
781
|
const start = this.pos;
|
|
629
|
-
|
|
630
|
-
|
|
782
|
+
// Leading negations: `!`, `not` (one each) and `is` (two — `is x` ≡ `!!x`).
|
|
783
|
+
const notStarts: number[] = [];
|
|
784
|
+
for (;;) {
|
|
785
|
+
if (this.atOp("!") || this.at("kw", "not")) notStarts.push(this.next().pos);
|
|
786
|
+
else if (this.at("kw", "is")) { const p = this.next().pos; notStarts.push(p, p); }
|
|
787
|
+
else break;
|
|
788
|
+
}
|
|
631
789
|
if (this.at("lparen")) {
|
|
632
|
-
this.next();
|
|
790
|
+
const lparen = this.next().pos;
|
|
633
791
|
const inner = this.parseWhere();
|
|
634
792
|
if (!this.at("rparen")) this.fail("expected ')' to close a grouped where expression");
|
|
635
793
|
this.next();
|
|
794
|
+
const group: Span = [lparen, this.end()];
|
|
636
795
|
if (this.atScalarContinuation()) {
|
|
637
|
-
let e = this.parsePostfix(this.whereToExpr(inner));
|
|
638
|
-
for (let i =
|
|
639
|
-
|
|
796
|
+
let e = this.parsePostfix(withSpan(this.whereToExpr(inner), group));
|
|
797
|
+
for (let i = notStarts.length - 1; i >= 0; i--) e = { kind: "unary", span: [notStarts[i]!, e.span[1]], op: "!", expr: e };
|
|
798
|
+
const expr = this.parseCmp(e);
|
|
799
|
+
return { kind: "scalar", span: expr.span, expr };
|
|
640
800
|
}
|
|
641
|
-
return wrapNot(inner,
|
|
801
|
+
return wrapNot(withSpan(inner, group), notStarts);
|
|
642
802
|
}
|
|
643
803
|
const op = this.tryOp(true);
|
|
644
|
-
|
|
804
|
+
// A `first`/`single` directive is a value, never a test: the scalar grammar
|
|
805
|
+
// reads it (as a value-position directive) with whatever follows it, and
|
|
806
|
+
// `rejectBareLookup` turns a bare one into the lookup error.
|
|
807
|
+
if (op && op.op !== "first" && op.op !== "single") return wrapNot(this.finishWhereOp(op), notStarts);
|
|
645
808
|
// A scalar leaf, re-read from the first `!` so the scalar grammar gives `!`
|
|
646
809
|
// its one precedence (tighter than comparison). A leaf whose whole value is
|
|
647
810
|
// a negation keeps the `not` node shape (`where !active`).
|
|
648
811
|
this.pos = start;
|
|
649
|
-
|
|
812
|
+
const leaf = scalarLeaf(this.parseCmp());
|
|
813
|
+
if (!this.deferLookup) this.rejectBareLookup(leaf);
|
|
814
|
+
return leaf;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// A where leaf that is exactly a `first`/`single` directive (under any `!`) is
|
|
818
|
+
// the select-position lookup error: a lookup is a value, and alone in `where`
|
|
819
|
+
// the writer almost certainly meant `exists`.
|
|
820
|
+
private rejectBareLookup(w: Where): void {
|
|
821
|
+
let core: Where = w;
|
|
822
|
+
while (core.kind === "not") core = core.expr;
|
|
823
|
+
if (core.kind !== "scalar") return;
|
|
824
|
+
let e = core.expr;
|
|
825
|
+
while (e.kind === "unary" && e.op === "!") e = e.expr;
|
|
826
|
+
if (e.kind === "op" && (e.op === "first" || e.op === "single")) {
|
|
827
|
+
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]!\`)`);
|
|
828
|
+
}
|
|
650
829
|
}
|
|
651
830
|
|
|
652
|
-
//
|
|
831
|
+
// `rejectBareLookup` over every leaf of a tree read with `deferLookup` on.
|
|
832
|
+
private rejectBareLookups(w: Where): void {
|
|
833
|
+
switch (w.kind) {
|
|
834
|
+
case "and": case "or": w.parts.forEach((p) => this.rejectBareLookups(p)); return;
|
|
835
|
+
case "not": case "scalar": this.rejectBareLookup(w); return;
|
|
836
|
+
case "op": return;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Whether the token after a `)` (or a directive) continues a scalar
|
|
841
|
+
// expression: an operator, `.`-navigation, a bracket, a postfix `!`, a range,
|
|
842
|
+
// `in`, or `is`.
|
|
653
843
|
private atScalarContinuation(): boolean {
|
|
654
844
|
const t = this.peek();
|
|
655
|
-
if (t.type === "op") return CMP_OPS.has(t.value) || ADD_OPS.has(t.value) || MUL_OPS.has(t.value);
|
|
656
|
-
|
|
845
|
+
if (t.type === "op") return CMP_OPS.has(t.value) || ADD_OPS.has(t.value) || MUL_OPS.has(t.value) || t.value === "!";
|
|
846
|
+
if (t.type === "kw") return t.value === "is";
|
|
847
|
+
return t.type === "dot" || t.type === "range" || t.type === "lbracket" || (t.type === "ident" && t.value === "in");
|
|
657
848
|
}
|
|
658
849
|
|
|
659
850
|
// A parenthesized where-group that turned out to be a scalar operand, as an
|
|
@@ -661,10 +852,10 @@ class Parser {
|
|
|
661
852
|
private whereToExpr(w: Where): Expr {
|
|
662
853
|
switch (w.kind) {
|
|
663
854
|
case "scalar": return w.expr;
|
|
664
|
-
case "not": return { kind: "unary", op: "!", expr: this.whereToExpr(w.expr) };
|
|
855
|
+
case "not": return { kind: "unary", span: w.span, op: "!", expr: this.whereToExpr(w.expr) };
|
|
665
856
|
case "and": case "or": {
|
|
666
857
|
const op = w.kind === "and" ? "&&" : "||";
|
|
667
|
-
return w.parts.map((p) => this.whereToExpr(p)).reduce((l, r) => ({ kind: "logical", op, left: l, right: r }));
|
|
858
|
+
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 }));
|
|
668
859
|
}
|
|
669
860
|
case "op": {
|
|
670
861
|
const hint = w.op === "count" ? ` — write \`<relation> count { … } ${this.peek().value} N\` without the parentheses` : "";
|
|
@@ -689,64 +880,161 @@ class Parser {
|
|
|
689
880
|
const v = Number(this.next().value);
|
|
690
881
|
if (!Number.isInteger(v)) this.fail("count comparison takes an integer");
|
|
691
882
|
op.countCmp = { op: relop, value: v };
|
|
883
|
+
op.span = [op.span[0], this.end()];
|
|
692
884
|
}
|
|
693
885
|
return op;
|
|
694
886
|
}
|
|
695
887
|
|
|
696
|
-
// Detect + parse a postfix consumer op `<receiver> <consumer> { <sub> }
|
|
697
|
-
//
|
|
888
|
+
// Detect + parse a postfix consumer op `<receiver> <consumer> { <sub> }` — or
|
|
889
|
+
// its implicit-`collect` form `<receiver> { <sub> }`. Returns null (rewinding)
|
|
890
|
+
// when the lookahead is not a directive.
|
|
698
891
|
private tryOp(inWhere: boolean): OpNode | null {
|
|
699
892
|
const start = this.pos;
|
|
893
|
+
const startPos = this.start();
|
|
700
894
|
let receiver: Expr;
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
895
|
+
// An absolute reference that does not enclose this scope is an error only
|
|
896
|
+
// if the tokens do form a directive (`0^docs collect { }` at the root);
|
|
897
|
+
// otherwise the expression grammar gets to read them (a top-level `0^name`
|
|
898
|
+
// item is a lift, with the lift's own error), so the failure is held back
|
|
899
|
+
// until the lookahead has decided.
|
|
900
|
+
let scopeErr: OqxError | null = null;
|
|
901
|
+
if (this.at("binding")) receiver = this.bindingNode(this.next());
|
|
902
|
+
else if (this.at("ident") || this.atScopeRefAt(0)) {
|
|
903
|
+
let levels = 0;
|
|
904
|
+
try {
|
|
905
|
+
levels = this.parseScopeRef();
|
|
906
|
+
} catch (err) {
|
|
907
|
+
if (!(err instanceof OqxError)) throw err;
|
|
908
|
+
scopeErr = err;
|
|
909
|
+
this.next(); this.next(); // the integer and the caret, as if it had parsed
|
|
910
|
+
levels = 1;
|
|
911
|
+
}
|
|
704
912
|
if (!this.at("ident")) { this.pos = start; return null; }
|
|
705
|
-
receiver = this.parseNavFrom(this.next(), levels).expr;
|
|
913
|
+
receiver = this.parseNavFrom(this.next(), levels, startPos).expr;
|
|
706
914
|
} else return null;
|
|
707
915
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (
|
|
714
|
-
|
|
715
|
-
const op = this.next().value as Consumer;
|
|
716
|
-
let distinct = false;
|
|
717
|
-
if (this.at("ident", "distinct")) { this.next(); distinct = true; }
|
|
718
|
-
this.next(); // '{'
|
|
719
|
-
const { sub, distinct: bodyDistinct } = this.parseSubquery({ top: false, op, liftsAllowed: inWhere && op === "collect" });
|
|
720
|
-
if (!this.at("rbrace")) this.fail(`expected '}' to close the ${op} { … } block`);
|
|
721
|
-
this.next();
|
|
722
|
-
return { kind: "op", receiver, op, sub, distinct: distinct || bodyDistinct };
|
|
916
|
+
const op = this.at("lbrace") ? "collect" : this.atConsumerBlock();
|
|
917
|
+
if (op) {
|
|
918
|
+
if (scopeErr) throw scopeErr;
|
|
919
|
+
if (receiver.kind === "ident" && LITERAL_WORDS.has(receiver.name)) this.fail(`\`${receiver.name}\` is a literal, not a collection`);
|
|
920
|
+
// `count { }` with nothing before it: the consumer word is not a receiver.
|
|
921
|
+
if (op === "collect" && receiver.kind === "ident" && CONSUMERS.has(receiver.name)) {
|
|
922
|
+
this.fail(`unexpected '{' after \`${receiver.name}\` — a consumer needs a receiver: \`<collection> ${receiver.name} { … }\``);
|
|
723
923
|
}
|
|
924
|
+
return this.parseConsumerBlock(receiver, op, inWhere);
|
|
724
925
|
}
|
|
725
926
|
this.pos = start;
|
|
726
927
|
return null;
|
|
727
928
|
}
|
|
728
929
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
930
|
+
// The cursor is at a consumer word that opens a block — `<op> {` or
|
|
931
|
+
// `<op> distinct {`. Consumes nothing.
|
|
932
|
+
private atConsumerBlock(): Consumer | null {
|
|
933
|
+
if (!this.at("ident") || !CONSUMERS.has(this.peek().value)) return null;
|
|
934
|
+
const after = this.peekAt(1);
|
|
935
|
+
const opThenBrace = after?.type === "lbrace";
|
|
936
|
+
const opDistinctBrace = after?.type === "ident" && after.value === "distinct" && this.peekAt(2)?.type === "lbrace";
|
|
937
|
+
return opThenBrace || opDistinctBrace ? (this.peek().value as Consumer) : null;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// `[<op>] [distinct] { <sub> }` over an already-parsed receiver; the cursor is
|
|
941
|
+
// at the consumer word, or at the `{` of the implicit `collect`.
|
|
942
|
+
private parseConsumerBlock(receiver: Expr, op: Consumer, inWhere: boolean): OpNode {
|
|
943
|
+
const startPos = receiver.span[0];
|
|
944
|
+
if (this.at("ident")) this.next(); // the consumer word (absent for `x { … }`)
|
|
945
|
+
let distinct = false;
|
|
946
|
+
if (this.at("ident", "distinct")) { this.next(); distinct = true; }
|
|
947
|
+
const lbrace = this.next().pos; // '{'
|
|
948
|
+
const defer = this.deferLookup;
|
|
949
|
+
this.deferLookup = false;
|
|
950
|
+
// The receiver was read at the enclosing depth; the block's rows are one deeper.
|
|
951
|
+
const body = this.atDepth(this.depth + 1, () => this.parseBody({ top: false, op, liftsAllowed: inWhere && op === "collect" }));
|
|
952
|
+
this.deferLookup = defer;
|
|
953
|
+
if (!this.at("rbrace")) this.fail(`expected '}' to close the ${op} { … } block`);
|
|
954
|
+
this.next();
|
|
955
|
+
const sub: Subquery = {
|
|
956
|
+
kind: "subquery",
|
|
957
|
+
span: this.sp(lbrace),
|
|
958
|
+
from: body.froms,
|
|
959
|
+
where: body.where,
|
|
960
|
+
select: body.select,
|
|
961
|
+
orderBy: body.orderBy,
|
|
962
|
+
follow: body.follow,
|
|
963
|
+
values: body.values,
|
|
964
|
+
limit: body.limit,
|
|
965
|
+
offset: body.offset,
|
|
734
966
|
};
|
|
967
|
+
return { kind: "op", span: this.sp(startPos), receiver, op, sub, countCmp: null, distinct: distinct || body.distinct };
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// A bracket suffix on `recv` (GRAMMAR §4): `[n]` / `[${…}]` is positional —
|
|
971
|
+
// `recv first { offset n }` — and anything else a predicate —
|
|
972
|
+
// `recv first { where p }`; a trailing `!` makes the index required and the
|
|
973
|
+
// predicate a `single` (required too, so that zero matches is an error).
|
|
974
|
+
private parseBracket(recv: Expr): Expr {
|
|
975
|
+
if (recv.kind === "lit") this.fail(`\`${describe(recv)}\` is a literal, not a collection`);
|
|
976
|
+
const start = recv.span[0];
|
|
977
|
+
const lbracket = this.next().pos; // '['
|
|
978
|
+
let index: Expr | null = null;
|
|
979
|
+
let where: Where | null = null;
|
|
980
|
+
if (this.at("number") && this.peekAt(1)?.type === "rbracket") {
|
|
981
|
+
const t = this.next();
|
|
982
|
+
const v = Number(t.value);
|
|
983
|
+
if (!Number.isInteger(v)) this.fail(`an index is a non-negative integer literal or a binding (got ${t.value})`);
|
|
984
|
+
index = { kind: "lit", span: [t.pos, t.end], value: v };
|
|
985
|
+
} else if (this.at("binding") && this.peekAt(1)?.type === "rbracket") {
|
|
986
|
+
index = this.bindingNode(this.next());
|
|
987
|
+
} else if (this.atOp("-") && this.peekAt(1)?.type === "number" && this.peekAt(2)?.type === "rbracket") {
|
|
988
|
+
this.fail("negative indices are not supported (`x[-1]`): an index is a non-negative integer literal or a binding");
|
|
989
|
+
} else {
|
|
990
|
+
const defer = this.deferLookup;
|
|
991
|
+
this.deferLookup = false;
|
|
992
|
+
// The predicate is the block's `where`: read in the rows' scope, one deeper.
|
|
993
|
+
where = this.atDepth(this.depth + 1, () => this.parseWhere());
|
|
994
|
+
this.deferLookup = defer;
|
|
995
|
+
}
|
|
996
|
+
if (!this.at("rbracket")) this.fail("expected ']' to close the bracket");
|
|
997
|
+
this.next();
|
|
998
|
+
const subSpan: Span = [lbracket, this.end()];
|
|
999
|
+
const sub = (parts: Partial<Subquery>): Subquery => ({
|
|
1000
|
+
kind: "subquery", span: subSpan, from: [], where: null, select: [], orderBy: null, follow: null, values: false, limit: null, offset: null, ...parts,
|
|
1001
|
+
});
|
|
1002
|
+
const bang = this.atOp("!") ? this.next() : null;
|
|
1003
|
+
if (index) {
|
|
1004
|
+
const op: OpNode = { kind: "op", span: [start, subSpan[1]], receiver: recv, op: "first", sub: sub({ offset: index }), countCmp: null, distinct: false };
|
|
1005
|
+
return bang ? { kind: "required", span: [start, bang.end], expr: op } : op;
|
|
1006
|
+
}
|
|
1007
|
+
if (bang) {
|
|
1008
|
+
const op: OpNode = { kind: "op", span: [start, bang.end], receiver: recv, op: "single", sub: sub({ where }), countCmp: null, distinct: false };
|
|
1009
|
+
return { kind: "required", span: [start, bang.end], expr: op };
|
|
1010
|
+
}
|
|
1011
|
+
return { kind: "op", span: [start, subSpan[1]], receiver: recv, op: "first", sub: sub({ where }), countCmp: null, distinct: false };
|
|
735
1012
|
}
|
|
736
1013
|
|
|
737
1014
|
// ---- expression Pratt parser ----------------------------------------------
|
|
738
|
-
// Value position (select/order/follow/source): full boolean+arithmetic.
|
|
1015
|
+
// Value position (select/order/follow/source): full boolean+arithmetic. Each
|
|
1016
|
+
// level takes an optional already-parsed `left` operand (a directive read by
|
|
1017
|
+
// `tryOp`, a promoted where-group), so it continues into the operator tail
|
|
1018
|
+
// without re-reading its tokens.
|
|
739
1019
|
private parseValueExpr(): Expr { return this.parseOr(); }
|
|
740
1020
|
|
|
741
|
-
private parseOr(): Expr {
|
|
742
|
-
|
|
743
|
-
while (this.
|
|
1021
|
+
private parseOr(left?: Expr): Expr {
|
|
1022
|
+
left = this.parseAnd(left);
|
|
1023
|
+
while (this.atOr()) {
|
|
1024
|
+
this.next();
|
|
1025
|
+
const right = this.parseAnd();
|
|
1026
|
+
left = { kind: "logical", span: [left.span[0], right.span[1]], op: "||", left, right };
|
|
1027
|
+
}
|
|
744
1028
|
return left;
|
|
745
1029
|
}
|
|
746
1030
|
|
|
747
|
-
private parseAnd(): Expr {
|
|
748
|
-
|
|
749
|
-
while (this.
|
|
1031
|
+
private parseAnd(left?: Expr): Expr {
|
|
1032
|
+
left = this.parseCmp(left);
|
|
1033
|
+
while (this.atAnd()) {
|
|
1034
|
+
this.next();
|
|
1035
|
+
const right = this.parseCmp();
|
|
1036
|
+
left = { kind: "logical", span: [left.span[0], right.span[1]], op: "&&", left, right };
|
|
1037
|
+
}
|
|
750
1038
|
return left;
|
|
751
1039
|
}
|
|
752
1040
|
|
|
@@ -760,11 +1048,16 @@ class Parser {
|
|
|
760
1048
|
private parseCmp(left?: Expr): Expr {
|
|
761
1049
|
const lhs = this.parseRange(left);
|
|
762
1050
|
const t = this.peek();
|
|
763
|
-
const isCmp = (): boolean => (this.peek().type === "op" && CMP_OPS.has(this.peek().value)) || this.at("ident", "in");
|
|
1051
|
+
const isCmp = (): boolean => (this.peek().type === "op" && CMP_OPS.has(this.peek().value)) || this.at("ident", "in") || this.at("kw", "is");
|
|
764
1052
|
if (!isCmp()) return lhs;
|
|
765
|
-
|
|
1053
|
+
let op = this.next().value;
|
|
1054
|
+
// `is` / `is not` — identity (SEMANTICS §5b); `is not` is the compound.
|
|
1055
|
+
if (op === "is" && this.at("kw", "not")) { this.next(); op = "is not"; }
|
|
766
1056
|
const rhs = this.parseRange();
|
|
767
|
-
const
|
|
1057
|
+
const span: Span = [lhs.span[0], rhs.span[1]];
|
|
1058
|
+
const result: Expr = op === "in"
|
|
1059
|
+
? { kind: "in", span, left: lhs, right: rhs }
|
|
1060
|
+
: { kind: "binary", span, op: op as BinaryOp, left: lhs, right: rhs };
|
|
768
1061
|
if (isCmp()) {
|
|
769
1062
|
this.fail(`comparisons do not chain: \`a ${t.value} b ${this.peek().value} c\` — write two comparisons joined with \`&&\``);
|
|
770
1063
|
}
|
|
@@ -778,15 +1071,18 @@ class Parser {
|
|
|
778
1071
|
// opens the high end.
|
|
779
1072
|
private parseRange(left?: Expr): Expr {
|
|
780
1073
|
if (left === undefined && this.at("range")) {
|
|
781
|
-
const
|
|
1074
|
+
const tok = this.next();
|
|
1075
|
+
const exclusiveEnd = tok.value === "...";
|
|
782
1076
|
if (!this.canStartValue()) this.fail("a range needs at least one bound: `lo..hi`, `lo..`, or `..hi`");
|
|
783
|
-
|
|
1077
|
+
const hi = this.parseAdd();
|
|
1078
|
+
return { kind: "range", span: [tok.pos, hi.span[1]], lo: null, hi, exclusiveEnd };
|
|
784
1079
|
}
|
|
785
1080
|
const lo = this.parseAdd(left);
|
|
786
1081
|
if (this.at("range")) {
|
|
787
|
-
const
|
|
1082
|
+
const tok = this.next();
|
|
1083
|
+
const exclusiveEnd = tok.value === "...";
|
|
788
1084
|
const hi = this.canStartValue() ? this.parseAdd() : null;
|
|
789
|
-
return { kind: "range", lo, hi, exclusiveEnd };
|
|
1085
|
+
return { kind: "range", span: [lo.span[0], hi ? hi.span[1] : tok.end], lo, hi, exclusiveEnd };
|
|
790
1086
|
}
|
|
791
1087
|
return lo;
|
|
792
1088
|
}
|
|
@@ -799,6 +1095,7 @@ class Parser {
|
|
|
799
1095
|
private canStartValue(): boolean {
|
|
800
1096
|
const t = this.peek();
|
|
801
1097
|
if (t.type === "ident") return !CLAUSE_WORDS.has(t.value);
|
|
1098
|
+
if (t.type === "kw") return t.value === "is" || t.value === "not"; // the prefix operators
|
|
802
1099
|
if (t.type === "number" || t.type === "string" ||
|
|
803
1100
|
t.type === "binding" || t.type === "lparen" || t.type === "caret") return true;
|
|
804
1101
|
return t.type === "op" && (t.value === "-" || t.value === "!");
|
|
@@ -807,8 +1104,9 @@ class Parser {
|
|
|
807
1104
|
private parseAdd(left?: Expr): Expr {
|
|
808
1105
|
left = this.parseMul(left);
|
|
809
1106
|
while (this.peek().type === "op" && ADD_OPS.has(this.peek().value)) {
|
|
810
|
-
const op = this.next().value;
|
|
811
|
-
|
|
1107
|
+
const op = this.next().value as BinaryOp;
|
|
1108
|
+
const right = this.parseMul();
|
|
1109
|
+
left = { kind: "binary", span: [left.span[0], right.span[1]], op, left, right };
|
|
812
1110
|
}
|
|
813
1111
|
return left;
|
|
814
1112
|
}
|
|
@@ -816,43 +1114,72 @@ class Parser {
|
|
|
816
1114
|
private parseMul(left?: Expr): Expr {
|
|
817
1115
|
left = this.parseUnary(left);
|
|
818
1116
|
while (this.peek().type === "op" && MUL_OPS.has(this.peek().value)) {
|
|
819
|
-
const op = this.next().value;
|
|
820
|
-
|
|
1117
|
+
const op = this.next().value as BinaryOp;
|
|
1118
|
+
const right = this.parseUnary();
|
|
1119
|
+
left = { kind: "binary", span: [left.span[0], right.span[1]], op, left, right };
|
|
821
1120
|
}
|
|
822
1121
|
return left;
|
|
823
1122
|
}
|
|
824
1123
|
|
|
825
1124
|
private parseUnary(left?: Expr): Expr {
|
|
826
1125
|
if (left !== undefined) return this.parsePostfix(left);
|
|
827
|
-
if (this.atOp("!")
|
|
828
|
-
|
|
1126
|
+
if (this.atOp("!") || this.atOp("-") || this.at("kw", "is") || this.at("kw", "not")) {
|
|
1127
|
+
const tok = this.next();
|
|
1128
|
+
const expr = this.parseUnary();
|
|
1129
|
+
const span: Span = [tok.pos, expr.span[1]];
|
|
1130
|
+
// `is x` ≡ `!!x`, `not x` ≡ `!x`: pure sugar, both nodes spanning the sugar.
|
|
1131
|
+
if (tok.type === "kw") {
|
|
1132
|
+
const not: Expr = { kind: "unary", span, op: "!", expr };
|
|
1133
|
+
return tok.value === "is" ? { kind: "unary", span, op: "!", expr: not } : not;
|
|
1134
|
+
}
|
|
1135
|
+
return { kind: "unary", span, op: tok.value as "!" | "-", expr };
|
|
1136
|
+
}
|
|
829
1137
|
return this.parsePostfix();
|
|
830
1138
|
}
|
|
831
1139
|
|
|
832
1140
|
private parsePostfix(left?: Expr): Expr {
|
|
833
1141
|
let expr = left ?? this.parsePrimary();
|
|
1142
|
+
const start = expr.span[0];
|
|
834
1143
|
for (;;) {
|
|
835
1144
|
if (this.at("dot")) {
|
|
836
1145
|
this.next();
|
|
837
1146
|
if (!this.at("ident")) this.fail("expected a property name after '.'");
|
|
838
1147
|
const name = this.next().value;
|
|
839
1148
|
if (this.at("lparen")) {
|
|
840
|
-
|
|
1149
|
+
const args = this.parseArgs();
|
|
1150
|
+
expr = { kind: "call", span: this.sp(start), recv: expr, name, args };
|
|
841
1151
|
} else {
|
|
842
|
-
expr = { kind: "member", recv: expr, name };
|
|
1152
|
+
expr = { kind: "member", span: this.sp(start), recv: expr, name };
|
|
843
1153
|
}
|
|
844
1154
|
} else if (this.at("lparen") && left === undefined && expr.kind === "ident") {
|
|
845
1155
|
// free function call: name(args) — a bare name read here, not `(f)(x)`
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1156
|
+
const args = this.parseArgs();
|
|
1157
|
+
expr = { kind: "call", span: this.sp(start), recv: null, name: expr.name, args };
|
|
1158
|
+
} else if (this.at("lbracket")) {
|
|
1159
|
+
expr = this.parseBracket(expr);
|
|
1160
|
+
} else if (this.atOp("!")) {
|
|
1161
|
+
// Postfix `!`: a `!` right after a complete chain is `required` — OQX
|
|
1162
|
+
// never places two operands side by side, so it cannot begin one here.
|
|
1163
|
+
const bang = this.next();
|
|
1164
|
+
expr = { kind: "required", span: [start, bang.end], expr };
|
|
1165
|
+
} else if (this.atValueDirective()) {
|
|
1166
|
+
// A value-position directive, `recv first { … }` — how a bracket chain
|
|
1167
|
+
// prints back; exists/none/count have no value form and are left to
|
|
1168
|
+
// the body's own error.
|
|
1169
|
+
if (expr.kind === "lit") this.fail(`\`${describe(expr)}\` is a literal, not a collection`);
|
|
1170
|
+
expr = this.parseConsumerBlock(expr, this.atConsumerBlock()!, false);
|
|
849
1171
|
} else {
|
|
850
|
-
break;
|
|
1172
|
+
break; // a bare `{` is a consumer block boundary — not part of a value expression; anything else ends the chain too
|
|
851
1173
|
}
|
|
852
1174
|
}
|
|
853
1175
|
return expr;
|
|
854
1176
|
}
|
|
855
1177
|
|
|
1178
|
+
private atValueDirective(): boolean {
|
|
1179
|
+
const op = this.atConsumerBlock();
|
|
1180
|
+
return op === "collect" || op === "first" || op === "single";
|
|
1181
|
+
}
|
|
1182
|
+
|
|
856
1183
|
private parseArgs(): Expr[] {
|
|
857
1184
|
this.next(); // '('
|
|
858
1185
|
const args: Expr[] = [];
|
|
@@ -871,27 +1198,29 @@ class Parser {
|
|
|
871
1198
|
// select-item head `^name:` is a lift, handled in parseSelectItem; here, in
|
|
872
1199
|
// expression position, `^` reads an enclosing row's field even when the
|
|
873
1200
|
// current row shadows the name.)
|
|
874
|
-
if (t.type === "caret") {
|
|
875
|
-
const levels = this.
|
|
1201
|
+
if (t.type === "caret" || this.atAbsoluteRef()) {
|
|
1202
|
+
const levels = this.parseScopeRef();
|
|
876
1203
|
if (!this.at("ident")) this.fail("expected an identifier after '^' (an outer reference)");
|
|
877
|
-
|
|
1204
|
+
const name = this.next();
|
|
1205
|
+
return { kind: "outer", span: [t.pos, name.end], levels, name: name.value };
|
|
878
1206
|
}
|
|
879
|
-
if (t.type === "number") { this.next(); return { kind: "lit", value: Number(t.value) }; }
|
|
880
|
-
if (t.type === "string") { this.next(); return { kind: "lit", value: t.value }; }
|
|
881
|
-
if (t.type === "binding") { this.next(); return
|
|
1207
|
+
if (t.type === "number") { this.next(); return { kind: "lit", span: [t.pos, t.end], value: Number(t.value) }; }
|
|
1208
|
+
if (t.type === "string") { this.next(); return { kind: "lit", span: [t.pos, t.end], value: t.value }; }
|
|
1209
|
+
if (t.type === "binding") { this.next(); return this.bindingNode(t); }
|
|
882
1210
|
if (t.type === "lparen") {
|
|
883
1211
|
this.next();
|
|
884
1212
|
const e = this.parseValueExpr();
|
|
885
1213
|
if (!this.at("rparen")) this.fail("expected ')'");
|
|
886
1214
|
this.next();
|
|
887
|
-
return e;
|
|
1215
|
+
return withSpan(e, [t.pos, this.end()]);
|
|
888
1216
|
}
|
|
889
1217
|
if (t.type === "ident") {
|
|
890
1218
|
this.next();
|
|
891
|
-
|
|
892
|
-
if (t.value === "
|
|
893
|
-
if (t.value === "
|
|
894
|
-
return { kind: "
|
|
1219
|
+
const span: Span = [t.pos, t.end];
|
|
1220
|
+
if (t.value === "true") return { kind: "lit", span, value: true };
|
|
1221
|
+
if (t.value === "false") return { kind: "lit", span, value: false };
|
|
1222
|
+
if (t.value === "null") return { kind: "lit", span, value: null };
|
|
1223
|
+
return { kind: "ident", span, name: t.value };
|
|
895
1224
|
}
|
|
896
1225
|
this.fail(`unexpected ${this.tokDesc()} — expected a value`);
|
|
897
1226
|
}
|
|
@@ -904,25 +1233,44 @@ function navKey(e: Expr): string | null {
|
|
|
904
1233
|
case "ident": return e.name;
|
|
905
1234
|
case "outer": return e.name;
|
|
906
1235
|
case "member": return e.name;
|
|
1236
|
+
case "required": return navKey(e.expr);
|
|
907
1237
|
default: return null;
|
|
908
1238
|
}
|
|
909
1239
|
}
|
|
910
1240
|
|
|
911
|
-
//
|
|
912
|
-
|
|
913
|
-
|
|
1241
|
+
// Whether a block's leading run, read as a where tree, is syntactically a
|
|
1242
|
+
// predicate (GRAMMAR §2): anything but a bare name, a dotted navigation or a lift
|
|
1243
|
+
// (an `outer`), with a parenthesized leading expression always a predicate.
|
|
1244
|
+
function predicateShaped(w: Where, first: Token): boolean {
|
|
1245
|
+
if (first.type === "lparen") return true;
|
|
1246
|
+
if (w.kind !== "scalar") return true;
|
|
1247
|
+
return !isNavigation(w.expr);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function isNavigation(e: Expr): boolean {
|
|
1251
|
+
switch (e.kind) {
|
|
1252
|
+
case "ident": case "outer": return true;
|
|
1253
|
+
case "member": return isNavigation(e.recv);
|
|
1254
|
+
default: return false;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// The canonical text of a node for an error message; a binding shows as its
|
|
1259
|
+
// `${n}` marker (the printer refuses to turn a bound value into source).
|
|
1260
|
+
function describe(node: Expr | Where): string {
|
|
1261
|
+
const t = printTemplate(node);
|
|
1262
|
+
return t.strings.map((s, i) => (i < t.count ? `${s}\${${t.indices[i]}}` : s)).join("");
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// `!` applied once per recorded `!` position to a where node, innermost first.
|
|
1266
|
+
function wrapNot(w: Where, notStarts: readonly number[]): Where {
|
|
1267
|
+
for (let i = notStarts.length - 1; i >= 0; i--) w = { kind: "not", span: [notStarts[i]!, w.span[1]], expr: w };
|
|
914
1268
|
return w;
|
|
915
1269
|
}
|
|
916
1270
|
|
|
917
1271
|
// A scalar where leaf. A leading `!` on the whole leaf becomes a `not` node (so
|
|
918
1272
|
// `where !active` keeps its shape); inside a comparison it stays a unary `!`.
|
|
919
1273
|
function scalarLeaf(e: Expr): Where {
|
|
920
|
-
if (e.kind === "unary" && e.op === "!") return { kind: "not", expr: scalarLeaf(e.expr) };
|
|
921
|
-
return { kind: "scalar", expr: e };
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// The optional `limit`/`offset` fields of a Query/Subquery, present only when set
|
|
925
|
-
// (so a Query built without them is unchanged).
|
|
926
|
-
function bounds(b: { limit?: Expr | null; offset?: Expr | null }): { limit?: Expr; offset?: Expr } {
|
|
927
|
-
return { ...(b.limit ? { limit: b.limit } : {}), ...(b.offset ? { offset: b.offset } : {}) };
|
|
1274
|
+
if (e.kind === "unary" && e.op === "!") return { kind: "not", span: e.span, expr: scalarLeaf(e.expr) };
|
|
1275
|
+
return { kind: "scalar", span: e.span, expr: e };
|
|
928
1276
|
}
|