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