@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,26 +1,33 @@
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
 
21
28
  import * as errors from "@superbuilders/errors"
22
- import { phantom } from "#brand.ts"
23
- import { assertDeclarationOrderKey, type FieldData } from "#fields.ts"
29
+ import type { AnyField } from "#fields.ts"
30
+ import { assertDeclarationOrderKey } from "#fields.ts"
24
31
  import type {
25
32
  AtomIr,
26
33
  ComparisonIr,
@@ -34,298 +41,1125 @@ import type {
34
41
  TaggedValue,
35
42
  TermIr
36
43
  } from "#native.ts"
37
-
38
44
  import type {
39
- AnyBodyItem,
40
- AnyCondition,
41
- AtomSourceData,
45
+ AggData,
46
+ AnyCond,
47
+ AtomData,
48
+ BindEnv,
42
49
  BindingEntry,
43
- CmpOpData,
44
- CmpTerm,
45
- ComparisonItem,
46
- MatchAtom
50
+ BindParamsShape,
51
+ CheckBindings,
52
+ CheckCond,
53
+ CmpData,
54
+ CmpKind,
55
+ CmpTermData,
56
+ CondData,
57
+ CondParamsShape,
58
+ MaskData,
59
+ MatchShape,
60
+ ParamUse,
61
+ RecData,
62
+ RuleData,
63
+ RuleItem,
64
+ SelectColumn,
65
+ SelectEntryData,
66
+ TreeData
47
67
  } from "#query/atom.ts"
48
-
49
- import type {
50
- ColumnValues,
51
- Predicate,
52
- PredicateColumnsInput,
53
- PredicateData,
54
- PredicateParams,
55
- PredicateRuleInput,
56
- PredicateSelf
57
- } from "#query/predicate.ts"
58
- import { makePredicate } from "#query/predicate.ts"
59
- import type {
60
- AnyTerm,
61
- AnyVar,
62
- ItemParams,
63
- MaskParam,
64
- Param,
65
- ParamSet,
66
- ParamsRecord,
67
- ParamsShape,
68
- QueryRegistry,
69
- Var
68
+ import { allen, and, covers, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts"
69
+ import type { EnvShape, Flatten, InferredOf, JoinOk, ParamEntry, ParamsRecord, Var } from "#query/scope.ts"
70
+ import {
71
+ fieldJoins,
72
+ inferred,
73
+ isTerm,
74
+ makeDuration,
75
+ makeMaskParam,
76
+ makeParam,
77
+ makeSetParam,
78
+ makeVar,
79
+ renderFieldKind,
80
+ term
70
81
  } from "#query/scope.ts"
71
-
72
- import { createRegistry, isTerm, scopeAllenParam, scopeParam, scopeParamSet, scopeVar, term } from "#query/scope.ts"
73
- import type { AggregateData, RowOf, SelectShape } from "#query/select.ts"
74
- import type { FieldRef } from "#relation.ts"
82
+ import type { CheckNameSelect, CheckSelect, RowOfNameSelect, RowOfSelect, SelectEntry } from "#query/select.ts"
83
+ import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/select.ts"
84
+ import type { AnyRelation, FieldsShape, RelationFields } from "#relation.ts"
75
85
  import type { AnySchema, Schema, SchemaRelations } from "#schema.ts"
76
86
 
87
+ /** The ordinary (matchable) relations of a schema's record — closed relations lack the relation shape entirely. */
88
+ type QueryRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], AnyRelation>
89
+
90
+ /** The environment after one bindings record: the incoming env plus every var the record binds. */
91
+ type EnvOfMatch<Env extends EnvShape, F extends FieldsShape, B> =
92
+ Flatten<Env & BindEnv<F, B>> extends infer E extends EnvShape ? E : never
93
+
94
+ /** Reads an inferred-params carrier off a rec reference or rule value. */
95
+ type ParamsOf<T> = InferredOf<T> extends { readonly params: infer P extends ParamsRecord } ? P : Record<never, never>
96
+
97
+ /** Reads an inferred-row carrier off a rule value or query. */
98
+ type RowOf<T> = InferredOf<T> extends { readonly row: infer R } ? R : never
99
+
77
100
  /**
78
- * The scope value the `query()` build callback receives: variable and
79
- * parameter declaration plus predicate declaration (engine recursion).
101
+ * A recursive predicate's HEAD signature as field descriptors, position
102
+ * for position carried on the rec reference so an `idb` join can be
103
+ * judged against it; `undefined` on values that carry no head (a plain
104
+ * query rule, or an unthreaded rec handle before its first rule).
80
105
  */
81
- interface Scope<Rels extends SchemaRelations> {
82
- /**
83
- * Declares one query variable, typed by the field it is declared from
84
- * usable in ANY atom position whose field carries the same brand
85
- * (the nominal join discipline). Two calls are two variables.
86
- */
87
- var<V>(field: FieldRef<keyof Rels & string, string, V>): Var<V>
88
- /**
89
- * Declares one scalar parameter under a mandatory name literal — the
90
- * key of the typed params object `execute` takes.
91
- */
92
- param<const Name extends string, V>(name: Name, field: FieldRef<keyof Rels & string, string, V>): Param<Name, V>
93
- /**
94
- * Declares one set parameter (the IR's `ParamSet`): bound at execution
95
- * to a readonly array; a binding position matches on set membership.
96
- */
97
- paramSet<const Name extends string, V>(name: Name, field: FieldRef<keyof Rels & string, string, V>): ParamSet<Name, V>
98
- /**
99
- * Declares one Allen-mask parameter (`MaskTerm::Param`): the temporal
100
- * relation as a bind-time 13-bit mask argument.
101
- */
102
- allenParam<const Name extends string>(name: Name): MaskParam<Name>
103
- /**
104
- * Declares one IDB predicate with a named, field-typed head; its rules
105
- * are given in the declaration through a callback receiving the
106
- * predicate's own reference (self-recursion is `self.match({...})`).
107
- * `.match` on the returned value uses it as a body atom elsewhere.
108
- */
109
- predicate<
110
- const Cols extends PredicateColumnsInput,
111
- const Rules extends readonly PredicateRuleInput<ColumnValues<Cols>>[]
112
- >(
113
- name: string,
114
- columns: Cols,
115
- rules: (self: PredicateSelf<ColumnValues<Cols>>) => Rules
116
- ): Predicate<ColumnValues<Cols>, PredicateParams<Rules>>
106
+ type HeadShape = readonly AnyField[] | undefined
107
+
108
+ /**
109
+ * One finished rule as a plain value: the runtime data plus the inferred
110
+ * row/params carrier (and, for a RECURSIVE rule, the head's positional
111
+ * field descriptors — the signature `idb` joins pair against).
112
+ * `.rule(...)` consumes it; hosts never build one by hand.
113
+ */
114
+ interface RuleValue<Row, P extends ParamsRecord, Head extends HeadShape = undefined> {
115
+ readonly rule: RuleData
116
+ readonly [inferred]?: { readonly row: Row; readonly params: P; readonly head: Head }
117
+ }
118
+
119
+ /** Any finished rule value. */
120
+ type AnyRuleValue = RuleValue<unknown, ParamsRecord, HeadShape>
121
+
122
+ /** The positional head-field tuple of a recursive rule's names-only select. */
123
+ type HeadFieldsOf<Env extends EnvShape, S extends readonly string[]> = {
124
+ readonly [I in keyof S]: Env[S[I] & keyof Env]
117
125
  }
118
126
 
127
+ /** Reads an inferred-head carrier off a rule value or rec reference. */
128
+ type HeadOf<T> = InferredOf<T> extends { readonly head: infer H extends readonly AnyField[] } ? H : undefined
129
+
119
130
  /**
120
- * What the build callback returns: `rules` is an array of conjunctions
121
- * (multiple rules = set union answers are SETS, no order/limit exists;
122
- * the host sorts); `select` is the head record non-aggregate entries are
123
- * the implicit group key.
131
+ * A recursive predicate REFERENCE the shape `idb()` targets carry: the
132
+ * name (type-level identity: a recursive rule's own `idb` accepts only its
133
+ * own name — the self-recursion cut), the runtime data (value identity),
134
+ * the params its attached rules have used so far, and the head signature
135
+ * its FIRST rule sealed (thread the value `.rule(...)` returns into an
136
+ * `idb` and the program's `Params` type stays exact AND the idb join is
137
+ * arity- and domain-checked against the head).
124
138
  */
125
- interface QueryBuild {
126
- readonly rules: readonly (readonly AnyBodyItem[])[]
127
- readonly select: SelectShape
139
+ interface RecRef<Name extends string, P extends ParamsRecord, Head extends HeadShape = HeadShape> {
140
+ readonly name: Name
141
+ readonly data: RecData
142
+ readonly [inferred]?: { readonly params: P; readonly head: Head }
128
143
  }
129
144
 
130
- /** One classified select column as runtime data. */
131
- type SelectEntryData =
132
- | { readonly kind: "var"; readonly over: AnyVar }
133
- | { readonly kind: "measure"; readonly over: AnyVar }
134
- | { readonly kind: "aggregate"; readonly aggregate: AggregateData }
145
+ /** One `idb` position's judgment: the var must be bound by a relation atom, domain-equal to the head column when the head is carried. */
146
+ type IdbVarOk<Env extends EnvShape, T, F> =
147
+ T extends Var<infer N extends string>
148
+ ? N extends keyof Env
149
+ ? F extends AnyField
150
+ ? JoinOk<Env[N], F>
151
+ : true
152
+ : false
153
+ : false
135
154
 
136
- /** One answer column: its name (the row object key) and its entry. */
137
- interface SelectColumn {
138
- readonly name: string
139
- readonly entry: SelectEntryData
155
+ /**
156
+ * The validated `idb` variable tuple: every var must already be bound by a
157
+ * relation atom of the rule; and when the target carries its head
158
+ * signature (the threaded rec handle), the tuple must match the head's
159
+ * arity and every position must be domain-equal to its head column — the
160
+ * same wall `JoinOk` holds for EDB atoms. An unthreaded handle carries no
161
+ * head; its joins stay boundness-checked here and arity/domain-judged at
162
+ * prepare (the engine's law stands behind both tiers).
163
+ */
164
+ type CheckIdbVars<Env extends EnvShape, V, Head extends HeadShape = undefined> = Head extends readonly AnyField[]
165
+ ? V extends readonly unknown[]
166
+ ? V["length"] extends Head["length"]
167
+ ? { readonly [I in keyof V]: IdbVarOk<Env, V[I], Head[I & keyof Head]> extends true ? V[I] : never }
168
+ : { readonly [I in keyof V]: never }
169
+ : never
170
+ : { readonly [I in keyof V]: IdbVarOk<Env, V[I], undefined> extends true ? V[I] : never }
171
+
172
+ /**
173
+ * The term/predicate/aggregate constructor vocabulary every rule builder
174
+ * carries — pure value builders, environment-free: the chain's `.where`,
175
+ * `.match`, and `.select` seams judge their output against the rule
176
+ * environment.
177
+ */
178
+ interface TermOps {
179
+ /** Declares/names one variable: typed by the field it first binds; reuse joins. */
180
+ readonly var: typeof makeVar
181
+ /** Names one scalar parameter: typed by its use; the key of the execute params object. */
182
+ readonly param: typeof makeParam
183
+ /** Names one ∈-set parameter (the IR's `ParamSet`): bound to a readonly array at execution. */
184
+ readonly inSet: typeof makeSetParam
185
+ /** Names one Allen-mask parameter (`MaskTerm::Param`): a bind-time 13-bit mask number. */
186
+ readonly maskParam: typeof makeMaskParam
187
+ /** The measure of an interval-typed variable: `|[s, e)| = e − s`, u64. */
188
+ readonly duration: typeof makeDuration
189
+ readonly eq: typeof eq
190
+ readonly ne: typeof ne
191
+ readonly lt: typeof lt
192
+ readonly le: typeof le
193
+ readonly gt: typeof gt
194
+ readonly ge: typeof ge
195
+ readonly pointIn: typeof pointIn
196
+ readonly covers: typeof covers
197
+ readonly allen: typeof allen
198
+ readonly and: typeof and
199
+ readonly or: typeof or
200
+ readonly not: typeof not
201
+ readonly count: typeof count
202
+ readonly countDistinct: typeof countDistinct
203
+ readonly sum: typeof sum
204
+ readonly min: typeof min
205
+ readonly max: typeof max
206
+ readonly argMax: typeof argMax
207
+ readonly argMin: typeof argMin
208
+ readonly pack: typeof pack
209
+ }
210
+
211
+ /** The rule builder a `query(S).rule(...)` callback receives: the ops plus the first atom. */
212
+ interface QueryRuleScope<Rels extends SchemaRelations> extends TermOps {
213
+ /** The first EDB atom of the rule: fields bind vars, params, ∈-sets, or bare literals; absence is the wildcard (same-named vars within the record join domain-equal). */
214
+ match<R extends QueryRelation<Rels>, const B extends MatchShape<RelationFields<R>>>(
215
+ relation: R,
216
+ bindings: B & CheckBindings<Record<never, never>, RelationFields<R>, B>
217
+ ): QueryRuleChain<Rels, EnvOfMatch<Record<never, never>, RelationFields<R>, B>, BindParamsShape<RelationFields<R>, B>>
218
+ }
219
+
220
+ /** The chain of a plain query rule: more atoms, residual predicates, then the head. */
221
+ interface QueryRuleChain<Rels extends SchemaRelations, Env extends EnvShape, P extends ParamsRecord> {
222
+ /** One more positive EDB atom — var reuse joins, domain-equal by the environment check. */
223
+ match<R extends QueryRelation<Rels>, const B extends MatchShape<RelationFields<R>>>(
224
+ relation: R,
225
+ bindings: B & CheckBindings<Env, RelationFields<R>, B>
226
+ ): QueryRuleChain<Rels, EnvOfMatch<Env, RelationFields<R>, B>, Flatten<P & BindParamsShape<RelationFields<R>, B>>>
227
+ /** One residual predicate: a comparison, an `and`/`or` tree, or a negated atom (`r.not`). */
228
+ where<const C extends AnyCond>(
229
+ cond: CheckCond<Env, C> & C
230
+ ): QueryRuleChain<Rels, Env, Flatten<P & CondParamsShape<Env, C>>>
231
+ /** The head projection: var names, the measure, and aggregates; written order = answer column order. */
232
+ select<const S extends readonly SelectEntry[]>(...entries: CheckSelect<Env, S> & S): RuleValue<RowOfSelect<Env, S>, P>
233
+ }
234
+
235
+ /** The rule builder an OUTPUT rule of a `program()` receives: a query rule plus finished-stratum `idb` atoms. */
236
+ interface OutputRuleScope<Rels extends SchemaRelations> extends TermOps {
237
+ match<R extends QueryRelation<Rels>, const B extends MatchShape<RelationFields<R>>>(
238
+ relation: R,
239
+ bindings: B & CheckBindings<Record<never, never>, RelationFields<R>, B>
240
+ ): OutputRuleChain<
241
+ Rels,
242
+ EnvOfMatch<Record<never, never>, RelationFields<R>, B>,
243
+ BindParamsShape<RelationFields<R>, B>
244
+ >
245
+ }
246
+
247
+ /** The chain of an output rule: atoms, predicates, `idb` joins over the program's recs, then the head. */
248
+ interface OutputRuleChain<Rels extends SchemaRelations, Env extends EnvShape, P extends ParamsRecord> {
249
+ match<R extends QueryRelation<Rels>, const B extends MatchShape<RelationFields<R>>>(
250
+ relation: R,
251
+ bindings: B & CheckBindings<Env, RelationFields<R>, B>
252
+ ): OutputRuleChain<Rels, EnvOfMatch<Env, RelationFields<R>, B>, Flatten<P & BindParamsShape<RelationFields<R>, B>>>
253
+ where<const C extends AnyCond>(
254
+ cond: CheckCond<Env, C> & C
255
+ ): OutputRuleChain<Rels, Env, Flatten<P & CondParamsShape<Env, C>>>
256
+ /**
257
+ * One `idb` atom over a FINISHED stratum (any rec of this program): a
258
+ * positional join against the rec's head. An idb atom is a join
259
+ * position — every variable must already be bound by a relation atom of
260
+ * the rule (the theory's own domain relation; the rec's answers are
261
+ * theory values, so the join is identity). Threading the rec value the
262
+ * last `.rule(...)` returned carries its rules' params into `Params`
263
+ * AND its head signature, so the join is arity- and domain-checked
264
+ * against the head at compile time.
265
+ */
266
+ idb<Target extends RecRef<string, ParamsRecord>, const V extends readonly Var<string>[]>(
267
+ target: Target,
268
+ ...vars: CheckIdbVars<Env, V, HeadOf<Target>> & V
269
+ ): OutputRuleChain<Rels, Env, Flatten<P & ParamsOf<Target>>>
270
+ select<const S extends readonly SelectEntry[]>(...entries: CheckSelect<Env, S> & S): RuleValue<RowOfSelect<Env, S>, P>
271
+ }
272
+
273
+ /** The rule builder a RECURSIVE rule (`rec.rule(...)`) receives. */
274
+ interface RecRuleScope<Rels extends SchemaRelations, Self extends string> extends TermOps {
275
+ match<R extends QueryRelation<Rels>, const B extends MatchShape<RelationFields<R>>>(
276
+ relation: R,
277
+ bindings: B & CheckBindings<Record<never, never>, RelationFields<R>, B>
278
+ ): RecRuleChain<
279
+ Rels,
280
+ Self,
281
+ EnvOfMatch<Record<never, never>, RelationFields<R>, B>,
282
+ BindParamsShape<RelationFields<R>, B>
283
+ >
140
284
  }
141
285
 
142
- /** A query's runtime description — everything lowering and execution read. */
286
+ /**
287
+ * The chain of a recursive rule. Its `idb` accepts ONLY the rec itself —
288
+ * the self-recursion cut as a type-level boundary (mutual recursion is
289
+ * unwritable; a finished lower stratum is folded by the OUTPUT rules) —
290
+ * and its `select` takes bound variable NAMES only: aggregates and the
291
+ * measure are unrepresentable in a recursive head (the strata judge's
292
+ * `AggregationThroughCycle`/`MeasureInRecursiveHead`, made unwritable).
293
+ */
294
+ interface RecRuleChain<
295
+ Rels extends SchemaRelations,
296
+ Self extends string,
297
+ Env extends EnvShape,
298
+ P extends ParamsRecord
299
+ > {
300
+ match<R extends QueryRelation<Rels>, const B extends MatchShape<RelationFields<R>>>(
301
+ relation: R,
302
+ bindings: B & CheckBindings<Env, RelationFields<R>, B>
303
+ ): RecRuleChain<Rels, Self, EnvOfMatch<Env, RelationFields<R>, B>, Flatten<P & BindParamsShape<RelationFields<R>, B>>>
304
+ where<const C extends AnyCond>(
305
+ cond: CheckCond<Env, C> & C
306
+ ): RecRuleChain<Rels, Self, Env, Flatten<P & CondParamsShape<Env, C>>>
307
+ /** The self-recursive atom: `idb(self, ...boundVars)` — only this rec's own reference is accepted (threaded, its head arity- and domain-checks the join). */
308
+ idb<Target extends RecRef<Self, ParamsRecord>, const V extends readonly Var<string>[]>(
309
+ target: Target,
310
+ ...vars: CheckIdbVars<Env, V, HeadOf<Target>> & V
311
+ ): RecRuleChain<Rels, Self, Env, P>
312
+ /** The recursive head: bound variable names only (the creation quarantine, restated for fixpoint topology); the value carries the head's field descriptors for `idb` pairing. */
313
+ select<const S extends readonly string[]>(
314
+ ...names: CheckNameSelect<Env, S> & S
315
+ ): RuleValue<RowOfNameSelect<Env, S>, P, HeadFieldsOf<Env, S>>
316
+ }
317
+
318
+ /** A query's runtime description — everything lowering, the wire marshal, and answer decode read. */
143
319
  interface QueryData {
144
- readonly registry: QueryRegistry
145
- readonly rules: readonly (readonly AnyBodyItem[])[]
320
+ /** The program's recursive predicates in declaration order (empty for a plain query); `PredId` = index. */
321
+ readonly recs: readonly RecData[]
322
+ /** The output rules in written order (multiple rules = set union). */
323
+ readonly rules: readonly RuleData[]
324
+ /** The head columns (every rule derives the same head; written order = answer column order). */
146
325
  readonly select: readonly SelectColumn[]
326
+ /** The registered params in first-use order across the program walk (= dense `ParamId`s). */
327
+ readonly params: readonly ParamEntry[]
147
328
  }
148
329
 
149
330
  /**
150
331
  * An inert query value. `Row` is the inferred answer-row object type;
151
- * `Params` the inferred execute-params object type. Prepare with
152
- * `db.prepare(q)`; nothing here touches an engine.
332
+ * `Params` the inferred execute-params object type exactly the params
333
+ * the rules use. Prepare with `db.prepare(q)`; nothing here touches an
334
+ * engine.
153
335
  */
154
336
  interface Query<Rels extends SchemaRelations, Row, Params extends ParamsRecord> {
155
337
  readonly schema: Schema<Rels>
156
338
  readonly data: QueryData
157
- readonly [phantom]?: { readonly row: Row; readonly params: Params }
339
+ /** One more rule the query's answers are the SET UNION of its rules' answers; every rule derives the same head. */
340
+ rule<RV extends AnyRuleValue>(
341
+ build: (r: QueryRuleScope<Rels>) => RV
342
+ ): Query<Rels, Row | RowOf<RV>, Flatten<Params & ParamsOf<RV>>>
343
+ readonly [inferred]?: { readonly row: Row; readonly params: Params }
158
344
  }
159
345
 
160
- /** Any query value, whatever its schema and inferred types. */
161
- type AnyQuery = Query<SchemaRelations, unknown, ParamsRecord>
346
+ /**
347
+ * Any query value as lowering and the runtime consume it: the theory it
348
+ * was built against and its runtime description — every `Query` (typed or
349
+ * program-built) carries exactly this.
350
+ */
351
+ interface AnyQuery {
352
+ readonly schema: AnySchema
353
+ readonly data: QueryData
354
+ }
162
355
 
163
356
  /** Extracts a query value's inferred answer-row type. */
164
- type QueryRow<Q extends AnyQuery> = Exclude<Q[typeof phantom], undefined>["row"]
357
+ type QueryRow<Q extends AnyQuery> = RowOf<Q>
165
358
 
166
359
  /** Extracts a query value's inferred execute-params type. */
167
- type QueryParams<Q extends AnyQuery> = Exclude<Q[typeof phantom], undefined>["params"]
360
+ type QueryParams<Q extends AnyQuery> = ParamsOf<Q>
361
+
362
+ /** The entry value of `query(S)`: the first `.rule` mints the query. */
363
+ interface QueryStart<Rels extends SchemaRelations> {
364
+ rule<RV extends AnyRuleValue>(build: (r: QueryRuleScope<Rels>) => RV): Query<Rels, RowOf<RV>, ParamsOf<RV>>
365
+ }
168
366
 
169
- /** The params object type a build result implies (union over every rule item). */
170
- type BuildParams<R extends QueryBuild> = ParamsShape<ItemParams<R["rules"][number][number]>>
367
+ /** The frozen constructor vocabulary every rule builder spreads. */
368
+ const termOps: TermOps = Object.freeze({
369
+ var: makeVar,
370
+ param: makeParam,
371
+ inSet: makeSetParam,
372
+ maskParam: makeMaskParam,
373
+ duration: makeDuration,
374
+ eq,
375
+ ne,
376
+ lt,
377
+ le,
378
+ gt,
379
+ ge,
380
+ pointIn,
381
+ covers,
382
+ allen,
383
+ and,
384
+ or,
385
+ not,
386
+ count,
387
+ countDistinct,
388
+ sum,
389
+ min,
390
+ max,
391
+ argMax,
392
+ argMin,
393
+ pack
394
+ })
171
395
 
172
- /**
173
- * Narrows an aggregate select value — a trusted seam over the surface's
174
- * own constructors (only `#query/select.ts` produces `aggregate`-carrying
175
- * values), the same direction as every other constructor-owned shape here.
176
- */
177
- function isAggregateEntry(value: object): value is { readonly aggregate: AggregateData } {
178
- return "aggregate" in value
396
+ /** One rule under construction: immutable — every chain step is a fresh state. */
397
+ interface RuleBuildState {
398
+ readonly items: readonly RuleItem[]
399
+ readonly varFields: Readonly<Record<string, AnyField>>
400
+ readonly paramUses: readonly ParamUse[]
179
401
  }
180
402
 
181
- /** Narrows a `duration()` select value (only `duration` produces `measure`). */
182
- function isMeasureEntry(value: object): value is { readonly measure: AnyVar } {
183
- return "measure" in value
403
+ /** The empty rule state. */
404
+ const EMPTY_RULE: RuleBuildState = Object.freeze({
405
+ items: Object.freeze([]),
406
+ varFields: Object.freeze({}),
407
+ paramUses: Object.freeze([])
408
+ })
409
+
410
+ /** One resolved bindings record: the atom entries, the vars it binds, and the params it uses. */
411
+ interface ResolvedBindings {
412
+ readonly atom: AtomData
413
+ readonly vars: ReadonlyArray<{ readonly name: string; readonly field: AnyField }>
414
+ readonly uses: readonly ParamUse[]
184
415
  }
185
416
 
186
- /** Classifies one select record value into its runtime entry. */
187
- function selectEntryOf(name: string, value: unknown): SelectEntryData {
188
- if (isTerm(value)) {
189
- if (value[term] !== "var") {
417
+ /**
418
+ * Resolves a bindings record against a relation's declared fields, in the
419
+ * record's written order: terms classify by their runtime tag, everything
420
+ * else is a bare literal (typed by the FIELD at lowering — the membership
421
+ * typing rule included).
422
+ */
423
+ function resolveBindings(
424
+ context: string,
425
+ relation: AnyRelation,
426
+ bindings: Readonly<Record<string, unknown>>
427
+ ): ResolvedBindings {
428
+ const entries: BindingEntry[] = []
429
+ const vars: Array<{ readonly name: string; readonly field: AnyField }> = []
430
+ const uses: ParamUse[] = []
431
+ for (const [fieldName, value] of Object.entries(bindings)) {
432
+ if (value === undefined) {
433
+ continue
434
+ }
435
+ const declared = relation.data.fields.find(function byName(candidate) {
436
+ return candidate.name === fieldName
437
+ })
438
+ if (declared === undefined) {
439
+ throw errors.new(`${context} has no field ${fieldName}`)
440
+ }
441
+ let bound: BindingEntry["term"]
442
+ if (isTerm(value)) {
443
+ switch (value[term]) {
444
+ case "var": {
445
+ bound = Object.freeze({ kind: "var" as const, name: value.name })
446
+ vars.push(Object.freeze({ name: value.name, field: declared.field }))
447
+ break
448
+ }
449
+ case "param": {
450
+ bound = Object.freeze({ kind: "param" as const, name: value.name })
451
+ uses.push(
452
+ Object.freeze({ name: value.name, shape: "value" as const, anchor: declared.field, op: "binding" as const })
453
+ )
454
+ break
455
+ }
456
+ case "setParam": {
457
+ bound = Object.freeze({ kind: "setParam" as const, name: value.name })
458
+ uses.push(
459
+ Object.freeze({ name: value.name, shape: "set" as const, anchor: declared.field, op: "binding" as const })
460
+ )
461
+ break
462
+ }
463
+ case "maskParam":
464
+ throw errors.new(
465
+ `${context}.${fieldName}: an Allen-mask param is not a field-typed value — masks live in allen() conditions only`
466
+ )
467
+ case "duration":
468
+ throw errors.new(
469
+ `${context}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and select entries`
470
+ )
471
+ }
472
+ } else {
473
+ bound = Object.freeze({ kind: "literal" as const, value })
474
+ }
475
+ entries.push(Object.freeze({ field: fieldName, data: declared.field, term: bound }))
476
+ }
477
+ return {
478
+ atom: Object.freeze({ relation, bindings: Object.freeze(entries) }),
479
+ vars,
480
+ uses
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Extends a rule state with one positive atom. Vars bind on first
486
+ * occurrence; every LATER occurrence (a later atom's field or a same-record
487
+ * sibling) is a join and must be domain-equal — the construction-time twin
488
+ * of the type tier's `JoinOk`, so the domain wall holds for untyped
489
+ * callers too.
490
+ */
491
+ function advanceMatch(
492
+ state: RuleBuildState,
493
+ relation: AnyRelation,
494
+ bindings: Readonly<Record<string, unknown>>
495
+ ): RuleBuildState {
496
+ const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings)
497
+ const varFields: Record<string, AnyField> = { ...state.varFields }
498
+ for (const bound of resolved.vars) {
499
+ const existing = varFields[bound.name]
500
+ if (existing === undefined) {
501
+ varFields[bound.name] = bound.field
502
+ } else if (!fieldJoins(existing, bound.field)) {
190
503
  throw errors.new(
191
- `query select column ${name}: a ${value[term]} is not projectableselect takes variables, duration(v), or aggregates`
504
+ `relation ${relation.name}: the variable ${bound.name} joins domain-unequal fieldsfirst bound at ${renderFieldKind(existing)}, reused at ${renderFieldKind(bound.field)} (a var joins only domain-equal fields)`
192
505
  )
193
506
  }
194
- return Object.freeze({ kind: "var" as const, over: value })
195
507
  }
196
- if (typeof value === "object" && value !== null) {
197
- if (isAggregateEntry(value)) {
198
- return Object.freeze({ kind: "aggregate" as const, aggregate: value.aggregate })
508
+ return {
509
+ items: Object.freeze([...state.items, Object.freeze({ kind: "atom" as const, atom: resolved.atom })]),
510
+ varFields: Object.freeze(varFields),
511
+ paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
512
+ }
513
+ }
514
+
515
+ /** Resolves one comparison side to its runtime term. */
516
+ function cmpTermDataOf(op: string, value: unknown): CmpTermData {
517
+ if (isTerm(value)) {
518
+ switch (value[term]) {
519
+ case "var":
520
+ return Object.freeze({ kind: "var" as const, name: value.name })
521
+ case "param":
522
+ return Object.freeze({ kind: "param" as const, name: value.name })
523
+ case "setParam":
524
+ return Object.freeze({ kind: "setParam" as const, name: value.name })
525
+ case "duration":
526
+ return Object.freeze({ kind: "measure" as const, name: value.name })
527
+ case "maskParam":
528
+ throw errors.new(`${op}: an Allen-mask param is not a comparison term — masks live in allen()'s mask position`)
199
529
  }
200
- if (isMeasureEntry(value)) {
201
- return Object.freeze({ kind: "measure" as const, over: value.measure })
530
+ }
531
+ return Object.freeze({ kind: "literal" as const, value })
532
+ }
533
+
534
+ /**
535
+ * One comparison side's contribution to the param census: a param/set side
536
+ * anchors to its SIBLING — a bound variable's field descriptor or the
537
+ * measure; an unanchorable use (literal or param sibling) records with no
538
+ * anchor and must be anchored by some other use of the same name.
539
+ */
540
+ function sideUses(
541
+ op: CmpKind,
542
+ side: CmpTermData,
543
+ sibling: CmpTermData,
544
+ varFields: Readonly<Record<string, AnyField>>,
545
+ uses: ParamUse[]
546
+ ): void {
547
+ if (side.kind !== "param" && side.kind !== "setParam") {
548
+ return
549
+ }
550
+ let anchor: AnyField | "measure" | undefined
551
+ if (sibling.kind === "var") {
552
+ anchor = varFields[sibling.name]
553
+ } else if (sibling.kind === "measure") {
554
+ anchor = "measure"
555
+ } else {
556
+ anchor = undefined
557
+ }
558
+ uses.push(
559
+ Object.freeze({
560
+ name: side.name,
561
+ shape: side.kind === "param" ? ("value" as const) : ("set" as const),
562
+ anchor,
563
+ op
564
+ })
565
+ )
566
+ }
567
+
568
+ /** Lowers one condition VALUE to its runtime data, recording param uses. */
569
+ function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, AnyField>>, uses: ParamUse[]): CondData {
570
+ if (cond.cond === "cmp") {
571
+ const lhs = cmpTermDataOf(cond.op, cond.lhs)
572
+ const rhs = cmpTermDataOf(cond.op, cond.rhs)
573
+ sideUses(cond.op, lhs, rhs, varFields, uses)
574
+ sideUses(cond.op, rhs, lhs, varFields, uses)
575
+ let mask: MaskData | undefined
576
+ if (cond.op === "allen") {
577
+ const maskValue = cond.mask
578
+ if (typeof maskValue === "number") {
579
+ mask = Object.freeze({ kind: "literal" as const, mask: maskValue })
580
+ } else if (isTerm(maskValue) && maskValue[term] === "maskParam") {
581
+ mask = Object.freeze({ kind: "param" as const, name: maskValue.name })
582
+ uses.push(
583
+ Object.freeze({ name: maskValue.name, shape: "mask" as const, anchor: undefined, op: "allen" as const })
584
+ )
585
+ } else {
586
+ throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam")
587
+ }
202
588
  }
589
+ const data: CmpData = Object.freeze({ kind: "cmp" as const, op: cond.op, mask, lhs, rhs })
590
+ return data
591
+ }
592
+ if (cond.cond === "tree") {
593
+ const children = cond.children.map(function lowerChild(child) {
594
+ return condDataOf(child, varFields, uses)
595
+ })
596
+ const data: TreeData = Object.freeze({ kind: "tree" as const, op: cond.op, children: Object.freeze(children) })
597
+ return data
203
598
  }
204
599
  throw errors.new(
205
- `query select column ${name}: not a select entryselect takes variables, duration(v), or aggregates`
600
+ "a negated atom is not a condition-tree nodepass not(...) to where() directly, never inside and()/or()"
206
601
  )
207
602
  }
208
603
 
209
- /** Renders an atom source's name for construction diagnostics. */
210
- function sourceName(source: AtomSourceData): string {
211
- if (source.kind === "relation") {
212
- return source.relation.name
604
+ /** Extends a rule state with one `.where` item (a condition or a negated atom). */
605
+ function advanceWhere(state: RuleBuildState, cond: AnyCond): RuleBuildState {
606
+ if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
607
+ throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom")
608
+ }
609
+ if (cond.cond === "not") {
610
+ const relation: AnyRelation = cond.relation
611
+ const bindings: Readonly<Record<string, unknown>> = Object.fromEntries(
612
+ Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
613
+ return value !== undefined
614
+ })
615
+ )
616
+ const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings)
617
+ return {
618
+ items: Object.freeze([...state.items, Object.freeze({ kind: "negated" as const, atom: resolved.atom })]),
619
+ varFields: state.varFields,
620
+ paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
621
+ }
622
+ }
623
+ const uses: ParamUse[] = []
624
+ const data = condDataOf(cond, state.varFields, uses)
625
+ return {
626
+ items: Object.freeze([...state.items, Object.freeze({ kind: "cond" as const, cond: data })]),
627
+ varFields: state.varFields,
628
+ paramUses: Object.freeze([...state.paramUses, ...uses])
213
629
  }
214
- return `predicate ${source.pred.name}`
215
630
  }
216
631
 
217
- /** Collects every variable a rule's positive atoms bind. */
218
- function positiveVarsOf(body: readonly AnyBodyItem[]): Set<AnyVar> {
219
- const positive = new Set<AnyVar>()
220
- for (const item of body) {
221
- if (item.item !== "atom" || item.negated) {
222
- continue
632
+ /** Extends a rule state with one `idb` atom (vars must be bound — validated at completion). */
633
+ function advanceIdb(state: RuleBuildState, rec: RecData, vars: readonly Var<string>[]): RuleBuildState {
634
+ const names = vars.map(function nameOf(variable) {
635
+ if (!isTerm(variable) || variable[term] !== "var") {
636
+ throw errors.new(`idb ${rec.name}: positions take variables — bind literals and params through where()/match()`)
223
637
  }
224
- for (const binding of item.bindings) {
225
- if (binding.term.kind === "term" && binding.term.value[term] === "var") {
226
- positive.add(binding.term.value)
227
- }
638
+ return variable.name
639
+ })
640
+ return {
641
+ items: Object.freeze([...state.items, Object.freeze({ kind: "idb" as const, rec, vars: Object.freeze(names) })]),
642
+ varFields: state.varFields,
643
+ paramUses: state.paramUses
644
+ }
645
+ }
646
+
647
+ /** Narrows a select entry to an aggregate value. */
648
+ function isAggregateEntry(
649
+ value: unknown
650
+ ): value is { readonly agg: string; readonly over: unknown; readonly key: unknown } {
651
+ return typeof value === "object" && value !== null && "agg" in value
652
+ }
653
+
654
+ /** Classifies one select entry into its named answer column. */
655
+ function selectColumnOf(entry: unknown): SelectColumn {
656
+ if (typeof entry === "string") {
657
+ return Object.freeze({ name: entry, entry: Object.freeze({ kind: "var" as const, over: entry }) })
658
+ }
659
+ if (isTerm(entry)) {
660
+ if (entry[term] === "duration") {
661
+ return Object.freeze({ name: entry.name, entry: Object.freeze({ kind: "measure" as const, over: entry.name }) })
228
662
  }
663
+ throw errors.new(
664
+ `query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`
665
+ )
666
+ }
667
+ if (isAggregateEntry(entry)) {
668
+ return aggregateColumnOf(entry)
229
669
  }
230
- return positive
670
+ throw errors.new("query select: not a select entry — select takes variable names, duration(v), or aggregates")
231
671
  }
232
672
 
233
- /** Judges one negated atom against the rule's positively-bound variables. */
234
- function assertNegatedAtomSafe(context: string, positive: ReadonlySet<AnyVar>, atom: MatchAtom<ParamsRecord>): void {
235
- for (const binding of atom.bindings) {
236
- if (binding.term.kind === "oneOf") {
237
- throw errors.new(
238
- `${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`
239
- )
673
+ /** Classifies one aggregate select entry. */
674
+ function aggregateColumnOf(entry: {
675
+ readonly agg: string
676
+ readonly over: unknown
677
+ readonly key: unknown
678
+ }): SelectColumn {
679
+ function column(name: string, agg: AggData): SelectColumn {
680
+ return Object.freeze({ name, entry: Object.freeze({ kind: "aggregate" as const, agg: Object.freeze(agg) }) })
681
+ }
682
+ const over = entry.over
683
+ switch (entry.agg) {
684
+ case "count":
685
+ return column("count", { op: "count" })
686
+ case "countDistinct": {
687
+ if (typeof over !== "string") {
688
+ throw errors.new("countDistinct takes a variable name")
689
+ }
690
+ return column(over, { op: "countDistinct", over })
240
691
  }
241
- if (binding.term.kind !== "term" || binding.term.value[term] !== "var") {
242
- continue
692
+ case "sum":
693
+ case "min":
694
+ case "max": {
695
+ if (typeof over === "string") {
696
+ return column(over, { op: "fold", fold: entry.agg, over })
697
+ }
698
+ if (isTerm(over) && over[term] === "duration") {
699
+ return column(over.name, { op: "fold", fold: entry.agg, over: Object.freeze({ duration: over.name }) })
700
+ }
701
+ throw errors.new(`${entry.agg} takes a variable name or duration(v)`)
243
702
  }
244
- const variable = binding.term.value
245
- if (!positive.has(variable)) {
246
- throw errors.new(
247
- `${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)`
248
- )
703
+ case "argMax":
704
+ case "argMin": {
705
+ if (typeof over !== "string" || typeof entry.key !== "string") {
706
+ throw errors.new(`${entry.agg} takes a carried variable name and an orderable key variable name`)
707
+ }
708
+ return column(over, { op: "arg", direction: entry.agg, over, key: entry.key })
249
709
  }
710
+ case "pack": {
711
+ if (typeof over !== "string") {
712
+ throw errors.new("pack takes a variable name")
713
+ }
714
+ return column(over, { op: "pack", over })
715
+ }
716
+ default:
717
+ throw errors.new(`unknown aggregate ${entry.agg}`)
718
+ }
719
+ }
720
+
721
+ /** Requires a var name to be bound by a relation atom of the rule. */
722
+ function assertBound(context: string, varFields: Readonly<Record<string, AnyField>>, name: string): AnyField {
723
+ const field = varFields[name]
724
+ if (field === undefined) {
725
+ throw errors.new(`${context}: the variable ${name} is not bound by a relation atom of the rule`)
726
+ }
727
+ return field
728
+ }
729
+
730
+ /** Requires a var name to be bound at an interval field (the measure's and pack's domain). */
731
+ function assertIntervalBound(context: string, varFields: Readonly<Record<string, AnyField>>, name: string): void {
732
+ const field = assertBound(context, varFields, name)
733
+ if (field.kind !== "interval") {
734
+ throw errors.new(
735
+ `${context}: ${name} is not interval-typed — the measure is defined over interval-typed variables only`
736
+ )
737
+ }
738
+ }
739
+
740
+ /** Validates one condition's variable references against the rule's bound names. */
741
+ function validateCond(context: string, varFields: Readonly<Record<string, AnyField>>, cond: CondData): void {
742
+ if (cond.kind === "cmp") {
743
+ for (const side of [cond.lhs, cond.rhs]) {
744
+ if (side.kind === "var") {
745
+ assertBound(context, varFields, side.name)
746
+ }
747
+ if (side.kind === "measure") {
748
+ assertIntervalBound(context, varFields, side.name)
749
+ }
750
+ }
751
+ return
752
+ }
753
+ for (const child of cond.children) {
754
+ validateCond(context, varFields, child)
755
+ }
756
+ }
757
+
758
+ /** Validates one select column's variable references. */
759
+ function validateColumn(context: string, varFields: Readonly<Record<string, AnyField>>, column: SelectColumn): void {
760
+ const entry = column.entry
761
+ if (entry.kind === "var") {
762
+ assertBound(`${context} select ${column.name}`, varFields, entry.over)
763
+ return
764
+ }
765
+ if (entry.kind === "measure") {
766
+ assertIntervalBound(`${context} select ${column.name}`, varFields, entry.over)
767
+ return
768
+ }
769
+ const agg = entry.agg
770
+ switch (agg.op) {
771
+ case "count":
772
+ return
773
+ case "countDistinct":
774
+ assertBound(`${context} select ${column.name}`, varFields, agg.over)
775
+ return
776
+ case "fold": {
777
+ if (typeof agg.over === "string") {
778
+ assertBound(`${context} select ${column.name}`, varFields, agg.over)
779
+ return
780
+ }
781
+ assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over.duration)
782
+ return
783
+ }
784
+ case "arg":
785
+ assertBound(`${context} select ${column.name}`, varFields, agg.over)
786
+ assertBound(`${context} select ${column.name}`, varFields, agg.key)
787
+ return
788
+ case "pack":
789
+ assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over)
790
+ return
250
791
  }
251
792
  }
252
793
 
253
794
  /**
254
- * The negation safety rule, judged at construction (earlier and warmer
255
- * than the engine's refusal, which also stands): every variable a negated
256
- * atom uses must be bound by a positive atom of the same rule — a negated
257
- * atom binds nothing, only rejects.
795
+ * Completes one rule: classifies the select record (written order = answer
796
+ * column order, names must be declaration-order-safe keys), and validates
797
+ * boundness every condition/select/idb variable bound by a relation atom,
798
+ * and every NEGATED atom's variable positively bound (the safety rule: a
799
+ * negated atom binds nothing, only rejects).
258
800
  */
259
- function assertNegationSafety(context: string, body: readonly AnyBodyItem[]): void {
260
- const positive = positiveVarsOf(body)
261
- for (const item of body) {
262
- if (item.item === "atom" && item.negated) {
263
- assertNegatedAtomSafe(context, positive, item)
801
+ function completeRule(context: string, state: RuleBuildState, columns: readonly SelectColumn[]): RuleData {
802
+ if (columns.length === 0) {
803
+ throw errors.new(`${context}: a select needs at least one entry`)
804
+ }
805
+ const seen = new Set<string>()
806
+ for (const column of columns) {
807
+ assertDeclarationOrderKey(`${context} select column`, column.name)
808
+ if (seen.has(column.name)) {
809
+ throw errors.new(`${context}: select names the answer column ${column.name} twice`)
810
+ }
811
+ seen.add(column.name)
812
+ validateColumn(context, state.varFields, column)
813
+ }
814
+ for (const item of state.items) {
815
+ if (item.kind === "negated") {
816
+ for (const binding of item.atom.bindings) {
817
+ if (binding.term.kind === "var") {
818
+ const bound = state.varFields[binding.term.name]
819
+ if (bound === undefined) {
820
+ throw errors.new(
821
+ `${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)`
822
+ )
823
+ }
824
+ if (!fieldJoins(bound, binding.data)) {
825
+ throw errors.new(
826
+ `${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`
827
+ )
828
+ }
829
+ }
830
+ }
831
+ }
832
+ if (item.kind === "idb") {
833
+ const head = item.rec.rules[0]
834
+ item.vars.forEach(function checkIdbVar(name, position) {
835
+ const bound = state.varFields[name]
836
+ if (bound === undefined) {
837
+ throw errors.new(
838
+ `${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`
839
+ )
840
+ }
841
+ const column = head?.select[position]
842
+ if (column === undefined || column.entry.kind !== "var") {
843
+ return
844
+ }
845
+ const headField = head?.varFields[column.entry.over]
846
+ if (headField !== undefined && !fieldJoins(headField, bound)) {
847
+ throw errors.new(
848
+ `${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`
849
+ )
850
+ }
851
+ })
852
+ }
853
+ if (item.kind === "cond") {
854
+ validateCond(context, state.varFields, item.cond)
264
855
  }
265
856
  }
857
+ return Object.freeze({
858
+ items: state.items,
859
+ select: Object.freeze([...columns]),
860
+ varFields: state.varFields,
861
+ paramUses: state.paramUses
862
+ })
863
+ }
864
+
865
+ /** Builds one typed rule value over completed rule data. */
866
+ function makeRuleValue<Row, P extends ParamsRecord>(rule: RuleData): RuleValue<Row, P> {
867
+ return Object.freeze({ rule })
266
868
  }
267
869
 
268
870
  /**
269
- * Builds a query as a typed value: runs `build` inside a fresh scope,
270
- * classifies the select record (written order = answer column order),
271
- * validates negation safety across every rule (the predicates' rules
272
- * included), and freezes. Rule counts, strata legality, and every deeper
273
- * roster stay the engine's judge at prepare.
871
+ * The one runtime chain every context shares non-generic on purpose: the
872
+ * typed chain interfaces (`QueryRuleChain`/`OutputRuleChain`/`RecRuleChain`)
873
+ * apply at the scope factories' boundaries, and the runtime beneath them is
874
+ * one plain value walk. Context gates the two context-bound verbs: `idb`
875
+ * (a program construct self-only inside a rec, any rec of the program in
876
+ * the output, refused in a plain query) and the recursive `select`
877
+ * (bound variable names only — the creation quarantine).
274
878
  */
275
- function query<Rels extends SchemaRelations, const R extends QueryBuild>(
276
- theory: Schema<Rels>,
277
- build: ($: Scope<Rels>) => R
278
- ): Query<Rels, RowOf<R["select"]>, BuildParams<R>> {
279
- const registry = createRegistry(theory)
280
- const scope: Scope<Rels> = Object.freeze({
281
- var<V>(field: FieldRef<keyof Rels & string, string, V>): Var<V> {
282
- return scopeVar(registry, field)
283
- },
284
- param<const Name extends string, V>(name: Name, field: FieldRef<keyof Rels & string, string, V>): Param<Name, V> {
285
- return scopeParam(registry, name, field)
879
+ interface RawChain {
880
+ match(relation: AnyRelation, bindings: Readonly<Record<string, unknown>>): RawChain
881
+ where(cond: AnyCond): RawChain
882
+ idb(target: RecRef<string, ParamsRecord>, ...vars: readonly Var<string>[]): RawChain
883
+ select(...entries: readonly SelectEntry[]): RuleValue<never, never>
884
+ }
885
+
886
+ /** The runtime rule-builder shape beneath every typed scope. */
887
+ interface RawScope extends TermOps {
888
+ match(relation: AnyRelation, bindings: Readonly<Record<string, unknown>>): RawChain
889
+ }
890
+
891
+ /** Which rule family a chain builds — gates `idb` and the recursive select. */
892
+ type ChainContext =
893
+ | { readonly kind: "query" }
894
+ | { readonly kind: "rec"; readonly self: RecData }
895
+ | { readonly kind: "output"; readonly program: ProgramState }
896
+
897
+ /** The diagnostic label of a chain context. */
898
+ function contextLabel(context: ChainContext): string {
899
+ switch (context.kind) {
900
+ case "query":
901
+ return "query rule"
902
+ case "rec":
903
+ return `rec ${context.self.name} rule`
904
+ case "output":
905
+ return "program output rule"
906
+ }
907
+ }
908
+
909
+ /** Validates and records one `idb` atom per the context's cut. */
910
+ function idbAdvance(
911
+ context: ChainContext,
912
+ state: RuleBuildState,
913
+ target: RecRef<string, ParamsRecord>,
914
+ vars: readonly Var<string>[]
915
+ ): RuleBuildState {
916
+ if (context.kind === "query") {
917
+ throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()")
918
+ }
919
+ if (context.kind === "rec") {
920
+ if (target.data !== context.self) {
921
+ throw errors.new(
922
+ `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)`
923
+ )
924
+ }
925
+ return advanceIdb(state, context.self, vars)
926
+ }
927
+ if (!context.program.recs.includes(target.data)) {
928
+ throw errors.new(
929
+ `idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`
930
+ )
931
+ }
932
+ return advanceIdb(state, target.data, vars)
933
+ }
934
+
935
+ /** Classifies one select tuple per the context (a recursive head projects bound NAMES only). */
936
+ function selectColumns(context: ChainContext, entries: readonly SelectEntry[]): SelectColumn[] {
937
+ return entries.map(function columnOf(entry): SelectColumn {
938
+ if (context.kind === "rec" && typeof entry !== "string") {
939
+ throw errors.new(
940
+ `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)`
941
+ )
942
+ }
943
+ return selectColumnOf(entry)
944
+ })
945
+ }
946
+
947
+ /** Builds one runtime chain (immutably — every step is a fresh chain over fresh state). */
948
+ function makeRawChain(context: ChainContext, state: RuleBuildState): RawChain {
949
+ const chain: RawChain = {
950
+ match(relation, bindings) {
951
+ return makeRawChain(context, advanceMatch(state, relation, bindings))
286
952
  },
287
- paramSet<const Name extends string, V>(
288
- name: Name,
289
- field: FieldRef<keyof Rels & string, string, V>
290
- ): ParamSet<Name, V> {
291
- return scopeParamSet(registry, name, field)
953
+ where(cond) {
954
+ return makeRawChain(context, advanceWhere(state, cond))
292
955
  },
293
- allenParam<const Name extends string>(name: Name): MaskParam<Name> {
294
- return scopeAllenParam(registry, name)
956
+ idb(target, ...vars) {
957
+ return makeRawChain(context, idbAdvance(context, state, target, vars))
295
958
  },
296
- predicate<
297
- const Cols extends PredicateColumnsInput,
298
- const Rules extends readonly PredicateRuleInput<ColumnValues<Cols>>[]
299
- >(
300
- name: string,
301
- columns: Cols,
302
- rules: (self: PredicateSelf<ColumnValues<Cols>>) => Rules
303
- ): Predicate<ColumnValues<Cols>, PredicateParams<Rules>> {
304
- return makePredicate(registry, name, columns, rules)
959
+ select(...entries) {
960
+ return makeRuleValue<never, never>(completeRule(contextLabel(context), state, selectColumns(context, entries)))
305
961
  }
306
- })
307
- const built = build(scope)
308
- const select: SelectColumn[] = []
309
- for (const [name, value] of Object.entries(built.select)) {
310
- assertDeclarationOrderKey("query select column", name)
311
- select.push(Object.freeze({ name, entry: selectEntryOf(name, value) }))
312
- }
313
- for (const pred of registry.predicates) {
314
- pred.rules.forEach(function validateRule(rule, index) {
315
- assertNegationSafety(`query construction (predicate ${pred.name}, rule ${index})`, rule.body)
962
+ }
963
+ Object.freeze(chain)
964
+ return chain
965
+ }
966
+
967
+ /** Builds one runtime rule-builder over a context. */
968
+ function makeRawScope(context: ChainContext): RawScope {
969
+ const scope: RawScope = {
970
+ ...termOps,
971
+ match(relation, bindings) {
972
+ return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings))
973
+ }
974
+ }
975
+ Object.freeze(scope)
976
+ return scope
977
+ }
978
+
979
+ /**
980
+ * The one trusted admission seam of the rule builders (the `relation()`
981
+ * `refsComplete` pattern): the raw builder is one runtime shape for every
982
+ * context, and this guard verifies the checkable fact — the builder verbs
983
+ * exist — before the value is admitted at its TYPED face. The type-level
984
+ * judgments (domain-equal joins, boundness, the recursion cut) live in the
985
+ * interfaces themselves; the runtime twin of every one of them is a
986
+ * construction-time validation in this module.
987
+ */
988
+ function isTypedScope<S>(scope: RawScope): scope is RawScope & S {
989
+ return typeof scope.match === "function"
990
+ }
991
+
992
+ /** Builds one query-rule builder (the typed face of the raw builder). */
993
+ function makeQueryRuleScope<Rels extends SchemaRelations>(): QueryRuleScope<Rels> {
994
+ const raw = makeRawScope({ kind: "query" })
995
+ if (!isTypedScope<QueryRuleScope<Rels>>(raw)) {
996
+ throw errors.new("query rule builder construction incomplete")
997
+ }
998
+ return raw
999
+ }
1000
+
1001
+ /** Builds one output-rule builder over a program's recs. */
1002
+ function makeOutputRuleScope<Rels extends SchemaRelations>(program: ProgramState): OutputRuleScope<Rels> {
1003
+ const raw = makeRawScope({ kind: "output", program })
1004
+ if (!isTypedScope<OutputRuleScope<Rels>>(raw)) {
1005
+ throw errors.new("program output rule builder construction incomplete")
1006
+ }
1007
+ return raw
1008
+ }
1009
+
1010
+ /** One program's build-time registry: its recs in declaration order, sealed when the output is declared. */
1011
+ interface ProgramState {
1012
+ readonly recs: RecData[]
1013
+ sealed: boolean
1014
+ }
1015
+
1016
+ /** Renders one head column's signature for the rule-alignment check. */
1017
+ function headSignature(column: SelectColumn): string {
1018
+ const entry = column.entry
1019
+ if (entry.kind === "var" || entry.kind === "measure") {
1020
+ return `${column.name}:var`
1021
+ }
1022
+ const agg = entry.agg
1023
+ if (agg.op === "fold") {
1024
+ return `${column.name}:${agg.fold}`
1025
+ }
1026
+ if (agg.op === "arg") {
1027
+ return `${column.name}:${agg.direction}`
1028
+ }
1029
+ return `${column.name}:${agg.op}`
1030
+ }
1031
+
1032
+ /**
1033
+ * Folds every rule's param uses (recs in declaration order first, output
1034
+ * rules last — exactly the lowering walk) into the query's registry:
1035
+ * first use mints the dense `ParamId`, the first FIELD-ANCHORED use types
1036
+ * the wire, and one name must keep one shape.
1037
+ */
1038
+ function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]): readonly ParamEntry[] {
1039
+ const order: string[] = []
1040
+ const byName = new Map<string, { shape: ParamEntry["shape"]; anchor: ParamEntry["anchor"]; op: ParamEntry["op"] }>()
1041
+ function fold(uses: readonly ParamUse[]): void {
1042
+ for (const use of uses) {
1043
+ const existing = byName.get(use.name)
1044
+ if (existing === undefined) {
1045
+ order.push(use.name)
1046
+ byName.set(use.name, { shape: use.shape, anchor: use.anchor, op: use.op })
1047
+ continue
1048
+ }
1049
+ if (existing.shape !== use.shape) {
1050
+ throw errors.new(
1051
+ `query param ${use.name} is used both as a ${existing.shape} param and a ${use.shape} param — one name, one shape`
1052
+ )
1053
+ }
1054
+ if (existing.anchor === undefined && use.anchor !== undefined) {
1055
+ existing.anchor = use.anchor
1056
+ existing.op = use.op
1057
+ }
1058
+ }
1059
+ }
1060
+ for (const rec of recs) {
1061
+ for (const rule of rec.rules) {
1062
+ fold(rule.paramUses)
1063
+ }
1064
+ }
1065
+ for (const rule of rules) {
1066
+ fold(rule.paramUses)
1067
+ }
1068
+ return Object.freeze(
1069
+ order.map(function entryOf(name): ParamEntry {
1070
+ const entry = byName.get(name)
1071
+ if (entry === undefined) {
1072
+ throw errors.new(`query param ${name} lost its registry entry`)
1073
+ }
1074
+ return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op })
316
1075
  })
1076
+ )
1077
+ }
1078
+
1079
+ /** The runtime query shape beneath the typed `Query` face. */
1080
+ interface RawQuery {
1081
+ readonly schema: AnySchema
1082
+ readonly data: QueryData
1083
+ rule(build: (r: RawScope) => RuleValue<never, never>): RawQuery
1084
+ }
1085
+
1086
+ /**
1087
+ * Assembles the runtime query value over completed rules: every rule must
1088
+ * derive the SAME head (name and aggregate shape, position for position —
1089
+ * the decode labels and the engine's alignment rule agree by
1090
+ * construction), and the param registry folds in program-walk order.
1091
+ */
1092
+ function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readonly RuleData[]): RawQuery {
1093
+ const first = rules[0]
1094
+ if (first === undefined) {
1095
+ throw errors.new("a query needs at least one rule")
317
1096
  }
318
- const rules = built.rules.map(function freezeRule(rule, index) {
319
- assertNegationSafety(`query construction (rule ${index})`, rule)
320
- return Object.freeze([...rule])
1097
+ const signature = first.select.map(headSignature).join(", ")
1098
+ rules.forEach(function verifyHead(rule, index) {
1099
+ const candidate = rule.select.map(headSignature).join(", ")
1100
+ if (candidate !== signature) {
1101
+ throw errors.new(
1102
+ `every rule of a query derives the same head — rule 0 selects (${signature}), rule ${index} selects (${candidate})`
1103
+ )
1104
+ }
321
1105
  })
322
- Object.freeze(registry.params)
323
- Object.freeze(registry.predicates)
324
- Object.freeze(registry)
325
- return Object.freeze({
326
- schema: theory,
327
- data: Object.freeze({ registry, rules: Object.freeze(rules), select: Object.freeze(select) })
1106
+ const data: QueryData = Object.freeze({
1107
+ recs: Object.freeze([...recs]),
1108
+ rules: Object.freeze([...rules]),
1109
+ select: first.select,
1110
+ params: paramRegistryOf(recs, rules)
328
1111
  })
1112
+ const value: RawQuery = {
1113
+ schema: theory,
1114
+ data,
1115
+ rule(build) {
1116
+ const built = build(makeRawScope({ kind: "query" }))
1117
+ return makeRawQuery(theory, recs, [...rules, built.rule])
1118
+ }
1119
+ }
1120
+ Object.freeze(value)
1121
+ return value
1122
+ }
1123
+
1124
+ /**
1125
+ * The query values' trusted admission seam (the `refsComplete` pattern):
1126
+ * the checkable fact — the value was assembled over the identical theory —
1127
+ * is verified before the raw value is admitted at its typed face.
1128
+ */
1129
+ function isQueryValue<Rels extends SchemaRelations, Row, P extends ParamsRecord>(
1130
+ theory: Schema<Rels>,
1131
+ value: RawQuery
1132
+ ): value is RawQuery & Query<Rels, Row, P> {
1133
+ return value.schema === theory
1134
+ }
1135
+
1136
+ /** Assembles one typed query value (rules already completed). */
1137
+ function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord>(
1138
+ theory: Schema<Rels>,
1139
+ recs: readonly RecData[],
1140
+ rules: readonly RuleData[]
1141
+ ): Query<Rels, Row, P> {
1142
+ const raw = makeRawQuery(theory, recs, rules)
1143
+ if (!isQueryValue<Rels, Row, P>(theory, raw)) {
1144
+ throw errors.new("query value construction incomplete")
1145
+ }
1146
+ return raw
1147
+ }
1148
+
1149
+ /**
1150
+ * Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule`
1151
+ * adds one conjunctive rule; multiple rules are the set union (answers are
1152
+ * SETS — no order or limit exists anywhere; hosts sort).
1153
+ */
1154
+ function query<Rels extends SchemaRelations>(theory: Schema<Rels>): QueryStart<Rels> {
1155
+ const start: QueryStart<Rels> = {
1156
+ rule<RV extends AnyRuleValue>(build: (r: QueryRuleScope<Rels>) => RV): Query<Rels, RowOf<RV>, ParamsOf<RV>> {
1157
+ const built = build(makeQueryRuleScope<Rels>())
1158
+ return makeQuery<Rels, RowOf<RV>, ParamsOf<RV>>(theory, [], [built.rule])
1159
+ }
1160
+ }
1161
+ Object.freeze(start)
1162
+ return start
329
1163
  }
330
1164
 
331
1165
  /** The typed shape refusal of the literal tagger — a genuine failure, never data. */
@@ -345,20 +1179,62 @@ function isIntervalShaped(value: unknown): value is { readonly start: bigint; re
345
1179
  )
346
1180
  }
347
1181
 
1182
+ /**
1183
+ * Tags one closed-reference literal: the bare handle id, verified against
1184
+ * the roster (the belt the type level cannot provide — structural values
1185
+ * make any bigint spellable here) and tagged u64 — queries cross ids,
1186
+ * never handle names.
1187
+ */
1188
+ function taggedHandleId(
1189
+ context: string,
1190
+ closed: { readonly name: string; readonly handles: readonly string[] },
1191
+ value: unknown
1192
+ ): TaggedValue {
1193
+ if (typeof value !== "bigint") {
1194
+ throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, value)
1195
+ }
1196
+ if (closed.handles[Number(value)] === undefined) {
1197
+ throw errors.new(
1198
+ `${context}: closed relation ${closed.name} has no handle with id ${value} (roster holds ${closed.handles.length})`
1199
+ )
1200
+ }
1201
+ return { kind: "u64", value }
1202
+ }
1203
+
1204
+ /**
1205
+ * Tags one literal in an interval element domain: a bigint tags as the
1206
+ * element (the membership typing rule's point side), an interval-shaped
1207
+ * value as the interval (value equality).
1208
+ */
1209
+ function taggedAtElementDomain(context: string, element: "u64" | "i64", value: unknown): TaggedValue {
1210
+ if (typeof value === "bigint") {
1211
+ if (element === "u64") {
1212
+ return { kind: "u64", value }
1213
+ }
1214
+ return { kind: "i64", value }
1215
+ }
1216
+ if (isIntervalShaped(value)) {
1217
+ if (element === "u64") {
1218
+ return { kind: "intervalU64", start: value.start, end: value.end }
1219
+ }
1220
+ return { kind: "intervalI64", start: value.start, end: value.end }
1221
+ }
1222
+ throw literalShapeError(context, "bigint (point) or { start, end } (interval)", value)
1223
+ }
1224
+
348
1225
  /**
349
1226
  * Tags one host literal at a FIELD position (atom bindings): the field's
350
- * structural type directs the tag, never a guess. At an interval field a
1227
+ * structural kind directs the tag, never a guess. At an interval field a
351
1228
  * bigint literal tags as the ELEMENT type — the IR's membership typing
352
1229
  * rule (point membership), an interval-shaped literal as the interval
353
- * (value equality). A closed-reference literal is its branded id, tagged
354
- * u64 after a roster re-verification (queries cross ids, never handle
355
- * names).
1230
+ * (value equality). A closed-reference literal is its bare handle id,
1231
+ * tagged u64 after a roster verification.
356
1232
  */
357
- function taggedLiteral(context: string, field: FieldData, value: unknown): TaggedValue {
358
- if (field.closed !== undefined) {
1233
+ function taggedLiteral(context: string, field: AnyField, value: unknown): TaggedValue {
1234
+ if ("closed" in field) {
359
1235
  return taggedHandleId(context, field.closed, value)
360
1236
  }
361
- switch (field.type.kind) {
1237
+ switch (field.kind) {
362
1238
  case "bool": {
363
1239
  if (typeof value !== "boolean") {
364
1240
  throw literalShapeError(context, "boolean", value)
@@ -377,79 +1253,41 @@ function taggedLiteral(context: string, field: FieldData, value: unknown): Tagge
377
1253
  }
378
1254
  return { kind: "i64", value }
379
1255
  }
380
- case "string": {
1256
+ case "str": {
381
1257
  if (typeof value !== "string") {
382
1258
  throw literalShapeError(context, "string", value)
383
1259
  }
384
1260
  return { kind: "string", value }
385
1261
  }
386
- case "fixedBytes": {
1262
+ case "bytes": {
387
1263
  if (!(value instanceof Uint8Array)) {
388
1264
  throw literalShapeError(context, "Uint8Array", value)
389
1265
  }
390
1266
  return { kind: "fixedBytes", value }
391
1267
  }
392
1268
  case "interval":
393
- return taggedAtElementDomain(context, field.type.element, value)
394
- }
395
- }
396
-
397
- /**
398
- * Tags one closed-reference literal: the branded id, re-verified against
399
- * the roster (the belt the type level cannot provide against forged
400
- * brands) and tagged u64 — queries cross ids, never handle names.
401
- */
402
- function taggedHandleId(context: string, closed: NonNullable<FieldData["closed"]>, value: unknown): TaggedValue {
403
- if (typeof value !== "bigint") {
404
- throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, value)
405
- }
406
- if (closed.handles[Number(value)] === undefined) {
407
- throw errors.new(
408
- `${context}: closed relation ${closed.name} has no handle with id ${value} (roster holds ${closed.handles.length})`
409
- )
1269
+ return taggedAtElementDomain(context, field.element, value)
410
1270
  }
411
- return { kind: "u64", value }
412
1271
  }
413
1272
 
414
1273
  /**
415
- * Tags one literal in an interval element domain: a bigint tags as the
416
- * element (the membership typing rule's point side), an interval-shaped
417
- * value as the interval (value equality).
418
- */
419
- function taggedAtElementDomain(context: string, element: "u64" | "i64", value: unknown): TaggedValue {
420
- if (typeof value === "bigint") {
421
- if (element === "u64") {
422
- return { kind: "u64", value }
423
- }
424
- return { kind: "i64", value }
425
- }
426
- if (isIntervalShaped(value)) {
427
- if (element === "u64") {
428
- return { kind: "intervalU64", start: value.start, end: value.end }
429
- }
430
- return { kind: "intervalI64", start: value.start, end: value.end }
431
- }
432
- throw literalShapeError(context, "bigint (point) or { start, end } (interval)", value)
433
- }
434
-
435
- /**
436
- * Tags one host literal at a COMPARISON position, where no field position
437
- * directs the type: the sibling term's element domain does — a measure
438
- * sibling is u64, an interval-field sibling contributes its element type
439
- * (so both a point literal in `covers` and a `span` literal in `allen`
440
- * tag correctly), a scalar sibling its own type. At `pointIn` the operand
441
- * order is interval-left, point-right (`ir::CmpOp::PointIn`), so an
442
- * interval-shaped literal beside a scalar element-typed sibling is the
443
- * LEGAL lhs of `covers(span(...), t)` and tags as the interval of the
444
- * sibling's element domain; under every other operator an interval shape
445
- * against a scalar sibling stays refused (the engine's IllegalComparison
446
- * — eq/lt interval-vs-scalar is not a comparison).
1274
+ * Tags one host literal at a COMPARISON or PARAM position, where the
1275
+ * SIBLING anchors the type: a measure sibling is u64, an interval-field
1276
+ * sibling contributes its element domain (so both a point literal in
1277
+ * `covers` and a `span` literal in `allen` tag correctly), a scalar
1278
+ * sibling its own type. At `pointIn` the operand order is interval-left,
1279
+ * point-right (`ir::CmpOp::PointIn`), so an interval-shaped literal
1280
+ * beside a scalar element-typed sibling is the LEGAL lhs of
1281
+ * `covers(span(...), t)` and tags as the interval of the sibling's
1282
+ * element domain; under every other operator an interval shape against a
1283
+ * scalar sibling stays refused (the engine's IllegalComparison — the
1284
+ * bug-hunt fix, preserved op-aware).
447
1285
  */
448
1286
  function taggedCmpLiteral(
449
1287
  context: string,
450
- sibling: FieldData | "measure",
1288
+ sibling: AnyField | "measure",
451
1289
  value: unknown,
452
- op: CmpOpData["kind"]
1290
+ op: CmpKind | "binding"
453
1291
  ): TaggedValue {
454
1292
  if (sibling === "measure") {
455
1293
  if (typeof value !== "bigint") {
@@ -457,11 +1295,16 @@ function taggedCmpLiteral(
457
1295
  }
458
1296
  return { kind: "u64", value }
459
1297
  }
460
- if (sibling.type.kind === "interval") {
461
- return taggedAtElementDomain(context, sibling.type.element, value)
1298
+ if (!("closed" in sibling) && sibling.kind === "interval") {
1299
+ return taggedAtElementDomain(context, sibling.element, value)
462
1300
  }
463
- if (op === "pointIn" && (sibling.type.kind === "u64" || sibling.type.kind === "i64") && isIntervalShaped(value)) {
464
- return taggedAtElementDomain(context, sibling.type.kind, value)
1301
+ if (
1302
+ op === "pointIn" &&
1303
+ !("closed" in sibling) &&
1304
+ (sibling.kind === "u64" || sibling.kind === "i64") &&
1305
+ isIntervalShaped(value)
1306
+ ) {
1307
+ return taggedAtElementDomain(context, sibling.kind, value)
465
1308
  }
466
1309
  return taggedLiteral(context, sibling, value)
467
1310
  }
@@ -470,225 +1313,173 @@ function taggedCmpLiteral(
470
1313
  interface LowerContext {
471
1314
  readonly theory: AnySchema
472
1315
  readonly relationIds: ReadonlyMap<string, number>
473
- readonly predicateIds: ReadonlyMap<PredicateData, number>
474
- readonly registry: QueryRegistry
475
- /** Every dense param id some lowered term referenced — the usage census the dead-declaration refusal reads. */
476
- readonly usedParams: Set<number>
1316
+ readonly recIds: ReadonlyMap<RecData, number>
1317
+ readonly paramIds: ReadonlyMap<string, number>
1318
+ readonly params: ReadonlyMap<string, ParamEntry>
477
1319
  }
478
1320
 
479
1321
  /** One rule's dense variable numbering: first occurrence in written order. */
480
1322
  interface VarIds {
481
- of(variable: AnyVar): number
482
- synthetic(): number
1323
+ of(name: string): number
483
1324
  }
484
1325
 
485
1326
  /** Creates one rule-scoped variable numberer. */
486
- function makeVarIds(registry: QueryRegistry): VarIds {
487
- const assigned = new Map<AnyVar, number>()
488
- const state = { next: 0 }
1327
+ function makeVarIds(): VarIds {
1328
+ const assigned = new Map<string, number>()
489
1329
  return {
490
- of(variable) {
491
- const existing = assigned.get(variable)
1330
+ of(name) {
1331
+ const existing = assigned.get(name)
492
1332
  if (existing !== undefined) {
493
1333
  return existing
494
1334
  }
495
- if (!registry.vars.has(variable)) {
496
- throw errors.new(
497
- `query lowering: the variable declared from ${variable.relation}.${variable.field} belongs to a different query scope`
498
- )
499
- }
500
- const id = state.next
501
- state.next += 1
502
- assigned.set(variable, id)
503
- return id
504
- },
505
- synthetic() {
506
- const id = state.next
507
- state.next += 1
1335
+ const id = assigned.size
1336
+ assigned.set(name, id)
508
1337
  return id
509
1338
  }
510
1339
  }
511
1340
  }
512
1341
 
513
- /** Resolves a param term to its dense positional id, recording the use. */
514
- function paramIdOf(ctx: LowerContext, value: AnyTerm, name: string): number {
515
- const index = ctx.registry.paramIndex.get(value)
516
- if (index === undefined) {
517
- throw errors.new(`query lowering: param ${name} belongs to a different query scope`)
1342
+ /** Resolves a param name to its dense positional id. */
1343
+ function paramIdOf(ctx: LowerContext, name: string): number {
1344
+ const id = ctx.paramIds.get(name)
1345
+ if (id === undefined) {
1346
+ throw errors.new(`query lowering: param ${name} is not in the query's registry`)
518
1347
  }
519
- ctx.usedParams.add(index)
520
- return index
1348
+ return id
521
1349
  }
522
1350
 
523
- /** Lowers one atom (either polarity) and appends any `oneOf` disjunctions. */
524
- function lowerAtom(
525
- ctx: LowerContext,
526
- atom: MatchAtom<ParamsRecord>,
527
- ids: VarIds,
528
- extraConditions: ConditionTreeIr[]
529
- ): AtomIr {
530
- const source = lowerSource(ctx, atom.source)
1351
+ /** Lowers one EDB atom (either polarity). */
1352
+ function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
1353
+ const member = ctx.theory.relations[atom.relation.name]
1354
+ if (member !== atom.relation) {
1355
+ throw errors.new(
1356
+ `query lowering: relation ${atom.relation.name} is not the relation value schema ${ctx.theory.name} declares`
1357
+ )
1358
+ }
1359
+ const relationId = ctx.relationIds.get(atom.relation.name)
1360
+ if (relationId === undefined) {
1361
+ throw errors.new(`query lowering: relation ${atom.relation.name} has no ordinal`)
1362
+ }
531
1363
  const bindings: Array<readonly [number, TermIr]> = atom.bindings.map(function lowerBinding(binding) {
532
- return [
533
- fieldOrdinal(atom.source, binding),
534
- lowerBindingTerm(ctx, atom.source, binding, ids, extraConditions)
535
- ] as const
1364
+ const ordinal = atom.relation.data.fields.findIndex(function byName(candidate) {
1365
+ return candidate.name === binding.field
1366
+ })
1367
+ if (ordinal < 0) {
1368
+ throw errors.new(`query lowering: relation ${atom.relation.name} has no field ${binding.field}`)
1369
+ }
1370
+ return [ordinal, lowerBindingTerm(ctx, `${atom.relation.name}.${binding.field}`, binding, ids)] as const
536
1371
  })
537
- return { source, bindings }
1372
+ return { source: { kind: "edb", relation: relationId }, bindings }
538
1373
  }
539
1374
 
540
- /** Lowers an atom source to its numeric id, verifying scope membership. */
541
- function lowerSource(ctx: LowerContext, source: AtomSourceData): AtomIr["source"] {
542
- if (source.kind === "relation") {
543
- const member = ctx.theory.relations[source.relation.name]
544
- if (member !== source.relation) {
545
- throw errors.new(
546
- `query lowering: relation ${source.relation.name} is not the relation value schema ${ctx.theory.name} declares`
547
- )
548
- }
549
- const id = ctx.relationIds.get(source.relation.name)
550
- if (id === undefined) {
551
- throw errors.new(`query lowering: relation ${source.relation.name} has no ordinal`)
552
- }
553
- return { kind: "edb", relation: id }
554
- }
555
- const pred = ctx.predicateIds.get(source.pred)
556
- if (pred === undefined) {
557
- throw errors.new(`query lowering: predicate ${source.pred.name} was declared in a different query scope`)
1375
+ /** Lowers one binding term. */
1376
+ function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEntry, ids: VarIds): TermIr {
1377
+ const bound = binding.term
1378
+ switch (bound.kind) {
1379
+ case "var":
1380
+ return { kind: "var", var: ids.of(bound.name) }
1381
+ case "param":
1382
+ return { kind: "param", param: paramIdOf(ctx, bound.name) }
1383
+ case "setParam":
1384
+ return { kind: "paramSet", param: paramIdOf(ctx, bound.name) }
1385
+ case "literal":
1386
+ return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) }
558
1387
  }
559
- return { kind: "idb", pred }
560
1388
  }
561
1389
 
562
- /** A binding's field ordinal: declaration index (relations) or head position (predicates). */
563
- function fieldOrdinal(source: AtomSourceData, binding: BindingEntry): number {
564
- if (source.kind === "relation") {
565
- const ordinal = source.relation.data.fields.findIndex(function byName(candidate) {
566
- return candidate.name === binding.field
567
- })
568
- if (ordinal < 0) {
569
- throw errors.new(`query lowering: relation ${source.relation.name} has no field ${binding.field}`)
570
- }
571
- return ordinal
1390
+ /** Lowers one idb atom: positional head bindings, `FieldId(i)` = head position i. */
1391
+ function lowerIdbAtom(ctx: LowerContext, rec: RecData, vars: readonly string[], ids: VarIds): AtomIr {
1392
+ const pred = ctx.recIds.get(rec)
1393
+ if (pred === undefined) {
1394
+ throw errors.new(`query lowering: rec ${rec.name} was declared by a different program`)
572
1395
  }
573
- const ordinal = source.pred.columns.findIndex(function byName(candidate) {
574
- return candidate.name === binding.field
575
- })
576
- if (ordinal < 0) {
577
- throw errors.new(`query lowering: predicate ${source.pred.name} has no column ${binding.field}`)
1396
+ const arity = rec.rules[0]?.select.length
1397
+ if (arity !== undefined && vars.length !== arity) {
1398
+ throw errors.new(`query lowering: idb ${rec.name} takes ${arity} positions, got ${vars.length}`)
578
1399
  }
579
- return ordinal
1400
+ const bindings: Array<readonly [number, TermIr]> = vars.map(function lowerPosition(name, position) {
1401
+ return [position, { kind: "var", var: ids.of(name) } as const] as const
1402
+ })
1403
+ return { source: { kind: "idb", pred }, bindings }
580
1404
  }
581
1405
 
582
- /** Lowers one binding term; an `oneOf` mints a fresh variable + disjunction. */
583
- function lowerBindingTerm(
1406
+ /** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
1407
+ function lowerCmpTerm(
584
1408
  ctx: LowerContext,
585
- source: AtomSourceData,
586
- binding: BindingEntry,
1409
+ rule: RuleData,
1410
+ side: CmpTermData,
1411
+ sibling: CmpTermData,
587
1412
  ids: VarIds,
588
- extraConditions: ConditionTreeIr[]
1413
+ op: CmpKind
589
1414
  ): TermIr {
590
- const context = `${sourceName(source)}.${binding.field}`
591
- const bound = binding.term
592
- if (bound.kind === "term") {
593
- const value = bound.value
594
- switch (value[term]) {
595
- case "var":
596
- return { kind: "var", var: ids.of(value) }
597
- case "param":
598
- return { kind: "param", param: paramIdOf(ctx, value, value.name) }
599
- case "paramSet":
600
- return { kind: "paramSet", param: paramIdOf(ctx, value, value.name) }
601
- case "maskParam":
602
- throw errors.new(`${context}: an Allen-mask param is not a field-typed binding`)
603
- }
604
- }
605
- if (bound.kind === "oneOf") {
606
- const minted = ids.synthetic()
607
- const leaves: ConditionTreeIr[] = bound.values.map(function equalityLeaf(candidate) {
608
- return {
609
- kind: "leaf",
610
- cmp: {
611
- op: { kind: "eq" },
612
- lhs: { kind: "var", var: minted },
613
- rhs: { kind: "literal", value: taggedLiteral(context, binding.data, candidate) }
614
- }
615
- }
616
- })
617
- extraConditions.push({ kind: "or", children: leaves })
618
- return { kind: "var", var: minted }
619
- }
620
- return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) }
621
- }
622
-
623
- /** Lowers one comparison side; literals tag by the sibling's element domain (op-aware at `pointIn`). */
624
- function lowerCmpTerm(ctx: LowerContext, side: CmpTerm, sibling: CmpTerm, ids: VarIds, op: CmpOpData["kind"]): TermIr {
625
- if (side.kind === "term") {
626
- const value = side.value
627
- switch (value[term]) {
628
- case "var":
629
- return { kind: "var", var: ids.of(value) }
630
- case "param":
631
- return { kind: "param", param: paramIdOf(ctx, value, value.name) }
632
- case "paramSet":
633
- return { kind: "paramSet", param: paramIdOf(ctx, value, value.name) }
634
- case "maskParam":
1415
+ switch (side.kind) {
1416
+ case "var":
1417
+ return { kind: "var", var: ids.of(side.name) }
1418
+ case "param":
1419
+ return { kind: "param", param: paramIdOf(ctx, side.name) }
1420
+ case "setParam":
1421
+ return { kind: "paramSet", param: paramIdOf(ctx, side.name) }
1422
+ case "measure":
1423
+ return { kind: "measure", var: ids.of(side.name) }
1424
+ case "literal": {
1425
+ const anchor = cmpAnchorOf(ctx, rule, sibling)
1426
+ if (anchor === undefined) {
635
1427
  throw errors.new(
636
- "query lowering: an Allen-mask param is not a comparison term masks live in allen()'s mask position"
1428
+ "query lowering: a comparison literal needs a bound-variable, measure, or anchored-param sibling to type it"
637
1429
  )
1430
+ }
1431
+ return { kind: "literal", value: taggedCmpLiteral("comparison literal", anchor, side.value, op) }
638
1432
  }
639
1433
  }
640
- if (side.kind === "measure") {
641
- return { kind: "measure", var: ids.of(side.over) }
642
- }
643
- if (sibling.kind === "term") {
644
- const value = sibling.value
645
- if (value[term] === "maskParam") {
646
- throw errors.new("query lowering: an Allen-mask param cannot type a literal side")
647
- }
648
- return {
649
- kind: "literal",
650
- value: taggedCmpLiteral("comparison literal", value.data, side.value, op)
651
- }
1434
+ }
1435
+
1436
+ /** Resolves the anchor a comparison literal tags by: the sibling's field, the measure, or an anchored param. */
1437
+ function cmpAnchorOf(ctx: LowerContext, rule: RuleData, sibling: CmpTermData): AnyField | "measure" | undefined {
1438
+ if (sibling.kind === "var") {
1439
+ return rule.varFields[sibling.name]
652
1440
  }
653
1441
  if (sibling.kind === "measure") {
654
- return {
655
- kind: "literal",
656
- value: taggedCmpLiteral("comparison literal", "measure", side.value, op)
657
- }
1442
+ return "measure"
1443
+ }
1444
+ if (sibling.kind === "param" || sibling.kind === "setParam") {
1445
+ return ctx.params.get(sibling.name)?.anchor
658
1446
  }
659
- throw errors.new("query lowering: a comparison without a variable or parameter side is constant-valued")
1447
+ return undefined
660
1448
  }
661
1449
 
662
1450
  /** Lowers one comparison. */
663
- function lowerComparison(ctx: LowerContext, cmp: ComparisonItem<ParamsRecord>, ids: VarIds): ComparisonIr {
664
- const op = cmp.op
665
- if (op.kind === "allen") {
1451
+ function lowerComparison(ctx: LowerContext, rule: RuleData, cmp: CmpData, ids: VarIds): ComparisonIr {
1452
+ if (cmp.op === "allen") {
1453
+ const maskData = cmp.mask
1454
+ if (maskData === undefined) {
1455
+ throw errors.new("query lowering: an allen comparison lost its mask")
1456
+ }
666
1457
  const mask =
667
- op.mask.kind === "literal"
668
- ? ({ kind: "literal", mask: op.mask.mask } as const)
669
- : ({ kind: "param", param: paramIdOf(ctx, op.mask.param, op.mask.param.name) } as const)
1458
+ maskData.kind === "literal"
1459
+ ? { kind: "literal" as const, mask: maskData.mask }
1460
+ : { kind: "param" as const, param: paramIdOf(ctx, maskData.name) }
670
1461
  return {
671
1462
  op: { kind: "allen", mask },
672
- lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
673
- rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
1463
+ lhs: lowerCmpTerm(ctx, rule, cmp.lhs, cmp.rhs, ids, "allen"),
1464
+ rhs: lowerCmpTerm(ctx, rule, cmp.rhs, cmp.lhs, ids, "allen")
674
1465
  }
675
1466
  }
676
1467
  return {
677
- op: { kind: op.kind },
678
- lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, op.kind),
679
- rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, op.kind)
1468
+ op: { kind: cmp.op },
1469
+ lhs: lowerCmpTerm(ctx, rule, cmp.lhs, cmp.rhs, ids, cmp.op),
1470
+ rhs: lowerCmpTerm(ctx, rule, cmp.rhs, cmp.lhs, ids, cmp.op)
680
1471
  }
681
1472
  }
682
1473
 
683
1474
  /** Lowers one condition node (comparison leaf or and/or tree). */
684
- function lowerCondition(ctx: LowerContext, condition: AnyCondition, ids: VarIds): ConditionTreeIr {
685
- if (condition.item === "cmp") {
686
- return { kind: "leaf", cmp: lowerComparison(ctx, condition, ids) }
1475
+ function lowerCondition(ctx: LowerContext, rule: RuleData, cond: CondData, ids: VarIds): ConditionTreeIr {
1476
+ if (cond.kind === "cmp") {
1477
+ return { kind: "leaf", cmp: lowerComparison(ctx, rule, cond, ids) }
687
1478
  }
688
1479
  return {
689
- kind: condition.op,
690
- children: condition.children.map(function lowerChild(child) {
691
- return lowerCondition(ctx, child, ids)
1480
+ kind: cond.op,
1481
+ children: cond.children.map(function lowerChild(child) {
1482
+ return lowerCondition(ctx, rule, child, ids)
692
1483
  })
693
1484
  }
694
1485
  }
@@ -701,41 +1492,36 @@ function lowerFind(entry: SelectEntryData, ids: VarIds): FindTermIr {
701
1492
  if (entry.kind === "measure") {
702
1493
  return { kind: "measure", var: ids.of(entry.over) }
703
1494
  }
704
- const aggregate = entry.aggregate
705
- switch (aggregate.op) {
1495
+ const agg = entry.agg
1496
+ switch (agg.op) {
706
1497
  case "count":
707
1498
  return { kind: "aggregate", op: { kind: "count" } }
708
1499
  case "countDistinct":
709
- return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(aggregate.over) }
1500
+ return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) }
710
1501
  case "fold": {
711
- const over = aggregate.over
712
- if (isTerm(over)) {
713
- return { kind: "aggregate", op: { kind: aggregate.fold }, over: ids.of(over) }
1502
+ if (typeof agg.over === "string") {
1503
+ return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) }
714
1504
  }
715
- return { kind: "aggregateMeasure", op: { kind: aggregate.fold }, over: ids.of(over.measure) }
1505
+ return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) }
716
1506
  }
717
1507
  case "arg":
718
- return {
719
- kind: "aggregate",
720
- op: { kind: aggregate.direction, key: ids.of(aggregate.key) },
721
- over: ids.of(aggregate.over)
722
- }
1508
+ return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) }
723
1509
  case "pack":
724
- return { kind: "aggregate", op: { kind: "pack" }, over: ids.of(aggregate.over) }
1510
+ return { kind: "aggregate", op: { kind: "pack" }, over: ids.of(agg.over) }
725
1511
  }
726
1512
  }
727
1513
 
728
1514
  /** One aggregate's var-free head-op kind (`AggOp::head_op`). */
729
- function headOpOf(aggregate: AggregateData): HeadOpIr {
730
- switch (aggregate.op) {
1515
+ function headOpOf(agg: AggData): HeadOpIr {
1516
+ switch (agg.op) {
731
1517
  case "count":
732
1518
  return "count"
733
1519
  case "countDistinct":
734
1520
  return "countDistinct"
735
1521
  case "fold":
736
- return aggregate.fold
1522
+ return agg.fold
737
1523
  case "arg":
738
- return aggregate.direction
1524
+ return agg.direction
739
1525
  case "pack":
740
1526
  return "pack"
741
1527
  }
@@ -747,43 +1533,53 @@ function headTermOf(column: SelectColumn): HeadTermIr {
747
1533
  if (entry.kind === "var" || entry.kind === "measure") {
748
1534
  return { kind: "var" }
749
1535
  }
750
- return { kind: "aggregate", op: headOpOf(entry.aggregate) }
1536
+ return { kind: "aggregate", op: headOpOf(entry.agg) }
751
1537
  }
752
1538
 
753
- /** Lowers one rule: body walked in written order, finds supplied per shape. */
754
- function lowerRule(ctx: LowerContext, body: readonly AnyBodyItem[], finds: (ids: VarIds) => FindTermIr[]): RuleIr {
755
- const ids = makeVarIds(ctx.registry)
1539
+ /** Lowers one rule: body walked in written order (var ids by first occurrence), finds last. */
1540
+ function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
1541
+ const ids = makeVarIds()
756
1542
  const atoms: AtomIr[] = []
757
1543
  const negated: AtomIr[] = []
758
1544
  const conditions: ConditionTreeIr[] = []
759
- const extraConditions: ConditionTreeIr[] = []
760
- for (const item of body) {
761
- if (item.item === "atom") {
762
- const lowered = lowerAtom(ctx, item, ids, extraConditions)
763
- if (item.negated) {
764
- negated.push(lowered)
765
- } else {
766
- atoms.push(lowered)
1545
+ for (const item of rule.items) {
1546
+ switch (item.kind) {
1547
+ case "atom": {
1548
+ atoms.push(lowerAtom(ctx, item.atom, ids))
1549
+ break
1550
+ }
1551
+ case "negated": {
1552
+ negated.push(lowerAtom(ctx, item.atom, ids))
1553
+ break
1554
+ }
1555
+ case "idb": {
1556
+ atoms.push(lowerIdbAtom(ctx, item.rec, item.vars, ids))
1557
+ break
1558
+ }
1559
+ case "cond": {
1560
+ conditions.push(lowerCondition(ctx, rule, item.cond, ids))
1561
+ break
767
1562
  }
768
- continue
769
1563
  }
770
- conditions.push(lowerCondition(ctx, item, ids))
771
1564
  }
772
1565
  return {
773
- finds: finds(ids),
1566
+ finds: rule.select.map(function findOf(column) {
1567
+ return lowerFind(column.entry, ids)
1568
+ }),
774
1569
  atoms,
775
1570
  negated,
776
- conditions: [...conditions, ...extraConditions]
1571
+ conditions
777
1572
  }
778
1573
  }
779
1574
 
780
1575
  /**
781
1576
  * Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
782
- * declared predicates in declaration order (`PredId` = index), the output
783
- * predicate (built from `rules` + `select`) appended last. Relations lower
784
- * by declaration ordinal, the law the engine's own manifest pins;
785
- * `db.prepare` re-verifies the alignment against the live manifest before
786
- * sending.
1577
+ * recs in declaration order (`PredId` = index), the output predicate
1578
+ * (rules + head) appended last. Relations lower by declaration ordinal,
1579
+ * the law the engine's own manifest pins; `db.prepare` re-verifies the
1580
+ * alignment against the live manifest before sending. Every registered
1581
+ * param must carry a field anchor by now — an unanchorable param (its
1582
+ * every use beside a literal) is refused here, naming it.
787
1583
  */
788
1584
  function lowerQuery(q: AnyQuery): ProgramIr {
789
1585
  const theory = q.schema
@@ -791,65 +1587,68 @@ function lowerQuery(q: AnyQuery): ProgramIr {
791
1587
  Object.keys(theory.relations).forEach(function assignOrdinal(name, index) {
792
1588
  relationIds.set(name, index)
793
1589
  })
794
- const registry = q.data.registry
795
- const predicateIds = new Map<PredicateData, number>()
796
- registry.predicates.forEach(function assignPredId(pred, index) {
797
- predicateIds.set(pred, index)
1590
+ const recIds = new Map<RecData, number>()
1591
+ q.data.recs.forEach(function assignPredId(rec, index) {
1592
+ recIds.set(rec, index)
798
1593
  })
799
- const ctx: LowerContext = { theory, relationIds, predicateIds, registry, usedParams: new Set() }
800
- const predicates: PredicateDefIr[] = registry.predicates.map(function lowerPredicate(pred) {
1594
+ const paramIds = new Map<string, number>()
1595
+ const params = new Map<string, ParamEntry>()
1596
+ q.data.params.forEach(function assignParamId(entry, index) {
1597
+ if (entry.anchor === undefined && entry.shape !== "mask") {
1598
+ throw errors.new(
1599
+ `query param ${entry.name} has no field-anchored use — bind it in an atom or compare it against a bound variable`
1600
+ )
1601
+ }
1602
+ paramIds.set(entry.name, index)
1603
+ params.set(entry.name, entry)
1604
+ })
1605
+ const ctx: LowerContext = { theory, relationIds, recIds, paramIds, params }
1606
+ const predicates: PredicateDefIr[] = q.data.recs.map(function lowerRec(rec) {
1607
+ const head = rec.rules[0]
1608
+ if (head === undefined) {
1609
+ throw errors.new(`query lowering: rec ${rec.name} has no rules`)
1610
+ }
801
1611
  return {
802
- head: pred.columns.map(function boundHead(): HeadTermIr {
803
- return { kind: "var" }
804
- }),
805
- rules: pred.rules.map(function lowerClause(rule) {
806
- return lowerRule(ctx, rule.body, function clauseFinds(ids) {
807
- return rule.finds.map(function findVar(variable): FindTermIr {
808
- return { kind: "var", var: ids.of(variable) }
809
- })
810
- })
1612
+ head: head.select.map(headTermOf),
1613
+ rules: rec.rules.map(function lowerRecRule(rule) {
1614
+ return lowerRule(ctx, rule)
811
1615
  })
812
1616
  }
813
1617
  })
814
1618
  predicates.push({
815
1619
  head: q.data.select.map(headTermOf),
816
- rules: q.data.rules.map(function lowerOutputRule(body) {
817
- return lowerRule(ctx, body, function outputFinds(ids) {
818
- return q.data.select.map(function findOf(column) {
819
- return lowerFind(column.entry, ids)
820
- })
821
- })
1620
+ rules: q.data.rules.map(function lowerOutputRule(rule) {
1621
+ return lowerRule(ctx, rule)
822
1622
  })
823
1623
  })
824
- /**
825
- * The dead-declaration refusal: a declared param no rule reached is
826
- * unexecutable BOTH ways — its inferred `Params` contribution is nothing
827
- * (params ride item phantoms), yet the wire marshal is the full registry
828
- * in declaration order, while the ENGINE's arity is usage-derived. The
829
- * contradiction is refused here, the earliest seam that knows usage, so
830
- * every query that lowers has registry == used set (and the dense-id
831
- * hole a skipped middle declaration would open never exists).
832
- */
833
- registry.params.forEach(function assertUsed(entry, index) {
834
- if (!ctx.usedParams.has(index)) {
835
- throw errors.new(
836
- `query declares param ${entry.name} but no rule uses it — remove the declaration or reference it`
837
- )
838
- }
839
- })
840
- return { predicates, output: registry.predicates.length }
1624
+ return { predicates, output: q.data.recs.length }
841
1625
  }
842
1626
 
843
1627
  export type {
844
1628
  AnyQuery,
845
- BuildParams,
1629
+ AnyRuleValue,
1630
+ HeadFieldsOf,
1631
+ HeadOf,
1632
+ HeadShape,
1633
+ OutputRuleChain,
1634
+ OutputRuleScope,
1635
+ ParamsOf,
1636
+ ProgramState,
846
1637
  Query,
847
- QueryBuild,
848
1638
  QueryData,
849
1639
  QueryParams,
1640
+ QueryRelation,
850
1641
  QueryRow,
851
- Scope,
852
- SelectColumn,
853
- SelectEntryData
1642
+ QueryRuleChain,
1643
+ QueryRuleScope,
1644
+ QueryStart,
1645
+ RawChain,
1646
+ RawScope,
1647
+ RecRef,
1648
+ RecRuleChain,
1649
+ RecRuleScope,
1650
+ RowOf,
1651
+ RuleValue,
1652
+ TermOps
854
1653
  }
855
- export { lowerQuery, query, taggedLiteral }
1654
+ export { lowerQuery, makeOutputRuleScope, makeQuery, makeRawScope, query, taggedCmpLiteral, taggedLiteral }