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