@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +308 -70
  3. package/dist/ast.d.ts +97 -27
  4. package/dist/ast.d.ts.map +1 -1
  5. package/dist/ast.js +21 -6
  6. package/dist/ast.js.map +1 -1
  7. package/dist/build.d.ts +43 -0
  8. package/dist/build.d.ts.map +1 -0
  9. package/dist/build.js +58 -0
  10. package/dist/build.js.map +1 -0
  11. package/dist/context.d.ts +30 -6
  12. package/dist/context.d.ts.map +1 -1
  13. package/dist/context.js +11 -10
  14. package/dist/context.js.map +1 -1
  15. package/dist/engine.d.ts +13 -3
  16. package/dist/engine.d.ts.map +1 -1
  17. package/dist/engine.js +281 -100
  18. package/dist/engine.js.map +1 -1
  19. package/dist/errors.d.ts +1 -1
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +2 -1
  22. package/dist/errors.js.map +1 -1
  23. package/dist/index.d.ts +19 -6
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +36 -13
  26. package/dist/index.js.map +1 -1
  27. package/dist/lexer.d.ts +15 -1
  28. package/dist/lexer.d.ts.map +1 -1
  29. package/dist/lexer.js +104 -51
  30. package/dist/lexer.js.map +1 -1
  31. package/dist/optimize/analysis.d.ts +3 -2
  32. package/dist/optimize/analysis.d.ts.map +1 -1
  33. package/dist/optimize/analysis.js +19 -18
  34. package/dist/optimize/analysis.js.map +1 -1
  35. package/dist/optimize/ir.d.ts +2 -2
  36. package/dist/optimize/ir.d.ts.map +1 -1
  37. package/dist/optimize/ir.js +3 -3
  38. package/dist/optimize/ir.js.map +1 -1
  39. package/dist/parser.d.ts.map +1 -1
  40. package/dist/parser.js +782 -335
  41. package/dist/parser.js.map +1 -1
  42. package/dist/plan.d.ts.map +1 -1
  43. package/dist/plan.js +4 -2
  44. package/dist/plan.js.map +1 -1
  45. package/dist/print.d.ts +17 -0
  46. package/dist/print.d.ts.map +1 -0
  47. package/dist/print.js +266 -0
  48. package/dist/print.js.map +1 -0
  49. package/dist/resolve.d.ts +23 -0
  50. package/dist/resolve.d.ts.map +1 -0
  51. package/dist/resolve.js +333 -0
  52. package/dist/resolve.js.map +1 -0
  53. package/dist/version.d.ts +5 -0
  54. package/dist/version.d.ts.map +1 -0
  55. package/dist/version.js +5 -0
  56. package/dist/version.js.map +1 -0
  57. package/dist/walk.d.ts +56 -0
  58. package/dist/walk.d.ts.map +1 -0
  59. package/dist/walk.js +157 -0
  60. package/dist/walk.js.map +1 -0
  61. package/package.json +1 -1
  62. package/src/ast.ts +126 -40
  63. package/src/build.ts +73 -0
  64. package/src/context.ts +36 -12
  65. package/src/engine.ts +285 -110
  66. package/src/errors.ts +3 -2
  67. package/src/index.ts +40 -14
  68. package/src/lexer.ts +102 -51
  69. package/src/optimize/analysis.ts +17 -15
  70. package/src/optimize/ir.ts +4 -4
  71. package/src/parser.ts +721 -289
  72. package/src/plan.ts +3 -2
  73. package/src/print.ts +276 -0
  74. package/src/resolve.ts +309 -0
  75. package/src/version.ts +4 -0
  76. package/src/walk.ts +201 -0
package/src/walk.ts ADDED
@@ -0,0 +1,201 @@
1
+ // Generic traversal over the AST (spec/oqx/AST.md §5): `visit` (enter/leave with
2
+ // a path, the enclosing clause and the scope depth), `transform` (rebuild a
3
+ // query with every expression mapped), `stripSpans`, `toJSON`. All three walks
4
+ // are driven by ONE child-key table per node kind — `CHILDREN` — so a future
5
+ // field is added in one place and every tool sees it.
6
+
7
+ import type { AstKind, AstNode, Expr, Query, QueryJson, Subquery } from "./ast.ts";
8
+ import { isExpr } from "./ast.ts";
9
+ import { LANGUAGE_VERSION } from "./version.ts";
10
+
11
+ /** The clause a node sits in, for `visit`/`transform` contexts. `source` is the
12
+ * root collection (`from <source>` or the directive receiver); `from` the further
13
+ * re-projections; `follow` the `Follow` node itself and `follow.*` its parts.
14
+ * The root node has no clause (`null`). */
15
+ export type Clause =
16
+ | "source" | "from" | "where" | "select" | "orderBy" | "limit" | "offset"
17
+ | "follow" | "follow.destination" | "follow.frontier";
18
+
19
+ /** Where a visited node sits. `depth` is the scope depth at which the node is
20
+ * evaluated (0 = the root scope: a top-level `source`, `limit`, `offset`; 1 =
21
+ * a top-level row: its `where`, `select`, `order by`, `from` re-projections and
22
+ * `follow`; a block's rows are one deeper than the block's receiver — a follow
23
+ * destination block's `where` therefore one deeper than the frontier row —
24
+ * and a destination block's `frontier` one level out of its rows, under the
25
+ * walk's enclosing scope). */
26
+ export interface VisitContext {
27
+ /** The ancestors of the node, root first (empty for the root). */
28
+ readonly path: readonly AstNode[];
29
+ readonly clause: Clause | null;
30
+ readonly depth: number;
31
+ }
32
+
33
+ export interface Visitor {
34
+ /** Called before a node's children; return `false` to skip the subtree
35
+ * (`leave` is still called for the node). */
36
+ enter?(node: AstNode, ctx: VisitContext): boolean | void;
37
+ /** Called after a node's children. */
38
+ leave?(node: AstNode, ctx: VisitContext): void;
39
+ }
40
+
41
+ /** One child slot of a node kind: the property, the clause it opens (inherited
42
+ * from the parent when absent) and how much deeper its scope is. */
43
+ export interface ChildKey {
44
+ readonly key: string;
45
+ readonly clause?: Clause;
46
+ readonly depth?: number;
47
+ }
48
+
49
+ /** The child-key table: every node kind's children, in canonical source order.
50
+ * A slot's value is a node, an array of nodes, or `null`. */
51
+ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
52
+ query: [
53
+ { key: "select", clause: "select", depth: 1 },
54
+ { key: "source", clause: "source" },
55
+ { key: "from", clause: "from", depth: 1 },
56
+ { key: "where", clause: "where", depth: 1 },
57
+ // The body's `frontier` (0.20) reads each seed in the row scope, like `where`.
58
+ { key: "frontier", clause: "follow.frontier", depth: 1 },
59
+ { key: "follow", clause: "follow", depth: 1 },
60
+ { key: "orderBy", clause: "orderBy", depth: 1 },
61
+ { key: "limit", clause: "limit" },
62
+ { key: "offset", clause: "offset" },
63
+ ],
64
+ subquery: [
65
+ { key: "select", clause: "select" },
66
+ { key: "from", clause: "from" },
67
+ { key: "where", clause: "where" },
68
+ // A destination block's `frontier` reads the occurrence under the walk's
69
+ // enclosing scope — one level OUT of the block's rows (SEMANTICS §20); any
70
+ // other block's reads its seeds in the row scope, like `where`
71
+ // (`childDepth` tells the two apart by the block's position).
72
+ { key: "frontier", clause: "follow.frontier", depth: -1 },
73
+ { key: "follow", clause: "follow" },
74
+ { key: "orderBy", clause: "orderBy" },
75
+ { key: "limit", clause: "limit" },
76
+ { key: "offset", clause: "offset" },
77
+ ],
78
+ op: [{ key: "receiver" }, { key: "sub", depth: 1 }],
79
+ follow: [{ key: "destinations", clause: "follow.destination" }],
80
+ order: [{ key: "expr" }],
81
+ field: [{ key: "expr" }],
82
+ collect: [{ key: "op" }],
83
+ and: [{ key: "parts" }],
84
+ or: [{ key: "parts" }],
85
+ not: [{ key: "expr" }],
86
+ scalar: [{ key: "expr" }],
87
+ lit: [],
88
+ ident: [],
89
+ outer: [],
90
+ binding: [],
91
+ member: [{ key: "recv" }],
92
+ call: [{ key: "recv" }, { key: "args" }],
93
+ unary: [{ key: "expr" }],
94
+ required: [{ key: "expr" }],
95
+ binary: [{ key: "left" }, { key: "right" }],
96
+ logical: [{ key: "left" }, { key: "right" }],
97
+ in: [{ key: "left" }, { key: "right" }],
98
+ range: [{ key: "lo" }, { key: "hi" }],
99
+ tuple: [{ key: "items" }],
100
+ };
101
+
102
+ type Slot = AstNode | AstNode[] | null;
103
+
104
+ function slot(node: AstNode, key: string): Slot {
105
+ return (node as unknown as Record<string, Slot>)[key] ?? null;
106
+ }
107
+
108
+ /** A block is a follow destination block when its `op` is a direct destination
109
+ * of a `follow` node. `path` is the ancestors with the block itself last. */
110
+ function isDestinationBlock(path: readonly AstNode[]): boolean {
111
+ const op = path[path.length - 2];
112
+ const owner = path[path.length - 3];
113
+ return op?.kind === "op" && owner?.kind === "follow";
114
+ }
115
+
116
+ /** How much deeper a child's scope is: the table's value, except a block's
117
+ * `frontier`, which is one level OUT only for a destination block (the walk's
118
+ * enclosing scope) and the row scope itself for a body with its own `follow`. */
119
+ function childDepth(node: AstNode, child: ChildKey, path: readonly AstNode[]): number {
120
+ if (node.kind === "subquery" && child.key === "frontier" && !isDestinationBlock(path)) return 0;
121
+ return child.depth ?? 0;
122
+ }
123
+
124
+ /** Walk a tree depth-first in canonical source order. */
125
+ export function visit(root: AstNode, visitor: Visitor): void {
126
+ const path: AstNode[] = [];
127
+ const go = (node: AstNode, clause: Clause | null, depth: number): void => {
128
+ const ctx: VisitContext = { path: path.slice(), clause, depth };
129
+ const descend = visitor.enter?.(node, ctx) !== false;
130
+ if (descend) {
131
+ path.push(node);
132
+ for (const child of CHILDREN[node.kind]) {
133
+ const v = slot(node, child.key);
134
+ if (v === null) continue;
135
+ const c = child.clause ?? clause;
136
+ const d = depth + childDepth(node, child, path);
137
+ if (Array.isArray(v)) for (const x of v) go(x, c, d);
138
+ else go(v, c, d);
139
+ }
140
+ path.pop();
141
+ }
142
+ visitor.leave?.(node, ctx);
143
+ };
144
+ go(root, null, 0);
145
+ }
146
+
147
+ /** Rebuild a query (or block body) with every expression mapped through `f`,
148
+ * children first (so `f` sees an expression whose operands are already mapped).
149
+ * Nodes `f` returns unchanged, and every ancestor whose children did not change,
150
+ * are kept by reference — spans and identity of untouched nodes survive. */
151
+ export function transform<T extends Query | Subquery>(root: T, f: (expr: Expr, ctx: VisitContext) => Expr): T {
152
+ const path: AstNode[] = [];
153
+ const go = (node: AstNode, clause: Clause | null, depth: number): AstNode => {
154
+ const ctx: VisitContext = { path: path.slice(), clause, depth };
155
+ path.push(node);
156
+ let out: AstNode = node;
157
+ for (const child of CHILDREN[node.kind]) {
158
+ const v = slot(node, child.key);
159
+ if (v === null) continue;
160
+ const c = child.clause ?? clause;
161
+ const d = depth + childDepth(node, child, path);
162
+ let next: Slot;
163
+ if (Array.isArray(v)) {
164
+ let changed = false;
165
+ const mapped = v.map((x) => { const y = go(x, c, d); if (y !== x) changed = true; return y; });
166
+ next = changed ? mapped : v;
167
+ } else next = go(v, c, d);
168
+ if (next !== v) out = { ...out, [child.key]: next } as AstNode;
169
+ }
170
+ path.pop();
171
+ return isExpr(out) ? f(out, ctx) : out;
172
+ };
173
+ return go(root, null, 0) as T;
174
+ }
175
+
176
+ /** The tree with every `span` removed, for shape comparisons (the fixtures and
177
+ * the round-trip law compare stripped trees). A deep copy of plain data. */
178
+ export type Stripped<T> =
179
+ T extends readonly (infer U)[] ? Stripped<U>[]
180
+ : T extends object ? { [K in keyof T as K extends "span" ? never : K]: Stripped<T[K]> }
181
+ : T;
182
+
183
+ export function stripSpans<T>(node: T): Stripped<T> {
184
+ if (Array.isArray(node)) return node.map(stripSpans) as Stripped<T>;
185
+ if (node !== null && typeof node === "object") {
186
+ const out: Record<string, unknown> = {};
187
+ for (const [k, v] of Object.entries(node as Record<string, unknown>)) {
188
+ if (k === "span") continue;
189
+ out[k] = stripSpans(v);
190
+ }
191
+ return out as Stripped<T>;
192
+ }
193
+ return node as Stripped<T>;
194
+ }
195
+
196
+ /** The query as the JSON document both implementations exchange: the tree
197
+ * (already plain data) stamped with the language version it was produced under
198
+ * (`{ "oqx": "0.16", "kind": "query", … }`). */
199
+ export function toJSON(query: Query): QueryJson {
200
+ return { oqx: LANGUAGE_VERSION, ...query };
201
+ }