@omgbase/oqx 0.15.0 → 0.20.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 +60 -0
- package/README.md +308 -70
- package/dist/ast.d.ts +97 -27
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +21 -6
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +43 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +58 -0
- package/dist/build.js.map +1 -0
- package/dist/context.d.ts +30 -6
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +11 -10
- package/dist/context.js.map +1 -1
- package/dist/engine.d.ts +13 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +281 -100
- package/dist/engine.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +19 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -13
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts +15 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +104 -51
- 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 +19 -18
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/optimize/ir.d.ts +2 -2
- package/dist/optimize/ir.d.ts.map +1 -1
- package/dist/optimize/ir.js +3 -3
- package/dist/optimize/ir.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +782 -335
- package/dist/parser.js.map +1 -1
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +4 -2
- package/dist/plan.js.map +1 -1
- package/dist/print.d.ts +17 -0
- package/dist/print.d.ts.map +1 -0
- package/dist/print.js +266 -0
- package/dist/print.js.map +1 -0
- package/dist/resolve.d.ts +23 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +333 -0
- package/dist/resolve.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/walk.d.ts +56 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +157 -0
- package/dist/walk.js.map +1 -0
- package/package.json +1 -1
- package/src/ast.ts +126 -40
- package/src/build.ts +73 -0
- package/src/context.ts +36 -12
- package/src/engine.ts +285 -110
- package/src/errors.ts +3 -2
- package/src/index.ts +40 -14
- package/src/lexer.ts +102 -51
- package/src/optimize/analysis.ts +17 -15
- package/src/optimize/ir.ts +4 -4
- package/src/parser.ts +721 -289
- package/src/plan.ts +3 -2
- package/src/print.ts +276 -0
- package/src/resolve.ts +309 -0
- package/src/version.ts +4 -0
- package/src/walk.ts +201 -0
package/src/errors.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// A single error type for every OQX failure — lex, parse,
|
|
1
|
+
// A single error type for every OQX failure — lex, parse, evaluation, and
|
|
2
|
+
// printing (a binding has no source text). `stage`
|
|
2
3
|
// distinguishes where it came from so callers (and tests) can branch without
|
|
3
4
|
// string-matching messages. Mirrors the reference impl's FilterInvalid, but the
|
|
4
5
|
// generic kernel has no SQL/CEL layer to name.
|
|
5
6
|
|
|
6
|
-
export type OqxStage = "lex" | "parse" | "eval";
|
|
7
|
+
export type OqxStage = "lex" | "parse" | "eval" | "print";
|
|
7
8
|
|
|
8
9
|
export class OqxError extends Error {
|
|
9
10
|
readonly stage: OqxStage;
|
package/src/index.ts
CHANGED
|
@@ -18,21 +18,26 @@
|
|
|
18
18
|
// with `PlannedEngine` finishing the residual in-memory.
|
|
19
19
|
// All backends must obey the scalar rules in `./semantics.ts` (the conformance
|
|
20
20
|
// suite verifies this).
|
|
21
|
+
//
|
|
22
|
+
// The parsed `Query` is a first-class AST (spec/oqx/AST.md): spans on every
|
|
23
|
+
// node, `visit`/`transform` to walk it, `print` to write it back, `toJSON` for
|
|
24
|
+
// the shared JSON shape. `where` keeps its surface form; the entry points below
|
|
25
|
+
// (`oqx`, `parse` + `run`, `execute`, `runQuery`) apply `resolveAliases` exactly
|
|
26
|
+
// once before evaluation — an `Engine` evaluates the query it is given.
|
|
21
27
|
|
|
22
28
|
import type { Query } from "./ast.ts";
|
|
23
29
|
import { parseTemplate, parseString } from "./parser.ts";
|
|
24
30
|
import type { Engine, OqxResult } from "./engine.ts";
|
|
25
|
-
import { InMemoryEngine
|
|
31
|
+
import { InMemoryEngine } from "./engine.ts";
|
|
26
32
|
import type { DataContext } from "./context.ts";
|
|
27
33
|
import { DefaultContext } from "./context.ts";
|
|
34
|
+
import { resolveAliases } from "./resolve.ts";
|
|
28
35
|
|
|
29
36
|
export { OqxError } from "./errors.ts";
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
* conforms to — distinct from the package's own semver. */
|
|
33
|
-
export const LANGUAGE_VERSION = "0.15";
|
|
37
|
+
export type { OqxStage } from "./errors.ts";
|
|
38
|
+
export { LANGUAGE_VERSION } from "./version.ts";
|
|
34
39
|
export type { OqxResult, Engine, InMemoryEngineOptions, TraceEvent } from "./engine.ts";
|
|
35
|
-
export { InMemoryEngine
|
|
40
|
+
export { InMemoryEngine } from "./engine.ts";
|
|
36
41
|
export type { BlockPlan, Correlation, Rule, RuleContext, RowIndex } from "./optimize/index.ts";
|
|
37
42
|
export {
|
|
38
43
|
DEFAULT_RULES, correlatedEqualityProbe, stableReceiver, invariantBlock, cardinalityOnly,
|
|
@@ -48,9 +53,19 @@ export { IndexedCollection } from "./adapters/indexed.ts";
|
|
|
48
53
|
export { ROWS_ROOT, partitionPushable, residualQuery, asEquality, isConst, constValue } from "./plan.ts";
|
|
49
54
|
export * as semantics from "./semantics.ts";
|
|
50
55
|
export type * from "./ast.ts";
|
|
56
|
+
export { isExpr } from "./ast.ts";
|
|
57
|
+
export { parseTemplate } from "./parser.ts";
|
|
58
|
+
export { rawSource, toUtf16, codePointLength } from "./lexer.ts";
|
|
59
|
+
export { resolveAliases } from "./resolve.ts";
|
|
60
|
+
export type { Clause, VisitContext, Visitor, ChildKey, Stripped } from "./walk.ts";
|
|
61
|
+
export { CHILDREN, visit, transform, stripSpans, toJSON } from "./walk.ts";
|
|
62
|
+
export type { Template } from "./print.ts";
|
|
63
|
+
export { print, printTemplate } from "./print.ts";
|
|
64
|
+
export * as build from "./build.ts";
|
|
51
65
|
|
|
52
66
|
// Compiled-query cache keyed by the template's stable `strings` identity, so the
|
|
53
|
-
// same call site parses once and re-runs with fresh
|
|
67
|
+
// same call site parses (and resolves its aliases) once and re-runs with fresh
|
|
68
|
+
// bindings.
|
|
54
69
|
const templateCache = new WeakMap<TemplateStringsArray, Query>();
|
|
55
70
|
|
|
56
71
|
/** The OQX tagged template. Returns the query result shaped by its consumer:
|
|
@@ -59,13 +74,13 @@ const templateCache = new WeakMap<TemplateStringsArray, Query>();
|
|
|
59
74
|
export function oqx(strings: TemplateStringsArray, ...values: unknown[]): unknown {
|
|
60
75
|
let query = templateCache.get(strings);
|
|
61
76
|
if (!query) {
|
|
62
|
-
query = parseTemplate(strings, values.length);
|
|
77
|
+
query = resolveAliases(parseTemplate(strings, values.length));
|
|
63
78
|
templateCache.set(strings, query);
|
|
64
79
|
}
|
|
65
|
-
return unwrap(
|
|
80
|
+
return unwrap(new InMemoryEngine(new DefaultContext()).run(query, values));
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
/** Parse a query string into a reusable AST. */
|
|
83
|
+
/** Parse a query string into a reusable AST (spec/oqx/AST.md). */
|
|
69
84
|
export function parse(source: string): Query {
|
|
70
85
|
return parseString(source);
|
|
71
86
|
}
|
|
@@ -78,20 +93,31 @@ export function execute(source: string, roots?: Record<string, unknown>): unknow
|
|
|
78
93
|
|
|
79
94
|
/** Run a pre-parsed query with explicit bindings and/or a backend, returning the
|
|
80
95
|
* full discriminated result. Provide `engine` (any `Engine`), or `context` (a
|
|
81
|
-
* `DataContext`, run in-memory), or `roots` (plain-object named roots).
|
|
96
|
+
* `DataContext`, run in-memory), or `roots` (plain-object named roots). The
|
|
97
|
+
* query's `select` aliases are resolved here (`resolveAliases`), once, before
|
|
98
|
+
* the engine sees it. */
|
|
82
99
|
export function run(
|
|
83
100
|
query: Query,
|
|
84
101
|
opts: { values?: readonly unknown[]; roots?: Record<string, unknown>; context?: DataContext; engine?: Engine } = {},
|
|
85
102
|
): OqxResult {
|
|
86
103
|
const values = opts.values ?? [];
|
|
87
|
-
|
|
104
|
+
const resolved = resolveAliases(query);
|
|
105
|
+
if (opts.engine) return opts.engine.run(resolved, values);
|
|
88
106
|
const context = opts.context ?? new DefaultContext(opts.roots ?? {});
|
|
89
|
-
return new InMemoryEngine(context).run(
|
|
107
|
+
return new InMemoryEngine(context).run(resolved, values);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Convenience: resolve the query's aliases and run it in-memory over
|
|
111
|
+
* plain-object roots (the default context). */
|
|
112
|
+
export function runQuery(query: Query, bindings: readonly unknown[], roots: unknown): OqxResult {
|
|
113
|
+
const ctx = new DefaultContext((roots as Record<string, unknown>) ?? {});
|
|
114
|
+
return new InMemoryEngine(ctx).run(resolveAliases(query), bindings);
|
|
90
115
|
}
|
|
91
116
|
|
|
92
117
|
function unwrap(result: OqxResult): unknown {
|
|
93
118
|
switch (result.consumer) {
|
|
94
|
-
|
|
119
|
+
// A `collect` over one object is that one record, or `null` (SEMANTICS §3).
|
|
120
|
+
case "collect": return result.single ? (result.rows[0] ?? null) : result.rows;
|
|
95
121
|
case "exists": return result.exists;
|
|
96
122
|
case "none": return result.none;
|
|
97
123
|
case "count": return result.count;
|
package/src/lexer.ts
CHANGED
|
@@ -11,18 +11,27 @@
|
|
|
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
13
|
// which is exactly the desired "interpolation is a value, never source text".)
|
|
14
|
+
//
|
|
15
|
+
// Positions (`Token.pos` / `Token.end`, the `span` of every AST node, and the
|
|
16
|
+
// offsets quoted in error messages) count Unicode CODE POINTS over the raw
|
|
17
|
+
// source (`rawSource` for a template, where a binding occupies its `${n}`
|
|
18
|
+
// marker) — not UTF-16 code units — so the two implementations agree on every
|
|
19
|
+
// offset (spec/oqx/AST.md §3). `toUtf16` converts a span for a UTF-16 editor.
|
|
14
20
|
|
|
15
21
|
import { OqxError } from "./errors.ts";
|
|
22
|
+
import type { Span } from "./ast.ts";
|
|
16
23
|
|
|
17
24
|
export type TokType =
|
|
18
25
|
| "ident"
|
|
19
|
-
| "kw" // from | where | select
|
|
26
|
+
| "kw" // from | where | select | is | not | and | or
|
|
20
27
|
| "string"
|
|
21
28
|
| "number"
|
|
22
29
|
| "lparen"
|
|
23
30
|
| "rparen"
|
|
24
31
|
| "lbrace"
|
|
25
32
|
| "rbrace"
|
|
33
|
+
| "lbracket" // [ — a bracket lookup (since 0.17)
|
|
34
|
+
| "rbracket"
|
|
26
35
|
| "comma"
|
|
27
36
|
| "colon"
|
|
28
37
|
| "caret" // ^ — one-scope lift marker
|
|
@@ -34,20 +43,33 @@ export type TokType =
|
|
|
34
43
|
|
|
35
44
|
export interface Token {
|
|
36
45
|
type: TokType;
|
|
46
|
+
/** 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 `""`. */
|
|
37
49
|
value: string;
|
|
50
|
+
/** Code-point offset of the token's first character. */
|
|
38
51
|
pos: number;
|
|
52
|
+
/** Code-point offset just past the token's last character (`[pos, end)`). */
|
|
53
|
+
end: number;
|
|
39
54
|
index?: number; // binding tokens only
|
|
40
55
|
}
|
|
41
56
|
|
|
42
|
-
const KEYWORDS = new Set(["from", "where", "select"]);
|
|
57
|
+
const KEYWORDS = new Set(["from", "where", "select", "is", "not", "and", "or"]);
|
|
43
58
|
|
|
44
59
|
// Multi-char operators, longest first (the scanner tries these before singles).
|
|
45
60
|
const MULTI_OPS = ["==", "!=", "<=", ">=", "&&", "||"];
|
|
46
61
|
const SINGLE_OPS = new Set(["<", ">", "!", "+", "-", "*", "/", "%"]);
|
|
47
62
|
|
|
48
|
-
const isIdentStart = (c: string): boolean =>
|
|
49
|
-
const isIdentPart = (c: string): boolean =>
|
|
50
|
-
const isDigit = (c: string): boolean => c >= "0" && c <= "9";
|
|
63
|
+
const isIdentStart = (c: string): boolean => /^[A-Za-z_$]$/.test(c);
|
|
64
|
+
const isIdentPart = (c: string): boolean => /^[A-Za-z0-9_$]$/.test(c);
|
|
65
|
+
const isDigit = (c: string): boolean => c.length === 1 && c >= "0" && c <= "9";
|
|
66
|
+
|
|
67
|
+
/** The length of a string in Unicode code points. */
|
|
68
|
+
export function codePointLength(s: string): number {
|
|
69
|
+
let n = 0;
|
|
70
|
+
for (const _ of s) n++;
|
|
71
|
+
return n;
|
|
72
|
+
}
|
|
51
73
|
|
|
52
74
|
/** Lex a tagged-template call: the cooked string fragments and the count of
|
|
53
75
|
* interpolated values. Emits a single flat token stream with `binding` tokens
|
|
@@ -57,18 +79,18 @@ export function lexTemplate(fragments: readonly string[], values: number): Token
|
|
|
57
79
|
let base = 0; // running offset across fragments + rendered `${…}` markers
|
|
58
80
|
for (let f = 0; f < fragments.length; f++) {
|
|
59
81
|
lexFragment(fragments[f]!, base, tokens);
|
|
60
|
-
base += fragments[f]
|
|
82
|
+
base += codePointLength(fragments[f]!);
|
|
61
83
|
if (f < fragments.length - 1) {
|
|
62
84
|
// account for the value's rendered width in the display source (see rawSource)
|
|
63
85
|
const marker = `\${${f}}`;
|
|
64
|
-
tokens.push({ type: "binding", value: marker, pos: base, index: f });
|
|
86
|
+
tokens.push({ type: "binding", value: marker, pos: base, end: base + marker.length, index: f });
|
|
65
87
|
base += marker.length;
|
|
66
88
|
}
|
|
67
89
|
}
|
|
68
90
|
if (values !== fragments.length - 1) {
|
|
69
91
|
throw new OqxError(`template arity mismatch: ${fragments.length} fragments, ${values} values`, "lex");
|
|
70
92
|
}
|
|
71
|
-
tokens.push({ type: "eof", value: "", pos: base });
|
|
93
|
+
tokens.push({ type: "eof", value: "", pos: base, end: base });
|
|
72
94
|
return tokens;
|
|
73
95
|
}
|
|
74
96
|
|
|
@@ -76,36 +98,43 @@ export function lexTemplate(fragments: readonly string[], values: number): Token
|
|
|
76
98
|
export function lexString(src: string): Token[] {
|
|
77
99
|
const tokens: Token[] = [];
|
|
78
100
|
lexFragment(src, 0, tokens);
|
|
79
|
-
|
|
101
|
+
const n = codePointLength(src);
|
|
102
|
+
tokens.push({ type: "eof", value: "", pos: n, end: n });
|
|
80
103
|
return tokens;
|
|
81
104
|
}
|
|
82
105
|
|
|
83
106
|
function lexFragment(src: string, base: number, out: Token[]): void {
|
|
107
|
+
// One entry per code point, so every index below is a code-point offset.
|
|
108
|
+
const cps = Array.from(src);
|
|
84
109
|
let i = 0;
|
|
85
|
-
const n =
|
|
86
|
-
const
|
|
87
|
-
|
|
110
|
+
const n = cps.length;
|
|
111
|
+
const at = (j: number): string => cps[j] ?? "";
|
|
112
|
+
const text = (from: number, to: number): string => cps.slice(from, to).join("");
|
|
113
|
+
const push = (type: TokType, value: string, start: number, end: number): void => {
|
|
114
|
+
out.push({ type, value, pos: base + start, end: base + end });
|
|
88
115
|
};
|
|
89
116
|
|
|
90
117
|
while (i < n) {
|
|
91
|
-
const c =
|
|
118
|
+
const c = cps[i]!;
|
|
92
119
|
|
|
93
120
|
if (c === " " || c === "\t" || c === "\n" || c === "\r") { i++; continue; }
|
|
94
121
|
|
|
95
|
-
if (c === "(") { push("lparen", c, i); i++; continue; }
|
|
96
|
-
if (c === ")") { push("rparen", c, i); i++; continue; }
|
|
97
|
-
if (c === "{") { push("lbrace", c, i); i++; continue; }
|
|
98
|
-
if (c === "}") { push("rbrace", c, i); i++; continue; }
|
|
99
|
-
if (c === "
|
|
100
|
-
if (c === "
|
|
101
|
-
if (c === "
|
|
122
|
+
if (c === "(") { push("lparen", c, i, i + 1); i++; continue; }
|
|
123
|
+
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; }
|
|
126
|
+
if (c === "[") { push("lbracket", c, i, i + 1); i++; continue; }
|
|
127
|
+
if (c === "]") { push("rbracket", c, i, i + 1); i++; continue; }
|
|
128
|
+
if (c === ",") { push("comma", c, i, i + 1); i++; continue; }
|
|
129
|
+
if (c === ":") { push("colon", c, i, i + 1); i++; continue; }
|
|
130
|
+
if (c === "^") { push("caret", c, i, i + 1); i++; continue; }
|
|
102
131
|
|
|
103
132
|
// range operator — `...` (exclusive end) or `..` (inclusive), longest first.
|
|
104
133
|
// Scanned before the dot rule so `a..b` never looks like member navigation,
|
|
105
134
|
// and before the number rule so the bounds lex as separate numbers.
|
|
106
|
-
if (c === "." &&
|
|
107
|
-
if (
|
|
108
|
-
push("range", "..", i); i += 2; continue;
|
|
135
|
+
if (c === "." && at(i + 1) === ".") {
|
|
136
|
+
if (at(i + 2) === ".") { push("range", "...", i, i + 3); i += 3; continue; }
|
|
137
|
+
push("range", "..", i, i + 2); i += 2; continue;
|
|
109
138
|
}
|
|
110
139
|
|
|
111
140
|
// `.` followed by a digit is neither navigation (a property name cannot start
|
|
@@ -113,12 +142,12 @@ function lexFragment(src: string, base: number, out: Token[]): void {
|
|
|
113
142
|
// malformed number, reported as such rather than surfacing as a confusing
|
|
114
143
|
// parse error downstream. Any other `.` is member navigation.
|
|
115
144
|
if (c === ".") {
|
|
116
|
-
if (isDigit(
|
|
117
|
-
const end = scanNumberTail(
|
|
118
|
-
const lit =
|
|
145
|
+
if (isDigit(at(i + 1))) {
|
|
146
|
+
const end = scanNumberTail(cps, i + 1, base);
|
|
147
|
+
const lit = text(i, end);
|
|
119
148
|
throw new OqxError(`malformed number ${JSON.stringify(lit)} at ${base + i} — a number starts with a digit (write 0${lit}), and a property name cannot be a digit (there is no index access)`, "lex");
|
|
120
149
|
}
|
|
121
|
-
push("dot", c, i); i++; continue;
|
|
150
|
+
push("dot", c, i, i + 1); i++; continue;
|
|
122
151
|
}
|
|
123
152
|
|
|
124
153
|
// string literal — decode into its VALUE (quotes stripped, escapes resolved).
|
|
@@ -127,18 +156,18 @@ function lexFragment(src: string, base: number, out: Token[]): void {
|
|
|
127
156
|
const start = i;
|
|
128
157
|
i++;
|
|
129
158
|
let sval = "";
|
|
130
|
-
while (i < n &&
|
|
131
|
-
if (
|
|
159
|
+
while (i < n && cps[i] !== quote) {
|
|
160
|
+
if (cps[i] === "\\") {
|
|
132
161
|
i++;
|
|
133
|
-
sval += unescape(
|
|
162
|
+
sval += unescape(cps[i]);
|
|
134
163
|
} else {
|
|
135
|
-
sval +=
|
|
164
|
+
sval += cps[i]!;
|
|
136
165
|
}
|
|
137
166
|
i++;
|
|
138
167
|
}
|
|
139
168
|
if (i >= n) throw new OqxError(`unterminated string literal at ${base + start}`, "lex");
|
|
140
169
|
i++; // closing quote
|
|
141
|
-
|
|
170
|
+
push("string", sval, start, i);
|
|
142
171
|
continue;
|
|
143
172
|
}
|
|
144
173
|
|
|
@@ -149,23 +178,23 @@ function lexFragment(src: string, base: number, out: Token[]): void {
|
|
|
149
178
|
// NaN or a dangling dot.
|
|
150
179
|
if (isDigit(c)) {
|
|
151
180
|
const start = i;
|
|
152
|
-
i = scanNumberTail(
|
|
153
|
-
push("number",
|
|
181
|
+
i = scanNumberTail(cps, i, base);
|
|
182
|
+
push("number", text(start, i), start, i);
|
|
154
183
|
continue;
|
|
155
184
|
}
|
|
156
185
|
|
|
157
186
|
// operators (multi-char first)
|
|
158
|
-
const two =
|
|
159
|
-
if (MULTI_OPS.includes(two)) { push("op", two, i); i += 2; continue; }
|
|
160
|
-
if (SINGLE_OPS.has(c)) { push("op", c, i); i++; continue; }
|
|
187
|
+
const two = c + at(i + 1);
|
|
188
|
+
if (MULTI_OPS.includes(two)) { push("op", two, i, i + 2); i += 2; continue; }
|
|
189
|
+
if (SINGLE_OPS.has(c)) { push("op", c, i, i + 1); i++; continue; }
|
|
161
190
|
|
|
162
191
|
// identifier / keyword
|
|
163
192
|
if (isIdentStart(c)) {
|
|
164
193
|
const start = i;
|
|
165
194
|
i++;
|
|
166
|
-
while (i < n && isIdentPart(
|
|
167
|
-
const word =
|
|
168
|
-
push(KEYWORDS.has(word) ? "kw" : "ident", word, start);
|
|
195
|
+
while (i < n && isIdentPart(cps[i]!)) i++;
|
|
196
|
+
const word = text(start, i);
|
|
197
|
+
push(KEYWORDS.has(word) ? "kw" : "ident", word, start, i);
|
|
169
198
|
continue;
|
|
170
199
|
}
|
|
171
200
|
|
|
@@ -176,24 +205,25 @@ function lexFragment(src: string, base: number, out: Token[]): void {
|
|
|
176
205
|
// Scan a number whose first digit is at `i`; return the index just past it.
|
|
177
206
|
// Throws the malformed-number lex error for a trailing decimal point or an
|
|
178
207
|
// exponent without digits.
|
|
179
|
-
function scanNumberTail(
|
|
208
|
+
function scanNumberTail(cps: readonly string[], i: number, base = 0): number {
|
|
180
209
|
const start = i;
|
|
181
|
-
const n =
|
|
210
|
+
const n = cps.length;
|
|
211
|
+
const at = (j: number): string => cps[j] ?? "";
|
|
182
212
|
const fail = (end: number, why: string): never => {
|
|
183
|
-
throw new OqxError(`malformed number ${JSON.stringify(
|
|
213
|
+
throw new OqxError(`malformed number ${JSON.stringify(cps.slice(start, end).join(""))} at ${base + start} — ${why}`, "lex");
|
|
184
214
|
};
|
|
185
|
-
while (i < n && isDigit(
|
|
186
|
-
if (
|
|
187
|
-
if (!isDigit(
|
|
215
|
+
while (i < n && isDigit(cps[i]!)) i++;
|
|
216
|
+
if (at(i) === "." && at(i + 1) !== ".") {
|
|
217
|
+
if (!isDigit(at(i + 1))) fail(i + 1, "a decimal point needs a digit after it (write 1.0, not 1.)");
|
|
188
218
|
i++;
|
|
189
|
-
while (i < n && isDigit(
|
|
219
|
+
while (i < n && isDigit(cps[i]!)) i++;
|
|
190
220
|
}
|
|
191
|
-
if (
|
|
221
|
+
if (at(i) === "e" || at(i) === "E") {
|
|
192
222
|
let j = i + 1;
|
|
193
|
-
if (
|
|
194
|
-
if (!isDigit(
|
|
223
|
+
if (at(j) === "+" || at(j) === "-") j++;
|
|
224
|
+
if (!isDigit(at(j))) fail(j, "an exponent needs at least one digit (write 1e5)");
|
|
195
225
|
i = j;
|
|
196
|
-
while (i < n && isDigit(
|
|
226
|
+
while (i < n && isDigit(cps[i]!)) i++;
|
|
197
227
|
}
|
|
198
228
|
return i;
|
|
199
229
|
}
|
|
@@ -214,3 +244,24 @@ function unescape(c: string | undefined): string {
|
|
|
214
244
|
export function rawSource(fragments: readonly string[]): string {
|
|
215
245
|
return fragments.map((s, i) => (i < fragments.length - 1 ? `${s}\${${i}}` : s)).join("");
|
|
216
246
|
}
|
|
247
|
+
|
|
248
|
+
/** Convert a code-point span (as every AST node carries) to UTF-16 code-unit
|
|
249
|
+
* offsets over `source` — the units a JavaScript string, a DOM range or an
|
|
250
|
+
* editor API counts in. `source` is the raw source the span was measured over
|
|
251
|
+
* (for a template, `rawSource(strings)`). Offsets past the end clamp. */
|
|
252
|
+
export function toUtf16(span: Span, source: string): Span {
|
|
253
|
+
const [start, end] = span;
|
|
254
|
+
let cp = 0;
|
|
255
|
+
let unit = 0;
|
|
256
|
+
let startUnit = -1;
|
|
257
|
+
let endUnit = -1;
|
|
258
|
+
for (const ch of source) {
|
|
259
|
+
if (cp === start) startUnit = unit;
|
|
260
|
+
if (cp === end) endUnit = unit;
|
|
261
|
+
cp++;
|
|
262
|
+
unit += ch.length;
|
|
263
|
+
}
|
|
264
|
+
if (startUnit < 0) startUnit = unit;
|
|
265
|
+
if (endUnit < 0) endUnit = unit;
|
|
266
|
+
return [startUnit, endUnit];
|
|
267
|
+
}
|
package/src/optimize/analysis.ts
CHANGED
|
@@ -29,11 +29,12 @@ export function exprReadsScopeIn(e: Expr, at: number, upTo: number): boolean {
|
|
|
29
29
|
case "ident": return at >= 1 && at <= upTo;
|
|
30
30
|
case "outer": { const d = at - e.levels; return d >= 1 && d <= upTo; }
|
|
31
31
|
case "member": return exprReadsScopeIn(e.recv, at, upTo);
|
|
32
|
-
case "index": return exprReadsScopeIn(e.recv, at, upTo) || exprReadsScopeIn(e.index, at, upTo);
|
|
33
32
|
case "call": return (e.recv !== null && exprReadsScopeIn(e.recv, at, upTo)) || e.args.some((a) => exprReadsScopeIn(a, at, upTo));
|
|
34
|
-
case "unary": return exprReadsScopeIn(e.expr, at, upTo);
|
|
33
|
+
case "unary": case "required": return exprReadsScopeIn(e.expr, at, upTo);
|
|
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));
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -47,22 +48,26 @@ export function whereReadsScopeIn(w: Where, at: number, upTo: number): boolean {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/** A directive evaluated in a scope at depth `at`: its receiver is read there;
|
|
50
|
-
* its rows are scopes at `at + 1`.
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
* its rows are scopes at `at + 1`. `destination` marks a follow destination
|
|
52
|
+
* block (its `frontier` is read one level out). */
|
|
53
|
+
export function opReadsScopeIn(op: OpNode, at: number, upTo: number, destination = false): boolean {
|
|
54
|
+
return exprReadsScopeIn(op.receiver, at, upTo) || subReadsScopeIn(op.sub, at + 1, upTo, destination);
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/** A block body whose rows are scopes at depth `at`. `from` re-projections,
|
|
56
58
|
* `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
|
-
|
|
59
|
+
* bound in a row-less scope at the same depth); a destination block's
|
|
60
|
+
* `frontier` is read one level out, under the walk's enclosing scope (§20),
|
|
61
|
+
* any other block's `frontier` (its seeds') in the row scope like `where`;
|
|
62
|
+
* `follow` walks occurrences at `at`. */
|
|
63
|
+
function subReadsScopeIn(sub: Subquery, at: number, upTo: number, destination: boolean): boolean {
|
|
60
64
|
if (sub.from.some((e) => exprReadsScopeIn(e, at, upTo))) return true;
|
|
61
65
|
if (sub.where && whereReadsScopeIn(sub.where, at, upTo)) return true;
|
|
62
66
|
if (sub.select.some((item) => itemReadsScopeIn(item, at, upTo))) return true;
|
|
63
67
|
if (sub.orderBy?.some((o) => exprReadsScopeIn(o.expr, at, upTo))) return true;
|
|
64
68
|
if (sub.limit && exprReadsScopeIn(sub.limit, at, upTo)) return true;
|
|
65
69
|
if (sub.offset && exprReadsScopeIn(sub.offset, at, upTo)) return true;
|
|
70
|
+
if (sub.frontier && exprReadsScopeIn(sub.frontier, destination ? at - 1 : at, upTo)) return true;
|
|
66
71
|
return sub.follow !== null && followReadsScopeIn(sub.follow, at, upTo);
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -71,12 +76,7 @@ function itemReadsScopeIn(item: SelectItem, at: number, upTo: number): boolean {
|
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
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);
|
|
79
|
+
return f.destinations.some((d) => (d.kind === "op" ? opReadsScopeIn(d, at, upTo, true) : exprReadsScopeIn(d, at, upTo)));
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/** Does `e` read the scope it is evaluated in (a bare identifier anywhere)? */
|
|
@@ -94,11 +94,13 @@ export function exprRaiseFree(e: Expr, ctx: RuleContext): boolean {
|
|
|
94
94
|
case "lit": case "ident": case "outer": return true;
|
|
95
95
|
case "binding": return e.index < ctx.bindingCount;
|
|
96
96
|
case "call": return false;
|
|
97
|
+
case "required": return false; // raises when absent
|
|
98
|
+
case "op": return opRaiseFree(e, ctx); // a value-position directive projects: never raise-free
|
|
97
99
|
case "member": return exprRaiseFree(e.recv, ctx);
|
|
98
|
-
case "index": return exprRaiseFree(e.recv, ctx) && exprRaiseFree(e.index, ctx);
|
|
99
100
|
case "unary": return exprRaiseFree(e.expr, ctx);
|
|
100
101
|
case "binary": case "logical": case "in": return exprRaiseFree(e.left, ctx) && exprRaiseFree(e.right, ctx);
|
|
101
102
|
case "range": return (e.lo === null || exprRaiseFree(e.lo, ctx)) && (e.hi === null || exprRaiseFree(e.hi, ctx));
|
|
103
|
+
case "tuple": return e.items.every((x) => exprRaiseFree(x, ctx));
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
|
package/src/optimize/ir.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface BlockPlan {
|
|
|
43
43
|
readonly projection: { readonly select: readonly SelectItem[]; readonly values: boolean };
|
|
44
44
|
readonly orderBy: readonly OrderSpec[] | null;
|
|
45
45
|
readonly distinct: boolean;
|
|
46
|
-
readonly bound: { readonly limit: Expr |
|
|
46
|
+
readonly bound: { readonly limit: Expr | null; readonly offset: Expr | null };
|
|
47
47
|
readonly follow: Follow | null;
|
|
48
48
|
/** Equalities answered by an index probe on the receiver (in conjunct order). */
|
|
49
49
|
readonly correlated: readonly Correlation[];
|
|
@@ -85,9 +85,9 @@ export function logicalBlock(node: OpNode, depth: number): BlockPlan {
|
|
|
85
85
|
receiver: node.receiver,
|
|
86
86
|
from: sub.from,
|
|
87
87
|
where: sub.where,
|
|
88
|
-
projection: { select: sub.select, values: sub.values
|
|
88
|
+
projection: { select: sub.select, values: sub.values },
|
|
89
89
|
orderBy: sub.orderBy,
|
|
90
|
-
distinct: node.distinct
|
|
90
|
+
distinct: node.distinct,
|
|
91
91
|
bound: { limit: sub.limit, offset: sub.offset },
|
|
92
92
|
follow: sub.follow,
|
|
93
93
|
correlated: [],
|
|
@@ -100,7 +100,7 @@ export function logicalBlock(node: OpNode, depth: number): BlockPlan {
|
|
|
100
100
|
|
|
101
101
|
/** Rebuild a conjunction from its remaining parts. */
|
|
102
102
|
export function conjunction(parts: readonly Where[]): Where | null {
|
|
103
|
-
return parts.length === 0 ? null : parts.length === 1 ? parts[0]! : { kind: "and", parts: [...parts] };
|
|
103
|
+
return parts.length === 0 ? null : parts.length === 1 ? parts[0]! : { kind: "and", span: [0, 0], parts: [...parts] };
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/** The top-level `&&` conjuncts of a where tree (a non-`and` tree is one). */
|