@bjornpagen/bumbledb 0.5.0 → 0.7.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 +140 -136
- package/README.md +24 -11
- package/dist/closed.d.ts +7 -5
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +4 -1
- package/dist/closed.js.map +1 -1
- package/dist/count.js +1 -1
- package/dist/count.js.map +1 -1
- package/dist/db.d.ts +110 -73
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +172 -116
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts +22 -10
- package/dist/exhume.d.ts.map +1 -1
- package/dist/exhume.js +42 -9
- package/dist/exhume.js.map +1 -1
- package/dist/face.d.ts +10 -6
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +38 -16
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +33 -8
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/lower.d.ts +9 -9
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +17 -12
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +17 -7
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +32 -10
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +60 -9
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +199 -203
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +39 -48
- 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 +131 -158
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +505 -494
- 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 +8 -8
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +16 -21
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +139 -74
- 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/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +3 -1
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +12 -2
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +20 -10
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +22 -2
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +26 -5
- package/dist/statements.js.map +1 -1
- package/package.json +3 -3
- package/src/closed.ts +13 -7
- package/src/count.ts +1 -1
- package/src/db.ts +293 -177
- package/src/exhume.ts +48 -11
- package/src/face.ts +13 -6
- package/src/fields.ts +51 -15
- package/src/index.ts +12 -8
- package/src/lower.ts +17 -12
- package/src/marshal.ts +38 -13
- package/src/native.ts +59 -8
- package/src/query/atom.ts +297 -275
- package/src/query/find.ts +212 -0
- package/src/query/lower.ts +692 -724
- package/src/query/predicate.ts +37 -45
- package/src/query/run.ts +17 -22
- package/src/query/scope.ts +184 -90
- package/src/relation.ts +3 -1
- package/src/schema.ts +14 -2
- package/src/spec.ts +21 -9
- package/src/statements.ts +30 -6
- 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,213 +286,217 @@ 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
|
}
|
|
304
|
+
if (cond.cond === "notIdb") {
|
|
305
|
+
const bindings = Object.fromEntries(Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
|
|
306
|
+
return value !== undefined;
|
|
307
|
+
}));
|
|
308
|
+
return notIdbAdvance(context, state, cond.target, bindings);
|
|
309
|
+
}
|
|
305
310
|
if (cond.cond === "not") {
|
|
306
311
|
const relation = cond.relation;
|
|
307
312
|
const bindings = Object.fromEntries(Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
|
|
308
313
|
return value !== undefined;
|
|
309
314
|
}));
|
|
310
|
-
const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings
|
|
311
|
-
return {
|
|
315
|
+
const resolved = resolveBindings(context, `negated relation ${relation.name}`, relation, bindings);
|
|
316
|
+
return Object.freeze({
|
|
312
317
|
items: Object.freeze([...state.items, Object.freeze({ kind: "negated", atom: resolved.atom })]),
|
|
313
|
-
|
|
318
|
+
bound: state.bound,
|
|
314
319
|
paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
|
|
315
|
-
};
|
|
320
|
+
});
|
|
316
321
|
}
|
|
317
322
|
const uses = [];
|
|
318
|
-
const data = condDataOf(cond,
|
|
319
|
-
return {
|
|
323
|
+
const data = condDataOf(cond, uses);
|
|
324
|
+
return Object.freeze({
|
|
320
325
|
items: Object.freeze([...state.items, Object.freeze({ kind: "cond", cond: data })]),
|
|
321
|
-
|
|
326
|
+
bound: state.bound,
|
|
322
327
|
paramUses: Object.freeze([...state.paramUses, ...uses])
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
/** Extends a rule state with one `idb` atom (vars must be bound — validated at completion). */
|
|
326
|
-
function advanceIdb(state, rec, vars) {
|
|
327
|
-
const names = vars.map(function nameOf(variable) {
|
|
328
|
-
if (!isTerm(variable) || variable[term] !== "var") {
|
|
329
|
-
throw errors.new(`idb ${rec.name}: positions take variables — bind literals and params through where()/match()`);
|
|
330
|
-
}
|
|
331
|
-
return variable.name;
|
|
332
328
|
});
|
|
333
|
-
return {
|
|
334
|
-
items: Object.freeze([...state.items, Object.freeze({ kind: "idb", rec, vars: Object.freeze(names) })]),
|
|
335
|
-
varFields: state.varFields,
|
|
336
|
-
paramUses: state.paramUses
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
/** Narrows a select entry to an aggregate value. */
|
|
340
|
-
function isAggregateEntry(value) {
|
|
341
|
-
return typeof value === "object" && value !== null && "agg" in value;
|
|
342
329
|
}
|
|
343
330
|
/**
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
331
|
+
* Extends a rule state with one `idb` atom (a named record over head keys;
|
|
332
|
+
* vars validated at completion). A POSITIVE idb atom is a positive
|
|
333
|
+
* occurrence exactly as the engine represents it (`check_atoms` walks Idb
|
|
334
|
+
* and Edb in one loop), so its variables GROUND: they enter the rule's
|
|
335
|
+
* boundness set, may ride the head, and satisfy negation safety — the
|
|
336
|
+
* idb-only identity projection of a finished stratum is spellable with no
|
|
337
|
+
* re-grounding join. A NEGATED one binds nothing, only rejects.
|
|
348
338
|
*/
|
|
349
|
-
function
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
});
|
|
339
|
+
function advanceIdb(state, rec, bindings, negated) {
|
|
340
|
+
const resolved = [];
|
|
341
|
+
for (const [key, value] of Object.entries(bindings)) {
|
|
342
|
+
if (value === undefined) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (!isTerm(value) || value[term] !== "var") {
|
|
346
|
+
throw errors.new(`idb ${rec.name}: position ${key} takes a variable — bind literals and params through where()/match()`);
|
|
364
347
|
}
|
|
365
|
-
|
|
348
|
+
resolved.push(Object.freeze({ key, ref: value }));
|
|
366
349
|
}
|
|
367
|
-
|
|
368
|
-
|
|
350
|
+
const bound = new Set(state.bound);
|
|
351
|
+
if (!negated) {
|
|
352
|
+
for (const binding of resolved) {
|
|
353
|
+
bound.add(binding.ref);
|
|
354
|
+
}
|
|
369
355
|
}
|
|
370
|
-
|
|
356
|
+
return Object.freeze({
|
|
357
|
+
items: Object.freeze([
|
|
358
|
+
...state.items,
|
|
359
|
+
Object.freeze({ kind: "idb", rec, bindings: Object.freeze(resolved), negated })
|
|
360
|
+
]),
|
|
361
|
+
bound,
|
|
362
|
+
paramUses: state.paramUses
|
|
363
|
+
});
|
|
371
364
|
}
|
|
372
|
-
/**
|
|
373
|
-
function
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
365
|
+
/** Narrows a find entry to an aggregate value. */
|
|
366
|
+
function isAggregateEntry(value) {
|
|
367
|
+
return typeof value === "object" && value !== null && "agg" in value;
|
|
368
|
+
}
|
|
369
|
+
/** Narrows a value to a variable term, else a pointed refusal. */
|
|
370
|
+
function asVarTerm(context, value) {
|
|
371
|
+
if (isTerm(value) && value[term] === "var") {
|
|
372
|
+
return value;
|
|
380
373
|
}
|
|
374
|
+
throw errors.new(`${context}: expected a variable`);
|
|
375
|
+
}
|
|
376
|
+
/** Classifies one aggregate find entry into its runtime data (variables ride by reference). */
|
|
377
|
+
function aggDataOf(name, entry) {
|
|
381
378
|
const over = entry.over;
|
|
382
379
|
switch (entry.agg) {
|
|
383
380
|
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
|
-
}
|
|
381
|
+
return Object.freeze({ op: "count" });
|
|
382
|
+
case "countDistinct":
|
|
383
|
+
return Object.freeze({ op: "countDistinct", over: asVarTerm(`find ${name} (countDistinct)`, over) });
|
|
391
384
|
case "sum":
|
|
392
385
|
case "min":
|
|
393
386
|
case "max": {
|
|
394
|
-
if (
|
|
395
|
-
return
|
|
387
|
+
if (isTerm(over) && over[term] === "var") {
|
|
388
|
+
return Object.freeze({ op: "fold", fold: entry.agg, over });
|
|
396
389
|
}
|
|
397
390
|
if (isTerm(over) && over[term] === "duration") {
|
|
398
|
-
return
|
|
391
|
+
return Object.freeze({ op: "fold", fold: entry.agg, over: Object.freeze({ duration: over.over }) });
|
|
399
392
|
}
|
|
400
|
-
throw errors.new(
|
|
393
|
+
throw errors.new(`find ${name} (${entry.agg}): takes a variable or r.duration(v)`);
|
|
401
394
|
}
|
|
402
395
|
case "argMax":
|
|
403
|
-
case "argMin":
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
396
|
+
case "argMin":
|
|
397
|
+
return Object.freeze({
|
|
398
|
+
op: "arg",
|
|
399
|
+
direction: entry.agg,
|
|
400
|
+
over: asVarTerm(`find ${name} (${entry.agg})`, over),
|
|
401
|
+
key: asVarTerm(`find ${name} (${entry.agg} key)`, entry.key)
|
|
402
|
+
});
|
|
403
|
+
case "pack":
|
|
404
|
+
return Object.freeze({ op: "pack", over: asVarTerm(`find ${name} (pack)`, over) });
|
|
405
|
+
default:
|
|
406
|
+
throw errors.new(`find ${name}: unknown aggregate ${entry.agg}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Classifies one find entry into its named answer column (the KEY names the
|
|
411
|
+
* column, `count` included). The `slot`/`closed` slices are resolved LATER,
|
|
412
|
+
* at rule completion, where boundness and the mint slots are in hand.
|
|
413
|
+
*/
|
|
414
|
+
function findColumnOf(name, entry) {
|
|
415
|
+
if (isTerm(entry)) {
|
|
416
|
+
if (entry[term] === "var") {
|
|
417
|
+
return Object.freeze({
|
|
418
|
+
name,
|
|
419
|
+
entry: Object.freeze({ kind: "var", over: entry }),
|
|
420
|
+
closed: undefined,
|
|
421
|
+
slot: undefined
|
|
422
|
+
});
|
|
408
423
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
424
|
+
if (entry[term] === "duration") {
|
|
425
|
+
return Object.freeze({
|
|
426
|
+
name,
|
|
427
|
+
entry: Object.freeze({ kind: "measure", over: entry.over }),
|
|
428
|
+
closed: undefined,
|
|
429
|
+
slot: undefined
|
|
430
|
+
});
|
|
414
431
|
}
|
|
415
|
-
|
|
416
|
-
|
|
432
|
+
throw errors.new(`find ${name}: a ${entry[term]} is not projectable — find takes variables, r.duration(v), or aggregates`);
|
|
433
|
+
}
|
|
434
|
+
if (isAggregateEntry(entry)) {
|
|
435
|
+
return Object.freeze({
|
|
436
|
+
name,
|
|
437
|
+
entry: Object.freeze({ kind: "aggregate", agg: aggDataOf(name, entry) }),
|
|
438
|
+
closed: undefined,
|
|
439
|
+
slot: undefined
|
|
440
|
+
});
|
|
417
441
|
}
|
|
442
|
+
throw errors.new(`find ${name}: not a find entry — find takes variables, r.duration(v), or aggregates`);
|
|
418
443
|
}
|
|
419
444
|
/**
|
|
420
445
|
* 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).
|
|
446
|
+
* § orderability): a closed reference is equality-and-membership only.
|
|
427
447
|
*/
|
|
428
448
|
function closedOrderError(context, position, vocabulary) {
|
|
429
449
|
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
450
|
}
|
|
431
|
-
/** The comparison ops under the orderable ban (order roster + point membership
|
|
451
|
+
/** The comparison ops under the orderable ban (order roster + point membership). */
|
|
432
452
|
function isOrderOp(op) {
|
|
433
453
|
return op === "lt" || op === "le" || op === "gt" || op === "ge" || op === "pointIn";
|
|
434
454
|
}
|
|
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`);
|
|
455
|
+
/** Requires a variable to be bound by a relation atom of the rule (the boundness wall — invisible to the type tier). */
|
|
456
|
+
function assertBound(where, bound, ref) {
|
|
457
|
+
if (!bound.has(ref)) {
|
|
458
|
+
throw errors.new(`${where}: the variable ${ref.label} is not bound by a relation atom of the rule`);
|
|
440
459
|
}
|
|
441
|
-
return slot;
|
|
442
460
|
}
|
|
443
|
-
/** Requires a
|
|
444
|
-
function
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
461
|
+
/** Requires a variable to be interval-typed (the measure's and pack's domain), off its own descriptor. */
|
|
462
|
+
function assertInterval(where, ref) {
|
|
463
|
+
if (ref.field.kind !== "interval") {
|
|
464
|
+
throw errors.new(`${where}: ${ref.label} is not interval-typed — the measure is defined over interval-typed variables only`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/** Requires a variable's own field to be non-closed (the orderable ban's runtime twin). */
|
|
468
|
+
function assertNotClosed(where, position, ref) {
|
|
469
|
+
const roster = rosterOf(ref.field);
|
|
470
|
+
if (roster !== undefined) {
|
|
471
|
+
throw closedOrderError(where, `${position} ${ref.label}`, roster.name);
|
|
448
472
|
}
|
|
449
473
|
}
|
|
450
474
|
/**
|
|
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.
|
|
475
|
+
* The classed mint slot one answer column's VALUES flow from: a projected
|
|
476
|
+
* variable's mint slot, or an Arg-carried payload's. Counts, folds, `pack`
|
|
477
|
+
* and the measure derive numbers/intervals, so they resolve no slot.
|
|
458
478
|
*/
|
|
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;
|
|
479
|
+
function findColumnSlotOf(context, column) {
|
|
480
|
+
const entry = column.entry;
|
|
481
|
+
if (entry.kind === "var") {
|
|
482
|
+
return mintSlotOf(context, entry.over);
|
|
481
483
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
+
if (entry.kind === "aggregate" && entry.agg.op === "arg") {
|
|
485
|
+
return mintSlotOf(context, entry.agg.over);
|
|
484
486
|
}
|
|
487
|
+
return undefined;
|
|
485
488
|
}
|
|
486
|
-
/** Validates one
|
|
487
|
-
function validateColumn(context,
|
|
489
|
+
/** Validates one find column's variable references (boundness + the orderable/interval walls, off the var's own field). */
|
|
490
|
+
function validateColumn(context, bound, column) {
|
|
491
|
+
const where = `${contextLabel(context)} find ${column.name}`;
|
|
488
492
|
const entry = column.entry;
|
|
489
493
|
if (entry.kind === "var") {
|
|
490
|
-
assertBound(
|
|
494
|
+
assertBound(where, bound, entry.over);
|
|
491
495
|
return;
|
|
492
496
|
}
|
|
493
497
|
if (entry.kind === "measure") {
|
|
494
|
-
|
|
498
|
+
assertBound(where, bound, entry.over);
|
|
499
|
+
assertInterval(where, entry.over);
|
|
495
500
|
return;
|
|
496
501
|
}
|
|
497
502
|
const agg = entry.agg;
|
|
@@ -499,128 +504,146 @@ function validateColumn(context, varFields, column) {
|
|
|
499
504
|
case "count":
|
|
500
505
|
return;
|
|
501
506
|
case "countDistinct":
|
|
502
|
-
assertBound(
|
|
507
|
+
assertBound(where, bound, agg.over);
|
|
503
508
|
return;
|
|
504
509
|
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
|
-
}
|
|
510
|
+
if ("duration" in agg.over) {
|
|
511
|
+
assertBound(where, bound, agg.over.duration);
|
|
512
|
+
assertInterval(where, agg.over.duration);
|
|
511
513
|
return;
|
|
512
514
|
}
|
|
513
|
-
|
|
515
|
+
assertBound(where, bound, agg.over);
|
|
516
|
+
assertNotClosed(where, `the ${agg.fold} input`, agg.over);
|
|
514
517
|
return;
|
|
515
518
|
}
|
|
516
519
|
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
|
-
}
|
|
520
|
+
assertBound(where, bound, agg.over);
|
|
521
|
+
assertBound(where, bound, agg.key);
|
|
522
|
+
assertNotClosed(where, `the ${agg.direction} key`, agg.key);
|
|
523
523
|
return;
|
|
524
524
|
}
|
|
525
525
|
case "pack":
|
|
526
|
-
|
|
526
|
+
assertBound(where, bound, agg.over);
|
|
527
|
+
assertInterval(where, agg.over);
|
|
527
528
|
return;
|
|
528
529
|
}
|
|
529
530
|
}
|
|
530
531
|
/**
|
|
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.
|
|
532
|
+
* Validates one condition's variable references against the rule's bound
|
|
533
|
+
* set — and, for `eq`/`ne` over two variables, holds the class wall through
|
|
534
|
+
* the mint slots (the unification IS a join; bare pairs only with bare).
|
|
539
535
|
*/
|
|
540
|
-
function
|
|
541
|
-
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
536
|
+
function validateCond(context, bound, cond) {
|
|
537
|
+
const label = contextLabel(context);
|
|
538
|
+
if (cond.kind === "cmp") {
|
|
539
|
+
for (const side of [cond.lhs, cond.rhs]) {
|
|
540
|
+
if (side.kind === "var") {
|
|
541
|
+
assertBound(label, bound, side.ref);
|
|
542
|
+
const roster = rosterOf(side.ref.field);
|
|
543
|
+
if (isOrderOp(cond.op) && roster !== undefined) {
|
|
544
|
+
throw closedOrderError(label, `the ${cond.op} side ${side.ref.label}`, roster.name);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (side.kind === "measure") {
|
|
548
|
+
assertBound(label, bound, side.ref);
|
|
549
|
+
assertInterval(label, side.ref);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
|
|
553
|
+
assertBound(label, bound, cond.lhs.ref);
|
|
554
|
+
assertBound(label, bound, cond.rhs.ref);
|
|
555
|
+
const lhs = mintSlotOf(context, cond.lhs.ref);
|
|
556
|
+
const rhs = mintSlotOf(context, cond.rhs.ref);
|
|
557
|
+
if (!fieldJoins(lhs, rhs)) {
|
|
558
|
+
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)`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return;
|
|
550
562
|
}
|
|
551
|
-
|
|
552
|
-
|
|
563
|
+
for (const child of cond.children) {
|
|
564
|
+
validateCond(context, bound, child);
|
|
553
565
|
}
|
|
554
|
-
return rosterOf(varFields[over]?.field);
|
|
555
566
|
}
|
|
556
567
|
/**
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
568
|
+
* Validates one `idb` item: every head column of the rec is bound exactly
|
|
569
|
+
* once (a missing or extra key is a pointed error) and each variable joins
|
|
570
|
+
* its head column's classed slot. A POSITIVE idb atom GROUNDS its
|
|
571
|
+
* variables (a positive occurrence, exactly the engine's representation),
|
|
572
|
+
* so no boundness precondition exists; a NEGATED one binds nothing — its
|
|
573
|
+
* variables must be positively bound elsewhere in the rule, the same
|
|
574
|
+
* safety rule as EDB negation. When the rec's own rule 0 is in flight
|
|
575
|
+
* (`rec.rules[0]` absent), the completing rule's OWN find columns ARE the
|
|
576
|
+
* head.
|
|
562
577
|
*/
|
|
563
|
-
function
|
|
564
|
-
|
|
565
|
-
|
|
578
|
+
function validateIdb(context, bound, item, columns) {
|
|
579
|
+
const label = contextLabel(context);
|
|
580
|
+
const head = item.rec.rules[0];
|
|
581
|
+
const headColumns = head !== undefined ? head.finds : columns;
|
|
582
|
+
const headNames = headColumns.map(function nameOf(column) {
|
|
583
|
+
return column.name;
|
|
584
|
+
});
|
|
585
|
+
const keys = item.bindings.map(function keyOf(binding) {
|
|
586
|
+
return binding.key;
|
|
587
|
+
});
|
|
588
|
+
for (const key of keys) {
|
|
589
|
+
if (!headNames.includes(key)) {
|
|
590
|
+
throw errors.new(`${label}: idb ${item.rec.name} binds ${key}, not a head column of ${item.rec.name} (head columns: ${headNames.join(", ")})`);
|
|
591
|
+
}
|
|
566
592
|
}
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (seen.has(column.name)) {
|
|
571
|
-
throw errors.new(`${context}: select names the answer column ${column.name} twice`);
|
|
593
|
+
for (const name of headNames) {
|
|
594
|
+
if (!keys.includes(name)) {
|
|
595
|
+
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
596
|
}
|
|
573
|
-
seen.add(column.name);
|
|
574
|
-
validateColumn(context, state.varFields, column);
|
|
575
597
|
}
|
|
598
|
+
for (const binding of item.bindings) {
|
|
599
|
+
if (item.negated && !bound.has(binding.ref)) {
|
|
600
|
+
throw errors.new(`${label}: negated idb ${item.rec.name} names the variable ${binding.ref.label}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`);
|
|
601
|
+
}
|
|
602
|
+
const headColumn = headColumns.find(function byName(column) {
|
|
603
|
+
return column.name === binding.key;
|
|
604
|
+
});
|
|
605
|
+
if (headColumn === undefined || headColumn.slot === undefined) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const mint = mintSlotOf(context, binding.ref);
|
|
609
|
+
if (!fieldJoins(headColumn.slot, mint)) {
|
|
610
|
+
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`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Completes one rule: enriches the find columns (declaration-order-safe
|
|
616
|
+
* keys, boundness validated, each column's classed slot and closed slice
|
|
617
|
+
* resolved), then walks the body walls — negated-atom boundness safety, idb
|
|
618
|
+
* head pairing, and condition validation.
|
|
619
|
+
*/
|
|
620
|
+
function completeRule(context, state, rawColumns) {
|
|
621
|
+
const label = contextLabel(context);
|
|
622
|
+
if (rawColumns.length === 0) {
|
|
623
|
+
throw errors.new(`${label}: a find needs at least one entry`);
|
|
624
|
+
}
|
|
625
|
+
const columns = rawColumns.map(function enrichColumn(column) {
|
|
626
|
+
assertDeclarationOrderKey(`${label} find column`, column.name);
|
|
627
|
+
validateColumn(context, state.bound, column);
|
|
628
|
+
const slot = findColumnSlotOf(context, column);
|
|
629
|
+
return Object.freeze({ name: column.name, entry: column.entry, slot, closed: rosterOf(slot?.field) });
|
|
630
|
+
});
|
|
576
631
|
for (const item of state.items) {
|
|
577
632
|
if (item.kind === "negated") {
|
|
578
633
|
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
|
-
}
|
|
634
|
+
if (binding.term.kind === "var" && !state.bound.has(binding.term.ref)) {
|
|
635
|
+
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
636
|
}
|
|
589
637
|
}
|
|
590
638
|
}
|
|
591
639
|
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
|
-
});
|
|
640
|
+
validateIdb(context, state.bound, item, columns);
|
|
607
641
|
}
|
|
608
642
|
if (item.kind === "cond") {
|
|
609
|
-
validateCond(context, state.
|
|
643
|
+
validateCond(context, state.bound, item.cond);
|
|
610
644
|
}
|
|
611
645
|
}
|
|
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
|
-
});
|
|
646
|
+
return Object.freeze({ items: state.items, finds: Object.freeze(columns), paramUses: state.paramUses });
|
|
624
647
|
}
|
|
625
648
|
/** Builds one typed rule value over completed rule data. */
|
|
626
649
|
function makeRuleValue(rule) {
|
|
@@ -638,7 +661,7 @@ function contextLabel(context) {
|
|
|
638
661
|
}
|
|
639
662
|
}
|
|
640
663
|
/** Validates and records one `idb` atom per the context's cut. */
|
|
641
|
-
function idbAdvance(context, state, target,
|
|
664
|
+
function idbAdvance(context, state, target, bindings) {
|
|
642
665
|
if (context.kind === "query") {
|
|
643
666
|
throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()");
|
|
644
667
|
}
|
|
@@ -646,36 +669,60 @@ function idbAdvance(context, state, target, vars) {
|
|
|
646
669
|
if (target.data !== context.self) {
|
|
647
670
|
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
671
|
}
|
|
649
|
-
return advanceIdb(state, context.self,
|
|
672
|
+
return advanceIdb(state, context.self, bindings, false);
|
|
650
673
|
}
|
|
651
674
|
if (!context.program.recs.includes(target.data)) {
|
|
652
675
|
throw errors.new(`idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`);
|
|
653
676
|
}
|
|
654
|
-
return advanceIdb(state, target.data,
|
|
677
|
+
return advanceIdb(state, target.data, bindings, false);
|
|
655
678
|
}
|
|
656
|
-
/**
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
679
|
+
/**
|
|
680
|
+
* Validates and records one NEGATED finished-stratum atom — output rules
|
|
681
|
+
* only: there every rec is a finished set before the output's operator
|
|
682
|
+
* runs (negation OF lower strata is engine-legal; the strata judge refuses
|
|
683
|
+
* only negation *through* a cycle, which the rec-context refusal here
|
|
684
|
+
* makes unwritable).
|
|
685
|
+
*/
|
|
686
|
+
function notIdbAdvance(context, state, target, bindings) {
|
|
687
|
+
if (context.kind === "query") {
|
|
688
|
+
throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()");
|
|
689
|
+
}
|
|
690
|
+
if (context.kind === "rec") {
|
|
691
|
+
throw errors.new(`rec ${context.self.name}: a recursive rule negates no stratum — self-negation is negation through the cycle (a finished set is what keeps the operator monotone), and a finished stratum's fold belongs in the output rules`);
|
|
692
|
+
}
|
|
693
|
+
if (!context.program.recs.includes(target.data)) {
|
|
694
|
+
throw errors.new(`idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`);
|
|
695
|
+
}
|
|
696
|
+
return advanceIdb(state, target.data, bindings, true);
|
|
697
|
+
}
|
|
698
|
+
/** Classifies one find record per the context (a recursive head projects bound variables only). */
|
|
699
|
+
function findColumns(context, entries) {
|
|
700
|
+
const columns = [];
|
|
701
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
702
|
+
if (entry === undefined) {
|
|
703
|
+
continue;
|
|
661
704
|
}
|
|
662
|
-
|
|
663
|
-
|
|
705
|
+
if (context.kind === "rec" && !(isTerm(entry) && entry[term] === "var")) {
|
|
706
|
+
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)`);
|
|
707
|
+
}
|
|
708
|
+
columns.push(findColumnOf(name, entry));
|
|
709
|
+
}
|
|
710
|
+
return columns;
|
|
664
711
|
}
|
|
665
712
|
/** Builds one runtime chain (immutably — every step is a fresh chain over fresh state). */
|
|
666
713
|
function makeRawChain(context, state) {
|
|
667
714
|
const chain = {
|
|
668
715
|
match(relation, bindings) {
|
|
669
|
-
return makeRawChain(context, advanceMatch(state, relation, bindings
|
|
716
|
+
return makeRawChain(context, advanceMatch(context, state, relation, bindings));
|
|
670
717
|
},
|
|
671
718
|
where(cond) {
|
|
672
|
-
return makeRawChain(context, advanceWhere(state, cond
|
|
719
|
+
return makeRawChain(context, advanceWhere(context, state, cond));
|
|
673
720
|
},
|
|
674
|
-
idb(target,
|
|
675
|
-
return makeRawChain(context, idbAdvance(context, state, target,
|
|
721
|
+
idb(target, bindings) {
|
|
722
|
+
return makeRawChain(context, idbAdvance(context, state, target, bindings));
|
|
676
723
|
},
|
|
677
|
-
|
|
678
|
-
return makeRuleValue(completeRule(
|
|
724
|
+
find(entries) {
|
|
725
|
+
return makeRuleValue(completeRule(context, state, findColumns(context, entries)));
|
|
679
726
|
}
|
|
680
727
|
};
|
|
681
728
|
Object.freeze(chain);
|
|
@@ -686,7 +733,10 @@ function makeRawScope(context) {
|
|
|
686
733
|
const scope = {
|
|
687
734
|
...termOps,
|
|
688
735
|
match(relation, bindings) {
|
|
689
|
-
return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings
|
|
736
|
+
return makeRawChain(context, advanceMatch(context, EMPTY_RULE, relation, bindings));
|
|
737
|
+
},
|
|
738
|
+
idb(target, bindings) {
|
|
739
|
+
return makeRawChain(context, idbAdvance(context, EMPTY_RULE, target, bindings));
|
|
690
740
|
}
|
|
691
741
|
};
|
|
692
742
|
Object.freeze(scope);
|
|
@@ -694,21 +744,19 @@ function makeRawScope(context) {
|
|
|
694
744
|
}
|
|
695
745
|
/**
|
|
696
746
|
* 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.
|
|
747
|
+
* trusted-admission-seam pattern the other mint guards cite: the raw builder
|
|
748
|
+
* is one runtime shape for every context, and this guard verifies the
|
|
749
|
+
* checkable fact — the builder verbs exist — before the value is admitted at
|
|
750
|
+
* its TYPED face. The type-level judgments (class-equal joins, the recursion
|
|
751
|
+
* cut) live in the interfaces themselves; boundness is a construction-time
|
|
752
|
+
* validation in this module (object identity is invisible to the type tier).
|
|
705
753
|
*/
|
|
706
754
|
function isTypedScope(scope) {
|
|
707
755
|
return typeof scope.match === "function";
|
|
708
756
|
}
|
|
709
757
|
/** Builds one query-rule builder (the typed face of the raw builder). */
|
|
710
|
-
function makeQueryRuleScope(
|
|
711
|
-
const raw = makeRawScope({ kind: "query", classes });
|
|
758
|
+
function makeQueryRuleScope(theory) {
|
|
759
|
+
const raw = makeRawScope({ kind: "query", classes: theory.classes, theory });
|
|
712
760
|
if (!isTypedScope(raw)) {
|
|
713
761
|
throw errors.new("query rule builder construction incomplete");
|
|
714
762
|
}
|
|
@@ -716,7 +764,7 @@ function makeQueryRuleScope(classes) {
|
|
|
716
764
|
}
|
|
717
765
|
/** Builds one output-rule builder over a program's recs. */
|
|
718
766
|
function makeOutputRuleScope(program) {
|
|
719
|
-
const raw = makeRawScope({ kind: "output", program, classes: program.classes });
|
|
767
|
+
const raw = makeRawScope({ kind: "output", program, classes: program.classes, theory: program.theory });
|
|
720
768
|
if (!isTypedScope(raw)) {
|
|
721
769
|
throw errors.new("program output rule builder construction incomplete");
|
|
722
770
|
}
|
|
@@ -741,25 +789,7 @@ function headSignature(column) {
|
|
|
741
789
|
}
|
|
742
790
|
return `${column.name}:${agg.op}`;
|
|
743
791
|
}
|
|
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). */
|
|
792
|
+
/** The roster a param anchor carries: present exactly on a closed-reference field anchor. */
|
|
763
793
|
function anchorRosterOf(anchor) {
|
|
764
794
|
return anchor === "measure" ? undefined : rosterOf(anchor);
|
|
765
795
|
}
|
|
@@ -769,20 +799,9 @@ function renderParamAnchor(roster) {
|
|
|
769
799
|
}
|
|
770
800
|
/**
|
|
771
801
|
* 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).
|
|
802
|
+
* rules last — exactly the lowering walk) into the query's registry: first
|
|
803
|
+
* use mints the dense `ParamId`, the first FIELD-ANCHORED use types the
|
|
804
|
+
* wire, and one name keeps one shape AND one closedness.
|
|
786
805
|
*/
|
|
787
806
|
function paramRegistryOf(recs, rules) {
|
|
788
807
|
const order = [];
|
|
@@ -840,64 +859,79 @@ function paramRegistryOf(recs, rules) {
|
|
|
840
859
|
if (entry.orderOp !== undefined && anchorRoster !== undefined) {
|
|
841
860
|
throw closedOrderError(`query param ${name}`, `its ${entry.orderOp} use's anchor`, anchorRoster.name);
|
|
842
861
|
}
|
|
843
|
-
|
|
862
|
+
/**
|
|
863
|
+
* A membership array's handle names are program constants, so the
|
|
864
|
+
* entry stores the resolved IMAGE: each name rides the one
|
|
865
|
+
* roster-verification point (`taggedHandleId`, through
|
|
866
|
+
* `taggedCmpLiteral`) exactly once, HERE — an out-of-roster name
|
|
867
|
+
* fails at build, and every execute returns this frozen value by
|
|
868
|
+
* reference.
|
|
869
|
+
*/
|
|
870
|
+
let membership;
|
|
871
|
+
if (entry.members !== undefined) {
|
|
872
|
+
const anchor = entry.anchor;
|
|
873
|
+
if (anchor === undefined) {
|
|
874
|
+
throw errors.new(`query param ${name} lost its membership anchor`);
|
|
875
|
+
}
|
|
876
|
+
membership = Object.freeze({
|
|
877
|
+
kind: "set",
|
|
878
|
+
values: Object.freeze(entry.members.map(function tagMember(member, index) {
|
|
879
|
+
return Object.freeze(taggedCmpLiteral(`membership array ${name}[${index}]`, anchor, member, entry.op));
|
|
880
|
+
}))
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op, membership });
|
|
844
884
|
}));
|
|
845
885
|
}
|
|
846
886
|
/**
|
|
847
887
|
* Assembles the runtime query value over completed rules: every rule must
|
|
848
888
|
* derive the SAME head (name and aggregate shape, position for position —
|
|
849
|
-
* the decode labels and the engine's alignment rule agree
|
|
850
|
-
*
|
|
889
|
+
* the decode labels and the engine's alignment rule agree), and the param
|
|
890
|
+
* registry folds in program-walk order.
|
|
851
891
|
*/
|
|
852
892
|
function makeRawQuery(theory, recs, rules) {
|
|
853
893
|
const first = rules[0];
|
|
854
894
|
if (first === undefined) {
|
|
855
895
|
throw errors.new("a query needs at least one rule");
|
|
856
896
|
}
|
|
857
|
-
const signature = first.
|
|
897
|
+
const signature = first.finds.map(headSignature).join(", ");
|
|
858
898
|
rules.forEach(function verifyHead(rule, index) {
|
|
859
|
-
const candidate = rule.
|
|
899
|
+
const candidate = rule.finds.map(headSignature).join(", ");
|
|
860
900
|
if (candidate !== signature) {
|
|
861
|
-
throw errors.new(`every rule of a query derives the same head — rule 0
|
|
901
|
+
throw errors.new(`every rule of a query derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`);
|
|
862
902
|
}
|
|
863
903
|
// 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];
|
|
904
|
+
// through one roster, so a union whose rules bind a column at different
|
|
905
|
+
// vocabularies (or one closed, one bare) is refused pointed.
|
|
906
|
+
rule.finds.forEach(function verifyClosedSlice(column, position) {
|
|
907
|
+
const lead = first.finds[position];
|
|
870
908
|
if (lead !== undefined && column.closed !== lead.closed) {
|
|
871
909
|
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
910
|
}
|
|
873
911
|
// 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.
|
|
912
|
+
// value space, so the classed mint slot each rule binds the column
|
|
913
|
+
// at must join across rules — the SAME fieldJoins judgment every
|
|
914
|
+
// join/eq/negated-atom position enforces (the SDK holds it because
|
|
915
|
+
// the wire IR carries no domains).
|
|
880
916
|
if (lead === undefined) {
|
|
881
917
|
return;
|
|
882
918
|
}
|
|
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)`);
|
|
919
|
+
if (lead.slot !== undefined && column.slot !== undefined && !fieldJoins(lead.slot, column.slot)) {
|
|
920
|
+
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
921
|
}
|
|
888
922
|
});
|
|
889
923
|
});
|
|
890
924
|
const data = Object.freeze({
|
|
891
925
|
recs: Object.freeze([...recs]),
|
|
892
926
|
rules: Object.freeze([...rules]),
|
|
893
|
-
|
|
927
|
+
finds: first.finds,
|
|
894
928
|
params: paramRegistryOf(recs, rules)
|
|
895
929
|
});
|
|
896
930
|
const value = {
|
|
897
931
|
schema: theory,
|
|
898
932
|
data,
|
|
899
933
|
rule(build) {
|
|
900
|
-
const built = build(makeRawScope({ kind: "query", classes: theory.classes }));
|
|
934
|
+
const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory }));
|
|
901
935
|
return makeRawQuery(theory, recs, [...rules, built.rule]);
|
|
902
936
|
}
|
|
903
937
|
};
|
|
@@ -921,16 +955,15 @@ function makeQuery(theory, recs, rules) {
|
|
|
921
955
|
return raw;
|
|
922
956
|
}
|
|
923
957
|
/**
|
|
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.
|
|
958
|
+
* Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule` adds
|
|
959
|
+
* one conjunctive rule; multiple rules are the set union. The schema's
|
|
960
|
+
* law-computed class map and theory value ride into every rule builder — the
|
|
961
|
+
* join walls compare against the mint slots off it.
|
|
929
962
|
*/
|
|
930
963
|
function query(theory) {
|
|
931
964
|
const start = {
|
|
932
965
|
rule(build) {
|
|
933
|
-
const built = build(makeQueryRuleScope(theory
|
|
966
|
+
const built = build(makeQueryRuleScope(theory));
|
|
934
967
|
return makeQuery(theory, [], [built.rule]);
|
|
935
968
|
}
|
|
936
969
|
};
|
|
@@ -939,13 +972,8 @@ function query(theory) {
|
|
|
939
972
|
}
|
|
940
973
|
/**
|
|
941
974
|
* 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).
|
|
975
|
+
* roster and translated to its declaration-order row id, tagged u64. THE
|
|
976
|
+
* single roster-verification point of the query surface.
|
|
949
977
|
*/
|
|
950
978
|
function taggedHandleId(context, closed, value) {
|
|
951
979
|
if (typeof value !== "string") {
|
|
@@ -979,11 +1007,7 @@ function taggedAtElementDomain(context, element, value) {
|
|
|
979
1007
|
}
|
|
980
1008
|
/**
|
|
981
1009
|
* 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.
|
|
1010
|
+
* structural kind directs the tag, never a guess.
|
|
987
1011
|
*/
|
|
988
1012
|
function taggedLiteral(context, field, value) {
|
|
989
1013
|
const roster = rosterOf(field);
|
|
@@ -1013,15 +1037,6 @@ function taggedLiteral(context, field, value) {
|
|
|
1013
1037
|
if (typeof value !== "string") {
|
|
1014
1038
|
throw literalShapeError(context, "string", value);
|
|
1015
1039
|
}
|
|
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
1040
|
if (!value.isWellFormed()) {
|
|
1026
1041
|
throw literalShapeError(context, "well-formed string", value);
|
|
1027
1042
|
}
|
|
@@ -1038,17 +1053,13 @@ function taggedLiteral(context, field, value) {
|
|
|
1038
1053
|
}
|
|
1039
1054
|
}
|
|
1040
1055
|
/**
|
|
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).
|
|
1056
|
+
* Tags one host literal at a COMPARISON or PARAM position, where the SIBLING
|
|
1057
|
+
* anchors the type: a measure sibling is u64, an interval-field sibling
|
|
1058
|
+
* contributes its element domain, a scalar sibling its own type. At
|
|
1059
|
+
* `pointIn` the operand order is interval-left, point-right, so an
|
|
1060
|
+
* interval-shaped literal beside a scalar element-typed sibling is the LEGAL
|
|
1061
|
+
* interval operand of `pointIn(t, span(...))`; under every other operator an
|
|
1062
|
+
* interval shape against a scalar sibling stays refused.
|
|
1052
1063
|
*/
|
|
1053
1064
|
function taggedCmpLiteral(context, sibling, value, op) {
|
|
1054
1065
|
if (sibling === "measure") {
|
|
@@ -1069,16 +1080,16 @@ function taggedCmpLiteral(context, sibling, value, op) {
|
|
|
1069
1080
|
return taggedLiteral(context, sibling, value);
|
|
1070
1081
|
}
|
|
1071
1082
|
/** Creates one rule-scoped variable numberer. */
|
|
1072
|
-
function
|
|
1083
|
+
function freshVarIds() {
|
|
1073
1084
|
const assigned = new Map();
|
|
1074
1085
|
return {
|
|
1075
|
-
of(
|
|
1076
|
-
const existing = assigned.get(
|
|
1086
|
+
of(ref) {
|
|
1087
|
+
const existing = assigned.get(ref);
|
|
1077
1088
|
if (existing !== undefined) {
|
|
1078
1089
|
return existing;
|
|
1079
1090
|
}
|
|
1080
1091
|
const id = assigned.size;
|
|
1081
|
-
assigned.set(
|
|
1092
|
+
assigned.set(ref, id);
|
|
1082
1093
|
return id;
|
|
1083
1094
|
}
|
|
1084
1095
|
};
|
|
@@ -1093,10 +1104,7 @@ function paramIdOf(ctx, name) {
|
|
|
1093
1104
|
}
|
|
1094
1105
|
/**
|
|
1095
1106
|
* 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).
|
|
1107
|
+
* same edb source, with field ordinals over the SEALED shape.
|
|
1100
1108
|
*/
|
|
1101
1109
|
function lowerAtom(ctx, atom, ids) {
|
|
1102
1110
|
const member = ctx.theory.relations[atom.relation.name];
|
|
@@ -1119,17 +1127,12 @@ function lowerAtom(ctx, atom, ids) {
|
|
|
1119
1127
|
});
|
|
1120
1128
|
return { source: { kind: "edb", relation: relationId }, bindings };
|
|
1121
1129
|
}
|
|
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
|
-
*/
|
|
1130
|
+
/** Lowers one binding term. A membership ARRAY lowers to the existing param-set term over its content-addressed entry. */
|
|
1128
1131
|
function lowerBindingTerm(ctx, context, binding, ids) {
|
|
1129
1132
|
const bound = binding.term;
|
|
1130
1133
|
switch (bound.kind) {
|
|
1131
1134
|
case "var":
|
|
1132
|
-
return { kind: "var", var: ids.of(bound.
|
|
1135
|
+
return { kind: "var", var: ids.of(bound.ref) };
|
|
1133
1136
|
case "param":
|
|
1134
1137
|
return { kind: "param", param: paramIdOf(ctx, bound.name) };
|
|
1135
1138
|
case "setParam":
|
|
@@ -1140,34 +1143,45 @@ function lowerBindingTerm(ctx, context, binding, ids) {
|
|
|
1140
1143
|
return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) };
|
|
1141
1144
|
}
|
|
1142
1145
|
}
|
|
1143
|
-
/**
|
|
1144
|
-
|
|
1146
|
+
/**
|
|
1147
|
+
* Lowers one idb atom: named bindings placed by HEAD order, `FieldId(i)` =
|
|
1148
|
+
* head position i. Every head column of the rec must be bound (a missing key
|
|
1149
|
+
* is refused pointed); the var-id assignment order is head order, so the
|
|
1150
|
+
* first-use numbering matches the name-keyed edition exactly.
|
|
1151
|
+
*/
|
|
1152
|
+
function lowerIdbAtom(ctx, rec, bindings, ids) {
|
|
1145
1153
|
const pred = ctx.recIds.get(rec);
|
|
1146
1154
|
if (pred === undefined) {
|
|
1147
1155
|
throw errors.new(`query lowering: rec ${rec.name} was declared by a different program`);
|
|
1148
1156
|
}
|
|
1149
|
-
const
|
|
1150
|
-
if (
|
|
1151
|
-
throw errors.new(`query lowering:
|
|
1157
|
+
const head = rec.rules[0];
|
|
1158
|
+
if (head === undefined) {
|
|
1159
|
+
throw errors.new(`query lowering: rec ${rec.name} has no rules`);
|
|
1152
1160
|
}
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1161
|
+
const irBindings = head.finds.map(function lowerPosition(column, position) {
|
|
1162
|
+
const binding = bindings.find(function byKey(candidate) {
|
|
1163
|
+
return candidate.key === column.name;
|
|
1164
|
+
});
|
|
1165
|
+
if (binding === undefined) {
|
|
1166
|
+
throw errors.new(`query lowering: idb ${rec.name} omits head column ${column.name}`);
|
|
1167
|
+
}
|
|
1168
|
+
return [position, { kind: "var", var: ids.of(binding.ref) }];
|
|
1155
1169
|
});
|
|
1156
|
-
return { source: { kind: "idb", pred }, bindings };
|
|
1170
|
+
return { source: { kind: "idb", pred }, bindings: irBindings };
|
|
1157
1171
|
}
|
|
1158
1172
|
/** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
|
|
1159
|
-
function lowerCmpTerm(ctx,
|
|
1173
|
+
function lowerCmpTerm(ctx, side, sibling, ids, op) {
|
|
1160
1174
|
switch (side.kind) {
|
|
1161
1175
|
case "var":
|
|
1162
|
-
return { kind: "var", var: ids.of(side.
|
|
1176
|
+
return { kind: "var", var: ids.of(side.ref) };
|
|
1163
1177
|
case "param":
|
|
1164
1178
|
return { kind: "param", param: paramIdOf(ctx, side.name) };
|
|
1165
1179
|
case "setParam":
|
|
1166
1180
|
return { kind: "paramSet", param: paramIdOf(ctx, side.name) };
|
|
1167
1181
|
case "measure":
|
|
1168
|
-
return { kind: "measure", var: ids.of(side.
|
|
1182
|
+
return { kind: "measure", var: ids.of(side.ref) };
|
|
1169
1183
|
case "literal": {
|
|
1170
|
-
const anchor = cmpAnchorOf(ctx,
|
|
1184
|
+
const anchor = cmpAnchorOf(ctx, sibling);
|
|
1171
1185
|
if (anchor === undefined) {
|
|
1172
1186
|
throw errors.new("query lowering: a comparison literal needs a bound-variable, measure, or anchored-param sibling to type it");
|
|
1173
1187
|
}
|
|
@@ -1175,10 +1189,10 @@ function lowerCmpTerm(ctx, rule, side, sibling, ids, op) {
|
|
|
1175
1189
|
}
|
|
1176
1190
|
}
|
|
1177
1191
|
}
|
|
1178
|
-
/** Resolves the anchor a comparison literal tags by: the sibling's field, the measure, or an anchored param. */
|
|
1179
|
-
function cmpAnchorOf(ctx,
|
|
1192
|
+
/** Resolves the anchor a comparison literal tags by: the sibling variable's field, the measure, or an anchored param. */
|
|
1193
|
+
function cmpAnchorOf(ctx, sibling) {
|
|
1180
1194
|
if (sibling.kind === "var") {
|
|
1181
|
-
return
|
|
1195
|
+
return sibling.ref.field;
|
|
1182
1196
|
}
|
|
1183
1197
|
if (sibling.kind === "measure") {
|
|
1184
1198
|
return "measure";
|
|
@@ -1189,7 +1203,7 @@ function cmpAnchorOf(ctx, rule, sibling) {
|
|
|
1189
1203
|
return undefined;
|
|
1190
1204
|
}
|
|
1191
1205
|
/** Lowers one comparison. */
|
|
1192
|
-
function lowerComparison(ctx,
|
|
1206
|
+
function lowerComparison(ctx, cmp, ids) {
|
|
1193
1207
|
if (cmp.op === "allen") {
|
|
1194
1208
|
const maskData = cmp.mask;
|
|
1195
1209
|
if (maskData === undefined) {
|
|
@@ -1200,29 +1214,29 @@ function lowerComparison(ctx, rule, cmp, ids) {
|
|
|
1200
1214
|
: { kind: "param", param: paramIdOf(ctx, maskData.name) };
|
|
1201
1215
|
return {
|
|
1202
1216
|
op: { kind: "allen", mask },
|
|
1203
|
-
lhs: lowerCmpTerm(ctx,
|
|
1204
|
-
rhs: lowerCmpTerm(ctx,
|
|
1217
|
+
lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
|
|
1218
|
+
rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
|
|
1205
1219
|
};
|
|
1206
1220
|
}
|
|
1207
1221
|
return {
|
|
1208
1222
|
op: { kind: cmp.op },
|
|
1209
|
-
lhs: lowerCmpTerm(ctx,
|
|
1210
|
-
rhs: lowerCmpTerm(ctx,
|
|
1223
|
+
lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, cmp.op),
|
|
1224
|
+
rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, cmp.op)
|
|
1211
1225
|
};
|
|
1212
1226
|
}
|
|
1213
1227
|
/** Lowers one condition node (comparison leaf or and/or tree). */
|
|
1214
|
-
function lowerCondition(ctx,
|
|
1228
|
+
function lowerCondition(ctx, cond, ids) {
|
|
1215
1229
|
if (cond.kind === "cmp") {
|
|
1216
|
-
return { kind: "leaf", cmp: lowerComparison(ctx,
|
|
1230
|
+
return { kind: "leaf", cmp: lowerComparison(ctx, cond, ids) };
|
|
1217
1231
|
}
|
|
1218
1232
|
return {
|
|
1219
1233
|
kind: cond.op,
|
|
1220
1234
|
children: cond.children.map(function lowerChild(child) {
|
|
1221
|
-
return lowerCondition(ctx,
|
|
1235
|
+
return lowerCondition(ctx, child, ids);
|
|
1222
1236
|
})
|
|
1223
1237
|
};
|
|
1224
1238
|
}
|
|
1225
|
-
/** Lowers one
|
|
1239
|
+
/** Lowers one find entry to its per-rule find term. */
|
|
1226
1240
|
function lowerFind(entry, ids) {
|
|
1227
1241
|
if (entry.kind === "var") {
|
|
1228
1242
|
return { kind: "var", var: ids.of(entry.over) };
|
|
@@ -1237,10 +1251,10 @@ function lowerFind(entry, ids) {
|
|
|
1237
1251
|
case "countDistinct":
|
|
1238
1252
|
return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) };
|
|
1239
1253
|
case "fold": {
|
|
1240
|
-
if (
|
|
1241
|
-
return { kind: "
|
|
1254
|
+
if ("duration" in agg.over) {
|
|
1255
|
+
return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) };
|
|
1242
1256
|
}
|
|
1243
|
-
return { kind: "
|
|
1257
|
+
return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) };
|
|
1244
1258
|
}
|
|
1245
1259
|
case "arg":
|
|
1246
1260
|
return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) };
|
|
@@ -1263,7 +1277,7 @@ function headOpOf(agg) {
|
|
|
1263
1277
|
return "pack";
|
|
1264
1278
|
}
|
|
1265
1279
|
}
|
|
1266
|
-
/** One
|
|
1280
|
+
/** One find entry's var-free head shape. */
|
|
1267
1281
|
function headTermOf(column) {
|
|
1268
1282
|
const entry = column.entry;
|
|
1269
1283
|
if (entry.kind === "var" || entry.kind === "measure") {
|
|
@@ -1273,7 +1287,7 @@ function headTermOf(column) {
|
|
|
1273
1287
|
}
|
|
1274
1288
|
/** Lowers one rule: body walked in written order (var ids by first occurrence), finds last. */
|
|
1275
1289
|
function lowerRule(ctx, rule) {
|
|
1276
|
-
const ids =
|
|
1290
|
+
const ids = freshVarIds();
|
|
1277
1291
|
const atoms = [];
|
|
1278
1292
|
const negated = [];
|
|
1279
1293
|
const conditions = [];
|
|
@@ -1288,17 +1302,18 @@ function lowerRule(ctx, rule) {
|
|
|
1288
1302
|
break;
|
|
1289
1303
|
}
|
|
1290
1304
|
case "idb": {
|
|
1291
|
-
|
|
1305
|
+
const bucket = item.negated ? negated : atoms;
|
|
1306
|
+
bucket.push(lowerIdbAtom(ctx, item.rec, item.bindings, ids));
|
|
1292
1307
|
break;
|
|
1293
1308
|
}
|
|
1294
1309
|
case "cond": {
|
|
1295
|
-
conditions.push(lowerCondition(ctx,
|
|
1310
|
+
conditions.push(lowerCondition(ctx, item.cond, ids));
|
|
1296
1311
|
break;
|
|
1297
1312
|
}
|
|
1298
1313
|
}
|
|
1299
1314
|
}
|
|
1300
1315
|
return {
|
|
1301
|
-
finds: rule.
|
|
1316
|
+
finds: rule.finds.map(function findOf(column) {
|
|
1302
1317
|
return lowerFind(column.entry, ids);
|
|
1303
1318
|
}),
|
|
1304
1319
|
atoms,
|
|
@@ -1309,11 +1324,7 @@ function lowerRule(ctx, rule) {
|
|
|
1309
1324
|
/**
|
|
1310
1325
|
* Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
|
|
1311
1326
|
* 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.
|
|
1327
|
+
* appended last. Every registered param must carry a field anchor by now.
|
|
1317
1328
|
*/
|
|
1318
1329
|
function lowerQuery(q) {
|
|
1319
1330
|
const theory = q.schema;
|
|
@@ -1341,14 +1352,14 @@ function lowerQuery(q) {
|
|
|
1341
1352
|
throw errors.new(`query lowering: rec ${rec.name} has no rules`);
|
|
1342
1353
|
}
|
|
1343
1354
|
return {
|
|
1344
|
-
head: head.
|
|
1355
|
+
head: head.finds.map(headTermOf),
|
|
1345
1356
|
rules: rec.rules.map(function lowerRecRule(rule) {
|
|
1346
1357
|
return lowerRule(ctx, rule);
|
|
1347
1358
|
})
|
|
1348
1359
|
};
|
|
1349
1360
|
});
|
|
1350
1361
|
predicates.push({
|
|
1351
|
-
head: q.data.
|
|
1362
|
+
head: q.data.finds.map(headTermOf),
|
|
1352
1363
|
rules: q.data.rules.map(function lowerOutputRule(rule) {
|
|
1353
1364
|
return lowerRule(ctx, rule);
|
|
1354
1365
|
})
|