@bjornpagen/bumbledb 0.1.0 → 0.3.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/COOKBOOK.md +1450 -0
- package/README.md +69 -30
- package/dist/closed.d.ts +157 -38
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +232 -47
- package/dist/closed.js.map +1 -1
- package/dist/count.d.ts +64 -17
- package/dist/count.d.ts.map +1 -1
- package/dist/count.js +37 -14
- package/dist/count.js.map +1 -1
- package/dist/db.d.ts +27 -8
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +33 -10
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts +12 -11
- package/dist/exhume.d.ts.map +1 -1
- package/dist/exhume.js +7 -6
- package/dist/exhume.js.map +1 -1
- package/dist/face.d.ts +135 -30
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +44 -19
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +130 -130
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +60 -94
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +29 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -14
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +224 -0
- package/dist/law.d.ts.map +1 -0
- package/dist/law.js +224 -0
- package/dist/law.js.map +1 -0
- package/dist/lower.d.ts +17 -7
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +66 -24
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +36 -21
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +41 -25
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +6 -2
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +2 -2
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +370 -174
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +75 -172
- package/dist/query/atom.js.map +1 -1
- package/dist/query/lower.d.ts +312 -109
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +906 -387
- package/dist/query/lower.js.map +1 -1
- package/dist/query/predicate.d.ts +70 -88
- package/dist/query/predicate.d.ts.map +1 -1
- package/dist/query/predicate.js +118 -72
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/run.d.ts +19 -16
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +31 -24
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +178 -127
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +111 -108
- package/dist/query/scope.js.map +1 -1
- package/dist/query/select.d.ts +102 -80
- package/dist/query/select.d.ts.map +1 -1
- package/dist/query/select.js +39 -34
- package/dist/query/select.js.map +1 -1
- package/dist/relation.d.ts +48 -38
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +24 -19
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts +41 -3
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +34 -9
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +7 -5
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +91 -36
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +42 -12
- package/dist/statements.js.map +1 -1
- package/package.json +4 -3
- package/src/closed.ts +448 -95
- package/src/count.ts +112 -18
- package/src/db.ts +45 -17
- package/src/exhume.ts +12 -11
- package/src/face.ts +211 -41
- package/src/fields.ts +199 -234
- package/src/index.ts +78 -61
- package/src/law.ts +519 -0
- package/src/lower.ts +82 -27
- package/src/marshal.ts +48 -30
- package/src/native.ts +8 -4
- package/src/query/atom.ts +525 -374
- package/src/query/lower.ts +1469 -538
- package/src/query/predicate.ts +208 -161
- package/src/query/run.ts +35 -25
- package/src/query/scope.ts +264 -218
- package/src/query/select.ts +168 -93
- package/src/relation.ts +47 -47
- package/src/schema.ts +69 -18
- package/src/spec.ts +7 -5
- package/src/statements.ts +124 -41
- package/dist/brand.d.ts +0 -59
- package/dist/brand.d.ts.map +0 -1
- package/dist/brand.js +0 -47
- package/dist/brand.js.map +0 -1
- package/src/brand.ts +0 -82
package/dist/query/lower.js
CHANGED
|
@@ -1,156 +1,717 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `query()` and the IR lowering
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
2
|
+
* `query()` and the IR lowering, STRUCTURAL edition. A query is built
|
|
3
|
+
* kysely-shaped — `query(S).rule(r => r.match(Rel, { f: r.var("x") })
|
|
4
|
+
* .where(r.eq(r.var("x"), r.param("p"))).select("x"))` — and is an INERT
|
|
5
|
+
* value: `Query<Rels, Row, Params>` with `Row` inferred from each rule's
|
|
6
|
+
* `.select` and `Params` inferred to be EXACTLY the params the rules use
|
|
7
|
+
* (params are typed BY USE; a param value no rule uses never registers, so
|
|
8
|
+
* every query executes under its own inferred type). Vars are string
|
|
9
|
+
* names, domain-typed by the field they first bind and joined by reuse —
|
|
10
|
+
* the rule builder's environment carries name → field descriptor through
|
|
11
|
+
* the chain, checked structurally at every reuse (`JoinOk`), so the old
|
|
12
|
+
* brand-equal join is now the domain-equal compile error. Lowering is a
|
|
13
|
+
* pure function of the query value down to the bridge's `ProgramIr`
|
|
14
|
+
* (`bumbledb/crates/bumbledb/src/ir.rs`, the bijection target): relations
|
|
15
|
+
* by declaration ordinal (the declaration-order-is-ids law the engine's
|
|
16
|
+
* manifest pins), variables by dense per-rule first-occurrence ids
|
|
17
|
+
* (rule-scoped, exactly as the IR scopes them), params by first-use order
|
|
18
|
+
* across the program walk. Lowering is STABLE — the same query value
|
|
19
|
+
* lowers to deeply-equal IR every time, and two identically-written
|
|
20
|
+
* queries lower identically. Construction validates negation safety and
|
|
21
|
+
* name-boundness (typed, naming the variable — earlier and warmer than
|
|
22
|
+
* the engine's refusal); everything else (strata, types, aggregate
|
|
23
|
+
* rosters, rule caps) is the ENGINE's judge, surfacing its typed errors
|
|
24
|
+
* at prepare. No invented limits: rule and predicate counts are never
|
|
25
|
+
* pre-checked here.
|
|
19
26
|
*/
|
|
20
27
|
import * as errors from "@superbuilders/errors";
|
|
21
|
-
import { phantom } from "#brand.ts";
|
|
22
28
|
import { assertDeclarationOrderKey } from "#fields.ts";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
29
|
+
import { allen, and, covers, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts";
|
|
30
|
+
import { fieldJoins, inferred, isTerm, makeDuration, makeMaskParam, makeParam, makeSetParam, makeVar, makeVars, renderFieldKind, term } from "#query/scope.ts";
|
|
31
|
+
import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/select.ts";
|
|
32
|
+
/** The frozen constructor vocabulary every rule builder spreads. */
|
|
33
|
+
const termOps = Object.freeze({
|
|
34
|
+
var: makeVar,
|
|
35
|
+
vars: makeVars,
|
|
36
|
+
param: makeParam,
|
|
37
|
+
inSet: makeSetParam,
|
|
38
|
+
maskParam: makeMaskParam,
|
|
39
|
+
duration: makeDuration,
|
|
40
|
+
eq,
|
|
41
|
+
ne,
|
|
42
|
+
lt,
|
|
43
|
+
le,
|
|
44
|
+
gt,
|
|
45
|
+
ge,
|
|
46
|
+
pointIn,
|
|
47
|
+
covers,
|
|
48
|
+
allen,
|
|
49
|
+
and,
|
|
50
|
+
or,
|
|
51
|
+
not,
|
|
52
|
+
count,
|
|
53
|
+
countDistinct,
|
|
54
|
+
sum,
|
|
55
|
+
min,
|
|
56
|
+
max,
|
|
57
|
+
argMax,
|
|
58
|
+
argMin,
|
|
59
|
+
pack
|
|
60
|
+
});
|
|
61
|
+
/** The empty rule state. */
|
|
62
|
+
const EMPTY_RULE = Object.freeze({
|
|
63
|
+
items: Object.freeze([]),
|
|
64
|
+
varFields: Object.freeze({}),
|
|
65
|
+
paramUses: Object.freeze([])
|
|
66
|
+
});
|
|
25
67
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
68
|
+
* The ordered matchable fields of an atom owner — the runtime twin of the
|
|
69
|
+
* type tier's `MatchFields`: a relation's declared fields; a closed
|
|
70
|
+
* relation's SEALED shape, the synthetic `id` (the value's own
|
|
71
|
+
* roster-carrying descriptor, by identity) at ordinal 0 and the declared
|
|
72
|
+
* payload columns at declared index + 1 (the sealed shift, mirroring
|
|
73
|
+
* `spec.rs`'s resolver — a `ClosedColumn` is structurally a
|
|
74
|
+
* {@link RelationField}). The lowering golden pins this mapping.
|
|
29
75
|
*/
|
|
30
|
-
function
|
|
31
|
-
|
|
76
|
+
function matchFieldsOf(owner) {
|
|
77
|
+
if ("axioms" in owner) {
|
|
78
|
+
return [Object.freeze({ name: "id", field: owner.id }), ...owner.data.columns];
|
|
79
|
+
}
|
|
80
|
+
return owner.data.fields;
|
|
32
81
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Resolves a bindings record against an atom owner's matchable fields (a
|
|
84
|
+
* relation's declared fields; a closed relation's sealed id + columns), in
|
|
85
|
+
* the record's written order: terms classify by their runtime tag,
|
|
86
|
+
* everything else is a bare literal (typed by the FIELD at lowering — the
|
|
87
|
+
* membership typing rule included). Every bound field carries its
|
|
88
|
+
* law-computed class, read off the schema value's frozen class map — the
|
|
89
|
+
* runtime twin of the type tier's `SlotAt` lookups.
|
|
90
|
+
*/
|
|
91
|
+
function resolveBindings(context, relation, bindings, classes) {
|
|
92
|
+
const entries = [];
|
|
93
|
+
const vars = [];
|
|
94
|
+
const uses = [];
|
|
95
|
+
const relationClasses = classes[relation.name];
|
|
96
|
+
const ordered = matchFieldsOf(relation);
|
|
97
|
+
for (const [fieldName, value] of Object.entries(bindings)) {
|
|
98
|
+
if (value === undefined) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const declared = ordered.find(function byName(candidate) {
|
|
102
|
+
return candidate.name === fieldName;
|
|
103
|
+
});
|
|
104
|
+
if (declared === undefined) {
|
|
105
|
+
throw errors.new(`${context} has no field ${fieldName}`);
|
|
106
|
+
}
|
|
107
|
+
const fieldClass = relationClasses?.[fieldName];
|
|
108
|
+
let bound;
|
|
109
|
+
if (isTerm(value)) {
|
|
110
|
+
switch (value[term]) {
|
|
111
|
+
case "var": {
|
|
112
|
+
bound = Object.freeze({ kind: "var", name: value.name });
|
|
113
|
+
vars.push(Object.freeze({ name: value.name, slot: Object.freeze({ field: declared.field, class: fieldClass }) }));
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "param": {
|
|
117
|
+
bound = Object.freeze({ kind: "param", name: value.name });
|
|
118
|
+
uses.push(Object.freeze({ name: value.name, shape: "value", anchor: declared.field, op: "binding" }));
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case "setParam": {
|
|
122
|
+
bound = Object.freeze({ kind: "setParam", name: value.name });
|
|
123
|
+
uses.push(Object.freeze({ name: value.name, shape: "set", anchor: declared.field, op: "binding" }));
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case "maskParam":
|
|
127
|
+
throw errors.new(`${context}.${fieldName}: an Allen-mask param is not a field-typed value — masks live in allen() conditions only`);
|
|
128
|
+
case "duration":
|
|
129
|
+
throw errors.new(`${context}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and select entries`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
bound = Object.freeze({ kind: "literal", value });
|
|
134
|
+
}
|
|
135
|
+
entries.push(Object.freeze({ field: fieldName, data: declared.field, class: fieldClass, term: bound }));
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
atom: Object.freeze({ relation, bindings: Object.freeze(entries) }),
|
|
139
|
+
vars,
|
|
140
|
+
uses
|
|
141
|
+
};
|
|
36
142
|
}
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Extends a rule state with one positive atom. Vars bind on first
|
|
145
|
+
* occurrence; every LATER occurrence (a later atom's field or a same-record
|
|
146
|
+
* sibling) is a join and must be class-equal — the construction-time twin
|
|
147
|
+
* of the type tier's `JoinOk` (bare pairs only with bare), so the domain
|
|
148
|
+
* wall holds for untyped callers too.
|
|
149
|
+
*/
|
|
150
|
+
function advanceMatch(state, relation, bindings, classes) {
|
|
151
|
+
const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings, classes);
|
|
152
|
+
const varFields = { ...state.varFields };
|
|
153
|
+
for (const bound of resolved.vars) {
|
|
154
|
+
const existing = varFields[bound.name];
|
|
155
|
+
if (existing === undefined) {
|
|
156
|
+
varFields[bound.name] = bound.slot;
|
|
157
|
+
}
|
|
158
|
+
else if (!fieldJoins(existing, bound.slot)) {
|
|
159
|
+
throw errors.new(`relation ${relation.name}: the variable ${bound.name} joins domain-unequal fields — first bound at ${renderFieldKind(existing)}, reused at ${renderFieldKind(bound.slot)} (a var joins only class-equal slots; bare pairs only with bare)`);
|
|
42
160
|
}
|
|
43
|
-
return Object.freeze({ kind: "var", over: value });
|
|
44
161
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
162
|
+
return {
|
|
163
|
+
items: Object.freeze([...state.items, Object.freeze({ kind: "atom", atom: resolved.atom })]),
|
|
164
|
+
varFields: Object.freeze(varFields),
|
|
165
|
+
paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** Resolves one comparison side to its runtime term. */
|
|
169
|
+
function cmpTermDataOf(op, value) {
|
|
170
|
+
if (isTerm(value)) {
|
|
171
|
+
switch (value[term]) {
|
|
172
|
+
case "var":
|
|
173
|
+
return Object.freeze({ kind: "var", name: value.name });
|
|
174
|
+
case "param":
|
|
175
|
+
return Object.freeze({ kind: "param", name: value.name });
|
|
176
|
+
case "setParam":
|
|
177
|
+
return Object.freeze({ kind: "setParam", name: value.name });
|
|
178
|
+
case "duration":
|
|
179
|
+
return Object.freeze({ kind: "measure", name: value.name });
|
|
180
|
+
case "maskParam":
|
|
181
|
+
throw errors.new(`${op}: an Allen-mask param is not a comparison term — masks live in allen()'s mask position`);
|
|
48
182
|
}
|
|
49
|
-
|
|
50
|
-
|
|
183
|
+
}
|
|
184
|
+
return Object.freeze({ kind: "literal", value });
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* One comparison side's contribution to the param census: a param/set side
|
|
188
|
+
* anchors to its SIBLING — a bound variable's field descriptor or the
|
|
189
|
+
* measure; an unanchorable use (literal or param sibling) records with no
|
|
190
|
+
* anchor and must be anchored by some other use of the same name.
|
|
191
|
+
*/
|
|
192
|
+
function sideUses(op, side, sibling, varFields, uses) {
|
|
193
|
+
if (side.kind !== "param" && side.kind !== "setParam") {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
let anchor;
|
|
197
|
+
if (sibling.kind === "var") {
|
|
198
|
+
anchor = varFields[sibling.name]?.field;
|
|
199
|
+
}
|
|
200
|
+
else if (sibling.kind === "measure") {
|
|
201
|
+
anchor = "measure";
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
anchor = undefined;
|
|
205
|
+
}
|
|
206
|
+
uses.push(Object.freeze({
|
|
207
|
+
name: side.name,
|
|
208
|
+
shape: side.kind === "param" ? "value" : "set",
|
|
209
|
+
anchor,
|
|
210
|
+
op
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
/** Lowers one condition VALUE to its runtime data, recording param uses. */
|
|
214
|
+
function condDataOf(cond, varFields, uses) {
|
|
215
|
+
if (cond.cond === "cmp") {
|
|
216
|
+
const lhs = cmpTermDataOf(cond.op, cond.lhs);
|
|
217
|
+
const rhs = cmpTermDataOf(cond.op, cond.rhs);
|
|
218
|
+
sideUses(cond.op, lhs, rhs, varFields, uses);
|
|
219
|
+
sideUses(cond.op, rhs, lhs, varFields, uses);
|
|
220
|
+
let mask;
|
|
221
|
+
if (cond.op === "allen") {
|
|
222
|
+
const maskValue = cond.mask;
|
|
223
|
+
if (typeof maskValue === "number") {
|
|
224
|
+
mask = Object.freeze({ kind: "literal", mask: maskValue });
|
|
225
|
+
}
|
|
226
|
+
else if (isTerm(maskValue) && maskValue[term] === "maskParam") {
|
|
227
|
+
mask = Object.freeze({ kind: "param", name: maskValue.name });
|
|
228
|
+
uses.push(Object.freeze({ name: maskValue.name, shape: "mask", anchor: undefined, op: "allen" }));
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam");
|
|
232
|
+
}
|
|
51
233
|
}
|
|
234
|
+
const data = Object.freeze({ kind: "cmp", op: cond.op, mask, lhs, rhs });
|
|
235
|
+
return data;
|
|
236
|
+
}
|
|
237
|
+
if (cond.cond === "tree") {
|
|
238
|
+
const children = cond.children.map(function lowerChild(child) {
|
|
239
|
+
return condDataOf(child, varFields, uses);
|
|
240
|
+
});
|
|
241
|
+
const data = Object.freeze({ kind: "tree", op: cond.op, children: Object.freeze(children) });
|
|
242
|
+
return data;
|
|
52
243
|
}
|
|
53
|
-
throw errors.new(
|
|
244
|
+
throw errors.new("a negated atom is not a condition-tree node — pass not(...) to where() directly, never inside and()/or()");
|
|
54
245
|
}
|
|
55
|
-
/**
|
|
56
|
-
function
|
|
57
|
-
if (
|
|
58
|
-
|
|
246
|
+
/** Extends a rule state with one `.where` item (a condition or a negated atom). */
|
|
247
|
+
function advanceWhere(state, cond, classes) {
|
|
248
|
+
if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
|
|
249
|
+
throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom");
|
|
59
250
|
}
|
|
60
|
-
|
|
251
|
+
if (cond.cond === "not") {
|
|
252
|
+
const relation = cond.relation;
|
|
253
|
+
const bindings = Object.fromEntries(Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
|
|
254
|
+
return value !== undefined;
|
|
255
|
+
}));
|
|
256
|
+
const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings, classes);
|
|
257
|
+
return {
|
|
258
|
+
items: Object.freeze([...state.items, Object.freeze({ kind: "negated", atom: resolved.atom })]),
|
|
259
|
+
varFields: state.varFields,
|
|
260
|
+
paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const uses = [];
|
|
264
|
+
const data = condDataOf(cond, state.varFields, uses);
|
|
265
|
+
return {
|
|
266
|
+
items: Object.freeze([...state.items, Object.freeze({ kind: "cond", cond: data })]),
|
|
267
|
+
varFields: state.varFields,
|
|
268
|
+
paramUses: Object.freeze([...state.paramUses, ...uses])
|
|
269
|
+
};
|
|
61
270
|
}
|
|
62
|
-
/**
|
|
63
|
-
function
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
continue;
|
|
271
|
+
/** Extends a rule state with one `idb` atom (vars must be bound — validated at completion). */
|
|
272
|
+
function advanceIdb(state, rec, vars) {
|
|
273
|
+
const names = vars.map(function nameOf(variable) {
|
|
274
|
+
if (!isTerm(variable) || variable[term] !== "var") {
|
|
275
|
+
throw errors.new(`idb ${rec.name}: positions take variables — bind literals and params through where()/match()`);
|
|
68
276
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
277
|
+
return variable.name;
|
|
278
|
+
});
|
|
279
|
+
return {
|
|
280
|
+
items: Object.freeze([...state.items, Object.freeze({ kind: "idb", rec, vars: Object.freeze(names) })]),
|
|
281
|
+
varFields: state.varFields,
|
|
282
|
+
paramUses: state.paramUses
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/** Narrows a select entry to an aggregate value. */
|
|
286
|
+
function isAggregateEntry(value) {
|
|
287
|
+
return typeof value === "object" && value !== null && "agg" in value;
|
|
288
|
+
}
|
|
289
|
+
/** Classifies one select entry into its named answer column. */
|
|
290
|
+
function selectColumnOf(entry) {
|
|
291
|
+
if (typeof entry === "string") {
|
|
292
|
+
return Object.freeze({ name: entry, entry: Object.freeze({ kind: "var", over: entry }) });
|
|
293
|
+
}
|
|
294
|
+
if (isTerm(entry)) {
|
|
295
|
+
if (entry[term] === "duration") {
|
|
296
|
+
return Object.freeze({ name: entry.name, entry: Object.freeze({ kind: "measure", over: entry.name }) });
|
|
73
297
|
}
|
|
298
|
+
throw errors.new(`query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`);
|
|
74
299
|
}
|
|
75
|
-
|
|
300
|
+
if (isAggregateEntry(entry)) {
|
|
301
|
+
return aggregateColumnOf(entry);
|
|
302
|
+
}
|
|
303
|
+
throw errors.new("query select: not a select entry — select takes variable names, duration(v), or aggregates");
|
|
76
304
|
}
|
|
77
|
-
/**
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
305
|
+
/** Classifies one aggregate select entry. */
|
|
306
|
+
function aggregateColumnOf(entry) {
|
|
307
|
+
function column(name, agg) {
|
|
308
|
+
return Object.freeze({ name, entry: Object.freeze({ kind: "aggregate", agg: Object.freeze(agg) }) });
|
|
309
|
+
}
|
|
310
|
+
const over = entry.over;
|
|
311
|
+
switch (entry.agg) {
|
|
312
|
+
case "count":
|
|
313
|
+
return column("count", { op: "count" });
|
|
314
|
+
case "countDistinct": {
|
|
315
|
+
if (typeof over !== "string") {
|
|
316
|
+
throw errors.new("countDistinct takes a variable name");
|
|
317
|
+
}
|
|
318
|
+
return column(over, { op: "countDistinct", over });
|
|
82
319
|
}
|
|
83
|
-
|
|
84
|
-
|
|
320
|
+
case "sum":
|
|
321
|
+
case "min":
|
|
322
|
+
case "max": {
|
|
323
|
+
if (typeof over === "string") {
|
|
324
|
+
return column(over, { op: "fold", fold: entry.agg, over });
|
|
325
|
+
}
|
|
326
|
+
if (isTerm(over) && over[term] === "duration") {
|
|
327
|
+
return column(over.name, { op: "fold", fold: entry.agg, over: Object.freeze({ duration: over.name }) });
|
|
328
|
+
}
|
|
329
|
+
throw errors.new(`${entry.agg} takes a variable name or duration(v)`);
|
|
330
|
+
}
|
|
331
|
+
case "argMax":
|
|
332
|
+
case "argMin": {
|
|
333
|
+
if (typeof over !== "string" || typeof entry.key !== "string") {
|
|
334
|
+
throw errors.new(`${entry.agg} takes a carried variable name and an orderable key variable name`);
|
|
335
|
+
}
|
|
336
|
+
return column(over, { op: "arg", direction: entry.agg, over, key: entry.key });
|
|
85
337
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
338
|
+
case "pack": {
|
|
339
|
+
if (typeof over !== "string") {
|
|
340
|
+
throw errors.new("pack takes a variable name");
|
|
341
|
+
}
|
|
342
|
+
return column(over, { op: "pack", over });
|
|
89
343
|
}
|
|
344
|
+
default:
|
|
345
|
+
throw errors.new(`unknown aggregate ${entry.agg}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** Requires a var name to be bound by a relation atom of the rule. */
|
|
349
|
+
function assertBound(context, varFields, name) {
|
|
350
|
+
const slot = varFields[name];
|
|
351
|
+
if (slot === undefined) {
|
|
352
|
+
throw errors.new(`${context}: the variable ${name} is not bound by a relation atom of the rule`);
|
|
353
|
+
}
|
|
354
|
+
return slot;
|
|
355
|
+
}
|
|
356
|
+
/** Requires a var name to be bound at an interval field (the measure's and pack's domain). */
|
|
357
|
+
function assertIntervalBound(context, varFields, name) {
|
|
358
|
+
const slot = assertBound(context, varFields, name);
|
|
359
|
+
if (slot.field.kind !== "interval") {
|
|
360
|
+
throw errors.new(`${context}: ${name} is not interval-typed — the measure is defined over interval-typed variables only`);
|
|
90
361
|
}
|
|
91
362
|
}
|
|
92
363
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
364
|
+
* Validates one condition's variable references against the rule's bound
|
|
365
|
+
* names — and, for `eq`/`ne` over two variables, holds the class wall: the
|
|
366
|
+
* unification IS a join, so the two slots must be class-equal exactly as a
|
|
367
|
+
* match-reuse join must be (the construction-time twin of the type tier's
|
|
368
|
+
* `EqOk` → `JoinOk`; bare pairs only with bare). The engine cannot backstop
|
|
369
|
+
* this one — the query IR carries no domains — so the wall lives here for
|
|
370
|
+
* untyped callers too.
|
|
97
371
|
*/
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
372
|
+
function validateCond(context, varFields, cond) {
|
|
373
|
+
if (cond.kind === "cmp") {
|
|
374
|
+
for (const side of [cond.lhs, cond.rhs]) {
|
|
375
|
+
if (side.kind === "var") {
|
|
376
|
+
assertBound(context, varFields, side.name);
|
|
377
|
+
}
|
|
378
|
+
if (side.kind === "measure") {
|
|
379
|
+
assertIntervalBound(context, varFields, side.name);
|
|
380
|
+
}
|
|
103
381
|
}
|
|
382
|
+
if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
|
|
383
|
+
const lhs = assertBound(context, varFields, cond.lhs.name);
|
|
384
|
+
const rhs = assertBound(context, varFields, cond.rhs.name);
|
|
385
|
+
if (!fieldJoins(lhs, rhs)) {
|
|
386
|
+
throw errors.new(`${context}: ${cond.op}(${cond.lhs.name}, ${cond.rhs.name}) unifies domain-unequal fields — ${cond.lhs.name} bound at ${renderFieldKind(lhs)}, ${cond.rhs.name} at ${renderFieldKind(rhs)} (a var joins only class-equal slots; bare pairs only with bare)`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
for (const child of cond.children) {
|
|
392
|
+
validateCond(context, varFields, child);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/** Validates one select column's variable references. */
|
|
396
|
+
function validateColumn(context, varFields, column) {
|
|
397
|
+
const entry = column.entry;
|
|
398
|
+
if (entry.kind === "var") {
|
|
399
|
+
assertBound(`${context} select ${column.name}`, varFields, entry.over);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (entry.kind === "measure") {
|
|
403
|
+
assertIntervalBound(`${context} select ${column.name}`, varFields, entry.over);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const agg = entry.agg;
|
|
407
|
+
switch (agg.op) {
|
|
408
|
+
case "count":
|
|
409
|
+
return;
|
|
410
|
+
case "countDistinct":
|
|
411
|
+
assertBound(`${context} select ${column.name}`, varFields, agg.over);
|
|
412
|
+
return;
|
|
413
|
+
case "fold": {
|
|
414
|
+
if (typeof agg.over === "string") {
|
|
415
|
+
assertBound(`${context} select ${column.name}`, varFields, agg.over);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over.duration);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
case "arg":
|
|
422
|
+
assertBound(`${context} select ${column.name}`, varFields, agg.over);
|
|
423
|
+
assertBound(`${context} select ${column.name}`, varFields, agg.key);
|
|
424
|
+
return;
|
|
425
|
+
case "pack":
|
|
426
|
+
assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over);
|
|
427
|
+
return;
|
|
104
428
|
}
|
|
105
429
|
}
|
|
106
430
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
431
|
+
* Completes one rule: classifies the select record (written order = answer
|
|
432
|
+
* column order, names must be declaration-order-safe keys), and validates
|
|
433
|
+
* boundness — every condition/select/idb variable bound by a relation atom,
|
|
434
|
+
* and every NEGATED atom's variable positively bound (the safety rule: a
|
|
435
|
+
* negated atom binds nothing, only rejects).
|
|
112
436
|
*/
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
437
|
+
function completeRule(context, state, columns) {
|
|
438
|
+
if (columns.length === 0) {
|
|
439
|
+
throw errors.new(`${context}: a select needs at least one entry`);
|
|
440
|
+
}
|
|
441
|
+
const seen = new Set();
|
|
442
|
+
for (const column of columns) {
|
|
443
|
+
assertDeclarationOrderKey(`${context} select column`, column.name);
|
|
444
|
+
if (seen.has(column.name)) {
|
|
445
|
+
throw errors.new(`${context}: select names the answer column ${column.name} twice`);
|
|
446
|
+
}
|
|
447
|
+
seen.add(column.name);
|
|
448
|
+
validateColumn(context, state.varFields, column);
|
|
449
|
+
}
|
|
450
|
+
for (const item of state.items) {
|
|
451
|
+
if (item.kind === "negated") {
|
|
452
|
+
for (const binding of item.atom.bindings) {
|
|
453
|
+
if (binding.term.kind === "var") {
|
|
454
|
+
const bound = state.varFields[binding.term.name];
|
|
455
|
+
if (bound === undefined) {
|
|
456
|
+
throw errors.new(`${context}: negated ${item.atom.relation.name} atom binds the variable ${binding.term.name} at position ${binding.field}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`);
|
|
457
|
+
}
|
|
458
|
+
const negatedSlot = { field: binding.data, class: binding.class };
|
|
459
|
+
if (!fieldJoins(bound, negatedSlot)) {
|
|
460
|
+
throw errors.new(`${context}: negated ${item.atom.relation.name} atom reuses the variable ${binding.term.name} at ${binding.field} (${renderFieldKind(negatedSlot)}), but the rule binds it at ${renderFieldKind(bound)} — a var joins only class-equal slots; bare pairs only with bare`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (item.kind === "idb") {
|
|
466
|
+
const head = item.rec.rules[0];
|
|
467
|
+
item.vars.forEach(function checkIdbVar(name, position) {
|
|
468
|
+
const bound = state.varFields[name];
|
|
469
|
+
if (bound === undefined) {
|
|
470
|
+
throw errors.new(`${context}: idb ${item.rec.name} names the variable ${name}, but no relation atom of the rule binds it — an idb atom is a join position; bind the variable through the theory's own relation first`);
|
|
471
|
+
}
|
|
472
|
+
const column = head?.select[position];
|
|
473
|
+
if (column === undefined || column.entry.kind !== "var") {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const headSlot = head?.varFields[column.entry.over];
|
|
477
|
+
if (headSlot !== undefined && !fieldJoins(headSlot, bound)) {
|
|
478
|
+
throw errors.new(`${context}: idb ${item.rec.name} joins the variable ${name} (${renderFieldKind(bound)}) at head position ${position} (${column.name}: ${renderFieldKind(headSlot)}) — a var joins only class-equal slots; bare pairs only with bare`);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (item.kind === "cond") {
|
|
483
|
+
validateCond(context, state.varFields, item.cond);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return Object.freeze({
|
|
487
|
+
items: state.items,
|
|
488
|
+
select: Object.freeze([...columns]),
|
|
489
|
+
varFields: state.varFields,
|
|
490
|
+
paramUses: state.paramUses
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
/** Builds one typed rule value over completed rule data. */
|
|
494
|
+
function makeRuleValue(rule) {
|
|
495
|
+
return Object.freeze({ rule });
|
|
496
|
+
}
|
|
497
|
+
/** The diagnostic label of a chain context. */
|
|
498
|
+
function contextLabel(context) {
|
|
499
|
+
switch (context.kind) {
|
|
500
|
+
case "query":
|
|
501
|
+
return "query rule";
|
|
502
|
+
case "rec":
|
|
503
|
+
return `rec ${context.self.name} rule`;
|
|
504
|
+
case "output":
|
|
505
|
+
return "program output rule";
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/** Validates and records one `idb` atom per the context's cut. */
|
|
509
|
+
function idbAdvance(context, state, target, vars) {
|
|
510
|
+
if (context.kind === "query") {
|
|
511
|
+
throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()");
|
|
512
|
+
}
|
|
513
|
+
if (context.kind === "rec") {
|
|
514
|
+
if (target.data !== context.self) {
|
|
515
|
+
throw errors.new(`rec ${context.self.name}: a recursive rule's idb target must be the rec itself — the self-recursion-only cut (mutual recursion is unwritable; fold a finished stratum in the output rules)`);
|
|
516
|
+
}
|
|
517
|
+
return advanceIdb(state, context.self, vars);
|
|
518
|
+
}
|
|
519
|
+
if (!context.program.recs.includes(target.data)) {
|
|
520
|
+
throw errors.new(`idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`);
|
|
521
|
+
}
|
|
522
|
+
return advanceIdb(state, target.data, vars);
|
|
523
|
+
}
|
|
524
|
+
/** Classifies one select tuple per the context (a recursive head projects bound NAMES only). */
|
|
525
|
+
function selectColumns(context, entries) {
|
|
526
|
+
return entries.map(function columnOf(entry) {
|
|
527
|
+
if (context.kind === "rec" && typeof entry !== "string") {
|
|
528
|
+
throw errors.new(`rec ${context.self.name}: a recursive head projects bound variable NAMES only — aggregates and the measure read finished sets (the strata judge's quarantine, unwritable here)`);
|
|
529
|
+
}
|
|
530
|
+
return selectColumnOf(entry);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
/** Builds one runtime chain (immutably — every step is a fresh chain over fresh state). */
|
|
534
|
+
function makeRawChain(context, state) {
|
|
535
|
+
const chain = {
|
|
536
|
+
match(relation, bindings) {
|
|
537
|
+
return makeRawChain(context, advanceMatch(state, relation, bindings, context.classes));
|
|
121
538
|
},
|
|
122
|
-
|
|
123
|
-
return
|
|
539
|
+
where(cond) {
|
|
540
|
+
return makeRawChain(context, advanceWhere(state, cond, context.classes));
|
|
124
541
|
},
|
|
125
|
-
|
|
126
|
-
return
|
|
542
|
+
idb(target, ...vars) {
|
|
543
|
+
return makeRawChain(context, idbAdvance(context, state, target, vars));
|
|
127
544
|
},
|
|
128
|
-
|
|
129
|
-
return
|
|
545
|
+
select(...entries) {
|
|
546
|
+
return makeRuleValue(completeRule(contextLabel(context), state, selectColumns(context, entries)));
|
|
130
547
|
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
548
|
+
};
|
|
549
|
+
Object.freeze(chain);
|
|
550
|
+
return chain;
|
|
551
|
+
}
|
|
552
|
+
/** Builds one runtime rule-builder over a context. */
|
|
553
|
+
function makeRawScope(context) {
|
|
554
|
+
const scope = {
|
|
555
|
+
...termOps,
|
|
556
|
+
match(relation, bindings) {
|
|
557
|
+
return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings, context.classes));
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
Object.freeze(scope);
|
|
561
|
+
return scope;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* The one trusted admission seam of the rule builders (the `relation()`
|
|
565
|
+
* `refsComplete` pattern): the raw builder is one runtime shape for every
|
|
566
|
+
* context, and this guard verifies the checkable fact — the builder verbs
|
|
567
|
+
* exist — before the value is admitted at its TYPED face. The type-level
|
|
568
|
+
* judgments (domain-equal joins, boundness, the recursion cut) live in the
|
|
569
|
+
* interfaces themselves; the runtime twin of every one of them is a
|
|
570
|
+
* construction-time validation in this module.
|
|
571
|
+
*/
|
|
572
|
+
function isTypedScope(scope) {
|
|
573
|
+
return typeof scope.match === "function";
|
|
574
|
+
}
|
|
575
|
+
/** Builds one query-rule builder (the typed face of the raw builder). */
|
|
576
|
+
function makeQueryRuleScope(classes) {
|
|
577
|
+
const raw = makeRawScope({ kind: "query", classes });
|
|
578
|
+
if (!isTypedScope(raw)) {
|
|
579
|
+
throw errors.new("query rule builder construction incomplete");
|
|
580
|
+
}
|
|
581
|
+
return raw;
|
|
582
|
+
}
|
|
583
|
+
/** Builds one output-rule builder over a program's recs. */
|
|
584
|
+
function makeOutputRuleScope(program) {
|
|
585
|
+
const raw = makeRawScope({ kind: "output", program, classes: program.classes });
|
|
586
|
+
if (!isTypedScope(raw)) {
|
|
587
|
+
throw errors.new("program output rule builder construction incomplete");
|
|
142
588
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
589
|
+
return raw;
|
|
590
|
+
}
|
|
591
|
+
/** Renders one head column's signature for the rule-alignment check. */
|
|
592
|
+
function headSignature(column) {
|
|
593
|
+
const entry = column.entry;
|
|
594
|
+
if (entry.kind === "var" || entry.kind === "measure") {
|
|
595
|
+
return `${column.name}:var`;
|
|
596
|
+
}
|
|
597
|
+
const agg = entry.agg;
|
|
598
|
+
if (agg.op === "fold") {
|
|
599
|
+
return `${column.name}:${agg.fold}`;
|
|
600
|
+
}
|
|
601
|
+
if (agg.op === "arg") {
|
|
602
|
+
return `${column.name}:${agg.direction}`;
|
|
603
|
+
}
|
|
604
|
+
return `${column.name}:${agg.op}`;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Folds every rule's param uses (recs in declaration order first, output
|
|
608
|
+
* rules last — exactly the lowering walk) into the query's registry:
|
|
609
|
+
* first use mints the dense `ParamId`, the first FIELD-ANCHORED use types
|
|
610
|
+
* the wire, and one name must keep one shape.
|
|
611
|
+
*/
|
|
612
|
+
function paramRegistryOf(recs, rules) {
|
|
613
|
+
const order = [];
|
|
614
|
+
const byName = new Map();
|
|
615
|
+
function fold(uses) {
|
|
616
|
+
for (const use of uses) {
|
|
617
|
+
const existing = byName.get(use.name);
|
|
618
|
+
if (existing === undefined) {
|
|
619
|
+
order.push(use.name);
|
|
620
|
+
byName.set(use.name, { shape: use.shape, anchor: use.anchor, op: use.op });
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (existing.shape !== use.shape) {
|
|
624
|
+
throw errors.new(`query param ${use.name} is used both as a ${existing.shape} param and a ${use.shape} param — one name, one shape`);
|
|
625
|
+
}
|
|
626
|
+
if (existing.anchor === undefined && use.anchor !== undefined) {
|
|
627
|
+
existing.anchor = use.anchor;
|
|
628
|
+
existing.op = use.op;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
for (const rec of recs) {
|
|
633
|
+
for (const rule of rec.rules) {
|
|
634
|
+
fold(rule.paramUses);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
for (const rule of rules) {
|
|
638
|
+
fold(rule.paramUses);
|
|
639
|
+
}
|
|
640
|
+
return Object.freeze(order.map(function entryOf(name) {
|
|
641
|
+
const entry = byName.get(name);
|
|
642
|
+
if (entry === undefined) {
|
|
643
|
+
throw errors.new(`query param ${name} lost its registry entry`);
|
|
644
|
+
}
|
|
645
|
+
return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op });
|
|
646
|
+
}));
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Assembles the runtime query value over completed rules: every rule must
|
|
650
|
+
* derive the SAME head (name and aggregate shape, position for position —
|
|
651
|
+
* the decode labels and the engine's alignment rule agree by
|
|
652
|
+
* construction), and the param registry folds in program-walk order.
|
|
653
|
+
*/
|
|
654
|
+
function makeRawQuery(theory, recs, rules) {
|
|
655
|
+
const first = rules[0];
|
|
656
|
+
if (first === undefined) {
|
|
657
|
+
throw errors.new("a query needs at least one rule");
|
|
658
|
+
}
|
|
659
|
+
const signature = first.select.map(headSignature).join(", ");
|
|
660
|
+
rules.forEach(function verifyHead(rule, index) {
|
|
661
|
+
const candidate = rule.select.map(headSignature).join(", ");
|
|
662
|
+
if (candidate !== signature) {
|
|
663
|
+
throw errors.new(`every rule of a query derives the same head — rule 0 selects (${signature}), rule ${index} selects (${candidate})`);
|
|
664
|
+
}
|
|
146
665
|
});
|
|
147
|
-
Object.freeze(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
data: Object.freeze({ registry, rules: Object.freeze(rules), select: Object.freeze(select) })
|
|
666
|
+
const data = Object.freeze({
|
|
667
|
+
recs: Object.freeze([...recs]),
|
|
668
|
+
rules: Object.freeze([...rules]),
|
|
669
|
+
select: first.select,
|
|
670
|
+
params: paramRegistryOf(recs, rules)
|
|
153
671
|
});
|
|
672
|
+
const value = {
|
|
673
|
+
schema: theory,
|
|
674
|
+
data,
|
|
675
|
+
rule(build) {
|
|
676
|
+
const built = build(makeRawScope({ kind: "query", classes: theory.classes }));
|
|
677
|
+
return makeRawQuery(theory, recs, [...rules, built.rule]);
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
Object.freeze(value);
|
|
681
|
+
return value;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* The query values' trusted admission seam (the `refsComplete` pattern):
|
|
685
|
+
* the checkable fact — the value was assembled over the identical theory —
|
|
686
|
+
* is verified before the raw value is admitted at its typed face.
|
|
687
|
+
*/
|
|
688
|
+
function isQueryValue(theory, value) {
|
|
689
|
+
return value.schema === theory;
|
|
690
|
+
}
|
|
691
|
+
/** Assembles one typed query value (rules already completed). */
|
|
692
|
+
function makeQuery(theory, recs, rules) {
|
|
693
|
+
const raw = makeRawQuery(theory, recs, rules);
|
|
694
|
+
if (!isQueryValue(theory, raw)) {
|
|
695
|
+
throw errors.new("query value construction incomplete");
|
|
696
|
+
}
|
|
697
|
+
return raw;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule`
|
|
701
|
+
* adds one conjunctive rule; multiple rules are the set union (answers are
|
|
702
|
+
* SETS — no order or limit exists anywhere; hosts sort). The schema's
|
|
703
|
+
* law-computed class map rides into every rule builder — the join walls
|
|
704
|
+
* compare class names off it, at the type level and at construction alike.
|
|
705
|
+
*/
|
|
706
|
+
function query(theory) {
|
|
707
|
+
const start = {
|
|
708
|
+
rule(build) {
|
|
709
|
+
const built = build(makeQueryRuleScope(theory.classes));
|
|
710
|
+
return makeQuery(theory, [], [built.rule]);
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
Object.freeze(start);
|
|
714
|
+
return start;
|
|
154
715
|
}
|
|
155
716
|
/** The typed shape refusal of the literal tagger — a genuine failure, never data. */
|
|
156
717
|
function literalShapeError(context, expected, value) {
|
|
@@ -165,20 +726,54 @@ function isIntervalShaped(value) {
|
|
|
165
726
|
typeof value.start === "bigint" &&
|
|
166
727
|
typeof value.end === "bigint");
|
|
167
728
|
}
|
|
729
|
+
/**
|
|
730
|
+
* Tags one closed-reference literal: the bare handle id, verified against
|
|
731
|
+
* the roster (the belt the type level cannot provide — structural values
|
|
732
|
+
* make any bigint spellable here) and tagged u64 — queries cross ids,
|
|
733
|
+
* never handle names.
|
|
734
|
+
*/
|
|
735
|
+
function taggedHandleId(context, closed, value) {
|
|
736
|
+
if (typeof value !== "bigint") {
|
|
737
|
+
throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, value);
|
|
738
|
+
}
|
|
739
|
+
if (closed.handles[Number(value)] === undefined) {
|
|
740
|
+
throw errors.new(`${context}: closed relation ${closed.name} has no handle with id ${value} (roster holds ${closed.handles.length})`);
|
|
741
|
+
}
|
|
742
|
+
return { kind: "u64", value };
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Tags one literal in an interval element domain: a bigint tags as the
|
|
746
|
+
* element (the membership typing rule's point side), an interval-shaped
|
|
747
|
+
* value as the interval (value equality).
|
|
748
|
+
*/
|
|
749
|
+
function taggedAtElementDomain(context, element, value) {
|
|
750
|
+
if (typeof value === "bigint") {
|
|
751
|
+
if (element === "u64") {
|
|
752
|
+
return { kind: "u64", value };
|
|
753
|
+
}
|
|
754
|
+
return { kind: "i64", value };
|
|
755
|
+
}
|
|
756
|
+
if (isIntervalShaped(value)) {
|
|
757
|
+
if (element === "u64") {
|
|
758
|
+
return { kind: "intervalU64", start: value.start, end: value.end };
|
|
759
|
+
}
|
|
760
|
+
return { kind: "intervalI64", start: value.start, end: value.end };
|
|
761
|
+
}
|
|
762
|
+
throw literalShapeError(context, "bigint (point) or { start, end } (interval)", value);
|
|
763
|
+
}
|
|
168
764
|
/**
|
|
169
765
|
* Tags one host literal at a FIELD position (atom bindings): the field's
|
|
170
|
-
* structural
|
|
766
|
+
* structural kind directs the tag, never a guess. At an interval field a
|
|
171
767
|
* bigint literal tags as the ELEMENT type — the IR's membership typing
|
|
172
768
|
* rule (point membership), an interval-shaped literal as the interval
|
|
173
|
-
* (value equality). A closed-reference literal is its
|
|
174
|
-
* u64 after a roster
|
|
175
|
-
* names).
|
|
769
|
+
* (value equality). A closed-reference literal is its bare handle id,
|
|
770
|
+
* tagged u64 after a roster verification.
|
|
176
771
|
*/
|
|
177
772
|
function taggedLiteral(context, field, value) {
|
|
178
|
-
if (
|
|
773
|
+
if ("closed" in field) {
|
|
179
774
|
return taggedHandleId(context, field.closed, value);
|
|
180
775
|
}
|
|
181
|
-
switch (field.
|
|
776
|
+
switch (field.kind) {
|
|
182
777
|
case "bool": {
|
|
183
778
|
if (typeof value !== "boolean") {
|
|
184
779
|
throw literalShapeError(context, "boolean", value);
|
|
@@ -197,68 +792,34 @@ function taggedLiteral(context, field, value) {
|
|
|
197
792
|
}
|
|
198
793
|
return { kind: "i64", value };
|
|
199
794
|
}
|
|
200
|
-
case "
|
|
795
|
+
case "str": {
|
|
201
796
|
if (typeof value !== "string") {
|
|
202
797
|
throw literalShapeError(context, "string", value);
|
|
203
798
|
}
|
|
204
799
|
return { kind: "string", value };
|
|
205
800
|
}
|
|
206
|
-
case "
|
|
801
|
+
case "bytes": {
|
|
207
802
|
if (!(value instanceof Uint8Array)) {
|
|
208
803
|
throw literalShapeError(context, "Uint8Array", value);
|
|
209
804
|
}
|
|
210
805
|
return { kind: "fixedBytes", value };
|
|
211
806
|
}
|
|
212
807
|
case "interval":
|
|
213
|
-
return taggedAtElementDomain(context, field.
|
|
808
|
+
return taggedAtElementDomain(context, field.element, value);
|
|
214
809
|
}
|
|
215
810
|
}
|
|
216
811
|
/**
|
|
217
|
-
* Tags one
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return { kind: "u64", value };
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Tags one literal in an interval element domain: a bigint tags as the
|
|
232
|
-
* element (the membership typing rule's point side), an interval-shaped
|
|
233
|
-
* value as the interval (value equality).
|
|
234
|
-
*/
|
|
235
|
-
function taggedAtElementDomain(context, element, value) {
|
|
236
|
-
if (typeof value === "bigint") {
|
|
237
|
-
if (element === "u64") {
|
|
238
|
-
return { kind: "u64", value };
|
|
239
|
-
}
|
|
240
|
-
return { kind: "i64", value };
|
|
241
|
-
}
|
|
242
|
-
if (isIntervalShaped(value)) {
|
|
243
|
-
if (element === "u64") {
|
|
244
|
-
return { kind: "intervalU64", start: value.start, end: value.end };
|
|
245
|
-
}
|
|
246
|
-
return { kind: "intervalI64", start: value.start, end: value.end };
|
|
247
|
-
}
|
|
248
|
-
throw literalShapeError(context, "bigint (point) or { start, end } (interval)", value);
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Tags one host literal at a COMPARISON position, where no field position
|
|
252
|
-
* directs the type: the sibling term's element domain does — a measure
|
|
253
|
-
* sibling is u64, an interval-field sibling contributes its element type
|
|
254
|
-
* (so both a point literal in `covers` and a `span` literal in `allen`
|
|
255
|
-
* tag correctly), a scalar sibling its own type. At `pointIn` the operand
|
|
256
|
-
* order is interval-left, point-right (`ir::CmpOp::PointIn`), so an
|
|
257
|
-
* interval-shaped literal beside a scalar element-typed sibling is the
|
|
258
|
-
* LEGAL lhs of `covers(span(...), t)` and tags as the interval of the
|
|
259
|
-
* sibling's element domain; under every other operator an interval shape
|
|
260
|
-
* against a scalar sibling stays refused (the engine's IllegalComparison
|
|
261
|
-
* — eq/lt interval-vs-scalar is not a comparison).
|
|
812
|
+
* Tags one host literal at a COMPARISON or PARAM position, where the
|
|
813
|
+
* SIBLING anchors the type: a measure sibling is u64, an interval-field
|
|
814
|
+
* sibling contributes its element domain (so both a point literal in
|
|
815
|
+
* `covers` and a `span` literal in `allen` tag correctly), a scalar
|
|
816
|
+
* sibling its own type. At `pointIn` the operand order is interval-left,
|
|
817
|
+
* point-right (`ir::CmpOp::PointIn`), so an interval-shaped literal
|
|
818
|
+
* beside a scalar element-typed sibling is the LEGAL lhs of
|
|
819
|
+
* `covers(span(...), t)` and tags as the interval of the sibling's
|
|
820
|
+
* element domain; under every other operator an interval shape against a
|
|
821
|
+
* scalar sibling stays refused (the engine's IllegalComparison — the
|
|
822
|
+
* bug-hunt fix, preserved op-aware).
|
|
262
823
|
*/
|
|
263
824
|
function taggedCmpLiteral(context, sibling, value, op) {
|
|
264
825
|
if (sibling === "measure") {
|
|
@@ -267,195 +828,161 @@ function taggedCmpLiteral(context, sibling, value, op) {
|
|
|
267
828
|
}
|
|
268
829
|
return { kind: "u64", value };
|
|
269
830
|
}
|
|
270
|
-
if (sibling.
|
|
271
|
-
return taggedAtElementDomain(context, sibling.
|
|
831
|
+
if (!("closed" in sibling) && sibling.kind === "interval") {
|
|
832
|
+
return taggedAtElementDomain(context, sibling.element, value);
|
|
272
833
|
}
|
|
273
|
-
if (op === "pointIn" &&
|
|
274
|
-
|
|
834
|
+
if (op === "pointIn" &&
|
|
835
|
+
!("closed" in sibling) &&
|
|
836
|
+
(sibling.kind === "u64" || sibling.kind === "i64") &&
|
|
837
|
+
isIntervalShaped(value)) {
|
|
838
|
+
return taggedAtElementDomain(context, sibling.kind, value);
|
|
275
839
|
}
|
|
276
840
|
return taggedLiteral(context, sibling, value);
|
|
277
841
|
}
|
|
278
842
|
/** Creates one rule-scoped variable numberer. */
|
|
279
|
-
function makeVarIds(
|
|
843
|
+
function makeVarIds() {
|
|
280
844
|
const assigned = new Map();
|
|
281
|
-
const state = { next: 0 };
|
|
282
845
|
return {
|
|
283
|
-
of(
|
|
284
|
-
const existing = assigned.get(
|
|
846
|
+
of(name) {
|
|
847
|
+
const existing = assigned.get(name);
|
|
285
848
|
if (existing !== undefined) {
|
|
286
849
|
return existing;
|
|
287
850
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
const id = state.next;
|
|
292
|
-
state.next += 1;
|
|
293
|
-
assigned.set(variable, id);
|
|
294
|
-
return id;
|
|
295
|
-
},
|
|
296
|
-
synthetic() {
|
|
297
|
-
const id = state.next;
|
|
298
|
-
state.next += 1;
|
|
851
|
+
const id = assigned.size;
|
|
852
|
+
assigned.set(name, id);
|
|
299
853
|
return id;
|
|
300
854
|
}
|
|
301
855
|
};
|
|
302
856
|
}
|
|
303
|
-
/** Resolves a param
|
|
304
|
-
function paramIdOf(ctx,
|
|
305
|
-
const
|
|
306
|
-
if (
|
|
307
|
-
throw errors.new(`query lowering: param ${name}
|
|
857
|
+
/** Resolves a param name to its dense positional id. */
|
|
858
|
+
function paramIdOf(ctx, name) {
|
|
859
|
+
const id = ctx.paramIds.get(name);
|
|
860
|
+
if (id === undefined) {
|
|
861
|
+
throw errors.new(`query lowering: param ${name} is not in the query's registry`);
|
|
308
862
|
}
|
|
309
|
-
|
|
310
|
-
return index;
|
|
311
|
-
}
|
|
312
|
-
/** Lowers one atom (either polarity) and appends any `oneOf` disjunctions. */
|
|
313
|
-
function lowerAtom(ctx, atom, ids, extraConditions) {
|
|
314
|
-
const source = lowerSource(ctx, atom.source);
|
|
315
|
-
const bindings = atom.bindings.map(function lowerBinding(binding) {
|
|
316
|
-
return [
|
|
317
|
-
fieldOrdinal(atom.source, binding),
|
|
318
|
-
lowerBindingTerm(ctx, atom.source, binding, ids, extraConditions)
|
|
319
|
-
];
|
|
320
|
-
});
|
|
321
|
-
return { source, bindings };
|
|
863
|
+
return id;
|
|
322
864
|
}
|
|
323
|
-
/**
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
return { kind: "edb", relation: id };
|
|
865
|
+
/**
|
|
866
|
+
* Lowers one EDB atom (either polarity). A CLOSED owner lowers through the
|
|
867
|
+
* same edb source — its ordinal is its record-declaration slot exactly like
|
|
868
|
+
* an ordinary relation's — with field ordinals over the SEALED shape: `id`
|
|
869
|
+
* at 0, each payload column at its declared index + 1 (`matchFieldsOf`
|
|
870
|
+
* carries the shift; the lowering golden pins it).
|
|
871
|
+
*/
|
|
872
|
+
function lowerAtom(ctx, atom, ids) {
|
|
873
|
+
const member = ctx.theory.relations[atom.relation.name];
|
|
874
|
+
if (member !== atom.relation) {
|
|
875
|
+
throw errors.new(`query lowering: relation ${atom.relation.name} is not the relation value schema ${ctx.theory.name} declares`);
|
|
335
876
|
}
|
|
336
|
-
const
|
|
337
|
-
if (
|
|
338
|
-
throw errors.new(`query lowering:
|
|
877
|
+
const relationId = ctx.relationIds.get(atom.relation.name);
|
|
878
|
+
if (relationId === undefined) {
|
|
879
|
+
throw errors.new(`query lowering: relation ${atom.relation.name} has no ordinal`);
|
|
339
880
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
function fieldOrdinal(source, binding) {
|
|
344
|
-
if (source.kind === "relation") {
|
|
345
|
-
const ordinal = source.relation.data.fields.findIndex(function byName(candidate) {
|
|
881
|
+
const ordered = matchFieldsOf(atom.relation);
|
|
882
|
+
const bindings = atom.bindings.map(function lowerBinding(binding) {
|
|
883
|
+
const ordinal = ordered.findIndex(function byName(candidate) {
|
|
346
884
|
return candidate.name === binding.field;
|
|
347
885
|
});
|
|
348
886
|
if (ordinal < 0) {
|
|
349
|
-
throw errors.new(`query lowering: relation ${
|
|
887
|
+
throw errors.new(`query lowering: relation ${atom.relation.name} has no field ${binding.field}`);
|
|
350
888
|
}
|
|
351
|
-
return ordinal;
|
|
352
|
-
}
|
|
353
|
-
const ordinal = source.pred.columns.findIndex(function byName(candidate) {
|
|
354
|
-
return candidate.name === binding.field;
|
|
889
|
+
return [ordinal, lowerBindingTerm(ctx, `${atom.relation.name}.${binding.field}`, binding, ids)];
|
|
355
890
|
});
|
|
356
|
-
|
|
357
|
-
throw errors.new(`query lowering: predicate ${source.pred.name} has no column ${binding.field}`);
|
|
358
|
-
}
|
|
359
|
-
return ordinal;
|
|
891
|
+
return { source: { kind: "edb", relation: relationId }, bindings };
|
|
360
892
|
}
|
|
361
|
-
/** Lowers one binding term
|
|
362
|
-
function lowerBindingTerm(ctx,
|
|
363
|
-
const context = `${sourceName(source)}.${binding.field}`;
|
|
893
|
+
/** Lowers one binding term. */
|
|
894
|
+
function lowerBindingTerm(ctx, context, binding, ids) {
|
|
364
895
|
const bound = binding.term;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
case "maskParam":
|
|
375
|
-
throw errors.new(`${context}: an Allen-mask param is not a field-typed binding`);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
if (bound.kind === "oneOf") {
|
|
379
|
-
const minted = ids.synthetic();
|
|
380
|
-
const leaves = bound.values.map(function equalityLeaf(candidate) {
|
|
381
|
-
return {
|
|
382
|
-
kind: "leaf",
|
|
383
|
-
cmp: {
|
|
384
|
-
op: { kind: "eq" },
|
|
385
|
-
lhs: { kind: "var", var: minted },
|
|
386
|
-
rhs: { kind: "literal", value: taggedLiteral(context, binding.data, candidate) }
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
});
|
|
390
|
-
extraConditions.push({ kind: "or", children: leaves });
|
|
391
|
-
return { kind: "var", var: minted };
|
|
896
|
+
switch (bound.kind) {
|
|
897
|
+
case "var":
|
|
898
|
+
return { kind: "var", var: ids.of(bound.name) };
|
|
899
|
+
case "param":
|
|
900
|
+
return { kind: "param", param: paramIdOf(ctx, bound.name) };
|
|
901
|
+
case "setParam":
|
|
902
|
+
return { kind: "paramSet", param: paramIdOf(ctx, bound.name) };
|
|
903
|
+
case "literal":
|
|
904
|
+
return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) };
|
|
392
905
|
}
|
|
393
|
-
return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) };
|
|
394
906
|
}
|
|
395
|
-
/** Lowers one
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
case "var":
|
|
401
|
-
return { kind: "var", var: ids.of(value) };
|
|
402
|
-
case "param":
|
|
403
|
-
return { kind: "param", param: paramIdOf(ctx, value, value.name) };
|
|
404
|
-
case "paramSet":
|
|
405
|
-
return { kind: "paramSet", param: paramIdOf(ctx, value, value.name) };
|
|
406
|
-
case "maskParam":
|
|
407
|
-
throw errors.new("query lowering: an Allen-mask param is not a comparison term — masks live in allen()'s mask position");
|
|
408
|
-
}
|
|
907
|
+
/** Lowers one idb atom: positional head bindings, `FieldId(i)` = head position i. */
|
|
908
|
+
function lowerIdbAtom(ctx, rec, vars, ids) {
|
|
909
|
+
const pred = ctx.recIds.get(rec);
|
|
910
|
+
if (pred === undefined) {
|
|
911
|
+
throw errors.new(`query lowering: rec ${rec.name} was declared by a different program`);
|
|
409
912
|
}
|
|
410
|
-
|
|
411
|
-
|
|
913
|
+
const arity = rec.rules[0]?.select.length;
|
|
914
|
+
if (arity !== undefined && vars.length !== arity) {
|
|
915
|
+
throw errors.new(`query lowering: idb ${rec.name} takes ${arity} positions, got ${vars.length}`);
|
|
412
916
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
917
|
+
const bindings = vars.map(function lowerPosition(name, position) {
|
|
918
|
+
return [position, { kind: "var", var: ids.of(name) }];
|
|
919
|
+
});
|
|
920
|
+
return { source: { kind: "idb", pred }, bindings };
|
|
921
|
+
}
|
|
922
|
+
/** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
|
|
923
|
+
function lowerCmpTerm(ctx, rule, side, sibling, ids, op) {
|
|
924
|
+
switch (side.kind) {
|
|
925
|
+
case "var":
|
|
926
|
+
return { kind: "var", var: ids.of(side.name) };
|
|
927
|
+
case "param":
|
|
928
|
+
return { kind: "param", param: paramIdOf(ctx, side.name) };
|
|
929
|
+
case "setParam":
|
|
930
|
+
return { kind: "paramSet", param: paramIdOf(ctx, side.name) };
|
|
931
|
+
case "measure":
|
|
932
|
+
return { kind: "measure", var: ids.of(side.name) };
|
|
933
|
+
case "literal": {
|
|
934
|
+
const anchor = cmpAnchorOf(ctx, rule, sibling);
|
|
935
|
+
if (anchor === undefined) {
|
|
936
|
+
throw errors.new("query lowering: a comparison literal needs a bound-variable, measure, or anchored-param sibling to type it");
|
|
937
|
+
}
|
|
938
|
+
return { kind: "literal", value: taggedCmpLiteral("comparison literal", anchor, side.value, op) };
|
|
417
939
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
/** Resolves the anchor a comparison literal tags by: the sibling's field, the measure, or an anchored param. */
|
|
943
|
+
function cmpAnchorOf(ctx, rule, sibling) {
|
|
944
|
+
if (sibling.kind === "var") {
|
|
945
|
+
return rule.varFields[sibling.name]?.field;
|
|
422
946
|
}
|
|
423
947
|
if (sibling.kind === "measure") {
|
|
424
|
-
return
|
|
425
|
-
kind: "literal",
|
|
426
|
-
value: taggedCmpLiteral("comparison literal", "measure", side.value, op)
|
|
427
|
-
};
|
|
948
|
+
return "measure";
|
|
428
949
|
}
|
|
429
|
-
|
|
950
|
+
if (sibling.kind === "param" || sibling.kind === "setParam") {
|
|
951
|
+
return ctx.params.get(sibling.name)?.anchor;
|
|
952
|
+
}
|
|
953
|
+
return undefined;
|
|
430
954
|
}
|
|
431
955
|
/** Lowers one comparison. */
|
|
432
|
-
function lowerComparison(ctx, cmp, ids) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
956
|
+
function lowerComparison(ctx, rule, cmp, ids) {
|
|
957
|
+
if (cmp.op === "allen") {
|
|
958
|
+
const maskData = cmp.mask;
|
|
959
|
+
if (maskData === undefined) {
|
|
960
|
+
throw errors.new("query lowering: an allen comparison lost its mask");
|
|
961
|
+
}
|
|
962
|
+
const mask = maskData.kind === "literal"
|
|
963
|
+
? { kind: "literal", mask: maskData.mask }
|
|
964
|
+
: { kind: "param", param: paramIdOf(ctx, maskData.name) };
|
|
438
965
|
return {
|
|
439
966
|
op: { kind: "allen", mask },
|
|
440
|
-
lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
|
|
441
|
-
rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
|
|
967
|
+
lhs: lowerCmpTerm(ctx, rule, cmp.lhs, cmp.rhs, ids, "allen"),
|
|
968
|
+
rhs: lowerCmpTerm(ctx, rule, cmp.rhs, cmp.lhs, ids, "allen")
|
|
442
969
|
};
|
|
443
970
|
}
|
|
444
971
|
return {
|
|
445
|
-
op: { kind: op
|
|
446
|
-
lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, op
|
|
447
|
-
rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, op
|
|
972
|
+
op: { kind: cmp.op },
|
|
973
|
+
lhs: lowerCmpTerm(ctx, rule, cmp.lhs, cmp.rhs, ids, cmp.op),
|
|
974
|
+
rhs: lowerCmpTerm(ctx, rule, cmp.rhs, cmp.lhs, ids, cmp.op)
|
|
448
975
|
};
|
|
449
976
|
}
|
|
450
977
|
/** Lowers one condition node (comparison leaf or and/or tree). */
|
|
451
|
-
function lowerCondition(ctx,
|
|
452
|
-
if (
|
|
453
|
-
return { kind: "leaf", cmp: lowerComparison(ctx,
|
|
978
|
+
function lowerCondition(ctx, rule, cond, ids) {
|
|
979
|
+
if (cond.kind === "cmp") {
|
|
980
|
+
return { kind: "leaf", cmp: lowerComparison(ctx, rule, cond, ids) };
|
|
454
981
|
}
|
|
455
982
|
return {
|
|
456
|
-
kind:
|
|
457
|
-
children:
|
|
458
|
-
return lowerCondition(ctx, child, ids);
|
|
983
|
+
kind: cond.op,
|
|
984
|
+
children: cond.children.map(function lowerChild(child) {
|
|
985
|
+
return lowerCondition(ctx, rule, child, ids);
|
|
459
986
|
})
|
|
460
987
|
};
|
|
461
988
|
}
|
|
@@ -467,40 +994,35 @@ function lowerFind(entry, ids) {
|
|
|
467
994
|
if (entry.kind === "measure") {
|
|
468
995
|
return { kind: "measure", var: ids.of(entry.over) };
|
|
469
996
|
}
|
|
470
|
-
const
|
|
471
|
-
switch (
|
|
997
|
+
const agg = entry.agg;
|
|
998
|
+
switch (agg.op) {
|
|
472
999
|
case "count":
|
|
473
1000
|
return { kind: "aggregate", op: { kind: "count" } };
|
|
474
1001
|
case "countDistinct":
|
|
475
|
-
return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(
|
|
1002
|
+
return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) };
|
|
476
1003
|
case "fold": {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
return { kind: "aggregate", op: { kind: aggregate.fold }, over: ids.of(over) };
|
|
1004
|
+
if (typeof agg.over === "string") {
|
|
1005
|
+
return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) };
|
|
480
1006
|
}
|
|
481
|
-
return { kind: "aggregateMeasure", op: { kind:
|
|
1007
|
+
return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) };
|
|
482
1008
|
}
|
|
483
1009
|
case "arg":
|
|
484
|
-
return {
|
|
485
|
-
kind: "aggregate",
|
|
486
|
-
op: { kind: aggregate.direction, key: ids.of(aggregate.key) },
|
|
487
|
-
over: ids.of(aggregate.over)
|
|
488
|
-
};
|
|
1010
|
+
return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) };
|
|
489
1011
|
case "pack":
|
|
490
|
-
return { kind: "aggregate", op: { kind: "pack" }, over: ids.of(
|
|
1012
|
+
return { kind: "aggregate", op: { kind: "pack" }, over: ids.of(agg.over) };
|
|
491
1013
|
}
|
|
492
1014
|
}
|
|
493
1015
|
/** One aggregate's var-free head-op kind (`AggOp::head_op`). */
|
|
494
|
-
function headOpOf(
|
|
495
|
-
switch (
|
|
1016
|
+
function headOpOf(agg) {
|
|
1017
|
+
switch (agg.op) {
|
|
496
1018
|
case "count":
|
|
497
1019
|
return "count";
|
|
498
1020
|
case "countDistinct":
|
|
499
1021
|
return "countDistinct";
|
|
500
1022
|
case "fold":
|
|
501
|
-
return
|
|
1023
|
+
return agg.fold;
|
|
502
1024
|
case "arg":
|
|
503
|
-
return
|
|
1025
|
+
return agg.direction;
|
|
504
1026
|
case "pack":
|
|
505
1027
|
return "pack";
|
|
506
1028
|
}
|
|
@@ -511,42 +1033,51 @@ function headTermOf(column) {
|
|
|
511
1033
|
if (entry.kind === "var" || entry.kind === "measure") {
|
|
512
1034
|
return { kind: "var" };
|
|
513
1035
|
}
|
|
514
|
-
return { kind: "aggregate", op: headOpOf(entry.
|
|
1036
|
+
return { kind: "aggregate", op: headOpOf(entry.agg) };
|
|
515
1037
|
}
|
|
516
|
-
/** Lowers one rule: body walked in written order, finds
|
|
517
|
-
function lowerRule(ctx,
|
|
518
|
-
const ids = makeVarIds(
|
|
1038
|
+
/** Lowers one rule: body walked in written order (var ids by first occurrence), finds last. */
|
|
1039
|
+
function lowerRule(ctx, rule) {
|
|
1040
|
+
const ids = makeVarIds();
|
|
519
1041
|
const atoms = [];
|
|
520
1042
|
const negated = [];
|
|
521
1043
|
const conditions = [];
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
negated.push(lowered);
|
|
1044
|
+
for (const item of rule.items) {
|
|
1045
|
+
switch (item.kind) {
|
|
1046
|
+
case "atom": {
|
|
1047
|
+
atoms.push(lowerAtom(ctx, item.atom, ids));
|
|
1048
|
+
break;
|
|
528
1049
|
}
|
|
529
|
-
|
|
530
|
-
|
|
1050
|
+
case "negated": {
|
|
1051
|
+
negated.push(lowerAtom(ctx, item.atom, ids));
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
case "idb": {
|
|
1055
|
+
atoms.push(lowerIdbAtom(ctx, item.rec, item.vars, ids));
|
|
1056
|
+
break;
|
|
1057
|
+
}
|
|
1058
|
+
case "cond": {
|
|
1059
|
+
conditions.push(lowerCondition(ctx, rule, item.cond, ids));
|
|
1060
|
+
break;
|
|
531
1061
|
}
|
|
532
|
-
continue;
|
|
533
1062
|
}
|
|
534
|
-
conditions.push(lowerCondition(ctx, item, ids));
|
|
535
1063
|
}
|
|
536
1064
|
return {
|
|
537
|
-
finds:
|
|
1065
|
+
finds: rule.select.map(function findOf(column) {
|
|
1066
|
+
return lowerFind(column.entry, ids);
|
|
1067
|
+
}),
|
|
538
1068
|
atoms,
|
|
539
1069
|
negated,
|
|
540
|
-
conditions
|
|
1070
|
+
conditions
|
|
541
1071
|
};
|
|
542
1072
|
}
|
|
543
1073
|
/**
|
|
544
1074
|
* Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
1075
|
+
* recs in declaration order (`PredId` = index), the output predicate
|
|
1076
|
+
* (rules + head) appended last. Relations lower by declaration ordinal,
|
|
1077
|
+
* the law the engine's own manifest pins; `db.prepare` re-verifies the
|
|
1078
|
+
* alignment against the live manifest before sending. Every registered
|
|
1079
|
+
* param must carry a field anchor by now — an unanchorable param (its
|
|
1080
|
+
* every use beside a literal) is refused here, naming it.
|
|
550
1081
|
*/
|
|
551
1082
|
function lowerQuery(q) {
|
|
552
1083
|
const theory = q.schema;
|
|
@@ -554,51 +1085,39 @@ function lowerQuery(q) {
|
|
|
554
1085
|
Object.keys(theory.relations).forEach(function assignOrdinal(name, index) {
|
|
555
1086
|
relationIds.set(name, index);
|
|
556
1087
|
});
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
1088
|
+
const recIds = new Map();
|
|
1089
|
+
q.data.recs.forEach(function assignPredId(rec, index) {
|
|
1090
|
+
recIds.set(rec, index);
|
|
1091
|
+
});
|
|
1092
|
+
const paramIds = new Map();
|
|
1093
|
+
const params = new Map();
|
|
1094
|
+
q.data.params.forEach(function assignParamId(entry, index) {
|
|
1095
|
+
if (entry.anchor === undefined && entry.shape !== "mask") {
|
|
1096
|
+
throw errors.new(`query param ${entry.name} has no field-anchored use — bind it in an atom or compare it against a bound variable`);
|
|
1097
|
+
}
|
|
1098
|
+
paramIds.set(entry.name, index);
|
|
1099
|
+
params.set(entry.name, entry);
|
|
561
1100
|
});
|
|
562
|
-
const ctx = { theory, relationIds,
|
|
563
|
-
const predicates =
|
|
1101
|
+
const ctx = { theory, relationIds, recIds, paramIds, params };
|
|
1102
|
+
const predicates = q.data.recs.map(function lowerRec(rec) {
|
|
1103
|
+
const head = rec.rules[0];
|
|
1104
|
+
if (head === undefined) {
|
|
1105
|
+
throw errors.new(`query lowering: rec ${rec.name} has no rules`);
|
|
1106
|
+
}
|
|
564
1107
|
return {
|
|
565
|
-
head:
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
rules: pred.rules.map(function lowerClause(rule) {
|
|
569
|
-
return lowerRule(ctx, rule.body, function clauseFinds(ids) {
|
|
570
|
-
return rule.finds.map(function findVar(variable) {
|
|
571
|
-
return { kind: "var", var: ids.of(variable) };
|
|
572
|
-
});
|
|
573
|
-
});
|
|
1108
|
+
head: head.select.map(headTermOf),
|
|
1109
|
+
rules: rec.rules.map(function lowerRecRule(rule) {
|
|
1110
|
+
return lowerRule(ctx, rule);
|
|
574
1111
|
})
|
|
575
1112
|
};
|
|
576
1113
|
});
|
|
577
1114
|
predicates.push({
|
|
578
1115
|
head: q.data.select.map(headTermOf),
|
|
579
|
-
rules: q.data.rules.map(function lowerOutputRule(
|
|
580
|
-
return lowerRule(ctx,
|
|
581
|
-
return q.data.select.map(function findOf(column) {
|
|
582
|
-
return lowerFind(column.entry, ids);
|
|
583
|
-
});
|
|
584
|
-
});
|
|
1116
|
+
rules: q.data.rules.map(function lowerOutputRule(rule) {
|
|
1117
|
+
return lowerRule(ctx, rule);
|
|
585
1118
|
})
|
|
586
1119
|
});
|
|
587
|
-
|
|
588
|
-
* The dead-declaration refusal: a declared param no rule reached is
|
|
589
|
-
* unexecutable BOTH ways — its inferred `Params` contribution is nothing
|
|
590
|
-
* (params ride item phantoms), yet the wire marshal is the full registry
|
|
591
|
-
* in declaration order, while the ENGINE's arity is usage-derived. The
|
|
592
|
-
* contradiction is refused here, the earliest seam that knows usage, so
|
|
593
|
-
* every query that lowers has registry == used set (and the dense-id
|
|
594
|
-
* hole a skipped middle declaration would open never exists).
|
|
595
|
-
*/
|
|
596
|
-
registry.params.forEach(function assertUsed(entry, index) {
|
|
597
|
-
if (!ctx.usedParams.has(index)) {
|
|
598
|
-
throw errors.new(`query declares param ${entry.name} but no rule uses it — remove the declaration or reference it`);
|
|
599
|
-
}
|
|
600
|
-
});
|
|
601
|
-
return { predicates, output: registry.predicates.length };
|
|
1120
|
+
return { predicates, output: q.data.recs.length };
|
|
602
1121
|
}
|
|
603
|
-
export { lowerQuery, query, taggedLiteral };
|
|
1122
|
+
export { lowerQuery, makeOutputRuleScope, makeQuery, makeRawScope, query, taggedCmpLiteral, taggedLiteral };
|
|
604
1123
|
//# sourceMappingURL=lower.js.map
|