@omgbase/oqx 0.18.0 → 0.21.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 +30 -0
- package/README.md +177 -64
- package/dist/ast.d.ts +28 -5
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +1 -1
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +11 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +10 -3
- package/dist/build.js.map +1 -1
- package/dist/context.d.ts +19 -5
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +6 -9
- package/dist/context.js.map +1 -1
- package/dist/engine.d.ts +12 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +263 -84
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts +3 -3
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +79 -2
- package/dist/lexer.js.map +1 -1
- package/dist/optimize/analysis.d.ts +3 -2
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +18 -15
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +250 -118
- package/dist/parser.js.map +1 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +47 -29
- package/dist/print.js.map +1 -1
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +15 -8
- package/dist/resolve.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walk.d.ts +5 -3
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +34 -11
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +33 -6
- package/src/build.ts +12 -8
- package/src/context.ts +22 -11
- package/src/engine.ts +258 -91
- package/src/index.ts +2 -1
- package/src/lexer.ts +76 -6
- package/src/optimize/analysis.ts +17 -12
- package/src/parser.ts +227 -101
- package/src/print.ts +44 -16
- package/src/resolve.ts +15 -7
- package/src/version.ts +1 -1
- package/src/walk.ts +39 -14
package/dist/parser.js
CHANGED
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
//
|
|
18
18
|
// A `follow` destination is a relation of the current row (a receiver, never
|
|
19
19
|
// an outer reference) or a destination block — a receiver (which MAY be an
|
|
20
|
-
// outer reference, `^people`)
|
|
21
|
-
// `single` [`distinct`] `{ body }`, exactly the
|
|
22
|
-
// `tryOp
|
|
20
|
+
// outer reference, `^people`, or a tuple `(a, b)`) followed by `{ body }`, or
|
|
21
|
+
// by `collect`/`first`/`single` [`distinct`] `{ body }`, exactly the
|
|
22
|
+
// select-position directive `tryOp` recognizes, re-evaluated per frontier row.
|
|
23
|
+
// Since 0.20 there is no options block: a brace after ANY destination opens
|
|
24
|
+
// that destination's block, whose body admits two clauses no other block has —
|
|
25
|
+
// `frontier P` and `depth n`, in the clause order after `where`.
|
|
23
26
|
//
|
|
24
27
|
// Every clause is optional except that a top-level body needs `from` (the
|
|
25
28
|
// receiver-plus-consumer form `<receiver> <consumer> { block }` supplies the
|
|
@@ -33,8 +36,9 @@
|
|
|
33
36
|
// out-of-order clause is a parse error naming the order.
|
|
34
37
|
//
|
|
35
38
|
// Sugar (0.17) desugars HERE, to existing nodes: `x { … }` is `x collect { … }`
|
|
36
|
-
//
|
|
37
|
-
//
|
|
39
|
+
// wherever a value stands (a select item, a where leaf, a follow destination, a
|
|
40
|
+
// call argument — since 0.20 without exception) and `x distinct { … }` is
|
|
41
|
+
// `x collect distinct { … }` (0.20); `x[p]` is `x first { where p }`, `x[n]` is `x first { offset n }`, with a
|
|
38
42
|
// trailing `!` making the first a `single` (predicate) or a `required` (index);
|
|
39
43
|
// `is x` is `!!x`, `not x` is `!x`. Two shapes are new: the postfix `required`
|
|
40
44
|
// (`x!`, tightest) and the identity operators `is` / `is not` (comparison
|
|
@@ -57,6 +61,14 @@
|
|
|
57
61
|
// source, from the first token that produced it to the end of the last; a
|
|
58
62
|
// parenthesized operand's span includes its parentheses (spec/oqx/AST.md).
|
|
59
63
|
//
|
|
64
|
+
// Tuples (0.20): `(e1, e2, …)` with two or more elements is a `tuple` node — a
|
|
65
|
+
// value and a receiver; one expression in parentheses stays grouping.
|
|
66
|
+
//
|
|
67
|
+
// Template literals (0.21): `` `text ${expr} text` `` is a `template` node — a
|
|
68
|
+
// primary value whose interpolations are full expressions read at the current
|
|
69
|
+
// depth (`parseTemplateLiteral`); the lexer balances the braces, so an
|
|
70
|
+
// interpolation may hold a `{ … }` block or another template literal.
|
|
71
|
+
//
|
|
60
72
|
// Principle of least surprise, applied to the grammar: a rule a careful user
|
|
61
73
|
// would not predict is a bug. Hence `!` binds tighter than comparison in every
|
|
62
74
|
// position (`!a == b` is `(!a) == b`, as in C); parentheses in `where` group a
|
|
@@ -71,7 +83,16 @@ import { resolveSelect, resolveWhere } from "./resolve.js";
|
|
|
71
83
|
import { printTemplate } from "./print.js";
|
|
72
84
|
const CONSUMERS = new Set(["collect", "exists", "none", "count", "first", "single"]);
|
|
73
85
|
// The fixed clause order of a body (ADR-020). Each clause appears at most once.
|
|
74
|
-
|
|
86
|
+
// `frontier` is a clause of every body that has a `follow` (a destination
|
|
87
|
+
// block's marks the occurrences it reaches, any other body's its seeds — 0.20);
|
|
88
|
+
// `depth` is legal only in a follow destination block, and the order sentence
|
|
89
|
+
// names it only there.
|
|
90
|
+
const CLAUSE_ORDER = ["select", "from", "where", "frontier", "depth", "follow", "order by", "limit", "offset"];
|
|
91
|
+
const DESTINATION_ONLY = new Set(["depth"]);
|
|
92
|
+
// The clauses a body may hold, in order.
|
|
93
|
+
function clausesOf(ctx) {
|
|
94
|
+
return CLAUSE_ORDER.filter((c) => ctx.destination || !DESTINATION_ONLY.has(c));
|
|
95
|
+
}
|
|
75
96
|
// Contextual clause words lexed as bare idents; an open-ended range must stop
|
|
76
97
|
// before them rather than consume them as its high bound.
|
|
77
98
|
const CLAUSE_WORDS = new Set([
|
|
@@ -86,7 +107,7 @@ const MUL_OPS = new Set(["*", "/", "%"]);
|
|
|
86
107
|
// `true`/`false`/`null` are literals in every position, so they can never name a
|
|
87
108
|
// receiver (`true exists { … }`, `follow null`).
|
|
88
109
|
const LITERAL_WORDS = new Set(["true", "false", "null"]);
|
|
89
|
-
const TOP_CTX = { top: true, op: null, liftsAllowed: false };
|
|
110
|
+
const TOP_CTX = { top: true, op: null, liftsAllowed: false, destination: false };
|
|
90
111
|
/** Parse a tagged-template call into a Query. */
|
|
91
112
|
export function parseTemplate(fragments, values) {
|
|
92
113
|
return new Parser(lexTemplate(fragments, values)).parseQuery();
|
|
@@ -140,7 +161,10 @@ class Parser {
|
|
|
140
161
|
return t.type === "op" && t.value === value;
|
|
141
162
|
}
|
|
142
163
|
fail(msg) {
|
|
143
|
-
|
|
164
|
+
this.failAt(this.peek().pos, msg);
|
|
165
|
+
}
|
|
166
|
+
failAt(pos, msg) {
|
|
167
|
+
throw new OqxError(`${msg} (at offset ${pos})`, "parse");
|
|
144
168
|
}
|
|
145
169
|
/** Code-point offset of the next token (the start of a node about to be parsed). */
|
|
146
170
|
start() { return this.peek().pos; }
|
|
@@ -207,6 +231,7 @@ class Parser {
|
|
|
207
231
|
orderBy: body.orderBy,
|
|
208
232
|
consumer: "collect",
|
|
209
233
|
follow: body.follow,
|
|
234
|
+
frontier: body.frontier,
|
|
210
235
|
distinct: body.distinct,
|
|
211
236
|
values: body.values,
|
|
212
237
|
limit: body.limit,
|
|
@@ -218,7 +243,7 @@ class Parser {
|
|
|
218
243
|
queryFromOp(e, start) {
|
|
219
244
|
return {
|
|
220
245
|
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,
|
|
246
|
+
orderBy: e.sub.orderBy, consumer: e.op, follow: e.sub.follow, frontier: e.sub.frontier, distinct: e.distinct, values: e.sub.values,
|
|
222
247
|
limit: e.sub.limit, offset: e.sub.offset,
|
|
223
248
|
};
|
|
224
249
|
}
|
|
@@ -237,6 +262,8 @@ class Parser {
|
|
|
237
262
|
let select = [];
|
|
238
263
|
let orderBy = null;
|
|
239
264
|
let follow = null;
|
|
265
|
+
let frontier = null;
|
|
266
|
+
let depth = null;
|
|
240
267
|
let distinct = false;
|
|
241
268
|
let values = false;
|
|
242
269
|
let limit = null;
|
|
@@ -251,7 +278,7 @@ class Parser {
|
|
|
251
278
|
if (idx === stage)
|
|
252
279
|
this.fail(`duplicate \`${clause}\` clause`);
|
|
253
280
|
if (idx < stage) {
|
|
254
|
-
this.fail(`\`${clause}\` must come before \`${CLAUSE_ORDER[stage]}\` — OQX clause order is ${
|
|
281
|
+
this.fail(`\`${clause}\` must come before \`${CLAUSE_ORDER[stage]}\` — OQX clause order is ${clausesOf(ctx).join(", ")}`);
|
|
255
282
|
}
|
|
256
283
|
stage = idx;
|
|
257
284
|
};
|
|
@@ -278,6 +305,28 @@ class Parser {
|
|
|
278
305
|
where = this.atDepth(rowDepth, () => this.parseWhere());
|
|
279
306
|
continue;
|
|
280
307
|
}
|
|
308
|
+
// `frontier P` (0.20) — in a follow destination block the destination's
|
|
309
|
+
// own clause, read in the occurrence's scope whose parent is the walk's
|
|
310
|
+
// enclosing scope: one level OUT of the block's rows (SEMANTICS §20), so
|
|
311
|
+
// `^` there is the enclosing scope, not the frontier row. In any other
|
|
312
|
+
// body it marks the seeds of the body's own `follow` (checked below) and
|
|
313
|
+
// is read in the seed's scope, like `where`.
|
|
314
|
+
if (this.at("ident", "frontier") && this.startsValue(this.peekAt(1))) {
|
|
315
|
+
enter("frontier");
|
|
316
|
+
this.next();
|
|
317
|
+
frontier = this.atDepth(ctx.destination ? rowDepth - 1 : rowDepth, () => this.parseValueExpr());
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
// `depth n` — a follow destination block's own clause (0.20).
|
|
321
|
+
if (ctx.destination && this.at("ident", "depth") && this.peekAt(1)?.type === "number") {
|
|
322
|
+
enter("depth");
|
|
323
|
+
this.next();
|
|
324
|
+
const v = Number(this.next().value);
|
|
325
|
+
if (!Number.isInteger(v) || v < 1 || v > 8)
|
|
326
|
+
this.fail("follow depth must be an integer between 1 and 8");
|
|
327
|
+
depth = v;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
281
330
|
if (this.atFollow()) {
|
|
282
331
|
enter("follow");
|
|
283
332
|
follow = this.atDepth(rowDepth, () => this.parseFollow());
|
|
@@ -330,7 +379,12 @@ class Parser {
|
|
|
330
379
|
if (where)
|
|
331
380
|
resolveWhere(select, where, (msg) => this.fail(msg));
|
|
332
381
|
resolveSelect(select, (msg) => this.fail(msg));
|
|
333
|
-
|
|
382
|
+
// Outside a destination block `frontier` marks the seeds of this body's walk,
|
|
383
|
+
// so it needs one.
|
|
384
|
+
if (frontier && !follow && !ctx.destination) {
|
|
385
|
+
this.fail("`frontier` needs a `follow` — a body's `frontier <expr>` marks which of its rows (the walk's seeds) are frontiers; a destination block's marks the occurrences reached through it");
|
|
386
|
+
}
|
|
387
|
+
return { froms, where, select, orderBy, follow, frontier, depth, distinct, values, limit, offset };
|
|
334
388
|
}
|
|
335
389
|
// The keyword-less run that opens a body. At the top level it is always the
|
|
336
390
|
// projection. In a block it is a `where`-first body when the leading expression
|
|
@@ -387,8 +441,8 @@ class Parser {
|
|
|
387
441
|
// consumer word where a whole-query directive was meant), or a stray token.
|
|
388
442
|
failUnexpectedInBody(stage, ctx) {
|
|
389
443
|
const t = this.peek();
|
|
390
|
-
const remaining = CLAUSE_ORDER.slice(stage + 1).join("/");
|
|
391
444
|
const last = stage >= 0 ? CLAUSE_ORDER[stage] : null;
|
|
445
|
+
const remaining = clausesOf(ctx).filter((c) => CLAUSE_ORDER.indexOf(c) > stage).join("/");
|
|
392
446
|
const isWord = t.type === "ident" || t.type === "kw" || t.type === "binding" || t.type === "caret";
|
|
393
447
|
// Punctuation, an operator, or a literal after a complete clause is a stray
|
|
394
448
|
// token, not a misplaced predicate: name where the body ends. (A comma keeps
|
|
@@ -408,6 +462,13 @@ class Parser {
|
|
|
408
462
|
if (t.value === "limit" || t.value === "offset") {
|
|
409
463
|
this.fail(`unexpected '${t.value}' after \`${last}\` — a bound is a non-negative number literal, a binding, or (inside a block) an outer reference \`^name\``);
|
|
410
464
|
}
|
|
465
|
+
if (t.value === "depth" && !ctx.destination) {
|
|
466
|
+
this.fail(`unexpected 'depth' after \`${last}\` — \`depth\` is a clause of a follow destination block only (\`follow <relation> { depth 2 }\`)`);
|
|
467
|
+
}
|
|
468
|
+
if (t.value === "depth")
|
|
469
|
+
this.fail("expected an integer after `depth` (`depth 1`..`depth 8`)");
|
|
470
|
+
if (t.value === "frontier")
|
|
471
|
+
this.fail(`unexpected 'frontier' after \`${last}\` — \`frontier\` takes an expression (\`frontier <expr>\`) and belongs to a body that has a \`follow\``);
|
|
411
472
|
}
|
|
412
473
|
if (last === "from" && t.type === "ident" && CONSUMERS.has(t.value)) {
|
|
413
474
|
this.fail(`unexpected \`${t.value}\` after \`from\` — a whole-query consumer is written \`<collection> ${t.value} { … }\`; to project a field named ${t.value} write \`select ${t.value} from …\`; a predicate needs \`where\``);
|
|
@@ -477,11 +538,12 @@ class Parser {
|
|
|
477
538
|
if (!this.at("ident", "follow"))
|
|
478
539
|
return false;
|
|
479
540
|
const nx = this.peekAt(1);
|
|
480
|
-
return !!nx && (nx.type === "ident" || nx.type === "binding" || this.atScopeRefAt(1));
|
|
541
|
+
return !!nx && (nx.type === "ident" || nx.type === "binding" || nx.type === "lparen" || this.atScopeRefAt(1));
|
|
481
542
|
}
|
|
482
|
-
// Whether the cursor is at something that can begin a `follow` destination
|
|
543
|
+
// Whether the cursor is at something that can begin a `follow` destination:
|
|
544
|
+
// a name, a binding, a scope reference, or a tuple's `(`.
|
|
483
545
|
atDestinationStart() {
|
|
484
|
-
return this.at("ident") || this.at("binding") || this.atScopeRefAt(0);
|
|
546
|
+
return this.at("ident") || this.at("binding") || this.at("lparen") || this.atScopeRefAt(0);
|
|
485
547
|
}
|
|
486
548
|
// ---- follow ---------------------------------------------------------------
|
|
487
549
|
parseFollow() {
|
|
@@ -505,93 +567,30 @@ class Parser {
|
|
|
505
567
|
}
|
|
506
568
|
destinations.push(this.parseFollowDestination());
|
|
507
569
|
}
|
|
508
|
-
|
|
509
|
-
if (!this.at("lbrace"))
|
|
510
|
-
return follow;
|
|
511
|
-
this.next(); // '{'
|
|
512
|
-
while (!this.at("eof") && !this.at("rbrace")) {
|
|
513
|
-
if (this.at("kw", "where")) {
|
|
514
|
-
if (follow.where)
|
|
515
|
-
this.fail("duplicate `where` in follow clause");
|
|
516
|
-
this.next();
|
|
517
|
-
// Read in the successor's scope, one deeper than the frontier row.
|
|
518
|
-
follow.where = this.atDepth(this.depth + 1, () => this.parseValueExpr());
|
|
519
|
-
}
|
|
520
|
-
else if (this.at("ident", "frontier")) {
|
|
521
|
-
if (follow.frontier)
|
|
522
|
-
this.fail("duplicate `frontier` in follow clause");
|
|
523
|
-
this.next();
|
|
524
|
-
follow.frontier = this.parseValueExpr();
|
|
525
|
-
}
|
|
526
|
-
else if (this.at("ident", "by")) {
|
|
527
|
-
if (follow.by !== null)
|
|
528
|
-
this.fail("duplicate `by` in follow clause");
|
|
529
|
-
this.next();
|
|
530
|
-
follow.by = this.parseValueExpr();
|
|
531
|
-
}
|
|
532
|
-
else if (this.at("ident", "depth")) {
|
|
533
|
-
if (follow.depth !== null)
|
|
534
|
-
this.fail("duplicate `depth` in follow clause");
|
|
535
|
-
this.next();
|
|
536
|
-
if (!this.at("number"))
|
|
537
|
-
this.fail("expected an integer after `depth`");
|
|
538
|
-
const v = Number(this.next().value);
|
|
539
|
-
if (!Number.isInteger(v) || v < 1 || v > 8)
|
|
540
|
-
this.fail("follow depth must be an integer between 1 and 8");
|
|
541
|
-
follow.depth = v;
|
|
542
|
-
}
|
|
543
|
-
else {
|
|
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 { … }\`)`);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
if (!this.at("rbrace"))
|
|
548
|
-
this.fail("expected '}' to close the follow block");
|
|
549
|
-
this.next();
|
|
550
|
-
follow.span = this.sp(start);
|
|
551
|
-
return follow;
|
|
570
|
+
return { kind: "follow", span: this.sp(start), destinations, distinct };
|
|
552
571
|
}
|
|
553
|
-
// One `follow` destination
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
//
|
|
572
|
+
// One `follow` destination: a receiver (a binding, a dotted name — headed by a
|
|
573
|
+
// bare name, an outer reference or a free-function call — or a tuple) that is
|
|
574
|
+
// either a plain relation of the current row or, when a block follows, a
|
|
575
|
+
// destination block. The block is what `tryOp` recognizes in select position
|
|
576
|
+
// — `{ … }` (the implicit `collect`), `distinct { … }`, or a consumer word and
|
|
577
|
+
// `{` / `distinct {` — read with the destination flag so its body admits
|
|
578
|
+
// `frontier` and `depth`. Only collect/first/single yield rows to walk; the
|
|
579
|
+
// where-position tests are rejected by name. A receiver headed by an outer
|
|
580
|
+
// reference can never be a plain destination (plain destinations are relations
|
|
581
|
+
// of the current row), so without a block it is an error naming the block
|
|
582
|
+
// spellings.
|
|
561
583
|
parseFollowDestination() {
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
if (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
else if (this.at("ident") || this.atScopeRefAt(0)) {
|
|
569
|
-
const startPos = this.start();
|
|
570
|
-
this.parseScopeRef();
|
|
571
|
-
if (this.at("ident")) {
|
|
572
|
-
this.parseNavFrom(this.next(), 0, startPos);
|
|
573
|
-
isReceiver = true;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
if (isReceiver && this.at("ident") && CONSUMERS.has(this.peek().value)) {
|
|
577
|
-
const op = this.peek().value;
|
|
578
|
-
const after = this.peekAt(1);
|
|
579
|
-
const opThenBrace = after?.type === "lbrace";
|
|
580
|
-
const opDistinctBrace = after?.type === "ident" && after.value === "distinct" && this.peekAt(2)?.type === "lbrace";
|
|
581
|
-
if (opThenBrace || opDistinctBrace) {
|
|
582
|
-
if (op !== "collect" && op !== "first" && op !== "single") {
|
|
583
|
-
this.fail(`a follow destination must use collect/first/single, not \`${op}\` (exists/none/count are where-position tests)`);
|
|
584
|
-
}
|
|
585
|
-
this.pos = start;
|
|
586
|
-
const block = this.tryOp(false);
|
|
587
|
-
if (!block)
|
|
588
|
-
this.fail("expected a destination block (`<receiver> collect { … }`) after `follow`");
|
|
589
|
-
return block;
|
|
584
|
+
const outer = this.atScopeRefAt(0);
|
|
585
|
+
const op = this.tryOp(false, true);
|
|
586
|
+
if (op) {
|
|
587
|
+
if (op.op !== "collect" && op.op !== "first" && op.op !== "single") {
|
|
588
|
+
this.fail(`a follow destination must use collect/first/single, not \`${op.op}\` (exists/none/count are where-position tests)`);
|
|
590
589
|
}
|
|
590
|
+
return op;
|
|
591
591
|
}
|
|
592
|
-
|
|
593
|
-
|
|
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 { … }`)");
|
|
592
|
+
if (outer) {
|
|
593
|
+
this.fail("`follow` takes a relation of the current row (`follow <relation>`); an outer reference walks a root collection only as a destination block — `follow ^docs { where … }` or `follow ^docs collect { … }`");
|
|
595
594
|
}
|
|
596
595
|
return this.parseReceiver();
|
|
597
596
|
}
|
|
@@ -603,6 +602,12 @@ class Parser {
|
|
|
603
602
|
parseReceiver() {
|
|
604
603
|
if (this.at("binding"))
|
|
605
604
|
return this.bindingNode(this.next());
|
|
605
|
+
if (this.at("lparen")) {
|
|
606
|
+
const t = this.tryTuple();
|
|
607
|
+
if (t)
|
|
608
|
+
return t;
|
|
609
|
+
this.fail("expected a collection navigation (a property/relation name) or a tuple `(a, b)`");
|
|
610
|
+
}
|
|
606
611
|
const start = this.start();
|
|
607
612
|
const levels = this.parseScopeRef();
|
|
608
613
|
if (!this.at("ident"))
|
|
@@ -812,6 +817,18 @@ class Parser {
|
|
|
812
817
|
break;
|
|
813
818
|
}
|
|
814
819
|
if (this.at("lparen")) {
|
|
820
|
+
// A tuple (0.20) is a value, not a group: the receiver of a consumer test
|
|
821
|
+
// (`(a, b) exists { }`) or a scalar operand (`(a, b).size() > 1`).
|
|
822
|
+
const tuple = this.tryTuple();
|
|
823
|
+
if (tuple) {
|
|
824
|
+
if (this.atBlock())
|
|
825
|
+
return wrapNot(this.finishWhereOp(this.parseConsumerBlock(tuple, this.atBlock(), true)), notStarts);
|
|
826
|
+
let e = this.parsePostfix(tuple);
|
|
827
|
+
for (let i = notStarts.length - 1; i >= 0; i--)
|
|
828
|
+
e = { kind: "unary", span: [notStarts[i], e.span[1]], op: "!", expr: e };
|
|
829
|
+
const expr = this.parseCmp(e);
|
|
830
|
+
return { kind: "scalar", span: expr.span, expr };
|
|
831
|
+
}
|
|
815
832
|
const lparen = this.next().pos;
|
|
816
833
|
const inner = this.parseWhere();
|
|
817
834
|
if (!this.at("rparen"))
|
|
@@ -925,9 +942,11 @@ class Parser {
|
|
|
925
942
|
return op;
|
|
926
943
|
}
|
|
927
944
|
// Detect + parse a postfix consumer op `<receiver> <consumer> { <sub> }` — or
|
|
928
|
-
// its implicit-`collect`
|
|
929
|
-
// when the lookahead is not a
|
|
930
|
-
|
|
945
|
+
// its implicit-`collect` forms `<receiver> { <sub> }` and `<receiver> distinct
|
|
946
|
+
// { <sub> }` (0.20). Returns null (rewinding) when the lookahead is not a
|
|
947
|
+
// directive. `destination` marks a follow destination block (its body admits
|
|
948
|
+
// `frontier`/`depth`).
|
|
949
|
+
tryOp(inWhere, destination = false) {
|
|
931
950
|
const start = this.pos;
|
|
932
951
|
const startPos = this.start();
|
|
933
952
|
let receiver;
|
|
@@ -939,6 +958,16 @@ class Parser {
|
|
|
939
958
|
let scopeErr = null;
|
|
940
959
|
if (this.at("binding"))
|
|
941
960
|
receiver = this.bindingNode(this.next());
|
|
961
|
+
else if (this.at("lparen")) {
|
|
962
|
+
// A tuple receiver, `(a, b) { … }`; a single parenthesized expression is
|
|
963
|
+
// grouping and never a receiver.
|
|
964
|
+
const t = this.tryTuple();
|
|
965
|
+
if (!t) {
|
|
966
|
+
this.pos = start;
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
receiver = t;
|
|
970
|
+
}
|
|
942
971
|
else if (this.at("ident") || this.atScopeRefAt(0)) {
|
|
943
972
|
let levels = 0;
|
|
944
973
|
try {
|
|
@@ -960,21 +989,31 @@ class Parser {
|
|
|
960
989
|
}
|
|
961
990
|
else
|
|
962
991
|
return null;
|
|
963
|
-
const op = this.
|
|
992
|
+
const op = this.atBlock();
|
|
964
993
|
if (op) {
|
|
965
994
|
if (scopeErr)
|
|
966
995
|
throw scopeErr;
|
|
967
996
|
if (receiver.kind === "ident" && LITERAL_WORDS.has(receiver.name))
|
|
968
997
|
this.fail(`\`${receiver.name}\` is a literal, not a collection`);
|
|
969
998
|
// `count { }` with nothing before it: the consumer word is not a receiver.
|
|
970
|
-
if (op === "collect" && receiver.kind === "ident" && CONSUMERS.has(receiver.name)) {
|
|
999
|
+
if (op === "collect" && receiver.kind === "ident" && CONSUMERS.has(receiver.name) && this.at("lbrace")) {
|
|
971
1000
|
this.fail(`unexpected '{' after \`${receiver.name}\` — a consumer needs a receiver: \`<collection> ${receiver.name} { … }\``);
|
|
972
1001
|
}
|
|
973
|
-
return this.parseConsumerBlock(receiver, op, inWhere);
|
|
1002
|
+
return this.parseConsumerBlock(receiver, op, inWhere, destination);
|
|
974
1003
|
}
|
|
975
1004
|
this.pos = start;
|
|
976
1005
|
return null;
|
|
977
1006
|
}
|
|
1007
|
+
// The cursor is at what opens a block over an already-read receiver: `{` or
|
|
1008
|
+
// `distinct {` (the implicit `collect`), or a consumer word and `{` /
|
|
1009
|
+
// `distinct {`. Consumes nothing.
|
|
1010
|
+
atBlock() {
|
|
1011
|
+
if (this.at("lbrace"))
|
|
1012
|
+
return "collect";
|
|
1013
|
+
if (this.at("ident", "distinct") && this.peekAt(1)?.type === "lbrace")
|
|
1014
|
+
return "collect";
|
|
1015
|
+
return this.atConsumerBlock();
|
|
1016
|
+
}
|
|
978
1017
|
// The cursor is at a consumer word that opens a block — `<op> {` or
|
|
979
1018
|
// `<op> distinct {`. Consumes nothing.
|
|
980
1019
|
atConsumerBlock() {
|
|
@@ -986,10 +1025,12 @@ class Parser {
|
|
|
986
1025
|
return opThenBrace || opDistinctBrace ? this.peek().value : null;
|
|
987
1026
|
}
|
|
988
1027
|
// `[<op>] [distinct] { <sub> }` over an already-parsed receiver; the cursor is
|
|
989
|
-
// at the consumer word, or at the `{` of the implicit `collect`.
|
|
990
|
-
|
|
1028
|
+
// at the consumer word, at `distinct`, or at the `{` of the implicit `collect`.
|
|
1029
|
+
// `destination` marks a follow destination block (0.20): its body may carry
|
|
1030
|
+
// `frontier` and `depth`.
|
|
1031
|
+
parseConsumerBlock(receiver, op, inWhere, destination = false) {
|
|
991
1032
|
const startPos = receiver.span[0];
|
|
992
|
-
if (this.at("ident"))
|
|
1033
|
+
if (this.at("ident") && CONSUMERS.has(this.peek().value))
|
|
993
1034
|
this.next(); // the consumer word (absent for `x { … }`)
|
|
994
1035
|
let distinct = false;
|
|
995
1036
|
if (this.at("ident", "distinct")) {
|
|
@@ -1000,7 +1041,7 @@ class Parser {
|
|
|
1000
1041
|
const defer = this.deferLookup;
|
|
1001
1042
|
this.deferLookup = false;
|
|
1002
1043
|
// 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" }));
|
|
1044
|
+
const body = this.atDepth(this.depth + 1, () => this.parseBody({ top: false, op, liftsAllowed: inWhere && op === "collect", destination }));
|
|
1004
1045
|
this.deferLookup = defer;
|
|
1005
1046
|
if (!this.at("rbrace"))
|
|
1006
1047
|
this.fail(`expected '}' to close the ${op} { … } block`);
|
|
@@ -1013,18 +1054,49 @@ class Parser {
|
|
|
1013
1054
|
select: body.select,
|
|
1014
1055
|
orderBy: body.orderBy,
|
|
1015
1056
|
follow: body.follow,
|
|
1057
|
+
frontier: body.frontier,
|
|
1058
|
+
depth: body.depth,
|
|
1016
1059
|
values: body.values,
|
|
1017
1060
|
limit: body.limit,
|
|
1018
1061
|
offset: body.offset,
|
|
1019
1062
|
};
|
|
1020
1063
|
return { kind: "op", span: this.sp(startPos), receiver, op, sub, countCmp: null, distinct: distinct || body.distinct };
|
|
1021
1064
|
}
|
|
1065
|
+
// A tuple `(e1, e2, …)` at the cursor's `(` (0.20): two or more comma-separated
|
|
1066
|
+
// expressions. A single parenthesized expression is grouping, not a tuple —
|
|
1067
|
+
// the cursor is rewound and null returned (also on a parse error, so the
|
|
1068
|
+
// caller's own reading reports its own error).
|
|
1069
|
+
tryTuple() {
|
|
1070
|
+
const save = this.pos;
|
|
1071
|
+
const lparen = this.next().pos; // '('
|
|
1072
|
+
try {
|
|
1073
|
+
const items = [this.parseValueExpr()];
|
|
1074
|
+
if (!this.at("comma")) {
|
|
1075
|
+
this.pos = save;
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
while (this.at("comma")) {
|
|
1079
|
+
this.next();
|
|
1080
|
+
items.push(this.parseValueExpr());
|
|
1081
|
+
}
|
|
1082
|
+
if (!this.at("rparen"))
|
|
1083
|
+
this.fail("expected ')' to close the tuple");
|
|
1084
|
+
this.next();
|
|
1085
|
+
return { kind: "tuple", span: [lparen, this.end()], items };
|
|
1086
|
+
}
|
|
1087
|
+
catch (err) {
|
|
1088
|
+
if (!(err instanceof OqxError))
|
|
1089
|
+
throw err;
|
|
1090
|
+
this.pos = save;
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1022
1094
|
// A bracket suffix on `recv` (GRAMMAR §4): `[n]` / `[${…}]` is positional —
|
|
1023
1095
|
// `recv first { offset n }` — and anything else a predicate —
|
|
1024
1096
|
// `recv first { where p }`; a trailing `!` makes the index required and the
|
|
1025
1097
|
// predicate a `single` (required too, so that zero matches is an error).
|
|
1026
1098
|
parseBracket(recv) {
|
|
1027
|
-
if (recv.kind === "lit")
|
|
1099
|
+
if (recv.kind === "lit" || recv.kind === "template")
|
|
1028
1100
|
this.fail(`\`${describe(recv)}\` is a literal, not a collection`);
|
|
1029
1101
|
const start = recv.span[0];
|
|
1030
1102
|
const lbracket = this.next().pos; // '['
|
|
@@ -1055,7 +1127,7 @@ class Parser {
|
|
|
1055
1127
|
this.next();
|
|
1056
1128
|
const subSpan = [lbracket, this.end()];
|
|
1057
1129
|
const sub = (parts) => ({
|
|
1058
|
-
kind: "subquery", span: subSpan, from: [], where: null, select: [], orderBy: null, follow: null, values: false, limit: null, offset: null, ...parts,
|
|
1130
|
+
kind: "subquery", span: subSpan, from: [], where: null, select: [], orderBy: null, follow: null, frontier: null, depth: null, values: false, limit: null, offset: null, ...parts,
|
|
1059
1131
|
});
|
|
1060
1132
|
const bang = this.atOp("!") ? this.next() : null;
|
|
1061
1133
|
if (index) {
|
|
@@ -1150,12 +1222,16 @@ class Parser {
|
|
|
1150
1222
|
// consumers, …) are lexed as bare idents, so they must NOT count as a value
|
|
1151
1223
|
// start, or `where age in 18.. order by name` would read `order` as the bound.
|
|
1152
1224
|
canStartValue() {
|
|
1153
|
-
|
|
1225
|
+
return this.startsValue(this.peek());
|
|
1226
|
+
}
|
|
1227
|
+
startsValue(t) {
|
|
1228
|
+
if (!t)
|
|
1229
|
+
return false;
|
|
1154
1230
|
if (t.type === "ident")
|
|
1155
1231
|
return !CLAUSE_WORDS.has(t.value);
|
|
1156
1232
|
if (t.type === "kw")
|
|
1157
1233
|
return t.value === "is" || t.value === "not"; // the prefix operators
|
|
1158
|
-
if (t.type === "number" || t.type === "string" ||
|
|
1234
|
+
if (t.type === "number" || t.type === "string" || t.type === "tmpl_start" ||
|
|
1159
1235
|
t.type === "binding" || t.type === "lparen" || t.type === "caret")
|
|
1160
1236
|
return true;
|
|
1161
1237
|
return t.type === "op" && (t.value === "-" || t.value === "!");
|
|
@@ -1225,16 +1301,18 @@ class Parser {
|
|
|
1225
1301
|
const bang = this.next();
|
|
1226
1302
|
expr = { kind: "required", span: [start, bang.end], expr };
|
|
1227
1303
|
}
|
|
1228
|
-
else if (this.atValueDirective()) {
|
|
1304
|
+
else if (this.atValueDirective() || this.at("lbrace") || (this.at("ident", "distinct") && this.peekAt(1)?.type === "lbrace")) {
|
|
1229
1305
|
// A value-position directive, `recv first { … }` — how a bracket chain
|
|
1230
|
-
// prints back
|
|
1231
|
-
// the
|
|
1232
|
-
|
|
1306
|
+
// prints back — or the implicit `collect` of a bare `{ … }` / `distinct
|
|
1307
|
+
// { … }` (0.20: the receiver-block sugar stands wherever a value does,
|
|
1308
|
+
// `size(children { is checked })`). exists/none/count have no value form
|
|
1309
|
+
// and are left to the body's own error.
|
|
1310
|
+
if (expr.kind === "lit" || expr.kind === "template")
|
|
1233
1311
|
this.fail(`\`${describe(expr)}\` is a literal, not a collection`);
|
|
1234
|
-
expr = this.parseConsumerBlock(expr, this.
|
|
1312
|
+
expr = this.parseConsumerBlock(expr, this.atBlock(), false);
|
|
1235
1313
|
}
|
|
1236
1314
|
else {
|
|
1237
|
-
break; //
|
|
1315
|
+
break; // anything else ends the chain
|
|
1238
1316
|
}
|
|
1239
1317
|
}
|
|
1240
1318
|
return expr;
|
|
@@ -1279,6 +1357,8 @@ class Parser {
|
|
|
1279
1357
|
this.next();
|
|
1280
1358
|
return { kind: "lit", span: [t.pos, t.end], value: t.value };
|
|
1281
1359
|
}
|
|
1360
|
+
if (t.type === "tmpl_start")
|
|
1361
|
+
return this.parseTemplateLiteral();
|
|
1282
1362
|
if (t.type === "binding") {
|
|
1283
1363
|
this.next();
|
|
1284
1364
|
return this.bindingNode(t);
|
|
@@ -1286,6 +1366,18 @@ class Parser {
|
|
|
1286
1366
|
if (t.type === "lparen") {
|
|
1287
1367
|
this.next();
|
|
1288
1368
|
const e = this.parseValueExpr();
|
|
1369
|
+
if (this.at("comma")) {
|
|
1370
|
+
// A tuple `(e1, e2, …)` (0.20): two or more elements.
|
|
1371
|
+
const items = [e];
|
|
1372
|
+
while (this.at("comma")) {
|
|
1373
|
+
this.next();
|
|
1374
|
+
items.push(this.parseValueExpr());
|
|
1375
|
+
}
|
|
1376
|
+
if (!this.at("rparen"))
|
|
1377
|
+
this.fail("expected ')' to close the tuple");
|
|
1378
|
+
this.next();
|
|
1379
|
+
return { kind: "tuple", span: [t.pos, this.end()], items };
|
|
1380
|
+
}
|
|
1289
1381
|
if (!this.at("rparen"))
|
|
1290
1382
|
this.fail("expected ')'");
|
|
1291
1383
|
this.next();
|
|
@@ -1304,6 +1396,46 @@ class Parser {
|
|
|
1304
1396
|
}
|
|
1305
1397
|
this.fail(`unexpected ${this.tokDesc()} — expected a value`);
|
|
1306
1398
|
}
|
|
1399
|
+
// A template literal (GRAMMAR §4, since 0.21): the cursor is at the opening
|
|
1400
|
+
// backtick. The lexer has already split the literal into text runs and `${ … }`
|
|
1401
|
+
// interpolations (with balanced braces, so an interpolation may hold a block
|
|
1402
|
+
// or another template literal); each interpolation holds one full value
|
|
1403
|
+
// expression, read at the current scope depth. `parts` alternates strictly —
|
|
1404
|
+
// text, expression, text, … — with a (possibly empty) text part first and last.
|
|
1405
|
+
parseTemplateLiteral() {
|
|
1406
|
+
const open = this.next(); // the backtick
|
|
1407
|
+
const parts = [];
|
|
1408
|
+
let text = "";
|
|
1409
|
+
for (;;) {
|
|
1410
|
+
const t = this.peek();
|
|
1411
|
+
if (t.type === "tmpl_text") {
|
|
1412
|
+
this.next();
|
|
1413
|
+
text += t.value;
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
if (t.type === "tmpl_end") {
|
|
1417
|
+
this.next();
|
|
1418
|
+
break;
|
|
1419
|
+
}
|
|
1420
|
+
if (t.type === "tmpl_interp_start") {
|
|
1421
|
+
this.next();
|
|
1422
|
+
if (this.at("tmpl_interp_end")) {
|
|
1423
|
+
this.failAt(t.pos, "empty interpolation `${}` in a template literal — an interpolation holds an expression (`${name}`)");
|
|
1424
|
+
}
|
|
1425
|
+
const e = this.parseValueExpr();
|
|
1426
|
+
if (!this.at("tmpl_interp_end"))
|
|
1427
|
+
this.fail("expected '}' to close the `${…}` interpolation of a template literal");
|
|
1428
|
+
this.next();
|
|
1429
|
+
parts.push(text, e);
|
|
1430
|
+
text = "";
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
// Unreachable: the lexer closes every literal it opens or fails.
|
|
1434
|
+
this.fail(`unexpected ${this.tokDesc()} in a template literal`);
|
|
1435
|
+
}
|
|
1436
|
+
parts.push(text);
|
|
1437
|
+
return { kind: "template", span: [open.pos, this.end()], parts };
|
|
1438
|
+
}
|
|
1307
1439
|
}
|
|
1308
1440
|
// The default key of an unaliased projection item: the last segment of a bare /
|
|
1309
1441
|
// dotted / outer navigation (`name`, `meta.slug` → "slug", `^name`), else null.
|