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