@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,20 +27,20 @@
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
  import * as errors from "@superbuilders/errors";
37
34
  import { sealedFieldsOf } from "#closed.ts";
38
35
  import { assertDeclarationOrderKey, isIntervalValue, literalShapeError, rosterOf } from "#fields.ts";
39
36
  import { allen, and, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts";
40
- import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/find.ts";
41
- import { fieldJoins, inferred, isTerm, makeDuration, makeMaskParam, makeParam, makeSetParam, renderFieldKind, term } from "#query/scope.ts";
37
+ import { count, max, min, pack, sum } from "#query/find.ts";
38
+ import { parseQueryIr } from "#query/parse-ir.ts";
39
+ import { fieldJoins, inferred, isTerm, makeDuration, makeParam, makeSetParam, renderFieldKind, term } from "#query/scope.ts";
42
40
  /** The frozen constructor vocabulary every rule builder spreads. */
43
41
  const termOps = Object.freeze({
44
42
  param: makeParam,
45
43
  inSet: makeSetParam,
46
- maskParam: makeMaskParam,
47
44
  duration: makeDuration,
48
45
  eq,
49
46
  ne,
@@ -57,12 +54,9 @@ const termOps = Object.freeze({
57
54
  or,
58
55
  not,
59
56
  count,
60
- countDistinct,
61
57
  sum,
62
58
  min,
63
59
  max,
64
- argMax,
65
- argMin,
66
60
  pack
67
61
  });
68
62
  /** The empty rule state. */
@@ -177,8 +171,6 @@ function resolveBindings(context, label, relation, bindings) {
177
171
  }));
178
172
  break;
179
173
  }
180
- case "maskParam":
181
- throw errors.new(`${label}.${fieldName}: an Allen-mask param is not a field-typed value — masks live in allen() conditions only`);
182
174
  case "duration":
183
175
  throw errors.new(`${label}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and find entries`);
184
176
  }
@@ -215,7 +207,7 @@ function advanceMatch(context, state, relation, bindings) {
215
207
  });
216
208
  }
217
209
  /** Resolves one comparison side to its runtime term (variables and the measure ride by reference). */
218
- function cmpTermDataOf(op, value) {
210
+ function cmpTermDataOf(value) {
219
211
  if (isTerm(value)) {
220
212
  switch (value[term]) {
221
213
  case "var":
@@ -226,8 +218,6 @@ function cmpTermDataOf(op, value) {
226
218
  return Object.freeze({ kind: "setParam", name: value.name });
227
219
  case "duration":
228
220
  return Object.freeze({ kind: "measure", ref: value.over });
229
- case "maskParam":
230
- throw errors.new(`${op}: an Allen-mask param is not a comparison term — masks live in allen()'s mask position`);
231
221
  }
232
222
  }
233
223
  return Object.freeze({ kind: "literal", value });
@@ -262,31 +252,23 @@ function sideUses(op, side, sibling, uses) {
262
252
  /** Lowers one condition VALUE to its runtime data, recording param uses. */
263
253
  function condDataOf(cond, uses) {
264
254
  if (cond.cond === "cmp") {
265
- const lhs = cmpTermDataOf(cond.op, cond.lhs);
266
- const rhs = cmpTermDataOf(cond.op, cond.rhs);
255
+ const lhs = cmpTermDataOf(cond.lhs);
256
+ const rhs = cmpTermDataOf(cond.rhs);
267
257
  sideUses(cond.op, lhs, rhs, uses);
268
258
  sideUses(cond.op, rhs, lhs, uses);
269
- let mask;
270
259
  if (cond.op === "allen") {
271
260
  const maskValue = cond.mask;
272
- if (typeof maskValue === "number") {
273
- mask = Object.freeze({ kind: "literal", mask: maskValue });
274
- }
275
- else if (isTerm(maskValue) && maskValue[term] === "maskParam") {
276
- mask = Object.freeze({ kind: "param", name: maskValue.name });
277
- uses.push(Object.freeze({
278
- name: maskValue.name,
279
- shape: "mask",
280
- anchor: undefined,
281
- op: "allen",
282
- members: undefined
283
- }));
284
- }
285
- else {
286
- throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam");
261
+ if (typeof maskValue !== "number") {
262
+ throw errors.new("allen: the mask position takes a 13-bit mask number built from the ALLEN constants");
287
263
  }
264
+ return Object.freeze({
265
+ kind: "cmp",
266
+ op: { kind: "allen", mask: maskValue },
267
+ lhs,
268
+ rhs
269
+ });
288
270
  }
289
- return Object.freeze({ kind: "cmp", op: cond.op, mask, lhs, rhs });
271
+ return Object.freeze({ kind: "cmp", op: { kind: cond.op }, lhs, rhs });
290
272
  }
291
273
  if (cond.cond === "tree") {
292
274
  const children = cond.children.map(function lowerChild(child) {
@@ -301,11 +283,11 @@ function advanceWhere(context, state, cond) {
301
283
  if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
302
284
  throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom");
303
285
  }
304
- if (cond.cond === "notIdb") {
286
+ if (cond.cond === "notInterior") {
305
287
  const bindings = Object.fromEntries(Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
306
288
  return value !== undefined;
307
289
  }));
308
- return notIdbAdvance(context, state, cond.target, bindings);
290
+ return notInteriorAdvance(context, state, cond.name, bindings);
309
291
  }
310
292
  if (cond.cond === "not") {
311
293
  const relation = cond.relation;
@@ -328,36 +310,33 @@ function advanceWhere(context, state, cond) {
328
310
  });
329
311
  }
330
312
  /**
331
- * Extends a rule state with one `idb` atom (a named record over head keys;
332
- * vars validated at completion). A POSITIVE idb atom is a positive
333
- * occurrence exactly as the engine represents it (`check_atoms` walks Idb
313
+ * Extends a rule state with one interior atom (a named record over head keys;
314
+ * vars validated at completion). A POSITIVE interior atom is a positive
315
+ * occurrence exactly as the engine represents it (`check_atoms` walks Interior
334
316
  * and Edb in one loop), so its variables GROUND: they enter the rule's
335
317
  * boundness set, may ride the head, and satisfy negation safety — the
336
- * idb-only identity projection of a finished stratum is spellable with no
318
+ * interior-only identity projection of a finished table is spellable with no
337
319
  * re-grounding join. A NEGATED one binds nothing, only rejects.
338
320
  */
339
- function advanceIdb(state, rec, bindings, negated) {
321
+ function advanceInterior(state, target, bindings, kind) {
340
322
  const resolved = [];
341
323
  for (const [key, value] of Object.entries(bindings)) {
342
324
  if (value === undefined) {
343
325
  continue;
344
326
  }
345
327
  if (!isTerm(value) || value[term] !== "var") {
346
- throw errors.new(`idb ${rec.name}: position ${key} takes a variable — bind literals and params through where()/match()`);
328
+ throw errors.new(`interior ${target.name}: position ${key} takes a variable — bind literals and params through where()/match()`);
347
329
  }
348
330
  resolved.push(Object.freeze({ key, ref: value }));
349
331
  }
350
332
  const bound = new Set(state.bound);
351
- if (!negated) {
333
+ if (kind === "interior") {
352
334
  for (const binding of resolved) {
353
335
  bound.add(binding.ref);
354
336
  }
355
337
  }
356
338
  return Object.freeze({
357
- items: Object.freeze([
358
- ...state.items,
359
- Object.freeze({ kind: "idb", rec, bindings: Object.freeze(resolved), negated })
360
- ]),
339
+ items: Object.freeze([...state.items, Object.freeze({ kind, target, bindings: Object.freeze(resolved) })]),
361
340
  bound,
362
341
  paramUses: state.paramUses
363
342
  });
@@ -379,8 +358,6 @@ function aggDataOf(name, entry) {
379
358
  switch (entry.agg) {
380
359
  case "count":
381
360
  return Object.freeze({ op: "count" });
382
- case "countDistinct":
383
- return Object.freeze({ op: "countDistinct", over: asVarTerm(`find ${name} (countDistinct)`, over) });
384
361
  case "sum":
385
362
  case "min":
386
363
  case "max": {
@@ -392,14 +369,6 @@ function aggDataOf(name, entry) {
392
369
  }
393
370
  throw errors.new(`find ${name} (${entry.agg}): takes a variable or r.duration(v)`);
394
371
  }
395
- case "argMax":
396
- case "argMin":
397
- return Object.freeze({
398
- op: "arg",
399
- direction: entry.agg,
400
- over: asVarTerm(`find ${name} (${entry.agg})`, over),
401
- key: asVarTerm(`find ${name} (${entry.agg} key)`, entry.key)
402
- });
403
372
  case "pack":
404
373
  return Object.freeze({ op: "pack", over: asVarTerm(`find ${name} (pack)`, over) });
405
374
  default:
@@ -473,17 +442,14 @@ function assertNotClosed(where, position, ref) {
473
442
  }
474
443
  /**
475
444
  * The classed mint slot one answer column's VALUES flow from: a projected
476
- * variable's mint slot, or an Arg-carried payload's. Counts, folds, `pack`
477
- * and the measure derive numbers/intervals, so they resolve no slot.
445
+ * variable's mint slot. Counts, folds, `pack` and the measure derive
446
+ * numbers/intervals, so they resolve no slot.
478
447
  */
479
448
  function findColumnSlotOf(context, column) {
480
449
  const entry = column.entry;
481
450
  if (entry.kind === "var") {
482
451
  return mintSlotOf(context, entry.over);
483
452
  }
484
- if (entry.kind === "aggregate" && entry.agg.op === "arg") {
485
- return mintSlotOf(context, entry.agg.over);
486
- }
487
453
  return undefined;
488
454
  }
489
455
  /** Validates one find column's variable references (boundness + the orderable/interval walls, off the var's own field). */
@@ -503,9 +469,6 @@ function validateColumn(context, bound, column) {
503
469
  switch (agg.op) {
504
470
  case "count":
505
471
  return;
506
- case "countDistinct":
507
- assertBound(where, bound, agg.over);
508
- return;
509
472
  case "fold": {
510
473
  if ("duration" in agg.over) {
511
474
  assertBound(where, bound, agg.over.duration);
@@ -516,12 +479,6 @@ function validateColumn(context, bound, column) {
516
479
  assertNotClosed(where, `the ${agg.fold} input`, agg.over);
517
480
  return;
518
481
  }
519
- case "arg": {
520
- assertBound(where, bound, agg.over);
521
- assertBound(where, bound, agg.key);
522
- assertNotClosed(where, `the ${agg.direction} key`, agg.key);
523
- return;
524
- }
525
482
  case "pack":
526
483
  assertBound(where, bound, agg.over);
527
484
  assertInterval(where, agg.over);
@@ -540,8 +497,8 @@ function validateCond(context, bound, cond) {
540
497
  if (side.kind === "var") {
541
498
  assertBound(label, bound, side.ref);
542
499
  const roster = rosterOf(side.ref.field);
543
- if (isOrderOp(cond.op) && roster !== undefined) {
544
- throw closedOrderError(label, `the ${cond.op} side ${side.ref.label}`, roster.name);
500
+ if (isOrderOp(cond.op.kind) && roster !== undefined) {
501
+ throw closedOrderError(label, `the ${cond.op.kind} side ${side.ref.label}`, roster.name);
545
502
  }
546
503
  }
547
504
  if (side.kind === "measure") {
@@ -549,13 +506,13 @@ function validateCond(context, bound, cond) {
549
506
  assertInterval(label, side.ref);
550
507
  }
551
508
  }
552
- if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
509
+ if ((cond.op.kind === "eq" || cond.op.kind === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
553
510
  assertBound(label, bound, cond.lhs.ref);
554
511
  assertBound(label, bound, cond.rhs.ref);
555
512
  const lhs = mintSlotOf(context, cond.lhs.ref);
556
513
  const rhs = mintSlotOf(context, cond.rhs.ref);
557
514
  if (!fieldJoins(lhs, rhs)) {
558
- throw errors.new(`${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)`);
515
+ throw errors.new(`${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)`);
559
516
  }
560
517
  }
561
518
  return;
@@ -565,20 +522,18 @@ function validateCond(context, bound, cond) {
565
522
  }
566
523
  }
567
524
  /**
568
- * Validates one `idb` item: every head column of the rec is bound exactly
525
+ * Validates one interior item: every head column of the table is bound exactly
569
526
  * once (a missing or extra key is a pointed error) and each variable joins
570
- * its head column's classed slot. A POSITIVE idb atom GROUNDS its
527
+ * its head column's classed slot. A POSITIVE interior atom GROUNDS its
571
528
  * variables (a positive occurrence, exactly the engine's representation),
572
529
  * so no boundness precondition exists; a NEGATED one binds nothing — its
573
530
  * variables must be positively bound elsewhere in the rule, the same
574
- * safety rule as EDB negation. When the rec's own rule 0 is in flight
575
- * (`rec.rules[0]` absent), the completing rule's OWN find columns ARE the
576
- * head.
531
+ * safety rule as EDB negation. When the table's own first rule is in flight
532
+ * (`finds` empty), the completing rule's OWN find columns ARE the head.
577
533
  */
578
- function validateIdb(context, bound, item, columns) {
534
+ function validateInterior(context, bound, item, columns) {
579
535
  const label = contextLabel(context);
580
- const head = item.rec.rules[0];
581
- const headColumns = head !== undefined ? head.finds : columns;
536
+ const headColumns = item.target.finds.length > 0 ? item.target.finds : columns;
582
537
  const headNames = headColumns.map(function nameOf(column) {
583
538
  return column.name;
584
539
  });
@@ -587,17 +542,17 @@ function validateIdb(context, bound, item, columns) {
587
542
  });
588
543
  for (const key of keys) {
589
544
  if (!headNames.includes(key)) {
590
- throw errors.new(`${label}: idb ${item.rec.name} binds ${key}, not a head column of ${item.rec.name} (head columns: ${headNames.join(", ")})`);
545
+ throw errors.new(`${label}: interior ${item.target.name} binds ${key}, not a head column of ${item.target.name} (head columns: ${headNames.join(", ")})`);
591
546
  }
592
547
  }
593
548
  for (const name of headNames) {
594
549
  if (!keys.includes(name)) {
595
- throw errors.new(`${label}: idb ${item.rec.name} omits the head column ${name} — an idb join binds every head column of ${item.rec.name}`);
550
+ throw errors.new(`${label}: interior ${item.target.name} omits the head column ${name} — an interior join binds every head column of ${item.target.name}`);
596
551
  }
597
552
  }
598
553
  for (const binding of item.bindings) {
599
- if (item.negated && !bound.has(binding.ref)) {
600
- throw errors.new(`${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)`);
554
+ if (item.kind === "negatedInterior" && !bound.has(binding.ref)) {
555
+ throw errors.new(`${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)`);
601
556
  }
602
557
  const headColumn = headColumns.find(function byName(column) {
603
558
  return column.name === binding.key;
@@ -607,14 +562,14 @@ function validateIdb(context, bound, item, columns) {
607
562
  }
608
563
  const mint = mintSlotOf(context, binding.ref);
609
564
  if (!fieldJoins(headColumn.slot, mint)) {
610
- throw errors.new(`${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`);
565
+ throw errors.new(`${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`);
611
566
  }
612
567
  }
613
568
  }
614
569
  /**
615
570
  * Completes one rule: enriches the find columns (declaration-order-safe
616
571
  * keys, boundness validated, each column's classed slot and closed slice
617
- * resolved), then walks the body walls — negated-atom boundness safety, idb
572
+ * resolved), then walks the body walls — negated-atom boundness safety, interior
618
573
  * head pairing, and condition validation.
619
574
  */
620
575
  function completeRule(context, state, rawColumns) {
@@ -636,8 +591,8 @@ function completeRule(context, state, rawColumns) {
636
591
  }
637
592
  }
638
593
  }
639
- if (item.kind === "idb") {
640
- validateIdb(context, state.bound, item, columns);
594
+ if (item.kind === "interior" || item.kind === "negatedInterior") {
595
+ validateInterior(context, state.bound, item, columns);
641
596
  }
642
597
  if (item.kind === "cond") {
643
598
  validateCond(context, state.bound, item.cond);
@@ -654,56 +609,65 @@ function contextLabel(context) {
654
609
  switch (context.kind) {
655
610
  case "query":
656
611
  return "query rule";
657
- case "rec":
658
- return `rec ${context.self.name} rule`;
659
- case "output":
660
- return "program output rule";
612
+ case "interior":
613
+ return `interior ${context.self} rule`;
614
+ case "rec-base":
615
+ return `recursive ${context.self.name} base`;
616
+ case "rec-arm":
617
+ return `recursive ${context.self.name} rec`;
661
618
  }
662
619
  }
663
- /** Validates and records one `idb` atom per the context's cut. */
664
- function idbAdvance(context, state, target, bindings) {
665
- if (context.kind === "query") {
666
- throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()");
667
- }
668
- if (context.kind === "rec") {
669
- if (target.data !== context.self) {
670
- throw errors.new(`rec ${context.self.name}: a recursive rule's idb target must be the rec itself — the self-recursion-only cut (mutual recursion is unwritable; fold a finished stratum in the output rules)`);
620
+ /** Resolves a derived-table name against the context's visible tables. */
621
+ function lookupDerived(context, name) {
622
+ const interior = context.interiors.find(function byName(candidate) {
623
+ return candidate.name === name;
624
+ });
625
+ if (interior !== undefined) {
626
+ if (context.kind === "interior" && name === context.self) {
627
+ throw errors.new(`interior ${name}: an interior does not read itself — declaration order is topological (a self-read is InteriorNotPrior)`);
671
628
  }
672
- return advanceIdb(state, context.self, bindings, false);
629
+ return interior;
673
630
  }
674
- if (!context.program.recs.includes(target.data)) {
675
- throw errors.new(`idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`);
631
+ if (context.rec !== null && context.rec.name === name) {
632
+ if (context.kind === "interior") {
633
+ throw errors.new(`interior ${context.self}: interiors cannot read the rec — this cut's interiors are a prefix`);
634
+ }
635
+ if (context.kind === "rec-base") {
636
+ throw errors.new(`recursive ${context.rec.name}: a base arm does not read the rec — self-atoms belong on rec arms`);
637
+ }
638
+ if (!("finds" in context.rec)) {
639
+ throw errors.new(`recursive ${context.rec.name}: rec arms resolve the rec head after base arms seal it`);
640
+ }
641
+ return context.rec;
676
642
  }
677
- return advanceIdb(state, target.data, bindings, false);
643
+ throw errors.new(`${contextLabel(context)}: no derived table named ${name} is in scope`);
644
+ }
645
+ /** Validates and records one interior atom per the context's cut. */
646
+ function interiorAdvance(context, state, name, bindings) {
647
+ return advanceInterior(state, lookupDerived(context, name), bindings, "interior");
678
648
  }
679
649
  /**
680
- * Validates and records one NEGATED finished-stratum atom — output rules
681
- * only: there every rec is a finished set before the output's operator
682
- * runs (negation OF lower strata is engine-legal; the strata judge refuses
683
- * only negation *through* a cycle, which the rec-context refusal here
684
- * makes unwritable).
650
+ * Validates and records one NEGATED finished-table atom — main and interior
651
+ * rules: a finished set is a set. Rec bodies refuse every negation
652
+ * (`NegationInRec` — self is the wall; EDB / earlier-interior is this-cut).
685
653
  */
686
- function notIdbAdvance(context, state, target, bindings) {
687
- if (context.kind === "query") {
688
- throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()");
654
+ function notInteriorAdvance(context, state, name, bindings) {
655
+ if (context.kind === "rec-base" || context.kind === "rec-arm") {
656
+ throw errors.new(`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`);
689
657
  }
690
- if (context.kind === "rec") {
691
- throw errors.new(`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`);
692
- }
693
- if (!context.program.recs.includes(target.data)) {
694
- throw errors.new(`idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`);
695
- }
696
- return advanceIdb(state, target.data, bindings, true);
658
+ return advanceInterior(state, lookupDerived(context, name), bindings, "negatedInterior");
697
659
  }
698
- /** Classifies one find record per the context (a recursive head projects bound variables only). */
660
+ /** Classifies one find record per the context (interior and rec heads project bound variables only). */
699
661
  function findColumns(context, entries) {
700
662
  const columns = [];
663
+ const derivedHead = context.kind !== "query";
701
664
  for (const [name, entry] of Object.entries(entries)) {
702
665
  if (entry === undefined) {
703
666
  continue;
704
667
  }
705
- if (context.kind === "rec" && !(isTerm(entry) && entry[term] === "var")) {
706
- throw errors.new(`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)`);
668
+ if (derivedHead && !(isTerm(entry) && entry[term] === "var")) {
669
+ const who = context.kind === "interior" ? `interior ${context.self}` : `recursive ${context.self.name}`;
670
+ throw errors.new(`${who}: a recursive head projects bound variables only — aggregates and the measure read finished sets (unwritable here)`);
707
671
  }
708
672
  columns.push(findColumnOf(name, entry));
709
673
  }
@@ -718,8 +682,8 @@ function makeRawChain(context, state) {
718
682
  where(cond) {
719
683
  return makeRawChain(context, advanceWhere(context, state, cond));
720
684
  },
721
- idb(target, bindings) {
722
- return makeRawChain(context, idbAdvance(context, state, target, bindings));
685
+ interior(name, bindings) {
686
+ return makeRawChain(context, interiorAdvance(context, state, name, bindings));
723
687
  },
724
688
  find(entries) {
725
689
  return makeRuleValue(completeRule(context, state, findColumns(context, entries)));
@@ -735,8 +699,8 @@ function makeRawScope(context) {
735
699
  match(relation, bindings) {
736
700
  return makeRawChain(context, advanceMatch(context, EMPTY_RULE, relation, bindings));
737
701
  },
738
- idb(target, bindings) {
739
- return makeRawChain(context, idbAdvance(context, EMPTY_RULE, target, bindings));
702
+ interior(name, bindings) {
703
+ return makeRawChain(context, interiorAdvance(context, EMPTY_RULE, name, bindings));
740
704
  }
741
705
  };
742
706
  Object.freeze(scope);
@@ -755,18 +719,39 @@ function isTypedScope(scope) {
755
719
  return typeof scope.match === "function";
756
720
  }
757
721
  /** Builds one query-rule builder (the typed face of the raw builder). */
758
- function makeQueryRuleScope(theory) {
759
- const raw = makeRawScope({ kind: "query", classes: theory.classes, theory });
722
+ function makeQueryRuleScope(theory, env) {
723
+ const raw = makeRawScope({ kind: "query", classes: theory.classes, theory, ...env });
760
724
  if (!isTypedScope(raw)) {
761
725
  throw errors.new("query rule builder construction incomplete");
762
726
  }
763
727
  return raw;
764
728
  }
765
- /** Builds one output-rule builder over a program's recs. */
766
- function makeOutputRuleScope(program) {
767
- const raw = makeRawScope({ kind: "output", program, classes: program.classes, theory: program.theory });
729
+ /** Builds one interior-rule builder. */
730
+ function makeInteriorRuleScope(theory, env, self) {
731
+ const raw = makeRawScope({ kind: "interior", self, classes: theory.classes, theory, ...env });
732
+ if (!isTypedScope(raw)) {
733
+ throw errors.new("interior rule builder construction incomplete");
734
+ }
735
+ return raw;
736
+ }
737
+ function makeRecRuleScope(theory, env, self, kind) {
738
+ const raw = kind === "rec-base"
739
+ ? makeRawScope({
740
+ kind: "rec-base",
741
+ self: self,
742
+ classes: theory.classes,
743
+ theory,
744
+ ...env
745
+ })
746
+ : makeRawScope({
747
+ kind: "rec-arm",
748
+ self: self,
749
+ classes: theory.classes,
750
+ theory,
751
+ ...env
752
+ });
768
753
  if (!isTypedScope(raw)) {
769
- throw errors.new("program output rule builder construction incomplete");
754
+ throw errors.new("recursive rule builder construction incomplete");
770
755
  }
771
756
  return raw;
772
757
  }
@@ -784,9 +769,6 @@ function headSignature(column) {
784
769
  if (agg.op === "fold") {
785
770
  return `${column.name}:${agg.fold}`;
786
771
  }
787
- if (agg.op === "arg") {
788
- return `${column.name}:${agg.direction}`;
789
- }
790
772
  return `${column.name}:${agg.op}`;
791
773
  }
792
774
  /** The roster a param anchor carries: present exactly on a closed-reference field anchor. */
@@ -798,12 +780,18 @@ function renderParamAnchor(roster) {
798
780
  return roster === undefined ? "a non-closed position" : `a ${roster.name} reference`;
799
781
  }
800
782
  /**
801
- * Folds every rule's param uses (recs in declaration order first, output
802
- * rules last — exactly the lowering walk) into the query's registry: first
803
- * use mints the dense `ParamId`, the first FIELD-ANCHORED use types the
804
- * wire, and one name keeps one shape AND one closedness.
783
+ * Folds every rule's param uses (interiors in declaration order, then rec
784
+ * base, then rec arms, then main — exactly the lowering walk) into the
785
+ * query's registry: first use mints the dense `ParamId`, the first
786
+ * FIELD-ANCHORED use types the wire, and one name keeps one shape AND one
787
+ * closedness. The orderable ban needs no registry arm: an order use always
788
+ * anchors its SIBLING's domain (the no-variable-side spelling is refused
789
+ * at the comparison constructor), so a closed-anchored param under an
790
+ * order op dies at the one-domain wall here — and an order use whose
791
+ * sibling is itself closed-bound dies at the comparison's own var-side
792
+ * wall first.
805
793
  */
806
- function paramRegistryOf(recs, rules) {
794
+ function paramRegistryOf(interiors, rec, rules) {
807
795
  const order = [];
808
796
  const byName = new Map();
809
797
  function fold(uses) {
@@ -815,8 +803,7 @@ function paramRegistryOf(recs, rules) {
815
803
  shape: use.shape,
816
804
  anchor: use.anchor,
817
805
  op: use.op,
818
- members: use.members,
819
- orderOp: isOrderOp(use.op) ? use.op : undefined
806
+ members: use.members
820
807
  });
821
808
  continue;
822
809
  }
@@ -837,13 +824,18 @@ function paramRegistryOf(recs, rules) {
837
824
  existing.anchor = use.anchor;
838
825
  existing.op = use.op;
839
826
  }
840
- if (existing.orderOp === undefined && isOrderOp(use.op)) {
841
- existing.orderOp = use.op;
842
- }
843
827
  }
844
828
  }
845
- for (const rec of recs) {
846
- for (const rule of rec.rules) {
829
+ for (const interior of interiors) {
830
+ for (const rule of interior.rules) {
831
+ fold(rule.paramUses);
832
+ }
833
+ }
834
+ if (rec !== null) {
835
+ for (const rule of rec.base) {
836
+ fold(rule.paramUses);
837
+ }
838
+ for (const rule of rec.rec) {
847
839
  fold(rule.paramUses);
848
840
  }
849
841
  }
@@ -855,10 +847,6 @@ function paramRegistryOf(recs, rules) {
855
847
  if (entry === undefined) {
856
848
  throw errors.new(`query param ${name} lost its registry entry`);
857
849
  }
858
- const anchorRoster = anchorRosterOf(entry.anchor);
859
- if (entry.orderOp !== undefined && anchorRoster !== undefined) {
860
- throw closedOrderError(`query param ${name}`, `its ${entry.orderOp} use's anchor`, anchorRoster.name);
861
- }
862
850
  /**
863
851
  * A membership array's handle names are program constants, so the
864
852
  * entry stores the resolved IMAGE: each name rides the one
@@ -883,93 +871,185 @@ function paramRegistryOf(recs, rules) {
883
871
  return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op, membership });
884
872
  }));
885
873
  }
886
- /**
887
- * Assembles the runtime query value over completed rules: every rule must
888
- * derive the SAME head (name and aggregate shape, position for position —
889
- * the decode labels and the engine's alignment rule agree), and the param
890
- * registry folds in program-walk order.
891
- */
892
- function makeRawQuery(theory, recs, rules) {
874
+ /** Asserts every rule in a list derives the same head (name, aggregate shape, closed slice, class). */
875
+ function assertAlignedHeads(label, rules) {
893
876
  const first = rules[0];
894
877
  if (first === undefined) {
895
- throw errors.new("a query needs at least one rule");
878
+ throw errors.new(`${label} needs at least one rule`);
896
879
  }
897
880
  const signature = first.finds.map(headSignature).join(", ");
898
881
  rules.forEach(function verifyHead(rule, index) {
899
882
  const candidate = rule.finds.map(headSignature).join(", ");
900
883
  if (candidate !== signature) {
901
- throw errors.new(`every rule of a query derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`);
884
+ throw errors.new(`every rule of ${label} derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`);
902
885
  }
903
- // The closed slice is part of the head too: one answer column decodes
904
- // through one roster, so a union whose rules bind a column at different
905
- // vocabularies (or one closed, one bare) is refused pointed.
906
886
  rule.finds.forEach(function verifyClosedSlice(column, position) {
907
887
  const lead = first.finds[position];
908
888
  if (lead !== undefined && column.closed !== lead.closed) {
909
- throw errors.new(`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)`);
889
+ throw errors.new(`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)`);
910
890
  }
911
- // The law-class wall on the union head: one answer column is one
912
- // value space, so the classed mint slot each rule binds the column
913
- // at must join across rules — the SAME fieldJoins judgment every
914
- // join/eq/negated-atom position enforces (the SDK holds it because
915
- // the wire IR carries no domains).
916
891
  if (lead === undefined) {
917
892
  return;
918
893
  }
919
894
  if (lead.slot !== undefined && column.slot !== undefined && !fieldJoins(lead.slot, column.slot)) {
920
- throw errors.new(`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)`);
895
+ throw errors.new(`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)`);
921
896
  }
922
897
  });
923
898
  });
899
+ }
900
+ function afterMainError(what) {
901
+ return errors.new(`query: ${what} after a main rule is unwritable — declaration order is interiors, then rec, then main`);
902
+ }
903
+ /**
904
+ * Assembles the runtime query value over completed rules: every rule must
905
+ * derive the SAME head (name and aggregate shape, position for position —
906
+ * the decode labels and the engine's alignment rule agree), and the param
907
+ * registry folds in query-walk order.
908
+ */
909
+ function makeRawQuery(theory, interiors, rec, rules) {
910
+ assertAlignedHeads("a query", rules);
911
+ const first = rules[0];
912
+ if (first === undefined) {
913
+ throw errors.new("a query needs at least one rule");
914
+ }
915
+ const env = { interiors, rec };
924
916
  const data = Object.freeze({
925
- recs: Object.freeze([...recs]),
917
+ interiors: Object.freeze([...interiors]),
918
+ rec,
926
919
  rules: Object.freeze([...rules]),
927
920
  finds: first.finds,
928
- params: paramRegistryOf(recs, rules)
921
+ params: paramRegistryOf(interiors, rec, rules)
929
922
  });
930
923
  const value = {
931
924
  schema: theory,
932
925
  data,
933
926
  rule(build) {
934
- const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory }));
935
- return makeRawQuery(theory, recs, [...rules, built.rule]);
927
+ const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory, ...env }));
928
+ return makeRawQuery(theory, interiors, rec, [...rules, built.rule]);
929
+ },
930
+ interior() {
931
+ throw afterMainError("interior");
932
+ },
933
+ recursive() {
934
+ throw afterMainError("recursive");
936
935
  }
937
936
  };
938
937
  Object.freeze(value);
939
938
  return value;
940
939
  }
941
- /**
942
- * The query values' trusted admission seam (the {@link isTypedScope} pattern):
943
- * the checkable fact — the value was assembled over the identical theory —
944
- * is verified before the raw value is admitted at its typed face.
945
- */
946
- function isQueryValue(theory, value) {
947
- return value.schema === theory;
940
+ function makeQuery(theory, interiors, rec, rules) {
941
+ return makeRawQuery(theory, interiors, rec, rules);
948
942
  }
949
- /** Assembles one typed query value (rules already completed). */
950
- function makeQuery(theory, recs, rules) {
951
- const raw = makeRawQuery(theory, recs, rules);
952
- if (!isQueryValue(theory, raw)) {
953
- throw errors.new("query value construction incomplete");
943
+ /** Collects one Interior from its builders. */
944
+ function collectInterior(theory, env, name, builds) {
945
+ if (builds.length === 0) {
946
+ throw errors.new(`query: interior ${name} needs at least one rule`);
954
947
  }
955
- return raw;
948
+ const rules = builds.map(function buildRule(buildOne) {
949
+ return buildOne(makeInteriorRuleScope(theory, env, name)).rule;
950
+ });
951
+ assertAlignedHeads(`interior ${name}`, rules);
952
+ const first = rules[0];
953
+ if (first === undefined) {
954
+ throw errors.new(`query: interior ${name} needs at least one rule`);
955
+ }
956
+ return Object.freeze({ name, finds: first.finds, rules: Object.freeze(rules) });
956
957
  }
957
- /**
958
- * Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule` adds
959
- * one conjunctive rule; multiple rules are the set union. The schema's
960
- * law-computed class map and theory value ride into every rule builder — the
961
- * join walls compare against the mint slots off it.
962
- */
963
- function query(theory) {
958
+ /** Collects the Rec from tagged base/rec builder arrays. */
959
+ function collectRec(theory, interiors, name, baseBuilds, recBuilds) {
960
+ if (baseBuilds.length === 0) {
961
+ throw errors.new(`query: recursive ${name} has no base arms`);
962
+ }
963
+ if (recBuilds.length === 0) {
964
+ throw errors.new(`query: recursive ${name} has no rec arms`);
965
+ }
966
+ const handle = Object.freeze({ name });
967
+ const baseEnv = { interiors, rec: handle };
968
+ const base = baseBuilds.map(function buildBase(buildOne) {
969
+ return buildOne(makeRecRuleScope(theory, baseEnv, handle, "rec-base")).rule;
970
+ });
971
+ assertAlignedHeads(`recursive ${name}`, base);
972
+ const first = base[0];
973
+ if (first === undefined) {
974
+ throw errors.new(`query: recursive ${name} has no base arms`);
975
+ }
976
+ const firstFind = first.finds[0];
977
+ if (firstFind === undefined) {
978
+ throw errors.new(`query: recursive ${name} has no head`);
979
+ }
980
+ const finds = [firstFind, ...first.finds.slice(1)];
981
+ const head = Object.freeze({ name, finds });
982
+ const recEnv = { interiors, rec: head };
983
+ const rec = recBuilds.map(function buildRec(buildOne) {
984
+ return buildOne(makeRecRuleScope(theory, recEnv, head, "rec-arm")).rule;
985
+ });
986
+ assertAlignedHeads(`recursive ${name}`, [...base, ...rec]);
987
+ const firstRec = rec[0];
988
+ if (firstRec === undefined) {
989
+ throw errors.new(`query: recursive ${name} has no rec arms`);
990
+ }
991
+ const sealedBase = [first, ...base.slice(1)];
992
+ const sealedRec = [firstRec, ...rec.slice(1)];
993
+ const recData = Object.freeze({
994
+ name,
995
+ finds,
996
+ base: sealedBase,
997
+ rec: sealedRec
998
+ });
999
+ return recData;
1000
+ }
1001
+ /** Builds the query start (interiors, then optional rec, then the first main rule). */
1002
+ function makeQueryStart(theory, interiors, rec) {
1003
+ const env = { interiors, rec };
964
1004
  const start = {
1005
+ interior(name, ...builds) {
1006
+ if (rec !== null) {
1007
+ throw errors.new("query: interior after recursive is unwritable — declaration order is interiors, then rec, then main");
1008
+ }
1009
+ if (interiors.some(function sameName(interior) {
1010
+ return interior.name === name;
1011
+ })) {
1012
+ throw errors.new(`query: interior ${name} is already declared — names are unique`);
1013
+ }
1014
+ if (name.length === 0) {
1015
+ throw errors.new("query: an interior needs a name");
1016
+ }
1017
+ const data = collectInterior(theory, env, name, builds);
1018
+ return makeQueryStart(theory, [...interiors, data], null);
1019
+ },
1020
+ recursive(name, arms) {
1021
+ if (rec !== null) {
1022
+ throw errors.new("query: a second recursive is unwritable — this cut admits one linear rec");
1023
+ }
1024
+ if (interiors.some(function sameName(interior) {
1025
+ return interior.name === name;
1026
+ })) {
1027
+ throw errors.new(`query: interior and recursive share the name ${name}`);
1028
+ }
1029
+ if (name.length === 0) {
1030
+ throw errors.new("query: recursive needs a name");
1031
+ }
1032
+ const data = collectRec(theory, interiors, name, arms.base, arms.rec);
1033
+ return makeQueryStart(theory, interiors, data);
1034
+ },
965
1035
  rule(build) {
966
- const built = build(makeQueryRuleScope(theory));
967
- return makeQuery(theory, [], [built.rule]);
1036
+ const built = build(makeQueryRuleScope(theory, env));
1037
+ return makeQuery(theory, interiors, rec, [built.rule]);
968
1038
  }
969
1039
  };
970
1040
  Object.freeze(start);
971
1041
  return start;
972
1042
  }
1043
+ /**
1044
+ * Opens a query over a schema: `query(S).rule(r => ...)`, optionally with
1045
+ * `interior` / `recursive` first. Each `.rule` adds one conjunctive rule;
1046
+ * multiple rules are the set union. The schema's law-computed class map and
1047
+ * theory value ride into every rule builder — the join walls compare
1048
+ * against the mint slots off it.
1049
+ */
1050
+ function query(theory) {
1051
+ return makeQueryStart(theory, [], null);
1052
+ }
973
1053
  /**
974
1054
  * Tags one closed-reference literal: the handle NAME, verified against the
975
1055
  * roster and translated to its declaration-order row id, tagged u64. THE
@@ -1144,30 +1224,29 @@ function lowerBindingTerm(ctx, context, binding, ids) {
1144
1224
  }
1145
1225
  }
1146
1226
  /**
1147
- * Lowers one idb atom: named bindings placed by HEAD order, `FieldId(i)` =
1148
- * head position i. Every head column of the rec must be bound (a missing key
1227
+ * Lowers one interior atom: named bindings placed by HEAD order, `FieldId(i)` =
1228
+ * head position i. Every head column of the table must be bound (a missing key
1149
1229
  * is refused pointed); the var-id assignment order is head order, so the
1150
1230
  * first-use numbering matches the name-keyed edition exactly.
1151
1231
  */
1152
- function lowerIdbAtom(ctx, rec, bindings, ids) {
1153
- const pred = ctx.recIds.get(rec);
1154
- if (pred === undefined) {
1155
- throw errors.new(`query lowering: rec ${rec.name} was declared by a different program`);
1232
+ function lowerInteriorAtom(ctx, target, bindings, ids) {
1233
+ const interior = ctx.interiorIds.get(target.name);
1234
+ if (interior === undefined) {
1235
+ throw errors.new(`query lowering: derived table ${target.name} was not declared on this query`);
1156
1236
  }
1157
- const head = rec.rules[0];
1158
- if (head === undefined) {
1159
- throw errors.new(`query lowering: rec ${rec.name} has no rules`);
1237
+ if (target.finds.length === 0) {
1238
+ throw errors.new(`query lowering: derived table ${target.name} has no head`);
1160
1239
  }
1161
- const irBindings = head.finds.map(function lowerPosition(column, position) {
1240
+ const irBindings = target.finds.map(function lowerPosition(column, position) {
1162
1241
  const binding = bindings.find(function byKey(candidate) {
1163
1242
  return candidate.key === column.name;
1164
1243
  });
1165
1244
  if (binding === undefined) {
1166
- throw errors.new(`query lowering: idb ${rec.name} omits head column ${column.name}`);
1245
+ throw errors.new(`query lowering: interior ${target.name} omits head column ${column.name}`);
1167
1246
  }
1168
1247
  return [position, { kind: "var", var: ids.of(binding.ref) }];
1169
1248
  });
1170
- return { source: { kind: "idb", pred }, bindings: irBindings };
1249
+ return { source: { kind: "interior", interior }, bindings: irBindings };
1171
1250
  }
1172
1251
  /** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
1173
1252
  function lowerCmpTerm(ctx, side, sibling, ids, op) {
@@ -1204,24 +1283,17 @@ function cmpAnchorOf(ctx, sibling) {
1204
1283
  }
1205
1284
  /** Lowers one comparison. */
1206
1285
  function lowerComparison(ctx, cmp, ids) {
1207
- if (cmp.op === "allen") {
1208
- const maskData = cmp.mask;
1209
- if (maskData === undefined) {
1210
- throw errors.new("query lowering: an allen comparison lost its mask");
1211
- }
1212
- const mask = maskData.kind === "literal"
1213
- ? { kind: "literal", mask: maskData.mask }
1214
- : { kind: "param", param: paramIdOf(ctx, maskData.name) };
1286
+ if (cmp.op.kind === "allen") {
1215
1287
  return {
1216
- op: { kind: "allen", mask },
1288
+ op: { kind: "allen", mask: cmp.op.mask },
1217
1289
  lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
1218
1290
  rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
1219
1291
  };
1220
1292
  }
1221
1293
  return {
1222
- op: { kind: cmp.op },
1223
- lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, cmp.op),
1224
- rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, cmp.op)
1294
+ op: { kind: cmp.op.kind },
1295
+ lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, cmp.op.kind),
1296
+ rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, cmp.op.kind)
1225
1297
  };
1226
1298
  }
1227
1299
  /** Lowers one condition node (comparison leaf or and/or tree). */
@@ -1248,16 +1320,12 @@ function lowerFind(entry, ids) {
1248
1320
  switch (agg.op) {
1249
1321
  case "count":
1250
1322
  return { kind: "aggregate", op: { kind: "count" } };
1251
- case "countDistinct":
1252
- return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) };
1253
1323
  case "fold": {
1254
1324
  if ("duration" in agg.over) {
1255
1325
  return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) };
1256
1326
  }
1257
1327
  return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) };
1258
1328
  }
1259
- case "arg":
1260
- return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) };
1261
1329
  case "pack":
1262
1330
  return { kind: "aggregate", op: { kind: "pack" }, over: ids.of(agg.over) };
1263
1331
  }
@@ -1267,12 +1335,8 @@ function headOpOf(agg) {
1267
1335
  switch (agg.op) {
1268
1336
  case "count":
1269
1337
  return "count";
1270
- case "countDistinct":
1271
- return "countDistinct";
1272
1338
  case "fold":
1273
1339
  return agg.fold;
1274
- case "arg":
1275
- return agg.direction;
1276
1340
  case "pack":
1277
1341
  return "pack";
1278
1342
  }
@@ -1301,9 +1365,12 @@ function lowerRule(ctx, rule) {
1301
1365
  negated.push(lowerAtom(ctx, item.atom, ids));
1302
1366
  break;
1303
1367
  }
1304
- case "idb": {
1305
- const bucket = item.negated ? negated : atoms;
1306
- bucket.push(lowerIdbAtom(ctx, item.rec, item.bindings, ids));
1368
+ case "interior": {
1369
+ atoms.push(lowerInteriorAtom(ctx, item.target, item.bindings, ids));
1370
+ break;
1371
+ }
1372
+ case "negatedInterior": {
1373
+ negated.push(lowerInteriorAtom(ctx, item.target, item.bindings, ids));
1307
1374
  break;
1308
1375
  }
1309
1376
  case "cond": {
@@ -1322,9 +1389,9 @@ function lowerRule(ctx, rule) {
1322
1389
  };
1323
1390
  }
1324
1391
  /**
1325
- * Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
1326
- * recs in declaration order (`PredId` = index), the output predicate
1327
- * appended last. Every registered param must carry a field anchor by now.
1392
+ * Lowers a query value to the bridge's `QueryIr` — pure and stable: interiors
1393
+ * in declaration order, optional rec, then main. Every registered param must
1394
+ * carry a field anchor by now.
1328
1395
  */
1329
1396
  function lowerQuery(q) {
1330
1397
  const theory = q.schema;
@@ -1332,39 +1399,48 @@ function lowerQuery(q) {
1332
1399
  Object.keys(theory.relations).forEach(function assignOrdinal(name, index) {
1333
1400
  relationIds.set(name, index);
1334
1401
  });
1335
- const recIds = new Map();
1336
- q.data.recs.forEach(function assignPredId(rec, index) {
1337
- recIds.set(rec, index);
1402
+ const interiorIds = new Map();
1403
+ q.data.interiors.forEach(function assignInteriorId(interior, index) {
1404
+ interiorIds.set(interior.name, index);
1338
1405
  });
1406
+ if (q.data.rec !== null) {
1407
+ interiorIds.set(q.data.rec.name, q.data.interiors.length);
1408
+ }
1339
1409
  const paramIds = new Map();
1340
1410
  const params = new Map();
1341
1411
  q.data.params.forEach(function assignParamId(entry, index) {
1342
- if (entry.anchor === undefined && entry.shape !== "mask") {
1412
+ if (entry.anchor === undefined) {
1343
1413
  throw errors.new(`query param ${entry.name} has no field-anchored use — bind it in an atom or compare it against a bound variable`);
1344
1414
  }
1345
1415
  paramIds.set(entry.name, index);
1346
1416
  params.set(entry.name, entry);
1347
1417
  });
1348
- const ctx = { theory, relationIds, recIds, paramIds, params };
1349
- const predicates = q.data.recs.map(function lowerRec(rec) {
1350
- const head = rec.rules[0];
1351
- if (head === undefined) {
1352
- throw errors.new(`query lowering: rec ${rec.name} has no rules`);
1353
- }
1354
- return {
1355
- head: head.finds.map(headTermOf),
1356
- rules: rec.rules.map(function lowerRecRule(rule) {
1357
- return lowerRule(ctx, rule);
1358
- })
1359
- };
1360
- });
1361
- predicates.push({
1418
+ const ctx = { theory, relationIds, interiorIds, paramIds, params };
1419
+ return parseQueryIr({
1420
+ interiors: q.data.interiors.map(function lowerInterior(interior) {
1421
+ return {
1422
+ head: interior.finds.map(headTermOf),
1423
+ rules: interior.rules.map(function lowerInteriorRule(rule) {
1424
+ return lowerRule(ctx, rule);
1425
+ })
1426
+ };
1427
+ }),
1428
+ rec: q.data.rec === null
1429
+ ? null
1430
+ : {
1431
+ head: q.data.rec.finds.map(headTermOf),
1432
+ base: q.data.rec.base.map(function lowerBase(rule) {
1433
+ return lowerRule(ctx, rule);
1434
+ }),
1435
+ rec: q.data.rec.rec.map(function lowerRecArm(rule) {
1436
+ return lowerRule(ctx, rule);
1437
+ })
1438
+ },
1362
1439
  head: q.data.finds.map(headTermOf),
1363
- rules: q.data.rules.map(function lowerOutputRule(rule) {
1440
+ rules: q.data.rules.map(function lowerMainRule(rule) {
1364
1441
  return lowerRule(ctx, rule);
1365
1442
  })
1366
1443
  });
1367
- return { predicates, output: q.data.recs.length };
1368
1444
  }
1369
- export { lowerQuery, makeOutputRuleScope, makeQuery, makeRawScope, query, taggedCmpLiteral, taggedLiteral };
1445
+ export { lowerQuery, makeRawScope, query, taggedCmpLiteral, taggedLiteral };
1370
1446
  //# sourceMappingURL=lower.js.map