@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.
@@ -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.21";
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.21";
4
+ export const LANGUAGE_VERSION = "0.23";
5
5
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omgbase/oqx",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "OQX (omgbase Query eXpressions): a composable query language and engine for JavaScript collections",
5
5
  "keywords": [
6
6
  "query",
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 operator of a `binary` expression: a comparison, an identity test, or arithmetic. */
30
- export type BinaryOp = RelOp | IdentityOp | "+" | "-" | "*" | "/" | "%";
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
- const r = this.ctx.callMethod?.(e.name, recv, args);
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 '.${e.name}(…)'`, "eval");
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
- const MULTI_OPS = ["==", "!=", "<=", ">=", "&&", "||"];
76
- const SINGLE_OPS = new Set(["<", ">", "!", "+", "-", "*", "/", "%"]);
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; }
@@ -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
- case "binary": case "logical": case "in": return exprRaiseFree(e.left, ctx) && exprRaiseFree(e.right, ctx);
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
- const CMP_OPS = new Set<string>(["==", "!=", "<", "<=", ">", ">="]);
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;