@omgbase/oqx 0.18.0 → 0.20.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 +24 -0
- package/README.md +149 -64
- package/dist/ast.d.ts +21 -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 +7 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +7 -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 +235 -83
- 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/optimize/analysis.d.ts +3 -2
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +14 -15
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +196 -114
- package/dist/parser.js.map +1 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +13 -13
- package/dist/print.js.map +1 -1
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +11 -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 +26 -8
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +23 -6
- package/src/build.ts +8 -7
- package/src/context.ts +22 -11
- package/src/engine.ts +236 -90
- package/src/index.ts +2 -1
- package/src/optimize/analysis.ts +13 -12
- package/src/parser.ts +180 -96
- package/src/print.ts +12 -10
- package/src/resolve.ts +11 -7
- package/src/version.ts +1 -1
- package/src/walk.ts +32 -11
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,9 @@
|
|
|
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
|
+
//
|
|
60
67
|
// Principle of least surprise, applied to the grammar: a rule a careful user
|
|
61
68
|
// would not predict is a bug. Hence `!` binds tighter than comparison in every
|
|
62
69
|
// position (`!a == b` is `(!a) == b`, as in C); parentheses in `where` group a
|
|
@@ -71,7 +78,16 @@ import { resolveSelect, resolveWhere } from "./resolve.js";
|
|
|
71
78
|
import { printTemplate } from "./print.js";
|
|
72
79
|
const CONSUMERS = new Set(["collect", "exists", "none", "count", "first", "single"]);
|
|
73
80
|
// The fixed clause order of a body (ADR-020). Each clause appears at most once.
|
|
74
|
-
|
|
81
|
+
// `frontier` is a clause of every body that has a `follow` (a destination
|
|
82
|
+
// block's marks the occurrences it reaches, any other body's its seeds — 0.20);
|
|
83
|
+
// `depth` is legal only in a follow destination block, and the order sentence
|
|
84
|
+
// names it only there.
|
|
85
|
+
const CLAUSE_ORDER = ["select", "from", "where", "frontier", "depth", "follow", "order by", "limit", "offset"];
|
|
86
|
+
const DESTINATION_ONLY = new Set(["depth"]);
|
|
87
|
+
// The clauses a body may hold, in order.
|
|
88
|
+
function clausesOf(ctx) {
|
|
89
|
+
return CLAUSE_ORDER.filter((c) => ctx.destination || !DESTINATION_ONLY.has(c));
|
|
90
|
+
}
|
|
75
91
|
// Contextual clause words lexed as bare idents; an open-ended range must stop
|
|
76
92
|
// before them rather than consume them as its high bound.
|
|
77
93
|
const CLAUSE_WORDS = new Set([
|
|
@@ -86,7 +102,7 @@ const MUL_OPS = new Set(["*", "/", "%"]);
|
|
|
86
102
|
// `true`/`false`/`null` are literals in every position, so they can never name a
|
|
87
103
|
// receiver (`true exists { … }`, `follow null`).
|
|
88
104
|
const LITERAL_WORDS = new Set(["true", "false", "null"]);
|
|
89
|
-
const TOP_CTX = { top: true, op: null, liftsAllowed: false };
|
|
105
|
+
const TOP_CTX = { top: true, op: null, liftsAllowed: false, destination: false };
|
|
90
106
|
/** Parse a tagged-template call into a Query. */
|
|
91
107
|
export function parseTemplate(fragments, values) {
|
|
92
108
|
return new Parser(lexTemplate(fragments, values)).parseQuery();
|
|
@@ -207,6 +223,7 @@ class Parser {
|
|
|
207
223
|
orderBy: body.orderBy,
|
|
208
224
|
consumer: "collect",
|
|
209
225
|
follow: body.follow,
|
|
226
|
+
frontier: body.frontier,
|
|
210
227
|
distinct: body.distinct,
|
|
211
228
|
values: body.values,
|
|
212
229
|
limit: body.limit,
|
|
@@ -218,7 +235,7 @@ class Parser {
|
|
|
218
235
|
queryFromOp(e, start) {
|
|
219
236
|
return {
|
|
220
237
|
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,
|
|
238
|
+
orderBy: e.sub.orderBy, consumer: e.op, follow: e.sub.follow, frontier: e.sub.frontier, distinct: e.distinct, values: e.sub.values,
|
|
222
239
|
limit: e.sub.limit, offset: e.sub.offset,
|
|
223
240
|
};
|
|
224
241
|
}
|
|
@@ -237,6 +254,8 @@ class Parser {
|
|
|
237
254
|
let select = [];
|
|
238
255
|
let orderBy = null;
|
|
239
256
|
let follow = null;
|
|
257
|
+
let frontier = null;
|
|
258
|
+
let depth = null;
|
|
240
259
|
let distinct = false;
|
|
241
260
|
let values = false;
|
|
242
261
|
let limit = null;
|
|
@@ -251,7 +270,7 @@ class Parser {
|
|
|
251
270
|
if (idx === stage)
|
|
252
271
|
this.fail(`duplicate \`${clause}\` clause`);
|
|
253
272
|
if (idx < stage) {
|
|
254
|
-
this.fail(`\`${clause}\` must come before \`${CLAUSE_ORDER[stage]}\` — OQX clause order is ${
|
|
273
|
+
this.fail(`\`${clause}\` must come before \`${CLAUSE_ORDER[stage]}\` — OQX clause order is ${clausesOf(ctx).join(", ")}`);
|
|
255
274
|
}
|
|
256
275
|
stage = idx;
|
|
257
276
|
};
|
|
@@ -278,6 +297,28 @@ class Parser {
|
|
|
278
297
|
where = this.atDepth(rowDepth, () => this.parseWhere());
|
|
279
298
|
continue;
|
|
280
299
|
}
|
|
300
|
+
// `frontier P` (0.20) — in a follow destination block the destination's
|
|
301
|
+
// own clause, read in the occurrence's scope whose parent is the walk's
|
|
302
|
+
// enclosing scope: one level OUT of the block's rows (SEMANTICS §20), so
|
|
303
|
+
// `^` there is the enclosing scope, not the frontier row. In any other
|
|
304
|
+
// body it marks the seeds of the body's own `follow` (checked below) and
|
|
305
|
+
// is read in the seed's scope, like `where`.
|
|
306
|
+
if (this.at("ident", "frontier") && this.startsValue(this.peekAt(1))) {
|
|
307
|
+
enter("frontier");
|
|
308
|
+
this.next();
|
|
309
|
+
frontier = this.atDepth(ctx.destination ? rowDepth - 1 : rowDepth, () => this.parseValueExpr());
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
// `depth n` — a follow destination block's own clause (0.20).
|
|
313
|
+
if (ctx.destination && this.at("ident", "depth") && this.peekAt(1)?.type === "number") {
|
|
314
|
+
enter("depth");
|
|
315
|
+
this.next();
|
|
316
|
+
const v = Number(this.next().value);
|
|
317
|
+
if (!Number.isInteger(v) || v < 1 || v > 8)
|
|
318
|
+
this.fail("follow depth must be an integer between 1 and 8");
|
|
319
|
+
depth = v;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
281
322
|
if (this.atFollow()) {
|
|
282
323
|
enter("follow");
|
|
283
324
|
follow = this.atDepth(rowDepth, () => this.parseFollow());
|
|
@@ -330,7 +371,12 @@ class Parser {
|
|
|
330
371
|
if (where)
|
|
331
372
|
resolveWhere(select, where, (msg) => this.fail(msg));
|
|
332
373
|
resolveSelect(select, (msg) => this.fail(msg));
|
|
333
|
-
|
|
374
|
+
// Outside a destination block `frontier` marks the seeds of this body's walk,
|
|
375
|
+
// so it needs one.
|
|
376
|
+
if (frontier && !follow && !ctx.destination) {
|
|
377
|
+
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");
|
|
378
|
+
}
|
|
379
|
+
return { froms, where, select, orderBy, follow, frontier, depth, distinct, values, limit, offset };
|
|
334
380
|
}
|
|
335
381
|
// The keyword-less run that opens a body. At the top level it is always the
|
|
336
382
|
// projection. In a block it is a `where`-first body when the leading expression
|
|
@@ -387,8 +433,8 @@ class Parser {
|
|
|
387
433
|
// consumer word where a whole-query directive was meant), or a stray token.
|
|
388
434
|
failUnexpectedInBody(stage, ctx) {
|
|
389
435
|
const t = this.peek();
|
|
390
|
-
const remaining = CLAUSE_ORDER.slice(stage + 1).join("/");
|
|
391
436
|
const last = stage >= 0 ? CLAUSE_ORDER[stage] : null;
|
|
437
|
+
const remaining = clausesOf(ctx).filter((c) => CLAUSE_ORDER.indexOf(c) > stage).join("/");
|
|
392
438
|
const isWord = t.type === "ident" || t.type === "kw" || t.type === "binding" || t.type === "caret";
|
|
393
439
|
// Punctuation, an operator, or a literal after a complete clause is a stray
|
|
394
440
|
// token, not a misplaced predicate: name where the body ends. (A comma keeps
|
|
@@ -408,6 +454,13 @@ class Parser {
|
|
|
408
454
|
if (t.value === "limit" || t.value === "offset") {
|
|
409
455
|
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
456
|
}
|
|
457
|
+
if (t.value === "depth" && !ctx.destination) {
|
|
458
|
+
this.fail(`unexpected 'depth' after \`${last}\` — \`depth\` is a clause of a follow destination block only (\`follow <relation> { depth 2 }\`)`);
|
|
459
|
+
}
|
|
460
|
+
if (t.value === "depth")
|
|
461
|
+
this.fail("expected an integer after `depth` (`depth 1`..`depth 8`)");
|
|
462
|
+
if (t.value === "frontier")
|
|
463
|
+
this.fail(`unexpected 'frontier' after \`${last}\` — \`frontier\` takes an expression (\`frontier <expr>\`) and belongs to a body that has a \`follow\``);
|
|
411
464
|
}
|
|
412
465
|
if (last === "from" && t.type === "ident" && CONSUMERS.has(t.value)) {
|
|
413
466
|
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 +530,12 @@ class Parser {
|
|
|
477
530
|
if (!this.at("ident", "follow"))
|
|
478
531
|
return false;
|
|
479
532
|
const nx = this.peekAt(1);
|
|
480
|
-
return !!nx && (nx.type === "ident" || nx.type === "binding" || this.atScopeRefAt(1));
|
|
533
|
+
return !!nx && (nx.type === "ident" || nx.type === "binding" || nx.type === "lparen" || this.atScopeRefAt(1));
|
|
481
534
|
}
|
|
482
|
-
// Whether the cursor is at something that can begin a `follow` destination
|
|
535
|
+
// Whether the cursor is at something that can begin a `follow` destination:
|
|
536
|
+
// a name, a binding, a scope reference, or a tuple's `(`.
|
|
483
537
|
atDestinationStart() {
|
|
484
|
-
return this.at("ident") || this.at("binding") || this.atScopeRefAt(0);
|
|
538
|
+
return this.at("ident") || this.at("binding") || this.at("lparen") || this.atScopeRefAt(0);
|
|
485
539
|
}
|
|
486
540
|
// ---- follow ---------------------------------------------------------------
|
|
487
541
|
parseFollow() {
|
|
@@ -505,93 +559,30 @@ class Parser {
|
|
|
505
559
|
}
|
|
506
560
|
destinations.push(this.parseFollowDestination());
|
|
507
561
|
}
|
|
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;
|
|
562
|
+
return { kind: "follow", span: this.sp(start), destinations, distinct };
|
|
552
563
|
}
|
|
553
|
-
// One `follow` destination
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
//
|
|
564
|
+
// One `follow` destination: a receiver (a binding, a dotted name — headed by a
|
|
565
|
+
// bare name, an outer reference or a free-function call — or a tuple) that is
|
|
566
|
+
// either a plain relation of the current row or, when a block follows, a
|
|
567
|
+
// destination block. The block is what `tryOp` recognizes in select position
|
|
568
|
+
// — `{ … }` (the implicit `collect`), `distinct { … }`, or a consumer word and
|
|
569
|
+
// `{` / `distinct {` — read with the destination flag so its body admits
|
|
570
|
+
// `frontier` and `depth`. Only collect/first/single yield rows to walk; the
|
|
571
|
+
// where-position tests are rejected by name. A receiver headed by an outer
|
|
572
|
+
// reference can never be a plain destination (plain destinations are relations
|
|
573
|
+
// of the current row), so without a block it is an error naming the block
|
|
574
|
+
// spellings.
|
|
561
575
|
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;
|
|
576
|
+
const outer = this.atScopeRefAt(0);
|
|
577
|
+
const op = this.tryOp(false, true);
|
|
578
|
+
if (op) {
|
|
579
|
+
if (op.op !== "collect" && op.op !== "first" && op.op !== "single") {
|
|
580
|
+
this.fail(`a follow destination must use collect/first/single, not \`${op.op}\` (exists/none/count are where-position tests)`);
|
|
590
581
|
}
|
|
582
|
+
return op;
|
|
591
583
|
}
|
|
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 { … }`)");
|
|
584
|
+
if (outer) {
|
|
585
|
+
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
586
|
}
|
|
596
587
|
return this.parseReceiver();
|
|
597
588
|
}
|
|
@@ -603,6 +594,12 @@ class Parser {
|
|
|
603
594
|
parseReceiver() {
|
|
604
595
|
if (this.at("binding"))
|
|
605
596
|
return this.bindingNode(this.next());
|
|
597
|
+
if (this.at("lparen")) {
|
|
598
|
+
const t = this.tryTuple();
|
|
599
|
+
if (t)
|
|
600
|
+
return t;
|
|
601
|
+
this.fail("expected a collection navigation (a property/relation name) or a tuple `(a, b)`");
|
|
602
|
+
}
|
|
606
603
|
const start = this.start();
|
|
607
604
|
const levels = this.parseScopeRef();
|
|
608
605
|
if (!this.at("ident"))
|
|
@@ -812,6 +809,18 @@ class Parser {
|
|
|
812
809
|
break;
|
|
813
810
|
}
|
|
814
811
|
if (this.at("lparen")) {
|
|
812
|
+
// A tuple (0.20) is a value, not a group: the receiver of a consumer test
|
|
813
|
+
// (`(a, b) exists { }`) or a scalar operand (`(a, b).size() > 1`).
|
|
814
|
+
const tuple = this.tryTuple();
|
|
815
|
+
if (tuple) {
|
|
816
|
+
if (this.atBlock())
|
|
817
|
+
return wrapNot(this.finishWhereOp(this.parseConsumerBlock(tuple, this.atBlock(), true)), notStarts);
|
|
818
|
+
let e = this.parsePostfix(tuple);
|
|
819
|
+
for (let i = notStarts.length - 1; i >= 0; i--)
|
|
820
|
+
e = { kind: "unary", span: [notStarts[i], e.span[1]], op: "!", expr: e };
|
|
821
|
+
const expr = this.parseCmp(e);
|
|
822
|
+
return { kind: "scalar", span: expr.span, expr };
|
|
823
|
+
}
|
|
815
824
|
const lparen = this.next().pos;
|
|
816
825
|
const inner = this.parseWhere();
|
|
817
826
|
if (!this.at("rparen"))
|
|
@@ -925,9 +934,11 @@ class Parser {
|
|
|
925
934
|
return op;
|
|
926
935
|
}
|
|
927
936
|
// Detect + parse a postfix consumer op `<receiver> <consumer> { <sub> }` — or
|
|
928
|
-
// its implicit-`collect`
|
|
929
|
-
// when the lookahead is not a
|
|
930
|
-
|
|
937
|
+
// its implicit-`collect` forms `<receiver> { <sub> }` and `<receiver> distinct
|
|
938
|
+
// { <sub> }` (0.20). Returns null (rewinding) when the lookahead is not a
|
|
939
|
+
// directive. `destination` marks a follow destination block (its body admits
|
|
940
|
+
// `frontier`/`depth`).
|
|
941
|
+
tryOp(inWhere, destination = false) {
|
|
931
942
|
const start = this.pos;
|
|
932
943
|
const startPos = this.start();
|
|
933
944
|
let receiver;
|
|
@@ -939,6 +950,16 @@ class Parser {
|
|
|
939
950
|
let scopeErr = null;
|
|
940
951
|
if (this.at("binding"))
|
|
941
952
|
receiver = this.bindingNode(this.next());
|
|
953
|
+
else if (this.at("lparen")) {
|
|
954
|
+
// A tuple receiver, `(a, b) { … }`; a single parenthesized expression is
|
|
955
|
+
// grouping and never a receiver.
|
|
956
|
+
const t = this.tryTuple();
|
|
957
|
+
if (!t) {
|
|
958
|
+
this.pos = start;
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
receiver = t;
|
|
962
|
+
}
|
|
942
963
|
else if (this.at("ident") || this.atScopeRefAt(0)) {
|
|
943
964
|
let levels = 0;
|
|
944
965
|
try {
|
|
@@ -960,21 +981,31 @@ class Parser {
|
|
|
960
981
|
}
|
|
961
982
|
else
|
|
962
983
|
return null;
|
|
963
|
-
const op = this.
|
|
984
|
+
const op = this.atBlock();
|
|
964
985
|
if (op) {
|
|
965
986
|
if (scopeErr)
|
|
966
987
|
throw scopeErr;
|
|
967
988
|
if (receiver.kind === "ident" && LITERAL_WORDS.has(receiver.name))
|
|
968
989
|
this.fail(`\`${receiver.name}\` is a literal, not a collection`);
|
|
969
990
|
// `count { }` with nothing before it: the consumer word is not a receiver.
|
|
970
|
-
if (op === "collect" && receiver.kind === "ident" && CONSUMERS.has(receiver.name)) {
|
|
991
|
+
if (op === "collect" && receiver.kind === "ident" && CONSUMERS.has(receiver.name) && this.at("lbrace")) {
|
|
971
992
|
this.fail(`unexpected '{' after \`${receiver.name}\` — a consumer needs a receiver: \`<collection> ${receiver.name} { … }\``);
|
|
972
993
|
}
|
|
973
|
-
return this.parseConsumerBlock(receiver, op, inWhere);
|
|
994
|
+
return this.parseConsumerBlock(receiver, op, inWhere, destination);
|
|
974
995
|
}
|
|
975
996
|
this.pos = start;
|
|
976
997
|
return null;
|
|
977
998
|
}
|
|
999
|
+
// The cursor is at what opens a block over an already-read receiver: `{` or
|
|
1000
|
+
// `distinct {` (the implicit `collect`), or a consumer word and `{` /
|
|
1001
|
+
// `distinct {`. Consumes nothing.
|
|
1002
|
+
atBlock() {
|
|
1003
|
+
if (this.at("lbrace"))
|
|
1004
|
+
return "collect";
|
|
1005
|
+
if (this.at("ident", "distinct") && this.peekAt(1)?.type === "lbrace")
|
|
1006
|
+
return "collect";
|
|
1007
|
+
return this.atConsumerBlock();
|
|
1008
|
+
}
|
|
978
1009
|
// The cursor is at a consumer word that opens a block — `<op> {` or
|
|
979
1010
|
// `<op> distinct {`. Consumes nothing.
|
|
980
1011
|
atConsumerBlock() {
|
|
@@ -986,10 +1017,12 @@ class Parser {
|
|
|
986
1017
|
return opThenBrace || opDistinctBrace ? this.peek().value : null;
|
|
987
1018
|
}
|
|
988
1019
|
// `[<op>] [distinct] { <sub> }` over an already-parsed receiver; the cursor is
|
|
989
|
-
// at the consumer word, or at the `{` of the implicit `collect`.
|
|
990
|
-
|
|
1020
|
+
// at the consumer word, at `distinct`, or at the `{` of the implicit `collect`.
|
|
1021
|
+
// `destination` marks a follow destination block (0.20): its body may carry
|
|
1022
|
+
// `frontier` and `depth`.
|
|
1023
|
+
parseConsumerBlock(receiver, op, inWhere, destination = false) {
|
|
991
1024
|
const startPos = receiver.span[0];
|
|
992
|
-
if (this.at("ident"))
|
|
1025
|
+
if (this.at("ident") && CONSUMERS.has(this.peek().value))
|
|
993
1026
|
this.next(); // the consumer word (absent for `x { … }`)
|
|
994
1027
|
let distinct = false;
|
|
995
1028
|
if (this.at("ident", "distinct")) {
|
|
@@ -1000,7 +1033,7 @@ class Parser {
|
|
|
1000
1033
|
const defer = this.deferLookup;
|
|
1001
1034
|
this.deferLookup = false;
|
|
1002
1035
|
// 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" }));
|
|
1036
|
+
const body = this.atDepth(this.depth + 1, () => this.parseBody({ top: false, op, liftsAllowed: inWhere && op === "collect", destination }));
|
|
1004
1037
|
this.deferLookup = defer;
|
|
1005
1038
|
if (!this.at("rbrace"))
|
|
1006
1039
|
this.fail(`expected '}' to close the ${op} { … } block`);
|
|
@@ -1013,12 +1046,43 @@ class Parser {
|
|
|
1013
1046
|
select: body.select,
|
|
1014
1047
|
orderBy: body.orderBy,
|
|
1015
1048
|
follow: body.follow,
|
|
1049
|
+
frontier: body.frontier,
|
|
1050
|
+
depth: body.depth,
|
|
1016
1051
|
values: body.values,
|
|
1017
1052
|
limit: body.limit,
|
|
1018
1053
|
offset: body.offset,
|
|
1019
1054
|
};
|
|
1020
1055
|
return { kind: "op", span: this.sp(startPos), receiver, op, sub, countCmp: null, distinct: distinct || body.distinct };
|
|
1021
1056
|
}
|
|
1057
|
+
// A tuple `(e1, e2, …)` at the cursor's `(` (0.20): two or more comma-separated
|
|
1058
|
+
// expressions. A single parenthesized expression is grouping, not a tuple —
|
|
1059
|
+
// the cursor is rewound and null returned (also on a parse error, so the
|
|
1060
|
+
// caller's own reading reports its own error).
|
|
1061
|
+
tryTuple() {
|
|
1062
|
+
const save = this.pos;
|
|
1063
|
+
const lparen = this.next().pos; // '('
|
|
1064
|
+
try {
|
|
1065
|
+
const items = [this.parseValueExpr()];
|
|
1066
|
+
if (!this.at("comma")) {
|
|
1067
|
+
this.pos = save;
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
while (this.at("comma")) {
|
|
1071
|
+
this.next();
|
|
1072
|
+
items.push(this.parseValueExpr());
|
|
1073
|
+
}
|
|
1074
|
+
if (!this.at("rparen"))
|
|
1075
|
+
this.fail("expected ')' to close the tuple");
|
|
1076
|
+
this.next();
|
|
1077
|
+
return { kind: "tuple", span: [lparen, this.end()], items };
|
|
1078
|
+
}
|
|
1079
|
+
catch (err) {
|
|
1080
|
+
if (!(err instanceof OqxError))
|
|
1081
|
+
throw err;
|
|
1082
|
+
this.pos = save;
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1022
1086
|
// A bracket suffix on `recv` (GRAMMAR §4): `[n]` / `[${…}]` is positional —
|
|
1023
1087
|
// `recv first { offset n }` — and anything else a predicate —
|
|
1024
1088
|
// `recv first { where p }`; a trailing `!` makes the index required and the
|
|
@@ -1055,7 +1119,7 @@ class Parser {
|
|
|
1055
1119
|
this.next();
|
|
1056
1120
|
const subSpan = [lbracket, this.end()];
|
|
1057
1121
|
const sub = (parts) => ({
|
|
1058
|
-
kind: "subquery", span: subSpan, from: [], where: null, select: [], orderBy: null, follow: null, values: false, limit: null, offset: null, ...parts,
|
|
1122
|
+
kind: "subquery", span: subSpan, from: [], where: null, select: [], orderBy: null, follow: null, frontier: null, depth: null, values: false, limit: null, offset: null, ...parts,
|
|
1059
1123
|
});
|
|
1060
1124
|
const bang = this.atOp("!") ? this.next() : null;
|
|
1061
1125
|
if (index) {
|
|
@@ -1150,7 +1214,11 @@ class Parser {
|
|
|
1150
1214
|
// consumers, …) are lexed as bare idents, so they must NOT count as a value
|
|
1151
1215
|
// start, or `where age in 18.. order by name` would read `order` as the bound.
|
|
1152
1216
|
canStartValue() {
|
|
1153
|
-
|
|
1217
|
+
return this.startsValue(this.peek());
|
|
1218
|
+
}
|
|
1219
|
+
startsValue(t) {
|
|
1220
|
+
if (!t)
|
|
1221
|
+
return false;
|
|
1154
1222
|
if (t.type === "ident")
|
|
1155
1223
|
return !CLAUSE_WORDS.has(t.value);
|
|
1156
1224
|
if (t.type === "kw")
|
|
@@ -1225,16 +1293,18 @@ class Parser {
|
|
|
1225
1293
|
const bang = this.next();
|
|
1226
1294
|
expr = { kind: "required", span: [start, bang.end], expr };
|
|
1227
1295
|
}
|
|
1228
|
-
else if (this.atValueDirective()) {
|
|
1296
|
+
else if (this.atValueDirective() || this.at("lbrace") || (this.at("ident", "distinct") && this.peekAt(1)?.type === "lbrace")) {
|
|
1229
1297
|
// A value-position directive, `recv first { … }` — how a bracket chain
|
|
1230
|
-
// prints back
|
|
1231
|
-
// the
|
|
1298
|
+
// prints back — or the implicit `collect` of a bare `{ … }` / `distinct
|
|
1299
|
+
// { … }` (0.20: the receiver-block sugar stands wherever a value does,
|
|
1300
|
+
// `size(children { is checked })`). exists/none/count have no value form
|
|
1301
|
+
// and are left to the body's own error.
|
|
1232
1302
|
if (expr.kind === "lit")
|
|
1233
1303
|
this.fail(`\`${describe(expr)}\` is a literal, not a collection`);
|
|
1234
|
-
expr = this.parseConsumerBlock(expr, this.
|
|
1304
|
+
expr = this.parseConsumerBlock(expr, this.atBlock(), false);
|
|
1235
1305
|
}
|
|
1236
1306
|
else {
|
|
1237
|
-
break; //
|
|
1307
|
+
break; // anything else ends the chain
|
|
1238
1308
|
}
|
|
1239
1309
|
}
|
|
1240
1310
|
return expr;
|
|
@@ -1286,6 +1356,18 @@ class Parser {
|
|
|
1286
1356
|
if (t.type === "lparen") {
|
|
1287
1357
|
this.next();
|
|
1288
1358
|
const e = this.parseValueExpr();
|
|
1359
|
+
if (this.at("comma")) {
|
|
1360
|
+
// A tuple `(e1, e2, …)` (0.20): two or more elements.
|
|
1361
|
+
const items = [e];
|
|
1362
|
+
while (this.at("comma")) {
|
|
1363
|
+
this.next();
|
|
1364
|
+
items.push(this.parseValueExpr());
|
|
1365
|
+
}
|
|
1366
|
+
if (!this.at("rparen"))
|
|
1367
|
+
this.fail("expected ')' to close the tuple");
|
|
1368
|
+
this.next();
|
|
1369
|
+
return { kind: "tuple", span: [t.pos, this.end()], items };
|
|
1370
|
+
}
|
|
1289
1371
|
if (!this.at("rparen"))
|
|
1290
1372
|
this.fail("expected ')'");
|
|
1291
1373
|
this.next();
|