@omgbase/oqx 0.14.1 → 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.
- package/CHANGELOG.md +41 -0
- package/README.md +192 -34
- package/dist/ast.d.ts +77 -23
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +21 -6
- package/dist/ast.js.map +1 -1
- package/dist/build.d.ts +37 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +54 -0
- package/dist/build.js.map +1 -0
- package/dist/context.d.ts +13 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +7 -3
- package/dist/context.js.map +1 -1
- package/dist/engine.d.ts +1 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +55 -26
- package/dist/engine.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +19 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -12
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts +15 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +104 -51
- package/dist/lexer.js.map +1 -1
- package/dist/optimize/analysis.d.ts +1 -1
- package/dist/optimize/analysis.d.ts.map +1 -1
- package/dist/optimize/analysis.js +7 -5
- package/dist/optimize/analysis.js.map +1 -1
- package/dist/optimize/ir.d.ts +3 -3
- package/dist/optimize/ir.d.ts.map +1 -1
- package/dist/optimize/ir.js +3 -3
- package/dist/optimize/ir.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +616 -251
- package/dist/parser.js.map +1 -1
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +5 -3
- package/dist/plan.js.map +1 -1
- package/dist/print.d.ts +17 -0
- package/dist/print.d.ts.map +1 -0
- package/dist/print.js +266 -0
- package/dist/print.js.map +1 -0
- package/dist/resolve.d.ts +23 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +330 -0
- package/dist/resolve.js.map +1 -0
- package/dist/semantics.js +1 -1
- package/dist/semantics.js.map +1 -1
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/walk.d.ts +54 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +139 -0
- package/dist/walk.js.map +1 -0
- package/package.json +1 -1
- package/src/ast.ts +105 -36
- package/src/build.ts +72 -0
- package/src/context.ts +16 -3
- package/src/engine.ts +57 -28
- package/src/errors.ts +3 -2
- package/src/index.ts +38 -13
- package/src/lexer.ts +102 -51
- package/src/optimize/analysis.ts +6 -5
- package/src/optimize/ir.ts +5 -5
- package/src/parser.ts +573 -225
- package/src/plan.ts +4 -3
- package/src/print.ts +274 -0
- package/src/resolve.ts +305 -0
- package/src/semantics.ts +1 -1
- package/src/version.ts +4 -0
- package/src/walk.ts +180 -0
package/dist/walk.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"walk.js","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/ast.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
// OQX
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// The OQX AST — a first-class, language-level contract (spec/oqx/AST.md, since
|
|
2
|
+
// language 0.16). The parser produces it, the interpreter walks it, and tools
|
|
3
|
+
// reflect on it without re-parsing: every node carries `kind` and `span`, every
|
|
4
|
+
// optional field is materialized (`null` / `false`), and the in-memory objects
|
|
5
|
+
// ARE the JSON shape both implementations serialize (plain data, no parent
|
|
6
|
+
// pointers, no methods). There is no separate lowering/IR pass: a "relation" is
|
|
7
|
+
// just an expression evaluated against the current row.
|
|
8
|
+
//
|
|
9
|
+
// `where` keeps its surface form: a `select` alias referenced in `where` stays a
|
|
10
|
+
// bare identifier here; `resolveAliases` (resolve.ts) performs the substitution
|
|
11
|
+
// before evaluation. Nodes built by a tool rather than the parser carry the
|
|
12
|
+
// empty span `[0, 0]` (see `build.ts`).
|
|
13
|
+
|
|
14
|
+
/** A half-open source range `[start, end)` in Unicode code points over the raw
|
|
15
|
+
* source (`rawSource` for a template, where a binding occupies its `${n}`
|
|
16
|
+
* marker). The empty span `[0, 0]` marks a node that did not come from source. */
|
|
17
|
+
export type Span = [start: number, end: number];
|
|
6
18
|
|
|
7
19
|
/** Query consumers — how a (sub)query's row set is shaped. `none` is the
|
|
8
20
|
* zero-cardinality complement of `exists` (true iff the block yields no rows). */
|
|
@@ -11,50 +23,80 @@ export type Consumer = "collect" | "exists" | "none" | "count" | "first" | "sing
|
|
|
11
23
|
/** Comparison operators usable in a `count { … } <op> <int>` test. */
|
|
12
24
|
export type RelOp = "==" | "!=" | "<" | "<=" | ">" | ">=";
|
|
13
25
|
|
|
26
|
+
/** The identity operators (SEMANTICS §5b, since 0.17). */
|
|
27
|
+
export type IdentityOp = "is" | "is not";
|
|
28
|
+
|
|
29
|
+
/** The operator of a `binary` expression: a comparison, an identity test, or arithmetic. */
|
|
30
|
+
export type BinaryOp = RelOp | IdentityOp | "+" | "-" | "*" | "/" | "%";
|
|
31
|
+
|
|
32
|
+
/** The operator of a `unary` expression. */
|
|
33
|
+
export type UnaryOp = "!" | "-";
|
|
34
|
+
|
|
35
|
+
/** The operator of a `logical` expression. */
|
|
36
|
+
export type LogicalOp = "&&" | "||";
|
|
37
|
+
|
|
14
38
|
/** Scalar value/predicate expression, evaluated against a row scope + bindings. */
|
|
15
39
|
export type Expr =
|
|
16
|
-
| { kind: "lit"; value: string | number | boolean | null }
|
|
17
|
-
| { kind: "ident"; name: string } // bare property of the CURRENT row/scope only (never climbs); `$
|
|
18
|
-
| { kind: "outer"; levels: number; name: string } // `^name` — read from exactly `levels` scopes out
|
|
19
|
-
| { kind: "binding"; index: number } // a ${…} interpolated host value
|
|
20
|
-
| { kind: "member"; recv: Expr; name: string } // .prop navigation on the value to its left
|
|
21
|
-
| { kind: "
|
|
22
|
-
| { kind: "
|
|
23
|
-
|
|
24
|
-
| { kind: "
|
|
25
|
-
| { kind: "
|
|
26
|
-
| { kind: "
|
|
40
|
+
| { kind: "lit"; span: Span; value: string | number | boolean | null }
|
|
41
|
+
| { kind: "ident"; span: Span; name: string } // bare property of the CURRENT row/scope only (never climbs); `$it` is the row itself
|
|
42
|
+
| { kind: "outer"; span: Span; levels: number; name: string } // `^name` — read from exactly `levels` scopes out (also what `N^name` parses to, since 0.18)
|
|
43
|
+
| { kind: "binding"; span: Span; index: number } // a ${…} interpolated host value
|
|
44
|
+
| { kind: "member"; span: Span; recv: Expr; name: string } // .prop navigation on the value to its left
|
|
45
|
+
| { kind: "call"; span: Span; recv: Expr | null; name: string; args: Expr[] } // fn / method
|
|
46
|
+
| { kind: "unary"; span: Span; op: UnaryOp; expr: Expr }
|
|
47
|
+
// `x!` — the value of `expr`, or an eval error when it is absent (since 0.17).
|
|
48
|
+
| { kind: "required"; span: Span; expr: Expr }
|
|
49
|
+
| { kind: "binary"; span: Span; op: BinaryOp; left: Expr; right: Expr } // arithmetic + comparison + identity
|
|
50
|
+
| { kind: "logical"; span: Span; op: LogicalOp; left: Expr; right: Expr }
|
|
51
|
+
| { kind: "in"; span: Span; left: Expr; right: Expr }
|
|
27
52
|
// A Ruby-style range value. `lo`/`hi` are null for the open-ended forms
|
|
28
53
|
// (`..5` / `5..`); `exclusiveEnd` distinguishes `1...5` from `1..5`. Evaluates
|
|
29
54
|
// to a runtime range value (see semantics.makeRange); primarily the RHS of `in`.
|
|
30
|
-
| { kind: "range"; lo: Expr | null; hi: Expr | null; exclusiveEnd: boolean }
|
|
55
|
+
| { kind: "range"; span: Span; lo: Expr | null; hi: Expr | null; exclusiveEnd: boolean }
|
|
56
|
+
// A value-position directive (since 0.17): `recv first { … }.pay`, and what a
|
|
57
|
+
// bracket lookup `x[…]` desugars to. `isExpr` is false for it — an op is an
|
|
58
|
+
// expression only by position; discriminate on `kind === "op"`.
|
|
59
|
+
| OpNode;
|
|
31
60
|
|
|
61
|
+
/** One `order by` term. */
|
|
32
62
|
export interface OrderSpec {
|
|
63
|
+
kind: "order";
|
|
64
|
+
span: Span;
|
|
33
65
|
expr: Expr;
|
|
34
66
|
desc: boolean;
|
|
35
67
|
}
|
|
36
68
|
|
|
37
69
|
/** One projection item: a named scalar/navigation value, or a named nested
|
|
38
|
-
* collection consumer. `lift` marks a `^name`
|
|
70
|
+
* collection consumer. `lift` marks a `^name` lift. */
|
|
39
71
|
export type SelectItem =
|
|
40
72
|
// `lift` is the number of `^` carets: 0 = an ordinary projection, N = a lift
|
|
41
|
-
// that binds this value N scopes out (see the engine's flatten-append).
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
// that binds this value N scopes out (see the engine's flatten-append). `name`
|
|
74
|
+
// is `""` for an unaliased non-navigation item, which the parser only admits
|
|
75
|
+
// under `values`.
|
|
76
|
+
| { kind: "field"; span: Span; name: string; expr: Expr; lift: number }
|
|
77
|
+
| { kind: "collect"; span: Span; name: string; op: OpNode };
|
|
78
|
+
|
|
79
|
+
/** The `count { … } <op> <int>` test attached to an `OpNode` — an attribute of
|
|
80
|
+
* the directive, not a node of its own. */
|
|
81
|
+
export interface CountCmp {
|
|
82
|
+
op: RelOp;
|
|
83
|
+
value: number;
|
|
84
|
+
}
|
|
44
85
|
|
|
45
86
|
/** A postfix consumer directive over a receiver collection:
|
|
46
87
|
* `<receiver> <op> { <sub> }`, optionally `count { … } <relop> <int>`. */
|
|
47
88
|
export interface OpNode {
|
|
48
89
|
kind: "op";
|
|
90
|
+
span: Span;
|
|
49
91
|
receiver: Expr;
|
|
50
92
|
op: Consumer;
|
|
51
93
|
sub: Subquery;
|
|
52
|
-
countCmp
|
|
94
|
+
countCmp: CountCmp | null;
|
|
53
95
|
/** `distinct` — dedup the rows this directive consumes by their projected
|
|
54
96
|
* value (the `select`), so `count distinct { … }` counts distinct projections
|
|
55
97
|
* and `collect distinct { … }` yields distinct rows. Empty select ⇒ dedup by
|
|
56
98
|
* row identity. Spellable as `<op> distinct { … }` or `{ select distinct … }`. */
|
|
57
|
-
distinct
|
|
99
|
+
distinct: boolean;
|
|
58
100
|
}
|
|
59
101
|
|
|
60
102
|
/** One `follow` destination: a relation of the current row (an `Expr` receiver), or a
|
|
@@ -64,6 +106,8 @@ export type FollowDestination = Expr | OpNode;
|
|
|
64
106
|
|
|
65
107
|
/** The recursive `follow` clause. */
|
|
66
108
|
export interface Follow {
|
|
109
|
+
kind: "follow";
|
|
110
|
+
span: Span;
|
|
67
111
|
destinations: FollowDestination[]; // ≥ 1, in source order; the walk is their union
|
|
68
112
|
distinct: boolean;
|
|
69
113
|
where: Expr | null; // successor predicate: which successors keep participating
|
|
@@ -72,8 +116,10 @@ export interface Follow {
|
|
|
72
116
|
by: Expr | null; // identity expression for cycle detection / dedup
|
|
73
117
|
}
|
|
74
118
|
|
|
75
|
-
/** A nested (sub)query body. */
|
|
119
|
+
/** A nested (sub)query body — the `{ … }` of a directive. */
|
|
76
120
|
export interface Subquery {
|
|
121
|
+
kind: "subquery";
|
|
122
|
+
span: Span;
|
|
77
123
|
from: Expr[]; // body-level `from E` re-projections (flatMap chain)
|
|
78
124
|
where: Where | null;
|
|
79
125
|
select: SelectItem[];
|
|
@@ -81,29 +127,31 @@ export interface Subquery {
|
|
|
81
127
|
follow: Follow | null;
|
|
82
128
|
/** `values` — scalar projection mode: the (single) projected expression is the
|
|
83
129
|
* row's result itself rather than being wrapped in a `{ name: value }` record,
|
|
84
|
-
* so `name values` yields `["Bob", …]` and `$
|
|
85
|
-
values
|
|
130
|
+
* so `name values` yields `["Bob", …]` and `$it values` yields the rows. */
|
|
131
|
+
values: boolean;
|
|
86
132
|
/** `limit N` / `offset N` — bound the row set AFTER where/order/distinct and
|
|
87
133
|
* BEFORE the consumer reduces it, so `count { … limit 5 }` is at most 5 and
|
|
88
134
|
* `first { … offset 1 }` is the second row. Each is a value expression
|
|
89
135
|
* (a literal, a `${…}` binding, or an outer reference) read as part of the
|
|
90
136
|
* block — `^n` is the enclosing row's field, as everywhere inside `{ … }` —
|
|
91
137
|
* and must yield a non-negative integer. */
|
|
92
|
-
limit
|
|
93
|
-
offset
|
|
138
|
+
limit: Expr | null;
|
|
139
|
+
offset: Expr | null;
|
|
94
140
|
}
|
|
95
141
|
|
|
96
142
|
/** The where-clause boolean tree: OQX owns &&/||/!/grouping so consumer ops
|
|
97
143
|
* (invisible to the scalar evaluator) compose with scalar predicates. */
|
|
98
144
|
export type Where =
|
|
99
|
-
| { kind: "and"; parts: Where[] }
|
|
100
|
-
| { kind: "or"; parts: Where[] }
|
|
101
|
-
| { kind: "not"; expr: Where }
|
|
102
|
-
| { kind: "scalar"; expr: Expr }
|
|
145
|
+
| { kind: "and"; span: Span; parts: Where[] }
|
|
146
|
+
| { kind: "or"; span: Span; parts: Where[] }
|
|
147
|
+
| { kind: "not"; span: Span; expr: Where }
|
|
148
|
+
| { kind: "scalar"; span: Span; expr: Expr }
|
|
103
149
|
| OpNode;
|
|
104
150
|
|
|
105
151
|
/** A top-level OQX query. */
|
|
106
152
|
export interface Query {
|
|
153
|
+
kind: "query";
|
|
154
|
+
span: Span;
|
|
107
155
|
source: Expr; // the root collection (`from <source>` or the directive receiver)
|
|
108
156
|
from: Expr[]; // further top-level `from E` re-projections
|
|
109
157
|
where: Where | null;
|
|
@@ -112,10 +160,31 @@ export interface Query {
|
|
|
112
160
|
consumer: Consumer;
|
|
113
161
|
follow: Follow | null;
|
|
114
162
|
/** `distinct` — dedup the result rows by their projected value (see OpNode). */
|
|
115
|
-
distinct
|
|
163
|
+
distinct: boolean;
|
|
116
164
|
/** `values` — scalar projection mode (see Subquery). */
|
|
117
|
-
values
|
|
165
|
+
values: boolean;
|
|
118
166
|
/** `limit N` / `offset N` (see Subquery); evaluated at the root scope. */
|
|
119
|
-
limit
|
|
120
|
-
offset
|
|
167
|
+
limit: Expr | null;
|
|
168
|
+
offset: Expr | null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Every node of the tree. `AstNode["kind"]` is the closed set of node kinds. */
|
|
172
|
+
export type AstNode = Query | Subquery | OpNode | Follow | OrderSpec | SelectItem | Where | Expr;
|
|
173
|
+
|
|
174
|
+
/** The closed set of node kinds. */
|
|
175
|
+
export type AstKind = AstNode["kind"];
|
|
176
|
+
|
|
177
|
+
/** The expression kinds (`Expr["kind"]`), for `isExpr`. */
|
|
178
|
+
const EXPR_KINDS: ReadonlySet<string> = new Set([
|
|
179
|
+
"lit", "ident", "outer", "binding", "member", "call", "unary", "required", "binary", "logical", "in", "range",
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
/** Whether a node is a scalar `Expr` (an `op` in expression position is not: it
|
|
183
|
+
* is an expression only by position, see AST.md §1). */
|
|
184
|
+
export function isExpr(node: AstNode): node is Expr {
|
|
185
|
+
return EXPR_KINDS.has(node.kind);
|
|
121
186
|
}
|
|
187
|
+
|
|
188
|
+
/** The parsed query's JSON form (`toJSON`): the tree with the language version
|
|
189
|
+
* it was produced under. */
|
|
190
|
+
export type QueryJson = Query & { oqx: string };
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Builders for AST nodes a tool constructs itself (spec/oqx/AST.md §7). Every
|
|
2
|
+
// node they make carries the empty span `[0, 0]` — "not from source" — and the
|
|
3
|
+
// materialized defaults (`distinct: false`, `limit: null`, …), so a hand-built
|
|
4
|
+
// tree has the same shape as a parsed one and prints with `print`.
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
BinaryOp, Consumer, CountCmp, Expr, Follow, FollowDestination, LogicalOp, OpNode, OrderSpec, Query, SelectItem, Span,
|
|
8
|
+
Subquery, UnaryOp, Where,
|
|
9
|
+
} from "./ast.ts";
|
|
10
|
+
|
|
11
|
+
/** A fresh empty span (never shared: spans are mutable tuples). */
|
|
12
|
+
export const span = (): Span => [0, 0];
|
|
13
|
+
|
|
14
|
+
export const lit = (value: string | number | boolean | null): Expr => ({ kind: "lit", span: span(), value });
|
|
15
|
+
export const ident = (name: string): Expr => ({ kind: "ident", span: span(), name });
|
|
16
|
+
export const outer = (levels: number, name: string): Expr => ({ kind: "outer", span: span(), levels, name });
|
|
17
|
+
export const binding = (index: number): Expr => ({ kind: "binding", span: span(), index });
|
|
18
|
+
export const member = (recv: Expr, name: string): Expr => ({ kind: "member", span: span(), recv, name });
|
|
19
|
+
/** `a.b.c` from a dotted path. */
|
|
20
|
+
export const path = (first: string, ...rest: string[]): Expr => rest.reduce<Expr>((e, n) => member(e, n), ident(first));
|
|
21
|
+
export const call = (recv: Expr | null, name: string, args: Expr[] = []): Expr => ({ kind: "call", span: span(), recv, name, args });
|
|
22
|
+
export const unary = (op: UnaryOp, expr: Expr): Expr => ({ kind: "unary", span: span(), op, expr });
|
|
23
|
+
/** `expr!` — the required value (SEMANTICS §5b). */
|
|
24
|
+
export const required = (expr: Expr): Expr => ({ kind: "required", span: span(), expr });
|
|
25
|
+
export const binary = (op: BinaryOp, left: Expr, right: Expr): Expr => ({ kind: "binary", span: span(), op, left, right });
|
|
26
|
+
export const logical = (op: LogicalOp, left: Expr, right: Expr): Expr => ({ kind: "logical", span: span(), op, left, right });
|
|
27
|
+
export const inOp = (left: Expr, right: Expr): Expr => ({ kind: "in", span: span(), left, right });
|
|
28
|
+
export const range = (lo: Expr | null, hi: Expr | null, exclusiveEnd = false): Expr => ({ kind: "range", span: span(), lo, hi, exclusiveEnd });
|
|
29
|
+
|
|
30
|
+
export const scalar = (expr: Expr): Where => ({ kind: "scalar", span: span(), expr });
|
|
31
|
+
export const and = (parts: Where[]): Where => ({ kind: "and", span: span(), parts });
|
|
32
|
+
export const or = (parts: Where[]): Where => ({ kind: "or", span: span(), parts });
|
|
33
|
+
export const not = (expr: Where): Where => ({ kind: "not", span: span(), expr });
|
|
34
|
+
|
|
35
|
+
export const field = (name: string, expr: Expr, lift = 0): SelectItem => ({ kind: "field", span: span(), name, expr, lift });
|
|
36
|
+
export const collect = (name: string, op: OpNode): SelectItem => ({ kind: "collect", span: span(), name, op });
|
|
37
|
+
export const order = (expr: Expr, desc = false): OrderSpec => ({ kind: "order", span: span(), expr, desc });
|
|
38
|
+
|
|
39
|
+
/** A block body; every clause defaults to empty/absent. */
|
|
40
|
+
export function subquery(parts: Partial<Omit<Subquery, "kind" | "span">> = {}): Subquery {
|
|
41
|
+
return {
|
|
42
|
+
kind: "subquery", span: span(),
|
|
43
|
+
from: [], where: null, select: [], orderBy: null, follow: null, values: false, limit: null, offset: null,
|
|
44
|
+
...parts,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `<receiver> <op> [distinct] { sub } [<relop> N]`. */
|
|
49
|
+
export function op(
|
|
50
|
+
receiver: Expr, consumer: Consumer, sub: Subquery = subquery(),
|
|
51
|
+
opts: { distinct?: boolean; countCmp?: CountCmp | null } = {},
|
|
52
|
+
): OpNode {
|
|
53
|
+
return { kind: "op", span: span(), receiver, op: consumer, sub, countCmp: opts.countCmp ?? null, distinct: opts.distinct ?? false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function follow(
|
|
57
|
+
destinations: FollowDestination[],
|
|
58
|
+
opts: Partial<Omit<Follow, "kind" | "span" | "destinations">> = {},
|
|
59
|
+
): Follow {
|
|
60
|
+
return { kind: "follow", span: span(), destinations, distinct: false, where: null, frontier: null, depth: null, by: null, ...opts };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A top-level query over `source`; every other clause defaults to empty/absent
|
|
64
|
+
* and the consumer to `collect`. */
|
|
65
|
+
export function query(source: Expr, parts: Partial<Omit<Query, "kind" | "span" | "source">> = {}): Query {
|
|
66
|
+
return {
|
|
67
|
+
kind: "query", span: span(), source,
|
|
68
|
+
from: [], where: null, select: [], orderBy: null, consumer: "collect", follow: null,
|
|
69
|
+
distinct: false, values: false, limit: null, offset: null,
|
|
70
|
+
...parts,
|
|
71
|
+
};
|
|
72
|
+
}
|
package/src/context.ts
CHANGED
|
@@ -28,6 +28,14 @@ export interface CallResult {
|
|
|
28
28
|
export interface DataContext {
|
|
29
29
|
/** Resolve a named root collection (the `from <name>` / directive receiver). */
|
|
30
30
|
root(name: string): unknown;
|
|
31
|
+
/** The root scope's row — the host's root object: what `$it` is at the root
|
|
32
|
+
* scope and `^$it` from a top-level row (SEMANTICS §2, since 0.18), so
|
|
33
|
+
* `^$it.people` navigates it through `get` and `entries(^$it)` enumerates it.
|
|
34
|
+
* Bare names at the root still resolve through `root(name)`, so a host with
|
|
35
|
+
* lazy roots is unaffected. Optional: a context without it (or returning
|
|
36
|
+
* `undefined`) has no root row — `$it` at the root is absent, the pre-0.18
|
|
37
|
+
* behavior. `DefaultContext` returns its roots record. */
|
|
38
|
+
rootObject?(): unknown;
|
|
31
39
|
/** Read a property/relation off a row: a bare identifier (`field`), a
|
|
32
40
|
* `.field` segment, or a `^field` outer reference all come through here, each
|
|
33
41
|
* against exactly the row of the scope it names. An absent property is
|
|
@@ -82,9 +90,10 @@ export interface DefaultContextOptions {
|
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
/** The default context: ordinary JavaScript objects. Named roots come from a
|
|
85
|
-
* plain `{ name: collection }` map
|
|
86
|
-
*
|
|
87
|
-
*
|
|
93
|
+
* plain `{ name: collection }` map, which is also the root object (`^$it` from
|
|
94
|
+
* a top-level row); properties are OWN keys only (an array exposes only its
|
|
95
|
+
* integer indices, a primitive has none); identity is `.id` when present, else
|
|
96
|
+
* the row itself (the engine keys it structurally). */
|
|
88
97
|
export class DefaultContext implements DataContext {
|
|
89
98
|
private roots: Record<string, unknown>;
|
|
90
99
|
readonly regexDialect: RegexDialect;
|
|
@@ -98,6 +107,10 @@ export class DefaultContext implements DataContext {
|
|
|
98
107
|
return Object.hasOwn(this.roots, name) ? this.roots[name] : undefined;
|
|
99
108
|
}
|
|
100
109
|
|
|
110
|
+
rootObject(): unknown {
|
|
111
|
+
return this.roots;
|
|
112
|
+
}
|
|
113
|
+
|
|
101
114
|
get(row: unknown, key: string): unknown {
|
|
102
115
|
if (row == null || typeof row !== "object") return undefined;
|
|
103
116
|
if (Array.isArray(row)) return isIndexKey(key) ? row[Number(key)] : undefined;
|
package/src/engine.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
relate, arith, membership, truthy, compareForSort, compare, canonicalKey, makeRange, isEntry, isRange,
|
|
30
30
|
} from "./semantics.ts";
|
|
31
31
|
import type { BlockPlan, Correlation, Rule, RowIndex } from "./optimize/index.ts";
|
|
32
|
+
import { printTemplate } from "./print.ts";
|
|
32
33
|
import { DEFAULT_RULES, HashIndex, intersectPositions, lookupOrder, planFor, residualWithout } from "./optimize/index.ts";
|
|
33
34
|
|
|
34
35
|
/** The shaped result of a top-level query, discriminated by consumer. */
|
|
@@ -46,7 +47,9 @@ export interface Engine {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
// One query scope: the row under evaluation plus the chain of enclosing scopes
|
|
49
|
-
// that `^` walks. The root scope (parent === null)
|
|
50
|
+
// that `^` walks. The root scope (parent === null) is a row too: its row is the
|
|
51
|
+
// host's root object (`DataContext.rootObject`, since 0.18 — what `$it` is
|
|
52
|
+
// there and `^$it` from a top-level row), while its bare names are the
|
|
50
53
|
// context's named roots. `lifts` holds values bound INTO this scope by `^name:`
|
|
51
54
|
// items in nested blocks; `meta` holds the scope's intrinsics: recursion
|
|
52
55
|
// metadata for a follow occurrence, and `$key` for an entry scope (a row that
|
|
@@ -140,7 +143,7 @@ export class InMemoryEngine implements Engine {
|
|
|
140
143
|
memo: new Map(),
|
|
141
144
|
};
|
|
142
145
|
if (this.trace) run.trace = this.trace;
|
|
143
|
-
const root: Scope = { row:
|
|
146
|
+
const root: Scope = { row: this.ctx.rootObject?.(), parent: null, bindings, run };
|
|
144
147
|
const rows = this.reproject(this.rowsOf(this.evalRaw(query.source, root)), query.from, root);
|
|
145
148
|
|
|
146
149
|
const bound = this.boundOf(query, root);
|
|
@@ -185,12 +188,16 @@ export class InMemoryEngine implements Engine {
|
|
|
185
188
|
// Evaluate a block's `limit`/`offset`. The bound is part of the block, so it
|
|
186
189
|
// is read in a row-less scope INSIDE it: a bare name is absent (there is no
|
|
187
190
|
// current item yet), `^name` is the enclosing row — exactly as in the block's
|
|
188
|
-
// body — and literals/bindings are themselves. For a top-level query
|
|
189
|
-
// is the root
|
|
191
|
+
// body — and literals/bindings are themselves. For a top-level query the
|
|
192
|
+
// scope is the root scope WITHOUT its row (SEMANTICS §18): bare names are
|
|
193
|
+
// still the named roots, but `$it` — reached through a bracket inside the
|
|
194
|
+
// bound — is absent, not the root object. Each must be a non-negative integer.
|
|
190
195
|
private boundOf(b: Pick<Subquery, "limit" | "offset">, enclosing: Scope): Bound {
|
|
191
196
|
if (!b.limit && !b.offset) return UNBOUNDED;
|
|
192
|
-
const scope: Scope = enclosing.parent === null
|
|
193
|
-
|
|
197
|
+
const scope: Scope = enclosing.parent === null
|
|
198
|
+
? { row: undefined, parent: null, bindings: enclosing.bindings, run: enclosing.run }
|
|
199
|
+
: rowless(enclosing);
|
|
200
|
+
const read = (e: Expr | null, word: string): number | null => {
|
|
194
201
|
if (!e) return null;
|
|
195
202
|
const v = this.evalExpr(e, scope);
|
|
196
203
|
if (typeof v !== "number" || !Number.isInteger(v) || v < 0) {
|
|
@@ -216,7 +223,7 @@ export class InMemoryEngine implements Engine {
|
|
|
216
223
|
}
|
|
217
224
|
|
|
218
225
|
// Make the scope for a row. An `entries()` entry is unwrapped here: the scope's
|
|
219
|
-
// row is the property's VALUE (so `$
|
|
226
|
+
// row is the property's VALUE (so `$it` and bare names read it) and the key
|
|
220
227
|
// becomes the `$key` intrinsic in `meta`. Every place a row becomes a scope
|
|
221
228
|
// goes through this, so entries behave the same at the top level, in nested
|
|
222
229
|
// blocks, as `from` re-projections, and as follow seeds.
|
|
@@ -719,20 +726,30 @@ export class InMemoryEngine implements Engine {
|
|
|
719
726
|
const r = this.evalExpr(e.recv, scope);
|
|
720
727
|
return r == null ? undefined : this.ctx.get(r, e.name);
|
|
721
728
|
}
|
|
722
|
-
case "index": {
|
|
723
|
-
const r = this.evalExpr(e.recv, scope);
|
|
724
|
-
const i = this.evalExpr(e.index, scope);
|
|
725
|
-
return r == null ? undefined : this.ctx.get(r, String(i));
|
|
726
|
-
}
|
|
727
729
|
case "call": return this.evalCall(e, scope);
|
|
728
730
|
case "unary": {
|
|
729
731
|
const v = this.evalExpr(e.expr, scope);
|
|
730
732
|
if (e.op === "!") return !truthy(v);
|
|
731
733
|
return v == null ? undefined : -(v as number);
|
|
732
734
|
}
|
|
735
|
+
// `x!` — the value, or an eval error naming the expression (and the row's
|
|
736
|
+
// identity when it has a scalar one). Never a filter, never a coercion.
|
|
737
|
+
case "required": {
|
|
738
|
+
const v = this.evalExpr(e.expr, scope);
|
|
739
|
+
if (v == null) throw new OqxError(`\`${describe(e.expr)}!\` is absent${this.rowIdentity(scope)}`, "eval");
|
|
740
|
+
return v;
|
|
741
|
+
}
|
|
742
|
+
// A value-position directive (`jobs first { … }.pay`, the desugared `x[…]`).
|
|
743
|
+
case "op": return this.evalCollectValue(e, scope);
|
|
733
744
|
case "binary": {
|
|
734
745
|
const l = this.evalExpr(e.left, scope);
|
|
735
746
|
const r = this.evalExpr(e.right, scope);
|
|
747
|
+
if (e.op === "is" || e.op === "is not") {
|
|
748
|
+
// Identity (SEMANTICS §5b): the context's identity of each side,
|
|
749
|
+
// compared structurally — the key `distinct` and `follow` use.
|
|
750
|
+
const same = canonicalKey(this.ctx.identity(l)) === canonicalKey(this.ctx.identity(r));
|
|
751
|
+
return e.op === "is" ? same : !same;
|
|
752
|
+
}
|
|
736
753
|
return isRelOp(e.op) ? relate(e.op, l, r) : arith(e.op, l, r);
|
|
737
754
|
}
|
|
738
755
|
case "logical": {
|
|
@@ -749,12 +766,15 @@ export class InMemoryEngine implements Engine {
|
|
|
749
766
|
}
|
|
750
767
|
|
|
751
768
|
// Resolve a name against ONE scope — never its ancestors. A scope provides,
|
|
752
|
-
// in order: `$
|
|
753
|
-
// type, so scalar collections are queryable;
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
//
|
|
769
|
+
// in order: `$it` (the scope's row itself — the current item, whatever its
|
|
770
|
+
// type, so scalar collections are queryable; at the root the host's root
|
|
771
|
+
// object, since 0.18; `$value`, its pre-0.15 spelling, is an ordinary
|
|
772
|
+
// property name now); `$key` (the property key) when it is an entry scope —
|
|
773
|
+
// see `enter`; the recursion intrinsics (`$depth`, …) when it is a follow
|
|
774
|
+
// occurrence; values lifted into it by `^name:` items; then either the row's
|
|
775
|
+
// own property or, for the root scope, the context's named roots (`root(name)`
|
|
776
|
+
// — never a property of the root object, so a host with lazy roots is
|
|
777
|
+
// unaffected by what its root object holds).
|
|
758
778
|
//
|
|
759
779
|
// The two metadata steps apply only where the scope CARRIES that metadata
|
|
760
780
|
// (SEMANTICS §2, since 0.13). Anywhere else — `$key` on an ordinary row,
|
|
@@ -770,13 +790,22 @@ export class InMemoryEngine implements Engine {
|
|
|
770
790
|
// always spelled explicitly as `^name`. Present-but-falsy values (null, false,
|
|
771
791
|
// 0, "") need no special case — there is no "absent, so look outward" rule.
|
|
772
792
|
private resolveIn(name: string, scope: Scope): unknown {
|
|
773
|
-
if (name === "$
|
|
793
|
+
if (name === "$it") return scope.row;
|
|
774
794
|
if ((name === KEY || RECUR.has(name)) && scope.meta && Object.hasOwn(scope.meta, name)) return scope.meta[name];
|
|
775
795
|
if (scope.lifts && Object.hasOwn(scope.lifts, name)) return scope.lifts[name];
|
|
776
796
|
if (scope.parent === null) return this.ctx.root(name);
|
|
777
797
|
return this.ctx.get(scope.row, name);
|
|
778
798
|
}
|
|
779
799
|
|
|
800
|
+
// ` on <identity>` for a required-value error when the scope has a row with a
|
|
801
|
+
// scalar identity (its `id`, through the context); empty otherwise, and at the
|
|
802
|
+
// root (the root object is not a row of the data).
|
|
803
|
+
private rowIdentity(scope: Scope): string {
|
|
804
|
+
if (scope.parent === null || scope.row == null) return "";
|
|
805
|
+
const id = this.ctx.identity(scope.row);
|
|
806
|
+
return typeof id === "string" || typeof id === "number" ? ` on ${JSON.stringify(id)}` : "";
|
|
807
|
+
}
|
|
808
|
+
|
|
780
809
|
private evalCall(e: Extract<Expr, { kind: "call" }>, scope: Scope): unknown {
|
|
781
810
|
const args = e.args.map((a) => this.evalExpr(a, scope));
|
|
782
811
|
if (e.recv === null) {
|
|
@@ -873,6 +902,13 @@ function boundedCount(n: number, b: Bound): number {
|
|
|
873
902
|
return b.limit == null ? rest : Math.min(rest, b.limit);
|
|
874
903
|
}
|
|
875
904
|
|
|
905
|
+
// The canonical text of an expression for an error message; a binding shows as
|
|
906
|
+
// its `${n}` marker (the printer refuses to turn a bound value into source).
|
|
907
|
+
function describe(e: Expr): string {
|
|
908
|
+
const t = printTemplate(e);
|
|
909
|
+
return t.strings.map((s, i) => (i < t.count ? `${s}\${${t.indices[i]}}` : s)).join("");
|
|
910
|
+
}
|
|
911
|
+
|
|
876
912
|
function describeReceiver(e: Expr): string {
|
|
877
913
|
if (e.kind === "ident") return e.name;
|
|
878
914
|
if (e.kind === "member") return `${describeReceiver(e.recv)}.${e.name}`;
|
|
@@ -890,7 +926,7 @@ function partitionRecur(w: Where): { seed: Where | null; post: Where | null } {
|
|
|
890
926
|
const seed: Where[] = [];
|
|
891
927
|
const post: Where[] = [];
|
|
892
928
|
for (const p of parts) (whereHasRecur(p) ? post : seed).push(p);
|
|
893
|
-
const rebuild = (ps: Where[]): Where | null => (ps.length === 0 ? null : ps.length === 1 ? ps[0]! : { kind: "and", parts: ps });
|
|
929
|
+
const rebuild = (ps: Where[]): Where | null => (ps.length === 0 ? null : ps.length === 1 ? ps[0]! : { kind: "and", span: [0, 0], parts: ps });
|
|
894
930
|
return { seed: rebuild(seed), post: rebuild(post) };
|
|
895
931
|
}
|
|
896
932
|
|
|
@@ -907,17 +943,10 @@ function exprHasRecur(e: Expr): boolean {
|
|
|
907
943
|
switch (e.kind) {
|
|
908
944
|
case "ident": return RECUR.has(e.name);
|
|
909
945
|
case "member": return exprHasRecur(e.recv);
|
|
910
|
-
case "index": return exprHasRecur(e.recv) || exprHasRecur(e.index);
|
|
911
946
|
case "call": return (e.recv ? exprHasRecur(e.recv) : false) || e.args.some(exprHasRecur);
|
|
912
|
-
case "unary": return exprHasRecur(e.expr);
|
|
947
|
+
case "unary": case "required": return exprHasRecur(e.expr);
|
|
913
948
|
case "binary": case "logical": case "in": return exprHasRecur(e.left) || exprHasRecur(e.right);
|
|
914
949
|
case "range": return (e.lo != null && exprHasRecur(e.lo)) || (e.hi != null && exprHasRecur(e.hi));
|
|
915
950
|
default: return false;
|
|
916
951
|
}
|
|
917
952
|
}
|
|
918
|
-
|
|
919
|
-
/** Convenience: run a query with plain-object roots (the default context). */
|
|
920
|
-
export function runQuery(query: Query, bindings: readonly unknown[], roots: unknown): OqxResult {
|
|
921
|
-
const ctx = new DefaultContext((roots as Record<string, unknown>) ?? {});
|
|
922
|
-
return new InMemoryEngine(ctx).run(query, bindings);
|
|
923
|
-
}
|
package/src/errors.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// A single error type for every OQX failure — lex, parse,
|
|
1
|
+
// A single error type for every OQX failure — lex, parse, evaluation, and
|
|
2
|
+
// printing (a binding has no source text). `stage`
|
|
2
3
|
// distinguishes where it came from so callers (and tests) can branch without
|
|
3
4
|
// string-matching messages. Mirrors the reference impl's FilterInvalid, but the
|
|
4
5
|
// generic kernel has no SQL/CEL layer to name.
|
|
5
6
|
|
|
6
|
-
export type OqxStage = "lex" | "parse" | "eval";
|
|
7
|
+
export type OqxStage = "lex" | "parse" | "eval" | "print";
|
|
7
8
|
|
|
8
9
|
export class OqxError extends Error {
|
|
9
10
|
readonly stage: OqxStage;
|