@bjornpagen/bumbledb 0.10.0 → 0.12.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 +5 -9
  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 +12 -17
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +8 -11
  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 +77 -51
  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 +128 -124
  26. package/dist/query/lower.d.ts.map +1 -1
  27. package/dist/query/lower.js +366 -258
  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 +11 -24
  50. package/src/lower.ts +8 -1
  51. package/src/native.ts +79 -43
  52. package/src/query/atom.ts +105 -138
  53. package/src/query/find.ts +39 -80
  54. package/src/query/lower.ts +626 -434
  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 rec 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,72 +254,80 @@ 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 `.reach("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 rec 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
+ * rec 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
- 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). */
336
- readonly rules: readonly RuleData[]
337
- /** The head columns (every rule derives the same head; written order = answer column order). */
338
- readonly finds: readonly FindColumn[]
339
- /** The registered params in first-use order across the program walk (= dense `ParamId`s). */
340
- readonly params: readonly ParamEntry[]
341
- }
306
+ type QueryData =
307
+ | {
308
+ readonly kind: "cq"
309
+ /** Named interiors in declaration order (DAG). */
310
+ readonly interiors: readonly InteriorData[]
311
+ /** The main rules in written order (multiple rules = set union). */
312
+ readonly rules: readonly RuleData[]
313
+ /** The head columns (every rule derives the same head; written order = answer column order). */
314
+ readonly finds: readonly FindColumn[]
315
+ /** The registered params in first-use order across the query walk (= dense `ParamId`s). */
316
+ readonly params: readonly ParamEntry[]
317
+ }
318
+ | {
319
+ readonly kind: "reach"
320
+ /** Named interiors in declaration order (DAG). */
321
+ readonly interiors: readonly InteriorData[]
322
+ /** The linear rec (base and rec arms nonempty by type). */
323
+ readonly rec: RecData
324
+ /** The main rules in written order (multiple rules = set union). */
325
+ readonly rules: readonly RuleData[]
326
+ /** The head columns (every rule derives the same head; written order = answer column order). */
327
+ readonly finds: readonly FindColumn[]
328
+ /** The registered params in first-use order across the query walk (= dense `ParamId`s). */
329
+ readonly params: readonly ParamEntry[]
330
+ }
342
331
 
343
332
  /**
344
333
  * An inert query value. `Row` is the inferred answer-row object type;
@@ -357,6 +346,10 @@ interface Query<
357
346
  rule<RV extends AnyRuleValue>(
358
347
  build: (r: QueryRuleScope<Rels, Classes>) => RV
359
348
  ): Query<Rels, Row | RowOf<RV>, Flatten<Params & ParamsOf<RV>>, Classes>
349
+ /** Construction error: interiors precede main rules. Uncallable after `.rule()`. */
350
+ interior(name: string, ...builds: never[]): never
351
+ /** Construction error: reach precedes main rules. Uncallable after `.rule()`. */
352
+ reach(name: string, arms: never): never
360
353
  readonly [inferred]?: { readonly row: Row; readonly params: Params }
361
354
  }
362
355
 
@@ -372,18 +365,44 @@ type QueryRow<Q extends AnyQuery> = RowOf<Q>
372
365
  /** Extracts a query value's inferred execute-params type. */
373
366
  type QueryParams<Q extends AnyQuery> = ParamsOf<Q>
374
367
 
375
- /** The entry value of `query(S)`: the first `.rule` mints the query. */
376
- interface QueryStart<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> {
368
+ /**
369
+ * The entry value of `query(S)`: interiors, then reach (moves to
370
+ * {@link QueryReachStart}), then the first `.rule` mints the query.
371
+ * `interior` / `reach` exist only on this CQ start.
372
+ */
373
+ type QueryStart<
374
+ Rels extends SchemaRelations,
375
+ Classes extends SchemaClasses = SchemaClasses,
376
+ P extends ParamsRecord = Record<never, never>
377
+ > = {
377
378
  rule<RV extends AnyRuleValue>(
378
379
  build: (r: QueryRuleScope<Rels, Classes>) => RV
379
- ): Query<Rels, RowOf<RV>, ParamsOf<RV>, Classes>
380
+ ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes>
381
+ interior<const Builds extends readonly InteriorBuild<Rels, Classes>[]>(
382
+ name: string,
383
+ ...builds: Builds
384
+ ): QueryStart<Rels, Classes, Flatten<P & BuildsParams<Builds>>>
385
+ reach<const Base extends readonly RecBuild<Rels, Classes>[], const Step extends readonly RecBuild<Rels, Classes>[]>(
386
+ name: string,
387
+ arms: { readonly base: Base; readonly rec: Step }
388
+ ): QueryReachStart<Rels, Classes, Flatten<P & BuildsParams<Base> & BuildsParams<Step>>>
389
+ }
390
+
391
+ /** After `.reach()`: interior/reach are unrepresentable; only `.rule` remains. */
392
+ type QueryReachStart<
393
+ Rels extends SchemaRelations,
394
+ Classes extends SchemaClasses = SchemaClasses,
395
+ P extends ParamsRecord = Record<never, never>
396
+ > = {
397
+ rule<RV extends AnyRuleValue>(
398
+ build: (r: QueryRuleScope<Rels, Classes>) => RV
399
+ ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes>
380
400
  }
381
401
 
382
402
  /** The frozen constructor vocabulary every rule builder spreads. */
383
403
  const termOps: TermOps = Object.freeze({
384
404
  param: makeParam,
385
405
  inSet: makeSetParam,
386
- maskParam: makeMaskParam,
387
406
  duration: makeDuration,
388
407
  eq,
389
408
  ne,
@@ -397,12 +416,9 @@ const termOps: TermOps = Object.freeze({
397
416
  or,
398
417
  not,
399
418
  count,
400
- countDistinct,
401
419
  sum,
402
420
  min,
403
421
  max,
404
- argMax,
405
- argMin,
406
422
  pack
407
423
  })
408
424
 
@@ -558,10 +574,6 @@ function resolveBindings(
558
574
  )
559
575
  break
560
576
  }
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
577
  case "duration":
566
578
  throw errors.new(
567
579
  `${label}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and find entries`
@@ -607,7 +619,7 @@ function advanceMatch(
607
619
  }
608
620
 
609
621
  /** Resolves one comparison side to its runtime term (variables and the measure ride by reference). */
610
- function cmpTermDataOf(op: string, value: unknown): CmpTermData {
622
+ function cmpTermDataOf(value: unknown): CmpTermData {
611
623
  if (isTerm(value)) {
612
624
  switch (value[term]) {
613
625
  case "var":
@@ -618,8 +630,6 @@ function cmpTermDataOf(op: string, value: unknown): CmpTermData {
618
630
  return Object.freeze({ kind: "setParam" as const, name: value.name })
619
631
  case "duration":
620
632
  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
633
  }
624
634
  }
625
635
  return Object.freeze({ kind: "literal" as const, value })
@@ -656,31 +666,23 @@ function sideUses(op: CmpKind, side: CmpTermData, sibling: CmpTermData, uses: Pa
656
666
  /** Lowers one condition VALUE to its runtime data, recording param uses. */
657
667
  function condDataOf(cond: AnyCond, uses: ParamUse[]): CondData {
658
668
  if (cond.cond === "cmp") {
659
- const lhs = cmpTermDataOf(cond.op, cond.lhs)
660
- const rhs = cmpTermDataOf(cond.op, cond.rhs)
669
+ const lhs = cmpTermDataOf(cond.lhs)
670
+ const rhs = cmpTermDataOf(cond.rhs)
661
671
  sideUses(cond.op, lhs, rhs, uses)
662
672
  sideUses(cond.op, rhs, lhs, uses)
663
- let mask: MaskData | undefined
664
673
  if (cond.op === "allen") {
665
674
  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")
675
+ if (typeof maskValue !== "number") {
676
+ throw errors.new("allen: the mask position takes a 13-bit mask number built from the ALLEN constants")
681
677
  }
678
+ return Object.freeze({
679
+ kind: "cmp" as const,
680
+ op: { kind: "allen" as const, mask: maskValue },
681
+ lhs,
682
+ rhs
683
+ })
682
684
  }
683
- return Object.freeze({ kind: "cmp" as const, op: cond.op, mask, lhs, rhs })
685
+ return Object.freeze({ kind: "cmp" as const, op: { kind: cond.op }, lhs, rhs })
684
686
  }
685
687
  if (cond.cond === "tree") {
686
688
  const children = cond.children.map(function lowerChild(child) {
@@ -698,13 +700,13 @@ function advanceWhere(context: ChainContext, state: RuleBuildState, cond: AnyCon
698
700
  if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
699
701
  throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom")
700
702
  }
701
- if (cond.cond === "notIdb") {
703
+ if (cond.cond === "notInterior") {
702
704
  const bindings: Readonly<Record<string, unknown>> = Object.fromEntries(
703
705
  Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
704
706
  return value !== undefined
705
707
  })
706
708
  )
707
- return notIdbAdvance(context, state, cond.target, bindings)
709
+ return notInteriorAdvance(context, state, cond.name, bindings)
708
710
  }
709
711
  if (cond.cond === "not") {
710
712
  const relation: MatchOwner = cond.relation
@@ -730,19 +732,19 @@ function advanceWhere(context: ChainContext, state: RuleBuildState, cond: AnyCon
730
732
  }
731
733
 
732
734
  /**
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
735
+ * Extends a rule state with one interior atom (a named record over head keys;
736
+ * vars validated at completion). A POSITIVE interior atom is a positive
737
+ * occurrence exactly as the engine represents it (`check_atoms` walks Interior
736
738
  * and Edb in one loop), so its variables GROUND: they enter the rule's
737
739
  * boundness set, may ride the head, and satisfy negation safety — the
738
- * idb-only identity projection of a finished stratum is spellable with no
740
+ * interior-only identity projection of a finished table is spellable with no
739
741
  * re-grounding join. A NEGATED one binds nothing, only rejects.
740
742
  */
741
- function advanceIdb(
743
+ function advanceInterior(
742
744
  state: RuleBuildState,
743
- rec: RecData,
745
+ target: DerivedTable,
744
746
  bindings: Readonly<Record<string, unknown>>,
745
- negated: boolean
747
+ kind: "interior" | "negatedInterior"
746
748
  ): RuleBuildState {
747
749
  const resolved: Array<{ readonly key: string; readonly ref: AnyVar }> = []
748
750
  for (const [key, value] of Object.entries(bindings)) {
@@ -751,31 +753,26 @@ function advanceIdb(
751
753
  }
752
754
  if (!isTerm(value) || value[term] !== "var") {
753
755
  throw errors.new(
754
- `idb ${rec.name}: position ${key} takes a variable — bind literals and params through where()/match()`
756
+ `interior ${target.name}: position ${key} takes a variable — bind literals and params through where()/match()`
755
757
  )
756
758
  }
757
759
  resolved.push(Object.freeze({ key, ref: value }))
758
760
  }
759
761
  const bound = new Set(state.bound)
760
- if (!negated) {
762
+ if (kind === "interior") {
761
763
  for (const binding of resolved) {
762
764
  bound.add(binding.ref)
763
765
  }
764
766
  }
765
767
  return Object.freeze({
766
- items: Object.freeze([
767
- ...state.items,
768
- Object.freeze({ kind: "idb" as const, rec, bindings: Object.freeze(resolved), negated })
769
- ]),
768
+ items: Object.freeze([...state.items, Object.freeze({ kind, target, bindings: Object.freeze(resolved) })]),
770
769
  bound,
771
770
  paramUses: state.paramUses
772
771
  })
773
772
  }
774
773
 
775
774
  /** 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 } {
775
+ function isAggregateEntry(value: unknown): value is { readonly agg: string; readonly over: unknown } {
779
776
  return typeof value === "object" && value !== null && "agg" in value
780
777
  }
781
778
 
@@ -788,16 +785,11 @@ function asVarTerm(context: string, value: unknown): AnyVar {
788
785
  }
789
786
 
790
787
  /** 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 {
788
+ function aggDataOf(name: string, entry: { readonly agg: string; readonly over: unknown }): AggData {
795
789
  const over = entry.over
796
790
  switch (entry.agg) {
797
791
  case "count":
798
792
  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
793
  case "sum":
802
794
  case "min":
803
795
  case "max": {
@@ -809,14 +801,6 @@ function aggDataOf(
809
801
  }
810
802
  throw errors.new(`find ${name} (${entry.agg}): takes a variable or r.duration(v)`)
811
803
  }
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
804
  case "pack":
821
805
  return Object.freeze({ op: "pack" as const, over: asVarTerm(`find ${name} (pack)`, over) })
822
806
  default:
@@ -903,17 +887,14 @@ function assertNotClosed(where: string, position: string, ref: AnyVar): void {
903
887
 
904
888
  /**
905
889
  * 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.
890
+ * variable's mint slot. Counts, folds, `pack` and the measure derive
891
+ * numbers/intervals, so they resolve no slot.
908
892
  */
909
893
  function findColumnSlotOf(context: ChainContext, column: FindColumn): ClassedField | undefined {
910
894
  const entry = column.entry
911
895
  if (entry.kind === "var") {
912
896
  return mintSlotOf(context, entry.over)
913
897
  }
914
- if (entry.kind === "aggregate" && entry.agg.op === "arg") {
915
- return mintSlotOf(context, entry.agg.over)
916
- }
917
898
  return undefined
918
899
  }
919
900
 
@@ -934,9 +915,6 @@ function validateColumn(context: ChainContext, bound: ReadonlySet<AnyVar>, colum
934
915
  switch (agg.op) {
935
916
  case "count":
936
917
  return
937
- case "countDistinct":
938
- assertBound(where, bound, agg.over)
939
- return
940
918
  case "fold": {
941
919
  if ("duration" in agg.over) {
942
920
  assertBound(where, bound, agg.over.duration)
@@ -947,12 +925,6 @@ function validateColumn(context: ChainContext, bound: ReadonlySet<AnyVar>, colum
947
925
  assertNotClosed(where, `the ${agg.fold} input`, agg.over)
948
926
  return
949
927
  }
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
928
  case "pack":
957
929
  assertBound(where, bound, agg.over)
958
930
  assertInterval(where, agg.over)
@@ -972,8 +944,8 @@ function validateCond(context: ChainContext, bound: ReadonlySet<AnyVar>, cond: C
972
944
  if (side.kind === "var") {
973
945
  assertBound(label, bound, side.ref)
974
946
  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)
947
+ if (isOrderOp(cond.op.kind) && roster !== undefined) {
948
+ throw closedOrderError(label, `the ${cond.op.kind} side ${side.ref.label}`, roster.name)
977
949
  }
978
950
  }
979
951
  if (side.kind === "measure") {
@@ -981,14 +953,14 @@ function validateCond(context: ChainContext, bound: ReadonlySet<AnyVar>, cond: C
981
953
  assertInterval(label, side.ref)
982
954
  }
983
955
  }
984
- if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
956
+ if ((cond.op.kind === "eq" || cond.op.kind === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
985
957
  assertBound(label, bound, cond.lhs.ref)
986
958
  assertBound(label, bound, cond.rhs.ref)
987
959
  const lhs = mintSlotOf(context, cond.lhs.ref)
988
960
  const rhs = mintSlotOf(context, cond.rhs.ref)
989
961
  if (!fieldJoins(lhs, rhs)) {
990
962
  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)`
963
+ `${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
964
  )
993
965
  }
994
966
  }
@@ -1000,29 +972,27 @@ function validateCond(context: ChainContext, bound: ReadonlySet<AnyVar>, cond: C
1000
972
  }
1001
973
 
1002
974
  /**
1003
- * Validates one `idb` item: every head column of the rec is bound exactly
975
+ * Validates one interior item: every head column of the table is bound exactly
1004
976
  * 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
977
+ * its head column's classed slot. A POSITIVE interior atom GROUNDS its
1006
978
  * variables (a positive occurrence, exactly the engine's representation),
1007
979
  * so no boundness precondition exists; a NEGATED one binds nothing — its
1008
980
  * 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.
981
+ * safety rule as EDB negation. When the table's own first rule is in flight
982
+ * (`finds` empty), the completing rule's OWN find columns ARE the head.
1012
983
  */
1013
- function validateIdb(
984
+ function validateInterior(
1014
985
  context: ChainContext,
1015
986
  bound: ReadonlySet<AnyVar>,
1016
987
  item: {
1017
- readonly rec: RecData
988
+ readonly kind: "interior" | "negatedInterior"
989
+ readonly target: DerivedTable
1018
990
  readonly bindings: ReadonlyArray<{ readonly key: string; readonly ref: AnyVar }>
1019
- readonly negated: boolean
1020
991
  },
1021
992
  columns: readonly FindColumn[]
1022
993
  ): void {
1023
994
  const label = contextLabel(context)
1024
- const head = item.rec.rules[0]
1025
- const headColumns = head !== undefined ? head.finds : columns
995
+ const headColumns = item.target.finds.length > 0 ? item.target.finds : columns
1026
996
  const headNames = headColumns.map(function nameOf(column) {
1027
997
  return column.name
1028
998
  })
@@ -1032,21 +1002,21 @@ function validateIdb(
1032
1002
  for (const key of keys) {
1033
1003
  if (!headNames.includes(key)) {
1034
1004
  throw errors.new(
1035
- `${label}: idb ${item.rec.name} binds ${key}, not a head column of ${item.rec.name} (head columns: ${headNames.join(", ")})`
1005
+ `${label}: interior ${item.target.name} binds ${key}, not a head column of ${item.target.name} (head columns: ${headNames.join(", ")})`
1036
1006
  )
1037
1007
  }
1038
1008
  }
1039
1009
  for (const name of headNames) {
1040
1010
  if (!keys.includes(name)) {
1041
1011
  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}`
1012
+ `${label}: interior ${item.target.name} omits the head column ${name} — an interior join binds every head column of ${item.target.name}`
1043
1013
  )
1044
1014
  }
1045
1015
  }
1046
1016
  for (const binding of item.bindings) {
1047
- if (item.negated && !bound.has(binding.ref)) {
1017
+ if (item.kind === "negatedInterior" && !bound.has(binding.ref)) {
1048
1018
  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)`
1019
+ `${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
1020
  )
1051
1021
  }
1052
1022
  const headColumn = headColumns.find(function byName(column) {
@@ -1058,7 +1028,7 @@ function validateIdb(
1058
1028
  const mint = mintSlotOf(context, binding.ref)
1059
1029
  if (!fieldJoins(headColumn.slot, mint)) {
1060
1030
  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`
1031
+ `${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
1032
  )
1063
1033
  }
1064
1034
  }
@@ -1067,7 +1037,7 @@ function validateIdb(
1067
1037
  /**
1068
1038
  * Completes one rule: enriches the find columns (declaration-order-safe
1069
1039
  * keys, boundness validated, each column's classed slot and closed slice
1070
- * resolved), then walks the body walls — negated-atom boundness safety, idb
1040
+ * resolved), then walks the body walls — negated-atom boundness safety, interior
1071
1041
  * head pairing, and condition validation.
1072
1042
  */
1073
1043
  function completeRule(context: ChainContext, state: RuleBuildState, rawColumns: readonly FindColumn[]): RuleData {
@@ -1091,8 +1061,8 @@ function completeRule(context: ChainContext, state: RuleBuildState, rawColumns:
1091
1061
  }
1092
1062
  }
1093
1063
  }
1094
- if (item.kind === "idb") {
1095
- validateIdb(context, state.bound, item, columns)
1064
+ if (item.kind === "interior" || item.kind === "negatedInterior") {
1065
+ validateInterior(context, state.bound, item, columns)
1096
1066
  }
1097
1067
  if (item.kind === "cond") {
1098
1068
  validateCond(context, state.bound, item.cond)
@@ -1113,100 +1083,119 @@ function makeRuleValue<Row, P extends ParamsRecord>(rule: RuleData): RuleValue<R
1113
1083
  interface RawChain {
1114
1084
  match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
1115
1085
  where(cond: AnyCond): RawChain
1116
- idb(target: RecRef<string, ParamsRecord>, bindings: Readonly<Record<string, unknown>>): RawChain
1086
+ interior(name: string, bindings: Readonly<Record<string, unknown>>): RawChain
1117
1087
  find(entries: Readonly<Record<string, unknown>>): RuleValue<never, never>
1118
1088
  }
1119
1089
 
1120
1090
  /** The runtime rule-builder shape beneath every typed scope. */
1121
1091
  interface RawScope extends TermOps {
1122
1092
  match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
1123
- idb(target: RecRef<string, ParamsRecord>, bindings: Readonly<Record<string, unknown>>): RawChain
1093
+ interior(name: string, bindings: Readonly<Record<string, unknown>>): RawChain
1124
1094
  }
1125
1095
 
1096
+ /** The declared derived tables a chain may name. */
1097
+ type DerivedEnv =
1098
+ | { readonly interiors: readonly InteriorData[] }
1099
+ | { readonly interiors: readonly InteriorData[]; readonly rec: RecHandle | RecHead | RecData }
1100
+
1126
1101
  /** 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
- )
1102
+ type ChainContext = { readonly classes: SchemaClasses; readonly theory: AnySchema } & DerivedEnv &
1103
+ (
1104
+ | { readonly kind: "query" }
1105
+ | { readonly kind: "interior"; readonly self: string }
1106
+ | { readonly kind: "rec-base"; readonly self: RecHandle }
1107
+ | { readonly kind: "rec-arm"; readonly self: RecHead }
1108
+ )
1132
1109
 
1133
1110
  /** The diagnostic label of a chain context. */
1134
1111
  function contextLabel(context: ChainContext): string {
1135
1112
  switch (context.kind) {
1136
1113
  case "query":
1137
1114
  return "query rule"
1138
- case "rec":
1139
- return `rec ${context.self.name} rule`
1140
- case "output":
1141
- return "program output rule"
1115
+ case "interior":
1116
+ return `interior ${context.self} rule`
1117
+ case "rec-base":
1118
+ return `rec ${context.self.name} base`
1119
+ case "rec-arm":
1120
+ return `rec ${context.self.name} rec`
1142
1121
  }
1143
1122
  }
1144
1123
 
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) {
1124
+ /** RecHandle is name-only staging; RecHead/RecData carry the sealed finds. */
1125
+ function isRecHead(rec: RecHandle | RecHead | RecData): rec is RecHead {
1126
+ return Array.isArray((rec as RecHead).finds)
1127
+ }
1128
+
1129
+ /** Resolves a derived-table name against the context's visible tables. */
1130
+ function lookupDerived(context: ChainContext, name: string): DerivedTable {
1131
+ const interior = context.interiors.find(function byName(candidate) {
1132
+ return candidate.name === name
1133
+ })
1134
+ if (interior !== undefined) {
1135
+ if (context.kind === "interior" && name === context.self) {
1157
1136
  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)`
1137
+ `interior ${name}: an interior does not read itself — declaration order is topological (a self-read is InteriorNotPrior)`
1159
1138
  )
1160
1139
  }
1161
- return advanceIdb(state, context.self, bindings, false)
1140
+ return interior
1162
1141
  }
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
- )
1142
+ const rec = "rec" in context ? context.rec : undefined
1143
+ if (rec !== undefined && rec.name === name) {
1144
+ if (context.kind === "interior") {
1145
+ throw errors.new(`interior ${context.self}: interiors cannot read the rec — this cut's interiors are a prefix`)
1146
+ }
1147
+ if (context.kind === "rec-base") {
1148
+ throw errors.new(`rec ${rec.name}: a base arm does not read the rec — self-atoms belong on rec arms`)
1149
+ }
1150
+ if (!isRecHead(rec)) {
1151
+ throw errors.new(`rec ${rec.name}: rec arms resolve the rec head after base arms seal it`)
1152
+ }
1153
+ return rec
1167
1154
  }
1168
- return advanceIdb(state, target.data, bindings, false)
1155
+ throw errors.new(`${contextLabel(context)}: no derived table named ${name} is in scope`)
1156
+ }
1157
+
1158
+ /** Validates and records one interior atom per the context's cut. */
1159
+ function interiorAdvance(
1160
+ context: ChainContext,
1161
+ state: RuleBuildState,
1162
+ name: string,
1163
+ bindings: Readonly<Record<string, unknown>>
1164
+ ): RuleBuildState {
1165
+ return advanceInterior(state, lookupDerived(context, name), bindings, "interior")
1169
1166
  }
1170
1167
 
1171
1168
  /**
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).
1169
+ * Validates and records one NEGATED finished-table atom — main and interior
1170
+ * rules: a finished set is a set. Rec bodies refuse every negation
1171
+ * (`NegationInRec` — self is the wall; EDB / earlier-interior is this-cut).
1177
1172
  */
1178
- function notIdbAdvance(
1173
+ function notInteriorAdvance(
1179
1174
  context: ChainContext,
1180
1175
  state: RuleBuildState,
1181
- target: { readonly name: string; readonly data: RecData },
1176
+ name: string,
1182
1177
  bindings: Readonly<Record<string, unknown>>
1183
1178
  ): 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") {
1188
- 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`
1190
- )
1191
- }
1192
- if (!context.program.recs.includes(target.data)) {
1179
+ if (context.kind === "rec-base" || context.kind === "rec-arm") {
1193
1180
  throw errors.new(
1194
- `idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`
1181
+ `rec ${context.self.name}: a rec 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`
1195
1182
  )
1196
1183
  }
1197
- return advanceIdb(state, target.data, bindings, true)
1184
+ return advanceInterior(state, lookupDerived(context, name), bindings, "negatedInterior")
1198
1185
  }
1199
1186
 
1200
- /** Classifies one find record per the context (a recursive head projects bound variables only). */
1187
+ /** Classifies one find record per the context (interior and rec heads project bound variables only). */
1201
1188
  function findColumns(context: ChainContext, entries: Readonly<Record<string, unknown>>): FindColumn[] {
1202
1189
  const columns: FindColumn[] = []
1190
+ const derivedHead = context.kind !== "query"
1203
1191
  for (const [name, entry] of Object.entries(entries)) {
1204
1192
  if (entry === undefined) {
1205
1193
  continue
1206
1194
  }
1207
- if (context.kind === "rec" && !(isTerm(entry) && entry[term] === "var")) {
1195
+ if (derivedHead && !(isTerm(entry) && entry[term] === "var")) {
1196
+ const who = context.kind === "interior" ? `interior ${context.self}` : `rec ${context.self.name}`
1208
1197
  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)`
1198
+ `${who}: a rec head projects bound variables only — aggregates and the measure read finished sets (unwritable here)`
1210
1199
  )
1211
1200
  }
1212
1201
  columns.push(findColumnOf(name, entry))
@@ -1223,8 +1212,8 @@ function makeRawChain(context: ChainContext, state: RuleBuildState): RawChain {
1223
1212
  where(cond) {
1224
1213
  return makeRawChain(context, advanceWhere(context, state, cond))
1225
1214
  },
1226
- idb(target, bindings) {
1227
- return makeRawChain(context, idbAdvance(context, state, target, bindings))
1215
+ interior(name, bindings) {
1216
+ return makeRawChain(context, interiorAdvance(context, state, name, bindings))
1228
1217
  },
1229
1218
  find(entries) {
1230
1219
  return makeRuleValue<never, never>(completeRule(context, state, findColumns(context, entries)))
@@ -1241,8 +1230,8 @@ function makeRawScope(context: ChainContext): RawScope {
1241
1230
  match(relation, bindings) {
1242
1231
  return makeRawChain(context, advanceMatch(context, EMPTY_RULE, relation, bindings))
1243
1232
  },
1244
- idb(target, bindings) {
1245
- return makeRawChain(context, idbAdvance(context, EMPTY_RULE, target, bindings))
1233
+ interior(name, bindings) {
1234
+ return makeRawChain(context, interiorAdvance(context, EMPTY_RULE, name, bindings))
1246
1235
  }
1247
1236
  }
1248
1237
  Object.freeze(scope)
@@ -1264,32 +1253,68 @@ function isTypedScope<S>(scope: RawScope): scope is RawScope & S {
1264
1253
 
1265
1254
  /** Builds one query-rule builder (the typed face of the raw builder). */
1266
1255
  function makeQueryRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1267
- theory: AnySchema
1256
+ theory: AnySchema,
1257
+ env: DerivedEnv
1268
1258
  ): QueryRuleScope<Rels, Classes> {
1269
- const raw = makeRawScope({ kind: "query", classes: theory.classes, theory })
1259
+ const raw = makeRawScope({ kind: "query", classes: theory.classes, theory, ...env })
1270
1260
  if (!isTypedScope<QueryRuleScope<Rels, Classes>>(raw)) {
1271
1261
  throw errors.new("query rule builder construction incomplete")
1272
1262
  }
1273
1263
  return raw
1274
1264
  }
1275
1265
 
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")
1266
+ /** Builds one interior-rule builder. */
1267
+ function makeInteriorRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1268
+ theory: AnySchema,
1269
+ env: DerivedEnv,
1270
+ self: string
1271
+ ): InteriorRuleScope<Rels, Classes> {
1272
+ const raw = makeRawScope({ kind: "interior", self, classes: theory.classes, theory, ...env })
1273
+ if (!isTypedScope<InteriorRuleScope<Rels, Classes>>(raw)) {
1274
+ throw errors.new("interior rule builder construction incomplete")
1283
1275
  }
1284
1276
  return raw
1285
1277
  }
1286
1278
 
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
1279
+ /** Builds one rec-arm builder. */
1280
+ function makeRecRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1281
+ theory: AnySchema,
1282
+ env: DerivedEnv,
1283
+ self: RecHandle,
1284
+ kind: "rec-base"
1285
+ ): RecRuleScope<Rels, Classes>
1286
+ function makeRecRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1287
+ theory: AnySchema,
1288
+ env: DerivedEnv,
1289
+ self: RecHead,
1290
+ kind: "rec-arm"
1291
+ ): RecRuleScope<Rels, Classes>
1292
+ function makeRecRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1293
+ theory: AnySchema,
1294
+ env: DerivedEnv,
1295
+ self: RecHandle | RecHead,
1296
+ kind: "rec-base" | "rec-arm"
1297
+ ): RecRuleScope<Rels, Classes> {
1298
+ const raw =
1299
+ kind === "rec-base"
1300
+ ? makeRawScope({
1301
+ kind: "rec-base",
1302
+ self: self as RecHandle,
1303
+ classes: theory.classes,
1304
+ theory,
1305
+ ...env
1306
+ })
1307
+ : makeRawScope({
1308
+ kind: "rec-arm",
1309
+ self: self as RecHead,
1310
+ classes: theory.classes,
1311
+ theory,
1312
+ ...env
1313
+ })
1314
+ if (!isTypedScope<RecRuleScope<Rels, Classes>>(raw)) {
1315
+ throw errors.new("rec rule builder construction incomplete")
1316
+ }
1317
+ return raw
1293
1318
  }
1294
1319
 
1295
1320
  /** Renders one head column's closed slice for the rule-alignment check's diagnostics. */
@@ -1307,9 +1332,6 @@ function headSignature(column: FindColumn): string {
1307
1332
  if (agg.op === "fold") {
1308
1333
  return `${column.name}:${agg.fold}`
1309
1334
  }
1310
- if (agg.op === "arg") {
1311
- return `${column.name}:${agg.direction}`
1312
- }
1313
1335
  return `${column.name}:${agg.op}`
1314
1336
  }
1315
1337
 
@@ -1324,17 +1346,22 @@ function renderParamAnchor(roster: ClosedRoster | undefined): string {
1324
1346
  }
1325
1347
 
1326
1348
  /**
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.
1349
+ * Folds every rule's param uses (interiors in declaration order, then rec
1350
+ * base, then rec arms, then main — exactly the lowering walk) into the
1351
+ * query's registry: first use mints the dense `ParamId`, the first
1352
+ * FIELD-ANCHORED use types the wire, and one name keeps one shape AND one
1353
+ * closedness. The orderable ban needs no registry arm: an order use always
1354
+ * anchors its SIBLING's domain (the no-variable-side spelling is refused
1355
+ * at the comparison constructor), so a closed-anchored param under an
1356
+ * order op dies at the one-domain wall here — and an order use whose
1357
+ * sibling is itself closed-bound dies at the comparison's own var-side
1358
+ * wall first.
1336
1359
  */
1337
- function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]): readonly ParamEntry[] {
1360
+ function paramRegistryOf(
1361
+ interiors: readonly InteriorData[],
1362
+ rec: RecData | undefined,
1363
+ rules: readonly RuleData[]
1364
+ ): readonly ParamEntry[] {
1338
1365
  const order: string[] = []
1339
1366
  const byName = new Map<
1340
1367
  string,
@@ -1383,8 +1410,16 @@ function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]):
1383
1410
  }
1384
1411
  }
1385
1412
  }
1386
- for (const rec of recs) {
1387
- for (const rule of rec.rules) {
1413
+ for (const interior of interiors) {
1414
+ for (const rule of interior.rules) {
1415
+ fold(rule.paramUses)
1416
+ }
1417
+ }
1418
+ if (rec !== undefined) {
1419
+ for (const rule of rec.base) {
1420
+ fold(rule.paramUses)
1421
+ }
1422
+ for (const rule of rec.rec) {
1388
1423
  fold(rule.paramUses)
1389
1424
  }
1390
1425
  }
@@ -1430,114 +1465,272 @@ interface RawQuery {
1430
1465
  readonly schema: AnySchema
1431
1466
  readonly data: QueryData
1432
1467
  rule(build: (r: RawScope) => RuleValue<never, never>): RawQuery
1468
+ interior(name: string, ...builds: never[]): never
1469
+ reach(name: string, arms: never): never
1433
1470
  }
1434
1471
 
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 {
1472
+ /** Asserts every rule in a list derives the same head (name, aggregate shape, closed slice, class). */
1473
+ function assertAlignedHeads(label: string, rules: readonly RuleData[]): void {
1442
1474
  const first = rules[0]
1443
1475
  if (first === undefined) {
1444
- throw errors.new("a query needs at least one rule")
1476
+ throw errors.new(`${label} needs at least one rule`)
1445
1477
  }
1446
1478
  const signature = first.finds.map(headSignature).join(", ")
1447
1479
  rules.forEach(function verifyHead(rule, index) {
1448
1480
  const candidate = rule.finds.map(headSignature).join(", ")
1449
1481
  if (candidate !== signature) {
1450
1482
  throw errors.new(
1451
- `every rule of a query derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`
1483
+ `every rule of ${label} derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`
1452
1484
  )
1453
1485
  }
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
1486
  rule.finds.forEach(function verifyClosedSlice(column, position) {
1458
1487
  const lead = first.finds[position]
1459
1488
  if (lead !== undefined && column.closed !== lead.closed) {
1460
1489
  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)`
1490
+ `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
1491
  )
1463
1492
  }
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
1493
  if (lead === undefined) {
1470
1494
  return
1471
1495
  }
1472
1496
  if (lead.slot !== undefined && column.slot !== undefined && !fieldJoins(lead.slot, column.slot)) {
1473
1497
  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)`
1498
+ `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
1499
  )
1476
1500
  }
1477
1501
  })
1478
1502
  })
1479
- const data: QueryData = Object.freeze({
1480
- recs: Object.freeze([...recs]),
1481
- rules: Object.freeze([...rules]),
1482
- finds: first.finds,
1483
- params: paramRegistryOf(recs, rules)
1484
- })
1503
+ }
1504
+
1505
+ function afterMainError(what: string): Error {
1506
+ return errors.new(
1507
+ `query: ${what} after a main rule is unwritable — declaration order is interiors, then rec, then main`
1508
+ )
1509
+ }
1510
+
1511
+ /**
1512
+ * Assembles the runtime query value over completed rules: every rule must
1513
+ * derive the SAME head (name and aggregate shape, position for position —
1514
+ * the decode labels and the engine's alignment rule agree), and the param
1515
+ * registry folds in query-walk order. CQ lowering does not mention rec;
1516
+ * Reach carries RecData by value.
1517
+ */
1518
+ function makeRawQuery(
1519
+ theory: AnySchema,
1520
+ interiors: readonly InteriorData[],
1521
+ rec: RecData | undefined,
1522
+ rules: readonly RuleData[]
1523
+ ): RawQuery {
1524
+ assertAlignedHeads("a query", rules)
1525
+ const first = rules[0]
1526
+ if (first === undefined) {
1527
+ throw errors.new("a query needs at least one rule")
1528
+ }
1529
+ const env: DerivedEnv = rec === undefined ? { interiors } : { interiors, rec }
1530
+ const frozenInteriors = Object.freeze([...interiors])
1531
+ const frozenRules = Object.freeze([...rules])
1532
+ const params = paramRegistryOf(interiors, rec, rules)
1533
+ const data: QueryData =
1534
+ rec === undefined
1535
+ ? Object.freeze({
1536
+ kind: "cq" as const,
1537
+ interiors: frozenInteriors,
1538
+ rules: frozenRules,
1539
+ finds: first.finds,
1540
+ params
1541
+ })
1542
+ : Object.freeze({
1543
+ kind: "reach" as const,
1544
+ interiors: frozenInteriors,
1545
+ rec,
1546
+ rules: frozenRules,
1547
+ finds: first.finds,
1548
+ params
1549
+ })
1485
1550
  const value: RawQuery = {
1486
1551
  schema: theory,
1487
1552
  data,
1488
1553
  rule(build) {
1489
- const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory }))
1490
- return makeRawQuery(theory, recs, [...rules, built.rule])
1554
+ const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory, ...env }))
1555
+ return makeRawQuery(theory, interiors, rec, [...rules, built.rule])
1556
+ },
1557
+ interior() {
1558
+ throw afterMainError("interior")
1559
+ },
1560
+ reach() {
1561
+ throw afterMainError("reach")
1491
1562
  }
1492
1563
  }
1493
1564
  Object.freeze(value)
1494
1565
  return value
1495
1566
  }
1496
1567
 
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
1568
  function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
1511
1569
  theory: Schema<Rels, Classes>,
1512
- recs: readonly RecData[],
1570
+ interiors: readonly InteriorData[],
1571
+ rec: RecData | undefined,
1513
1572
  rules: readonly RuleData[]
1514
1573
  ): 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")
1574
+ return makeRawQuery(theory, interiors, rec, rules) as unknown as Query<Rels, Row, P, Classes>
1575
+ }
1576
+
1577
+ /** Collects one Interior from its builders. */
1578
+ function collectInterior<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1579
+ theory: Schema<Rels, Classes>,
1580
+ env: DerivedEnv,
1581
+ name: string,
1582
+ builds: readonly InteriorBuild<Rels, Classes>[]
1583
+ ): InteriorData {
1584
+ if (builds.length === 0) {
1585
+ throw errors.new(`query: interior ${name} needs at least one rule`)
1518
1586
  }
1519
- return raw
1587
+ const rules = builds.map(function buildRule(buildOne) {
1588
+ return buildOne(makeInteriorRuleScope<Rels, Classes>(theory, env, name)).rule
1589
+ })
1590
+ assertAlignedHeads(`interior ${name}`, rules)
1591
+ const first = rules[0]
1592
+ if (first === undefined) {
1593
+ throw errors.new(`query: interior ${name} needs at least one rule`)
1594
+ }
1595
+ return Object.freeze({ name, finds: first.finds, rules: Object.freeze(rules) })
1520
1596
  }
1521
1597
 
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> = {
1598
+ /** Collects the Rec from tagged base/rec builder arrays. */
1599
+ function collectRec<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1600
+ theory: Schema<Rels, Classes>,
1601
+ interiors: readonly InteriorData[],
1602
+ name: string,
1603
+ baseBuilds: readonly RecBuild<Rels, Classes>[],
1604
+ recBuilds: readonly RecBuild<Rels, Classes>[]
1605
+ ): RecData {
1606
+ if (baseBuilds.length === 0) {
1607
+ throw errors.new(`query: rec ${name} has no base arms`)
1608
+ }
1609
+ if (recBuilds.length === 0) {
1610
+ throw errors.new(`query: rec ${name} has no rec arms`)
1611
+ }
1612
+ const handle: RecHandle = Object.freeze({ name })
1613
+ const baseEnv: DerivedEnv = { interiors, rec: handle }
1614
+ const base = baseBuilds.map(function buildBase(buildOne) {
1615
+ return buildOne(makeRecRuleScope<Rels, Classes>(theory, baseEnv, handle, "rec-base")).rule
1616
+ })
1617
+ assertAlignedHeads(`rec ${name}`, base)
1618
+ const first = base[0]
1619
+ if (first === undefined) {
1620
+ throw errors.new(`query: rec ${name} has no base arms`)
1621
+ }
1622
+ const firstFind = first.finds[0]
1623
+ if (firstFind === undefined) {
1624
+ throw errors.new(`query: rec ${name} has no head`)
1625
+ }
1626
+ const finds: RecHead["finds"] = [firstFind, ...first.finds.slice(1)]
1627
+ const head: RecHead = Object.freeze({ name, finds })
1628
+ const recEnv: DerivedEnv = { interiors, rec: head }
1629
+ const rec = recBuilds.map(function buildRec(buildOne) {
1630
+ return buildOne(makeRecRuleScope<Rels, Classes>(theory, recEnv, head, "rec-arm")).rule
1631
+ })
1632
+ assertAlignedHeads(`rec ${name}`, [...base, ...rec])
1633
+ const firstRec = rec[0]
1634
+ if (firstRec === undefined) {
1635
+ throw errors.new(`query: rec ${name} has no rec arms`)
1636
+ }
1637
+ const sealedBase: RecData["base"] = [first, ...base.slice(1)]
1638
+ const sealedRec: RecData["rec"] = [firstRec, ...rec.slice(1)]
1639
+ const recData: RecData = Object.freeze({
1640
+ name,
1641
+ finds,
1642
+ base: sealedBase,
1643
+ rec: sealedRec
1644
+ })
1645
+ return recData
1646
+ }
1647
+
1648
+ /** Builds the CQ query start (interiors, then reach or the first main rule). */
1649
+ function makeQueryStart<Rels extends SchemaRelations, Classes extends SchemaClasses, P extends ParamsRecord>(
1650
+ theory: Schema<Rels, Classes>,
1651
+ interiors: readonly InteriorData[]
1652
+ ): QueryStart<Rels, Classes, P> {
1653
+ const env: DerivedEnv = { interiors }
1654
+ const start = {
1655
+ interior<const Builds extends readonly InteriorBuild<Rels, Classes>[]>(
1656
+ name: string,
1657
+ ...builds: Builds
1658
+ ): QueryStart<Rels, Classes, Flatten<P & BuildsParams<Builds>>> {
1659
+ if (
1660
+ interiors.some(function sameName(interior) {
1661
+ return interior.name === name
1662
+ })
1663
+ ) {
1664
+ throw errors.new(`query: interior ${name} is already declared — names are unique`)
1665
+ }
1666
+ if (name.length === 0) {
1667
+ throw errors.new("query: an interior needs a name")
1668
+ }
1669
+ const data = collectInterior(theory, env, name, builds)
1670
+ return makeQueryStart<Rels, Classes, Flatten<P & BuildsParams<Builds>>>(theory, [...interiors, data])
1671
+ },
1672
+ reach<const Base extends readonly RecBuild<Rels, Classes>[], const Step extends readonly RecBuild<Rels, Classes>[]>(
1673
+ name: string,
1674
+ arms: { readonly base: Base; readonly rec: Step }
1675
+ ): QueryReachStart<Rels, Classes, Flatten<P & BuildsParams<Base> & BuildsParams<Step>>> {
1676
+ if (
1677
+ interiors.some(function sameName(interior) {
1678
+ return interior.name === name
1679
+ })
1680
+ ) {
1681
+ throw errors.new(`query: interior and rec share the name ${name}`)
1682
+ }
1683
+ if (name.length === 0) {
1684
+ throw errors.new("query: reach needs a name")
1685
+ }
1686
+ const data = collectRec(theory, interiors, name, arms.base, arms.rec)
1687
+ return makeQueryReachStart<Rels, Classes, Flatten<P & BuildsParams<Base> & BuildsParams<Step>>>(
1688
+ theory,
1689
+ interiors,
1690
+ data
1691
+ )
1692
+ },
1693
+ rule<RV extends AnyRuleValue>(
1694
+ build: (r: QueryRuleScope<Rels, Classes>) => RV
1695
+ ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes> {
1696
+ const built = build(makeQueryRuleScope<Rels, Classes>(theory, env))
1697
+ return makeQuery<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes>(theory, interiors, undefined, [built.rule])
1698
+ }
1699
+ }
1700
+ Object.freeze(start)
1701
+ return start as unknown as QueryStart<Rels, Classes, P>
1702
+ }
1703
+
1704
+ /** Builds the Reach query start (rec sealed; only the first main rule remains). */
1705
+ function makeQueryReachStart<Rels extends SchemaRelations, Classes extends SchemaClasses, P extends ParamsRecord>(
1706
+ theory: Schema<Rels, Classes>,
1707
+ interiors: readonly InteriorData[],
1708
+ rec: RecData
1709
+ ): QueryReachStart<Rels, Classes, P> {
1710
+ const env: DerivedEnv = { interiors, rec }
1711
+ const start = {
1532
1712
  rule<RV extends AnyRuleValue>(
1533
1713
  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])
1714
+ ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes> {
1715
+ const built = build(makeQueryRuleScope<Rels, Classes>(theory, env))
1716
+ return makeQuery<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes>(theory, interiors, rec, [built.rule])
1537
1717
  }
1538
1718
  }
1539
1719
  Object.freeze(start)
1540
- return start
1720
+ return start as unknown as QueryReachStart<Rels, Classes, P>
1721
+ }
1722
+
1723
+ /**
1724
+ * Opens a query over a schema: `query(S).rule(r => ...)`, optionally with
1725
+ * `interior` / `reach` first. Each `.rule` adds one conjunctive rule;
1726
+ * multiple rules are the set union. The schema's law-computed class map and
1727
+ * theory value ride into every rule builder — the join walls compare
1728
+ * against the mint slots off it.
1729
+ */
1730
+ function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1731
+ theory: Schema<Rels, Classes>
1732
+ ): QueryStart<Rels, Classes> {
1733
+ return makeQueryStart<Rels, Classes, Record<never, never>>(theory, [])
1541
1734
  }
1542
1735
 
1543
1736
  /**
@@ -1670,7 +1863,7 @@ function taggedCmpLiteral(
1670
1863
  interface LowerContext {
1671
1864
  readonly theory: AnySchema
1672
1865
  readonly relationIds: ReadonlyMap<string, number>
1673
- readonly recIds: ReadonlyMap<RecData, number>
1866
+ readonly interiorIds: ReadonlyMap<string, number>
1674
1867
  readonly paramIds: ReadonlyMap<string, number>
1675
1868
  readonly params: ReadonlyMap<string, ParamEntry>
1676
1869
  }
@@ -1751,35 +1944,34 @@ function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEn
1751
1944
  }
1752
1945
 
1753
1946
  /**
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
1947
+ * Lowers one interior atom: named bindings placed by HEAD order, `FieldId(i)` =
1948
+ * head position i. Every head column of the table must be bound (a missing key
1756
1949
  * is refused pointed); the var-id assignment order is head order, so the
1757
1950
  * first-use numbering matches the name-keyed edition exactly.
1758
1951
  */
1759
- function lowerIdbAtom(
1952
+ function lowerInteriorAtom(
1760
1953
  ctx: LowerContext,
1761
- rec: RecData,
1954
+ target: DerivedTable,
1762
1955
  bindings: ReadonlyArray<{ readonly key: string; readonly ref: AnyVar }>,
1763
1956
  ids: VarIds
1764
1957
  ): 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`)
1958
+ const interior = ctx.interiorIds.get(target.name)
1959
+ if (interior === undefined) {
1960
+ throw errors.new(`query lowering: derived table ${target.name} was not declared on this query`)
1768
1961
  }
1769
- const head = rec.rules[0]
1770
- if (head === undefined) {
1771
- throw errors.new(`query lowering: rec ${rec.name} has no rules`)
1962
+ if (target.finds.length === 0) {
1963
+ throw errors.new(`query lowering: derived table ${target.name} has no head`)
1772
1964
  }
1773
- const irBindings: Array<readonly [number, TermIr]> = head.finds.map(function lowerPosition(column, position) {
1965
+ const irBindings: Array<readonly [number, TermIr]> = target.finds.map(function lowerPosition(column, position) {
1774
1966
  const binding = bindings.find(function byKey(candidate) {
1775
1967
  return candidate.key === column.name
1776
1968
  })
1777
1969
  if (binding === undefined) {
1778
- throw errors.new(`query lowering: idb ${rec.name} omits head column ${column.name}`)
1970
+ throw errors.new(`query lowering: interior ${target.name} omits head column ${column.name}`)
1779
1971
  }
1780
1972
  return [position, { kind: "var", var: ids.of(binding.ref) } as const] as const
1781
1973
  })
1782
- return { source: { kind: "idb", pred }, bindings: irBindings }
1974
+ return { source: { kind: "interior", interior }, bindings: irBindings }
1783
1975
  }
1784
1976
 
1785
1977
  /** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
@@ -1821,25 +2013,17 @@ function cmpAnchorOf(ctx: LowerContext, sibling: CmpTermData): AnyField | "measu
1821
2013
 
1822
2014
  /** Lowers one comparison. */
1823
2015
  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) }
2016
+ if (cmp.op.kind === "allen") {
1833
2017
  return {
1834
- op: { kind: "allen", mask },
2018
+ op: { kind: "allen", mask: cmp.op.mask },
1835
2019
  lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
1836
2020
  rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
1837
2021
  }
1838
2022
  }
1839
2023
  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)
2024
+ op: { kind: cmp.op.kind },
2025
+ lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, cmp.op.kind),
2026
+ rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, cmp.op.kind)
1843
2027
  }
1844
2028
  }
1845
2029
 
@@ -1868,16 +2052,12 @@ function lowerFind(entry: FindEntryData, ids: VarIds): FindTermIr {
1868
2052
  switch (agg.op) {
1869
2053
  case "count":
1870
2054
  return { kind: "aggregate", op: { kind: "count" } }
1871
- case "countDistinct":
1872
- return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) }
1873
2055
  case "fold": {
1874
2056
  if ("duration" in agg.over) {
1875
2057
  return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) }
1876
2058
  }
1877
2059
  return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) }
1878
2060
  }
1879
- case "arg":
1880
- return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) }
1881
2061
  case "pack":
1882
2062
  return { kind: "aggregate", op: { kind: "pack" }, over: ids.of(agg.over) }
1883
2063
  }
@@ -1888,12 +2068,8 @@ function headOpOf(agg: AggData): HeadOpIr {
1888
2068
  switch (agg.op) {
1889
2069
  case "count":
1890
2070
  return "count"
1891
- case "countDistinct":
1892
- return "countDistinct"
1893
2071
  case "fold":
1894
2072
  return agg.fold
1895
- case "arg":
1896
- return agg.direction
1897
2073
  case "pack":
1898
2074
  return "pack"
1899
2075
  }
@@ -1924,9 +2100,12 @@ function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
1924
2100
  negated.push(lowerAtom(ctx, item.atom, ids))
1925
2101
  break
1926
2102
  }
1927
- case "idb": {
1928
- const bucket = item.negated ? negated : atoms
1929
- bucket.push(lowerIdbAtom(ctx, item.rec, item.bindings, ids))
2103
+ case "interior": {
2104
+ atoms.push(lowerInteriorAtom(ctx, item.target, item.bindings, ids))
2105
+ break
2106
+ }
2107
+ case "negatedInterior": {
2108
+ negated.push(lowerInteriorAtom(ctx, item.target, item.bindings, ids))
1930
2109
  break
1931
2110
  }
1932
2111
  case "cond": {
@@ -1946,24 +2125,27 @@ function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
1946
2125
  }
1947
2126
 
1948
2127
  /**
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.
2128
+ * Lowers a query value to the bridge's `QueryIr` — pure and stable:
2129
+ * interiors in declaration order, then CQ or Reach, then main. Every
2130
+ * registered param must carry a field anchor by now.
1952
2131
  */
1953
- function lowerQuery(q: AnyQuery): ProgramIr {
2132
+ function lowerQuery(q: AnyQuery): ParsedQuery {
1954
2133
  const theory = q.schema
1955
2134
  const relationIds = new Map<string, number>()
1956
2135
  Object.keys(theory.relations).forEach(function assignOrdinal(name, index) {
1957
2136
  relationIds.set(name, index)
1958
2137
  })
1959
- const recIds = new Map<RecData, number>()
1960
- q.data.recs.forEach(function assignPredId(rec, index) {
1961
- recIds.set(rec, index)
2138
+ const interiorIds = new Map<string, number>()
2139
+ q.data.interiors.forEach(function assignInteriorId(interior, index) {
2140
+ interiorIds.set(interior.name, index)
1962
2141
  })
2142
+ if (q.data.kind === "reach") {
2143
+ interiorIds.set(q.data.rec.name, q.data.interiors.length)
2144
+ }
1963
2145
  const paramIds = new Map<string, number>()
1964
2146
  const params = new Map<string, ParamEntry>()
1965
2147
  q.data.params.forEach(function assignParamId(entry, index) {
1966
- if (entry.anchor === undefined && entry.shape !== "mask") {
2148
+ if (entry.anchor === undefined) {
1967
2149
  throw errors.new(
1968
2150
  `query param ${entry.name} has no field-anchored use — bind it in an atom or compare it against a bound variable`
1969
2151
  )
@@ -1971,26 +2153,37 @@ function lowerQuery(q: AnyQuery): ProgramIr {
1971
2153
  paramIds.set(entry.name, index)
1972
2154
  params.set(entry.name, entry)
1973
2155
  })
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
- }
2156
+ const ctx: LowerContext = { theory, relationIds, interiorIds, paramIds, params }
2157
+ const interiors = q.data.interiors.map(function lowerInterior(interior) {
1980
2158
  return {
1981
- head: head.finds.map(headTermOf),
1982
- rules: rec.rules.map(function lowerRecRule(rule) {
2159
+ head: interior.finds.map(headTermOf),
2160
+ rules: interior.rules.map(function lowerInteriorRule(rule) {
1983
2161
  return lowerRule(ctx, rule)
1984
2162
  })
1985
2163
  }
1986
2164
  })
1987
- predicates.push({
1988
- head: q.data.finds.map(headTermOf),
1989
- rules: q.data.rules.map(function lowerOutputRule(rule) {
1990
- return lowerRule(ctx, rule)
1991
- })
2165
+ const head = q.data.finds.map(headTermOf)
2166
+ const rules = q.data.rules.map(function lowerMainRule(rule) {
2167
+ return lowerRule(ctx, rule)
2168
+ })
2169
+ if (q.data.kind === "cq") {
2170
+ return parseQueryIr({ kind: "cq", interiors, head, rules })
2171
+ }
2172
+ return parseQueryIr({
2173
+ kind: "reach",
2174
+ interiors,
2175
+ rec: {
2176
+ head: q.data.rec.finds.map(headTermOf),
2177
+ base: q.data.rec.base.map(function lowerBase(rule) {
2178
+ return lowerRule(ctx, rule)
2179
+ }),
2180
+ rec: q.data.rec.rec.map(function lowerRecArm(rule) {
2181
+ return lowerRule(ctx, rule)
2182
+ })
2183
+ },
2184
+ head,
2185
+ rules
1992
2186
  })
1993
- return { predicates, output: q.data.recs.length }
1994
2187
  }
1995
2188
 
1996
2189
  export type {
@@ -1998,13 +2191,13 @@ export type {
1998
2191
  AnyRuleValue,
1999
2192
  HeadOf,
2000
2193
  HeadShape,
2001
- OutputRuleChain,
2002
- OutputRuleScope,
2194
+ InteriorRuleChain,
2195
+ InteriorRuleScope,
2003
2196
  ParamsOf,
2004
- ProgramState,
2005
2197
  Query,
2006
2198
  QueryData,
2007
2199
  QueryParams,
2200
+ QueryReachStart,
2008
2201
  QueryRelation,
2009
2202
  QueryRow,
2010
2203
  QueryRuleChain,
@@ -2012,11 +2205,10 @@ export type {
2012
2205
  QueryStart,
2013
2206
  RawChain,
2014
2207
  RawScope,
2015
- RecRef,
2016
2208
  RecRuleChain,
2017
2209
  RecRuleScope,
2018
2210
  RowOf,
2019
2211
  RuleValue,
2020
2212
  TermOps
2021
2213
  }
2022
- export { lowerQuery, makeOutputRuleScope, makeQuery, makeRawScope, query, taggedCmpLiteral, taggedLiteral }
2214
+ export { lowerQuery, makeRawScope, query, taggedCmpLiteral, taggedLiteral }