@bjornpagen/bumbledb 0.1.0 → 0.2.0

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