@omgbase/oqx 0.18.0 → 0.21.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 (57) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +177 -64
  3. package/dist/ast.d.ts +28 -5
  4. package/dist/ast.d.ts.map +1 -1
  5. package/dist/ast.js +1 -1
  6. package/dist/ast.js.map +1 -1
  7. package/dist/build.d.ts +11 -2
  8. package/dist/build.d.ts.map +1 -1
  9. package/dist/build.js +10 -3
  10. package/dist/build.js.map +1 -1
  11. package/dist/context.d.ts +19 -5
  12. package/dist/context.d.ts.map +1 -1
  13. package/dist/context.js +6 -9
  14. package/dist/context.js.map +1 -1
  15. package/dist/engine.d.ts +12 -1
  16. package/dist/engine.d.ts.map +1 -1
  17. package/dist/engine.js +263 -84
  18. package/dist/engine.js.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/lexer.d.ts +3 -3
  23. package/dist/lexer.d.ts.map +1 -1
  24. package/dist/lexer.js +79 -2
  25. package/dist/lexer.js.map +1 -1
  26. package/dist/optimize/analysis.d.ts +3 -2
  27. package/dist/optimize/analysis.d.ts.map +1 -1
  28. package/dist/optimize/analysis.js +18 -15
  29. package/dist/optimize/analysis.js.map +1 -1
  30. package/dist/parser.d.ts.map +1 -1
  31. package/dist/parser.js +250 -118
  32. package/dist/parser.js.map +1 -1
  33. package/dist/print.d.ts.map +1 -1
  34. package/dist/print.js +47 -29
  35. package/dist/print.js.map +1 -1
  36. package/dist/resolve.d.ts.map +1 -1
  37. package/dist/resolve.js +15 -8
  38. package/dist/resolve.js.map +1 -1
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/dist/walk.d.ts +5 -3
  42. package/dist/walk.d.ts.map +1 -1
  43. package/dist/walk.js +34 -11
  44. package/dist/walk.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/ast.ts +33 -6
  47. package/src/build.ts +12 -8
  48. package/src/context.ts +22 -11
  49. package/src/engine.ts +258 -91
  50. package/src/index.ts +2 -1
  51. package/src/lexer.ts +76 -6
  52. package/src/optimize/analysis.ts +17 -12
  53. package/src/parser.ts +227 -101
  54. package/src/print.ts +44 -16
  55. package/src/resolve.ts +15 -7
  56. package/src/version.ts +1 -1
  57. package/src/walk.ts +39 -14
package/src/print.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  // Single spaces, double-quoted strings with GRAMMAR §1 escapes, the minimal
3
3
  // parentheses the precedence table (GRAMMAR §4) needs, `select` written at the
4
4
  // top level, the connectives as the words `and` / `or` (since 0.18; `!` stays
5
- // `!`), every option in its canonical position. The law both spec runners
5
+ // `!`), every clause in its canonical position (a destination block's
6
+ // `frontier` and `depth` after its `where`, since 0.20). The law both spec runners
6
7
  // enforce over every fixture query:
7
8
  //
8
9
  // strip(parse(print(parse(q)))) ≡ strip(parse(q))
@@ -65,7 +66,7 @@ const OR = 1, AND = 2, CMP = 3, RANGE = 4, ADD = 5, MUL = 6, UNARY = 7, POSTFIX
65
66
 
66
67
  function prec(e: Expr): number {
67
68
  switch (e.kind) {
68
- case "lit": case "ident": case "outer": case "binding": return PRIMARY;
69
+ case "lit": case "ident": case "outer": case "binding": case "tuple": case "template": return PRIMARY;
69
70
  case "member": case "call": case "required": case "op": return POSTFIX;
70
71
  case "unary": return UNARY;
71
72
  case "binary": return e.op === "+" || e.op === "-" ? ADD : e.op === "*" || e.op === "/" || e.op === "%" ? MUL : CMP;
@@ -113,14 +114,18 @@ function query(q: Query): Out {
113
114
  return cat(expr(q.source), ` ${q.consumer}${q.distinct ? " distinct" : ""} `, braces(body(q)));
114
115
  }
115
116
 
116
- type Body = Pick<Subquery, "select" | "values" | "from" | "where" | "follow" | "orderBy" | "limit" | "offset">;
117
+ type Body = Pick<Subquery, "select" | "values" | "from" | "where" | "follow" | "orderBy" | "limit" | "offset">
118
+ & Partial<Pick<Subquery, "frontier" | "depth">>;
117
119
 
118
120
  // The clauses after `from` (a query) or after the projection (a block): the
119
- // fixed clause order, each present clause once.
121
+ // fixed clause order, each present clause once. `frontier`/`depth` exist only
122
+ // on a follow destination block's body (a `Query` has neither).
120
123
  function tail(b: Omit<Body, "select" | "values">): Out[] {
121
124
  const out: Out[] = [];
122
125
  for (const e of b.from) out.push(cat("from ", expr(e)));
123
126
  if (b.where) out.push(cat("where ", where(b.where)));
127
+ if (b.frontier) out.push(cat("frontier ", expr(b.frontier)));
128
+ if (b.depth != null) out.push([`depth ${number(b.depth)}`]);
124
129
  if (b.follow) out.push(follow(b.follow));
125
130
  if (b.orderBy && b.orderBy.length > 0) out.push(cat("order by ", join(b.orderBy.map(order), ", ")));
126
131
  if (b.limit) out.push(cat("limit ", expr(b.limit)));
@@ -170,12 +175,7 @@ function op(o: OpNode): Out {
170
175
 
171
176
  function follow(f: Follow): Out {
172
177
  const dests = join(f.destinations.map((d) => (d.kind === "op" ? op(d) : expr(d))), ", ");
173
- const opts: Out[] = [];
174
- if (f.where) opts.push(cat("where ", expr(f.where)));
175
- if (f.frontier) opts.push(cat("frontier ", expr(f.frontier)));
176
- if (f.depth !== null) opts.push([`depth ${f.depth}`]);
177
- if (f.by) opts.push(cat("by ", expr(f.by)));
178
- return cat(`follow${f.distinct ? " distinct" : ""} `, dests, opts.length > 0 ? cat(" { ", join(opts, " "), " }") : "");
178
+ return cat(`follow${f.distinct ? " distinct" : ""} `, dests);
179
179
  }
180
180
 
181
181
  // ---- where ------------------------------------------------------------------
@@ -229,6 +229,12 @@ function expr(e: Expr): Out {
229
229
  case "in": return cat(operand(e.left, CMP + 1), " in ", operand(e.right, CMP + 1));
230
230
  case "range":
231
231
  return cat(e.lo ? operand(e.lo, ADD) : "", e.exclusiveEnd ? "..." : "..", e.hi ? operand(e.hi, ADD) : "");
232
+ // A tuple is self-delimiting; its elements are full expressions.
233
+ case "tuple": return cat("(", join(e.items.map(expr), ", "), ")");
234
+ // A template literal prints as one: its text with the GRAMMAR §1 escapes
235
+ // (`` \` ``, `\\`, `\$` before a `{`, and the control characters), each
236
+ // interpolation as `${…}` around the full expression.
237
+ case "template": return cat("`", ...e.parts.map((p) => (typeof p === "string" ? templateText(p) : cat("${", expr(p), "}"))), "`");
232
238
  }
233
239
  }
234
240
 
@@ -262,13 +268,35 @@ function quote(s: string): string {
262
268
  for (const ch of s) {
263
269
  switch (ch) {
264
270
  case '"': out += '\\"'; break;
265
- case "\\": out += "\\\\"; break;
266
- case "\n": out += "\\n"; break;
267
- case "\t": out += "\\t"; break;
268
- case "\r": out += "\\r"; break;
269
- case "\0": out += "\\0"; break;
270
- default: out += ch;
271
+ default: out += escapeChar(ch);
271
272
  }
272
273
  }
273
274
  return out + '"';
274
275
  }
276
+
277
+ /** The text of a template literal part, escaped so it re-lexes to itself: the
278
+ * backtick and the backslash, the control characters as in a string, and a `$`
279
+ * that a `{` follows (which would otherwise open an interpolation). */
280
+ function templateText(s: string): string {
281
+ const cps = Array.from(s);
282
+ let out = "";
283
+ for (let i = 0; i < cps.length; i++) {
284
+ const ch = cps[i]!;
285
+ if (ch === "`") out += "\\`";
286
+ else if (ch === "$" && cps[i + 1] === "{") out += "\\$";
287
+ else out += escapeChar(ch);
288
+ }
289
+ return out;
290
+ }
291
+
292
+ /** The escapes a string literal and a template literal share (GRAMMAR §1). */
293
+ function escapeChar(ch: string): string {
294
+ switch (ch) {
295
+ case "\\": return "\\\\";
296
+ case "\n": return "\\n";
297
+ case "\t": return "\\t";
298
+ case "\r": return "\\r";
299
+ case "\0": return "\\0";
300
+ default: return ch;
301
+ }
302
+ }
package/src/resolve.ts CHANGED
@@ -61,12 +61,16 @@ function resolveBody<T extends Query | Subquery>(node: T, fail: Fail): T {
61
61
  });
62
62
  const limit = node.limit === null ? null : resolveExpr(node.limit, fail);
63
63
  const offset = node.offset === null ? null : resolveExpr(node.offset, fail);
64
+ // `frontier` (0.20) is not rewritten against the select: a destination
65
+ // block's reads the occurrence, a body's reads the seed row — not a projected
66
+ // record (like `order by`).
67
+ const frontier = node.frontier === null ? null : resolveExpr(node.frontier, fail);
64
68
  const source = node.kind === "query" ? resolveExpr(node.source, fail) : null;
65
69
  const same = where2 === node.where && select === node.select && follow === node.follow && from === node.from
66
- && orderBy === node.orderBy && limit === node.limit && offset === node.offset
70
+ && orderBy === node.orderBy && limit === node.limit && offset === node.offset && frontier === node.frontier
67
71
  && (node.kind !== "query" || source === node.source);
68
72
  if (same) return node;
69
- const out = { ...node, where: where2, select, follow, from, orderBy, limit, offset };
73
+ const out = { ...node, where: where2, select, follow, frontier, from, orderBy, limit, offset };
70
74
  if (node.kind === "query") (out as Query).source = source!;
71
75
  return out;
72
76
  }
@@ -79,11 +83,7 @@ function resolveOp(op: OpNode, fail: Fail): OpNode {
79
83
 
80
84
  function resolveFollow(f: Follow, fail: Fail): Follow {
81
85
  const destinations = mapSame(f.destinations, (d: FollowDestination) => (d.kind === "op" ? resolveOp(d, fail) : resolveExpr(d, fail)));
82
- const where = f.where === null ? null : resolveExpr(f.where, fail);
83
- const frontier = f.frontier === null ? null : resolveExpr(f.frontier, fail);
84
- const by = f.by === null ? null : resolveExpr(f.by, fail);
85
- if (destinations === f.destinations && where === f.where && frontier === f.frontier && by === f.by) return f;
86
- return { ...f, destinations, where, frontier, by };
86
+ return destinations === f.destinations ? f : { ...f, destinations };
87
87
  }
88
88
 
89
89
  // Nested blocks inside a where tree are their own scopes: resolve each against
@@ -137,6 +137,14 @@ function mapExpr(e: Expr, f: (e: Expr) => Expr | null): Expr {
137
137
  const hi = e.hi ? go(e.hi) : null;
138
138
  return lo === e.lo && hi === e.hi ? e : { ...e, lo, hi };
139
139
  }
140
+ case "tuple": {
141
+ const items = mapSame(e.items, go);
142
+ return items === e.items ? e : { ...e, items };
143
+ }
144
+ case "template": { // the text parts are strings; only the interpolations are expressions
145
+ const parts = mapSame(e.parts, (p) => (typeof p === "string" ? p : go(p)));
146
+ return parts === e.parts ? e : { ...e, parts };
147
+ }
140
148
  case "op": { // the receiver is read in this scope; the block is its own scope
141
149
  const receiver = go(e.receiver);
142
150
  return receiver === e.receiver ? e : { ...e, receiver };
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.18";
4
+ export const LANGUAGE_VERSION = "0.21";
package/src/walk.ts CHANGED
@@ -14,13 +14,15 @@ import { LANGUAGE_VERSION } from "./version.ts";
14
14
  * The root node has no clause (`null`). */
15
15
  export type Clause =
16
16
  | "source" | "from" | "where" | "select" | "orderBy" | "limit" | "offset"
17
- | "follow" | "follow.destination" | "follow.where" | "follow.frontier" | "follow.by";
17
+ | "follow" | "follow.destination" | "follow.frontier";
18
18
 
19
19
  /** Where a visited node sits. `depth` is the scope depth at which the node is
20
20
  * evaluated (0 = the root scope: a top-level `source`, `limit`, `offset`; 1 =
21
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). */
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). */
24
26
  export interface VisitContext {
25
27
  /** The ancestors of the node, root first (empty for the root). */
26
28
  readonly path: readonly AstNode[];
@@ -52,6 +54,8 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
52
54
  { key: "source", clause: "source" },
53
55
  { key: "from", clause: "from", depth: 1 },
54
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 },
55
59
  { key: "follow", clause: "follow", depth: 1 },
56
60
  { key: "orderBy", clause: "orderBy", depth: 1 },
57
61
  { key: "limit", clause: "limit" },
@@ -61,18 +65,18 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
61
65
  { key: "select", clause: "select" },
62
66
  { key: "from", clause: "from" },
63
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 },
64
73
  { key: "follow", clause: "follow" },
65
74
  { key: "orderBy", clause: "orderBy" },
66
75
  { key: "limit", clause: "limit" },
67
76
  { key: "offset", clause: "offset" },
68
77
  ],
69
78
  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
- ],
79
+ follow: [{ key: "destinations", clause: "follow.destination" }],
76
80
  order: [{ key: "expr" }],
77
81
  field: [{ key: "expr" }],
78
82
  collect: [{ key: "op" }],
@@ -92,14 +96,35 @@ export const CHILDREN: { readonly [K in AstKind]: readonly ChildKey[] } = {
92
96
  logical: [{ key: "left" }, { key: "right" }],
93
97
  in: [{ key: "left" }, { key: "right" }],
94
98
  range: [{ key: "lo" }, { key: "hi" }],
99
+ tuple: [{ key: "items" }],
100
+ // The interpolations; the text parts between them are strings, not nodes.
101
+ template: [{ key: "parts" }],
95
102
  };
96
103
 
97
- type Slot = AstNode | AstNode[] | null;
104
+ /** A child slot's value: a node, a list of nodes (a template's `parts` also
105
+ * holds its text strings, which the walks skip), or `null`. */
106
+ type Slot = AstNode | (AstNode | string)[] | null;
98
107
 
99
108
  function slot(node: AstNode, key: string): Slot {
100
109
  return (node as unknown as Record<string, Slot>)[key] ?? null;
101
110
  }
102
111
 
112
+ /** A block is a follow destination block when its `op` is a direct destination
113
+ * of a `follow` node. `path` is the ancestors with the block itself last. */
114
+ function isDestinationBlock(path: readonly AstNode[]): boolean {
115
+ const op = path[path.length - 2];
116
+ const owner = path[path.length - 3];
117
+ return op?.kind === "op" && owner?.kind === "follow";
118
+ }
119
+
120
+ /** How much deeper a child's scope is: the table's value, except a block's
121
+ * `frontier`, which is one level OUT only for a destination block (the walk's
122
+ * enclosing scope) and the row scope itself for a body with its own `follow`. */
123
+ function childDepth(node: AstNode, child: ChildKey, path: readonly AstNode[]): number {
124
+ if (node.kind === "subquery" && child.key === "frontier" && !isDestinationBlock(path)) return 0;
125
+ return child.depth ?? 0;
126
+ }
127
+
103
128
  /** Walk a tree depth-first in canonical source order. */
104
129
  export function visit(root: AstNode, visitor: Visitor): void {
105
130
  const path: AstNode[] = [];
@@ -112,8 +137,8 @@ export function visit(root: AstNode, visitor: Visitor): void {
112
137
  const v = slot(node, child.key);
113
138
  if (v === null) continue;
114
139
  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);
140
+ const d = depth + childDepth(node, child, path);
141
+ if (Array.isArray(v)) { for (const x of v) if (typeof x !== "string") go(x, c, d); }
117
142
  else go(v, c, d);
118
143
  }
119
144
  path.pop();
@@ -137,11 +162,11 @@ export function transform<T extends Query | Subquery>(root: T, f: (expr: Expr, c
137
162
  const v = slot(node, child.key);
138
163
  if (v === null) continue;
139
164
  const c = child.clause ?? clause;
140
- const d = depth + (child.depth ?? 0);
165
+ const d = depth + childDepth(node, child, path);
141
166
  let next: Slot;
142
167
  if (Array.isArray(v)) {
143
168
  let changed = false;
144
- const mapped = v.map((x) => { const y = go(x, c, d); if (y !== x) changed = true; return y; });
169
+ const mapped = v.map((x) => { if (typeof x === "string") return x; const y = go(x, c, d); if (y !== x) changed = true; return y; });
145
170
  next = changed ? mapped : v;
146
171
  } else next = go(v, c, d);
147
172
  if (next !== v) out = { ...out, [child.key]: next } as AstNode;