@omgbase/oqx 0.15.0 → 0.18.0

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