@omgbase/oqx 0.18.0 → 0.21.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 (57) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +177 -64
  3. package/dist/ast.d.ts +28 -5
  4. package/dist/ast.d.ts.map +1 -1
  5. package/dist/ast.js +1 -1
  6. package/dist/ast.js.map +1 -1
  7. package/dist/build.d.ts +11 -2
  8. package/dist/build.d.ts.map +1 -1
  9. package/dist/build.js +10 -3
  10. package/dist/build.js.map +1 -1
  11. package/dist/context.d.ts +19 -5
  12. package/dist/context.d.ts.map +1 -1
  13. package/dist/context.js +6 -9
  14. package/dist/context.js.map +1 -1
  15. package/dist/engine.d.ts +12 -1
  16. package/dist/engine.d.ts.map +1 -1
  17. package/dist/engine.js +263 -84
  18. package/dist/engine.js.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/lexer.d.ts +3 -3
  23. package/dist/lexer.d.ts.map +1 -1
  24. package/dist/lexer.js +79 -2
  25. package/dist/lexer.js.map +1 -1
  26. package/dist/optimize/analysis.d.ts +3 -2
  27. package/dist/optimize/analysis.d.ts.map +1 -1
  28. package/dist/optimize/analysis.js +18 -15
  29. package/dist/optimize/analysis.js.map +1 -1
  30. package/dist/parser.d.ts.map +1 -1
  31. package/dist/parser.js +250 -118
  32. package/dist/parser.js.map +1 -1
  33. package/dist/print.d.ts.map +1 -1
  34. package/dist/print.js +47 -29
  35. package/dist/print.js.map +1 -1
  36. package/dist/resolve.d.ts.map +1 -1
  37. package/dist/resolve.js +15 -8
  38. package/dist/resolve.js.map +1 -1
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/dist/walk.d.ts +5 -3
  42. package/dist/walk.d.ts.map +1 -1
  43. package/dist/walk.js +34 -11
  44. package/dist/walk.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/ast.ts +33 -6
  47. package/src/build.ts +12 -8
  48. package/src/context.ts +22 -11
  49. package/src/engine.ts +258 -91
  50. package/src/index.ts +2 -1
  51. package/src/lexer.ts +76 -6
  52. package/src/optimize/analysis.ts +17 -12
  53. package/src/parser.ts +227 -101
  54. package/src/print.ts +44 -16
  55. package/src/resolve.ts +15 -7
  56. package/src/version.ts +1 -1
  57. package/src/walk.ts +39 -14
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
@@ -39,13 +46,21 @@ export type TokType =
39
46
  | "range" // `..` (inclusive) or `...` (exclusive end) — a Ruby-style range operator (value carries which)
40
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;
@@ -78,7 +93,16 @@ export function lexTemplate(fragments: readonly string[], values: number): Token
78
93
  const tokens: Token[] = [];
79
94
  let base = 0; // running offset across fragments + rendered `${…}` markers
80
95
  for (let f = 0; f < fragments.length; f++) {
81
- lexFragment(fragments[f]!, base, tokens);
96
+ try {
97
+ lexFragment(fragments[f]!, base, tokens);
98
+ } catch (err) {
99
+ // A fragment ends inside a backtick literal: the bound value would have
100
+ // to stand inside it, and a bound value is never source text (GRAMMAR §1).
101
+ if (err instanceof OqxError && f < fragments.length - 1 && err.message.startsWith("unterminated template literal")) {
102
+ 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");
103
+ }
104
+ throw err;
105
+ }
82
106
  base += codePointLength(fragments[f]!);
83
107
  if (f < fragments.length - 1) {
84
108
  // account for the value's rendered width in the display source (see rawSource)
@@ -103,6 +127,14 @@ export function lexString(src: string): Token[] {
103
127
  return tokens;
104
128
  }
105
129
 
130
+ // The lexer's mode inside a template literal (GRAMMAR §1, since 0.21): the text of
131
+ // the literal, or a `${…}` interpolation, which is ordinary source again — with a
132
+ // brace depth, so the `}` that closes the interpolation is told from a block's.
133
+ // Nesting is a stack: an interpolation may hold another template literal.
134
+ type Mode =
135
+ | { kind: "template"; start: number }
136
+ | { kind: "interp"; start: number; depth: number };
137
+
106
138
  function lexFragment(src: string, base: number, out: Token[]): void {
107
139
  // One entry per code point, so every index below is a code-point offset.
108
140
  const cps = Array.from(src);
@@ -113,16 +145,47 @@ function lexFragment(src: string, base: number, out: Token[]): void {
113
145
  const push = (type: TokType, value: string, start: number, end: number): void => {
114
146
  out.push({ type, value, pos: base + start, end: base + end });
115
147
  };
148
+ const modes: Mode[] = [];
116
149
 
117
150
  while (i < n) {
151
+ const mode = modes[modes.length - 1];
152
+
153
+ // Template text: everything up to the closing backtick or the `${` of an
154
+ // interpolation, decoded with the string escapes (`\<c>` is `<c>`, so `` \` ``
155
+ // and `\$` keep a backtick or a dollar literal). Newlines are kept verbatim.
156
+ if (mode?.kind === "template") {
157
+ const start = i;
158
+ let sval = "";
159
+ while (i < n) {
160
+ const c = cps[i]!;
161
+ if (c === "`" || (c === "$" && at(i + 1) === "{")) break;
162
+ if (c === "\\") { i++; sval += unescape(cps[i]); i++; continue; }
163
+ sval += c;
164
+ i++;
165
+ }
166
+ if (i > start) push("tmpl_text", sval, start, Math.min(i, n));
167
+ if (i >= n) break; // unterminated — reported below
168
+ if (cps[i] === "`") { push("tmpl_end", "`", i, i + 1); i++; modes.pop(); continue; }
169
+ push("tmpl_interp_start", "${", i, i + 2);
170
+ modes.push({ kind: "interp", start: i, depth: 0 });
171
+ i += 2;
172
+ continue;
173
+ }
174
+
118
175
  const c = cps[i]!;
119
176
 
120
177
  if (c === " " || c === "\t" || c === "\n" || c === "\r") { i++; continue; }
121
178
 
179
+ if (c === "`") { push("tmpl_start", c, i, i + 1); modes.push({ kind: "template", start: i }); i++; continue; }
122
180
  if (c === "(") { push("lparen", c, i, i + 1); i++; continue; }
123
181
  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; }
182
+ if (c === "{") { if (mode) mode.depth++; push("lbrace", c, i, i + 1); i++; continue; }
183
+ if (c === "}") {
184
+ // Inside an interpolation a `}` at depth 0 closes it; deeper, it closes a block.
185
+ if (mode && mode.depth === 0) { push("tmpl_interp_end", c, i, i + 1); i++; modes.pop(); continue; }
186
+ if (mode) mode.depth--;
187
+ push("rbrace", c, i, i + 1); i++; continue;
188
+ }
126
189
  if (c === "[") { push("lbracket", c, i, i + 1); i++; continue; }
127
190
  if (c === "]") { push("rbracket", c, i, i + 1); i++; continue; }
128
191
  if (c === ",") { push("comma", c, i, i + 1); i++; continue; }
@@ -200,6 +263,13 @@ function lexFragment(src: string, base: number, out: Token[]): void {
200
263
 
201
264
  throw new OqxError(`unexpected character ${JSON.stringify(c)} at ${base + i}`, "lex");
202
265
  }
266
+
267
+ // The end of the source inside a template literal.
268
+ const open = modes[modes.length - 1];
269
+ if (open?.kind === "template") throw new OqxError(`unterminated template literal at ${base + open.start}`, "lex");
270
+ if (open?.kind === "interp") {
271
+ throw new OqxError(`unterminated \${…} interpolation in a template literal at ${base + open.start} — expected '}'`, "lex");
272
+ }
203
273
  }
204
274
 
205
275
  // Scan a number whose first digit is at `i`; return the index just past it.
@@ -34,6 +34,8 @@ export function exprReadsScopeIn(e: Expr, at: number, upTo: number): boolean {
34
34
  case "op": return opReadsScopeIn(e, at, upTo);
35
35
  case "binary": case "logical": case "in": return exprReadsScopeIn(e.left, at, upTo) || exprReadsScopeIn(e.right, at, upTo);
36
36
  case "range": return (e.lo !== null && exprReadsScopeIn(e.lo, at, upTo)) || (e.hi !== null && exprReadsScopeIn(e.hi, at, upTo));
37
+ case "tuple": return e.items.some((x) => exprReadsScopeIn(x, at, upTo));
38
+ case "template": return e.parts.some((p) => typeof p !== "string" && exprReadsScopeIn(p, at, upTo));
37
39
  }
38
40
  }
39
41
 
@@ -47,22 +49,26 @@ export function whereReadsScopeIn(w: Where, at: number, upTo: number): boolean {
47
49
  }
48
50
 
49
51
  /** A directive evaluated in a scope at depth `at`: its receiver is read there;
50
- * its rows are scopes at `at + 1`. */
51
- export function opReadsScopeIn(op: OpNode, at: number, upTo: number): boolean {
52
- return exprReadsScopeIn(op.receiver, at, upTo) || subReadsScopeIn(op.sub, at + 1, upTo);
52
+ * its rows are scopes at `at + 1`. `destination` marks a follow destination
53
+ * block (its `frontier` is read one level out). */
54
+ export function opReadsScopeIn(op: OpNode, at: number, upTo: number, destination = false): boolean {
55
+ return exprReadsScopeIn(op.receiver, at, upTo) || subReadsScopeIn(op.sub, at + 1, upTo, destination);
53
56
  }
54
57
 
55
58
  /** A block body whose rows are scopes at depth `at`. `from` re-projections,
56
59
  * `where`, `select`, `order by` and the bound are read in the row scope (the
57
- * bound in a row-less scope at the same depth); `follow` walks occurrences at
58
- * `at`, with its successor `where` one deeper (§20). */
59
- function subReadsScopeIn(sub: Subquery, at: number, upTo: number): boolean {
60
+ * bound in a row-less scope at the same depth); a destination block's
61
+ * `frontier` is read one level out, under the walk's enclosing scope (§20),
62
+ * any other block's `frontier` (its seeds') in the row scope like `where`;
63
+ * `follow` walks occurrences at `at`. */
64
+ function subReadsScopeIn(sub: Subquery, at: number, upTo: number, destination: boolean): boolean {
60
65
  if (sub.from.some((e) => exprReadsScopeIn(e, at, upTo))) return true;
61
66
  if (sub.where && whereReadsScopeIn(sub.where, at, upTo)) return true;
62
67
  if (sub.select.some((item) => itemReadsScopeIn(item, at, upTo))) return true;
63
68
  if (sub.orderBy?.some((o) => exprReadsScopeIn(o.expr, at, upTo))) return true;
64
69
  if (sub.limit && exprReadsScopeIn(sub.limit, at, upTo)) return true;
65
70
  if (sub.offset && exprReadsScopeIn(sub.offset, at, upTo)) return true;
71
+ if (sub.frontier && exprReadsScopeIn(sub.frontier, destination ? at - 1 : at, upTo)) return true;
66
72
  return sub.follow !== null && followReadsScopeIn(sub.follow, at, upTo);
67
73
  }
68
74
 
@@ -71,12 +77,7 @@ function itemReadsScopeIn(item: SelectItem, at: number, upTo: number): boolean {
71
77
  }
72
78
 
73
79
  function followReadsScopeIn(f: Follow, at: number, upTo: number): boolean {
74
- for (const d of f.destinations) {
75
- if (d.kind === "op" ? opReadsScopeIn(d, at, upTo) : exprReadsScopeIn(d, at, upTo)) return true;
76
- }
77
- if (f.where && exprReadsScopeIn(f.where, at + 1, upTo)) return true;
78
- if (f.frontier && exprReadsScopeIn(f.frontier, at, upTo)) return true;
79
- return f.by !== null && exprReadsScopeIn(f.by, at, upTo);
80
+ return f.destinations.some((d) => (d.kind === "op" ? opReadsScopeIn(d, at, upTo, true) : exprReadsScopeIn(d, at, upTo)));
80
81
  }
81
82
 
82
83
  /** Does `e` read the scope it is evaluated in (a bare identifier anywhere)? */
@@ -100,6 +101,10 @@ export function exprRaiseFree(e: Expr, ctx: RuleContext): boolean {
100
101
  case "unary": return exprRaiseFree(e.expr, ctx);
101
102
  case "binary": case "logical": case "in": return exprRaiseFree(e.left, ctx) && exprRaiseFree(e.right, ctx);
102
103
  case "range": return (e.lo === null || exprRaiseFree(e.lo, ctx)) && (e.hi === null || exprRaiseFree(e.hi, ctx));
104
+ case "tuple": return e.items.every((x) => exprRaiseFree(x, ctx));
105
+ // A template literal raises on a list or record interpolation (§7b), which
106
+ // only the data decides: with an interpolation it is never raise-free.
107
+ case "template": return e.parts.every((p) => typeof p === "string");
103
108
  }
104
109
  }
105
110