@bjornpagen/bumbledb 0.5.0 → 0.6.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 +96 -131
- package/README.md +9 -9
- package/dist/db.js +2 -2
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/query/atom.d.ts +132 -201
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +30 -42
- package/dist/query/atom.js.map +1 -1
- package/dist/query/find.d.ts +116 -0
- package/dist/query/find.d.ts.map +1 -0
- package/dist/query/{select.js → find.js} +22 -22
- package/dist/query/find.js.map +1 -0
- package/dist/query/lower.d.ts +123 -158
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +437 -493
- package/dist/query/lower.js.map +1 -1
- package/dist/query/predicate.d.ts +22 -14
- package/dist/query/predicate.d.ts.map +1 -1
- package/dist/query/predicate.js +35 -42
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/run.d.ts +3 -3
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +9 -9
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +127 -72
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +80 -33
- package/dist/query/scope.js.map +1 -1
- package/package.json +2 -2
- package/src/db.ts +4 -4
- package/src/index.ts +11 -7
- package/src/query/atom.ts +181 -263
- package/src/query/find.ts +212 -0
- package/src/query/lower.ts +588 -722
- package/src/query/predicate.ts +37 -45
- package/src/query/run.ts +10 -10
- package/src/query/scope.ts +172 -87
- package/dist/query/select.d.ts +0 -128
- package/dist/query/select.d.ts.map +0 -1
- package/dist/query/select.js.map +0 -1
- package/src/query/select.ts +0 -215
package/dist/query/lower.js
CHANGED
|
@@ -1,38 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `query()` and the IR lowering,
|
|
3
|
-
* kysely-shaped —
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* name-
|
|
22
|
-
* the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
2
|
+
* `query()` and the IR lowering, REFERENCE-IDENTITY edition. A query is
|
|
3
|
+
* built kysely-shaped — variables minted by {@link v} outside the rule and
|
|
4
|
+
* reused by REFERENCE to join:
|
|
5
|
+
*
|
|
6
|
+
* query(S).rule((r) => {
|
|
7
|
+
* const acct = v(Account)
|
|
8
|
+
* const h = v(Holder)
|
|
9
|
+
* return r
|
|
10
|
+
* .match(Account, { id: acct.id, holder: acct.holder })
|
|
11
|
+
* .match(Holder, { id: acct.holder })
|
|
12
|
+
* .where(r.eq(acct.holder, r.param("root")))
|
|
13
|
+
* .find({ account: acct.id, holder: acct.holder })
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* — and is an INERT value: `Query<Rels, Row, Params>` with `Row` inferred
|
|
17
|
+
* from each rule's `.find` RECORD (its keys ARE the answer columns) and
|
|
18
|
+
* `Params` inferred to be EXACTLY the params the rules use (params are typed
|
|
19
|
+
* BY USE; a param no rule uses never registers). Variable IDENTITY is the
|
|
20
|
+
* object reference: reusing one value across binding positions IS the join,
|
|
21
|
+
* and a name-collision join is unrepresentable. Each binding position is
|
|
22
|
+
* judged against the variable's MINT slot — and because {@link JoinOk} is an
|
|
23
|
+
* equality, that alone makes every cross-binding join transitively
|
|
24
|
+
* class-equal. Lowering is a pure function of the query value down to the
|
|
25
|
+
* bridge's `ProgramIr` (`bumbledb/crates/bumbledb/src/ir.rs`): relations by
|
|
26
|
+
* declaration ordinal, variables by dense per-rule first-occurrence ids
|
|
27
|
+
* (keyed on the object REFERENCE — the discipline is unchanged, only the map
|
|
28
|
+
* key moved from name to reference), params by first-use order. Lowering is
|
|
29
|
+
* STABLE — the same query value lowers to deeply-equal IR every time, and
|
|
30
|
+
* two identically-written queries (fresh mints each) lower identically.
|
|
31
|
+
* Construction validates negation safety and boundness (typed by the var's
|
|
32
|
+
* label — object identity is invisible to the type tier, so these are
|
|
33
|
+
* construction-time walls); everything else (strata, types, aggregate
|
|
34
|
+
* rosters, rule caps) is the ENGINE's judge, surfacing at prepare.
|
|
26
35
|
*/
|
|
27
36
|
import * as errors from "@superbuilders/errors";
|
|
28
37
|
import { sealedFieldsOf } from "#closed.ts";
|
|
29
38
|
import { assertDeclarationOrderKey, isIntervalValue, literalShapeError, rosterOf } from "#fields.ts";
|
|
30
39
|
import { allen, and, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts";
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
40
|
+
import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/find.ts";
|
|
41
|
+
import { fieldJoins, inferred, isTerm, makeDuration, makeMaskParam, makeParam, makeSetParam, renderFieldKind, term } from "#query/scope.ts";
|
|
33
42
|
/** The frozen constructor vocabulary every rule builder spreads. */
|
|
34
43
|
const termOps = Object.freeze({
|
|
35
|
-
var: makeVar,
|
|
36
44
|
param: makeParam,
|
|
37
45
|
inSet: makeSetParam,
|
|
38
46
|
maskParam: makeMaskParam,
|
|
@@ -60,23 +68,28 @@ const termOps = Object.freeze({
|
|
|
60
68
|
/** The empty rule state. */
|
|
61
69
|
const EMPTY_RULE = Object.freeze({
|
|
62
70
|
items: Object.freeze([]),
|
|
63
|
-
|
|
71
|
+
bound: new Set(),
|
|
64
72
|
paramUses: Object.freeze([])
|
|
65
73
|
});
|
|
74
|
+
/**
|
|
75
|
+
* The MINT slot of a variable, the runtime twin of {@link MintSlotOf}: (i)
|
|
76
|
+
* verifies the mint owner is the schema's own member value — a variable
|
|
77
|
+
* minted from a foreign relation is refused, naming its label — and (ii)
|
|
78
|
+
* returns the descriptor it was minted at plus the law-computed class read
|
|
79
|
+
* off the schema's frozen class map. Because {@link fieldJoins} is an
|
|
80
|
+
* equality, judging every binding position against this one slot makes all
|
|
81
|
+
* cross-binding joins mutually class-equal by transitivity.
|
|
82
|
+
*/
|
|
83
|
+
function mintSlotOf(context, ref) {
|
|
84
|
+
if (context.theory.relations[ref.owner.name] !== ref.owner) {
|
|
85
|
+
throw errors.new(`the variable ${ref.label} was minted from a relation schema ${context.theory.name} does not declare — mint variables with v() from the schema's own relations`);
|
|
86
|
+
}
|
|
87
|
+
return { field: ref.field, class: context.classes[ref.owner.name]?.[ref.column] };
|
|
88
|
+
}
|
|
66
89
|
/**
|
|
67
90
|
* Judges one membership ARRAY at a binding position — legal exactly at a
|
|
68
|
-
* CLOSED-reference field
|
|
69
|
-
*
|
|
70
|
-
* vocabulary's spelling), holding ≥ 2 DISTINCT handle names (the
|
|
71
|
-
* degenerate sets are refusals: empty selects nothing, one element is the
|
|
72
|
-
* bare literal respelled, and a duplicate member is the same respelling in
|
|
73
|
-
* disguise — write each member once). The returned name is
|
|
74
|
-
* CONTENT-ADDRESSED (vocabulary + the member SET — the key sorts a copy,
|
|
75
|
-
* so two spellings of one set, reordered or not, share one dense
|
|
76
|
-
* `ParamId`); the members are shape-checked strings here and
|
|
77
|
-
* roster-verified at the one verification point (`taggedHandleId`) when
|
|
78
|
-
* the SDK supplies the set at execute — the same moment a bound `r.inSet`
|
|
79
|
-
* param's members are judged.
|
|
91
|
+
* CLOSED-reference field, holding ≥ 2 DISTINCT handle names. The returned
|
|
92
|
+
* name is CONTENT-ADDRESSED (vocabulary + the member SET).
|
|
80
93
|
*/
|
|
81
94
|
function membershipSet(context, field, value) {
|
|
82
95
|
const roster = rosterOf(field);
|
|
@@ -104,19 +117,18 @@ function membershipSet(context, field, value) {
|
|
|
104
117
|
return { name: `∈ ${roster.name} ${JSON.stringify(key)}`, members: Object.freeze(members) };
|
|
105
118
|
}
|
|
106
119
|
/**
|
|
107
|
-
* Resolves a bindings record against an atom owner's matchable fields
|
|
108
|
-
* relation's declared fields; a closed relation's sealed id + columns), in
|
|
120
|
+
* Resolves a bindings record against an atom owner's matchable fields, in
|
|
109
121
|
* the record's written order: terms classify by their runtime tag,
|
|
110
|
-
* everything else is a bare literal
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
122
|
+
* everything else is a bare literal. Every VARIABLE binding judges
|
|
123
|
+
* `fieldJoins(mintSlot, positionSlot)` and throws on a class-unequal reuse
|
|
124
|
+
* (the runtime twin of `CheckBindings`); the bound refs are collected for
|
|
125
|
+
* the rule's boundness set.
|
|
114
126
|
*/
|
|
115
|
-
function resolveBindings(context, relation, bindings
|
|
127
|
+
function resolveBindings(context, label, relation, bindings) {
|
|
116
128
|
const entries = [];
|
|
117
129
|
const vars = [];
|
|
118
130
|
const uses = [];
|
|
119
|
-
const relationClasses = classes[relation.name];
|
|
131
|
+
const relationClasses = context.classes[relation.name];
|
|
120
132
|
const ordered = sealedFieldsOf(relation);
|
|
121
133
|
for (const [fieldName, value] of Object.entries(bindings)) {
|
|
122
134
|
if (value === undefined) {
|
|
@@ -126,15 +138,21 @@ function resolveBindings(context, relation, bindings, classes) {
|
|
|
126
138
|
return candidate.name === fieldName;
|
|
127
139
|
});
|
|
128
140
|
if (declared === undefined) {
|
|
129
|
-
throw errors.new(`${
|
|
141
|
+
throw errors.new(`${label} has no field ${fieldName}`);
|
|
130
142
|
}
|
|
131
143
|
const fieldClass = relationClasses?.[fieldName];
|
|
132
144
|
let bound;
|
|
133
145
|
if (isTerm(value)) {
|
|
134
146
|
switch (value[term]) {
|
|
135
147
|
case "var": {
|
|
136
|
-
|
|
137
|
-
|
|
148
|
+
const ref = value;
|
|
149
|
+
const mint = mintSlotOf(context, ref);
|
|
150
|
+
const positionSlot = { field: declared.field, class: fieldClass };
|
|
151
|
+
if (!fieldJoins(mint, positionSlot)) {
|
|
152
|
+
throw errors.new(`${label}: the variable ${ref.label} joins domain-unequal fields — minted at ${renderFieldKind(mint)}, reused at ${renderFieldKind(positionSlot)} (a var joins only class-equal slots; bare pairs only with bare)`);
|
|
153
|
+
}
|
|
154
|
+
bound = Object.freeze({ kind: "var", ref });
|
|
155
|
+
vars.push(ref);
|
|
138
156
|
break;
|
|
139
157
|
}
|
|
140
158
|
case "param": {
|
|
@@ -160,13 +178,13 @@ function resolveBindings(context, relation, bindings, classes) {
|
|
|
160
178
|
break;
|
|
161
179
|
}
|
|
162
180
|
case "maskParam":
|
|
163
|
-
throw errors.new(`${
|
|
181
|
+
throw errors.new(`${label}.${fieldName}: an Allen-mask param is not a field-typed value — masks live in allen() conditions only`);
|
|
164
182
|
case "duration":
|
|
165
|
-
throw errors.new(`${
|
|
183
|
+
throw errors.new(`${label}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and find entries`);
|
|
166
184
|
}
|
|
167
185
|
}
|
|
168
186
|
else if (Array.isArray(value)) {
|
|
169
|
-
const set = membershipSet(`${
|
|
187
|
+
const set = membershipSet(`${label}.${fieldName}`, declared.field, value);
|
|
170
188
|
bound = Object.freeze({ kind: "literalSet", name: set.name, members: set.members });
|
|
171
189
|
uses.push(Object.freeze({
|
|
172
190
|
name: set.name,
|
|
@@ -181,49 +199,33 @@ function resolveBindings(context, relation, bindings, classes) {
|
|
|
181
199
|
}
|
|
182
200
|
entries.push(Object.freeze({ field: fieldName, data: declared.field, class: fieldClass, term: bound }));
|
|
183
201
|
}
|
|
184
|
-
return {
|
|
185
|
-
atom: Object.freeze({ relation, bindings: Object.freeze(entries) }),
|
|
186
|
-
vars,
|
|
187
|
-
uses
|
|
188
|
-
};
|
|
202
|
+
return { atom: Object.freeze({ relation, bindings: Object.freeze(entries) }), vars, uses };
|
|
189
203
|
}
|
|
190
|
-
/**
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
*/
|
|
197
|
-
function advanceMatch(state, relation, bindings, classes) {
|
|
198
|
-
const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings, classes);
|
|
199
|
-
const varFields = { ...state.varFields };
|
|
200
|
-
for (const bound of resolved.vars) {
|
|
201
|
-
const existing = varFields[bound.name];
|
|
202
|
-
if (existing === undefined) {
|
|
203
|
-
varFields[bound.name] = bound.slot;
|
|
204
|
-
}
|
|
205
|
-
else if (!fieldJoins(existing, bound.slot)) {
|
|
206
|
-
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)`);
|
|
207
|
-
}
|
|
204
|
+
/** Extends a rule state with one positive atom; the bound variable references accumulate into the boundness set. */
|
|
205
|
+
function advanceMatch(context, state, relation, bindings) {
|
|
206
|
+
const resolved = resolveBindings(context, `relation ${relation.name}`, relation, bindings);
|
|
207
|
+
const bound = new Set(state.bound);
|
|
208
|
+
for (const ref of resolved.vars) {
|
|
209
|
+
bound.add(ref);
|
|
208
210
|
}
|
|
209
|
-
return {
|
|
211
|
+
return Object.freeze({
|
|
210
212
|
items: Object.freeze([...state.items, Object.freeze({ kind: "atom", atom: resolved.atom })]),
|
|
211
|
-
|
|
213
|
+
bound,
|
|
212
214
|
paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
|
|
213
|
-
};
|
|
215
|
+
});
|
|
214
216
|
}
|
|
215
|
-
/** Resolves one comparison side to its runtime term. */
|
|
217
|
+
/** Resolves one comparison side to its runtime term (variables and the measure ride by reference). */
|
|
216
218
|
function cmpTermDataOf(op, value) {
|
|
217
219
|
if (isTerm(value)) {
|
|
218
220
|
switch (value[term]) {
|
|
219
221
|
case "var":
|
|
220
|
-
return Object.freeze({ kind: "var",
|
|
222
|
+
return Object.freeze({ kind: "var", ref: value });
|
|
221
223
|
case "param":
|
|
222
224
|
return Object.freeze({ kind: "param", name: value.name });
|
|
223
225
|
case "setParam":
|
|
224
226
|
return Object.freeze({ kind: "setParam", name: value.name });
|
|
225
227
|
case "duration":
|
|
226
|
-
return Object.freeze({ kind: "measure",
|
|
228
|
+
return Object.freeze({ kind: "measure", ref: value.over });
|
|
227
229
|
case "maskParam":
|
|
228
230
|
throw errors.new(`${op}: an Allen-mask param is not a comparison term — masks live in allen()'s mask position`);
|
|
229
231
|
}
|
|
@@ -232,17 +234,16 @@ function cmpTermDataOf(op, value) {
|
|
|
232
234
|
}
|
|
233
235
|
/**
|
|
234
236
|
* One comparison side's contribution to the param census: a param/set side
|
|
235
|
-
* anchors to its SIBLING — a
|
|
236
|
-
*
|
|
237
|
-
* anchor and must be anchored by some other use of the same name.
|
|
237
|
+
* anchors to its SIBLING — a variable's field descriptor or the measure; an
|
|
238
|
+
* unanchorable use records with no anchor.
|
|
238
239
|
*/
|
|
239
|
-
function sideUses(op, side, sibling,
|
|
240
|
+
function sideUses(op, side, sibling, uses) {
|
|
240
241
|
if (side.kind !== "param" && side.kind !== "setParam") {
|
|
241
242
|
return;
|
|
242
243
|
}
|
|
243
244
|
let anchor;
|
|
244
245
|
if (sibling.kind === "var") {
|
|
245
|
-
anchor =
|
|
246
|
+
anchor = sibling.ref.field;
|
|
246
247
|
}
|
|
247
248
|
else if (sibling.kind === "measure") {
|
|
248
249
|
anchor = "measure";
|
|
@@ -259,12 +260,12 @@ function sideUses(op, side, sibling, varFields, uses) {
|
|
|
259
260
|
}));
|
|
260
261
|
}
|
|
261
262
|
/** Lowers one condition VALUE to its runtime data, recording param uses. */
|
|
262
|
-
function condDataOf(cond,
|
|
263
|
+
function condDataOf(cond, uses) {
|
|
263
264
|
if (cond.cond === "cmp") {
|
|
264
265
|
const lhs = cmpTermDataOf(cond.op, cond.lhs);
|
|
265
266
|
const rhs = cmpTermDataOf(cond.op, cond.rhs);
|
|
266
|
-
sideUses(cond.op, lhs, rhs,
|
|
267
|
-
sideUses(cond.op, rhs, lhs,
|
|
267
|
+
sideUses(cond.op, lhs, rhs, uses);
|
|
268
|
+
sideUses(cond.op, rhs, lhs, uses);
|
|
268
269
|
let mask;
|
|
269
270
|
if (cond.op === "allen") {
|
|
270
271
|
const maskValue = cond.mask;
|
|
@@ -285,20 +286,18 @@ function condDataOf(cond, varFields, uses) {
|
|
|
285
286
|
throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam");
|
|
286
287
|
}
|
|
287
288
|
}
|
|
288
|
-
|
|
289
|
-
return data;
|
|
289
|
+
return Object.freeze({ kind: "cmp", op: cond.op, mask, lhs, rhs });
|
|
290
290
|
}
|
|
291
291
|
if (cond.cond === "tree") {
|
|
292
292
|
const children = cond.children.map(function lowerChild(child) {
|
|
293
|
-
return condDataOf(child,
|
|
293
|
+
return condDataOf(child, uses);
|
|
294
294
|
});
|
|
295
|
-
|
|
296
|
-
return data;
|
|
295
|
+
return Object.freeze({ kind: "tree", op: cond.op, children: Object.freeze(children) });
|
|
297
296
|
}
|
|
298
297
|
throw errors.new("a negated atom is not a condition-tree node — pass not(...) to where() directly, never inside and()/or()");
|
|
299
298
|
}
|
|
300
299
|
/** Extends a rule state with one `.where` item (a condition or a negated atom). */
|
|
301
|
-
function advanceWhere(state, cond
|
|
300
|
+
function advanceWhere(context, state, cond) {
|
|
302
301
|
if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
|
|
303
302
|
throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom");
|
|
304
303
|
}
|
|
@@ -307,191 +306,177 @@ function advanceWhere(state, cond, classes) {
|
|
|
307
306
|
const bindings = Object.fromEntries(Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
|
|
308
307
|
return value !== undefined;
|
|
309
308
|
}));
|
|
310
|
-
const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings
|
|
311
|
-
return {
|
|
309
|
+
const resolved = resolveBindings(context, `negated relation ${relation.name}`, relation, bindings);
|
|
310
|
+
return Object.freeze({
|
|
312
311
|
items: Object.freeze([...state.items, Object.freeze({ kind: "negated", atom: resolved.atom })]),
|
|
313
|
-
|
|
312
|
+
bound: state.bound,
|
|
314
313
|
paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
|
|
315
|
-
};
|
|
314
|
+
});
|
|
316
315
|
}
|
|
317
316
|
const uses = [];
|
|
318
|
-
const data = condDataOf(cond,
|
|
319
|
-
return {
|
|
317
|
+
const data = condDataOf(cond, uses);
|
|
318
|
+
return Object.freeze({
|
|
320
319
|
items: Object.freeze([...state.items, Object.freeze({ kind: "cond", cond: data })]),
|
|
321
|
-
|
|
320
|
+
bound: state.bound,
|
|
322
321
|
paramUses: Object.freeze([...state.paramUses, ...uses])
|
|
323
|
-
};
|
|
322
|
+
});
|
|
324
323
|
}
|
|
325
|
-
/** Extends a rule state with one `idb` atom (
|
|
326
|
-
function advanceIdb(state, rec,
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
324
|
+
/** Extends a rule state with one `idb` atom (a named record over head keys; vars validated at completion). */
|
|
325
|
+
function advanceIdb(state, rec, bindings) {
|
|
326
|
+
const resolved = [];
|
|
327
|
+
for (const [key, value] of Object.entries(bindings)) {
|
|
328
|
+
if (value === undefined) {
|
|
329
|
+
continue;
|
|
330
330
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
331
|
+
if (!isTerm(value) || value[term] !== "var") {
|
|
332
|
+
throw errors.new(`idb ${rec.name}: position ${key} takes a variable — bind literals and params through where()/match()`);
|
|
333
|
+
}
|
|
334
|
+
resolved.push(Object.freeze({ key, ref: value }));
|
|
335
|
+
}
|
|
336
|
+
return Object.freeze({
|
|
337
|
+
items: Object.freeze([
|
|
338
|
+
...state.items,
|
|
339
|
+
Object.freeze({ kind: "idb", rec, bindings: Object.freeze(resolved) })
|
|
340
|
+
]),
|
|
341
|
+
bound: state.bound,
|
|
336
342
|
paramUses: state.paramUses
|
|
337
|
-
};
|
|
343
|
+
});
|
|
338
344
|
}
|
|
339
|
-
/** Narrows a
|
|
345
|
+
/** Narrows a find entry to an aggregate value. */
|
|
340
346
|
function isAggregateEntry(value) {
|
|
341
347
|
return typeof value === "object" && value !== null && "agg" in value;
|
|
342
348
|
}
|
|
343
|
-
/**
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
* bare.
|
|
348
|
-
*/
|
|
349
|
-
function selectColumnOf(entry) {
|
|
350
|
-
if (typeof entry === "string") {
|
|
351
|
-
return Object.freeze({
|
|
352
|
-
name: entry,
|
|
353
|
-
entry: Object.freeze({ kind: "var", over: entry }),
|
|
354
|
-
closed: undefined
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
if (isTerm(entry)) {
|
|
358
|
-
if (entry[term] === "duration") {
|
|
359
|
-
return Object.freeze({
|
|
360
|
-
name: entry.name,
|
|
361
|
-
entry: Object.freeze({ kind: "measure", over: entry.name }),
|
|
362
|
-
closed: undefined
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
throw errors.new(`query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`);
|
|
366
|
-
}
|
|
367
|
-
if (isAggregateEntry(entry)) {
|
|
368
|
-
return aggregateColumnOf(entry);
|
|
349
|
+
/** Narrows a value to a variable term, else a pointed refusal. */
|
|
350
|
+
function asVarTerm(context, value) {
|
|
351
|
+
if (isTerm(value) && value[term] === "var") {
|
|
352
|
+
return value;
|
|
369
353
|
}
|
|
370
|
-
throw errors.new(
|
|
354
|
+
throw errors.new(`${context}: expected a variable`);
|
|
371
355
|
}
|
|
372
|
-
/** Classifies one aggregate
|
|
373
|
-
function
|
|
374
|
-
function column(name, agg) {
|
|
375
|
-
return Object.freeze({
|
|
376
|
-
name,
|
|
377
|
-
entry: Object.freeze({ kind: "aggregate", agg: Object.freeze(agg) }),
|
|
378
|
-
closed: undefined
|
|
379
|
-
});
|
|
380
|
-
}
|
|
356
|
+
/** Classifies one aggregate find entry into its runtime data (variables ride by reference). */
|
|
357
|
+
function aggDataOf(name, entry) {
|
|
381
358
|
const over = entry.over;
|
|
382
359
|
switch (entry.agg) {
|
|
383
360
|
case "count":
|
|
384
|
-
return
|
|
385
|
-
case "countDistinct":
|
|
386
|
-
|
|
387
|
-
throw errors.new("countDistinct takes a variable name");
|
|
388
|
-
}
|
|
389
|
-
return column(over, { op: "countDistinct", over });
|
|
390
|
-
}
|
|
361
|
+
return Object.freeze({ op: "count" });
|
|
362
|
+
case "countDistinct":
|
|
363
|
+
return Object.freeze({ op: "countDistinct", over: asVarTerm(`find ${name} (countDistinct)`, over) });
|
|
391
364
|
case "sum":
|
|
392
365
|
case "min":
|
|
393
366
|
case "max": {
|
|
394
|
-
if (
|
|
395
|
-
return
|
|
367
|
+
if (isTerm(over) && over[term] === "var") {
|
|
368
|
+
return Object.freeze({ op: "fold", fold: entry.agg, over });
|
|
396
369
|
}
|
|
397
370
|
if (isTerm(over) && over[term] === "duration") {
|
|
398
|
-
return
|
|
371
|
+
return Object.freeze({ op: "fold", fold: entry.agg, over: Object.freeze({ duration: over.over }) });
|
|
399
372
|
}
|
|
400
|
-
throw errors.new(
|
|
373
|
+
throw errors.new(`find ${name} (${entry.agg}): takes a variable or r.duration(v)`);
|
|
401
374
|
}
|
|
402
375
|
case "argMax":
|
|
403
|
-
case "argMin":
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
376
|
+
case "argMin":
|
|
377
|
+
return Object.freeze({
|
|
378
|
+
op: "arg",
|
|
379
|
+
direction: entry.agg,
|
|
380
|
+
over: asVarTerm(`find ${name} (${entry.agg})`, over),
|
|
381
|
+
key: asVarTerm(`find ${name} (${entry.agg} key)`, entry.key)
|
|
382
|
+
});
|
|
383
|
+
case "pack":
|
|
384
|
+
return Object.freeze({ op: "pack", over: asVarTerm(`find ${name} (pack)`, over) });
|
|
385
|
+
default:
|
|
386
|
+
throw errors.new(`find ${name}: unknown aggregate ${entry.agg}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Classifies one find entry into its named answer column (the KEY names the
|
|
391
|
+
* column, `count` included). The `slot`/`closed` slices are resolved LATER,
|
|
392
|
+
* at rule completion, where boundness and the mint slots are in hand.
|
|
393
|
+
*/
|
|
394
|
+
function findColumnOf(name, entry) {
|
|
395
|
+
if (isTerm(entry)) {
|
|
396
|
+
if (entry[term] === "var") {
|
|
397
|
+
return Object.freeze({
|
|
398
|
+
name,
|
|
399
|
+
entry: Object.freeze({ kind: "var", over: entry }),
|
|
400
|
+
closed: undefined,
|
|
401
|
+
slot: undefined
|
|
402
|
+
});
|
|
408
403
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
404
|
+
if (entry[term] === "duration") {
|
|
405
|
+
return Object.freeze({
|
|
406
|
+
name,
|
|
407
|
+
entry: Object.freeze({ kind: "measure", over: entry.over }),
|
|
408
|
+
closed: undefined,
|
|
409
|
+
slot: undefined
|
|
410
|
+
});
|
|
414
411
|
}
|
|
415
|
-
|
|
416
|
-
|
|
412
|
+
throw errors.new(`find ${name}: a ${entry[term]} is not projectable — find takes variables, r.duration(v), or aggregates`);
|
|
413
|
+
}
|
|
414
|
+
if (isAggregateEntry(entry)) {
|
|
415
|
+
return Object.freeze({
|
|
416
|
+
name,
|
|
417
|
+
entry: Object.freeze({ kind: "aggregate", agg: aggDataOf(name, entry) }),
|
|
418
|
+
closed: undefined,
|
|
419
|
+
slot: undefined
|
|
420
|
+
});
|
|
417
421
|
}
|
|
422
|
+
throw errors.new(`find ${name}: not a find entry — find takes variables, r.duration(v), or aggregates`);
|
|
418
423
|
}
|
|
419
424
|
/**
|
|
420
425
|
* The orderable ban's pointed refusal (`docs/architecture/10-data-model.md`
|
|
421
|
-
* § orderability): a closed reference is equality-and-membership only
|
|
422
|
-
* its declaration-id order is an encoding accident, so every
|
|
423
|
-
* order-comparison and fold position refuses it. The construction-time
|
|
424
|
-
* twin of the type tier's `OrderVarOk` exclusion, so the wall holds for
|
|
425
|
-
* untyped callers too (the engine cannot backstop this one: the wire IR
|
|
426
|
-
* carries plain u64s, no rosters).
|
|
426
|
+
* § orderability): a closed reference is equality-and-membership only.
|
|
427
427
|
*/
|
|
428
428
|
function closedOrderError(context, position, vocabulary) {
|
|
429
429
|
return errors.new(`${context}: ${position} is a ${vocabulary} reference — declaration order is an accident, not semantics: vocabularies do not order (docs/architecture/10-data-model.md; equality, membership, and counting remain)`);
|
|
430
430
|
}
|
|
431
|
-
/** The comparison ops under the orderable ban (order roster + point membership
|
|
431
|
+
/** The comparison ops under the orderable ban (order roster + point membership). */
|
|
432
432
|
function isOrderOp(op) {
|
|
433
433
|
return op === "lt" || op === "le" || op === "gt" || op === "ge" || op === "pointIn";
|
|
434
434
|
}
|
|
435
|
-
/** Requires a
|
|
436
|
-
function assertBound(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
throw errors.new(`${context}: the variable ${name} is not bound by a relation atom of the rule`);
|
|
435
|
+
/** Requires a variable to be bound by a relation atom of the rule (the boundness wall — invisible to the type tier). */
|
|
436
|
+
function assertBound(where, bound, ref) {
|
|
437
|
+
if (!bound.has(ref)) {
|
|
438
|
+
throw errors.new(`${where}: the variable ${ref.label} is not bound by a relation atom of the rule`);
|
|
440
439
|
}
|
|
441
|
-
return slot;
|
|
442
440
|
}
|
|
443
|
-
/** Requires a
|
|
444
|
-
function
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
441
|
+
/** Requires a variable to be interval-typed (the measure's and pack's domain), off its own descriptor. */
|
|
442
|
+
function assertInterval(where, ref) {
|
|
443
|
+
if (ref.field.kind !== "interval") {
|
|
444
|
+
throw errors.new(`${where}: ${ref.label} is not interval-typed — the measure is defined over interval-typed variables only`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/** Requires a variable's own field to be non-closed (the orderable ban's runtime twin). */
|
|
448
|
+
function assertNotClosed(where, position, ref) {
|
|
449
|
+
const roster = rosterOf(ref.field);
|
|
450
|
+
if (roster !== undefined) {
|
|
451
|
+
throw closedOrderError(where, `${position} ${ref.label}`, roster.name);
|
|
448
452
|
}
|
|
449
453
|
}
|
|
450
454
|
/**
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
* match-reuse join must be (the construction-time twin of the type tier's
|
|
455
|
-
* `EqOk` → `JoinOk`; bare pairs only with bare). The engine cannot backstop
|
|
456
|
-
* this one — the query IR carries no domains — so the wall lives here for
|
|
457
|
-
* untyped callers too.
|
|
455
|
+
* The classed mint slot one answer column's VALUES flow from: a projected
|
|
456
|
+
* variable's mint slot, or an Arg-carried payload's. Counts, folds, `pack`
|
|
457
|
+
* and the measure derive numbers/intervals, so they resolve no slot.
|
|
458
458
|
*/
|
|
459
|
-
function
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const slot = assertBound(context, varFields, side.name);
|
|
464
|
-
const roster = rosterOf(slot.field);
|
|
465
|
-
if (isOrderOp(cond.op) && roster !== undefined) {
|
|
466
|
-
throw closedOrderError(context, `the ${cond.op} side ${side.name}`, roster.name);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
if (side.kind === "measure") {
|
|
470
|
-
assertIntervalBound(context, varFields, side.name);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
|
|
474
|
-
const lhs = assertBound(context, varFields, cond.lhs.name);
|
|
475
|
-
const rhs = assertBound(context, varFields, cond.rhs.name);
|
|
476
|
-
if (!fieldJoins(lhs, rhs)) {
|
|
477
|
-
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)`);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
return;
|
|
459
|
+
function findColumnSlotOf(context, column) {
|
|
460
|
+
const entry = column.entry;
|
|
461
|
+
if (entry.kind === "var") {
|
|
462
|
+
return mintSlotOf(context, entry.over);
|
|
481
463
|
}
|
|
482
|
-
|
|
483
|
-
|
|
464
|
+
if (entry.kind === "aggregate" && entry.agg.op === "arg") {
|
|
465
|
+
return mintSlotOf(context, entry.agg.over);
|
|
484
466
|
}
|
|
467
|
+
return undefined;
|
|
485
468
|
}
|
|
486
|
-
/** Validates one
|
|
487
|
-
function validateColumn(context,
|
|
469
|
+
/** Validates one find column's variable references (boundness + the orderable/interval walls, off the var's own field). */
|
|
470
|
+
function validateColumn(context, bound, column) {
|
|
471
|
+
const where = `${contextLabel(context)} find ${column.name}`;
|
|
488
472
|
const entry = column.entry;
|
|
489
473
|
if (entry.kind === "var") {
|
|
490
|
-
assertBound(
|
|
474
|
+
assertBound(where, bound, entry.over);
|
|
491
475
|
return;
|
|
492
476
|
}
|
|
493
477
|
if (entry.kind === "measure") {
|
|
494
|
-
|
|
478
|
+
assertBound(where, bound, entry.over);
|
|
479
|
+
assertInterval(where, entry.over);
|
|
495
480
|
return;
|
|
496
481
|
}
|
|
497
482
|
const agg = entry.agg;
|
|
@@ -499,128 +484,143 @@ function validateColumn(context, varFields, column) {
|
|
|
499
484
|
case "count":
|
|
500
485
|
return;
|
|
501
486
|
case "countDistinct":
|
|
502
|
-
assertBound(
|
|
487
|
+
assertBound(where, bound, agg.over);
|
|
503
488
|
return;
|
|
504
489
|
case "fold": {
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if (roster !== undefined) {
|
|
509
|
-
throw closedOrderError(`${context} select ${column.name}`, `the ${agg.fold} input ${agg.over}`, roster.name);
|
|
510
|
-
}
|
|
490
|
+
if ("duration" in agg.over) {
|
|
491
|
+
assertBound(where, bound, agg.over.duration);
|
|
492
|
+
assertInterval(where, agg.over.duration);
|
|
511
493
|
return;
|
|
512
494
|
}
|
|
513
|
-
|
|
495
|
+
assertBound(where, bound, agg.over);
|
|
496
|
+
assertNotClosed(where, `the ${agg.fold} input`, agg.over);
|
|
514
497
|
return;
|
|
515
498
|
}
|
|
516
499
|
case "arg": {
|
|
517
|
-
assertBound(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (keyRoster !== undefined) {
|
|
521
|
-
throw closedOrderError(`${context} select ${column.name}`, `the ${agg.direction} key ${agg.key}`, keyRoster.name);
|
|
522
|
-
}
|
|
500
|
+
assertBound(where, bound, agg.over);
|
|
501
|
+
assertBound(where, bound, agg.key);
|
|
502
|
+
assertNotClosed(where, `the ${agg.direction} key`, agg.key);
|
|
523
503
|
return;
|
|
524
504
|
}
|
|
525
505
|
case "pack":
|
|
526
|
-
|
|
506
|
+
assertBound(where, bound, agg.over);
|
|
507
|
+
assertInterval(where, agg.over);
|
|
527
508
|
return;
|
|
528
509
|
}
|
|
529
510
|
}
|
|
530
511
|
/**
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
* machinery reads), and `decodeAnswers` lifts the column's row ids back to
|
|
535
|
-
* handle NAMES through it — the runtime twin of the row type's `Infer`
|
|
536
|
-
* claim. Every other entry decodes bare: counts are counts, the measure
|
|
537
|
-
* and `pack` are never closed, and a closed FOLD is banned outright
|
|
538
|
-
* ({@link closedOrderError}) before this resolution runs.
|
|
512
|
+
* Validates one condition's variable references against the rule's bound
|
|
513
|
+
* set — and, for `eq`/`ne` over two variables, holds the class wall through
|
|
514
|
+
* the mint slots (the unification IS a join; bare pairs only with bare).
|
|
539
515
|
*/
|
|
540
|
-
function
|
|
541
|
-
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
516
|
+
function validateCond(context, bound, cond) {
|
|
517
|
+
const label = contextLabel(context);
|
|
518
|
+
if (cond.kind === "cmp") {
|
|
519
|
+
for (const side of [cond.lhs, cond.rhs]) {
|
|
520
|
+
if (side.kind === "var") {
|
|
521
|
+
assertBound(label, bound, side.ref);
|
|
522
|
+
const roster = rosterOf(side.ref.field);
|
|
523
|
+
if (isOrderOp(cond.op) && roster !== undefined) {
|
|
524
|
+
throw closedOrderError(label, `the ${cond.op} side ${side.ref.label}`, roster.name);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (side.kind === "measure") {
|
|
528
|
+
assertBound(label, bound, side.ref);
|
|
529
|
+
assertInterval(label, side.ref);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
|
|
533
|
+
assertBound(label, bound, cond.lhs.ref);
|
|
534
|
+
assertBound(label, bound, cond.rhs.ref);
|
|
535
|
+
const lhs = mintSlotOf(context, cond.lhs.ref);
|
|
536
|
+
const rhs = mintSlotOf(context, cond.rhs.ref);
|
|
537
|
+
if (!fieldJoins(lhs, rhs)) {
|
|
538
|
+
throw errors.new(`${label}: ${cond.op}(${cond.lhs.ref.label}, ${cond.rhs.ref.label}) unifies domain-unequal fields — ${cond.lhs.ref.label} bound at ${renderFieldKind(lhs)}, ${cond.rhs.ref.label} at ${renderFieldKind(rhs)} (a var joins only class-equal slots; bare pairs only with bare)`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return;
|
|
550
542
|
}
|
|
551
|
-
|
|
552
|
-
|
|
543
|
+
for (const child of cond.children) {
|
|
544
|
+
validateCond(context, bound, child);
|
|
553
545
|
}
|
|
554
|
-
return rosterOf(varFields[over]?.field);
|
|
555
546
|
}
|
|
556
547
|
/**
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
548
|
+
* Validates one `idb` item: every head column of the rec is bound exactly
|
|
549
|
+
* once (a missing or extra key is a pointed error), every bound variable is
|
|
550
|
+
* positively bound by a relation atom of the rule, and each variable joins
|
|
551
|
+
* its head column's classed slot. When the rec's own rule 0 is in flight
|
|
552
|
+
* (`rec.rules[0]` absent), the completing rule's OWN find columns ARE the
|
|
553
|
+
* head.
|
|
562
554
|
*/
|
|
563
|
-
function
|
|
564
|
-
|
|
565
|
-
|
|
555
|
+
function validateIdb(context, bound, item, columns) {
|
|
556
|
+
const label = contextLabel(context);
|
|
557
|
+
const head = item.rec.rules[0];
|
|
558
|
+
const headColumns = head !== undefined ? head.finds : columns;
|
|
559
|
+
const headNames = headColumns.map(function nameOf(column) {
|
|
560
|
+
return column.name;
|
|
561
|
+
});
|
|
562
|
+
const keys = item.bindings.map(function keyOf(binding) {
|
|
563
|
+
return binding.key;
|
|
564
|
+
});
|
|
565
|
+
for (const key of keys) {
|
|
566
|
+
if (!headNames.includes(key)) {
|
|
567
|
+
throw errors.new(`${label}: idb ${item.rec.name} binds ${key}, not a head column of ${item.rec.name} (head columns: ${headNames.join(", ")})`);
|
|
568
|
+
}
|
|
566
569
|
}
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (seen.has(column.name)) {
|
|
571
|
-
throw errors.new(`${context}: select names the answer column ${column.name} twice`);
|
|
570
|
+
for (const name of headNames) {
|
|
571
|
+
if (!keys.includes(name)) {
|
|
572
|
+
throw errors.new(`${label}: idb ${item.rec.name} omits the head column ${name} — an idb join binds every head column of ${item.rec.name}`);
|
|
572
573
|
}
|
|
573
|
-
seen.add(column.name);
|
|
574
|
-
validateColumn(context, state.varFields, column);
|
|
575
574
|
}
|
|
575
|
+
for (const binding of item.bindings) {
|
|
576
|
+
if (!bound.has(binding.ref)) {
|
|
577
|
+
throw errors.new(`${label}: idb ${item.rec.name} names the variable ${binding.ref.label}, 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`);
|
|
578
|
+
}
|
|
579
|
+
const headColumn = headColumns.find(function byName(column) {
|
|
580
|
+
return column.name === binding.key;
|
|
581
|
+
});
|
|
582
|
+
if (headColumn === undefined || headColumn.slot === undefined) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const mint = mintSlotOf(context, binding.ref);
|
|
586
|
+
if (!fieldJoins(headColumn.slot, mint)) {
|
|
587
|
+
throw errors.new(`${label}: idb ${item.rec.name} joins the variable ${binding.ref.label} (${renderFieldKind(mint)}) at head column ${binding.key} (${renderFieldKind(headColumn.slot)}) — a var joins only class-equal slots; bare pairs only with bare`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Completes one rule: enriches the find columns (declaration-order-safe
|
|
593
|
+
* keys, boundness validated, each column's classed slot and closed slice
|
|
594
|
+
* resolved), then walks the body walls — negated-atom boundness safety, idb
|
|
595
|
+
* head pairing, and condition validation.
|
|
596
|
+
*/
|
|
597
|
+
function completeRule(context, state, rawColumns) {
|
|
598
|
+
const label = contextLabel(context);
|
|
599
|
+
if (rawColumns.length === 0) {
|
|
600
|
+
throw errors.new(`${label}: a find needs at least one entry`);
|
|
601
|
+
}
|
|
602
|
+
const columns = rawColumns.map(function enrichColumn(column) {
|
|
603
|
+
assertDeclarationOrderKey(`${label} find column`, column.name);
|
|
604
|
+
validateColumn(context, state.bound, column);
|
|
605
|
+
const slot = findColumnSlotOf(context, column);
|
|
606
|
+
return Object.freeze({ name: column.name, entry: column.entry, slot, closed: rosterOf(slot?.field) });
|
|
607
|
+
});
|
|
576
608
|
for (const item of state.items) {
|
|
577
609
|
if (item.kind === "negated") {
|
|
578
610
|
for (const binding of item.atom.bindings) {
|
|
579
|
-
if (binding.term.kind === "var") {
|
|
580
|
-
|
|
581
|
-
if (bound === undefined) {
|
|
582
|
-
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)`);
|
|
583
|
-
}
|
|
584
|
-
const negatedSlot = { field: binding.data, class: binding.class };
|
|
585
|
-
if (!fieldJoins(bound, negatedSlot)) {
|
|
586
|
-
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`);
|
|
587
|
-
}
|
|
611
|
+
if (binding.term.kind === "var" && !state.bound.has(binding.term.ref)) {
|
|
612
|
+
throw errors.new(`${label}: negated ${item.atom.relation.name} atom binds the variable ${binding.term.ref.label} at position ${binding.field}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`);
|
|
588
613
|
}
|
|
589
614
|
}
|
|
590
615
|
}
|
|
591
616
|
if (item.kind === "idb") {
|
|
592
|
-
|
|
593
|
-
item.vars.forEach(function checkIdbVar(name, position) {
|
|
594
|
-
const bound = state.varFields[name];
|
|
595
|
-
if (bound === undefined) {
|
|
596
|
-
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`);
|
|
597
|
-
}
|
|
598
|
-
const column = head?.select[position];
|
|
599
|
-
if (column === undefined || column.entry.kind !== "var") {
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
const headSlot = head?.varFields[column.entry.over];
|
|
603
|
-
if (headSlot !== undefined && !fieldJoins(headSlot, bound)) {
|
|
604
|
-
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`);
|
|
605
|
-
}
|
|
606
|
-
});
|
|
617
|
+
validateIdb(context, state.bound, item, columns);
|
|
607
618
|
}
|
|
608
619
|
if (item.kind === "cond") {
|
|
609
|
-
validateCond(context, state.
|
|
620
|
+
validateCond(context, state.bound, item.cond);
|
|
610
621
|
}
|
|
611
622
|
}
|
|
612
|
-
return Object.freeze({
|
|
613
|
-
items: state.items,
|
|
614
|
-
select: Object.freeze(columns.map(function enrichColumn(column) {
|
|
615
|
-
return Object.freeze({
|
|
616
|
-
name: column.name,
|
|
617
|
-
entry: column.entry,
|
|
618
|
-
closed: selectClosedOf(state.varFields, column.entry)
|
|
619
|
-
});
|
|
620
|
-
})),
|
|
621
|
-
varFields: state.varFields,
|
|
622
|
-
paramUses: state.paramUses
|
|
623
|
-
});
|
|
623
|
+
return Object.freeze({ items: state.items, finds: Object.freeze(columns), paramUses: state.paramUses });
|
|
624
624
|
}
|
|
625
625
|
/** Builds one typed rule value over completed rule data. */
|
|
626
626
|
function makeRuleValue(rule) {
|
|
@@ -638,7 +638,7 @@ function contextLabel(context) {
|
|
|
638
638
|
}
|
|
639
639
|
}
|
|
640
640
|
/** Validates and records one `idb` atom per the context's cut. */
|
|
641
|
-
function idbAdvance(context, state, target,
|
|
641
|
+
function idbAdvance(context, state, target, bindings) {
|
|
642
642
|
if (context.kind === "query") {
|
|
643
643
|
throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()");
|
|
644
644
|
}
|
|
@@ -646,36 +646,41 @@ function idbAdvance(context, state, target, vars) {
|
|
|
646
646
|
if (target.data !== context.self) {
|
|
647
647
|
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)`);
|
|
648
648
|
}
|
|
649
|
-
return advanceIdb(state, context.self,
|
|
649
|
+
return advanceIdb(state, context.self, bindings);
|
|
650
650
|
}
|
|
651
651
|
if (!context.program.recs.includes(target.data)) {
|
|
652
652
|
throw errors.new(`idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`);
|
|
653
653
|
}
|
|
654
|
-
return advanceIdb(state, target.data,
|
|
654
|
+
return advanceIdb(state, target.data, bindings);
|
|
655
655
|
}
|
|
656
|
-
/** Classifies one
|
|
657
|
-
function
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
656
|
+
/** Classifies one find record per the context (a recursive head projects bound variables only). */
|
|
657
|
+
function findColumns(context, entries) {
|
|
658
|
+
const columns = [];
|
|
659
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
660
|
+
if (entry === undefined) {
|
|
661
|
+
continue;
|
|
661
662
|
}
|
|
662
|
-
|
|
663
|
-
|
|
663
|
+
if (context.kind === "rec" && !(isTerm(entry) && entry[term] === "var")) {
|
|
664
|
+
throw errors.new(`rec ${context.self.name}: a recursive head projects bound variables only — aggregates and the measure read finished sets (the strata judge's quarantine, unwritable here)`);
|
|
665
|
+
}
|
|
666
|
+
columns.push(findColumnOf(name, entry));
|
|
667
|
+
}
|
|
668
|
+
return columns;
|
|
664
669
|
}
|
|
665
670
|
/** Builds one runtime chain (immutably — every step is a fresh chain over fresh state). */
|
|
666
671
|
function makeRawChain(context, state) {
|
|
667
672
|
const chain = {
|
|
668
673
|
match(relation, bindings) {
|
|
669
|
-
return makeRawChain(context, advanceMatch(state, relation, bindings
|
|
674
|
+
return makeRawChain(context, advanceMatch(context, state, relation, bindings));
|
|
670
675
|
},
|
|
671
676
|
where(cond) {
|
|
672
|
-
return makeRawChain(context, advanceWhere(state, cond
|
|
677
|
+
return makeRawChain(context, advanceWhere(context, state, cond));
|
|
673
678
|
},
|
|
674
|
-
idb(target,
|
|
675
|
-
return makeRawChain(context, idbAdvance(context, state, target,
|
|
679
|
+
idb(target, bindings) {
|
|
680
|
+
return makeRawChain(context, idbAdvance(context, state, target, bindings));
|
|
676
681
|
},
|
|
677
|
-
|
|
678
|
-
return makeRuleValue(completeRule(
|
|
682
|
+
find(entries) {
|
|
683
|
+
return makeRuleValue(completeRule(context, state, findColumns(context, entries)));
|
|
679
684
|
}
|
|
680
685
|
};
|
|
681
686
|
Object.freeze(chain);
|
|
@@ -686,7 +691,7 @@ function makeRawScope(context) {
|
|
|
686
691
|
const scope = {
|
|
687
692
|
...termOps,
|
|
688
693
|
match(relation, bindings) {
|
|
689
|
-
return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings
|
|
694
|
+
return makeRawChain(context, advanceMatch(context, EMPTY_RULE, relation, bindings));
|
|
690
695
|
}
|
|
691
696
|
};
|
|
692
697
|
Object.freeze(scope);
|
|
@@ -694,21 +699,19 @@ function makeRawScope(context) {
|
|
|
694
699
|
}
|
|
695
700
|
/**
|
|
696
701
|
* The rule builders' trusted admission seam — THE home of the
|
|
697
|
-
* trusted-admission-seam pattern the other mint guards cite
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
* interfaces themselves; the runtime twin of every one of them is a
|
|
704
|
-
* construction-time validation in this module.
|
|
702
|
+
* trusted-admission-seam pattern the other mint guards cite: the raw builder
|
|
703
|
+
* is one runtime shape for every context, and this guard verifies the
|
|
704
|
+
* checkable fact — the builder verbs exist — before the value is admitted at
|
|
705
|
+
* its TYPED face. The type-level judgments (class-equal joins, the recursion
|
|
706
|
+
* cut) live in the interfaces themselves; boundness is a construction-time
|
|
707
|
+
* validation in this module (object identity is invisible to the type tier).
|
|
705
708
|
*/
|
|
706
709
|
function isTypedScope(scope) {
|
|
707
710
|
return typeof scope.match === "function";
|
|
708
711
|
}
|
|
709
712
|
/** Builds one query-rule builder (the typed face of the raw builder). */
|
|
710
|
-
function makeQueryRuleScope(
|
|
711
|
-
const raw = makeRawScope({ kind: "query", classes });
|
|
713
|
+
function makeQueryRuleScope(theory) {
|
|
714
|
+
const raw = makeRawScope({ kind: "query", classes: theory.classes, theory });
|
|
712
715
|
if (!isTypedScope(raw)) {
|
|
713
716
|
throw errors.new("query rule builder construction incomplete");
|
|
714
717
|
}
|
|
@@ -716,7 +719,7 @@ function makeQueryRuleScope(classes) {
|
|
|
716
719
|
}
|
|
717
720
|
/** Builds one output-rule builder over a program's recs. */
|
|
718
721
|
function makeOutputRuleScope(program) {
|
|
719
|
-
const raw = makeRawScope({ kind: "output", program, classes: program.classes });
|
|
722
|
+
const raw = makeRawScope({ kind: "output", program, classes: program.classes, theory: program.theory });
|
|
720
723
|
if (!isTypedScope(raw)) {
|
|
721
724
|
throw errors.new("program output rule builder construction incomplete");
|
|
722
725
|
}
|
|
@@ -741,25 +744,7 @@ function headSignature(column) {
|
|
|
741
744
|
}
|
|
742
745
|
return `${column.name}:${agg.op}`;
|
|
743
746
|
}
|
|
744
|
-
/**
|
|
745
|
-
* The classed slot one answer column's VALUES flow from, resolved through
|
|
746
|
-
* the rule's own binding environment: a projected var's first-binding slot,
|
|
747
|
-
* or an Arg-carried payload's (`argMax`/`argMin` carry `over` verbatim —
|
|
748
|
-
* the same two shapes the closed slice lifts). Counts, folds, `pack` and
|
|
749
|
-
* the measure derive numbers/intervals rather than carrying a slot's ids,
|
|
750
|
-
* so they resolve no slot (`undefined`).
|
|
751
|
-
*/
|
|
752
|
-
function headSlotOf(rule, column) {
|
|
753
|
-
const entry = column.entry;
|
|
754
|
-
if (entry.kind === "var") {
|
|
755
|
-
return rule.varFields[entry.over];
|
|
756
|
-
}
|
|
757
|
-
if (entry.kind === "aggregate" && entry.agg.op === "arg") {
|
|
758
|
-
return rule.varFields[entry.agg.over];
|
|
759
|
-
}
|
|
760
|
-
return undefined;
|
|
761
|
-
}
|
|
762
|
-
/** The roster a param anchor carries: present exactly on a closed-reference field anchor (rides THE one `rosterOf` reader). */
|
|
747
|
+
/** The roster a param anchor carries: present exactly on a closed-reference field anchor. */
|
|
763
748
|
function anchorRosterOf(anchor) {
|
|
764
749
|
return anchor === "measure" ? undefined : rosterOf(anchor);
|
|
765
750
|
}
|
|
@@ -769,20 +754,9 @@ function renderParamAnchor(roster) {
|
|
|
769
754
|
}
|
|
770
755
|
/**
|
|
771
756
|
* Folds every rule's param uses (recs in declaration order first, output
|
|
772
|
-
* rules last — exactly the lowering walk) into the query's registry:
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
* anchored use of one name must agree on the roster (value identity), so a
|
|
776
|
-
* param anchored at a closed reference is GUARANTEED to ride the one
|
|
777
|
-
* roster-verification point (`taggedHandleId`) at execute; a name anchored
|
|
778
|
-
* both at a closed reference and at a non-closed position (or at two
|
|
779
|
-
* vocabularies) is refused here, because the wire would translate only the
|
|
780
|
-
* first anchor's reading (the type tier intersects the uses to `never`;
|
|
781
|
-
* this is its runtime twin for untyped callers). A param whose anchor is a
|
|
782
|
-
* CLOSED reference must never sit in an order-comparison position — the
|
|
783
|
-
* anchor types its value a handle name and the engine would order the
|
|
784
|
-
* translated row ids, so the pairing is refused here too (the registry is
|
|
785
|
-
* the one place a name's every use and its anchoring field meet).
|
|
757
|
+
* rules last — exactly the lowering walk) into the query's registry: first
|
|
758
|
+
* use mints the dense `ParamId`, the first FIELD-ANCHORED use types the
|
|
759
|
+
* wire, and one name keeps one shape AND one closedness.
|
|
786
760
|
*/
|
|
787
761
|
function paramRegistryOf(recs, rules) {
|
|
788
762
|
const order = [];
|
|
@@ -846,58 +820,52 @@ function paramRegistryOf(recs, rules) {
|
|
|
846
820
|
/**
|
|
847
821
|
* Assembles the runtime query value over completed rules: every rule must
|
|
848
822
|
* derive the SAME head (name and aggregate shape, position for position —
|
|
849
|
-
* the decode labels and the engine's alignment rule agree
|
|
850
|
-
*
|
|
823
|
+
* the decode labels and the engine's alignment rule agree), and the param
|
|
824
|
+
* registry folds in program-walk order.
|
|
851
825
|
*/
|
|
852
826
|
function makeRawQuery(theory, recs, rules) {
|
|
853
827
|
const first = rules[0];
|
|
854
828
|
if (first === undefined) {
|
|
855
829
|
throw errors.new("a query needs at least one rule");
|
|
856
830
|
}
|
|
857
|
-
const signature = first.
|
|
831
|
+
const signature = first.finds.map(headSignature).join(", ");
|
|
858
832
|
rules.forEach(function verifyHead(rule, index) {
|
|
859
|
-
const candidate = rule.
|
|
833
|
+
const candidate = rule.finds.map(headSignature).join(", ");
|
|
860
834
|
if (candidate !== signature) {
|
|
861
|
-
throw errors.new(`every rule of a query derives the same head — rule 0
|
|
835
|
+
throw errors.new(`every rule of a query derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`);
|
|
862
836
|
}
|
|
863
837
|
// The closed slice is part of the head too: one answer column decodes
|
|
864
|
-
// through one roster, so a union whose rules bind a column at
|
|
865
|
-
//
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
rule.select.forEach(function verifyClosedSlice(column, position) {
|
|
869
|
-
const lead = first.select[position];
|
|
838
|
+
// through one roster, so a union whose rules bind a column at different
|
|
839
|
+
// vocabularies (or one closed, one bare) is refused pointed.
|
|
840
|
+
rule.finds.forEach(function verifyClosedSlice(column, position) {
|
|
841
|
+
const lead = first.finds[position];
|
|
870
842
|
if (lead !== undefined && column.closed !== lead.closed) {
|
|
871
843
|
throw errors.new(`every rule of a query derives the same head — the answer column ${lead.name} is ${renderClosedSlice(lead.closed)} in rule 0 but ${renderClosedSlice(column.closed)} in rule ${index} (one column decodes through one roster)`);
|
|
872
844
|
}
|
|
873
845
|
// The law-class wall on the union head: one answer column is one
|
|
874
|
-
// value space, so the classed slot each rule binds the column
|
|
875
|
-
// must join across rules — the SAME fieldJoins judgment every
|
|
876
|
-
// join/eq/negated-atom position enforces
|
|
877
|
-
//
|
|
878
|
-
// backstop it, and without it a union mixes (say) Holder ids and
|
|
879
|
-
// Account ids in one column the consumer reads as one id space.
|
|
846
|
+
// value space, so the classed mint slot each rule binds the column
|
|
847
|
+
// at must join across rules — the SAME fieldJoins judgment every
|
|
848
|
+
// join/eq/negated-atom position enforces (the SDK holds it because
|
|
849
|
+
// the wire IR carries no domains).
|
|
880
850
|
if (lead === undefined) {
|
|
881
851
|
return;
|
|
882
852
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (leadSlot !== undefined && slot !== undefined && !fieldJoins(leadSlot, slot)) {
|
|
886
|
-
throw errors.new(`every rule of a query derives the same head — the answer column ${lead.name} unions domain-unequal fields: bound at ${renderFieldKind(leadSlot)} in rule 0 but at ${renderFieldKind(slot)} in rule ${index} (a union column joins only class-equal slots; bare pairs only with bare)`);
|
|
853
|
+
if (lead.slot !== undefined && column.slot !== undefined && !fieldJoins(lead.slot, column.slot)) {
|
|
854
|
+
throw errors.new(`every rule of a query derives the same head — the answer column ${lead.name} unions domain-unequal fields: bound at ${renderFieldKind(lead.slot)} in rule 0 but at ${renderFieldKind(column.slot)} in rule ${index} (a union column joins only class-equal slots; bare pairs only with bare)`);
|
|
887
855
|
}
|
|
888
856
|
});
|
|
889
857
|
});
|
|
890
858
|
const data = Object.freeze({
|
|
891
859
|
recs: Object.freeze([...recs]),
|
|
892
860
|
rules: Object.freeze([...rules]),
|
|
893
|
-
|
|
861
|
+
finds: first.finds,
|
|
894
862
|
params: paramRegistryOf(recs, rules)
|
|
895
863
|
});
|
|
896
864
|
const value = {
|
|
897
865
|
schema: theory,
|
|
898
866
|
data,
|
|
899
867
|
rule(build) {
|
|
900
|
-
const built = build(makeRawScope({ kind: "query", classes: theory.classes }));
|
|
868
|
+
const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory }));
|
|
901
869
|
return makeRawQuery(theory, recs, [...rules, built.rule]);
|
|
902
870
|
}
|
|
903
871
|
};
|
|
@@ -921,16 +889,15 @@ function makeQuery(theory, recs, rules) {
|
|
|
921
889
|
return raw;
|
|
922
890
|
}
|
|
923
891
|
/**
|
|
924
|
-
* Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule`
|
|
925
|
-
*
|
|
926
|
-
*
|
|
927
|
-
*
|
|
928
|
-
* compare class names off it, at the type level and at construction alike.
|
|
892
|
+
* Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule` adds
|
|
893
|
+
* one conjunctive rule; multiple rules are the set union. The schema's
|
|
894
|
+
* law-computed class map and theory value ride into every rule builder — the
|
|
895
|
+
* join walls compare against the mint slots off it.
|
|
929
896
|
*/
|
|
930
897
|
function query(theory) {
|
|
931
898
|
const start = {
|
|
932
899
|
rule(build) {
|
|
933
|
-
const built = build(makeQueryRuleScope(theory
|
|
900
|
+
const built = build(makeQueryRuleScope(theory));
|
|
934
901
|
return makeQuery(theory, [], [built.rule]);
|
|
935
902
|
}
|
|
936
903
|
};
|
|
@@ -939,13 +906,8 @@ function query(theory) {
|
|
|
939
906
|
}
|
|
940
907
|
/**
|
|
941
908
|
* Tags one closed-reference literal: the handle NAME, verified against the
|
|
942
|
-
* roster
|
|
943
|
-
*
|
|
944
|
-
* declaration-order row id, tagged u64 — queries cross ids, never handle
|
|
945
|
-
* names; the wire is untouched. THE single roster-verification point of
|
|
946
|
-
* the query surface: atom-binding literals, comparison literals,
|
|
947
|
-
* execute-time params, and membership-array members all reach it (never
|
|
948
|
-
* duplicate the check per call site).
|
|
909
|
+
* roster and translated to its declaration-order row id, tagged u64. THE
|
|
910
|
+
* single roster-verification point of the query surface.
|
|
949
911
|
*/
|
|
950
912
|
function taggedHandleId(context, closed, value) {
|
|
951
913
|
if (typeof value !== "string") {
|
|
@@ -979,11 +941,7 @@ function taggedAtElementDomain(context, element, value) {
|
|
|
979
941
|
}
|
|
980
942
|
/**
|
|
981
943
|
* Tags one host literal at a FIELD position (atom bindings): the field's
|
|
982
|
-
* structural kind directs the tag, never a guess.
|
|
983
|
-
* bigint literal tags as the ELEMENT type — the IR's membership typing
|
|
984
|
-
* rule (point membership), an interval-shaped literal as the interval
|
|
985
|
-
* (value equality). A closed-reference literal is its bare handle id,
|
|
986
|
-
* tagged u64 after a roster verification.
|
|
944
|
+
* structural kind directs the tag, never a guess.
|
|
987
945
|
*/
|
|
988
946
|
function taggedLiteral(context, field, value) {
|
|
989
947
|
const roster = rosterOf(field);
|
|
@@ -1013,15 +971,6 @@ function taggedLiteral(context, field, value) {
|
|
|
1013
971
|
if (typeof value !== "string") {
|
|
1014
972
|
throw literalShapeError(context, "string", value);
|
|
1015
973
|
}
|
|
1016
|
-
/**
|
|
1017
|
-
* The marshal's bijection law at the query seam (`marshal.ts`
|
|
1018
|
-
* cellOf): a lone surrogate would be lossily replaced with
|
|
1019
|
-
* U+FFFD at the bridge's UTF-8 crossing and silently match a
|
|
1020
|
-
* fact the typed write surface can never store — distinct JS
|
|
1021
|
-
* strings collapsing to one wire query. This is the single
|
|
1022
|
-
* seam every query string literal, string param
|
|
1023
|
-
* (`taggedCmpLiteral`), and membership member lowers through.
|
|
1024
|
-
*/
|
|
1025
974
|
if (!value.isWellFormed()) {
|
|
1026
975
|
throw literalShapeError(context, "well-formed string", value);
|
|
1027
976
|
}
|
|
@@ -1038,17 +987,13 @@ function taggedLiteral(context, field, value) {
|
|
|
1038
987
|
}
|
|
1039
988
|
}
|
|
1040
989
|
/**
|
|
1041
|
-
* Tags one host literal at a COMPARISON or PARAM position, where the
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1044
|
-
* `pointIn`
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1048
|
-
* `pointIn(t, span(...))` and tags as the interval of the sibling's
|
|
1049
|
-
* element domain; under every other operator an interval shape against a
|
|
1050
|
-
* scalar sibling stays refused (the engine's IllegalComparison — the
|
|
1051
|
-
* bug-hunt fix, preserved op-aware).
|
|
990
|
+
* Tags one host literal at a COMPARISON or PARAM position, where the SIBLING
|
|
991
|
+
* anchors the type: a measure sibling is u64, an interval-field sibling
|
|
992
|
+
* contributes its element domain, a scalar sibling its own type. At
|
|
993
|
+
* `pointIn` the operand order is interval-left, point-right, so an
|
|
994
|
+
* interval-shaped literal beside a scalar element-typed sibling is the LEGAL
|
|
995
|
+
* interval operand of `pointIn(t, span(...))`; under every other operator an
|
|
996
|
+
* interval shape against a scalar sibling stays refused.
|
|
1052
997
|
*/
|
|
1053
998
|
function taggedCmpLiteral(context, sibling, value, op) {
|
|
1054
999
|
if (sibling === "measure") {
|
|
@@ -1069,16 +1014,16 @@ function taggedCmpLiteral(context, sibling, value, op) {
|
|
|
1069
1014
|
return taggedLiteral(context, sibling, value);
|
|
1070
1015
|
}
|
|
1071
1016
|
/** Creates one rule-scoped variable numberer. */
|
|
1072
|
-
function
|
|
1017
|
+
function freshVarIds() {
|
|
1073
1018
|
const assigned = new Map();
|
|
1074
1019
|
return {
|
|
1075
|
-
of(
|
|
1076
|
-
const existing = assigned.get(
|
|
1020
|
+
of(ref) {
|
|
1021
|
+
const existing = assigned.get(ref);
|
|
1077
1022
|
if (existing !== undefined) {
|
|
1078
1023
|
return existing;
|
|
1079
1024
|
}
|
|
1080
1025
|
const id = assigned.size;
|
|
1081
|
-
assigned.set(
|
|
1026
|
+
assigned.set(ref, id);
|
|
1082
1027
|
return id;
|
|
1083
1028
|
}
|
|
1084
1029
|
};
|
|
@@ -1093,10 +1038,7 @@ function paramIdOf(ctx, name) {
|
|
|
1093
1038
|
}
|
|
1094
1039
|
/**
|
|
1095
1040
|
* Lowers one EDB atom (either polarity). A CLOSED owner lowers through the
|
|
1096
|
-
* same edb source
|
|
1097
|
-
* an ordinary relation's — with field ordinals over the SEALED shape: `id`
|
|
1098
|
-
* at 0, each payload column at its declared index + 1 (`sealedFieldsOf`
|
|
1099
|
-
* carries the shift; the lowering golden pins it).
|
|
1041
|
+
* same edb source, with field ordinals over the SEALED shape.
|
|
1100
1042
|
*/
|
|
1101
1043
|
function lowerAtom(ctx, atom, ids) {
|
|
1102
1044
|
const member = ctx.theory.relations[atom.relation.name];
|
|
@@ -1119,17 +1061,12 @@ function lowerAtom(ctx, atom, ids) {
|
|
|
1119
1061
|
});
|
|
1120
1062
|
return { source: { kind: "edb", relation: relationId }, bindings };
|
|
1121
1063
|
}
|
|
1122
|
-
/**
|
|
1123
|
-
* Lowers one binding term. A membership ARRAY (`literalSet`) lowers to the
|
|
1124
|
-
* existing param-set term over its content-addressed registry entry — the
|
|
1125
|
-
* program IR is byte-identical to the same set spelled `r.inSet`; the SDK
|
|
1126
|
-
* supplies the translated member set itself at execute (`wireParams`).
|
|
1127
|
-
*/
|
|
1064
|
+
/** Lowers one binding term. A membership ARRAY lowers to the existing param-set term over its content-addressed entry. */
|
|
1128
1065
|
function lowerBindingTerm(ctx, context, binding, ids) {
|
|
1129
1066
|
const bound = binding.term;
|
|
1130
1067
|
switch (bound.kind) {
|
|
1131
1068
|
case "var":
|
|
1132
|
-
return { kind: "var", var: ids.of(bound.
|
|
1069
|
+
return { kind: "var", var: ids.of(bound.ref) };
|
|
1133
1070
|
case "param":
|
|
1134
1071
|
return { kind: "param", param: paramIdOf(ctx, bound.name) };
|
|
1135
1072
|
case "setParam":
|
|
@@ -1140,34 +1077,45 @@ function lowerBindingTerm(ctx, context, binding, ids) {
|
|
|
1140
1077
|
return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) };
|
|
1141
1078
|
}
|
|
1142
1079
|
}
|
|
1143
|
-
/**
|
|
1144
|
-
|
|
1080
|
+
/**
|
|
1081
|
+
* Lowers one idb atom: named bindings placed by HEAD order, `FieldId(i)` =
|
|
1082
|
+
* head position i. Every head column of the rec must be bound (a missing key
|
|
1083
|
+
* is refused pointed); the var-id assignment order is head order, so the
|
|
1084
|
+
* first-use numbering matches the name-keyed edition exactly.
|
|
1085
|
+
*/
|
|
1086
|
+
function lowerIdbAtom(ctx, rec, bindings, ids) {
|
|
1145
1087
|
const pred = ctx.recIds.get(rec);
|
|
1146
1088
|
if (pred === undefined) {
|
|
1147
1089
|
throw errors.new(`query lowering: rec ${rec.name} was declared by a different program`);
|
|
1148
1090
|
}
|
|
1149
|
-
const
|
|
1150
|
-
if (
|
|
1151
|
-
throw errors.new(`query lowering:
|
|
1091
|
+
const head = rec.rules[0];
|
|
1092
|
+
if (head === undefined) {
|
|
1093
|
+
throw errors.new(`query lowering: rec ${rec.name} has no rules`);
|
|
1152
1094
|
}
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1095
|
+
const irBindings = head.finds.map(function lowerPosition(column, position) {
|
|
1096
|
+
const binding = bindings.find(function byKey(candidate) {
|
|
1097
|
+
return candidate.key === column.name;
|
|
1098
|
+
});
|
|
1099
|
+
if (binding === undefined) {
|
|
1100
|
+
throw errors.new(`query lowering: idb ${rec.name} omits head column ${column.name}`);
|
|
1101
|
+
}
|
|
1102
|
+
return [position, { kind: "var", var: ids.of(binding.ref) }];
|
|
1155
1103
|
});
|
|
1156
|
-
return { source: { kind: "idb", pred }, bindings };
|
|
1104
|
+
return { source: { kind: "idb", pred }, bindings: irBindings };
|
|
1157
1105
|
}
|
|
1158
1106
|
/** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
|
|
1159
|
-
function lowerCmpTerm(ctx,
|
|
1107
|
+
function lowerCmpTerm(ctx, side, sibling, ids, op) {
|
|
1160
1108
|
switch (side.kind) {
|
|
1161
1109
|
case "var":
|
|
1162
|
-
return { kind: "var", var: ids.of(side.
|
|
1110
|
+
return { kind: "var", var: ids.of(side.ref) };
|
|
1163
1111
|
case "param":
|
|
1164
1112
|
return { kind: "param", param: paramIdOf(ctx, side.name) };
|
|
1165
1113
|
case "setParam":
|
|
1166
1114
|
return { kind: "paramSet", param: paramIdOf(ctx, side.name) };
|
|
1167
1115
|
case "measure":
|
|
1168
|
-
return { kind: "measure", var: ids.of(side.
|
|
1116
|
+
return { kind: "measure", var: ids.of(side.ref) };
|
|
1169
1117
|
case "literal": {
|
|
1170
|
-
const anchor = cmpAnchorOf(ctx,
|
|
1118
|
+
const anchor = cmpAnchorOf(ctx, sibling);
|
|
1171
1119
|
if (anchor === undefined) {
|
|
1172
1120
|
throw errors.new("query lowering: a comparison literal needs a bound-variable, measure, or anchored-param sibling to type it");
|
|
1173
1121
|
}
|
|
@@ -1175,10 +1123,10 @@ function lowerCmpTerm(ctx, rule, side, sibling, ids, op) {
|
|
|
1175
1123
|
}
|
|
1176
1124
|
}
|
|
1177
1125
|
}
|
|
1178
|
-
/** Resolves the anchor a comparison literal tags by: the sibling's field, the measure, or an anchored param. */
|
|
1179
|
-
function cmpAnchorOf(ctx,
|
|
1126
|
+
/** Resolves the anchor a comparison literal tags by: the sibling variable's field, the measure, or an anchored param. */
|
|
1127
|
+
function cmpAnchorOf(ctx, sibling) {
|
|
1180
1128
|
if (sibling.kind === "var") {
|
|
1181
|
-
return
|
|
1129
|
+
return sibling.ref.field;
|
|
1182
1130
|
}
|
|
1183
1131
|
if (sibling.kind === "measure") {
|
|
1184
1132
|
return "measure";
|
|
@@ -1189,7 +1137,7 @@ function cmpAnchorOf(ctx, rule, sibling) {
|
|
|
1189
1137
|
return undefined;
|
|
1190
1138
|
}
|
|
1191
1139
|
/** Lowers one comparison. */
|
|
1192
|
-
function lowerComparison(ctx,
|
|
1140
|
+
function lowerComparison(ctx, cmp, ids) {
|
|
1193
1141
|
if (cmp.op === "allen") {
|
|
1194
1142
|
const maskData = cmp.mask;
|
|
1195
1143
|
if (maskData === undefined) {
|
|
@@ -1200,29 +1148,29 @@ function lowerComparison(ctx, rule, cmp, ids) {
|
|
|
1200
1148
|
: { kind: "param", param: paramIdOf(ctx, maskData.name) };
|
|
1201
1149
|
return {
|
|
1202
1150
|
op: { kind: "allen", mask },
|
|
1203
|
-
lhs: lowerCmpTerm(ctx,
|
|
1204
|
-
rhs: lowerCmpTerm(ctx,
|
|
1151
|
+
lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
|
|
1152
|
+
rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
|
|
1205
1153
|
};
|
|
1206
1154
|
}
|
|
1207
1155
|
return {
|
|
1208
1156
|
op: { kind: cmp.op },
|
|
1209
|
-
lhs: lowerCmpTerm(ctx,
|
|
1210
|
-
rhs: lowerCmpTerm(ctx,
|
|
1157
|
+
lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, cmp.op),
|
|
1158
|
+
rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, cmp.op)
|
|
1211
1159
|
};
|
|
1212
1160
|
}
|
|
1213
1161
|
/** Lowers one condition node (comparison leaf or and/or tree). */
|
|
1214
|
-
function lowerCondition(ctx,
|
|
1162
|
+
function lowerCondition(ctx, cond, ids) {
|
|
1215
1163
|
if (cond.kind === "cmp") {
|
|
1216
|
-
return { kind: "leaf", cmp: lowerComparison(ctx,
|
|
1164
|
+
return { kind: "leaf", cmp: lowerComparison(ctx, cond, ids) };
|
|
1217
1165
|
}
|
|
1218
1166
|
return {
|
|
1219
1167
|
kind: cond.op,
|
|
1220
1168
|
children: cond.children.map(function lowerChild(child) {
|
|
1221
|
-
return lowerCondition(ctx,
|
|
1169
|
+
return lowerCondition(ctx, child, ids);
|
|
1222
1170
|
})
|
|
1223
1171
|
};
|
|
1224
1172
|
}
|
|
1225
|
-
/** Lowers one
|
|
1173
|
+
/** Lowers one find entry to its per-rule find term. */
|
|
1226
1174
|
function lowerFind(entry, ids) {
|
|
1227
1175
|
if (entry.kind === "var") {
|
|
1228
1176
|
return { kind: "var", var: ids.of(entry.over) };
|
|
@@ -1237,10 +1185,10 @@ function lowerFind(entry, ids) {
|
|
|
1237
1185
|
case "countDistinct":
|
|
1238
1186
|
return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) };
|
|
1239
1187
|
case "fold": {
|
|
1240
|
-
if (
|
|
1241
|
-
return { kind: "
|
|
1188
|
+
if ("duration" in agg.over) {
|
|
1189
|
+
return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) };
|
|
1242
1190
|
}
|
|
1243
|
-
return { kind: "
|
|
1191
|
+
return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) };
|
|
1244
1192
|
}
|
|
1245
1193
|
case "arg":
|
|
1246
1194
|
return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) };
|
|
@@ -1263,7 +1211,7 @@ function headOpOf(agg) {
|
|
|
1263
1211
|
return "pack";
|
|
1264
1212
|
}
|
|
1265
1213
|
}
|
|
1266
|
-
/** One
|
|
1214
|
+
/** One find entry's var-free head shape. */
|
|
1267
1215
|
function headTermOf(column) {
|
|
1268
1216
|
const entry = column.entry;
|
|
1269
1217
|
if (entry.kind === "var" || entry.kind === "measure") {
|
|
@@ -1273,7 +1221,7 @@ function headTermOf(column) {
|
|
|
1273
1221
|
}
|
|
1274
1222
|
/** Lowers one rule: body walked in written order (var ids by first occurrence), finds last. */
|
|
1275
1223
|
function lowerRule(ctx, rule) {
|
|
1276
|
-
const ids =
|
|
1224
|
+
const ids = freshVarIds();
|
|
1277
1225
|
const atoms = [];
|
|
1278
1226
|
const negated = [];
|
|
1279
1227
|
const conditions = [];
|
|
@@ -1288,17 +1236,17 @@ function lowerRule(ctx, rule) {
|
|
|
1288
1236
|
break;
|
|
1289
1237
|
}
|
|
1290
1238
|
case "idb": {
|
|
1291
|
-
atoms.push(lowerIdbAtom(ctx, item.rec, item.
|
|
1239
|
+
atoms.push(lowerIdbAtom(ctx, item.rec, item.bindings, ids));
|
|
1292
1240
|
break;
|
|
1293
1241
|
}
|
|
1294
1242
|
case "cond": {
|
|
1295
|
-
conditions.push(lowerCondition(ctx,
|
|
1243
|
+
conditions.push(lowerCondition(ctx, item.cond, ids));
|
|
1296
1244
|
break;
|
|
1297
1245
|
}
|
|
1298
1246
|
}
|
|
1299
1247
|
}
|
|
1300
1248
|
return {
|
|
1301
|
-
finds: rule.
|
|
1249
|
+
finds: rule.finds.map(function findOf(column) {
|
|
1302
1250
|
return lowerFind(column.entry, ids);
|
|
1303
1251
|
}),
|
|
1304
1252
|
atoms,
|
|
@@ -1309,11 +1257,7 @@ function lowerRule(ctx, rule) {
|
|
|
1309
1257
|
/**
|
|
1310
1258
|
* Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
|
|
1311
1259
|
* recs in declaration order (`PredId` = index), the output predicate
|
|
1312
|
-
*
|
|
1313
|
-
* the law the engine's own manifest pins; `db.prepare` re-verifies the
|
|
1314
|
-
* alignment against the live manifest before sending. Every registered
|
|
1315
|
-
* param must carry a field anchor by now — an unanchorable param (its
|
|
1316
|
-
* every use beside a literal) is refused here, naming it.
|
|
1260
|
+
* appended last. Every registered param must carry a field anchor by now.
|
|
1317
1261
|
*/
|
|
1318
1262
|
function lowerQuery(q) {
|
|
1319
1263
|
const theory = q.schema;
|
|
@@ -1341,14 +1285,14 @@ function lowerQuery(q) {
|
|
|
1341
1285
|
throw errors.new(`query lowering: rec ${rec.name} has no rules`);
|
|
1342
1286
|
}
|
|
1343
1287
|
return {
|
|
1344
|
-
head: head.
|
|
1288
|
+
head: head.finds.map(headTermOf),
|
|
1345
1289
|
rules: rec.rules.map(function lowerRecRule(rule) {
|
|
1346
1290
|
return lowerRule(ctx, rule);
|
|
1347
1291
|
})
|
|
1348
1292
|
};
|
|
1349
1293
|
});
|
|
1350
1294
|
predicates.push({
|
|
1351
|
-
head: q.data.
|
|
1295
|
+
head: q.data.finds.map(headTermOf),
|
|
1352
1296
|
rules: q.data.rules.map(function lowerOutputRule(rule) {
|
|
1353
1297
|
return lowerRule(ctx, rule);
|
|
1354
1298
|
})
|