@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.
- package/CHANGELOG.md +17 -0
- package/README.md +65 -0
- package/dist/ast.d.ts +20 -2
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +11 -1
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +4 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +3 -0
- package/dist/build.js.map +1 -1
- package/dist/engine.d.ts +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +45 -3
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -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 +92 -5
- package/dist/lexer.js.map +1 -1
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +8 -1
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +58 -5
- package/dist/parser.js.map +1 -1
- package/dist/print.js +36 -17
- package/dist/print.js.map +1 -1
- package/dist/references.d.ts +26 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +225 -0
- package/dist/references.js.map +1 -0
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +4 -0
- 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.map +1 -1
- package/dist/walk.js +8 -3
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +31 -3
- package/src/build.ts +4 -1
- package/src/engine.ts +40 -3
- package/src/index.ts +3 -1
- package/src/lexer.ts +86 -10
- package/src/optimize/analysis.ts +9 -1
- package/src/parser.ts +51 -6
- package/src/print.ts +34 -7
- package/src/references.ts +238 -0
- package/src/resolve.ts +4 -0
- package/src/version.ts +1 -1
- 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)
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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 === "}") {
|
|
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.
|
package/src/optimize/analysis.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
+
export const LANGUAGE_VERSION = "0.23";
|