@omgbase/oqx 0.21.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 +11 -0
- package/README.md +37 -0
- package/dist/ast.d.ts +13 -2
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +10 -0
- package/dist/ast.js.map +1 -1
- package/dist/engine.d.ts +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +17 -2
- 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.map +1 -1
- package/dist/lexer.js +13 -3
- package/dist/lexer.js.map +1 -1
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +4 -1
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +4 -1
- package/dist/parser.js.map +1 -1
- package/dist/print.js +1 -0
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/ast.ts +20 -2
- package/src/engine.ts +18 -2
- package/src/index.ts +3 -1
- package/src/lexer.ts +10 -4
- package/src/optimize/analysis.ts +5 -1
- package/src/parser.ts +4 -1
- package/src/print.ts +1 -0
- package/src/references.ts +238 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,225 @@
|
|
|
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
|
+
import { visit } from "./walk.js";
|
|
57
|
+
/** A dotted name headed by an `ident` or `outer`: the head and the path after it. */
|
|
58
|
+
function dottedName(e) {
|
|
59
|
+
if (e.kind === "ident" || e.kind === "outer")
|
|
60
|
+
return { head: e, path: [] };
|
|
61
|
+
if (e.kind === "member") {
|
|
62
|
+
const inner = dottedName(e.recv);
|
|
63
|
+
return inner === null ? null : { head: inner.head, path: [...inner.path, e.name] };
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/** The alias an item defines (SEMANTICS §14): a named `collect`, or a named non-lift field. */
|
|
68
|
+
function aliasName(it) {
|
|
69
|
+
if (it.kind === "collect")
|
|
70
|
+
return it.name === "" ? null : it.name;
|
|
71
|
+
return it.lift === 0 && it.name !== "" ? it.name : null;
|
|
72
|
+
}
|
|
73
|
+
/** Every name a query (or block body) reads from a row, in canonical source order. */
|
|
74
|
+
export function references(root) {
|
|
75
|
+
const out = [];
|
|
76
|
+
const lifts = new Map();
|
|
77
|
+
visit(root, {
|
|
78
|
+
enter(node, ctx) {
|
|
79
|
+
const named = dottedName(node);
|
|
80
|
+
if (named === null)
|
|
81
|
+
return;
|
|
82
|
+
const { head } = named;
|
|
83
|
+
// The head of a navigation is handled here, once, with the chain's full name.
|
|
84
|
+
let name = head.name;
|
|
85
|
+
let path = named.path;
|
|
86
|
+
if (name === "$it") {
|
|
87
|
+
if (path.length === 0)
|
|
88
|
+
return false; // the row itself
|
|
89
|
+
name = path[0];
|
|
90
|
+
path = path.slice(1);
|
|
91
|
+
}
|
|
92
|
+
else if (name.startsWith("$"))
|
|
93
|
+
return false; // a language value
|
|
94
|
+
if (ctx.clause === null)
|
|
95
|
+
return false; // unreachable: the root node is never a name
|
|
96
|
+
if (head.kind === "ident" && isResolvedAlias(name, ctx.path, ctx.clause))
|
|
97
|
+
return false;
|
|
98
|
+
const levels = head.kind === "outer" ? head.levels : 0;
|
|
99
|
+
const depth = ctx.depth - levels;
|
|
100
|
+
if (isLiftBound(name, depth, ctx.path, lifts))
|
|
101
|
+
return false;
|
|
102
|
+
const full = [name, ...path].join(".");
|
|
103
|
+
const steps = scopeSteps(ctx.path, node);
|
|
104
|
+
const scoped = levels === 0 ? steps : cutAt(steps, depth);
|
|
105
|
+
out.push({ name: full, clause: ctx.clause, scope: { depth, steps: scoped }, span: node.span });
|
|
106
|
+
return false;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
/** Whether a bare `name` in this position is a `select` alias the language
|
|
112
|
+
* substitutes (so the name is the alias, not a row property): in the body's
|
|
113
|
+
* `where`, any alias; in a `select` item, an alias defined to its LEFT. */
|
|
114
|
+
function isResolvedAlias(name, path, clause) {
|
|
115
|
+
if (clause !== "where" && clause !== "select")
|
|
116
|
+
return false;
|
|
117
|
+
// The nearest body, and the item under it when we are inside its select.
|
|
118
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
119
|
+
const a = path[i];
|
|
120
|
+
if (a.kind !== "query" && a.kind !== "subquery")
|
|
121
|
+
continue;
|
|
122
|
+
const index = new Map();
|
|
123
|
+
a.select.forEach((it, j) => { const n = aliasName(it); if (n !== null)
|
|
124
|
+
index.set(n, j); });
|
|
125
|
+
const j = index.get(name);
|
|
126
|
+
if (j === undefined)
|
|
127
|
+
return false;
|
|
128
|
+
if (clause === "where")
|
|
129
|
+
return true;
|
|
130
|
+
const item = path[i + 1];
|
|
131
|
+
const at = item === undefined ? -1 : a.select.indexOf(item);
|
|
132
|
+
return at >= 0 && j < at;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Whether `name`, read at scope `depth`, is bound there by a lift (`^name: expr`
|
|
138
|
+
* in a nested block, SEMANTICS §19): the lifted value is appended to that
|
|
139
|
+
* scope's row record, so the name is a binding, not a row property. The body
|
|
140
|
+
* whose rows are at `depth` is the depth-th body on the path (every nested body
|
|
141
|
+
* is one scope deeper than the one around it).
|
|
142
|
+
*/
|
|
143
|
+
function isLiftBound(name, depth, path, cache) {
|
|
144
|
+
if (depth < 1)
|
|
145
|
+
return false;
|
|
146
|
+
const bodies = path.filter((a) => a.kind === "query" || a.kind === "subquery");
|
|
147
|
+
const body = bodies[depth - 1];
|
|
148
|
+
if (body === undefined)
|
|
149
|
+
return false;
|
|
150
|
+
let names = cache.get(body);
|
|
151
|
+
if (names === undefined) {
|
|
152
|
+
names = liftsLandingOn(body);
|
|
153
|
+
cache.set(body, names);
|
|
154
|
+
}
|
|
155
|
+
return names.has(name);
|
|
156
|
+
}
|
|
157
|
+
/** The names the nested blocks of `body` lift onto it: a `field` item with `lift` L in a block L scopes below it. */
|
|
158
|
+
function liftsLandingOn(body) {
|
|
159
|
+
const names = new Set();
|
|
160
|
+
const own = body.kind === "subquery" ? 1 : 0;
|
|
161
|
+
visit(body, {
|
|
162
|
+
enter(node, ctx) {
|
|
163
|
+
if (node.kind !== "field" || node.lift === 0)
|
|
164
|
+
return;
|
|
165
|
+
const below = ctx.path.filter((a) => a.kind === "subquery").length - own;
|
|
166
|
+
if (node.lift === below)
|
|
167
|
+
names.add(node.name);
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
return names;
|
|
171
|
+
}
|
|
172
|
+
/** The row chain of the scope a node is evaluated in, from its ancestors. */
|
|
173
|
+
function scopeSteps(path, node) {
|
|
174
|
+
const steps = [];
|
|
175
|
+
let depth = 0;
|
|
176
|
+
for (let i = 0; i < path.length; i++) {
|
|
177
|
+
const a = path[i];
|
|
178
|
+
const next = path[i + 1] ?? node;
|
|
179
|
+
switch (a.kind) {
|
|
180
|
+
case "query": {
|
|
181
|
+
if (next === a.source || next === a.limit || next === a.offset)
|
|
182
|
+
break; // read at the root scope
|
|
183
|
+
depth = 1;
|
|
184
|
+
steps.push({ expr: a.source, depth });
|
|
185
|
+
pushFroms(steps, a.from, next, depth);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case "subquery": {
|
|
189
|
+
// The receiver step was pushed by the enclosing `op`; this body's rows
|
|
190
|
+
// are one deeper than where that receiver was read.
|
|
191
|
+
pushFroms(steps, a.from, next, depth);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "op": {
|
|
195
|
+
if (next !== a.sub)
|
|
196
|
+
break; // inside the receiver: read where the block is read
|
|
197
|
+
depth += 1;
|
|
198
|
+
steps.push({ expr: a.receiver, depth });
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case "follow": {
|
|
202
|
+
// The destinations are read on the walk's frontier rows, at the body's depth.
|
|
203
|
+
steps.push({ expr: a, depth });
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
default: break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return steps;
|
|
210
|
+
}
|
|
211
|
+
/** The `from` re-projections that apply at `next`: all of them unless `next` is one of them, in which case those before it. */
|
|
212
|
+
function pushFroms(steps, from, next, depth) {
|
|
213
|
+
const at = from.indexOf(next);
|
|
214
|
+
const upto = at === -1 ? from.length : at;
|
|
215
|
+
for (let k = 0; k < upto; k++)
|
|
216
|
+
steps.push({ expr: from[k], depth });
|
|
217
|
+
}
|
|
218
|
+
/** The steps up to and including the last one producing rows at `depth` (none for the root scope). */
|
|
219
|
+
function cutAt(steps, depth) {
|
|
220
|
+
let end = 0;
|
|
221
|
+
steps.forEach((s, i) => { if (s.depth === depth)
|
|
222
|
+
end = i + 1; });
|
|
223
|
+
return steps.slice(0, end);
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=references.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"references.js","sourceRoot":"","sources":["../src/references.ts"],"names":[],"mappings":"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"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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 declare const LANGUAGE_VERSION = "0.
|
|
4
|
+
export declare const LANGUAGE_VERSION = "0.23";
|
|
5
5
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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";
|
|
5
5
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
package/src/ast.ts
CHANGED
|
@@ -26,8 +26,26 @@ export type RelOp = "==" | "!=" | "<" | "<=" | ">" | ">=";
|
|
|
26
26
|
/** The identity operators (SEMANTICS §5b, since 0.17). */
|
|
27
27
|
export type IdentityOp = "is" | "is not";
|
|
28
28
|
|
|
29
|
-
/** The
|
|
30
|
-
|
|
29
|
+
/** The infix regex operators (SEMANTICS §11, since 0.22): sugar over `matches` —
|
|
30
|
+
* `a ~ b` ≡ `a.matches(b)`, `a ~* b` ≡ `a.matches(b, "i")`, `a !~ b` ≡
|
|
31
|
+
* `!a.matches(b)`, `a !~* b` ≡ `!a.matches(b, "i")`. The tree keeps the
|
|
32
|
+
* operator as written so the canonical print is what the user wrote. */
|
|
33
|
+
export type RegexOp = "~" | "~*" | "!~" | "!~*";
|
|
34
|
+
|
|
35
|
+
/** The operator of a `binary` expression: a comparison, an identity test, a regex match, or arithmetic. */
|
|
36
|
+
export type BinaryOp = RelOp | IdentityOp | RegexOp | "+" | "-" | "*" | "/" | "%";
|
|
37
|
+
|
|
38
|
+
/** The regex operators, with the `matches` call each stands for. */
|
|
39
|
+
export const REGEX_OPS: Readonly<Record<RegexOp, { negated: boolean; flags: string | undefined }>> = {
|
|
40
|
+
"~": { negated: false, flags: undefined },
|
|
41
|
+
"~*": { negated: false, flags: "i" },
|
|
42
|
+
"!~": { negated: true, flags: undefined },
|
|
43
|
+
"!~*": { negated: true, flags: "i" },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function isRegexOp(op: string): op is RegexOp {
|
|
47
|
+
return Object.hasOwn(REGEX_OPS, op);
|
|
48
|
+
}
|
|
31
49
|
|
|
32
50
|
/** The operator of a `unary` expression. */
|
|
33
51
|
export type UnaryOp = "!" | "-";
|
package/src/engine.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import type {
|
|
23
23
|
Query, Where, Expr, OpNode, SelectItem, OrderSpec, Follow, Subquery,
|
|
24
24
|
} from "./ast.ts";
|
|
25
|
+
import { isRegexOp, REGEX_OPS } from "./ast.ts";
|
|
25
26
|
import type { DataContext } from "./context.ts";
|
|
26
27
|
import { DefaultContext } from "./context.ts";
|
|
27
28
|
import { OqxError } from "./errors.ts";
|
|
@@ -877,6 +878,15 @@ export class InMemoryEngine implements Engine {
|
|
|
877
878
|
const same = canonicalKey(this.hostIdentity(l)) === canonicalKey(this.hostIdentity(r));
|
|
878
879
|
return e.op === "is" ? same : !same;
|
|
879
880
|
}
|
|
881
|
+
if (isRegexOp(e.op)) {
|
|
882
|
+
// The regex operators (SEMANTICS §11, since 0.22) are sugar over
|
|
883
|
+
// `matches`: `a ~ b` is `a.matches(b)`, `~*` adds the `i` flag, `!~`
|
|
884
|
+
// / `!~*` negate. They go through the same method dispatch as the
|
|
885
|
+
// call would, so a host's `matches` (and its regex dialect) applies.
|
|
886
|
+
const { negated, flags } = REGEX_OPS[e.op];
|
|
887
|
+
const hit = this.callMethod("matches", l, flags === undefined ? [r] : [r, flags]);
|
|
888
|
+
return negated ? !truthy(hit) : hit;
|
|
889
|
+
}
|
|
880
890
|
return isRelOp(e.op) ? relate(e.op, l, r) : arith(e.op, l, r);
|
|
881
891
|
}
|
|
882
892
|
case "logical": {
|
|
@@ -978,9 +988,15 @@ export class InMemoryEngine implements Engine {
|
|
|
978
988
|
throw new OqxError(`unknown function '${e.name}(…)'`, "eval");
|
|
979
989
|
}
|
|
980
990
|
const recv = this.evalExpr(e.recv, scope);
|
|
981
|
-
|
|
991
|
+
return this.callMethod(e.name, recv, args);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// `recv.name(args)` on evaluated values — the dispatch a method call and the
|
|
995
|
+
// regex operators share.
|
|
996
|
+
private callMethod(name: string, recv: unknown, args: unknown[]): unknown {
|
|
997
|
+
const r = this.ctx.callMethod?.(name, recv, args);
|
|
982
998
|
if (r?.handled) return r.value;
|
|
983
|
-
throw new OqxError(`unknown method '.${
|
|
999
|
+
throw new OqxError(`unknown method '.${name}(…)'`, "eval");
|
|
984
1000
|
}
|
|
985
1001
|
}
|
|
986
1002
|
|
package/src/index.ts
CHANGED
|
@@ -53,11 +53,13 @@ export { IndexedCollection } from "./adapters/indexed.ts";
|
|
|
53
53
|
export { ROWS_ROOT, partitionPushable, residualQuery, asEquality, isConst, constValue } from "./plan.ts";
|
|
54
54
|
export * as semantics from "./semantics.ts";
|
|
55
55
|
export type * from "./ast.ts";
|
|
56
|
-
export { isExpr } from "./ast.ts";
|
|
56
|
+
export { isExpr, isRegexOp, REGEX_OPS } from "./ast.ts";
|
|
57
57
|
export { parseTemplate } from "./parser.ts";
|
|
58
58
|
export { rawSource, toUtf16, codePointLength } from "./lexer.ts";
|
|
59
59
|
export { resolveAliases } from "./resolve.ts";
|
|
60
60
|
export type { Clause, VisitContext, Visitor, ChildKey, Stripped } from "./walk.ts";
|
|
61
|
+
export type { Reference, ReferenceScope, ReferenceStep } from "./references.ts";
|
|
62
|
+
export { references } from "./references.ts";
|
|
61
63
|
export { CHILDREN, visit, transform, stripSpans, toJSON } from "./walk.ts";
|
|
62
64
|
export type { Template } from "./print.ts";
|
|
63
65
|
export { print, printTemplate } from "./print.ts";
|
package/src/lexer.ts
CHANGED
|
@@ -44,7 +44,7 @@ export type TokType =
|
|
|
44
44
|
| "caret" // ^ — one-scope lift marker
|
|
45
45
|
| "dot"
|
|
46
46
|
| "range" // `..` (inclusive) or `...` (exclusive end) — a Ruby-style range operator (value carries which)
|
|
47
|
-
| "op" // == != <= >= < > && || ! + - * / % (value carries the operator)
|
|
47
|
+
| "op" // == != <= >= < > && || ! + - * / % ~ ~* !~ !~* (value carries the operator)
|
|
48
48
|
| "binding" // a ${…} interpolation; `index` names the value slot
|
|
49
49
|
// A template literal (since 0.21): the opening and closing backticks, a run of
|
|
50
50
|
// its decoded text, and the `${` / `}` around an interpolation — whose `}` is a
|
|
@@ -72,8 +72,12 @@ export interface Token {
|
|
|
72
72
|
const KEYWORDS = new Set(["from", "where", "select", "is", "not", "and", "or"]);
|
|
73
73
|
|
|
74
74
|
// Multi-char operators, longest first (the scanner tries these before singles).
|
|
75
|
-
|
|
76
|
-
|
|
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(["<", ">", "!", "+", "-", "*", "/", "%", "~"]);
|
|
77
81
|
|
|
78
82
|
const isIdentStart = (c: string): boolean => /^[A-Za-z_$]$/.test(c);
|
|
79
83
|
const isIdentPart = (c: string): boolean => /^[A-Za-z0-9_$]$/.test(c);
|
|
@@ -246,7 +250,9 @@ function lexFragment(src: string, base: number, out: Token[]): void {
|
|
|
246
250
|
continue;
|
|
247
251
|
}
|
|
248
252
|
|
|
249
|
-
// 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; }
|
|
250
256
|
const two = c + at(i + 1);
|
|
251
257
|
if (MULTI_OPS.includes(two)) { push("op", two, i, i + 2); i += 2; continue; }
|
|
252
258
|
if (SINGLE_OPS.has(c)) { push("op", c, i, i + 1); i++; continue; }
|
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 --------------------------------------------------------------
|
|
@@ -99,7 +100,10 @@ export function exprRaiseFree(e: Expr, ctx: RuleContext): boolean {
|
|
|
99
100
|
case "op": return opRaiseFree(e, ctx); // a value-position directive projects: never raise-free
|
|
100
101
|
case "member": return exprRaiseFree(e.recv, ctx);
|
|
101
102
|
case "unary": return exprRaiseFree(e.expr, ctx);
|
|
102
|
-
|
|
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);
|
|
103
107
|
case "range": return (e.lo === null || exprRaiseFree(e.lo, ctx)) && (e.hi === null || exprRaiseFree(e.hi, ctx));
|
|
104
108
|
case "tuple": return e.items.every((x) => exprRaiseFree(x, ctx));
|
|
105
109
|
// A template literal raises on a list or record interpolation (§7b), which
|
package/src/parser.ts
CHANGED
|
@@ -109,7 +109,10 @@ const CLAUSE_WORDS = new Set<string>([
|
|
|
109
109
|
"limit", "offset",
|
|
110
110
|
]);
|
|
111
111
|
const RELOPS = new Set<string>(["==", "!=", "<", "<=", ">", ">="]);
|
|
112
|
-
|
|
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>(["==", "!=", "<", "<=", ">", ">=", "~", "~*", "!~", "!~*"]);
|
|
113
116
|
const ADD_OPS = new Set<string>(["+", "-"]);
|
|
114
117
|
const MUL_OPS = new Set<string>(["*", "/", "%"]);
|
|
115
118
|
// `true`/`false`/`null` are literals in every position, so they can never name a
|
package/src/print.ts
CHANGED
|
@@ -69,6 +69,7 @@ function prec(e: Expr): number {
|
|
|
69
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;
|