@bjornpagen/bumbledb 0.4.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 +191 -92
- package/README.md +9 -7
- package/dist/closed.d.ts +30 -2
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +66 -20
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +33 -6
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +89 -72
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts.map +1 -1
- package/dist/exhume.js +1 -14
- package/dist/exhume.js.map +1 -1
- package/dist/face.d.ts +1 -1
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +2 -1
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +18 -1
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +44 -16
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +18 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -6
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +2 -1
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +15 -14
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +1 -7
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +15 -27
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +21 -2
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +20 -3
- package/dist/native.js.map +1 -1
- package/dist/order.d.ts +36 -0
- package/dist/order.d.ts.map +1 -0
- package/dist/order.js +135 -0
- package/dist/order.js.map +1 -0
- package/dist/query/atom.d.ts +140 -206
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +33 -52
- 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 +124 -162
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +462 -507
- 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 +45 -20
- 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 +10 -11
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +124 -83
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +77 -70
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts +9 -22
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +9 -28
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +7 -31
- package/dist/schema.js.map +1 -1
- package/dist/statements.d.ts +5 -2
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +30 -35
- package/dist/statements.js.map +1 -1
- package/package.json +2 -5
- package/src/closed.ts +73 -28
- package/src/db.ts +126 -88
- package/src/exhume.ts +1 -15
- package/src/face.ts +4 -3
- package/src/fields.ts +58 -16
- package/src/index.ts +19 -13
- package/src/law.ts +15 -14
- package/src/lower.ts +2 -9
- package/src/marshal.ts +15 -31
- package/src/native.ts +22 -4
- package/src/order.ts +156 -0
- package/src/query/atom.ts +186 -276
- package/src/query/find.ts +212 -0
- package/src/query/lower.ts +614 -749
- package/src/query/predicate.ts +47 -20
- package/src/query/run.ts +11 -13
- package/src/query/scope.ts +165 -132
- package/src/relation.ts +9 -51
- package/src/schema.ts +7 -33
- package/src/statements.ts +33 -38
- 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
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/
|
|
37
|
+
import { sealedFieldsOf } from "#closed.ts";
|
|
38
|
+
import { assertDeclarationOrderKey, isIntervalValue, literalShapeError, rosterOf } from "#fields.ts";
|
|
39
|
+
import { allen, and, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts";
|
|
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";
|
|
32
42
|
/** The frozen constructor vocabulary every rule builder spreads. */
|
|
33
43
|
const termOps = Object.freeze({
|
|
34
|
-
var: makeVar,
|
|
35
|
-
vars: makeVars,
|
|
36
44
|
param: makeParam,
|
|
37
45
|
inSet: makeSetParam,
|
|
38
46
|
maskParam: makeMaskParam,
|
|
@@ -44,7 +52,6 @@ const termOps = Object.freeze({
|
|
|
44
52
|
gt,
|
|
45
53
|
ge,
|
|
46
54
|
pointIn,
|
|
47
|
-
covers,
|
|
48
55
|
allen,
|
|
49
56
|
and,
|
|
50
57
|
or,
|
|
@@ -61,41 +68,32 @@ const termOps = Object.freeze({
|
|
|
61
68
|
/** The empty rule state. */
|
|
62
69
|
const EMPTY_RULE = Object.freeze({
|
|
63
70
|
items: Object.freeze([]),
|
|
64
|
-
|
|
71
|
+
bound: new Set(),
|
|
65
72
|
paramUses: Object.freeze([])
|
|
66
73
|
});
|
|
67
74
|
/**
|
|
68
|
-
* The
|
|
69
|
-
*
|
|
70
|
-
* relation
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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.
|
|
75
82
|
*/
|
|
76
|
-
function
|
|
77
|
-
if (
|
|
78
|
-
|
|
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`);
|
|
79
86
|
}
|
|
80
|
-
return owner.
|
|
87
|
+
return { field: ref.field, class: context.classes[ref.owner.name]?.[ref.column] };
|
|
81
88
|
}
|
|
82
89
|
/**
|
|
83
90
|
* Judges one membership ARRAY at a binding position — legal exactly at a
|
|
84
|
-
* CLOSED-reference field
|
|
85
|
-
*
|
|
86
|
-
* vocabulary's spelling), holding ≥ 2 DISTINCT handle names (the
|
|
87
|
-
* degenerate sets are refusals: empty selects nothing, one element is the
|
|
88
|
-
* bare literal respelled, and a duplicate member is the same respelling in
|
|
89
|
-
* disguise — write each member once). The returned name is
|
|
90
|
-
* CONTENT-ADDRESSED (vocabulary + the member SET — the key sorts a copy,
|
|
91
|
-
* so two spellings of one set, reordered or not, share one dense
|
|
92
|
-
* `ParamId`); the members are shape-checked strings here and
|
|
93
|
-
* roster-verified at the one verification point (`taggedHandleId`) when
|
|
94
|
-
* the SDK supplies the set at execute — the same moment a bound `r.inSet`
|
|
95
|
-
* 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).
|
|
96
93
|
*/
|
|
97
94
|
function membershipSet(context, field, value) {
|
|
98
|
-
|
|
95
|
+
const roster = rosterOf(field);
|
|
96
|
+
if (roster === undefined) {
|
|
99
97
|
throw errors.new(`${context}: a membership array is the closed-reference spelling — ordinary field membership is a bound ∈-set param (r.inSet)`);
|
|
100
98
|
}
|
|
101
99
|
if (value.length === 0) {
|
|
@@ -107,7 +105,7 @@ function membershipSet(context, field, value) {
|
|
|
107
105
|
const seen = new Set();
|
|
108
106
|
const members = value.map(function memberName(member) {
|
|
109
107
|
if (typeof member !== "string") {
|
|
110
|
-
throw literalShapeError(context, `a ${
|
|
108
|
+
throw literalShapeError(context, `a ${roster.name} handle name (string)`, member);
|
|
111
109
|
}
|
|
112
110
|
if (seen.has(member)) {
|
|
113
111
|
throw errors.new(`${context}: the membership array spells ${member} twice — write it once (the canonical-utterance law: one meaning, one spelling)`);
|
|
@@ -116,23 +114,22 @@ function membershipSet(context, field, value) {
|
|
|
116
114
|
return member;
|
|
117
115
|
});
|
|
118
116
|
const key = [...members].sort();
|
|
119
|
-
return { name: `∈ ${
|
|
117
|
+
return { name: `∈ ${roster.name} ${JSON.stringify(key)}`, members: Object.freeze(members) };
|
|
120
118
|
}
|
|
121
119
|
/**
|
|
122
|
-
* Resolves a bindings record against an atom owner's matchable fields
|
|
123
|
-
* 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
|
|
124
121
|
* the record's written order: terms classify by their runtime tag,
|
|
125
|
-
* everything else is a bare literal
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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.
|
|
129
126
|
*/
|
|
130
|
-
function resolveBindings(context, relation, bindings
|
|
127
|
+
function resolveBindings(context, label, relation, bindings) {
|
|
131
128
|
const entries = [];
|
|
132
129
|
const vars = [];
|
|
133
130
|
const uses = [];
|
|
134
|
-
const relationClasses = classes[relation.name];
|
|
135
|
-
const ordered =
|
|
131
|
+
const relationClasses = context.classes[relation.name];
|
|
132
|
+
const ordered = sealedFieldsOf(relation);
|
|
136
133
|
for (const [fieldName, value] of Object.entries(bindings)) {
|
|
137
134
|
if (value === undefined) {
|
|
138
135
|
continue;
|
|
@@ -141,15 +138,21 @@ function resolveBindings(context, relation, bindings, classes) {
|
|
|
141
138
|
return candidate.name === fieldName;
|
|
142
139
|
});
|
|
143
140
|
if (declared === undefined) {
|
|
144
|
-
throw errors.new(`${
|
|
141
|
+
throw errors.new(`${label} has no field ${fieldName}`);
|
|
145
142
|
}
|
|
146
143
|
const fieldClass = relationClasses?.[fieldName];
|
|
147
144
|
let bound;
|
|
148
145
|
if (isTerm(value)) {
|
|
149
146
|
switch (value[term]) {
|
|
150
147
|
case "var": {
|
|
151
|
-
|
|
152
|
-
|
|
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);
|
|
153
156
|
break;
|
|
154
157
|
}
|
|
155
158
|
case "param": {
|
|
@@ -175,13 +178,13 @@ function resolveBindings(context, relation, bindings, classes) {
|
|
|
175
178
|
break;
|
|
176
179
|
}
|
|
177
180
|
case "maskParam":
|
|
178
|
-
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`);
|
|
179
182
|
case "duration":
|
|
180
|
-
throw errors.new(`${
|
|
183
|
+
throw errors.new(`${label}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and find entries`);
|
|
181
184
|
}
|
|
182
185
|
}
|
|
183
186
|
else if (Array.isArray(value)) {
|
|
184
|
-
const set = membershipSet(`${
|
|
187
|
+
const set = membershipSet(`${label}.${fieldName}`, declared.field, value);
|
|
185
188
|
bound = Object.freeze({ kind: "literalSet", name: set.name, members: set.members });
|
|
186
189
|
uses.push(Object.freeze({
|
|
187
190
|
name: set.name,
|
|
@@ -196,49 +199,33 @@ function resolveBindings(context, relation, bindings, classes) {
|
|
|
196
199
|
}
|
|
197
200
|
entries.push(Object.freeze({ field: fieldName, data: declared.field, class: fieldClass, term: bound }));
|
|
198
201
|
}
|
|
199
|
-
return {
|
|
200
|
-
atom: Object.freeze({ relation, bindings: Object.freeze(entries) }),
|
|
201
|
-
vars,
|
|
202
|
-
uses
|
|
203
|
-
};
|
|
202
|
+
return { atom: Object.freeze({ relation, bindings: Object.freeze(entries) }), vars, uses };
|
|
204
203
|
}
|
|
205
|
-
/**
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
*/
|
|
212
|
-
function advanceMatch(state, relation, bindings, classes) {
|
|
213
|
-
const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings, classes);
|
|
214
|
-
const varFields = { ...state.varFields };
|
|
215
|
-
for (const bound of resolved.vars) {
|
|
216
|
-
const existing = varFields[bound.name];
|
|
217
|
-
if (existing === undefined) {
|
|
218
|
-
varFields[bound.name] = bound.slot;
|
|
219
|
-
}
|
|
220
|
-
else if (!fieldJoins(existing, bound.slot)) {
|
|
221
|
-
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)`);
|
|
222
|
-
}
|
|
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);
|
|
223
210
|
}
|
|
224
|
-
return {
|
|
211
|
+
return Object.freeze({
|
|
225
212
|
items: Object.freeze([...state.items, Object.freeze({ kind: "atom", atom: resolved.atom })]),
|
|
226
|
-
|
|
213
|
+
bound,
|
|
227
214
|
paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
|
|
228
|
-
};
|
|
215
|
+
});
|
|
229
216
|
}
|
|
230
|
-
/** Resolves one comparison side to its runtime term. */
|
|
217
|
+
/** Resolves one comparison side to its runtime term (variables and the measure ride by reference). */
|
|
231
218
|
function cmpTermDataOf(op, value) {
|
|
232
219
|
if (isTerm(value)) {
|
|
233
220
|
switch (value[term]) {
|
|
234
221
|
case "var":
|
|
235
|
-
return Object.freeze({ kind: "var",
|
|
222
|
+
return Object.freeze({ kind: "var", ref: value });
|
|
236
223
|
case "param":
|
|
237
224
|
return Object.freeze({ kind: "param", name: value.name });
|
|
238
225
|
case "setParam":
|
|
239
226
|
return Object.freeze({ kind: "setParam", name: value.name });
|
|
240
227
|
case "duration":
|
|
241
|
-
return Object.freeze({ kind: "measure",
|
|
228
|
+
return Object.freeze({ kind: "measure", ref: value.over });
|
|
242
229
|
case "maskParam":
|
|
243
230
|
throw errors.new(`${op}: an Allen-mask param is not a comparison term — masks live in allen()'s mask position`);
|
|
244
231
|
}
|
|
@@ -247,17 +234,16 @@ function cmpTermDataOf(op, value) {
|
|
|
247
234
|
}
|
|
248
235
|
/**
|
|
249
236
|
* One comparison side's contribution to the param census: a param/set side
|
|
250
|
-
* anchors to its SIBLING — a
|
|
251
|
-
*
|
|
252
|
-
* 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.
|
|
253
239
|
*/
|
|
254
|
-
function sideUses(op, side, sibling,
|
|
240
|
+
function sideUses(op, side, sibling, uses) {
|
|
255
241
|
if (side.kind !== "param" && side.kind !== "setParam") {
|
|
256
242
|
return;
|
|
257
243
|
}
|
|
258
244
|
let anchor;
|
|
259
245
|
if (sibling.kind === "var") {
|
|
260
|
-
anchor =
|
|
246
|
+
anchor = sibling.ref.field;
|
|
261
247
|
}
|
|
262
248
|
else if (sibling.kind === "measure") {
|
|
263
249
|
anchor = "measure";
|
|
@@ -274,12 +260,12 @@ function sideUses(op, side, sibling, varFields, uses) {
|
|
|
274
260
|
}));
|
|
275
261
|
}
|
|
276
262
|
/** Lowers one condition VALUE to its runtime data, recording param uses. */
|
|
277
|
-
function condDataOf(cond,
|
|
263
|
+
function condDataOf(cond, uses) {
|
|
278
264
|
if (cond.cond === "cmp") {
|
|
279
265
|
const lhs = cmpTermDataOf(cond.op, cond.lhs);
|
|
280
266
|
const rhs = cmpTermDataOf(cond.op, cond.rhs);
|
|
281
|
-
sideUses(cond.op, lhs, rhs,
|
|
282
|
-
sideUses(cond.op, rhs, lhs,
|
|
267
|
+
sideUses(cond.op, lhs, rhs, uses);
|
|
268
|
+
sideUses(cond.op, rhs, lhs, uses);
|
|
283
269
|
let mask;
|
|
284
270
|
if (cond.op === "allen") {
|
|
285
271
|
const maskValue = cond.mask;
|
|
@@ -300,20 +286,18 @@ function condDataOf(cond, varFields, uses) {
|
|
|
300
286
|
throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam");
|
|
301
287
|
}
|
|
302
288
|
}
|
|
303
|
-
|
|
304
|
-
return data;
|
|
289
|
+
return Object.freeze({ kind: "cmp", op: cond.op, mask, lhs, rhs });
|
|
305
290
|
}
|
|
306
291
|
if (cond.cond === "tree") {
|
|
307
292
|
const children = cond.children.map(function lowerChild(child) {
|
|
308
|
-
return condDataOf(child,
|
|
293
|
+
return condDataOf(child, uses);
|
|
309
294
|
});
|
|
310
|
-
|
|
311
|
-
return data;
|
|
295
|
+
return Object.freeze({ kind: "tree", op: cond.op, children: Object.freeze(children) });
|
|
312
296
|
}
|
|
313
297
|
throw errors.new("a negated atom is not a condition-tree node — pass not(...) to where() directly, never inside and()/or()");
|
|
314
298
|
}
|
|
315
299
|
/** Extends a rule state with one `.where` item (a condition or a negated atom). */
|
|
316
|
-
function advanceWhere(state, cond
|
|
300
|
+
function advanceWhere(context, state, cond) {
|
|
317
301
|
if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
|
|
318
302
|
throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom");
|
|
319
303
|
}
|
|
@@ -322,190 +306,177 @@ function advanceWhere(state, cond, classes) {
|
|
|
322
306
|
const bindings = Object.fromEntries(Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
|
|
323
307
|
return value !== undefined;
|
|
324
308
|
}));
|
|
325
|
-
const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings
|
|
326
|
-
return {
|
|
309
|
+
const resolved = resolveBindings(context, `negated relation ${relation.name}`, relation, bindings);
|
|
310
|
+
return Object.freeze({
|
|
327
311
|
items: Object.freeze([...state.items, Object.freeze({ kind: "negated", atom: resolved.atom })]),
|
|
328
|
-
|
|
312
|
+
bound: state.bound,
|
|
329
313
|
paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
|
|
330
|
-
};
|
|
314
|
+
});
|
|
331
315
|
}
|
|
332
316
|
const uses = [];
|
|
333
|
-
const data = condDataOf(cond,
|
|
334
|
-
return {
|
|
317
|
+
const data = condDataOf(cond, uses);
|
|
318
|
+
return Object.freeze({
|
|
335
319
|
items: Object.freeze([...state.items, Object.freeze({ kind: "cond", cond: data })]),
|
|
336
|
-
|
|
320
|
+
bound: state.bound,
|
|
337
321
|
paramUses: Object.freeze([...state.paramUses, ...uses])
|
|
338
|
-
};
|
|
322
|
+
});
|
|
339
323
|
}
|
|
340
|
-
/** Extends a rule state with one `idb` atom (
|
|
341
|
-
function advanceIdb(state, rec,
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
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;
|
|
345
330
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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,
|
|
351
342
|
paramUses: state.paramUses
|
|
352
|
-
};
|
|
343
|
+
});
|
|
353
344
|
}
|
|
354
|
-
/** Narrows a
|
|
345
|
+
/** Narrows a find entry to an aggregate value. */
|
|
355
346
|
function isAggregateEntry(value) {
|
|
356
347
|
return typeof value === "object" && value !== null && "agg" in value;
|
|
357
348
|
}
|
|
358
|
-
/**
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
* bare.
|
|
363
|
-
*/
|
|
364
|
-
function selectColumnOf(entry) {
|
|
365
|
-
if (typeof entry === "string") {
|
|
366
|
-
return Object.freeze({
|
|
367
|
-
name: entry,
|
|
368
|
-
entry: Object.freeze({ kind: "var", over: entry }),
|
|
369
|
-
closed: undefined
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
if (isTerm(entry)) {
|
|
373
|
-
if (entry[term] === "duration") {
|
|
374
|
-
return Object.freeze({
|
|
375
|
-
name: entry.name,
|
|
376
|
-
entry: Object.freeze({ kind: "measure", over: entry.name }),
|
|
377
|
-
closed: undefined
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
throw errors.new(`query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`);
|
|
381
|
-
}
|
|
382
|
-
if (isAggregateEntry(entry)) {
|
|
383
|
-
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;
|
|
384
353
|
}
|
|
385
|
-
throw errors.new(
|
|
354
|
+
throw errors.new(`${context}: expected a variable`);
|
|
386
355
|
}
|
|
387
|
-
/** Classifies one aggregate
|
|
388
|
-
function
|
|
389
|
-
function column(name, agg) {
|
|
390
|
-
return Object.freeze({
|
|
391
|
-
name,
|
|
392
|
-
entry: Object.freeze({ kind: "aggregate", agg: Object.freeze(agg) }),
|
|
393
|
-
closed: undefined
|
|
394
|
-
});
|
|
395
|
-
}
|
|
356
|
+
/** Classifies one aggregate find entry into its runtime data (variables ride by reference). */
|
|
357
|
+
function aggDataOf(name, entry) {
|
|
396
358
|
const over = entry.over;
|
|
397
359
|
switch (entry.agg) {
|
|
398
360
|
case "count":
|
|
399
|
-
return
|
|
400
|
-
case "countDistinct":
|
|
401
|
-
|
|
402
|
-
throw errors.new("countDistinct takes a variable name");
|
|
403
|
-
}
|
|
404
|
-
return column(over, { op: "countDistinct", over });
|
|
405
|
-
}
|
|
361
|
+
return Object.freeze({ op: "count" });
|
|
362
|
+
case "countDistinct":
|
|
363
|
+
return Object.freeze({ op: "countDistinct", over: asVarTerm(`find ${name} (countDistinct)`, over) });
|
|
406
364
|
case "sum":
|
|
407
365
|
case "min":
|
|
408
366
|
case "max": {
|
|
409
|
-
if (
|
|
410
|
-
return
|
|
367
|
+
if (isTerm(over) && over[term] === "var") {
|
|
368
|
+
return Object.freeze({ op: "fold", fold: entry.agg, over });
|
|
411
369
|
}
|
|
412
370
|
if (isTerm(over) && over[term] === "duration") {
|
|
413
|
-
return
|
|
371
|
+
return Object.freeze({ op: "fold", fold: entry.agg, over: Object.freeze({ duration: over.over }) });
|
|
414
372
|
}
|
|
415
|
-
throw errors.new(
|
|
373
|
+
throw errors.new(`find ${name} (${entry.agg}): takes a variable or r.duration(v)`);
|
|
416
374
|
}
|
|
417
375
|
case "argMax":
|
|
418
|
-
case "argMin":
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
+
});
|
|
423
403
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
+
});
|
|
429
411
|
}
|
|
430
|
-
|
|
431
|
-
|
|
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
|
+
});
|
|
432
421
|
}
|
|
422
|
+
throw errors.new(`find ${name}: not a find entry — find takes variables, r.duration(v), or aggregates`);
|
|
433
423
|
}
|
|
434
424
|
/**
|
|
435
425
|
* The orderable ban's pointed refusal (`docs/architecture/10-data-model.md`
|
|
436
|
-
* § orderability): a closed reference is equality-and-membership only
|
|
437
|
-
* its declaration-id order is an encoding accident, so every
|
|
438
|
-
* order-comparison and fold position refuses it. The construction-time
|
|
439
|
-
* twin of the type tier's `OrderVarOk` exclusion, so the wall holds for
|
|
440
|
-
* untyped callers too (the engine cannot backstop this one: the wire IR
|
|
441
|
-
* carries plain u64s, no rosters).
|
|
426
|
+
* § orderability): a closed reference is equality-and-membership only.
|
|
442
427
|
*/
|
|
443
428
|
function closedOrderError(context, position, vocabulary) {
|
|
444
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)`);
|
|
445
430
|
}
|
|
446
|
-
/** The comparison ops the orderable ban
|
|
431
|
+
/** The comparison ops under the orderable ban (order roster + point membership). */
|
|
447
432
|
function isOrderOp(op) {
|
|
448
433
|
return op === "lt" || op === "le" || op === "gt" || op === "ge" || op === "pointIn";
|
|
449
434
|
}
|
|
450
|
-
/** Requires a
|
|
451
|
-
function assertBound(
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
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`);
|
|
455
439
|
}
|
|
456
|
-
return slot;
|
|
457
440
|
}
|
|
458
|
-
/** Requires a
|
|
459
|
-
function
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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);
|
|
463
452
|
}
|
|
464
453
|
}
|
|
465
454
|
/**
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
* match-reuse join must be (the construction-time twin of the type tier's
|
|
470
|
-
* `EqOk` → `JoinOk`; bare pairs only with bare). The engine cannot backstop
|
|
471
|
-
* this one — the query IR carries no domains — so the wall lives here for
|
|
472
|
-
* 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.
|
|
473
458
|
*/
|
|
474
|
-
function
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const slot = assertBound(context, varFields, side.name);
|
|
479
|
-
if (isOrderOp(cond.op) && "closed" in slot.field) {
|
|
480
|
-
throw closedOrderError(context, `the ${cond.op} side ${side.name}`, slot.field.closed.name);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
if (side.kind === "measure") {
|
|
484
|
-
assertIntervalBound(context, varFields, side.name);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
|
|
488
|
-
const lhs = assertBound(context, varFields, cond.lhs.name);
|
|
489
|
-
const rhs = assertBound(context, varFields, cond.rhs.name);
|
|
490
|
-
if (!fieldJoins(lhs, rhs)) {
|
|
491
|
-
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)`);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
return;
|
|
459
|
+
function findColumnSlotOf(context, column) {
|
|
460
|
+
const entry = column.entry;
|
|
461
|
+
if (entry.kind === "var") {
|
|
462
|
+
return mintSlotOf(context, entry.over);
|
|
495
463
|
}
|
|
496
|
-
|
|
497
|
-
|
|
464
|
+
if (entry.kind === "aggregate" && entry.agg.op === "arg") {
|
|
465
|
+
return mintSlotOf(context, entry.agg.over);
|
|
498
466
|
}
|
|
467
|
+
return undefined;
|
|
499
468
|
}
|
|
500
|
-
/** Validates one
|
|
501
|
-
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}`;
|
|
502
472
|
const entry = column.entry;
|
|
503
473
|
if (entry.kind === "var") {
|
|
504
|
-
assertBound(
|
|
474
|
+
assertBound(where, bound, entry.over);
|
|
505
475
|
return;
|
|
506
476
|
}
|
|
507
477
|
if (entry.kind === "measure") {
|
|
508
|
-
|
|
478
|
+
assertBound(where, bound, entry.over);
|
|
479
|
+
assertInterval(where, entry.over);
|
|
509
480
|
return;
|
|
510
481
|
}
|
|
511
482
|
const agg = entry.agg;
|
|
@@ -513,130 +484,143 @@ function validateColumn(context, varFields, column) {
|
|
|
513
484
|
case "count":
|
|
514
485
|
return;
|
|
515
486
|
case "countDistinct":
|
|
516
|
-
assertBound(
|
|
487
|
+
assertBound(where, bound, agg.over);
|
|
517
488
|
return;
|
|
518
489
|
case "fold": {
|
|
519
|
-
if (
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
throw closedOrderError(`${context} select ${column.name}`, `the ${agg.fold} input ${agg.over}`, slot.field.closed.name);
|
|
523
|
-
}
|
|
490
|
+
if ("duration" in agg.over) {
|
|
491
|
+
assertBound(where, bound, agg.over.duration);
|
|
492
|
+
assertInterval(where, agg.over.duration);
|
|
524
493
|
return;
|
|
525
494
|
}
|
|
526
|
-
|
|
495
|
+
assertBound(where, bound, agg.over);
|
|
496
|
+
assertNotClosed(where, `the ${agg.fold} input`, agg.over);
|
|
527
497
|
return;
|
|
528
498
|
}
|
|
529
499
|
case "arg": {
|
|
530
|
-
assertBound(
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
throw closedOrderError(`${context} select ${column.name}`, `the ${agg.direction} key ${agg.key}`, key.field.closed.name);
|
|
534
|
-
}
|
|
500
|
+
assertBound(where, bound, agg.over);
|
|
501
|
+
assertBound(where, bound, agg.key);
|
|
502
|
+
assertNotClosed(where, `the ${agg.direction} key`, agg.key);
|
|
535
503
|
return;
|
|
536
504
|
}
|
|
537
505
|
case "pack":
|
|
538
|
-
|
|
506
|
+
assertBound(where, bound, agg.over);
|
|
507
|
+
assertInterval(where, agg.over);
|
|
539
508
|
return;
|
|
540
509
|
}
|
|
541
510
|
}
|
|
542
511
|
/**
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
* machinery reads), and `decodeAnswers` lifts the column's row ids back to
|
|
547
|
-
* handle NAMES through it — the runtime twin of the row type's `Infer`
|
|
548
|
-
* claim. Every other entry decodes bare: counts are counts, the measure
|
|
549
|
-
* and `pack` are never closed, and a closed FOLD is banned outright
|
|
550
|
-
* ({@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).
|
|
551
515
|
*/
|
|
552
|
-
function
|
|
553
|
-
|
|
554
|
-
if (
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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;
|
|
565
542
|
}
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
return field.closed;
|
|
543
|
+
for (const child of cond.children) {
|
|
544
|
+
validateCond(context, bound, child);
|
|
569
545
|
}
|
|
570
|
-
return undefined;
|
|
571
546
|
}
|
|
572
547
|
/**
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
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.
|
|
578
554
|
*/
|
|
579
|
-
function
|
|
580
|
-
|
|
581
|
-
|
|
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
|
+
}
|
|
582
569
|
}
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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}`);
|
|
573
|
+
}
|
|
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
588
|
}
|
|
589
|
-
seen.add(column.name);
|
|
590
|
-
validateColumn(context, state.varFields, column);
|
|
591
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
|
+
});
|
|
592
608
|
for (const item of state.items) {
|
|
593
609
|
if (item.kind === "negated") {
|
|
594
610
|
for (const binding of item.atom.bindings) {
|
|
595
|
-
if (binding.term.kind === "var") {
|
|
596
|
-
|
|
597
|
-
if (bound === undefined) {
|
|
598
|
-
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)`);
|
|
599
|
-
}
|
|
600
|
-
const negatedSlot = { field: binding.data, class: binding.class };
|
|
601
|
-
if (!fieldJoins(bound, negatedSlot)) {
|
|
602
|
-
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`);
|
|
603
|
-
}
|
|
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)`);
|
|
604
613
|
}
|
|
605
614
|
}
|
|
606
615
|
}
|
|
607
616
|
if (item.kind === "idb") {
|
|
608
|
-
|
|
609
|
-
item.vars.forEach(function checkIdbVar(name, position) {
|
|
610
|
-
const bound = state.varFields[name];
|
|
611
|
-
if (bound === undefined) {
|
|
612
|
-
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`);
|
|
613
|
-
}
|
|
614
|
-
const column = head?.select[position];
|
|
615
|
-
if (column === undefined || column.entry.kind !== "var") {
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
const headSlot = head?.varFields[column.entry.over];
|
|
619
|
-
if (headSlot !== undefined && !fieldJoins(headSlot, bound)) {
|
|
620
|
-
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`);
|
|
621
|
-
}
|
|
622
|
-
});
|
|
617
|
+
validateIdb(context, state.bound, item, columns);
|
|
623
618
|
}
|
|
624
619
|
if (item.kind === "cond") {
|
|
625
|
-
validateCond(context, state.
|
|
620
|
+
validateCond(context, state.bound, item.cond);
|
|
626
621
|
}
|
|
627
622
|
}
|
|
628
|
-
return Object.freeze({
|
|
629
|
-
items: state.items,
|
|
630
|
-
select: Object.freeze(columns.map(function enrichColumn(column) {
|
|
631
|
-
return Object.freeze({
|
|
632
|
-
name: column.name,
|
|
633
|
-
entry: column.entry,
|
|
634
|
-
closed: selectClosedOf(state.varFields, column.entry)
|
|
635
|
-
});
|
|
636
|
-
})),
|
|
637
|
-
varFields: state.varFields,
|
|
638
|
-
paramUses: state.paramUses
|
|
639
|
-
});
|
|
623
|
+
return Object.freeze({ items: state.items, finds: Object.freeze(columns), paramUses: state.paramUses });
|
|
640
624
|
}
|
|
641
625
|
/** Builds one typed rule value over completed rule data. */
|
|
642
626
|
function makeRuleValue(rule) {
|
|
@@ -654,7 +638,7 @@ function contextLabel(context) {
|
|
|
654
638
|
}
|
|
655
639
|
}
|
|
656
640
|
/** Validates and records one `idb` atom per the context's cut. */
|
|
657
|
-
function idbAdvance(context, state, target,
|
|
641
|
+
function idbAdvance(context, state, target, bindings) {
|
|
658
642
|
if (context.kind === "query") {
|
|
659
643
|
throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()");
|
|
660
644
|
}
|
|
@@ -662,36 +646,41 @@ function idbAdvance(context, state, target, vars) {
|
|
|
662
646
|
if (target.data !== context.self) {
|
|
663
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)`);
|
|
664
648
|
}
|
|
665
|
-
return advanceIdb(state, context.self,
|
|
649
|
+
return advanceIdb(state, context.self, bindings);
|
|
666
650
|
}
|
|
667
651
|
if (!context.program.recs.includes(target.data)) {
|
|
668
652
|
throw errors.new(`idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`);
|
|
669
653
|
}
|
|
670
|
-
return advanceIdb(state, target.data,
|
|
654
|
+
return advanceIdb(state, target.data, bindings);
|
|
671
655
|
}
|
|
672
|
-
/** Classifies one
|
|
673
|
-
function
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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;
|
|
677
662
|
}
|
|
678
|
-
|
|
679
|
-
|
|
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;
|
|
680
669
|
}
|
|
681
670
|
/** Builds one runtime chain (immutably — every step is a fresh chain over fresh state). */
|
|
682
671
|
function makeRawChain(context, state) {
|
|
683
672
|
const chain = {
|
|
684
673
|
match(relation, bindings) {
|
|
685
|
-
return makeRawChain(context, advanceMatch(state, relation, bindings
|
|
674
|
+
return makeRawChain(context, advanceMatch(context, state, relation, bindings));
|
|
686
675
|
},
|
|
687
676
|
where(cond) {
|
|
688
|
-
return makeRawChain(context, advanceWhere(state, cond
|
|
677
|
+
return makeRawChain(context, advanceWhere(context, state, cond));
|
|
689
678
|
},
|
|
690
|
-
idb(target,
|
|
691
|
-
return makeRawChain(context, idbAdvance(context, state, target,
|
|
679
|
+
idb(target, bindings) {
|
|
680
|
+
return makeRawChain(context, idbAdvance(context, state, target, bindings));
|
|
692
681
|
},
|
|
693
|
-
|
|
694
|
-
return makeRuleValue(completeRule(
|
|
682
|
+
find(entries) {
|
|
683
|
+
return makeRuleValue(completeRule(context, state, findColumns(context, entries)));
|
|
695
684
|
}
|
|
696
685
|
};
|
|
697
686
|
Object.freeze(chain);
|
|
@@ -702,27 +691,27 @@ function makeRawScope(context) {
|
|
|
702
691
|
const scope = {
|
|
703
692
|
...termOps,
|
|
704
693
|
match(relation, bindings) {
|
|
705
|
-
return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings
|
|
694
|
+
return makeRawChain(context, advanceMatch(context, EMPTY_RULE, relation, bindings));
|
|
706
695
|
}
|
|
707
696
|
};
|
|
708
697
|
Object.freeze(scope);
|
|
709
698
|
return scope;
|
|
710
699
|
}
|
|
711
700
|
/**
|
|
712
|
-
* The
|
|
713
|
-
*
|
|
714
|
-
* context, and this guard verifies the
|
|
715
|
-
* exist — before the value is admitted at
|
|
716
|
-
* judgments (
|
|
717
|
-
*
|
|
718
|
-
*
|
|
701
|
+
* The rule builders' trusted admission seam — THE home of the
|
|
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).
|
|
719
708
|
*/
|
|
720
709
|
function isTypedScope(scope) {
|
|
721
710
|
return typeof scope.match === "function";
|
|
722
711
|
}
|
|
723
712
|
/** Builds one query-rule builder (the typed face of the raw builder). */
|
|
724
|
-
function makeQueryRuleScope(
|
|
725
|
-
const raw = makeRawScope({ kind: "query", classes });
|
|
713
|
+
function makeQueryRuleScope(theory) {
|
|
714
|
+
const raw = makeRawScope({ kind: "query", classes: theory.classes, theory });
|
|
726
715
|
if (!isTypedScope(raw)) {
|
|
727
716
|
throw errors.new("query rule builder construction incomplete");
|
|
728
717
|
}
|
|
@@ -730,7 +719,7 @@ function makeQueryRuleScope(classes) {
|
|
|
730
719
|
}
|
|
731
720
|
/** Builds one output-rule builder over a program's recs. */
|
|
732
721
|
function makeOutputRuleScope(program) {
|
|
733
|
-
const raw = makeRawScope({ kind: "output", program, classes: program.classes });
|
|
722
|
+
const raw = makeRawScope({ kind: "output", program, classes: program.classes, theory: program.theory });
|
|
734
723
|
if (!isTypedScope(raw)) {
|
|
735
724
|
throw errors.new("program output rule builder construction incomplete");
|
|
736
725
|
}
|
|
@@ -757,13 +746,7 @@ function headSignature(column) {
|
|
|
757
746
|
}
|
|
758
747
|
/** The roster a param anchor carries: present exactly on a closed-reference field anchor. */
|
|
759
748
|
function anchorRosterOf(anchor) {
|
|
760
|
-
|
|
761
|
-
return undefined;
|
|
762
|
-
}
|
|
763
|
-
if ("closed" in anchor) {
|
|
764
|
-
return anchor.closed;
|
|
765
|
-
}
|
|
766
|
-
return undefined;
|
|
749
|
+
return anchor === "measure" ? undefined : rosterOf(anchor);
|
|
767
750
|
}
|
|
768
751
|
/** Renders one param anchor's closedness for the registry's coherence diagnostics. */
|
|
769
752
|
function renderParamAnchor(roster) {
|
|
@@ -771,20 +754,9 @@ function renderParamAnchor(roster) {
|
|
|
771
754
|
}
|
|
772
755
|
/**
|
|
773
756
|
* Folds every rule's param uses (recs in declaration order first, output
|
|
774
|
-
* rules last — exactly the lowering walk) into the query's registry:
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
* anchored use of one name must agree on the roster (value identity), so a
|
|
778
|
-
* param anchored at a closed reference is GUARANTEED to ride the one
|
|
779
|
-
* roster-verification point (`taggedHandleId`) at execute; a name anchored
|
|
780
|
-
* both at a closed reference and at a non-closed position (or at two
|
|
781
|
-
* vocabularies) is refused here, because the wire would translate only the
|
|
782
|
-
* first anchor's reading (the type tier intersects the uses to `never`;
|
|
783
|
-
* this is its runtime twin for untyped callers). A param whose anchor is a
|
|
784
|
-
* CLOSED reference must never sit in an order-comparison position — the
|
|
785
|
-
* anchor types its value a handle name and the engine would order the
|
|
786
|
-
* translated row ids, so the pairing is refused here too (the registry is
|
|
787
|
-
* 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.
|
|
788
760
|
*/
|
|
789
761
|
function paramRegistryOf(recs, rules) {
|
|
790
762
|
const order = [];
|
|
@@ -838,11 +810,9 @@ function paramRegistryOf(recs, rules) {
|
|
|
838
810
|
if (entry === undefined) {
|
|
839
811
|
throw errors.new(`query param ${name} lost its registry entry`);
|
|
840
812
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
entry.
|
|
844
|
-
"closed" in entry.anchor) {
|
|
845
|
-
throw closedOrderError(`query param ${name}`, `its ${entry.orderOp} use's anchor`, entry.anchor.closed.name);
|
|
813
|
+
const anchorRoster = anchorRosterOf(entry.anchor);
|
|
814
|
+
if (entry.orderOp !== undefined && anchorRoster !== undefined) {
|
|
815
|
+
throw closedOrderError(`query param ${name}`, `its ${entry.orderOp} use's anchor`, anchorRoster.name);
|
|
846
816
|
}
|
|
847
817
|
return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op, members: entry.members });
|
|
848
818
|
}));
|
|
@@ -850,43 +820,52 @@ function paramRegistryOf(recs, rules) {
|
|
|
850
820
|
/**
|
|
851
821
|
* Assembles the runtime query value over completed rules: every rule must
|
|
852
822
|
* derive the SAME head (name and aggregate shape, position for position —
|
|
853
|
-
* the decode labels and the engine's alignment rule agree
|
|
854
|
-
*
|
|
823
|
+
* the decode labels and the engine's alignment rule agree), and the param
|
|
824
|
+
* registry folds in program-walk order.
|
|
855
825
|
*/
|
|
856
826
|
function makeRawQuery(theory, recs, rules) {
|
|
857
827
|
const first = rules[0];
|
|
858
828
|
if (first === undefined) {
|
|
859
829
|
throw errors.new("a query needs at least one rule");
|
|
860
830
|
}
|
|
861
|
-
const signature = first.
|
|
831
|
+
const signature = first.finds.map(headSignature).join(", ");
|
|
862
832
|
rules.forEach(function verifyHead(rule, index) {
|
|
863
|
-
const candidate = rule.
|
|
833
|
+
const candidate = rule.finds.map(headSignature).join(", ");
|
|
864
834
|
if (candidate !== signature) {
|
|
865
|
-
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})`);
|
|
866
836
|
}
|
|
867
837
|
// The closed slice is part of the head too: one answer column decodes
|
|
868
|
-
// through one roster, so a union whose rules bind a column at
|
|
869
|
-
//
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
rule.select.forEach(function verifyClosedSlice(column, position) {
|
|
873
|
-
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];
|
|
874
842
|
if (lead !== undefined && column.closed !== lead.closed) {
|
|
875
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)`);
|
|
876
844
|
}
|
|
845
|
+
// The law-class wall on the union head: one answer column is one
|
|
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).
|
|
850
|
+
if (lead === undefined) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
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)`);
|
|
855
|
+
}
|
|
877
856
|
});
|
|
878
857
|
});
|
|
879
858
|
const data = Object.freeze({
|
|
880
859
|
recs: Object.freeze([...recs]),
|
|
881
860
|
rules: Object.freeze([...rules]),
|
|
882
|
-
|
|
861
|
+
finds: first.finds,
|
|
883
862
|
params: paramRegistryOf(recs, rules)
|
|
884
863
|
});
|
|
885
864
|
const value = {
|
|
886
865
|
schema: theory,
|
|
887
866
|
data,
|
|
888
867
|
rule(build) {
|
|
889
|
-
const built = build(makeRawScope({ kind: "query", classes: theory.classes }));
|
|
868
|
+
const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory }));
|
|
890
869
|
return makeRawQuery(theory, recs, [...rules, built.rule]);
|
|
891
870
|
}
|
|
892
871
|
};
|
|
@@ -894,7 +873,7 @@ function makeRawQuery(theory, recs, rules) {
|
|
|
894
873
|
return value;
|
|
895
874
|
}
|
|
896
875
|
/**
|
|
897
|
-
* The query values' trusted admission seam (the
|
|
876
|
+
* The query values' trusted admission seam (the {@link isTypedScope} pattern):
|
|
898
877
|
* the checkable fact — the value was assembled over the identical theory —
|
|
899
878
|
* is verified before the raw value is admitted at its typed face.
|
|
900
879
|
*/
|
|
@@ -910,44 +889,25 @@ function makeQuery(theory, recs, rules) {
|
|
|
910
889
|
return raw;
|
|
911
890
|
}
|
|
912
891
|
/**
|
|
913
|
-
* Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule`
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
* 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.
|
|
918
896
|
*/
|
|
919
897
|
function query(theory) {
|
|
920
898
|
const start = {
|
|
921
899
|
rule(build) {
|
|
922
|
-
const built = build(makeQueryRuleScope(theory
|
|
900
|
+
const built = build(makeQueryRuleScope(theory));
|
|
923
901
|
return makeQuery(theory, [], [built.rule]);
|
|
924
902
|
}
|
|
925
903
|
};
|
|
926
904
|
Object.freeze(start);
|
|
927
905
|
return start;
|
|
928
906
|
}
|
|
929
|
-
/** The typed shape refusal of the literal tagger — a genuine failure, never data. */
|
|
930
|
-
function literalShapeError(context, expected, value) {
|
|
931
|
-
return errors.new(`${context}: expected ${expected}, got ${typeof value}`);
|
|
932
|
-
}
|
|
933
|
-
/** Narrows an interval-shaped literal (a plain `{ start, end }` bigint pair). */
|
|
934
|
-
function isIntervalShaped(value) {
|
|
935
|
-
return (typeof value === "object" &&
|
|
936
|
-
value !== null &&
|
|
937
|
-
"start" in value &&
|
|
938
|
-
"end" in value &&
|
|
939
|
-
typeof value.start === "bigint" &&
|
|
940
|
-
typeof value.end === "bigint");
|
|
941
|
-
}
|
|
942
907
|
/**
|
|
943
908
|
* Tags one closed-reference literal: the handle NAME, verified against the
|
|
944
|
-
* roster
|
|
945
|
-
*
|
|
946
|
-
* declaration-order row id, tagged u64 — queries cross ids, never handle
|
|
947
|
-
* names; the wire is untouched. THE single roster-verification point of
|
|
948
|
-
* the query surface: atom-binding literals, comparison literals,
|
|
949
|
-
* execute-time params, and membership-array members all reach it (never
|
|
950
|
-
* 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.
|
|
951
911
|
*/
|
|
952
912
|
function taggedHandleId(context, closed, value) {
|
|
953
913
|
if (typeof value !== "string") {
|
|
@@ -971,7 +931,7 @@ function taggedAtElementDomain(context, element, value) {
|
|
|
971
931
|
}
|
|
972
932
|
return { kind: "i64", value };
|
|
973
933
|
}
|
|
974
|
-
if (
|
|
934
|
+
if (isIntervalValue(value)) {
|
|
975
935
|
if (element === "u64") {
|
|
976
936
|
return { kind: "intervalU64", start: value.start, end: value.end };
|
|
977
937
|
}
|
|
@@ -981,15 +941,12 @@ function taggedAtElementDomain(context, element, value) {
|
|
|
981
941
|
}
|
|
982
942
|
/**
|
|
983
943
|
* Tags one host literal at a FIELD position (atom bindings): the field's
|
|
984
|
-
* structural kind directs the tag, never a guess.
|
|
985
|
-
* bigint literal tags as the ELEMENT type — the IR's membership typing
|
|
986
|
-
* rule (point membership), an interval-shaped literal as the interval
|
|
987
|
-
* (value equality). A closed-reference literal is its bare handle id,
|
|
988
|
-
* tagged u64 after a roster verification.
|
|
944
|
+
* structural kind directs the tag, never a guess.
|
|
989
945
|
*/
|
|
990
946
|
function taggedLiteral(context, field, value) {
|
|
991
|
-
|
|
992
|
-
|
|
947
|
+
const roster = rosterOf(field);
|
|
948
|
+
if (roster !== undefined) {
|
|
949
|
+
return taggedHandleId(context, roster, value);
|
|
993
950
|
}
|
|
994
951
|
switch (field.kind) {
|
|
995
952
|
case "bool": {
|
|
@@ -1014,6 +971,9 @@ function taggedLiteral(context, field, value) {
|
|
|
1014
971
|
if (typeof value !== "string") {
|
|
1015
972
|
throw literalShapeError(context, "string", value);
|
|
1016
973
|
}
|
|
974
|
+
if (!value.isWellFormed()) {
|
|
975
|
+
throw literalShapeError(context, "well-formed string", value);
|
|
976
|
+
}
|
|
1017
977
|
return { kind: "string", value };
|
|
1018
978
|
}
|
|
1019
979
|
case "bytes": {
|
|
@@ -1027,17 +987,13 @@ function taggedLiteral(context, field, value) {
|
|
|
1027
987
|
}
|
|
1028
988
|
}
|
|
1029
989
|
/**
|
|
1030
|
-
* Tags one host literal at a COMPARISON or PARAM position, where the
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
* `
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
* `covers(span(...), t)` and tags as the interval of the sibling's
|
|
1038
|
-
* element domain; under every other operator an interval shape against a
|
|
1039
|
-
* scalar sibling stays refused (the engine's IllegalComparison — the
|
|
1040
|
-
* 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.
|
|
1041
997
|
*/
|
|
1042
998
|
function taggedCmpLiteral(context, sibling, value, op) {
|
|
1043
999
|
if (sibling === "measure") {
|
|
@@ -1046,28 +1002,28 @@ function taggedCmpLiteral(context, sibling, value, op) {
|
|
|
1046
1002
|
}
|
|
1047
1003
|
return { kind: "u64", value };
|
|
1048
1004
|
}
|
|
1049
|
-
if (
|
|
1005
|
+
if (rosterOf(sibling) === undefined && sibling.kind === "interval") {
|
|
1050
1006
|
return taggedAtElementDomain(context, sibling.element, value);
|
|
1051
1007
|
}
|
|
1052
1008
|
if (op === "pointIn" &&
|
|
1053
|
-
|
|
1009
|
+
rosterOf(sibling) === undefined &&
|
|
1054
1010
|
(sibling.kind === "u64" || sibling.kind === "i64") &&
|
|
1055
|
-
|
|
1011
|
+
isIntervalValue(value)) {
|
|
1056
1012
|
return taggedAtElementDomain(context, sibling.kind, value);
|
|
1057
1013
|
}
|
|
1058
1014
|
return taggedLiteral(context, sibling, value);
|
|
1059
1015
|
}
|
|
1060
1016
|
/** Creates one rule-scoped variable numberer. */
|
|
1061
|
-
function
|
|
1017
|
+
function freshVarIds() {
|
|
1062
1018
|
const assigned = new Map();
|
|
1063
1019
|
return {
|
|
1064
|
-
of(
|
|
1065
|
-
const existing = assigned.get(
|
|
1020
|
+
of(ref) {
|
|
1021
|
+
const existing = assigned.get(ref);
|
|
1066
1022
|
if (existing !== undefined) {
|
|
1067
1023
|
return existing;
|
|
1068
1024
|
}
|
|
1069
1025
|
const id = assigned.size;
|
|
1070
|
-
assigned.set(
|
|
1026
|
+
assigned.set(ref, id);
|
|
1071
1027
|
return id;
|
|
1072
1028
|
}
|
|
1073
1029
|
};
|
|
@@ -1082,10 +1038,7 @@ function paramIdOf(ctx, name) {
|
|
|
1082
1038
|
}
|
|
1083
1039
|
/**
|
|
1084
1040
|
* Lowers one EDB atom (either polarity). A CLOSED owner lowers through the
|
|
1085
|
-
* same edb source
|
|
1086
|
-
* an ordinary relation's — with field ordinals over the SEALED shape: `id`
|
|
1087
|
-
* at 0, each payload column at its declared index + 1 (`matchFieldsOf`
|
|
1088
|
-
* carries the shift; the lowering golden pins it).
|
|
1041
|
+
* same edb source, with field ordinals over the SEALED shape.
|
|
1089
1042
|
*/
|
|
1090
1043
|
function lowerAtom(ctx, atom, ids) {
|
|
1091
1044
|
const member = ctx.theory.relations[atom.relation.name];
|
|
@@ -1096,7 +1049,7 @@ function lowerAtom(ctx, atom, ids) {
|
|
|
1096
1049
|
if (relationId === undefined) {
|
|
1097
1050
|
throw errors.new(`query lowering: relation ${atom.relation.name} has no ordinal`);
|
|
1098
1051
|
}
|
|
1099
|
-
const ordered =
|
|
1052
|
+
const ordered = sealedFieldsOf(atom.relation);
|
|
1100
1053
|
const bindings = atom.bindings.map(function lowerBinding(binding) {
|
|
1101
1054
|
const ordinal = ordered.findIndex(function byName(candidate) {
|
|
1102
1055
|
return candidate.name === binding.field;
|
|
@@ -1108,17 +1061,12 @@ function lowerAtom(ctx, atom, ids) {
|
|
|
1108
1061
|
});
|
|
1109
1062
|
return { source: { kind: "edb", relation: relationId }, bindings };
|
|
1110
1063
|
}
|
|
1111
|
-
/**
|
|
1112
|
-
* Lowers one binding term. A membership ARRAY (`literalSet`) lowers to the
|
|
1113
|
-
* existing param-set term over its content-addressed registry entry — the
|
|
1114
|
-
* program IR is byte-identical to the same set spelled `r.inSet`; the SDK
|
|
1115
|
-
* supplies the translated member set itself at execute (`wireParams`).
|
|
1116
|
-
*/
|
|
1064
|
+
/** Lowers one binding term. A membership ARRAY lowers to the existing param-set term over its content-addressed entry. */
|
|
1117
1065
|
function lowerBindingTerm(ctx, context, binding, ids) {
|
|
1118
1066
|
const bound = binding.term;
|
|
1119
1067
|
switch (bound.kind) {
|
|
1120
1068
|
case "var":
|
|
1121
|
-
return { kind: "var", var: ids.of(bound.
|
|
1069
|
+
return { kind: "var", var: ids.of(bound.ref) };
|
|
1122
1070
|
case "param":
|
|
1123
1071
|
return { kind: "param", param: paramIdOf(ctx, bound.name) };
|
|
1124
1072
|
case "setParam":
|
|
@@ -1129,34 +1077,45 @@ function lowerBindingTerm(ctx, context, binding, ids) {
|
|
|
1129
1077
|
return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) };
|
|
1130
1078
|
}
|
|
1131
1079
|
}
|
|
1132
|
-
/**
|
|
1133
|
-
|
|
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) {
|
|
1134
1087
|
const pred = ctx.recIds.get(rec);
|
|
1135
1088
|
if (pred === undefined) {
|
|
1136
1089
|
throw errors.new(`query lowering: rec ${rec.name} was declared by a different program`);
|
|
1137
1090
|
}
|
|
1138
|
-
const
|
|
1139
|
-
if (
|
|
1140
|
-
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`);
|
|
1141
1094
|
}
|
|
1142
|
-
const
|
|
1143
|
-
|
|
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) }];
|
|
1144
1103
|
});
|
|
1145
|
-
return { source: { kind: "idb", pred }, bindings };
|
|
1104
|
+
return { source: { kind: "idb", pred }, bindings: irBindings };
|
|
1146
1105
|
}
|
|
1147
1106
|
/** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
|
|
1148
|
-
function lowerCmpTerm(ctx,
|
|
1107
|
+
function lowerCmpTerm(ctx, side, sibling, ids, op) {
|
|
1149
1108
|
switch (side.kind) {
|
|
1150
1109
|
case "var":
|
|
1151
|
-
return { kind: "var", var: ids.of(side.
|
|
1110
|
+
return { kind: "var", var: ids.of(side.ref) };
|
|
1152
1111
|
case "param":
|
|
1153
1112
|
return { kind: "param", param: paramIdOf(ctx, side.name) };
|
|
1154
1113
|
case "setParam":
|
|
1155
1114
|
return { kind: "paramSet", param: paramIdOf(ctx, side.name) };
|
|
1156
1115
|
case "measure":
|
|
1157
|
-
return { kind: "measure", var: ids.of(side.
|
|
1116
|
+
return { kind: "measure", var: ids.of(side.ref) };
|
|
1158
1117
|
case "literal": {
|
|
1159
|
-
const anchor = cmpAnchorOf(ctx,
|
|
1118
|
+
const anchor = cmpAnchorOf(ctx, sibling);
|
|
1160
1119
|
if (anchor === undefined) {
|
|
1161
1120
|
throw errors.new("query lowering: a comparison literal needs a bound-variable, measure, or anchored-param sibling to type it");
|
|
1162
1121
|
}
|
|
@@ -1164,10 +1123,10 @@ function lowerCmpTerm(ctx, rule, side, sibling, ids, op) {
|
|
|
1164
1123
|
}
|
|
1165
1124
|
}
|
|
1166
1125
|
}
|
|
1167
|
-
/** Resolves the anchor a comparison literal tags by: the sibling's field, the measure, or an anchored param. */
|
|
1168
|
-
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) {
|
|
1169
1128
|
if (sibling.kind === "var") {
|
|
1170
|
-
return
|
|
1129
|
+
return sibling.ref.field;
|
|
1171
1130
|
}
|
|
1172
1131
|
if (sibling.kind === "measure") {
|
|
1173
1132
|
return "measure";
|
|
@@ -1178,7 +1137,7 @@ function cmpAnchorOf(ctx, rule, sibling) {
|
|
|
1178
1137
|
return undefined;
|
|
1179
1138
|
}
|
|
1180
1139
|
/** Lowers one comparison. */
|
|
1181
|
-
function lowerComparison(ctx,
|
|
1140
|
+
function lowerComparison(ctx, cmp, ids) {
|
|
1182
1141
|
if (cmp.op === "allen") {
|
|
1183
1142
|
const maskData = cmp.mask;
|
|
1184
1143
|
if (maskData === undefined) {
|
|
@@ -1189,29 +1148,29 @@ function lowerComparison(ctx, rule, cmp, ids) {
|
|
|
1189
1148
|
: { kind: "param", param: paramIdOf(ctx, maskData.name) };
|
|
1190
1149
|
return {
|
|
1191
1150
|
op: { kind: "allen", mask },
|
|
1192
|
-
lhs: lowerCmpTerm(ctx,
|
|
1193
|
-
rhs: lowerCmpTerm(ctx,
|
|
1151
|
+
lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
|
|
1152
|
+
rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
|
|
1194
1153
|
};
|
|
1195
1154
|
}
|
|
1196
1155
|
return {
|
|
1197
1156
|
op: { kind: cmp.op },
|
|
1198
|
-
lhs: lowerCmpTerm(ctx,
|
|
1199
|
-
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)
|
|
1200
1159
|
};
|
|
1201
1160
|
}
|
|
1202
1161
|
/** Lowers one condition node (comparison leaf or and/or tree). */
|
|
1203
|
-
function lowerCondition(ctx,
|
|
1162
|
+
function lowerCondition(ctx, cond, ids) {
|
|
1204
1163
|
if (cond.kind === "cmp") {
|
|
1205
|
-
return { kind: "leaf", cmp: lowerComparison(ctx,
|
|
1164
|
+
return { kind: "leaf", cmp: lowerComparison(ctx, cond, ids) };
|
|
1206
1165
|
}
|
|
1207
1166
|
return {
|
|
1208
1167
|
kind: cond.op,
|
|
1209
1168
|
children: cond.children.map(function lowerChild(child) {
|
|
1210
|
-
return lowerCondition(ctx,
|
|
1169
|
+
return lowerCondition(ctx, child, ids);
|
|
1211
1170
|
})
|
|
1212
1171
|
};
|
|
1213
1172
|
}
|
|
1214
|
-
/** Lowers one
|
|
1173
|
+
/** Lowers one find entry to its per-rule find term. */
|
|
1215
1174
|
function lowerFind(entry, ids) {
|
|
1216
1175
|
if (entry.kind === "var") {
|
|
1217
1176
|
return { kind: "var", var: ids.of(entry.over) };
|
|
@@ -1226,10 +1185,10 @@ function lowerFind(entry, ids) {
|
|
|
1226
1185
|
case "countDistinct":
|
|
1227
1186
|
return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) };
|
|
1228
1187
|
case "fold": {
|
|
1229
|
-
if (
|
|
1230
|
-
return { kind: "
|
|
1188
|
+
if ("duration" in agg.over) {
|
|
1189
|
+
return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) };
|
|
1231
1190
|
}
|
|
1232
|
-
return { kind: "
|
|
1191
|
+
return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) };
|
|
1233
1192
|
}
|
|
1234
1193
|
case "arg":
|
|
1235
1194
|
return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) };
|
|
@@ -1252,7 +1211,7 @@ function headOpOf(agg) {
|
|
|
1252
1211
|
return "pack";
|
|
1253
1212
|
}
|
|
1254
1213
|
}
|
|
1255
|
-
/** One
|
|
1214
|
+
/** One find entry's var-free head shape. */
|
|
1256
1215
|
function headTermOf(column) {
|
|
1257
1216
|
const entry = column.entry;
|
|
1258
1217
|
if (entry.kind === "var" || entry.kind === "measure") {
|
|
@@ -1262,7 +1221,7 @@ function headTermOf(column) {
|
|
|
1262
1221
|
}
|
|
1263
1222
|
/** Lowers one rule: body walked in written order (var ids by first occurrence), finds last. */
|
|
1264
1223
|
function lowerRule(ctx, rule) {
|
|
1265
|
-
const ids =
|
|
1224
|
+
const ids = freshVarIds();
|
|
1266
1225
|
const atoms = [];
|
|
1267
1226
|
const negated = [];
|
|
1268
1227
|
const conditions = [];
|
|
@@ -1277,17 +1236,17 @@ function lowerRule(ctx, rule) {
|
|
|
1277
1236
|
break;
|
|
1278
1237
|
}
|
|
1279
1238
|
case "idb": {
|
|
1280
|
-
atoms.push(lowerIdbAtom(ctx, item.rec, item.
|
|
1239
|
+
atoms.push(lowerIdbAtom(ctx, item.rec, item.bindings, ids));
|
|
1281
1240
|
break;
|
|
1282
1241
|
}
|
|
1283
1242
|
case "cond": {
|
|
1284
|
-
conditions.push(lowerCondition(ctx,
|
|
1243
|
+
conditions.push(lowerCondition(ctx, item.cond, ids));
|
|
1285
1244
|
break;
|
|
1286
1245
|
}
|
|
1287
1246
|
}
|
|
1288
1247
|
}
|
|
1289
1248
|
return {
|
|
1290
|
-
finds: rule.
|
|
1249
|
+
finds: rule.finds.map(function findOf(column) {
|
|
1291
1250
|
return lowerFind(column.entry, ids);
|
|
1292
1251
|
}),
|
|
1293
1252
|
atoms,
|
|
@@ -1298,11 +1257,7 @@ function lowerRule(ctx, rule) {
|
|
|
1298
1257
|
/**
|
|
1299
1258
|
* Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
|
|
1300
1259
|
* recs in declaration order (`PredId` = index), the output predicate
|
|
1301
|
-
*
|
|
1302
|
-
* the law the engine's own manifest pins; `db.prepare` re-verifies the
|
|
1303
|
-
* alignment against the live manifest before sending. Every registered
|
|
1304
|
-
* param must carry a field anchor by now — an unanchorable param (its
|
|
1305
|
-
* every use beside a literal) is refused here, naming it.
|
|
1260
|
+
* appended last. Every registered param must carry a field anchor by now.
|
|
1306
1261
|
*/
|
|
1307
1262
|
function lowerQuery(q) {
|
|
1308
1263
|
const theory = q.schema;
|
|
@@ -1330,14 +1285,14 @@ function lowerQuery(q) {
|
|
|
1330
1285
|
throw errors.new(`query lowering: rec ${rec.name} has no rules`);
|
|
1331
1286
|
}
|
|
1332
1287
|
return {
|
|
1333
|
-
head: head.
|
|
1288
|
+
head: head.finds.map(headTermOf),
|
|
1334
1289
|
rules: rec.rules.map(function lowerRecRule(rule) {
|
|
1335
1290
|
return lowerRule(ctx, rule);
|
|
1336
1291
|
})
|
|
1337
1292
|
};
|
|
1338
1293
|
});
|
|
1339
1294
|
predicates.push({
|
|
1340
|
-
head: q.data.
|
|
1295
|
+
head: q.data.finds.map(headTermOf),
|
|
1341
1296
|
rules: q.data.rules.map(function lowerOutputRule(rule) {
|
|
1342
1297
|
return lowerRule(ctx, rule);
|
|
1343
1298
|
})
|