@omgbase/oqx 0.15.0 → 0.18.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 (75) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +170 -17
  3. package/dist/ast.d.ts +76 -22
  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 +37 -0
  8. package/dist/build.d.ts.map +1 -0
  9. package/dist/build.js +54 -0
  10. package/dist/build.js.map +1 -0
  11. package/dist/context.d.ts +13 -3
  12. package/dist/context.d.ts.map +1 -1
  13. package/dist/context.js +7 -3
  14. package/dist/context.js.map +1 -1
  15. package/dist/engine.d.ts +1 -2
  16. package/dist/engine.d.ts.map +1 -1
  17. package/dist/engine.js +52 -23
  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 +34 -12
  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.map +1 -1
  32. package/dist/optimize/analysis.js +5 -3
  33. package/dist/optimize/analysis.js.map +1 -1
  34. package/dist/optimize/ir.d.ts +2 -2
  35. package/dist/optimize/ir.d.ts.map +1 -1
  36. package/dist/optimize/ir.js +3 -3
  37. package/dist/optimize/ir.js.map +1 -1
  38. package/dist/parser.d.ts.map +1 -1
  39. package/dist/parser.js +616 -251
  40. package/dist/parser.js.map +1 -1
  41. package/dist/plan.d.ts.map +1 -1
  42. package/dist/plan.js +4 -2
  43. package/dist/plan.js.map +1 -1
  44. package/dist/print.d.ts +17 -0
  45. package/dist/print.d.ts.map +1 -0
  46. package/dist/print.js +266 -0
  47. package/dist/print.js.map +1 -0
  48. package/dist/resolve.d.ts +23 -0
  49. package/dist/resolve.d.ts.map +1 -0
  50. package/dist/resolve.js +330 -0
  51. package/dist/resolve.js.map +1 -0
  52. package/dist/version.d.ts +5 -0
  53. package/dist/version.d.ts.map +1 -0
  54. package/dist/version.js +5 -0
  55. package/dist/version.js.map +1 -0
  56. package/dist/walk.d.ts +54 -0
  57. package/dist/walk.d.ts.map +1 -0
  58. package/dist/walk.js +139 -0
  59. package/dist/walk.js.map +1 -0
  60. package/package.json +1 -1
  61. package/src/ast.ts +104 -35
  62. package/src/build.ts +72 -0
  63. package/src/context.ts +16 -3
  64. package/src/engine.ts +55 -26
  65. package/src/errors.ts +3 -2
  66. package/src/index.ts +38 -13
  67. package/src/lexer.ts +102 -51
  68. package/src/optimize/analysis.ts +4 -3
  69. package/src/optimize/ir.ts +4 -4
  70. package/src/parser.ts +573 -225
  71. package/src/plan.ts +3 -2
  72. package/src/print.ts +274 -0
  73. package/src/resolve.ts +305 -0
  74. package/src/version.ts +4 -0
  75. package/src/walk.ts +180 -0
package/src/walk.ts ADDED
@@ -0,0 +1,180 @@
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.where" | "follow.frontier" | "follow.by";
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
+ * `where` is one deeper than the frontier row it tests successors of). */
24
+ export interface VisitContext {
25
+ /** The ancestors of the node, root first (empty for the root). */
26
+ readonly path: readonly AstNode[];
27
+ readonly clause: Clause | null;
28
+ readonly depth: number;
29
+ }
30
+
31
+ export interface Visitor {
32
+ /** Called before a node's children; return `false` to skip the subtree
33
+ * (`leave` is still called for the node). */
34
+ enter?(node: AstNode, ctx: VisitContext): boolean | void;
35
+ /** Called after a node's children. */
36
+ leave?(node: AstNode, ctx: VisitContext): void;
37
+ }
38
+
39
+ /** One child slot of a node kind: the property, the clause it opens (inherited
40
+ * from the parent when absent) and how much deeper its scope is. */
41
+ export interface ChildKey {
42
+ readonly key: string;
43
+ readonly clause?: Clause;
44
+ readonly depth?: number;
45
+ }
46
+
47
+ /** The child-key table: every node kind's children, in canonical source order.
48
+ * A slot's value is a node, an array of nodes, or `null`. */
49
+ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
50
+ query: [
51
+ { key: "select", clause: "select", depth: 1 },
52
+ { key: "source", clause: "source" },
53
+ { key: "from", clause: "from", depth: 1 },
54
+ { key: "where", clause: "where", depth: 1 },
55
+ { key: "follow", clause: "follow", depth: 1 },
56
+ { key: "orderBy", clause: "orderBy", depth: 1 },
57
+ { key: "limit", clause: "limit" },
58
+ { key: "offset", clause: "offset" },
59
+ ],
60
+ subquery: [
61
+ { key: "select", clause: "select" },
62
+ { key: "from", clause: "from" },
63
+ { key: "where", clause: "where" },
64
+ { key: "follow", clause: "follow" },
65
+ { key: "orderBy", clause: "orderBy" },
66
+ { key: "limit", clause: "limit" },
67
+ { key: "offset", clause: "offset" },
68
+ ],
69
+ op: [{ key: "receiver" }, { key: "sub", depth: 1 }],
70
+ follow: [
71
+ { key: "destinations", clause: "follow.destination" },
72
+ { key: "where", clause: "follow.where", depth: 1 },
73
+ { key: "frontier", clause: "follow.frontier" },
74
+ { key: "by", clause: "follow.by" },
75
+ ],
76
+ order: [{ key: "expr" }],
77
+ field: [{ key: "expr" }],
78
+ collect: [{ key: "op" }],
79
+ and: [{ key: "parts" }],
80
+ or: [{ key: "parts" }],
81
+ not: [{ key: "expr" }],
82
+ scalar: [{ key: "expr" }],
83
+ lit: [],
84
+ ident: [],
85
+ outer: [],
86
+ binding: [],
87
+ member: [{ key: "recv" }],
88
+ call: [{ key: "recv" }, { key: "args" }],
89
+ unary: [{ key: "expr" }],
90
+ required: [{ key: "expr" }],
91
+ binary: [{ key: "left" }, { key: "right" }],
92
+ logical: [{ key: "left" }, { key: "right" }],
93
+ in: [{ key: "left" }, { key: "right" }],
94
+ range: [{ key: "lo" }, { key: "hi" }],
95
+ };
96
+
97
+ type Slot = AstNode | AstNode[] | null;
98
+
99
+ function slot(node: AstNode, key: string): Slot {
100
+ return (node as unknown as Record<string, Slot>)[key] ?? null;
101
+ }
102
+
103
+ /** Walk a tree depth-first in canonical source order. */
104
+ export function visit(root: AstNode, visitor: Visitor): void {
105
+ const path: AstNode[] = [];
106
+ const go = (node: AstNode, clause: Clause | null, depth: number): void => {
107
+ const ctx: VisitContext = { path: path.slice(), clause, depth };
108
+ const descend = visitor.enter?.(node, ctx) !== false;
109
+ if (descend) {
110
+ path.push(node);
111
+ for (const child of CHILDREN[node.kind]) {
112
+ const v = slot(node, child.key);
113
+ if (v === null) continue;
114
+ const c = child.clause ?? clause;
115
+ const d = depth + (child.depth ?? 0);
116
+ if (Array.isArray(v)) for (const x of v) go(x, c, d);
117
+ else go(v, c, d);
118
+ }
119
+ path.pop();
120
+ }
121
+ visitor.leave?.(node, ctx);
122
+ };
123
+ go(root, null, 0);
124
+ }
125
+
126
+ /** Rebuild a query (or block body) with every expression mapped through `f`,
127
+ * children first (so `f` sees an expression whose operands are already mapped).
128
+ * Nodes `f` returns unchanged, and every ancestor whose children did not change,
129
+ * are kept by reference — spans and identity of untouched nodes survive. */
130
+ export function transform<T extends Query | Subquery>(root: T, f: (expr: Expr, ctx: VisitContext) => Expr): T {
131
+ const path: AstNode[] = [];
132
+ const go = (node: AstNode, clause: Clause | null, depth: number): AstNode => {
133
+ const ctx: VisitContext = { path: path.slice(), clause, depth };
134
+ path.push(node);
135
+ let out: AstNode = node;
136
+ for (const child of CHILDREN[node.kind]) {
137
+ const v = slot(node, child.key);
138
+ if (v === null) continue;
139
+ const c = child.clause ?? clause;
140
+ const d = depth + (child.depth ?? 0);
141
+ let next: Slot;
142
+ if (Array.isArray(v)) {
143
+ let changed = false;
144
+ const mapped = v.map((x) => { const y = go(x, c, d); if (y !== x) changed = true; return y; });
145
+ next = changed ? mapped : v;
146
+ } else next = go(v, c, d);
147
+ if (next !== v) out = { ...out, [child.key]: next } as AstNode;
148
+ }
149
+ path.pop();
150
+ return isExpr(out) ? f(out, ctx) : out;
151
+ };
152
+ return go(root, null, 0) as T;
153
+ }
154
+
155
+ /** The tree with every `span` removed, for shape comparisons (the fixtures and
156
+ * the round-trip law compare stripped trees). A deep copy of plain data. */
157
+ export type Stripped<T> =
158
+ T extends readonly (infer U)[] ? Stripped<U>[]
159
+ : T extends object ? { [K in keyof T as K extends "span" ? never : K]: Stripped<T[K]> }
160
+ : T;
161
+
162
+ export function stripSpans<T>(node: T): Stripped<T> {
163
+ if (Array.isArray(node)) return node.map(stripSpans) as Stripped<T>;
164
+ if (node !== null && typeof node === "object") {
165
+ const out: Record<string, unknown> = {};
166
+ for (const [k, v] of Object.entries(node as Record<string, unknown>)) {
167
+ if (k === "span") continue;
168
+ out[k] = stripSpans(v);
169
+ }
170
+ return out as Stripped<T>;
171
+ }
172
+ return node as Stripped<T>;
173
+ }
174
+
175
+ /** The query as the JSON document both implementations exchange: the tree
176
+ * (already plain data) stamped with the language version it was produced under
177
+ * (`{ "oqx": "0.16", "kind": "query", … }`). */
178
+ export function toJSON(query: Query): QueryJson {
179
+ return { oqx: LANGUAGE_VERSION, ...query };
180
+ }