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