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