@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.
- package/CHANGELOG.md +30 -0
- package/README.md +177 -64
- package/dist/ast.d.ts +28 -5
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +1 -1
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +11 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +10 -3
- package/dist/build.js.map +1 -1
- package/dist/context.d.ts +19 -5
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +6 -9
- package/dist/context.js.map +1 -1
- package/dist/engine.d.ts +12 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +263 -84
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts +3 -3
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +79 -2
- package/dist/lexer.js.map +1 -1
- package/dist/optimize/analysis.d.ts +3 -2
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +18 -15
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +250 -118
- package/dist/parser.js.map +1 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +47 -29
- package/dist/print.js.map +1 -1
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +15 -8
- package/dist/resolve.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walk.d.ts +5 -3
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +34 -11
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +33 -6
- package/src/build.ts +12 -8
- package/src/context.ts +22 -11
- package/src/engine.ts +258 -91
- package/src/index.ts +2 -1
- package/src/lexer.ts +76 -6
- package/src/optimize/analysis.ts +17 -12
- package/src/parser.ts +227 -101
- package/src/print.ts +44 -16
- package/src/resolve.ts +15 -7
- package/src/version.ts +1 -1
- 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)
|
|
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
|
-
|
|
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 === "}") {
|
|
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.
|
package/src/optimize/analysis.ts
CHANGED
|
@@ -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
|
-
|
|
52
|
-
|
|
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);
|
|
58
|
-
* `
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|