@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,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/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.21";
4
+ export const LANGUAGE_VERSION = "0.23";