@bjornpagen/bumbledb 0.10.0 → 0.11.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 (69) hide show
  1. package/COOKBOOK.md +155 -136
  2. package/README.md +3 -7
  3. package/dist/db.d.ts +77 -109
  4. package/dist/db.d.ts.map +1 -1
  5. package/dist/db.js +121 -339
  6. package/dist/db.js.map +1 -1
  7. package/dist/index.d.ts +11 -16
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +7 -10
  10. package/dist/index.js.map +1 -1
  11. package/dist/lower.d.ts.map +1 -1
  12. package/dist/lower.js +8 -1
  13. package/dist/lower.js.map +1 -1
  14. package/dist/native.d.ts +69 -50
  15. package/dist/native.d.ts.map +1 -1
  16. package/dist/native.js.map +1 -1
  17. package/dist/query/atom.d.ts +94 -112
  18. package/dist/query/atom.d.ts.map +1 -1
  19. package/dist/query/atom.js +8 -17
  20. package/dist/query/atom.js.map +1 -1
  21. package/dist/query/find.d.ts +18 -35
  22. package/dist/query/find.d.ts.map +1 -1
  23. package/dist/query/find.js +13 -32
  24. package/dist/query/find.js.map +1 -1
  25. package/dist/query/lower.d.ts +113 -122
  26. package/dist/query/lower.d.ts.map +1 -1
  27. package/dist/query/lower.js +335 -256
  28. package/dist/query/lower.js.map +1 -1
  29. package/dist/query/parse-ir.d.ts +12 -0
  30. package/dist/query/parse-ir.d.ts.map +1 -0
  31. package/dist/query/parse-ir.js +71 -0
  32. package/dist/query/parse-ir.js.map +1 -0
  33. package/dist/query/run.d.ts +2 -2
  34. package/dist/query/run.d.ts.map +1 -1
  35. package/dist/query/run.js +2 -13
  36. package/dist/query/run.js.map +1 -1
  37. package/dist/query/scope.d.ts +4 -16
  38. package/dist/query/scope.d.ts.map +1 -1
  39. package/dist/query/scope.js +1 -6
  40. package/dist/query/scope.js.map +1 -1
  41. package/dist/schema.js +2 -2
  42. package/dist/schema.js.map +1 -1
  43. package/dist/statements.d.ts +8 -4
  44. package/dist/statements.d.ts.map +1 -1
  45. package/dist/statements.js +7 -9
  46. package/dist/statements.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/db.ts +182 -443
  49. package/src/index.ts +9 -23
  50. package/src/lower.ts +8 -1
  51. package/src/native.ts +69 -42
  52. package/src/query/atom.ts +105 -138
  53. package/src/query/find.ts +39 -80
  54. package/src/query/lower.ts +577 -427
  55. package/src/query/parse-ir.ts +82 -0
  56. package/src/query/run.ts +2 -14
  57. package/src/query/scope.ts +3 -21
  58. package/src/schema.ts +2 -2
  59. package/src/statements.ts +17 -13
  60. package/dist/order.d.ts +0 -87
  61. package/dist/order.d.ts.map +0 -1
  62. package/dist/order.js +0 -153
  63. package/dist/order.js.map +0 -1
  64. package/dist/query/predicate.d.ts +0 -91
  65. package/dist/query/predicate.d.ts.map +0 -1
  66. package/dist/query/predicate.js +0 -156
  67. package/dist/query/predicate.js.map +0 -1
  68. package/src/order.ts +0 -234
  69. package/src/query/predicate.ts +0 -269
@@ -18,11 +18,8 @@
18
18
  * `Params` inferred to be EXACTLY the params the rules use (params are typed
19
19
  * BY USE; a param no rule uses never registers). Variable IDENTITY is the
20
20
  * object reference: reusing one value across binding positions IS the join,
21
- * and a name-collision join is unrepresentable. Each binding position is
22
- * judged against the variable's MINT slot — and because {@link JoinOk} is an
23
- * equality, that alone makes every cross-binding join transitively
24
- * class-equal. Lowering is a pure function of the query value down to the
25
- * bridge's `ProgramIr` (`bumbledb/crates/bumbledb/src/ir.rs`): relations by
21
+ * lowering is a pure function of the query value down to the
22
+ * bridge's `QueryIr` (`bumbledb/crates/bumbledb/src/ir.rs`): relations by
26
23
  * declaration ordinal, variables by dense per-rule first-occurrence ids
27
24
  * (keyed on the object REFERENCE — the discipline is unchanged, only the map
28
25
  * key moved from name to reference), params by first-use order. Lowering is
@@ -30,7 +27,7 @@
30
27
  * two identically-written queries (fresh mints each) lower identically.
31
28
  * Construction validates negation safety and boundness (typed by the var's
32
29
  * label — object identity is invisible to the type tier, so these are
33
- * construction-time walls); everything else (strata, types, aggregate
30
+ * construction-time walls); everything else (types, aggregate
34
31
  * rosters, rule caps) is the ENGINE's judge, surfacing at prepare.
35
32
  */
36
33
 
@@ -46,8 +43,7 @@ import type {
46
43
  FindTermIr,
47
44
  HeadOpIr,
48
45
  HeadTermIr,
49
- PredicateDefIr,
50
- ProgramIr,
46
+ ParsedQuery,
51
47
  QueryParam,
52
48
  RuleIr,
53
49
  TaggedValue,
@@ -66,36 +62,30 @@ import type {
66
62
  CmpTermData,
67
63
  CondData,
68
64
  CondParamsShape,
65
+ DerivedTable,
69
66
  FindColumn,
70
67
  FindEntryData,
71
- MaskData,
68
+ InteriorData,
72
69
  MatchFields,
73
70
  MatchOwner,
74
71
  MatchShape,
75
72
  ParamUse,
76
73
  RecData,
74
+ RecHandle,
75
+ RecHead,
77
76
  RuleData,
78
77
  RuleItem
79
78
  } from "#query/atom.ts"
80
79
  import { allen, and, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts"
81
80
  import type { CheckFind, CheckRecFind, FindShape, HeadRecordOf, RowOfFind } from "#query/find.ts"
82
- import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/find.ts"
83
- import type {
84
- AnyVar,
85
- ClassedField,
86
- Flatten,
87
- InferredOf,
88
- JoinOk,
89
- MintSlotOf,
90
- ParamEntry,
91
- ParamsRecord
92
- } from "#query/scope.ts"
81
+ import { count, max, min, pack, sum } from "#query/find.ts"
82
+ import { parseQueryIr } from "#query/parse-ir.ts"
83
+ import type { AnyVar, ClassedField, Flatten, InferredOf, ParamEntry, ParamsRecord, ShapeOf } from "#query/scope.ts"
93
84
  import {
94
85
  fieldJoins,
95
86
  inferred,
96
87
  isTerm,
97
88
  makeDuration,
98
- makeMaskParam,
99
89
  makeParam,
100
90
  makeSetParam,
101
91
  renderFieldKind,
@@ -117,16 +107,16 @@ type ParamsOf<T> = InferredOf<T> extends { readonly params: infer P extends Para
117
107
  type RowOf<T> = InferredOf<T> extends { readonly row: infer R } ? R : never
118
108
 
119
109
  /**
120
- * A recursive predicate's HEAD signature as classed slots, keyed by column
121
- * name — carried on the rec reference so an `idb` join can be judged against
122
- * it; `undefined` on values that carry no head.
110
+ * A derived table's HEAD signature as classed slots, keyed by column
111
+ * name; `undefined` on values that carry no head.
123
112
  */
124
113
  type HeadShape = Readonly<Record<string, ClassedField>> | undefined
125
114
 
126
115
  /**
127
116
  * One finished rule as a plain value: the runtime data plus the inferred
128
- * row/params carrier (and, for a RECURSIVE rule, the head record of classed
129
- * slots `idb` pairs against). `.rule(...)` consumes it.
117
+ * row/params carrier (and, for an interior or recursive rule, the head
118
+ * record of classed slots an `.interior(name)` join pairs against).
119
+ * `.rule(...)` consumes it.
130
120
  */
131
121
  interface RuleValue<Row, P extends ParamsRecord, Head extends HeadShape = undefined> {
132
122
  readonly rule: RuleData
@@ -140,48 +130,34 @@ type AnyRuleValue = RuleValue<unknown, ParamsRecord, HeadShape>
140
130
  type HeadOf<T> =
141
131
  InferredOf<T> extends { readonly head: infer H extends Readonly<Record<string, ClassedField>> } ? H : undefined
142
132
 
133
+ /** One `.interior(name)` position's judgment: a variable. */
134
+ type InteriorBindingOk<V> = V extends AnyVar ? true : false
135
+
143
136
  /**
144
- * A recursive predicate REFERENCE — the shape `idb()` targets carry: the
145
- * name (type-level identity), the runtime data (value identity), the params
146
- * its rules have used, and the head signature its FIRST rule sealed.
137
+ * The validated `.interior(name)` bindings record: every entry must be a
138
+ * variable. Arity and class against the named table's head are
139
+ * construction-time (the name is a string, so the head is not a type-level
140
+ * fact).
147
141
  */
148
- interface RecRef<Name extends string, P extends ParamsRecord, Head extends HeadShape = HeadShape> {
149
- readonly name: Name
150
- readonly data: RecData
151
- readonly [inferred]?: { readonly params: P; readonly head: Head }
142
+ type CheckInteriorBindings<B> = {
143
+ readonly [K in keyof B]: InteriorBindingOk<B[K]> extends true ? B[K] : never
152
144
  }
153
145
 
154
- /** One `idb` position's judgment: a variable class-equal to the head slot when the head is carried. */
155
- type IdbBindingOk<Classes extends SchemaClasses, HeadSlot, V> = V extends AnyVar
156
- ? HeadSlot extends ClassedField
157
- ? JoinOk<HeadSlot, MintSlotOf<Classes, V>> extends true
158
- ? true
159
- : false
160
- : true
161
- : false
146
+ /** One interior-rule builder function. */
147
+ type InteriorBuild<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> = (
148
+ r: InteriorRuleScope<Rels, Classes>
149
+ ) => AnyRuleValue
162
150
 
163
- /**
164
- * The validated `idb` bindings record: when the target carries its head
165
- * (a threaded rec handle), the record's key set must EXACTLY equal the
166
- * head's (a missing or extra key maps every property to `never`) and each
167
- * variable must be class-equal to its head slot — the same wall `JoinOk`
168
- * holds for EDB atoms. An unthreaded handle carries no head; every entry
169
- * must still be a variable, arity/class judged at construction and prepare.
170
- */
171
- type CheckIdbBindings<Classes extends SchemaClasses, Head, B> =
172
- Head extends Readonly<Record<string, ClassedField>>
173
- ? [keyof B] extends [keyof Head]
174
- ? [keyof Head] extends [keyof B]
175
- ? {
176
- readonly [K in keyof B]: K extends keyof Head
177
- ? IdbBindingOk<Classes, Head[K], B[K]> extends true
178
- ? B[K]
179
- : never
180
- : never
181
- }
182
- : { readonly [K in keyof B]: never }
183
- : { readonly [K in keyof B]: never }
184
- : { readonly [K in keyof B]: B[K] extends AnyVar ? B[K] : never }
151
+ /** One rec-arm builder function. */
152
+ type RecBuild<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> = (
153
+ r: RecRuleScope<Rels, Classes>
154
+ ) => AnyRuleValue
155
+
156
+ /** A build function's rule value. */
157
+ type BuiltRule<F> = F extends (r: never) => infer RV ? RV : never
158
+
159
+ /** The intersected params record of a tuple of rule builds. */
160
+ type BuildsParams<Builds extends readonly ((r: never) => AnyRuleValue)[]> = ShapeOf<ParamsOf<BuiltRule<Builds[number]>>>
185
161
 
186
162
  /**
187
163
  * The term/predicate/aggregate constructor vocabulary every rule builder
@@ -193,8 +169,6 @@ interface TermOps {
193
169
  readonly param: typeof makeParam
194
170
  /** Names one ∈-set parameter (the IR's `ParamSet`): bound to a readonly array at execution. */
195
171
  readonly inSet: typeof makeSetParam
196
- /** Names one Allen-mask parameter (`MaskTerm::Param`): a bind-time 13-bit mask number. */
197
- readonly maskParam: typeof makeMaskParam
198
172
  /** The measure of an interval-typed variable: `|[s, e)| = e − s`, u64. */
199
173
  readonly duration: typeof makeDuration
200
174
  readonly eq: typeof eq
@@ -209,12 +183,9 @@ interface TermOps {
209
183
  readonly or: typeof or
210
184
  readonly not: typeof not
211
185
  readonly count: typeof count
212
- readonly countDistinct: typeof countDistinct
213
186
  readonly sum: typeof sum
214
187
  readonly min: typeof min
215
188
  readonly max: typeof max
216
- readonly argMax: typeof argMax
217
- readonly argMin: typeof argMin
218
189
  readonly pack: typeof pack
219
190
  }
220
191
 
@@ -225,6 +196,16 @@ interface QueryRuleScope<Rels extends SchemaRelations, Classes extends SchemaCla
225
196
  relation: R,
226
197
  bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
227
198
  ): QueryRuleChain<Rels, BindParamsShape<MatchFields<R>, B>, Classes>
199
+ /**
200
+ * A rule may START with a finished table: an interior atom is a positive
201
+ * occurrence exactly as the engine represents it, so its variables ground
202
+ * — the identity projection `(c) | reach(c);` is spellable with no
203
+ * re-grounding join over a domain relation.
204
+ */
205
+ interior<const B extends Readonly<Record<string, AnyVar>>>(
206
+ name: string,
207
+ bindings: B & CheckInteriorBindings<B>
208
+ ): QueryRuleChain<Rels, Record<never, never>, Classes>
228
209
  }
229
210
 
230
211
  /** The chain of a plain query rule: more atoms, residual predicates, then the head. */
@@ -242,30 +223,30 @@ interface QueryRuleChain<
242
223
  where<const C extends AnyCond>(
243
224
  cond: CheckCond<Classes, C> & C
244
225
  ): QueryRuleChain<Rels, Flatten<P & CondParamsShape<C>>, Classes>
226
+ /** One interior atom over a finished table (an earlier interior, or the rec). */
227
+ interior<const B extends Readonly<Record<string, AnyVar>>>(
228
+ name: string,
229
+ bindings: B & CheckInteriorBindings<B>
230
+ ): QueryRuleChain<Rels, P, Classes>
245
231
  /** The head projection: a `find` RECORD whose keys name the answer columns. */
246
232
  find<const F extends FindShape>(entries: F & CheckFind<F>): RuleValue<RowOfFind<F>, P>
247
233
  }
248
234
 
249
- /** The rule builder an OUTPUT rule of a `program()` receives: a query rule plus finished-stratum `idb` atoms. */
250
- interface OutputRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> extends TermOps {
235
+ /** The rule builder an `interior("mid", ...)` callback receives. */
236
+ interface InteriorRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses>
237
+ extends TermOps {
251
238
  match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
252
239
  relation: R,
253
240
  bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
254
- ): OutputRuleChain<Rels, BindParamsShape<MatchFields<R>, B>, Classes>
255
- /**
256
- * A rule may START with the finished stratum: an idb atom is a positive
257
- * occurrence exactly as the engine represents it, so its variables ground
258
- * — the identity projection `(c) | reach(c);` is spellable with no
259
- * re-grounding join over a domain relation.
260
- */
261
- idb<Target extends RecRef<string, ParamsRecord>, const B extends Readonly<Record<string, AnyVar>>>(
262
- target: Target,
263
- bindings: B & CheckIdbBindings<Classes, HeadOf<Target>, B>
264
- ): OutputRuleChain<Rels, ParamsOf<Target>, Classes>
241
+ ): InteriorRuleChain<Rels, BindParamsShape<MatchFields<R>, B>, Classes>
242
+ interior<const B extends Readonly<Record<string, AnyVar>>>(
243
+ name: string,
244
+ bindings: B & CheckInteriorBindings<B>
245
+ ): InteriorRuleChain<Rels, Record<never, never>, Classes>
265
246
  }
266
247
 
267
- /** The chain of an output rule: atoms, predicates, `idb` joins over the program's recs, then the head. */
268
- interface OutputRuleChain<
248
+ /** The chain of an interior rule: bound-variable heads only. */
249
+ interface InteriorRuleChain<
269
250
  Rels extends SchemaRelations,
270
251
  P extends ParamsRecord,
271
252
  Classes extends SchemaClasses = SchemaClasses
@@ -273,70 +254,65 @@ interface OutputRuleChain<
273
254
  match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
274
255
  relation: R,
275
256
  bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
276
- ): OutputRuleChain<Rels, Flatten<P & BindParamsShape<MatchFields<R>, B>>, Classes>
257
+ ): InteriorRuleChain<Rels, Flatten<P & BindParamsShape<MatchFields<R>, B>>, Classes>
277
258
  where<const C extends AnyCond>(
278
259
  cond: CheckCond<Classes, C> & C
279
- ): OutputRuleChain<Rels, Flatten<P & CondParamsShape<C>>, Classes>
280
- /**
281
- * One `idb` atom over a FINISHED stratum (any rec of this program): a
282
- * NAMED join against the rec's head — the bindings record's keys are the
283
- * head columns, each bound to a variable positively bound by a relation
284
- * atom of the rule. Threading the rec value the last `.rule(...)` returned
285
- * carries its params into `Params` AND its head signature, so the join is
286
- * key-exact and class-checked against the head at compile time.
287
- */
288
- idb<Target extends RecRef<string, ParamsRecord>, const B extends Readonly<Record<string, AnyVar>>>(
289
- target: Target,
290
- bindings: B & CheckIdbBindings<Classes, HeadOf<Target>, B>
291
- ): OutputRuleChain<Rels, Flatten<P & ParamsOf<Target>>, Classes>
292
- find<const F extends FindShape>(entries: F & CheckFind<F>): RuleValue<RowOfFind<F>, P>
260
+ ): InteriorRuleChain<Rels, Flatten<P & CondParamsShape<C>>, Classes>
261
+ interior<const B extends Readonly<Record<string, AnyVar>>>(
262
+ name: string,
263
+ bindings: B & CheckInteriorBindings<B>
264
+ ): InteriorRuleChain<Rels, P, Classes>
265
+ find<const F extends FindShape>(entries: F & CheckRecFind<F>): RuleValue<RowOfFind<F>, P, HeadRecordOf<Classes, F>>
293
266
  }
294
267
 
295
- /** The rule builder a RECURSIVE rule (`rec.rule(...)`) receives. */
296
- interface RecRuleScope<Rels extends SchemaRelations, Self extends string, Classes extends SchemaClasses = SchemaClasses>
297
- extends TermOps {
268
+ /** The rule builder a `recursive("reach", { base, rec })` arm receives. */
269
+ interface RecRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> extends TermOps {
298
270
  match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
299
271
  relation: R,
300
272
  bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
301
- ): RecRuleChain<Rels, Self, BindParamsShape<MatchFields<R>, B>, Classes>
273
+ ): RecRuleChain<Rels, BindParamsShape<MatchFields<R>, B>, Classes>
274
+ interior<const B extends Readonly<Record<string, AnyVar>>>(
275
+ name: string,
276
+ bindings: B & CheckInteriorBindings<B>
277
+ ): RecRuleChain<Rels, Record<never, never>, Classes>
302
278
  }
303
279
 
304
280
  /**
305
- * The chain of a recursive rule. Its `idb` accepts ONLY the rec itself (the
306
- * self-recursion cut) and its `find` takes bound variables only — aggregates
307
- * and the measure are unrepresentable in a recursive head.
281
+ * The chain of a recursive arm. `.interior("reach", …)` is the self-atom
282
+ * on rec arms (and a prior interior on either list). `find` takes bound
283
+ * variables only — aggregates and the measure are unrepresentable in a
284
+ * recursive head.
308
285
  */
309
286
  interface RecRuleChain<
310
287
  Rels extends SchemaRelations,
311
- Self extends string,
312
288
  P extends ParamsRecord,
313
289
  Classes extends SchemaClasses = SchemaClasses
314
290
  > {
315
291
  match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
316
292
  relation: R,
317
293
  bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
318
- ): RecRuleChain<Rels, Self, Flatten<P & BindParamsShape<MatchFields<R>, B>>, Classes>
294
+ ): RecRuleChain<Rels, Flatten<P & BindParamsShape<MatchFields<R>, B>>, Classes>
319
295
  where<const C extends AnyCond>(
320
296
  cond: CheckCond<Classes, C> & C
321
- ): RecRuleChain<Rels, Self, Flatten<P & CondParamsShape<C>>, Classes>
322
- /** The self-recursive atom: `idb(self, { headKey: boundVar })` — only this rec's own reference is accepted. */
323
- idb<Target extends RecRef<Self, ParamsRecord>, const B extends Readonly<Record<string, AnyVar>>>(
324
- target: Target,
325
- bindings: B & CheckIdbBindings<Classes, HeadOf<Target>, B>
326
- ): RecRuleChain<Rels, Self, P, Classes>
327
- /** The recursive head: a `find` record of bound variables only; the value carries the head's classed slots for `idb` pairing. */
297
+ ): RecRuleChain<Rels, Flatten<P & CondParamsShape<C>>, Classes>
298
+ interior<const B extends Readonly<Record<string, AnyVar>>>(
299
+ name: string,
300
+ bindings: B & CheckInteriorBindings<B>
301
+ ): RecRuleChain<Rels, P, Classes>
328
302
  find<const F extends FindShape>(entries: F & CheckRecFind<F>): RuleValue<RowOfFind<F>, P, HeadRecordOf<Classes, F>>
329
303
  }
330
304
 
331
305
  /** A query's runtime description — everything lowering, the wire marshal, and answer decode read. */
332
306
  interface QueryData {
333
- /** The program's recursive predicates in declaration order (empty for a plain query); `PredId` = index. */
334
- readonly recs: readonly RecData[]
335
- /** The output rules in written order (multiple rules = set union). */
307
+ /** Named interiors in declaration order (DAG). */
308
+ readonly interiors: readonly InteriorData[]
309
+ /** The optional linear rec. */
310
+ readonly rec: RecData | null
311
+ /** The main rules in written order (multiple rules = set union). */
336
312
  readonly rules: readonly RuleData[]
337
313
  /** The head columns (every rule derives the same head; written order = answer column order). */
338
314
  readonly finds: readonly FindColumn[]
339
- /** The registered params in first-use order across the program walk (= dense `ParamId`s). */
315
+ /** The registered params in first-use order across the query walk (= dense `ParamId`s). */
340
316
  readonly params: readonly ParamEntry[]
341
317
  }
342
318
 
@@ -357,6 +333,10 @@ interface Query<
357
333
  rule<RV extends AnyRuleValue>(
358
334
  build: (r: QueryRuleScope<Rels, Classes>) => RV
359
335
  ): Query<Rels, Row | RowOf<RV>, Flatten<Params & ParamsOf<RV>>, Classes>
336
+ /** Construction error: interiors precede main rules. Uncallable after `.rule()`. */
337
+ interior(name: string, ...builds: never[]): never
338
+ /** Construction error: recursive precedes main rules. Uncallable after `.rule()`. */
339
+ recursive(name: string, arms: never): never
360
340
  readonly [inferred]?: { readonly row: Row; readonly params: Params }
361
341
  }
362
342
 
@@ -372,18 +352,43 @@ type QueryRow<Q extends AnyQuery> = RowOf<Q>
372
352
  /** Extracts a query value's inferred execute-params type. */
373
353
  type QueryParams<Q extends AnyQuery> = ParamsOf<Q>
374
354
 
375
- /** The entry value of `query(S)`: the first `.rule` mints the query. */
376
- interface QueryStart<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> {
355
+ /**
356
+ * The entry value of `query(S)`: interiors, then optional recursive, then
357
+ * the first `.rule` mints the query. `interior` / `recursive` exist only
358
+ * while `Rec` is `null`; `.recursive()` moves the type parameter.
359
+ */
360
+ type QueryStart<
361
+ Rels extends SchemaRelations,
362
+ Classes extends SchemaClasses = SchemaClasses,
363
+ P extends ParamsRecord = Record<never, never>,
364
+ Rec extends RecData | null = null
365
+ > = {
377
366
  rule<RV extends AnyRuleValue>(
378
367
  build: (r: QueryRuleScope<Rels, Classes>) => RV
379
- ): Query<Rels, RowOf<RV>, ParamsOf<RV>, Classes>
380
- }
368
+ ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes>
369
+ } & (Rec extends null
370
+ ? {
371
+ interior<const Builds extends readonly InteriorBuild<Rels, Classes>[]>(
372
+ name: string,
373
+ ...builds: Builds
374
+ ): QueryStart<Rels, Classes, Flatten<P & BuildsParams<Builds>>, null>
375
+ recursive<
376
+ const Base extends readonly RecBuild<Rels, Classes>[],
377
+ const Step extends readonly RecBuild<Rels, Classes>[]
378
+ >(
379
+ name: string,
380
+ arms: { readonly base: Base; readonly rec: Step }
381
+ ): QueryStart<Rels, Classes, Flatten<P & BuildsParams<Base> & BuildsParams<Step>>, RecData>
382
+ }
383
+ : {
384
+ interior: never
385
+ recursive: never
386
+ })
381
387
 
382
388
  /** The frozen constructor vocabulary every rule builder spreads. */
383
389
  const termOps: TermOps = Object.freeze({
384
390
  param: makeParam,
385
391
  inSet: makeSetParam,
386
- maskParam: makeMaskParam,
387
392
  duration: makeDuration,
388
393
  eq,
389
394
  ne,
@@ -397,12 +402,9 @@ const termOps: TermOps = Object.freeze({
397
402
  or,
398
403
  not,
399
404
  count,
400
- countDistinct,
401
405
  sum,
402
406
  min,
403
407
  max,
404
- argMax,
405
- argMin,
406
408
  pack
407
409
  })
408
410
 
@@ -558,10 +560,6 @@ function resolveBindings(
558
560
  )
559
561
  break
560
562
  }
561
- case "maskParam":
562
- throw errors.new(
563
- `${label}.${fieldName}: an Allen-mask param is not a field-typed value — masks live in allen() conditions only`
564
- )
565
563
  case "duration":
566
564
  throw errors.new(
567
565
  `${label}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and find entries`
@@ -607,7 +605,7 @@ function advanceMatch(
607
605
  }
608
606
 
609
607
  /** Resolves one comparison side to its runtime term (variables and the measure ride by reference). */
610
- function cmpTermDataOf(op: string, value: unknown): CmpTermData {
608
+ function cmpTermDataOf(value: unknown): CmpTermData {
611
609
  if (isTerm(value)) {
612
610
  switch (value[term]) {
613
611
  case "var":
@@ -618,8 +616,6 @@ function cmpTermDataOf(op: string, value: unknown): CmpTermData {
618
616
  return Object.freeze({ kind: "setParam" as const, name: value.name })
619
617
  case "duration":
620
618
  return Object.freeze({ kind: "measure" as const, ref: value.over })
621
- case "maskParam":
622
- throw errors.new(`${op}: an Allen-mask param is not a comparison term — masks live in allen()'s mask position`)
623
619
  }
624
620
  }
625
621
  return Object.freeze({ kind: "literal" as const, value })
@@ -656,31 +652,23 @@ function sideUses(op: CmpKind, side: CmpTermData, sibling: CmpTermData, uses: Pa
656
652
  /** Lowers one condition VALUE to its runtime data, recording param uses. */
657
653
  function condDataOf(cond: AnyCond, uses: ParamUse[]): CondData {
658
654
  if (cond.cond === "cmp") {
659
- const lhs = cmpTermDataOf(cond.op, cond.lhs)
660
- const rhs = cmpTermDataOf(cond.op, cond.rhs)
655
+ const lhs = cmpTermDataOf(cond.lhs)
656
+ const rhs = cmpTermDataOf(cond.rhs)
661
657
  sideUses(cond.op, lhs, rhs, uses)
662
658
  sideUses(cond.op, rhs, lhs, uses)
663
- let mask: MaskData | undefined
664
659
  if (cond.op === "allen") {
665
660
  const maskValue = cond.mask
666
- if (typeof maskValue === "number") {
667
- mask = Object.freeze({ kind: "literal" as const, mask: maskValue })
668
- } else if (isTerm(maskValue) && maskValue[term] === "maskParam") {
669
- mask = Object.freeze({ kind: "param" as const, name: maskValue.name })
670
- uses.push(
671
- Object.freeze({
672
- name: maskValue.name,
673
- shape: "mask" as const,
674
- anchor: undefined,
675
- op: "allen" as const,
676
- members: undefined
677
- })
678
- )
679
- } else {
680
- throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam")
661
+ if (typeof maskValue !== "number") {
662
+ throw errors.new("allen: the mask position takes a 13-bit mask number built from the ALLEN constants")
681
663
  }
664
+ return Object.freeze({
665
+ kind: "cmp" as const,
666
+ op: { kind: "allen" as const, mask: maskValue },
667
+ lhs,
668
+ rhs
669
+ })
682
670
  }
683
- return Object.freeze({ kind: "cmp" as const, op: cond.op, mask, lhs, rhs })
671
+ return Object.freeze({ kind: "cmp" as const, op: { kind: cond.op }, lhs, rhs })
684
672
  }
685
673
  if (cond.cond === "tree") {
686
674
  const children = cond.children.map(function lowerChild(child) {
@@ -698,13 +686,13 @@ function advanceWhere(context: ChainContext, state: RuleBuildState, cond: AnyCon
698
686
  if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
699
687
  throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom")
700
688
  }
701
- if (cond.cond === "notIdb") {
689
+ if (cond.cond === "notInterior") {
702
690
  const bindings: Readonly<Record<string, unknown>> = Object.fromEntries(
703
691
  Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
704
692
  return value !== undefined
705
693
  })
706
694
  )
707
- return notIdbAdvance(context, state, cond.target, bindings)
695
+ return notInteriorAdvance(context, state, cond.name, bindings)
708
696
  }
709
697
  if (cond.cond === "not") {
710
698
  const relation: MatchOwner = cond.relation
@@ -730,19 +718,19 @@ function advanceWhere(context: ChainContext, state: RuleBuildState, cond: AnyCon
730
718
  }
731
719
 
732
720
  /**
733
- * Extends a rule state with one `idb` atom (a named record over head keys;
734
- * vars validated at completion). A POSITIVE idb atom is a positive
735
- * occurrence exactly as the engine represents it (`check_atoms` walks Idb
721
+ * Extends a rule state with one interior atom (a named record over head keys;
722
+ * vars validated at completion). A POSITIVE interior atom is a positive
723
+ * occurrence exactly as the engine represents it (`check_atoms` walks Interior
736
724
  * and Edb in one loop), so its variables GROUND: they enter the rule's
737
725
  * boundness set, may ride the head, and satisfy negation safety — the
738
- * idb-only identity projection of a finished stratum is spellable with no
726
+ * interior-only identity projection of a finished table is spellable with no
739
727
  * re-grounding join. A NEGATED one binds nothing, only rejects.
740
728
  */
741
- function advanceIdb(
729
+ function advanceInterior(
742
730
  state: RuleBuildState,
743
- rec: RecData,
731
+ target: DerivedTable,
744
732
  bindings: Readonly<Record<string, unknown>>,
745
- negated: boolean
733
+ kind: "interior" | "negatedInterior"
746
734
  ): RuleBuildState {
747
735
  const resolved: Array<{ readonly key: string; readonly ref: AnyVar }> = []
748
736
  for (const [key, value] of Object.entries(bindings)) {
@@ -751,31 +739,26 @@ function advanceIdb(
751
739
  }
752
740
  if (!isTerm(value) || value[term] !== "var") {
753
741
  throw errors.new(
754
- `idb ${rec.name}: position ${key} takes a variable — bind literals and params through where()/match()`
742
+ `interior ${target.name}: position ${key} takes a variable — bind literals and params through where()/match()`
755
743
  )
756
744
  }
757
745
  resolved.push(Object.freeze({ key, ref: value }))
758
746
  }
759
747
  const bound = new Set(state.bound)
760
- if (!negated) {
748
+ if (kind === "interior") {
761
749
  for (const binding of resolved) {
762
750
  bound.add(binding.ref)
763
751
  }
764
752
  }
765
753
  return Object.freeze({
766
- items: Object.freeze([
767
- ...state.items,
768
- Object.freeze({ kind: "idb" as const, rec, bindings: Object.freeze(resolved), negated })
769
- ]),
754
+ items: Object.freeze([...state.items, Object.freeze({ kind, target, bindings: Object.freeze(resolved) })]),
770
755
  bound,
771
756
  paramUses: state.paramUses
772
757
  })
773
758
  }
774
759
 
775
760
  /** Narrows a find entry to an aggregate value. */
776
- function isAggregateEntry(
777
- value: unknown
778
- ): value is { readonly agg: string; readonly over: unknown; readonly key: unknown } {
761
+ function isAggregateEntry(value: unknown): value is { readonly agg: string; readonly over: unknown } {
779
762
  return typeof value === "object" && value !== null && "agg" in value
780
763
  }
781
764
 
@@ -788,16 +771,11 @@ function asVarTerm(context: string, value: unknown): AnyVar {
788
771
  }
789
772
 
790
773
  /** Classifies one aggregate find entry into its runtime data (variables ride by reference). */
791
- function aggDataOf(
792
- name: string,
793
- entry: { readonly agg: string; readonly over: unknown; readonly key: unknown }
794
- ): AggData {
774
+ function aggDataOf(name: string, entry: { readonly agg: string; readonly over: unknown }): AggData {
795
775
  const over = entry.over
796
776
  switch (entry.agg) {
797
777
  case "count":
798
778
  return Object.freeze({ op: "count" as const })
799
- case "countDistinct":
800
- return Object.freeze({ op: "countDistinct" as const, over: asVarTerm(`find ${name} (countDistinct)`, over) })
801
779
  case "sum":
802
780
  case "min":
803
781
  case "max": {
@@ -809,14 +787,6 @@ function aggDataOf(
809
787
  }
810
788
  throw errors.new(`find ${name} (${entry.agg}): takes a variable or r.duration(v)`)
811
789
  }
812
- case "argMax":
813
- case "argMin":
814
- return Object.freeze({
815
- op: "arg" as const,
816
- direction: entry.agg,
817
- over: asVarTerm(`find ${name} (${entry.agg})`, over),
818
- key: asVarTerm(`find ${name} (${entry.agg} key)`, entry.key)
819
- })
820
790
  case "pack":
821
791
  return Object.freeze({ op: "pack" as const, over: asVarTerm(`find ${name} (pack)`, over) })
822
792
  default:
@@ -903,17 +873,14 @@ function assertNotClosed(where: string, position: string, ref: AnyVar): void {
903
873
 
904
874
  /**
905
875
  * The classed mint slot one answer column's VALUES flow from: a projected
906
- * variable's mint slot, or an Arg-carried payload's. Counts, folds, `pack`
907
- * and the measure derive numbers/intervals, so they resolve no slot.
876
+ * variable's mint slot. Counts, folds, `pack` and the measure derive
877
+ * numbers/intervals, so they resolve no slot.
908
878
  */
909
879
  function findColumnSlotOf(context: ChainContext, column: FindColumn): ClassedField | undefined {
910
880
  const entry = column.entry
911
881
  if (entry.kind === "var") {
912
882
  return mintSlotOf(context, entry.over)
913
883
  }
914
- if (entry.kind === "aggregate" && entry.agg.op === "arg") {
915
- return mintSlotOf(context, entry.agg.over)
916
- }
917
884
  return undefined
918
885
  }
919
886
 
@@ -934,9 +901,6 @@ function validateColumn(context: ChainContext, bound: ReadonlySet<AnyVar>, colum
934
901
  switch (agg.op) {
935
902
  case "count":
936
903
  return
937
- case "countDistinct":
938
- assertBound(where, bound, agg.over)
939
- return
940
904
  case "fold": {
941
905
  if ("duration" in agg.over) {
942
906
  assertBound(where, bound, agg.over.duration)
@@ -947,12 +911,6 @@ function validateColumn(context: ChainContext, bound: ReadonlySet<AnyVar>, colum
947
911
  assertNotClosed(where, `the ${agg.fold} input`, agg.over)
948
912
  return
949
913
  }
950
- case "arg": {
951
- assertBound(where, bound, agg.over)
952
- assertBound(where, bound, agg.key)
953
- assertNotClosed(where, `the ${agg.direction} key`, agg.key)
954
- return
955
- }
956
914
  case "pack":
957
915
  assertBound(where, bound, agg.over)
958
916
  assertInterval(where, agg.over)
@@ -972,8 +930,8 @@ function validateCond(context: ChainContext, bound: ReadonlySet<AnyVar>, cond: C
972
930
  if (side.kind === "var") {
973
931
  assertBound(label, bound, side.ref)
974
932
  const roster = rosterOf(side.ref.field)
975
- if (isOrderOp(cond.op) && roster !== undefined) {
976
- throw closedOrderError(label, `the ${cond.op} side ${side.ref.label}`, roster.name)
933
+ if (isOrderOp(cond.op.kind) && roster !== undefined) {
934
+ throw closedOrderError(label, `the ${cond.op.kind} side ${side.ref.label}`, roster.name)
977
935
  }
978
936
  }
979
937
  if (side.kind === "measure") {
@@ -981,14 +939,14 @@ function validateCond(context: ChainContext, bound: ReadonlySet<AnyVar>, cond: C
981
939
  assertInterval(label, side.ref)
982
940
  }
983
941
  }
984
- if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
942
+ if ((cond.op.kind === "eq" || cond.op.kind === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
985
943
  assertBound(label, bound, cond.lhs.ref)
986
944
  assertBound(label, bound, cond.rhs.ref)
987
945
  const lhs = mintSlotOf(context, cond.lhs.ref)
988
946
  const rhs = mintSlotOf(context, cond.rhs.ref)
989
947
  if (!fieldJoins(lhs, rhs)) {
990
948
  throw errors.new(
991
- `${label}: ${cond.op}(${cond.lhs.ref.label}, ${cond.rhs.ref.label}) unifies domain-unequal fields — ${cond.lhs.ref.label} bound at ${renderFieldKind(lhs)}, ${cond.rhs.ref.label} at ${renderFieldKind(rhs)} (a var joins only class-equal slots; bare pairs only with bare)`
949
+ `${label}: ${cond.op.kind}(${cond.lhs.ref.label}, ${cond.rhs.ref.label}) unifies domain-unequal fields — ${cond.lhs.ref.label} bound at ${renderFieldKind(lhs)}, ${cond.rhs.ref.label} at ${renderFieldKind(rhs)} (a var joins only class-equal slots; bare pairs only with bare)`
992
950
  )
993
951
  }
994
952
  }
@@ -1000,29 +958,27 @@ function validateCond(context: ChainContext, bound: ReadonlySet<AnyVar>, cond: C
1000
958
  }
1001
959
 
1002
960
  /**
1003
- * Validates one `idb` item: every head column of the rec is bound exactly
961
+ * Validates one interior item: every head column of the table is bound exactly
1004
962
  * once (a missing or extra key is a pointed error) and each variable joins
1005
- * its head column's classed slot. A POSITIVE idb atom GROUNDS its
963
+ * its head column's classed slot. A POSITIVE interior atom GROUNDS its
1006
964
  * variables (a positive occurrence, exactly the engine's representation),
1007
965
  * so no boundness precondition exists; a NEGATED one binds nothing — its
1008
966
  * variables must be positively bound elsewhere in the rule, the same
1009
- * safety rule as EDB negation. When the rec's own rule 0 is in flight
1010
- * (`rec.rules[0]` absent), the completing rule's OWN find columns ARE the
1011
- * head.
967
+ * safety rule as EDB negation. When the table's own first rule is in flight
968
+ * (`finds` empty), the completing rule's OWN find columns ARE the head.
1012
969
  */
1013
- function validateIdb(
970
+ function validateInterior(
1014
971
  context: ChainContext,
1015
972
  bound: ReadonlySet<AnyVar>,
1016
973
  item: {
1017
- readonly rec: RecData
974
+ readonly kind: "interior" | "negatedInterior"
975
+ readonly target: DerivedTable
1018
976
  readonly bindings: ReadonlyArray<{ readonly key: string; readonly ref: AnyVar }>
1019
- readonly negated: boolean
1020
977
  },
1021
978
  columns: readonly FindColumn[]
1022
979
  ): void {
1023
980
  const label = contextLabel(context)
1024
- const head = item.rec.rules[0]
1025
- const headColumns = head !== undefined ? head.finds : columns
981
+ const headColumns = item.target.finds.length > 0 ? item.target.finds : columns
1026
982
  const headNames = headColumns.map(function nameOf(column) {
1027
983
  return column.name
1028
984
  })
@@ -1032,21 +988,21 @@ function validateIdb(
1032
988
  for (const key of keys) {
1033
989
  if (!headNames.includes(key)) {
1034
990
  throw errors.new(
1035
- `${label}: idb ${item.rec.name} binds ${key}, not a head column of ${item.rec.name} (head columns: ${headNames.join(", ")})`
991
+ `${label}: interior ${item.target.name} binds ${key}, not a head column of ${item.target.name} (head columns: ${headNames.join(", ")})`
1036
992
  )
1037
993
  }
1038
994
  }
1039
995
  for (const name of headNames) {
1040
996
  if (!keys.includes(name)) {
1041
997
  throw errors.new(
1042
- `${label}: idb ${item.rec.name} omits the head column ${name} — an idb join binds every head column of ${item.rec.name}`
998
+ `${label}: interior ${item.target.name} omits the head column ${name} — an interior join binds every head column of ${item.target.name}`
1043
999
  )
1044
1000
  }
1045
1001
  }
1046
1002
  for (const binding of item.bindings) {
1047
- if (item.negated && !bound.has(binding.ref)) {
1003
+ if (item.kind === "negatedInterior" && !bound.has(binding.ref)) {
1048
1004
  throw errors.new(
1049
- `${label}: negated idb ${item.rec.name} names the variable ${binding.ref.label}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`
1005
+ `${label}: negated interior ${item.target.name} names the variable ${binding.ref.label}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`
1050
1006
  )
1051
1007
  }
1052
1008
  const headColumn = headColumns.find(function byName(column) {
@@ -1058,7 +1014,7 @@ function validateIdb(
1058
1014
  const mint = mintSlotOf(context, binding.ref)
1059
1015
  if (!fieldJoins(headColumn.slot, mint)) {
1060
1016
  throw errors.new(
1061
- `${label}: idb ${item.rec.name} joins the variable ${binding.ref.label} (${renderFieldKind(mint)}) at head column ${binding.key} (${renderFieldKind(headColumn.slot)}) — a var joins only class-equal slots; bare pairs only with bare`
1017
+ `${label}: interior ${item.target.name} joins the variable ${binding.ref.label} (${renderFieldKind(mint)}) at head column ${binding.key} (${renderFieldKind(headColumn.slot)}) — a var joins only class-equal slots; bare pairs only with bare`
1062
1018
  )
1063
1019
  }
1064
1020
  }
@@ -1067,7 +1023,7 @@ function validateIdb(
1067
1023
  /**
1068
1024
  * Completes one rule: enriches the find columns (declaration-order-safe
1069
1025
  * keys, boundness validated, each column's classed slot and closed slice
1070
- * resolved), then walks the body walls — negated-atom boundness safety, idb
1026
+ * resolved), then walks the body walls — negated-atom boundness safety, interior
1071
1027
  * head pairing, and condition validation.
1072
1028
  */
1073
1029
  function completeRule(context: ChainContext, state: RuleBuildState, rawColumns: readonly FindColumn[]): RuleData {
@@ -1091,8 +1047,8 @@ function completeRule(context: ChainContext, state: RuleBuildState, rawColumns:
1091
1047
  }
1092
1048
  }
1093
1049
  }
1094
- if (item.kind === "idb") {
1095
- validateIdb(context, state.bound, item, columns)
1050
+ if (item.kind === "interior" || item.kind === "negatedInterior") {
1051
+ validateInterior(context, state.bound, item, columns)
1096
1052
  }
1097
1053
  if (item.kind === "cond") {
1098
1054
  validateCond(context, state.bound, item.cond)
@@ -1113,100 +1069,116 @@ function makeRuleValue<Row, P extends ParamsRecord>(rule: RuleData): RuleValue<R
1113
1069
  interface RawChain {
1114
1070
  match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
1115
1071
  where(cond: AnyCond): RawChain
1116
- idb(target: RecRef<string, ParamsRecord>, bindings: Readonly<Record<string, unknown>>): RawChain
1072
+ interior(name: string, bindings: Readonly<Record<string, unknown>>): RawChain
1117
1073
  find(entries: Readonly<Record<string, unknown>>): RuleValue<never, never>
1118
1074
  }
1119
1075
 
1120
1076
  /** The runtime rule-builder shape beneath every typed scope. */
1121
1077
  interface RawScope extends TermOps {
1122
1078
  match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
1123
- idb(target: RecRef<string, ParamsRecord>, bindings: Readonly<Record<string, unknown>>): RawChain
1079
+ interior(name: string, bindings: Readonly<Record<string, unknown>>): RawChain
1080
+ }
1081
+
1082
+ /** The declared derived tables a chain may name. */
1083
+ interface DerivedEnv {
1084
+ readonly interiors: readonly InteriorData[]
1085
+ readonly rec: RecHandle | RecHead | RecData | null
1124
1086
  }
1125
1087
 
1126
1088
  /** Which rule family a chain builds — plus the schema's runtime class map and theory value (the join judge's authority). */
1127
- type ChainContext = { readonly classes: SchemaClasses; readonly theory: AnySchema } & (
1128
- | { readonly kind: "query" }
1129
- | { readonly kind: "rec"; readonly self: RecData }
1130
- | { readonly kind: "output"; readonly program: ProgramState }
1131
- )
1089
+ type ChainContext = { readonly classes: SchemaClasses; readonly theory: AnySchema } & DerivedEnv &
1090
+ (
1091
+ | { readonly kind: "query" }
1092
+ | { readonly kind: "interior"; readonly self: string }
1093
+ | { readonly kind: "rec-base"; readonly self: RecHandle }
1094
+ | { readonly kind: "rec-arm"; readonly self: RecHead }
1095
+ )
1132
1096
 
1133
1097
  /** The diagnostic label of a chain context. */
1134
1098
  function contextLabel(context: ChainContext): string {
1135
1099
  switch (context.kind) {
1136
1100
  case "query":
1137
1101
  return "query rule"
1138
- case "rec":
1139
- return `rec ${context.self.name} rule`
1140
- case "output":
1141
- return "program output rule"
1102
+ case "interior":
1103
+ return `interior ${context.self} rule`
1104
+ case "rec-base":
1105
+ return `recursive ${context.self.name} base`
1106
+ case "rec-arm":
1107
+ return `recursive ${context.self.name} rec`
1142
1108
  }
1143
1109
  }
1144
1110
 
1145
- /** Validates and records one `idb` atom per the context's cut. */
1146
- function idbAdvance(
1147
- context: ChainContext,
1148
- state: RuleBuildState,
1149
- target: RecRef<string, ParamsRecord>,
1150
- bindings: Readonly<Record<string, unknown>>
1151
- ): RuleBuildState {
1152
- if (context.kind === "query") {
1153
- throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()")
1154
- }
1155
- if (context.kind === "rec") {
1156
- if (target.data !== context.self) {
1111
+ /** Resolves a derived-table name against the context's visible tables. */
1112
+ function lookupDerived(context: ChainContext, name: string): DerivedTable {
1113
+ const interior = context.interiors.find(function byName(candidate) {
1114
+ return candidate.name === name
1115
+ })
1116
+ if (interior !== undefined) {
1117
+ if (context.kind === "interior" && name === context.self) {
1157
1118
  throw errors.new(
1158
- `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)`
1119
+ `interior ${name}: an interior does not read itself — declaration order is topological (a self-read is InteriorNotPrior)`
1159
1120
  )
1160
1121
  }
1161
- return advanceIdb(state, context.self, bindings, false)
1122
+ return interior
1162
1123
  }
1163
- if (!context.program.recs.includes(target.data)) {
1164
- throw errors.new(
1165
- `idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`
1166
- )
1124
+ if (context.rec !== null && context.rec.name === name) {
1125
+ if (context.kind === "interior") {
1126
+ throw errors.new(`interior ${context.self}: interiors cannot read the rec — this cut's interiors are a prefix`)
1127
+ }
1128
+ if (context.kind === "rec-base") {
1129
+ throw errors.new(
1130
+ `recursive ${context.rec.name}: a base arm does not read the rec — self-atoms belong on rec arms`
1131
+ )
1132
+ }
1133
+ if (!("finds" in context.rec)) {
1134
+ throw errors.new(`recursive ${context.rec.name}: rec arms resolve the rec head after base arms seal it`)
1135
+ }
1136
+ return context.rec
1167
1137
  }
1168
- return advanceIdb(state, target.data, bindings, false)
1138
+ throw errors.new(`${contextLabel(context)}: no derived table named ${name} is in scope`)
1139
+ }
1140
+
1141
+ /** Validates and records one interior atom per the context's cut. */
1142
+ function interiorAdvance(
1143
+ context: ChainContext,
1144
+ state: RuleBuildState,
1145
+ name: string,
1146
+ bindings: Readonly<Record<string, unknown>>
1147
+ ): RuleBuildState {
1148
+ return advanceInterior(state, lookupDerived(context, name), bindings, "interior")
1169
1149
  }
1170
1150
 
1171
1151
  /**
1172
- * Validates and records one NEGATED finished-stratum atom — output rules
1173
- * only: there every rec is a finished set before the output's operator
1174
- * runs (negation OF lower strata is engine-legal; the strata judge refuses
1175
- * only negation *through* a cycle, which the rec-context refusal here
1176
- * makes unwritable).
1152
+ * Validates and records one NEGATED finished-table atom — main and interior
1153
+ * rules: a finished set is a set. Rec bodies refuse every negation
1154
+ * (`NegationInRec` — self is the wall; EDB / earlier-interior is this-cut).
1177
1155
  */
1178
- function notIdbAdvance(
1156
+ function notInteriorAdvance(
1179
1157
  context: ChainContext,
1180
1158
  state: RuleBuildState,
1181
- target: { readonly name: string; readonly data: RecData },
1159
+ name: string,
1182
1160
  bindings: Readonly<Record<string, unknown>>
1183
1161
  ): RuleBuildState {
1184
- if (context.kind === "query") {
1185
- throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()")
1186
- }
1187
- if (context.kind === "rec") {
1162
+ if (context.kind === "rec-base" || context.kind === "rec-arm") {
1188
1163
  throw errors.new(
1189
- `rec ${context.self.name}: a recursive rule negates no stratum — self-negation is negation through the cycle (a finished set is what keeps the operator monotone), and a finished stratum's fold belongs in the output rules`
1164
+ `recursive ${context.self.name}: a recursive rule negates no table — self-negation is negation through the cycle (a finished set is what keeps the operator monotone), and a finished table's fold belongs in the main rules`
1190
1165
  )
1191
1166
  }
1192
- if (!context.program.recs.includes(target.data)) {
1193
- throw errors.new(
1194
- `idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`
1195
- )
1196
- }
1197
- return advanceIdb(state, target.data, bindings, true)
1167
+ return advanceInterior(state, lookupDerived(context, name), bindings, "negatedInterior")
1198
1168
  }
1199
1169
 
1200
- /** Classifies one find record per the context (a recursive head projects bound variables only). */
1170
+ /** Classifies one find record per the context (interior and rec heads project bound variables only). */
1201
1171
  function findColumns(context: ChainContext, entries: Readonly<Record<string, unknown>>): FindColumn[] {
1202
1172
  const columns: FindColumn[] = []
1173
+ const derivedHead = context.kind !== "query"
1203
1174
  for (const [name, entry] of Object.entries(entries)) {
1204
1175
  if (entry === undefined) {
1205
1176
  continue
1206
1177
  }
1207
- if (context.kind === "rec" && !(isTerm(entry) && entry[term] === "var")) {
1178
+ if (derivedHead && !(isTerm(entry) && entry[term] === "var")) {
1179
+ const who = context.kind === "interior" ? `interior ${context.self}` : `recursive ${context.self.name}`
1208
1180
  throw errors.new(
1209
- `rec ${context.self.name}: a recursive head projects bound variables only — aggregates and the measure read finished sets (the strata judge's quarantine, unwritable here)`
1181
+ `${who}: a recursive head projects bound variables only — aggregates and the measure read finished sets (unwritable here)`
1210
1182
  )
1211
1183
  }
1212
1184
  columns.push(findColumnOf(name, entry))
@@ -1223,8 +1195,8 @@ function makeRawChain(context: ChainContext, state: RuleBuildState): RawChain {
1223
1195
  where(cond) {
1224
1196
  return makeRawChain(context, advanceWhere(context, state, cond))
1225
1197
  },
1226
- idb(target, bindings) {
1227
- return makeRawChain(context, idbAdvance(context, state, target, bindings))
1198
+ interior(name, bindings) {
1199
+ return makeRawChain(context, interiorAdvance(context, state, name, bindings))
1228
1200
  },
1229
1201
  find(entries) {
1230
1202
  return makeRuleValue<never, never>(completeRule(context, state, findColumns(context, entries)))
@@ -1241,8 +1213,8 @@ function makeRawScope(context: ChainContext): RawScope {
1241
1213
  match(relation, bindings) {
1242
1214
  return makeRawChain(context, advanceMatch(context, EMPTY_RULE, relation, bindings))
1243
1215
  },
1244
- idb(target, bindings) {
1245
- return makeRawChain(context, idbAdvance(context, EMPTY_RULE, target, bindings))
1216
+ interior(name, bindings) {
1217
+ return makeRawChain(context, interiorAdvance(context, EMPTY_RULE, name, bindings))
1246
1218
  }
1247
1219
  }
1248
1220
  Object.freeze(scope)
@@ -1264,32 +1236,68 @@ function isTypedScope<S>(scope: RawScope): scope is RawScope & S {
1264
1236
 
1265
1237
  /** Builds one query-rule builder (the typed face of the raw builder). */
1266
1238
  function makeQueryRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1267
- theory: AnySchema
1239
+ theory: AnySchema,
1240
+ env: DerivedEnv
1268
1241
  ): QueryRuleScope<Rels, Classes> {
1269
- const raw = makeRawScope({ kind: "query", classes: theory.classes, theory })
1242
+ const raw = makeRawScope({ kind: "query", classes: theory.classes, theory, ...env })
1270
1243
  if (!isTypedScope<QueryRuleScope<Rels, Classes>>(raw)) {
1271
1244
  throw errors.new("query rule builder construction incomplete")
1272
1245
  }
1273
1246
  return raw
1274
1247
  }
1275
1248
 
1276
- /** Builds one output-rule builder over a program's recs. */
1277
- function makeOutputRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1278
- program: ProgramState
1279
- ): OutputRuleScope<Rels, Classes> {
1280
- const raw = makeRawScope({ kind: "output", program, classes: program.classes, theory: program.theory })
1281
- if (!isTypedScope<OutputRuleScope<Rels, Classes>>(raw)) {
1282
- throw errors.new("program output rule builder construction incomplete")
1249
+ /** Builds one interior-rule builder. */
1250
+ function makeInteriorRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1251
+ theory: AnySchema,
1252
+ env: DerivedEnv,
1253
+ self: string
1254
+ ): InteriorRuleScope<Rels, Classes> {
1255
+ const raw = makeRawScope({ kind: "interior", self, classes: theory.classes, theory, ...env })
1256
+ if (!isTypedScope<InteriorRuleScope<Rels, Classes>>(raw)) {
1257
+ throw errors.new("interior rule builder construction incomplete")
1283
1258
  }
1284
1259
  return raw
1285
1260
  }
1286
1261
 
1287
- /** One program's build-time registry: its recs in declaration order, the theory value, and its class map. */
1288
- interface ProgramState {
1289
- readonly recs: RecData[]
1290
- readonly classes: SchemaClasses
1291
- readonly theory: AnySchema
1292
- sealed: boolean
1262
+ /** Builds one rec-arm builder. */
1263
+ function makeRecRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1264
+ theory: AnySchema,
1265
+ env: DerivedEnv,
1266
+ self: RecHandle,
1267
+ kind: "rec-base"
1268
+ ): RecRuleScope<Rels, Classes>
1269
+ function makeRecRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1270
+ theory: AnySchema,
1271
+ env: DerivedEnv,
1272
+ self: RecHead,
1273
+ kind: "rec-arm"
1274
+ ): RecRuleScope<Rels, Classes>
1275
+ function makeRecRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1276
+ theory: AnySchema,
1277
+ env: DerivedEnv,
1278
+ self: RecHandle | RecHead,
1279
+ kind: "rec-base" | "rec-arm"
1280
+ ): RecRuleScope<Rels, Classes> {
1281
+ const raw =
1282
+ kind === "rec-base"
1283
+ ? makeRawScope({
1284
+ kind: "rec-base",
1285
+ self: self as RecHandle,
1286
+ classes: theory.classes,
1287
+ theory,
1288
+ ...env
1289
+ })
1290
+ : makeRawScope({
1291
+ kind: "rec-arm",
1292
+ self: self as RecHead,
1293
+ classes: theory.classes,
1294
+ theory,
1295
+ ...env
1296
+ })
1297
+ if (!isTypedScope<RecRuleScope<Rels, Classes>>(raw)) {
1298
+ throw errors.new("recursive rule builder construction incomplete")
1299
+ }
1300
+ return raw
1293
1301
  }
1294
1302
 
1295
1303
  /** Renders one head column's closed slice for the rule-alignment check's diagnostics. */
@@ -1307,9 +1315,6 @@ function headSignature(column: FindColumn): string {
1307
1315
  if (agg.op === "fold") {
1308
1316
  return `${column.name}:${agg.fold}`
1309
1317
  }
1310
- if (agg.op === "arg") {
1311
- return `${column.name}:${agg.direction}`
1312
- }
1313
1318
  return `${column.name}:${agg.op}`
1314
1319
  }
1315
1320
 
@@ -1324,17 +1329,22 @@ function renderParamAnchor(roster: ClosedRoster | undefined): string {
1324
1329
  }
1325
1330
 
1326
1331
  /**
1327
- * Folds every rule's param uses (recs in declaration order first, output
1328
- * rules last — exactly the lowering walk) into the query's registry: first
1329
- * use mints the dense `ParamId`, the first FIELD-ANCHORED use types the
1330
- * wire, and one name keeps one shape AND one closedness. The orderable
1331
- * ban needs no registry arm: an order use always anchors its SIBLING's
1332
- * domain (the no-variable-side spelling is refused at the comparison
1333
- * constructor), so a closed-anchored param under an order op dies at the
1334
- * one-domain wall here — and an order use whose sibling is itself
1335
- * closed-bound dies at the comparison's own var-side wall first.
1332
+ * Folds every rule's param uses (interiors in declaration order, then rec
1333
+ * base, then rec arms, then main — exactly the lowering walk) into the
1334
+ * query's registry: first use mints the dense `ParamId`, the first
1335
+ * FIELD-ANCHORED use types the wire, and one name keeps one shape AND one
1336
+ * closedness. The orderable ban needs no registry arm: an order use always
1337
+ * anchors its SIBLING's domain (the no-variable-side spelling is refused
1338
+ * at the comparison constructor), so a closed-anchored param under an
1339
+ * order op dies at the one-domain wall here — and an order use whose
1340
+ * sibling is itself closed-bound dies at the comparison's own var-side
1341
+ * wall first.
1336
1342
  */
1337
- function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]): readonly ParamEntry[] {
1343
+ function paramRegistryOf(
1344
+ interiors: readonly InteriorData[],
1345
+ rec: RecData | null,
1346
+ rules: readonly RuleData[]
1347
+ ): readonly ParamEntry[] {
1338
1348
  const order: string[] = []
1339
1349
  const byName = new Map<
1340
1350
  string,
@@ -1383,8 +1393,16 @@ function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]):
1383
1393
  }
1384
1394
  }
1385
1395
  }
1386
- for (const rec of recs) {
1387
- for (const rule of rec.rules) {
1396
+ for (const interior of interiors) {
1397
+ for (const rule of interior.rules) {
1398
+ fold(rule.paramUses)
1399
+ }
1400
+ }
1401
+ if (rec !== null) {
1402
+ for (const rule of rec.base) {
1403
+ fold(rule.paramUses)
1404
+ }
1405
+ for (const rule of rec.rec) {
1388
1406
  fold(rule.paramUses)
1389
1407
  }
1390
1408
  }
@@ -1430,114 +1448,252 @@ interface RawQuery {
1430
1448
  readonly schema: AnySchema
1431
1449
  readonly data: QueryData
1432
1450
  rule(build: (r: RawScope) => RuleValue<never, never>): RawQuery
1451
+ interior(name: string, ...builds: never[]): never
1452
+ recursive(name: string, arms: never): never
1433
1453
  }
1434
1454
 
1435
- /**
1436
- * Assembles the runtime query value over completed rules: every rule must
1437
- * derive the SAME head (name and aggregate shape, position for position —
1438
- * the decode labels and the engine's alignment rule agree), and the param
1439
- * registry folds in program-walk order.
1440
- */
1441
- function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readonly RuleData[]): RawQuery {
1455
+ /** Asserts every rule in a list derives the same head (name, aggregate shape, closed slice, class). */
1456
+ function assertAlignedHeads(label: string, rules: readonly RuleData[]): void {
1442
1457
  const first = rules[0]
1443
1458
  if (first === undefined) {
1444
- throw errors.new("a query needs at least one rule")
1459
+ throw errors.new(`${label} needs at least one rule`)
1445
1460
  }
1446
1461
  const signature = first.finds.map(headSignature).join(", ")
1447
1462
  rules.forEach(function verifyHead(rule, index) {
1448
1463
  const candidate = rule.finds.map(headSignature).join(", ")
1449
1464
  if (candidate !== signature) {
1450
1465
  throw errors.new(
1451
- `every rule of a query derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`
1466
+ `every rule of ${label} derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`
1452
1467
  )
1453
1468
  }
1454
- // The closed slice is part of the head too: one answer column decodes
1455
- // through one roster, so a union whose rules bind a column at different
1456
- // vocabularies (or one closed, one bare) is refused pointed.
1457
1469
  rule.finds.forEach(function verifyClosedSlice(column, position) {
1458
1470
  const lead = first.finds[position]
1459
1471
  if (lead !== undefined && column.closed !== lead.closed) {
1460
1472
  throw errors.new(
1461
- `every rule of a query derives the same head — the answer column ${lead.name} is ${renderClosedSlice(lead.closed)} in rule 0 but ${renderClosedSlice(column.closed)} in rule ${index} (one column decodes through one roster)`
1473
+ `every rule of ${label} derives the same head — the head column ${lead.name} is ${renderClosedSlice(lead.closed)} in rule 0 but ${renderClosedSlice(column.closed)} in rule ${index} (one column decodes through one roster)`
1462
1474
  )
1463
1475
  }
1464
- // The law-class wall on the union head: one answer column is one
1465
- // value space, so the classed mint slot each rule binds the column
1466
- // at must join across rules — the SAME fieldJoins judgment every
1467
- // join/eq/negated-atom position enforces (the SDK holds it because
1468
- // the wire IR carries no domains).
1469
1476
  if (lead === undefined) {
1470
1477
  return
1471
1478
  }
1472
1479
  if (lead.slot !== undefined && column.slot !== undefined && !fieldJoins(lead.slot, column.slot)) {
1473
1480
  throw errors.new(
1474
- `every rule of a query derives the same head — the answer column ${lead.name} unions domain-unequal fields: bound at ${renderFieldKind(lead.slot)} in rule 0 but at ${renderFieldKind(column.slot)} in rule ${index} (a union column joins only class-equal slots; bare pairs only with bare)`
1481
+ `every rule of ${label} derives the same head — the head column ${lead.name} is bound at ${renderFieldKind(lead.slot)} in rule 0 but at ${renderFieldKind(column.slot)} in rule ${index} (a head column joins only class-equal slots; bare pairs only with bare)`
1475
1482
  )
1476
1483
  }
1477
1484
  })
1478
1485
  })
1486
+ }
1487
+
1488
+ function afterMainError(what: string): Error {
1489
+ return errors.new(
1490
+ `query: ${what} after a main rule is unwritable — declaration order is interiors, then rec, then main`
1491
+ )
1492
+ }
1493
+
1494
+ /**
1495
+ * Assembles the runtime query value over completed rules: every rule must
1496
+ * derive the SAME head (name and aggregate shape, position for position —
1497
+ * the decode labels and the engine's alignment rule agree), and the param
1498
+ * registry folds in query-walk order.
1499
+ */
1500
+ function makeRawQuery(
1501
+ theory: AnySchema,
1502
+ interiors: readonly InteriorData[],
1503
+ rec: RecData | null,
1504
+ rules: readonly RuleData[]
1505
+ ): RawQuery {
1506
+ assertAlignedHeads("a query", rules)
1507
+ const first = rules[0]
1508
+ if (first === undefined) {
1509
+ throw errors.new("a query needs at least one rule")
1510
+ }
1511
+ const env: DerivedEnv = { interiors, rec }
1479
1512
  const data: QueryData = Object.freeze({
1480
- recs: Object.freeze([...recs]),
1513
+ interiors: Object.freeze([...interiors]),
1514
+ rec,
1481
1515
  rules: Object.freeze([...rules]),
1482
1516
  finds: first.finds,
1483
- params: paramRegistryOf(recs, rules)
1517
+ params: paramRegistryOf(interiors, rec, rules)
1484
1518
  })
1485
1519
  const value: RawQuery = {
1486
1520
  schema: theory,
1487
1521
  data,
1488
1522
  rule(build) {
1489
- const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory }))
1490
- return makeRawQuery(theory, recs, [...rules, built.rule])
1523
+ const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory, ...env }))
1524
+ return makeRawQuery(theory, interiors, rec, [...rules, built.rule])
1525
+ },
1526
+ interior() {
1527
+ throw afterMainError("interior")
1528
+ },
1529
+ recursive() {
1530
+ throw afterMainError("recursive")
1491
1531
  }
1492
1532
  }
1493
1533
  Object.freeze(value)
1494
1534
  return value
1495
1535
  }
1496
1536
 
1497
- /**
1498
- * The query values' trusted admission seam (the {@link isTypedScope} pattern):
1499
- * the checkable fact — the value was assembled over the identical theory —
1500
- * is verified before the raw value is admitted at its typed face.
1501
- */
1502
- function isQueryValue<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
1503
- theory: Schema<Rels, Classes>,
1504
- value: RawQuery
1505
- ): value is RawQuery & Query<Rels, Row, P, Classes> {
1506
- return value.schema === theory
1507
- }
1508
-
1509
- /** Assembles one typed query value (rules already completed). */
1510
1537
  function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
1511
1538
  theory: Schema<Rels, Classes>,
1512
- recs: readonly RecData[],
1539
+ interiors: readonly InteriorData[],
1540
+ rec: RecData | null,
1513
1541
  rules: readonly RuleData[]
1514
1542
  ): Query<Rels, Row, P, Classes> {
1515
- const raw = makeRawQuery(theory, recs, rules)
1516
- if (!isQueryValue<Rels, Row, P, Classes>(theory, raw)) {
1517
- throw errors.new("query value construction incomplete")
1543
+ return makeRawQuery(theory, interiors, rec, rules) as unknown as Query<Rels, Row, P, Classes>
1544
+ }
1545
+
1546
+ /** Collects one Interior from its builders. */
1547
+ function collectInterior<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1548
+ theory: Schema<Rels, Classes>,
1549
+ env: DerivedEnv,
1550
+ name: string,
1551
+ builds: readonly InteriorBuild<Rels, Classes>[]
1552
+ ): InteriorData {
1553
+ if (builds.length === 0) {
1554
+ throw errors.new(`query: interior ${name} needs at least one rule`)
1518
1555
  }
1519
- return raw
1556
+ const rules = builds.map(function buildRule(buildOne) {
1557
+ return buildOne(makeInteriorRuleScope<Rels, Classes>(theory, env, name)).rule
1558
+ })
1559
+ assertAlignedHeads(`interior ${name}`, rules)
1560
+ const first = rules[0]
1561
+ if (first === undefined) {
1562
+ throw errors.new(`query: interior ${name} needs at least one rule`)
1563
+ }
1564
+ return Object.freeze({ name, finds: first.finds, rules: Object.freeze(rules) })
1520
1565
  }
1521
1566
 
1522
- /**
1523
- * Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule` adds
1524
- * one conjunctive rule; multiple rules are the set union. The schema's
1525
- * law-computed class map and theory value ride into every rule builder — the
1526
- * join walls compare against the mint slots off it.
1527
- */
1528
- function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1529
- theory: Schema<Rels, Classes>
1530
- ): QueryStart<Rels, Classes> {
1531
- const start: QueryStart<Rels, Classes> = {
1567
+ /** Collects the Rec from tagged base/rec builder arrays. */
1568
+ function collectRec<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1569
+ theory: Schema<Rels, Classes>,
1570
+ interiors: readonly InteriorData[],
1571
+ name: string,
1572
+ baseBuilds: readonly RecBuild<Rels, Classes>[],
1573
+ recBuilds: readonly RecBuild<Rels, Classes>[]
1574
+ ): RecData {
1575
+ if (baseBuilds.length === 0) {
1576
+ throw errors.new(`query: recursive ${name} has no base arms`)
1577
+ }
1578
+ if (recBuilds.length === 0) {
1579
+ throw errors.new(`query: recursive ${name} has no rec arms`)
1580
+ }
1581
+ const handle: RecHandle = Object.freeze({ name })
1582
+ const baseEnv: DerivedEnv = { interiors, rec: handle }
1583
+ const base = baseBuilds.map(function buildBase(buildOne) {
1584
+ return buildOne(makeRecRuleScope<Rels, Classes>(theory, baseEnv, handle, "rec-base")).rule
1585
+ })
1586
+ assertAlignedHeads(`recursive ${name}`, base)
1587
+ const first = base[0]
1588
+ if (first === undefined) {
1589
+ throw errors.new(`query: recursive ${name} has no base arms`)
1590
+ }
1591
+ const firstFind = first.finds[0]
1592
+ if (firstFind === undefined) {
1593
+ throw errors.new(`query: recursive ${name} has no head`)
1594
+ }
1595
+ const finds: RecHead["finds"] = [firstFind, ...first.finds.slice(1)]
1596
+ const head: RecHead = Object.freeze({ name, finds })
1597
+ const recEnv: DerivedEnv = { interiors, rec: head }
1598
+ const rec = recBuilds.map(function buildRec(buildOne) {
1599
+ return buildOne(makeRecRuleScope<Rels, Classes>(theory, recEnv, head, "rec-arm")).rule
1600
+ })
1601
+ assertAlignedHeads(`recursive ${name}`, [...base, ...rec])
1602
+ const firstRec = rec[0]
1603
+ if (firstRec === undefined) {
1604
+ throw errors.new(`query: recursive ${name} has no rec arms`)
1605
+ }
1606
+ const sealedBase: RecData["base"] = [first, ...base.slice(1)]
1607
+ const sealedRec: RecData["rec"] = [firstRec, ...rec.slice(1)]
1608
+ const recData: RecData = Object.freeze({
1609
+ name,
1610
+ finds,
1611
+ base: sealedBase,
1612
+ rec: sealedRec
1613
+ })
1614
+ return recData
1615
+ }
1616
+
1617
+ /** Builds the query start (interiors, then optional rec, then the first main rule). */
1618
+ function makeQueryStart<
1619
+ Rels extends SchemaRelations,
1620
+ Classes extends SchemaClasses,
1621
+ P extends ParamsRecord,
1622
+ Rec extends RecData | null
1623
+ >(theory: Schema<Rels, Classes>, interiors: readonly InteriorData[], rec: Rec): QueryStart<Rels, Classes, P, Rec> {
1624
+ const env: DerivedEnv = { interiors, rec }
1625
+ const start = {
1626
+ interior<const Builds extends readonly InteriorBuild<Rels, Classes>[]>(
1627
+ name: string,
1628
+ ...builds: Builds
1629
+ ): QueryStart<Rels, Classes, Flatten<P & BuildsParams<Builds>>, null> {
1630
+ if (rec !== null) {
1631
+ throw errors.new(
1632
+ "query: interior after recursive is unwritable — declaration order is interiors, then rec, then main"
1633
+ )
1634
+ }
1635
+ if (
1636
+ interiors.some(function sameName(interior) {
1637
+ return interior.name === name
1638
+ })
1639
+ ) {
1640
+ throw errors.new(`query: interior ${name} is already declared — names are unique`)
1641
+ }
1642
+ if (name.length === 0) {
1643
+ throw errors.new("query: an interior needs a name")
1644
+ }
1645
+ const data = collectInterior(theory, env, name, builds)
1646
+ return makeQueryStart<Rels, Classes, Flatten<P & BuildsParams<Builds>>, null>(theory, [...interiors, data], null)
1647
+ },
1648
+ recursive<
1649
+ const Base extends readonly RecBuild<Rels, Classes>[],
1650
+ const Step extends readonly RecBuild<Rels, Classes>[]
1651
+ >(
1652
+ name: string,
1653
+ arms: { readonly base: Base; readonly rec: Step }
1654
+ ): QueryStart<Rels, Classes, Flatten<P & BuildsParams<Base> & BuildsParams<Step>>, RecData> {
1655
+ if (rec !== null) {
1656
+ throw errors.new("query: a second recursive is unwritable — this cut admits one linear rec")
1657
+ }
1658
+ if (
1659
+ interiors.some(function sameName(interior) {
1660
+ return interior.name === name
1661
+ })
1662
+ ) {
1663
+ throw errors.new(`query: interior and recursive share the name ${name}`)
1664
+ }
1665
+ if (name.length === 0) {
1666
+ throw errors.new("query: recursive needs a name")
1667
+ }
1668
+ const data = collectRec(theory, interiors, name, arms.base, arms.rec)
1669
+ return makeQueryStart<Rels, Classes, Flatten<P & BuildsParams<Base> & BuildsParams<Step>>, RecData>(
1670
+ theory,
1671
+ interiors,
1672
+ data
1673
+ )
1674
+ },
1532
1675
  rule<RV extends AnyRuleValue>(
1533
1676
  build: (r: QueryRuleScope<Rels, Classes>) => RV
1534
- ): Query<Rels, RowOf<RV>, ParamsOf<RV>, Classes> {
1535
- const built = build(makeQueryRuleScope<Rels, Classes>(theory))
1536
- return makeQuery<Rels, RowOf<RV>, ParamsOf<RV>, Classes>(theory, [], [built.rule])
1677
+ ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes> {
1678
+ const built = build(makeQueryRuleScope<Rels, Classes>(theory, env))
1679
+ return makeQuery<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes>(theory, interiors, rec, [built.rule])
1537
1680
  }
1538
1681
  }
1539
1682
  Object.freeze(start)
1540
- return start
1683
+ return start as unknown as QueryStart<Rels, Classes, P, Rec>
1684
+ }
1685
+
1686
+ /**
1687
+ * Opens a query over a schema: `query(S).rule(r => ...)`, optionally with
1688
+ * `interior` / `recursive` first. Each `.rule` adds one conjunctive rule;
1689
+ * multiple rules are the set union. The schema's law-computed class map and
1690
+ * theory value ride into every rule builder — the join walls compare
1691
+ * against the mint slots off it.
1692
+ */
1693
+ function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1694
+ theory: Schema<Rels, Classes>
1695
+ ): QueryStart<Rels, Classes> {
1696
+ return makeQueryStart<Rels, Classes, Record<never, never>, null>(theory, [], null)
1541
1697
  }
1542
1698
 
1543
1699
  /**
@@ -1670,7 +1826,7 @@ function taggedCmpLiteral(
1670
1826
  interface LowerContext {
1671
1827
  readonly theory: AnySchema
1672
1828
  readonly relationIds: ReadonlyMap<string, number>
1673
- readonly recIds: ReadonlyMap<RecData, number>
1829
+ readonly interiorIds: ReadonlyMap<string, number>
1674
1830
  readonly paramIds: ReadonlyMap<string, number>
1675
1831
  readonly params: ReadonlyMap<string, ParamEntry>
1676
1832
  }
@@ -1751,35 +1907,34 @@ function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEn
1751
1907
  }
1752
1908
 
1753
1909
  /**
1754
- * Lowers one idb atom: named bindings placed by HEAD order, `FieldId(i)` =
1755
- * head position i. Every head column of the rec must be bound (a missing key
1910
+ * Lowers one interior atom: named bindings placed by HEAD order, `FieldId(i)` =
1911
+ * head position i. Every head column of the table must be bound (a missing key
1756
1912
  * is refused pointed); the var-id assignment order is head order, so the
1757
1913
  * first-use numbering matches the name-keyed edition exactly.
1758
1914
  */
1759
- function lowerIdbAtom(
1915
+ function lowerInteriorAtom(
1760
1916
  ctx: LowerContext,
1761
- rec: RecData,
1917
+ target: DerivedTable,
1762
1918
  bindings: ReadonlyArray<{ readonly key: string; readonly ref: AnyVar }>,
1763
1919
  ids: VarIds
1764
1920
  ): AtomIr {
1765
- const pred = ctx.recIds.get(rec)
1766
- if (pred === undefined) {
1767
- throw errors.new(`query lowering: rec ${rec.name} was declared by a different program`)
1921
+ const interior = ctx.interiorIds.get(target.name)
1922
+ if (interior === undefined) {
1923
+ throw errors.new(`query lowering: derived table ${target.name} was not declared on this query`)
1768
1924
  }
1769
- const head = rec.rules[0]
1770
- if (head === undefined) {
1771
- throw errors.new(`query lowering: rec ${rec.name} has no rules`)
1925
+ if (target.finds.length === 0) {
1926
+ throw errors.new(`query lowering: derived table ${target.name} has no head`)
1772
1927
  }
1773
- const irBindings: Array<readonly [number, TermIr]> = head.finds.map(function lowerPosition(column, position) {
1928
+ const irBindings: Array<readonly [number, TermIr]> = target.finds.map(function lowerPosition(column, position) {
1774
1929
  const binding = bindings.find(function byKey(candidate) {
1775
1930
  return candidate.key === column.name
1776
1931
  })
1777
1932
  if (binding === undefined) {
1778
- throw errors.new(`query lowering: idb ${rec.name} omits head column ${column.name}`)
1933
+ throw errors.new(`query lowering: interior ${target.name} omits head column ${column.name}`)
1779
1934
  }
1780
1935
  return [position, { kind: "var", var: ids.of(binding.ref) } as const] as const
1781
1936
  })
1782
- return { source: { kind: "idb", pred }, bindings: irBindings }
1937
+ return { source: { kind: "interior", interior }, bindings: irBindings }
1783
1938
  }
1784
1939
 
1785
1940
  /** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
@@ -1821,25 +1976,17 @@ function cmpAnchorOf(ctx: LowerContext, sibling: CmpTermData): AnyField | "measu
1821
1976
 
1822
1977
  /** Lowers one comparison. */
1823
1978
  function lowerComparison(ctx: LowerContext, cmp: CmpData, ids: VarIds): ComparisonIr {
1824
- if (cmp.op === "allen") {
1825
- const maskData = cmp.mask
1826
- if (maskData === undefined) {
1827
- throw errors.new("query lowering: an allen comparison lost its mask")
1828
- }
1829
- const mask =
1830
- maskData.kind === "literal"
1831
- ? { kind: "literal" as const, mask: maskData.mask }
1832
- : { kind: "param" as const, param: paramIdOf(ctx, maskData.name) }
1979
+ if (cmp.op.kind === "allen") {
1833
1980
  return {
1834
- op: { kind: "allen", mask },
1981
+ op: { kind: "allen", mask: cmp.op.mask },
1835
1982
  lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
1836
1983
  rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
1837
1984
  }
1838
1985
  }
1839
1986
  return {
1840
- op: { kind: cmp.op },
1841
- lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, cmp.op),
1842
- rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, cmp.op)
1987
+ op: { kind: cmp.op.kind },
1988
+ lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, cmp.op.kind),
1989
+ rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, cmp.op.kind)
1843
1990
  }
1844
1991
  }
1845
1992
 
@@ -1868,16 +2015,12 @@ function lowerFind(entry: FindEntryData, ids: VarIds): FindTermIr {
1868
2015
  switch (agg.op) {
1869
2016
  case "count":
1870
2017
  return { kind: "aggregate", op: { kind: "count" } }
1871
- case "countDistinct":
1872
- return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) }
1873
2018
  case "fold": {
1874
2019
  if ("duration" in agg.over) {
1875
2020
  return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) }
1876
2021
  }
1877
2022
  return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) }
1878
2023
  }
1879
- case "arg":
1880
- return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) }
1881
2024
  case "pack":
1882
2025
  return { kind: "aggregate", op: { kind: "pack" }, over: ids.of(agg.over) }
1883
2026
  }
@@ -1888,12 +2031,8 @@ function headOpOf(agg: AggData): HeadOpIr {
1888
2031
  switch (agg.op) {
1889
2032
  case "count":
1890
2033
  return "count"
1891
- case "countDistinct":
1892
- return "countDistinct"
1893
2034
  case "fold":
1894
2035
  return agg.fold
1895
- case "arg":
1896
- return agg.direction
1897
2036
  case "pack":
1898
2037
  return "pack"
1899
2038
  }
@@ -1924,9 +2063,12 @@ function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
1924
2063
  negated.push(lowerAtom(ctx, item.atom, ids))
1925
2064
  break
1926
2065
  }
1927
- case "idb": {
1928
- const bucket = item.negated ? negated : atoms
1929
- bucket.push(lowerIdbAtom(ctx, item.rec, item.bindings, ids))
2066
+ case "interior": {
2067
+ atoms.push(lowerInteriorAtom(ctx, item.target, item.bindings, ids))
2068
+ break
2069
+ }
2070
+ case "negatedInterior": {
2071
+ negated.push(lowerInteriorAtom(ctx, item.target, item.bindings, ids))
1930
2072
  break
1931
2073
  }
1932
2074
  case "cond": {
@@ -1946,24 +2088,27 @@ function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
1946
2088
  }
1947
2089
 
1948
2090
  /**
1949
- * Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
1950
- * recs in declaration order (`PredId` = index), the output predicate
1951
- * appended last. Every registered param must carry a field anchor by now.
2091
+ * Lowers a query value to the bridge's `QueryIr` — pure and stable: interiors
2092
+ * in declaration order, optional rec, then main. Every registered param must
2093
+ * carry a field anchor by now.
1952
2094
  */
1953
- function lowerQuery(q: AnyQuery): ProgramIr {
2095
+ function lowerQuery(q: AnyQuery): ParsedQuery {
1954
2096
  const theory = q.schema
1955
2097
  const relationIds = new Map<string, number>()
1956
2098
  Object.keys(theory.relations).forEach(function assignOrdinal(name, index) {
1957
2099
  relationIds.set(name, index)
1958
2100
  })
1959
- const recIds = new Map<RecData, number>()
1960
- q.data.recs.forEach(function assignPredId(rec, index) {
1961
- recIds.set(rec, index)
2101
+ const interiorIds = new Map<string, number>()
2102
+ q.data.interiors.forEach(function assignInteriorId(interior, index) {
2103
+ interiorIds.set(interior.name, index)
1962
2104
  })
2105
+ if (q.data.rec !== null) {
2106
+ interiorIds.set(q.data.rec.name, q.data.interiors.length)
2107
+ }
1963
2108
  const paramIds = new Map<string, number>()
1964
2109
  const params = new Map<string, ParamEntry>()
1965
2110
  q.data.params.forEach(function assignParamId(entry, index) {
1966
- if (entry.anchor === undefined && entry.shape !== "mask") {
2111
+ if (entry.anchor === undefined) {
1967
2112
  throw errors.new(
1968
2113
  `query param ${entry.name} has no field-anchored use — bind it in an atom or compare it against a bound variable`
1969
2114
  )
@@ -1971,26 +2116,33 @@ function lowerQuery(q: AnyQuery): ProgramIr {
1971
2116
  paramIds.set(entry.name, index)
1972
2117
  params.set(entry.name, entry)
1973
2118
  })
1974
- const ctx: LowerContext = { theory, relationIds, recIds, paramIds, params }
1975
- const predicates: PredicateDefIr[] = q.data.recs.map(function lowerRec(rec) {
1976
- const head = rec.rules[0]
1977
- if (head === undefined) {
1978
- throw errors.new(`query lowering: rec ${rec.name} has no rules`)
1979
- }
1980
- return {
1981
- head: head.finds.map(headTermOf),
1982
- rules: rec.rules.map(function lowerRecRule(rule) {
1983
- return lowerRule(ctx, rule)
1984
- })
1985
- }
1986
- })
1987
- predicates.push({
2119
+ const ctx: LowerContext = { theory, relationIds, interiorIds, paramIds, params }
2120
+ return parseQueryIr({
2121
+ interiors: q.data.interiors.map(function lowerInterior(interior) {
2122
+ return {
2123
+ head: interior.finds.map(headTermOf),
2124
+ rules: interior.rules.map(function lowerInteriorRule(rule) {
2125
+ return lowerRule(ctx, rule)
2126
+ })
2127
+ }
2128
+ }),
2129
+ rec:
2130
+ q.data.rec === null
2131
+ ? null
2132
+ : {
2133
+ head: q.data.rec.finds.map(headTermOf),
2134
+ base: q.data.rec.base.map(function lowerBase(rule) {
2135
+ return lowerRule(ctx, rule)
2136
+ }),
2137
+ rec: q.data.rec.rec.map(function lowerRecArm(rule) {
2138
+ return lowerRule(ctx, rule)
2139
+ })
2140
+ },
1988
2141
  head: q.data.finds.map(headTermOf),
1989
- rules: q.data.rules.map(function lowerOutputRule(rule) {
2142
+ rules: q.data.rules.map(function lowerMainRule(rule) {
1990
2143
  return lowerRule(ctx, rule)
1991
2144
  })
1992
2145
  })
1993
- return { predicates, output: q.data.recs.length }
1994
2146
  }
1995
2147
 
1996
2148
  export type {
@@ -1998,10 +2150,9 @@ export type {
1998
2150
  AnyRuleValue,
1999
2151
  HeadOf,
2000
2152
  HeadShape,
2001
- OutputRuleChain,
2002
- OutputRuleScope,
2153
+ InteriorRuleChain,
2154
+ InteriorRuleScope,
2003
2155
  ParamsOf,
2004
- ProgramState,
2005
2156
  Query,
2006
2157
  QueryData,
2007
2158
  QueryParams,
@@ -2012,11 +2163,10 @@ export type {
2012
2163
  QueryStart,
2013
2164
  RawChain,
2014
2165
  RawScope,
2015
- RecRef,
2016
2166
  RecRuleChain,
2017
2167
  RecRuleScope,
2018
2168
  RowOf,
2019
2169
  RuleValue,
2020
2170
  TermOps
2021
2171
  }
2022
- export { lowerQuery, makeOutputRuleScope, makeQuery, makeRawScope, query, taggedCmpLiteral, taggedLiteral }
2172
+ export { lowerQuery, makeRawScope, query, taggedCmpLiteral, taggedLiteral }