@omgbase/oqx 0.20.0 → 0.23.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 (55) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +65 -0
  3. package/dist/ast.d.ts +20 -2
  4. package/dist/ast.d.ts.map +1 -1
  5. package/dist/ast.js +11 -1
  6. package/dist/ast.js.map +1 -1
  7. package/dist/build.d.ts +4 -1
  8. package/dist/build.d.ts.map +1 -1
  9. package/dist/build.js +3 -0
  10. package/dist/build.js.map +1 -1
  11. package/dist/engine.d.ts +1 -0
  12. package/dist/engine.d.ts.map +1 -1
  13. package/dist/engine.js +45 -3
  14. package/dist/engine.js.map +1 -1
  15. package/dist/index.d.ts +3 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/lexer.d.ts +3 -3
  20. package/dist/lexer.d.ts.map +1 -1
  21. package/dist/lexer.js +92 -5
  22. package/dist/lexer.js.map +1 -1
  23. package/dist/optimize/analysis.d.ts.map +1 -1
  24. package/dist/optimize/analysis.js +8 -1
  25. package/dist/optimize/analysis.js.map +1 -1
  26. package/dist/parser.d.ts.map +1 -1
  27. package/dist/parser.js +58 -5
  28. package/dist/parser.js.map +1 -1
  29. package/dist/print.js +36 -17
  30. package/dist/print.js.map +1 -1
  31. package/dist/references.d.ts +26 -0
  32. package/dist/references.d.ts.map +1 -0
  33. package/dist/references.js +225 -0
  34. package/dist/references.js.map +1 -0
  35. package/dist/resolve.d.ts.map +1 -1
  36. package/dist/resolve.js +4 -0
  37. package/dist/resolve.js.map +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/dist/walk.d.ts.map +1 -1
  41. package/dist/walk.js +8 -3
  42. package/dist/walk.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/ast.ts +31 -3
  45. package/src/build.ts +4 -1
  46. package/src/engine.ts +40 -3
  47. package/src/index.ts +3 -1
  48. package/src/lexer.ts +86 -10
  49. package/src/optimize/analysis.ts +9 -1
  50. package/src/parser.ts +51 -6
  51. package/src/print.ts +34 -7
  52. package/src/references.ts +238 -0
  53. package/src/resolve.ts +4 -0
  54. package/src/version.ts +1 -1
  55. package/src/walk.ts +7 -3
package/src/lexer.ts CHANGED
@@ -10,7 +10,14 @@
10
10
  // alter the grammar — the prepared-statement / injection-safe property the
11
11
  // host-bindings design note calls for. (A consequence: `${x}` inside a string
12
12
  // literal does not interpolate — that fragment would be an unterminated string —
13
- // which is exactly the desired "interpolation is a value, never source text".)
13
+ // which is exactly the desired "interpolation is a value, never source text".
14
+ // The same holds for a backtick template literal (0.21): a fragment that ends
15
+ // inside one is an unterminated literal, so a bound value never stands in one.)
16
+ //
17
+ // Template literals `` `text ${expr}` `` are lexed with a mode stack: the text
18
+ // between the backticks is one `tmpl_text` token per run (decoded), a `${`
19
+ // opens an interpolation whose tokens are ordinary source until the `}` that
20
+ // balances it, and an interpolation may hold another template literal.
14
21
  //
15
22
  // Positions (`Token.pos` / `Token.end`, the `span` of every AST node, and the
16
23
  // offsets quoted in error messages) count Unicode CODE POINTS over the raw
@@ -37,15 +44,23 @@ export type TokType =
37
44
  | "caret" // ^ — one-scope lift marker
38
45
  | "dot"
39
46
  | "range" // `..` (inclusive) or `...` (exclusive end) — a Ruby-style range operator (value carries which)
40
- | "op" // == != <= >= < > && || ! + - * / % (value carries the operator)
47
+ | "op" // == != <= >= < > && || ! + - * / % ~ ~* !~ !~* (value carries the operator)
41
48
  | "binding" // a ${…} interpolation; `index` names the value slot
49
+ // A template literal (since 0.21): the opening and closing backticks, a run of
50
+ // its decoded text, and the `${` / `}` around an interpolation — whose `}` is a
51
+ // token of its own, so a block's `}` inside the interpolation stays `rbrace`.
52
+ | "tmpl_start"
53
+ | "tmpl_text"
54
+ | "tmpl_interp_start"
55
+ | "tmpl_interp_end"
56
+ | "tmpl_end"
42
57
  | "eof";
43
58
 
44
59
  export interface Token {
45
60
  type: TokType;
46
61
  /** The source text of the token, except: a `string` token carries its decoded
47
- * value (quotes stripped, escapes resolved); a `binding` carries the display
48
- * marker `${N}`; `eof` carries `""`. */
62
+ * value (quotes stripped, escapes resolved), and so does a `tmpl_text` run; a
63
+ * `binding` carries the display marker `${N}`; `eof` carries `""`. */
49
64
  value: string;
50
65
  /** Code-point offset of the token's first character. */
51
66
  pos: number;
@@ -57,8 +72,12 @@ export interface Token {
57
72
  const KEYWORDS = new Set(["from", "where", "select", "is", "not", "and", "or"]);
58
73
 
59
74
  // Multi-char operators, longest first (the scanner tries these before singles).
60
- const MULTI_OPS = ["==", "!=", "<=", ">=", "&&", "||"];
61
- const SINGLE_OPS = new Set(["<", ">", "!", "+", "-", "*", "/", "%"]);
75
+ // The regex operators (since 0.22) are among them: `!~*` is tried before `!~`,
76
+ // which is tried before `!` — so `name!~"B"` is `name !~ "B"`, not a required
77
+ // `name!` — and `~*` before `~`, so `x~*y` is `x ~* y`.
78
+ const TRIPLE_OPS = ["!~*"];
79
+ const MULTI_OPS = ["==", "!=", "<=", ">=", "&&", "||", "!~", "~*"];
80
+ const SINGLE_OPS = new Set(["<", ">", "!", "+", "-", "*", "/", "%", "~"]);
62
81
 
63
82
  const isIdentStart = (c: string): boolean => /^[A-Za-z_$]$/.test(c);
64
83
  const isIdentPart = (c: string): boolean => /^[A-Za-z0-9_$]$/.test(c);
@@ -78,7 +97,16 @@ export function lexTemplate(fragments: readonly string[], values: number): Token
78
97
  const tokens: Token[] = [];
79
98
  let base = 0; // running offset across fragments + rendered `${…}` markers
80
99
  for (let f = 0; f < fragments.length; f++) {
81
- lexFragment(fragments[f]!, base, tokens);
100
+ try {
101
+ lexFragment(fragments[f]!, base, tokens);
102
+ } catch (err) {
103
+ // A fragment ends inside a backtick literal: the bound value would have
104
+ // to stand inside it, and a bound value is never source text (GRAMMAR §1).
105
+ if (err instanceof OqxError && f < fragments.length - 1 && err.message.startsWith("unterminated template literal")) {
106
+ throw new OqxError(`${err.message} — a template literal must be complete within one fragment: a \${…} binding cannot stand inside a backtick literal (interpolate a name, or bind the whole string)`, "lex");
107
+ }
108
+ throw err;
109
+ }
82
110
  base += codePointLength(fragments[f]!);
83
111
  if (f < fragments.length - 1) {
84
112
  // account for the value's rendered width in the display source (see rawSource)
@@ -103,6 +131,14 @@ export function lexString(src: string): Token[] {
103
131
  return tokens;
104
132
  }
105
133
 
134
+ // The lexer's mode inside a template literal (GRAMMAR §1, since 0.21): the text of
135
+ // the literal, or a `${…}` interpolation, which is ordinary source again — with a
136
+ // brace depth, so the `}` that closes the interpolation is told from a block's.
137
+ // Nesting is a stack: an interpolation may hold another template literal.
138
+ type Mode =
139
+ | { kind: "template"; start: number }
140
+ | { kind: "interp"; start: number; depth: number };
141
+
106
142
  function lexFragment(src: string, base: number, out: Token[]): void {
107
143
  // One entry per code point, so every index below is a code-point offset.
108
144
  const cps = Array.from(src);
@@ -113,16 +149,47 @@ function lexFragment(src: string, base: number, out: Token[]): void {
113
149
  const push = (type: TokType, value: string, start: number, end: number): void => {
114
150
  out.push({ type, value, pos: base + start, end: base + end });
115
151
  };
152
+ const modes: Mode[] = [];
116
153
 
117
154
  while (i < n) {
155
+ const mode = modes[modes.length - 1];
156
+
157
+ // Template text: everything up to the closing backtick or the `${` of an
158
+ // interpolation, decoded with the string escapes (`\<c>` is `<c>`, so `` \` ``
159
+ // and `\$` keep a backtick or a dollar literal). Newlines are kept verbatim.
160
+ if (mode?.kind === "template") {
161
+ const start = i;
162
+ let sval = "";
163
+ while (i < n) {
164
+ const c = cps[i]!;
165
+ if (c === "`" || (c === "$" && at(i + 1) === "{")) break;
166
+ if (c === "\\") { i++; sval += unescape(cps[i]); i++; continue; }
167
+ sval += c;
168
+ i++;
169
+ }
170
+ if (i > start) push("tmpl_text", sval, start, Math.min(i, n));
171
+ if (i >= n) break; // unterminated — reported below
172
+ if (cps[i] === "`") { push("tmpl_end", "`", i, i + 1); i++; modes.pop(); continue; }
173
+ push("tmpl_interp_start", "${", i, i + 2);
174
+ modes.push({ kind: "interp", start: i, depth: 0 });
175
+ i += 2;
176
+ continue;
177
+ }
178
+
118
179
  const c = cps[i]!;
119
180
 
120
181
  if (c === " " || c === "\t" || c === "\n" || c === "\r") { i++; continue; }
121
182
 
183
+ if (c === "`") { push("tmpl_start", c, i, i + 1); modes.push({ kind: "template", start: i }); i++; continue; }
122
184
  if (c === "(") { push("lparen", c, i, i + 1); i++; continue; }
123
185
  if (c === ")") { push("rparen", c, i, i + 1); i++; continue; }
124
- if (c === "{") { push("lbrace", c, i, i + 1); i++; continue; }
125
- if (c === "}") { push("rbrace", c, i, i + 1); i++; continue; }
186
+ if (c === "{") { if (mode) mode.depth++; push("lbrace", c, i, i + 1); i++; continue; }
187
+ if (c === "}") {
188
+ // Inside an interpolation a `}` at depth 0 closes it; deeper, it closes a block.
189
+ if (mode && mode.depth === 0) { push("tmpl_interp_end", c, i, i + 1); i++; modes.pop(); continue; }
190
+ if (mode) mode.depth--;
191
+ push("rbrace", c, i, i + 1); i++; continue;
192
+ }
126
193
  if (c === "[") { push("lbracket", c, i, i + 1); i++; continue; }
127
194
  if (c === "]") { push("rbracket", c, i, i + 1); i++; continue; }
128
195
  if (c === ",") { push("comma", c, i, i + 1); i++; continue; }
@@ -183,7 +250,9 @@ function lexFragment(src: string, base: number, out: Token[]): void {
183
250
  continue;
184
251
  }
185
252
 
186
- // operators (multi-char first)
253
+ // operators (multi-char first, longest first)
254
+ const three = c + at(i + 1) + at(i + 2);
255
+ if (TRIPLE_OPS.includes(three)) { push("op", three, i, i + 3); i += 3; continue; }
187
256
  const two = c + at(i + 1);
188
257
  if (MULTI_OPS.includes(two)) { push("op", two, i, i + 2); i += 2; continue; }
189
258
  if (SINGLE_OPS.has(c)) { push("op", c, i, i + 1); i++; continue; }
@@ -200,6 +269,13 @@ function lexFragment(src: string, base: number, out: Token[]): void {
200
269
 
201
270
  throw new OqxError(`unexpected character ${JSON.stringify(c)} at ${base + i}`, "lex");
202
271
  }
272
+
273
+ // The end of the source inside a template literal.
274
+ const open = modes[modes.length - 1];
275
+ if (open?.kind === "template") throw new OqxError(`unterminated template literal at ${base + open.start}`, "lex");
276
+ if (open?.kind === "interp") {
277
+ throw new OqxError(`unterminated \${…} interpolation in a template literal at ${base + open.start} — expected '}'`, "lex");
278
+ }
203
279
  }
204
280
 
205
281
  // Scan a number whose first digit is at `i`; return the index just past it.
@@ -16,6 +16,7 @@
16
16
  // value into an enclosing scope (§19), observable by later conjuncts.
17
17
 
18
18
  import type { Expr, Follow, OpNode, SelectItem, Subquery, Where } from "../ast.ts";
19
+ import { isRegexOp } from "../ast.ts";
19
20
  import type { RuleContext } from "./ir.ts";
20
21
 
21
22
  // ---- scope reads --------------------------------------------------------------
@@ -35,6 +36,7 @@ export function exprReadsScopeIn(e: Expr, at: number, upTo: number): boolean {
35
36
  case "binary": case "logical": case "in": return exprReadsScopeIn(e.left, at, upTo) || exprReadsScopeIn(e.right, at, upTo);
36
37
  case "range": return (e.lo !== null && exprReadsScopeIn(e.lo, at, upTo)) || (e.hi !== null && exprReadsScopeIn(e.hi, at, upTo));
37
38
  case "tuple": return e.items.some((x) => exprReadsScopeIn(x, at, upTo));
39
+ case "template": return e.parts.some((p) => typeof p !== "string" && exprReadsScopeIn(p, at, upTo));
38
40
  }
39
41
  }
40
42
 
@@ -98,9 +100,15 @@ export function exprRaiseFree(e: Expr, ctx: RuleContext): boolean {
98
100
  case "op": return opRaiseFree(e, ctx); // a value-position directive projects: never raise-free
99
101
  case "member": return exprRaiseFree(e.recv, ctx);
100
102
  case "unary": return exprRaiseFree(e.expr, ctx);
101
- case "binary": case "logical": case "in": return exprRaiseFree(e.left, ctx) && exprRaiseFree(e.right, ctx);
103
+ // A regex operator (0.22) is a `matches` call in disguise: the pattern may
104
+ // be invalid, so it is never raise-free.
105
+ case "binary": return !isRegexOp(e.op) && exprRaiseFree(e.left, ctx) && exprRaiseFree(e.right, ctx);
106
+ case "logical": case "in": return exprRaiseFree(e.left, ctx) && exprRaiseFree(e.right, ctx);
102
107
  case "range": return (e.lo === null || exprRaiseFree(e.lo, ctx)) && (e.hi === null || exprRaiseFree(e.hi, ctx));
103
108
  case "tuple": return e.items.every((x) => exprRaiseFree(x, ctx));
109
+ // A template literal raises on a list or record interpolation (§7b), which
110
+ // only the data decides: with an interpolation it is never raise-free.
111
+ case "template": return e.parts.every((p) => typeof p === "string");
104
112
  }
105
113
  }
106
114
 
package/src/parser.ts CHANGED
@@ -64,6 +64,11 @@
64
64
  // Tuples (0.20): `(e1, e2, …)` with two or more elements is a `tuple` node — a
65
65
  // value and a receiver; one expression in parentheses stays grouping.
66
66
  //
67
+ // Template literals (0.21): `` `text ${expr} text` `` is a `template` node — a
68
+ // primary value whose interpolations are full expressions read at the current
69
+ // depth (`parseTemplateLiteral`); the lexer balances the braces, so an
70
+ // interpolation may hold a `{ … }` block or another template literal.
71
+ //
67
72
  // Principle of least surprise, applied to the grammar: a rule a careful user
68
73
  // would not predict is a bug. Hence `!` binds tighter than comparison in every
69
74
  // position (`!a == b` is `(!a) == b`, as in C); parentheses in `where` group a
@@ -78,7 +83,7 @@ import { lexTemplate, lexString } from "./lexer.ts";
78
83
  import { OqxError } from "./errors.ts";
79
84
  import type {
80
85
  Query, Where, Expr, OpNode, Subquery, SelectItem, OrderSpec, Follow, FollowDestination, Consumer, RelOp,
81
- BinaryOp, Span,
86
+ BinaryOp, Span, TemplatePart,
82
87
  } from "./ast.ts";
83
88
  import { resolveSelect, resolveWhere } from "./resolve.ts";
84
89
  import { printTemplate } from "./print.ts";
@@ -104,7 +109,10 @@ const CLAUSE_WORDS = new Set<string>([
104
109
  "limit", "offset",
105
110
  ]);
106
111
  const RELOPS = new Set<string>(["==", "!=", "<", "<=", ">", ">="]);
107
- const CMP_OPS = new Set<string>(["==", "!=", "<", "<=", ">", ">="]);
112
+ // The comparison level (GRAMMAR §4): the six relational operators and, since
113
+ // 0.22, the four regex operators — sugar over `matches`, non-associative like
114
+ // the rest of the level.
115
+ const CMP_OPS = new Set<string>(["==", "!=", "<", "<=", ">", ">=", "~", "~*", "!~", "!~*"]);
108
116
  const ADD_OPS = new Set<string>(["+", "-"]);
109
117
  const MUL_OPS = new Set<string>(["*", "/", "%"]);
110
118
  // `true`/`false`/`null` are literals in every position, so they can never name a
@@ -194,7 +202,10 @@ class Parser {
194
202
  return t.type === "op" && t.value === value;
195
203
  }
196
204
  private fail(msg: string): never {
197
- throw new OqxError(`${msg} (at offset ${this.peek().pos})`, "parse");
205
+ this.failAt(this.peek().pos, msg);
206
+ }
207
+ private failAt(pos: number, msg: string): never {
208
+ throw new OqxError(`${msg} (at offset ${pos})`, "parse");
198
209
  }
199
210
  /** Code-point offset of the next token (the start of a node about to be parsed). */
200
211
  private start(): number { return this.peek().pos; }
@@ -1042,7 +1053,7 @@ class Parser {
1042
1053
  // `recv first { where p }`; a trailing `!` makes the index required and the
1043
1054
  // predicate a `single` (required too, so that zero matches is an error).
1044
1055
  private parseBracket(recv: Expr): Expr {
1045
- if (recv.kind === "lit") this.fail(`\`${describe(recv)}\` is a literal, not a collection`);
1056
+ if (recv.kind === "lit" || recv.kind === "template") this.fail(`\`${describe(recv)}\` is a literal, not a collection`);
1046
1057
  const start = recv.span[0];
1047
1058
  const lbracket = this.next().pos; // '['
1048
1059
  let index: Expr | null = null;
@@ -1170,7 +1181,7 @@ class Parser {
1170
1181
  if (!t) return false;
1171
1182
  if (t.type === "ident") return !CLAUSE_WORDS.has(t.value);
1172
1183
  if (t.type === "kw") return t.value === "is" || t.value === "not"; // the prefix operators
1173
- if (t.type === "number" || t.type === "string" ||
1184
+ if (t.type === "number" || t.type === "string" || t.type === "tmpl_start" ||
1174
1185
  t.type === "binding" || t.type === "lparen" || t.type === "caret") return true;
1175
1186
  return t.type === "op" && (t.value === "-" || t.value === "!");
1176
1187
  }
@@ -1242,7 +1253,7 @@ class Parser {
1242
1253
  // { … }` (0.20: the receiver-block sugar stands wherever a value does,
1243
1254
  // `size(children { is checked })`). exists/none/count have no value form
1244
1255
  // and are left to the body's own error.
1245
- if (expr.kind === "lit") this.fail(`\`${describe(expr)}\` is a literal, not a collection`);
1256
+ if (expr.kind === "lit" || expr.kind === "template") this.fail(`\`${describe(expr)}\` is a literal, not a collection`);
1246
1257
  expr = this.parseConsumerBlock(expr, this.atBlock()!, false);
1247
1258
  } else {
1248
1259
  break; // anything else ends the chain
@@ -1282,6 +1293,7 @@ class Parser {
1282
1293
  }
1283
1294
  if (t.type === "number") { this.next(); return { kind: "lit", span: [t.pos, t.end], value: Number(t.value) }; }
1284
1295
  if (t.type === "string") { this.next(); return { kind: "lit", span: [t.pos, t.end], value: t.value }; }
1296
+ if (t.type === "tmpl_start") return this.parseTemplateLiteral();
1285
1297
  if (t.type === "binding") { this.next(); return this.bindingNode(t); }
1286
1298
  if (t.type === "lparen") {
1287
1299
  this.next();
@@ -1308,6 +1320,39 @@ class Parser {
1308
1320
  }
1309
1321
  this.fail(`unexpected ${this.tokDesc()} — expected a value`);
1310
1322
  }
1323
+
1324
+ // A template literal (GRAMMAR §4, since 0.21): the cursor is at the opening
1325
+ // backtick. The lexer has already split the literal into text runs and `${ … }`
1326
+ // interpolations (with balanced braces, so an interpolation may hold a block
1327
+ // or another template literal); each interpolation holds one full value
1328
+ // expression, read at the current scope depth. `parts` alternates strictly —
1329
+ // text, expression, text, … — with a (possibly empty) text part first and last.
1330
+ private parseTemplateLiteral(): Expr {
1331
+ const open = this.next(); // the backtick
1332
+ const parts: TemplatePart[] = [];
1333
+ let text = "";
1334
+ for (;;) {
1335
+ const t = this.peek();
1336
+ if (t.type === "tmpl_text") { this.next(); text += t.value; continue; }
1337
+ if (t.type === "tmpl_end") { this.next(); break; }
1338
+ if (t.type === "tmpl_interp_start") {
1339
+ this.next();
1340
+ if (this.at("tmpl_interp_end")) {
1341
+ this.failAt(t.pos, "empty interpolation `${}` in a template literal — an interpolation holds an expression (`${name}`)");
1342
+ }
1343
+ const e = this.parseValueExpr();
1344
+ if (!this.at("tmpl_interp_end")) this.fail("expected '}' to close the `${…}` interpolation of a template literal");
1345
+ this.next();
1346
+ parts.push(text, e);
1347
+ text = "";
1348
+ continue;
1349
+ }
1350
+ // Unreachable: the lexer closes every literal it opens or fails.
1351
+ this.fail(`unexpected ${this.tokDesc()} in a template literal`);
1352
+ }
1353
+ parts.push(text);
1354
+ return { kind: "template", span: [open.pos, this.end()], parts };
1355
+ }
1311
1356
  }
1312
1357
 
1313
1358
  // The default key of an unaliased projection item: the last segment of a bare /
package/src/print.ts CHANGED
@@ -66,9 +66,10 @@ const OR = 1, AND = 2, CMP = 3, RANGE = 4, ADD = 5, MUL = 6, UNARY = 7, POSTFIX
66
66
 
67
67
  function prec(e: Expr): number {
68
68
  switch (e.kind) {
69
- case "lit": case "ident": case "outer": case "binding": case "tuple": return PRIMARY;
69
+ case "lit": case "ident": case "outer": case "binding": case "tuple": case "template": return PRIMARY;
70
70
  case "member": case "call": case "required": case "op": return POSTFIX;
71
71
  case "unary": return UNARY;
72
+ // Comparison, identity and the regex operators (0.22) share the cmp level.
72
73
  case "binary": return e.op === "+" || e.op === "-" ? ADD : e.op === "*" || e.op === "/" || e.op === "%" ? MUL : CMP;
73
74
  case "in": return CMP;
74
75
  case "range": return RANGE;
@@ -231,6 +232,10 @@ function expr(e: Expr): Out {
231
232
  return cat(e.lo ? operand(e.lo, ADD) : "", e.exclusiveEnd ? "..." : "..", e.hi ? operand(e.hi, ADD) : "");
232
233
  // A tuple is self-delimiting; its elements are full expressions.
233
234
  case "tuple": return cat("(", join(e.items.map(expr), ", "), ")");
235
+ // A template literal prints as one: its text with the GRAMMAR §1 escapes
236
+ // (`` \` ``, `\\`, `\$` before a `{`, and the control characters), each
237
+ // interpolation as `${…}` around the full expression.
238
+ case "template": return cat("`", ...e.parts.map((p) => (typeof p === "string" ? templateText(p) : cat("${", expr(p), "}"))), "`");
234
239
  }
235
240
  }
236
241
 
@@ -264,13 +269,35 @@ function quote(s: string): string {
264
269
  for (const ch of s) {
265
270
  switch (ch) {
266
271
  case '"': out += '\\"'; break;
267
- case "\\": out += "\\\\"; break;
268
- case "\n": out += "\\n"; break;
269
- case "\t": out += "\\t"; break;
270
- case "\r": out += "\\r"; break;
271
- case "\0": out += "\\0"; break;
272
- default: out += ch;
272
+ default: out += escapeChar(ch);
273
273
  }
274
274
  }
275
275
  return out + '"';
276
276
  }
277
+
278
+ /** The text of a template literal part, escaped so it re-lexes to itself: the
279
+ * backtick and the backslash, the control characters as in a string, and a `$`
280
+ * that a `{` follows (which would otherwise open an interpolation). */
281
+ function templateText(s: string): string {
282
+ const cps = Array.from(s);
283
+ let out = "";
284
+ for (let i = 0; i < cps.length; i++) {
285
+ const ch = cps[i]!;
286
+ if (ch === "`") out += "\\`";
287
+ else if (ch === "$" && cps[i + 1] === "{") out += "\\$";
288
+ else out += escapeChar(ch);
289
+ }
290
+ return out;
291
+ }
292
+
293
+ /** The escapes a string literal and a template literal share (GRAMMAR §1). */
294
+ function escapeChar(ch: string): string {
295
+ switch (ch) {
296
+ case "\\": return "\\\\";
297
+ case "\n": return "\\n";
298
+ case "\t": return "\\t";
299
+ case "\r": return "\\r";
300
+ case "\0": return "\\0";
301
+ default: return ch;
302
+ }
303
+ }
@@ -0,0 +1,238 @@
1
+ // The `references` static pass (spec/oqx/AST.md §5b, language 0.23): every
2
+ // NAME a query reads from a row — a bare identifier, a dotted navigation headed
3
+ // by one, an outer reference — with the clause it stands in and the SCOPE whose
4
+ // rows it is read from, described as the chain of expressions that produce
5
+ // those rows (the source, each `from` re-projection, each block receiver, each
6
+ // `follow` walk). The pass knows no data: a host that knows what rows an
7
+ // expression yields maps each scope to a row type and judges the name against
8
+ // it (omgbase: "is `target` a field of links?"). Built on `visit`, so a future
9
+ // node kind is walked here the day it is added to `CHILDREN`.
10
+ //
11
+ // What is a reference and what is not (the rules, pinned by
12
+ // `cases/references.json`):
13
+ // • a bare `ident` is a reference to the current row's property, with the
14
+ // name as written; a `member` chain headed by an `ident` or `outer` is ONE
15
+ // reference whose name is the full dotted path (`doc.layer`, `meta.owner`);
16
+ // a chain headed by anything else (a call, a block, a literal, a tuple) is
17
+ // not — its head is walked and the trailing `.name`s are ignored, since the
18
+ // head's value is not a row of any scope;
19
+ // • an `outer` (`^name`, `N^name`) is a reference read in the scope `levels`
20
+ // out; `$it` as a head is the row itself (`$it.title` ≡ `title`,
21
+ // `^$it.title` ≡ `^title`) and `$it` alone is no reference;
22
+ // • `$`-names (`$key`, `$depth`, `$stop`, …) are the language's values, never
23
+ // references; a `binding` is a host value; a function or method NAME is
24
+ // not a reference, its receiver and arguments are walked
25
+ // (`target.contains("x")` references `target`); a name a nested block
26
+ // LIFTS onto a scope (`^open: value`, SEMANTICS §19) is a binding of that
27
+ // scope, so reading it there (`select open`, `^open` from deeper) is not a
28
+ // reference either;
29
+ // • a `select` alias is not a reference where the language resolves it
30
+ // (SEMANTICS §14): in the same body's `where`, and in the items to its
31
+ // right — a bare identifier naming such an alias, or a chain headed by it,
32
+ // is skipped; an alias's own name inside its own item is the row field and
33
+ // IS a reference; `order by`, `frontier`, `limit`/`offset`, `from` and
34
+ // `follow` are not rewritten against aliases, so a name there is a
35
+ // reference whatever the `select` says;
36
+ // • a name read at the ROOT scope — the head of the top-level `source`
37
+ // (`from people`: clause `source`), an outer reference with as many carets
38
+ // as there are enclosing scopes (`^docs` from a top-level row) — is
39
+ // reported with depth 0 and no steps: it names one of the host's roots,
40
+ // and the host decides what to make of it;
41
+ // • a `follow` destination that is a plain expression is a reference read
42
+ // on the frontier row (clause `follow.destination`), like a `from`.
43
+ //
44
+ // The scope: `{ depth, steps }`. `depth` is the scope depth the name is
45
+ // evaluated at (`visit`'s depth; for an outer reference, that depth minus its
46
+ // levels). `steps` are the expressions that produce the scope's rows, root
47
+ // first, each with the depth of the rows it produces: the query's `source`
48
+ // (depth 1), each `from` re-projection in turn (the same depth — a `from`
49
+ // replaces the rows), a block's receiver (one deeper than where it is read),
50
+ // then the block's own `from`s; a `follow` node is a step too — the walk's
51
+ // frontier rows, at the body's depth, which the destinations are read on
52
+ // (a destination block's receiver is then one deeper). A name read inside the
53
+ // k-th `from` sees only the steps before it. For an outer reference the steps
54
+ // are cut after the last step at its scope's depth. The empty list is the root
55
+ // scope.
56
+
57
+ import type { AstNode, Expr, Follow, Query, SelectItem, Span, Subquery } from "./ast.ts";
58
+ import type { Clause } from "./walk.ts";
59
+ import { visit } from "./walk.ts";
60
+
61
+ /** One step of a scope's row chain: an expression (a source, a `from`, a block
62
+ * receiver, a plain destination) or a `follow` node (the walk's frontier rows),
63
+ * with the scope depth of the rows it produces. */
64
+ export interface ReferenceStep {
65
+ readonly expr: Expr | Follow;
66
+ readonly depth: number;
67
+ }
68
+
69
+ export interface ReferenceScope {
70
+ /** The scope depth the name is read at (SEMANTICS §2; 0 = the root scope). */
71
+ readonly depth: number;
72
+ /** The row chain producing the scope's rows, root first. */
73
+ readonly steps: readonly ReferenceStep[];
74
+ }
75
+
76
+ export interface Reference {
77
+ /** The name as written — the full dotted path for a navigation (`doc.layer`), without a `$it.` head. */
78
+ readonly name: string;
79
+ readonly clause: Clause;
80
+ readonly scope: ReferenceScope;
81
+ /** The span of the whole reference (the chain, for a navigation). */
82
+ readonly span: Span;
83
+ }
84
+
85
+ /** A dotted name headed by an `ident` or `outer`: the head and the path after it. */
86
+ function dottedName(e: Expr): { head: Expr & { kind: "ident" | "outer" }; path: string[] } | null {
87
+ if (e.kind === "ident" || e.kind === "outer") return { head: e, path: [] };
88
+ if (e.kind === "member") {
89
+ const inner = dottedName(e.recv);
90
+ return inner === null ? null : { head: inner.head, path: [...inner.path, e.name] };
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /** The alias an item defines (SEMANTICS §14): a named `collect`, or a named non-lift field. */
96
+ function aliasName(it: SelectItem): string | null {
97
+ if (it.kind === "collect") return it.name === "" ? null : it.name;
98
+ return it.lift === 0 && it.name !== "" ? it.name : null;
99
+ }
100
+
101
+ /** Every name a query (or block body) reads from a row, in canonical source order. */
102
+ export function references(root: Query | Subquery): Reference[] {
103
+ const out: Reference[] = [];
104
+ const lifts = new Map<Query | Subquery, ReadonlySet<string>>();
105
+ visit(root, {
106
+ enter(node, ctx) {
107
+ const named = dottedName(node as Expr);
108
+ if (named === null) return;
109
+ const { head } = named;
110
+ // The head of a navigation is handled here, once, with the chain's full name.
111
+ let name = head.name;
112
+ let path = named.path;
113
+ if (name === "$it") {
114
+ if (path.length === 0) return false; // the row itself
115
+ name = path[0]!;
116
+ path = path.slice(1);
117
+ } else if (name.startsWith("$")) return false; // a language value
118
+ if (ctx.clause === null) return false; // unreachable: the root node is never a name
119
+ if (head.kind === "ident" && isResolvedAlias(name, ctx.path, ctx.clause)) return false;
120
+ const levels = head.kind === "outer" ? head.levels : 0;
121
+ const depth = ctx.depth - levels;
122
+ if (isLiftBound(name, depth, ctx.path, lifts)) return false;
123
+ const full = [name, ...path].join(".");
124
+ const steps = scopeSteps(ctx.path, node);
125
+ const scoped = levels === 0 ? steps : cutAt(steps, depth);
126
+ out.push({ name: full, clause: ctx.clause, scope: { depth, steps: scoped }, span: node.span });
127
+ return false;
128
+ },
129
+ });
130
+ return out;
131
+ }
132
+
133
+ /** Whether a bare `name` in this position is a `select` alias the language
134
+ * substitutes (so the name is the alias, not a row property): in the body's
135
+ * `where`, any alias; in a `select` item, an alias defined to its LEFT. */
136
+ function isResolvedAlias(name: string, path: readonly AstNode[], clause: Clause): boolean {
137
+ if (clause !== "where" && clause !== "select") return false;
138
+ // The nearest body, and the item under it when we are inside its select.
139
+ for (let i = path.length - 1; i >= 0; i--) {
140
+ const a = path[i]!;
141
+ if (a.kind !== "query" && a.kind !== "subquery") continue;
142
+ const index = new Map<string, number>();
143
+ a.select.forEach((it, j) => { const n = aliasName(it); if (n !== null) index.set(n, j); });
144
+ const j = index.get(name);
145
+ if (j === undefined) return false;
146
+ if (clause === "where") return true;
147
+ const item = path[i + 1];
148
+ const at = item === undefined ? -1 : a.select.indexOf(item as SelectItem);
149
+ return at >= 0 && j < at;
150
+ }
151
+ return false;
152
+ }
153
+
154
+ /**
155
+ * Whether `name`, read at scope `depth`, is bound there by a lift (`^name: expr`
156
+ * in a nested block, SEMANTICS §19): the lifted value is appended to that
157
+ * scope's row record, so the name is a binding, not a row property. The body
158
+ * whose rows are at `depth` is the depth-th body on the path (every nested body
159
+ * is one scope deeper than the one around it).
160
+ */
161
+ function isLiftBound(name: string, depth: number, path: readonly AstNode[], cache: Map<Query | Subquery, ReadonlySet<string>>): boolean {
162
+ if (depth < 1) return false;
163
+ const bodies = path.filter((a): a is Query | Subquery => a.kind === "query" || a.kind === "subquery");
164
+ const body = bodies[depth - 1];
165
+ if (body === undefined) return false;
166
+ let names = cache.get(body);
167
+ if (names === undefined) {
168
+ names = liftsLandingOn(body);
169
+ cache.set(body, names);
170
+ }
171
+ return names.has(name);
172
+ }
173
+
174
+ /** The names the nested blocks of `body` lift onto it: a `field` item with `lift` L in a block L scopes below it. */
175
+ function liftsLandingOn(body: Query | Subquery): ReadonlySet<string> {
176
+ const names = new Set<string>();
177
+ const own = body.kind === "subquery" ? 1 : 0;
178
+ visit(body, {
179
+ enter(node, ctx) {
180
+ if (node.kind !== "field" || node.lift === 0) return;
181
+ const below = ctx.path.filter((a) => a.kind === "subquery").length - own;
182
+ if (node.lift === below) names.add(node.name);
183
+ },
184
+ });
185
+ return names;
186
+ }
187
+
188
+ /** The row chain of the scope a node is evaluated in, from its ancestors. */
189
+ function scopeSteps(path: readonly AstNode[], node: AstNode): ReferenceStep[] {
190
+ const steps: ReferenceStep[] = [];
191
+ let depth = 0;
192
+ for (let i = 0; i < path.length; i++) {
193
+ const a = path[i]!;
194
+ const next: AstNode = path[i + 1] ?? node;
195
+ switch (a.kind) {
196
+ case "query": {
197
+ if (next === a.source || next === a.limit || next === a.offset) break; // read at the root scope
198
+ depth = 1;
199
+ steps.push({ expr: a.source, depth });
200
+ pushFroms(steps, a.from, next, depth);
201
+ break;
202
+ }
203
+ case "subquery": {
204
+ // The receiver step was pushed by the enclosing `op`; this body's rows
205
+ // are one deeper than where that receiver was read.
206
+ pushFroms(steps, a.from, next, depth);
207
+ break;
208
+ }
209
+ case "op": {
210
+ if (next !== a.sub) break; // inside the receiver: read where the block is read
211
+ depth += 1;
212
+ steps.push({ expr: a.receiver, depth });
213
+ break;
214
+ }
215
+ case "follow": {
216
+ // The destinations are read on the walk's frontier rows, at the body's depth.
217
+ steps.push({ expr: a, depth });
218
+ break;
219
+ }
220
+ default: break;
221
+ }
222
+ }
223
+ return steps;
224
+ }
225
+
226
+ /** The `from` re-projections that apply at `next`: all of them unless `next` is one of them, in which case those before it. */
227
+ function pushFroms(steps: ReferenceStep[], from: readonly Expr[], next: AstNode, depth: number): void {
228
+ const at = from.indexOf(next as Expr);
229
+ const upto = at === -1 ? from.length : at;
230
+ for (let k = 0; k < upto; k++) steps.push({ expr: from[k]!, depth });
231
+ }
232
+
233
+ /** The steps up to and including the last one producing rows at `depth` (none for the root scope). */
234
+ function cutAt(steps: readonly ReferenceStep[], depth: number): ReferenceStep[] {
235
+ let end = 0;
236
+ steps.forEach((s, i) => { if (s.depth === depth) end = i + 1; });
237
+ return steps.slice(0, end);
238
+ }
package/src/resolve.ts CHANGED
@@ -141,6 +141,10 @@ function mapExpr(e: Expr, f: (e: Expr) => Expr | null): Expr {
141
141
  const items = mapSame(e.items, go);
142
142
  return items === e.items ? e : { ...e, items };
143
143
  }
144
+ case "template": { // the text parts are strings; only the interpolations are expressions
145
+ const parts = mapSame(e.parts, (p) => (typeof p === "string" ? p : go(p)));
146
+ return parts === e.parts ? e : { ...e, parts };
147
+ }
144
148
  case "op": { // the receiver is read in this scope; the block is its own scope
145
149
  const receiver = go(e.receiver);
146
150
  return receiver === e.receiver ? e : { ...e, receiver };
package/src/version.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** The OQX language version (`spec/oqx/VERSION`, `major.minor`) this implementation
2
2
  * conforms to — distinct from the package's own semver, whose `major.minor` it
3
3
  * equals. Stamped on every `toJSON` document. */
4
- export const LANGUAGE_VERSION = "0.20";
4
+ export const LANGUAGE_VERSION = "0.23";