@bjornpagen/bumbledb 0.9.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 (73) hide show
  1. package/COOKBOOK.md +155 -136
  2. package/README.md +3 -7
  3. package/dist/capacity.d.ts +14 -1
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js.map +1 -1
  6. package/dist/db.d.ts +77 -109
  7. package/dist/db.d.ts.map +1 -1
  8. package/dist/db.js +121 -339
  9. package/dist/db.js.map +1 -1
  10. package/dist/index.d.ts +11 -16
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +7 -10
  13. package/dist/index.js.map +1 -1
  14. package/dist/lower.d.ts.map +1 -1
  15. package/dist/lower.js +8 -1
  16. package/dist/lower.js.map +1 -1
  17. package/dist/native.d.ts +69 -50
  18. package/dist/native.d.ts.map +1 -1
  19. package/dist/native.js.map +1 -1
  20. package/dist/query/atom.d.ts +162 -122
  21. package/dist/query/atom.d.ts.map +1 -1
  22. package/dist/query/atom.js +26 -22
  23. package/dist/query/atom.js.map +1 -1
  24. package/dist/query/find.d.ts +18 -35
  25. package/dist/query/find.d.ts.map +1 -1
  26. package/dist/query/find.js +13 -32
  27. package/dist/query/find.js.map +1 -1
  28. package/dist/query/lower.d.ts +113 -122
  29. package/dist/query/lower.d.ts.map +1 -1
  30. package/dist/query/lower.js +336 -260
  31. package/dist/query/lower.js.map +1 -1
  32. package/dist/query/parse-ir.d.ts +12 -0
  33. package/dist/query/parse-ir.d.ts.map +1 -0
  34. package/dist/query/parse-ir.js +71 -0
  35. package/dist/query/parse-ir.js.map +1 -0
  36. package/dist/query/run.d.ts +2 -2
  37. package/dist/query/run.d.ts.map +1 -1
  38. package/dist/query/run.js +2 -13
  39. package/dist/query/run.js.map +1 -1
  40. package/dist/query/scope.d.ts +4 -16
  41. package/dist/query/scope.d.ts.map +1 -1
  42. package/dist/query/scope.js +1 -6
  43. package/dist/query/scope.js.map +1 -1
  44. package/dist/schema.js +2 -2
  45. package/dist/schema.js.map +1 -1
  46. package/dist/statements.d.ts +15 -9
  47. package/dist/statements.d.ts.map +1 -1
  48. package/dist/statements.js +14 -9
  49. package/dist/statements.js.map +1 -1
  50. package/package.json +2 -2
  51. package/src/capacity.ts +24 -1
  52. package/src/db.ts +182 -443
  53. package/src/index.ts +9 -23
  54. package/src/lower.ts +8 -1
  55. package/src/native.ts +69 -42
  56. package/src/query/atom.ts +255 -165
  57. package/src/query/find.ts +39 -80
  58. package/src/query/lower.ts +578 -432
  59. package/src/query/parse-ir.ts +82 -0
  60. package/src/query/run.ts +2 -14
  61. package/src/query/scope.ts +3 -21
  62. package/src/schema.ts +2 -2
  63. package/src/statements.ts +33 -17
  64. package/dist/order.d.ts +0 -87
  65. package/dist/order.d.ts.map +0 -1
  66. package/dist/order.js +0 -153
  67. package/dist/order.js.map +0 -1
  68. package/dist/query/predicate.d.ts +0 -91
  69. package/dist/query/predicate.d.ts.map +0 -1
  70. package/dist/query/predicate.js +0 -156
  71. package/dist/query/predicate.js.map +0 -1
  72. package/src/order.ts +0 -234
  73. 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,12 +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.
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.
1331
1342
  */
1332
- 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[] {
1333
1348
  const order: string[] = []
1334
1349
  const byName = new Map<
1335
1350
  string,
@@ -1338,7 +1353,6 @@ function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]):
1338
1353
  anchor: ParamEntry["anchor"]
1339
1354
  op: ParamEntry["op"]
1340
1355
  members: readonly string[] | undefined
1341
- orderOp: "lt" | "le" | "gt" | "ge" | "pointIn" | undefined
1342
1356
  }
1343
1357
  >()
1344
1358
  function fold(uses: readonly ParamUse[]): void {
@@ -1350,8 +1364,7 @@ function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]):
1350
1364
  shape: use.shape,
1351
1365
  anchor: use.anchor,
1352
1366
  op: use.op,
1353
- members: use.members,
1354
- orderOp: isOrderOp(use.op) ? use.op : undefined
1367
+ members: use.members
1355
1368
  })
1356
1369
  continue
1357
1370
  }
@@ -1378,13 +1391,18 @@ function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]):
1378
1391
  existing.anchor = use.anchor
1379
1392
  existing.op = use.op
1380
1393
  }
1381
- if (existing.orderOp === undefined && isOrderOp(use.op)) {
1382
- existing.orderOp = use.op
1383
- }
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
  }
@@ -1397,10 +1415,6 @@ function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]):
1397
1415
  if (entry === undefined) {
1398
1416
  throw errors.new(`query param ${name} lost its registry entry`)
1399
1417
  }
1400
- const anchorRoster = anchorRosterOf(entry.anchor)
1401
- if (entry.orderOp !== undefined && anchorRoster !== undefined) {
1402
- throw closedOrderError(`query param ${name}`, `its ${entry.orderOp} use's anchor`, anchorRoster.name)
1403
- }
1404
1418
  /**
1405
1419
  * A membership array's handle names are program constants, so the
1406
1420
  * entry stores the resolved IMAGE: each name rides the one
@@ -1434,114 +1448,252 @@ interface RawQuery {
1434
1448
  readonly schema: AnySchema
1435
1449
  readonly data: QueryData
1436
1450
  rule(build: (r: RawScope) => RuleValue<never, never>): RawQuery
1451
+ interior(name: string, ...builds: never[]): never
1452
+ recursive(name: string, arms: never): never
1437
1453
  }
1438
1454
 
1439
- /**
1440
- * Assembles the runtime query value over completed rules: every rule must
1441
- * derive the SAME head (name and aggregate shape, position for position —
1442
- * the decode labels and the engine's alignment rule agree), and the param
1443
- * registry folds in program-walk order.
1444
- */
1445
- 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 {
1446
1457
  const first = rules[0]
1447
1458
  if (first === undefined) {
1448
- throw errors.new("a query needs at least one rule")
1459
+ throw errors.new(`${label} needs at least one rule`)
1449
1460
  }
1450
1461
  const signature = first.finds.map(headSignature).join(", ")
1451
1462
  rules.forEach(function verifyHead(rule, index) {
1452
1463
  const candidate = rule.finds.map(headSignature).join(", ")
1453
1464
  if (candidate !== signature) {
1454
1465
  throw errors.new(
1455
- `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})`
1456
1467
  )
1457
1468
  }
1458
- // The closed slice is part of the head too: one answer column decodes
1459
- // through one roster, so a union whose rules bind a column at different
1460
- // vocabularies (or one closed, one bare) is refused pointed.
1461
1469
  rule.finds.forEach(function verifyClosedSlice(column, position) {
1462
1470
  const lead = first.finds[position]
1463
1471
  if (lead !== undefined && column.closed !== lead.closed) {
1464
1472
  throw errors.new(
1465
- `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)`
1466
1474
  )
1467
1475
  }
1468
- // The law-class wall on the union head: one answer column is one
1469
- // value space, so the classed mint slot each rule binds the column
1470
- // at must join across rules — the SAME fieldJoins judgment every
1471
- // join/eq/negated-atom position enforces (the SDK holds it because
1472
- // the wire IR carries no domains).
1473
1476
  if (lead === undefined) {
1474
1477
  return
1475
1478
  }
1476
1479
  if (lead.slot !== undefined && column.slot !== undefined && !fieldJoins(lead.slot, column.slot)) {
1477
1480
  throw errors.new(
1478
- `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)`
1479
1482
  )
1480
1483
  }
1481
1484
  })
1482
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 }
1483
1512
  const data: QueryData = Object.freeze({
1484
- recs: Object.freeze([...recs]),
1513
+ interiors: Object.freeze([...interiors]),
1514
+ rec,
1485
1515
  rules: Object.freeze([...rules]),
1486
1516
  finds: first.finds,
1487
- params: paramRegistryOf(recs, rules)
1517
+ params: paramRegistryOf(interiors, rec, rules)
1488
1518
  })
1489
1519
  const value: RawQuery = {
1490
1520
  schema: theory,
1491
1521
  data,
1492
1522
  rule(build) {
1493
- const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory }))
1494
- 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")
1495
1531
  }
1496
1532
  }
1497
1533
  Object.freeze(value)
1498
1534
  return value
1499
1535
  }
1500
1536
 
1501
- /**
1502
- * The query values' trusted admission seam (the {@link isTypedScope} pattern):
1503
- * the checkable fact — the value was assembled over the identical theory —
1504
- * is verified before the raw value is admitted at its typed face.
1505
- */
1506
- function isQueryValue<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
1507
- theory: Schema<Rels, Classes>,
1508
- value: RawQuery
1509
- ): value is RawQuery & Query<Rels, Row, P, Classes> {
1510
- return value.schema === theory
1511
- }
1512
-
1513
- /** Assembles one typed query value (rules already completed). */
1514
1537
  function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
1515
1538
  theory: Schema<Rels, Classes>,
1516
- recs: readonly RecData[],
1539
+ interiors: readonly InteriorData[],
1540
+ rec: RecData | null,
1517
1541
  rules: readonly RuleData[]
1518
1542
  ): Query<Rels, Row, P, Classes> {
1519
- const raw = makeRawQuery(theory, recs, rules)
1520
- if (!isQueryValue<Rels, Row, P, Classes>(theory, raw)) {
1521
- 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`)
1522
1555
  }
1523
- 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) })
1524
1565
  }
1525
1566
 
1526
- /**
1527
- * Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule` adds
1528
- * one conjunctive rule; multiple rules are the set union. The schema's
1529
- * law-computed class map and theory value ride into every rule builder — the
1530
- * join walls compare against the mint slots off it.
1531
- */
1532
- function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1533
- theory: Schema<Rels, Classes>
1534
- ): QueryStart<Rels, Classes> {
1535
- 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
+ },
1536
1675
  rule<RV extends AnyRuleValue>(
1537
1676
  build: (r: QueryRuleScope<Rels, Classes>) => RV
1538
- ): Query<Rels, RowOf<RV>, ParamsOf<RV>, Classes> {
1539
- const built = build(makeQueryRuleScope<Rels, Classes>(theory))
1540
- 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])
1541
1680
  }
1542
1681
  }
1543
1682
  Object.freeze(start)
1544
- 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)
1545
1697
  }
1546
1698
 
1547
1699
  /**
@@ -1674,7 +1826,7 @@ function taggedCmpLiteral(
1674
1826
  interface LowerContext {
1675
1827
  readonly theory: AnySchema
1676
1828
  readonly relationIds: ReadonlyMap<string, number>
1677
- readonly recIds: ReadonlyMap<RecData, number>
1829
+ readonly interiorIds: ReadonlyMap<string, number>
1678
1830
  readonly paramIds: ReadonlyMap<string, number>
1679
1831
  readonly params: ReadonlyMap<string, ParamEntry>
1680
1832
  }
@@ -1755,35 +1907,34 @@ function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEn
1755
1907
  }
1756
1908
 
1757
1909
  /**
1758
- * Lowers one idb atom: named bindings placed by HEAD order, `FieldId(i)` =
1759
- * 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
1760
1912
  * is refused pointed); the var-id assignment order is head order, so the
1761
1913
  * first-use numbering matches the name-keyed edition exactly.
1762
1914
  */
1763
- function lowerIdbAtom(
1915
+ function lowerInteriorAtom(
1764
1916
  ctx: LowerContext,
1765
- rec: RecData,
1917
+ target: DerivedTable,
1766
1918
  bindings: ReadonlyArray<{ readonly key: string; readonly ref: AnyVar }>,
1767
1919
  ids: VarIds
1768
1920
  ): AtomIr {
1769
- const pred = ctx.recIds.get(rec)
1770
- if (pred === undefined) {
1771
- 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`)
1772
1924
  }
1773
- const head = rec.rules[0]
1774
- if (head === undefined) {
1775
- 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`)
1776
1927
  }
1777
- 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) {
1778
1929
  const binding = bindings.find(function byKey(candidate) {
1779
1930
  return candidate.key === column.name
1780
1931
  })
1781
1932
  if (binding === undefined) {
1782
- 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}`)
1783
1934
  }
1784
1935
  return [position, { kind: "var", var: ids.of(binding.ref) } as const] as const
1785
1936
  })
1786
- return { source: { kind: "idb", pred }, bindings: irBindings }
1937
+ return { source: { kind: "interior", interior }, bindings: irBindings }
1787
1938
  }
1788
1939
 
1789
1940
  /** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
@@ -1825,25 +1976,17 @@ function cmpAnchorOf(ctx: LowerContext, sibling: CmpTermData): AnyField | "measu
1825
1976
 
1826
1977
  /** Lowers one comparison. */
1827
1978
  function lowerComparison(ctx: LowerContext, cmp: CmpData, ids: VarIds): ComparisonIr {
1828
- if (cmp.op === "allen") {
1829
- const maskData = cmp.mask
1830
- if (maskData === undefined) {
1831
- throw errors.new("query lowering: an allen comparison lost its mask")
1832
- }
1833
- const mask =
1834
- maskData.kind === "literal"
1835
- ? { kind: "literal" as const, mask: maskData.mask }
1836
- : { kind: "param" as const, param: paramIdOf(ctx, maskData.name) }
1979
+ if (cmp.op.kind === "allen") {
1837
1980
  return {
1838
- op: { kind: "allen", mask },
1981
+ op: { kind: "allen", mask: cmp.op.mask },
1839
1982
  lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
1840
1983
  rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
1841
1984
  }
1842
1985
  }
1843
1986
  return {
1844
- op: { kind: cmp.op },
1845
- lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, cmp.op),
1846
- 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)
1847
1990
  }
1848
1991
  }
1849
1992
 
@@ -1872,16 +2015,12 @@ function lowerFind(entry: FindEntryData, ids: VarIds): FindTermIr {
1872
2015
  switch (agg.op) {
1873
2016
  case "count":
1874
2017
  return { kind: "aggregate", op: { kind: "count" } }
1875
- case "countDistinct":
1876
- return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) }
1877
2018
  case "fold": {
1878
2019
  if ("duration" in agg.over) {
1879
2020
  return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) }
1880
2021
  }
1881
2022
  return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) }
1882
2023
  }
1883
- case "arg":
1884
- return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) }
1885
2024
  case "pack":
1886
2025
  return { kind: "aggregate", op: { kind: "pack" }, over: ids.of(agg.over) }
1887
2026
  }
@@ -1892,12 +2031,8 @@ function headOpOf(agg: AggData): HeadOpIr {
1892
2031
  switch (agg.op) {
1893
2032
  case "count":
1894
2033
  return "count"
1895
- case "countDistinct":
1896
- return "countDistinct"
1897
2034
  case "fold":
1898
2035
  return agg.fold
1899
- case "arg":
1900
- return agg.direction
1901
2036
  case "pack":
1902
2037
  return "pack"
1903
2038
  }
@@ -1928,9 +2063,12 @@ function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
1928
2063
  negated.push(lowerAtom(ctx, item.atom, ids))
1929
2064
  break
1930
2065
  }
1931
- case "idb": {
1932
- const bucket = item.negated ? negated : atoms
1933
- 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))
1934
2072
  break
1935
2073
  }
1936
2074
  case "cond": {
@@ -1950,24 +2088,27 @@ function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
1950
2088
  }
1951
2089
 
1952
2090
  /**
1953
- * Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
1954
- * recs in declaration order (`PredId` = index), the output predicate
1955
- * 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.
1956
2094
  */
1957
- function lowerQuery(q: AnyQuery): ProgramIr {
2095
+ function lowerQuery(q: AnyQuery): ParsedQuery {
1958
2096
  const theory = q.schema
1959
2097
  const relationIds = new Map<string, number>()
1960
2098
  Object.keys(theory.relations).forEach(function assignOrdinal(name, index) {
1961
2099
  relationIds.set(name, index)
1962
2100
  })
1963
- const recIds = new Map<RecData, number>()
1964
- q.data.recs.forEach(function assignPredId(rec, index) {
1965
- 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)
1966
2104
  })
2105
+ if (q.data.rec !== null) {
2106
+ interiorIds.set(q.data.rec.name, q.data.interiors.length)
2107
+ }
1967
2108
  const paramIds = new Map<string, number>()
1968
2109
  const params = new Map<string, ParamEntry>()
1969
2110
  q.data.params.forEach(function assignParamId(entry, index) {
1970
- if (entry.anchor === undefined && entry.shape !== "mask") {
2111
+ if (entry.anchor === undefined) {
1971
2112
  throw errors.new(
1972
2113
  `query param ${entry.name} has no field-anchored use — bind it in an atom or compare it against a bound variable`
1973
2114
  )
@@ -1975,26 +2116,33 @@ function lowerQuery(q: AnyQuery): ProgramIr {
1975
2116
  paramIds.set(entry.name, index)
1976
2117
  params.set(entry.name, entry)
1977
2118
  })
1978
- const ctx: LowerContext = { theory, relationIds, recIds, paramIds, params }
1979
- const predicates: PredicateDefIr[] = q.data.recs.map(function lowerRec(rec) {
1980
- const head = rec.rules[0]
1981
- if (head === undefined) {
1982
- throw errors.new(`query lowering: rec ${rec.name} has no rules`)
1983
- }
1984
- return {
1985
- head: head.finds.map(headTermOf),
1986
- rules: rec.rules.map(function lowerRecRule(rule) {
1987
- return lowerRule(ctx, rule)
1988
- })
1989
- }
1990
- })
1991
- 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
+ },
1992
2141
  head: q.data.finds.map(headTermOf),
1993
- rules: q.data.rules.map(function lowerOutputRule(rule) {
2142
+ rules: q.data.rules.map(function lowerMainRule(rule) {
1994
2143
  return lowerRule(ctx, rule)
1995
2144
  })
1996
2145
  })
1997
- return { predicates, output: q.data.recs.length }
1998
2146
  }
1999
2147
 
2000
2148
  export type {
@@ -2002,10 +2150,9 @@ export type {
2002
2150
  AnyRuleValue,
2003
2151
  HeadOf,
2004
2152
  HeadShape,
2005
- OutputRuleChain,
2006
- OutputRuleScope,
2153
+ InteriorRuleChain,
2154
+ InteriorRuleScope,
2007
2155
  ParamsOf,
2008
- ProgramState,
2009
2156
  Query,
2010
2157
  QueryData,
2011
2158
  QueryParams,
@@ -2016,11 +2163,10 @@ export type {
2016
2163
  QueryStart,
2017
2164
  RawChain,
2018
2165
  RawScope,
2019
- RecRef,
2020
2166
  RecRuleChain,
2021
2167
  RecRuleScope,
2022
2168
  RowOf,
2023
2169
  RuleValue,
2024
2170
  TermOps
2025
2171
  }
2026
- export { lowerQuery, makeOutputRuleScope, makeQuery, makeRawScope, query, taggedCmpLiteral, taggedLiteral }
2172
+ export { lowerQuery, makeRawScope, query, taggedCmpLiteral, taggedLiteral }