@bjornpagen/bumbledb 1.2.1 → 1.3.1

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 (108) hide show
  1. package/COOKBOOK.md +240 -9
  2. package/README.md +9 -6
  3. package/dist/alternatives.d.ts +25 -0
  4. package/dist/alternatives.d.ts.map +1 -0
  5. package/dist/alternatives.js +59 -0
  6. package/dist/alternatives.js.map +1 -0
  7. package/dist/changes.d.ts +0 -1
  8. package/dist/changes.d.ts.map +1 -1
  9. package/dist/changes.js +2 -8
  10. package/dist/changes.js.map +1 -1
  11. package/dist/codec.d.ts +8 -1
  12. package/dist/codec.d.ts.map +1 -1
  13. package/dist/codec.js +29 -2
  14. package/dist/codec.js.map +1 -1
  15. package/dist/db-native.d.ts +2 -9
  16. package/dist/db-native.d.ts.map +1 -1
  17. package/dist/db-native.js.map +1 -1
  18. package/dist/index.d.ts +5 -4
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/internal/log.d.ts +4 -4
  23. package/dist/internal/log.d.ts.map +1 -1
  24. package/dist/internal/log.js +2 -2
  25. package/dist/internal/log.js.map +1 -1
  26. package/dist/native.d.ts +23 -9
  27. package/dist/native.d.ts.map +1 -1
  28. package/dist/native.js.map +1 -1
  29. package/dist/query/atom.d.ts +2 -1
  30. package/dist/query/atom.d.ts.map +1 -1
  31. package/dist/query/atom.js.map +1 -1
  32. package/dist/query/compute.d.ts +19 -4
  33. package/dist/query/compute.d.ts.map +1 -1
  34. package/dist/query/compute.js +17 -13
  35. package/dist/query/compute.js.map +1 -1
  36. package/dist/query/description.d.ts +10 -2
  37. package/dist/query/description.d.ts.map +1 -1
  38. package/dist/query/description.js +14 -5
  39. package/dist/query/description.js.map +1 -1
  40. package/dist/query/find.d.ts +13 -6
  41. package/dist/query/find.d.ts.map +1 -1
  42. package/dist/query/find.js +1 -1
  43. package/dist/query/find.js.map +1 -1
  44. package/dist/query/lower.d.ts +14 -2
  45. package/dist/query/lower.d.ts.map +1 -1
  46. package/dist/query/lower.js +64 -50
  47. package/dist/query/lower.js.map +1 -1
  48. package/dist/query/parse-ir.d.ts.map +1 -1
  49. package/dist/query/parse-ir.js +26 -3
  50. package/dist/query/parse-ir.js.map +1 -1
  51. package/dist/query/scope.d.ts.map +1 -1
  52. package/dist/query/scope.js +6 -1
  53. package/dist/query/scope.js.map +1 -1
  54. package/dist/query/segments.d.ts +20 -0
  55. package/dist/query/segments.d.ts.map +1 -0
  56. package/dist/query/segments.js +35 -0
  57. package/dist/query/segments.js.map +1 -0
  58. package/dist/runtime-codes.d.ts +1 -1
  59. package/dist/runtime-codes.d.ts.map +1 -1
  60. package/dist/runtime-codes.js +1 -0
  61. package/dist/runtime-codes.js.map +1 -1
  62. package/dist/runtime-errors.d.ts +23 -3
  63. package/dist/runtime-errors.d.ts.map +1 -1
  64. package/dist/runtime-errors.js +16 -3
  65. package/dist/runtime-errors.js.map +1 -1
  66. package/dist/runtime-native.d.ts +3 -8
  67. package/dist/runtime-native.d.ts.map +1 -1
  68. package/dist/runtime-native.js.map +1 -1
  69. package/dist/runtime.d.ts +0 -23
  70. package/dist/runtime.d.ts.map +1 -1
  71. package/dist/runtime.js +0 -36
  72. package/dist/runtime.js.map +1 -1
  73. package/dist/scalar.d.ts +46 -94
  74. package/dist/scalar.d.ts.map +1 -1
  75. package/dist/scalar.js +69 -156
  76. package/dist/scalar.js.map +1 -1
  77. package/dist/schema-file.d.ts +7 -0
  78. package/dist/schema-file.d.ts.map +1 -0
  79. package/dist/schema-file.js +24 -0
  80. package/dist/schema-file.js.map +1 -0
  81. package/pack-provenance.json +4 -4
  82. package/package.json +4 -4
  83. package/src/alternatives.ts +95 -0
  84. package/src/changes.ts +2 -8
  85. package/src/codec.ts +43 -3
  86. package/src/db-native.ts +2 -10
  87. package/src/index.ts +23 -6
  88. package/src/internal/log.ts +2 -4
  89. package/src/native.ts +21 -7
  90. package/src/query/atom.ts +2 -0
  91. package/src/query/compute.ts +46 -16
  92. package/src/query/description.ts +38 -8
  93. package/src/query/find.ts +37 -26
  94. package/src/query/lower.ts +89 -52
  95. package/src/query/parse-ir.ts +27 -3
  96. package/src/query/scope.ts +8 -1
  97. package/src/query/segments.ts +61 -0
  98. package/src/runtime-codes.ts +1 -0
  99. package/src/runtime-errors.ts +16 -3
  100. package/src/runtime-native.ts +1 -9
  101. package/src/runtime.ts +1 -70
  102. package/src/scalar.ts +138 -285
  103. package/src/schema-file.ts +38 -0
  104. package/dist/migration.d.ts +0 -31
  105. package/dist/migration.d.ts.map +0 -1
  106. package/dist/migration.js +0 -47
  107. package/dist/migration.js.map +0 -1
  108. package/src/migration.ts +0 -61
@@ -10,9 +10,19 @@ import type { AnyQuery, ChainContext, Query } from "#query/lower.ts"
10
10
  import { alignedHeadOf, EMPTY_RULE, lowerQuery, makeRawChain, makeRawQuery, taggedCmpLiteral } from "#query/lower.ts"
11
11
  import { parseQueryIr } from "#query/parse-ir.ts"
12
12
  import { type AnyVar, type MatchOwner, makeParam, makeSetParam, type ParamsRecord, v } from "#query/scope.ts"
13
+ import { difference, type IntervalVar, intersection } from "#query/segments.ts"
13
14
  import type { FieldsShape } from "#relation.ts"
14
15
  import { handleOf } from "#rows.ts"
15
- import { queryVarLeaf, scalarBinary, scalarCast, scalarFloatPredicate, scalarLiteral, scalarNegate } from "#scalar.ts"
16
+ import {
17
+ queryVarLeaf,
18
+ scalarBinary,
19
+ scalarCast,
20
+ scalarFloatPredicate,
21
+ scalarLiteral,
22
+ scalarMeasure,
23
+ scalarMulDiv,
24
+ scalarNegate
25
+ } from "#scalar.ts"
16
26
  import type { Schema, SchemaRelations } from "#schema.ts"
17
27
  import { schemaDescriptor } from "#schema.ts"
18
28
  import { arrayValue, recordValue, valueDescriptor } from "#values.ts"
@@ -39,6 +49,12 @@ interface DescriptionParameter {
39
49
 
40
50
  type DescriptionRow<F extends FieldsShape> = { readonly [K in keyof F]: Infer<F[K]> }
41
51
 
52
+ /** Checked result fields remain exact through v(imported), just as at runtime.
53
+ * Carrier classes come from the replayed head and remain runtime-checked. */
54
+ type DescriptionHead<F extends FieldsShape> = {
55
+ readonly [K in keyof F]: { readonly field: F[K]; readonly class: string | undefined }
56
+ }
57
+
42
58
  function refused(context: string, expected: string): never {
43
59
  throw new AuthoringError({
44
60
  message: `${context}: ${expected}`,
@@ -237,6 +253,11 @@ function replayRule(
237
253
  case "var": {
238
254
  const variable = variableAt(input.var)
239
255
  const kind = variable.field.kind
256
+ if (variable.field.kind === "interval")
257
+ return queryVarLeaf(
258
+ variable,
259
+ ({ u64: "intervalU64", i64: "intervalI64", f64: "intervalF64" } as const)[variable.field.element]
260
+ )
240
261
  if (
241
262
  rosterOf(variable.field) !== undefined ||
242
263
  (kind !== "bool" && kind !== "u64" && kind !== "i64" && kind !== "f64")
@@ -248,17 +269,21 @@ function replayRule(
248
269
  const value = input.value
249
270
  switch (value.kind) {
250
271
  case "u64":
251
- return scalarLiteral("query-var", { u64: value.value })
272
+ return scalarLiteral({ u64: value.value })
252
273
  case "i64":
253
- return scalarLiteral("query-var", { i64: value.value })
274
+ return scalarLiteral({ i64: value.value })
254
275
  case "f64":
255
- return scalarLiteral("query-var", { f64: value.value })
276
+ return scalarLiteral({ f64: value.value })
256
277
  case "bool":
257
- return scalarLiteral("query-var", { bool: value.value })
278
+ return scalarLiteral({ bool: value.value })
258
279
  default:
259
280
  return refused("query compute", "requires a scalar literal")
260
281
  }
261
282
  }
283
+ case "measure":
284
+ return scalarMeasure("query compute", scalar(input.expr))
285
+ case "mulDiv":
286
+ return scalarMulDiv("query compute", scalar(input.a), scalar(input.b), scalar(input.divisor), input.rounding)
262
287
  case "negate":
263
288
  return scalarNegate("query compute", scalar(input.expr))
264
289
  case "isNaN":
@@ -281,6 +306,11 @@ function replayRule(
281
306
  let value: unknown
282
307
  if (find.kind === "var") value = variableAt(find.var)
283
308
  else if (find.kind === "compute") value = scalar(find.expr)
309
+ else if (find.kind === "segments")
310
+ value = (find.op === "intersection" ? intersection : difference)(
311
+ variableAt(find.left) as IntervalVar,
312
+ variableAt(find.right) as IntervalVar
313
+ )
284
314
  else if (find.kind === "count") value = { agg: "count" }
285
315
  else value = { agg: find.kind === "pack" ? "pack" : find.op.kind, over: variableAt(find.over) }
286
316
  return [column, value]
@@ -293,7 +323,7 @@ function queryFromDescription<Rels extends SchemaRelations, Classes extends Sche
293
323
  schema: Schema<Rels, Classes>,
294
324
  input: unknown,
295
325
  result: F
296
- ): Query<Rels, DescriptionRow<F>, ParamsRecord, Classes> {
326
+ ): Query<Rels, DescriptionRow<F>, ParamsRecord, Classes, DescriptionHead<F>> {
297
327
  const theory = schemaDescriptor(schema)
298
328
  const description = checkedDescription(input)
299
329
  const resultRecord = recordValue(
@@ -387,8 +417,8 @@ function queryFromDescription<Rels extends SchemaRelations, Classes extends Sche
387
417
  if (usedParameters.size !== description.parameters.length) refused("query parameters", "declared parameter is unused")
388
418
  // Complete shared lowering also checks tagged literals and ordinal bounds.
389
419
  lowerQuery(value)
390
- return value as unknown as Query<Rels, DescriptionRow<F>, ParamsRecord, Classes>
420
+ return value as unknown as Query<Rels, DescriptionRow<F>, ParamsRecord, Classes, DescriptionHead<F>>
391
421
  }
392
422
 
393
- export type { DescriptionParameter, DescriptionRow, DescriptionTable, QueryDescription }
423
+ export type { DescriptionHead, DescriptionParameter, DescriptionRow, DescriptionTable, QueryDescription }
394
424
  export { describeQuery, queryFromDescription }
package/src/query/find.ts CHANGED
@@ -1,8 +1,9 @@
1
- import type { BoolField, F64Field, I64Field, Infer, U64Field } from "#fields.ts"
1
+ import type { BoolField, F64Field, I64Field, Infer, IntervalField, U64Field } from "#fields.ts"
2
2
  import type { SchemaClasses } from "#law.ts"
3
3
  import type { IntervalVarOk, NumericVarOk } from "#query/atom.ts"
4
4
  import type { AnyComputeExpr, ComputeExpr, ComputeValue } from "#query/compute.ts"
5
5
  import type { AnyVar, MintSlotOf } from "#query/scope.ts"
6
+ import type { Segments } from "#query/segments.ts"
6
7
  import type { ScalarKind } from "#scalar.ts"
7
8
 
8
9
  type FoldOpName = "sum" | "mean" | "min" | "max" | "pack"
@@ -18,7 +19,7 @@ interface Agg<Op extends FoldOpName, Over extends AnyVar> {
18
19
 
19
20
  type AnyAgg = CountAgg | Agg<FoldOpName, AnyVar>
20
21
 
21
- type FindEntry = AnyVar | AnyAgg | AnyComputeExpr
22
+ type FindEntry = AnyVar | AnyAgg | AnyComputeExpr | Segments
22
23
 
23
24
  type FindShape = Readonly<Record<string, FindEntry>>
24
25
 
@@ -33,7 +34,7 @@ function count(): CountAgg {
33
34
  /**
34
35
  * Exact checked sum over a NUMERIC (u64/i64/f64) variable — wide accumulator,
35
36
  * one finalize range check; overflow is the engine's typed runtime error —
36
- * or over the measure (`r.sum(r.duration(w))`). Bool stays refused: a
37
+ * of a measured interval in a following stage. Bool stays refused: a
37
38
  * quantifier is not an addition (R3).
38
39
  */
39
40
  function sum<const O extends AnyVar>(over: O): Agg<"sum", O> {
@@ -69,7 +70,7 @@ type FindEntryOk<E> = E extends AnyVar
69
70
  ? NumericVarOk<O>
70
71
  : E extends Agg<"pack", infer V extends AnyVar>
71
72
  ? IntervalVarOk<V>
72
- : E extends AnyComputeExpr
73
+ : E extends AnyComputeExpr | Segments
73
74
  ? true
74
75
  : false
75
76
 
@@ -81,17 +82,20 @@ type CheckRecFind<F extends FindShape> = {
81
82
  readonly [K in keyof F]: F[K] extends AnyVar ? F[K] : never
82
83
  }
83
84
 
84
- type FindValue<E> = E extends AnyVar
85
- ? Infer<E["field"]>
86
- : E extends CountAgg
87
- ? bigint
88
- : E extends Agg<"sum" | "mean" | "min" | "max", infer O extends AnyVar>
89
- ? Infer<O["field"]>
90
- : E extends Agg<"pack", infer V extends AnyVar>
91
- ? Infer<V["field"]>
92
- : E extends ComputeExpr<infer K extends ScalarKind>
93
- ? ComputeValue<K>
94
- : never
85
+ type FindValue<E> =
86
+ E extends Segments<infer Element>
87
+ ? Infer<IntervalField<Element, undefined>>
88
+ : E extends AnyVar
89
+ ? Infer<E["field"]>
90
+ : E extends CountAgg
91
+ ? bigint
92
+ : E extends Agg<"sum" | "mean" | "min" | "max", infer O extends AnyVar>
93
+ ? Infer<O["field"]>
94
+ : E extends Agg<"pack", infer V extends AnyVar>
95
+ ? Infer<V["field"]>
96
+ : E extends ComputeExpr<infer K extends ScalarKind>
97
+ ? ComputeValue<K>
98
+ : never
95
99
 
96
100
  type RowOfFind<F extends FindShape> = { readonly [K in keyof F]: FindValue<F[K]> }
97
101
 
@@ -108,17 +112,24 @@ type ComputeFields = {
108
112
  * produce bare values, matching findColumnSlotOf at runtime.
109
113
  */
110
114
  type HeadRecordOf<Classes extends SchemaClasses, F extends FindShape> = {
111
- readonly [K in keyof F]: F[K] extends AnyVar
112
- ? MintSlotOf<Classes, F[K]>
113
- : F[K] extends ComputeExpr<infer Kind extends ScalarKind>
114
- ? { readonly field: ComputeFields[Kind]; readonly class: undefined }
115
- : F[K] extends CountAgg
116
- ? { readonly field: U64Field; readonly class: undefined }
117
- : F[K] extends Agg<"mean", AnyVar>
118
- ? { readonly field: F64Field; readonly class: undefined }
119
- : F[K] extends Agg<FoldOpName, infer Over extends AnyVar>
120
- ? { readonly field: Over["field"]; readonly class: undefined }
121
- : never
115
+ readonly [K in keyof F]: F[K] extends Segments<infer Element>
116
+ ? { readonly field: IntervalField<Element, undefined>; readonly class: undefined }
117
+ : F[K] extends AnyVar
118
+ ? MintSlotOf<Classes, F[K]>
119
+ : F[K] extends ComputeExpr<infer Kind extends ScalarKind>
120
+ ? { readonly field: ComputeFields[Kind]; readonly class: undefined }
121
+ : F[K] extends CountAgg
122
+ ? { readonly field: U64Field; readonly class: undefined }
123
+ : F[K] extends Agg<"mean", AnyVar>
124
+ ? { readonly field: F64Field; readonly class: undefined }
125
+ : F[K] extends Agg<"pack", infer Over extends AnyVar>
126
+ ? {
127
+ readonly field: IntervalField<Over["field"] extends IntervalField<infer E> ? E : never, undefined>
128
+ readonly class: undefined
129
+ }
130
+ : F[K] extends Agg<FoldOpName, infer Over extends AnyVar>
131
+ ? { readonly field: Over["field"]; readonly class: undefined }
132
+ : never
122
133
  }
123
134
 
124
135
  export type { Agg, CheckFind, CheckRecFind, FindEntry, FindShape, HeadRecordOf, RowOfFind }
@@ -1,6 +1,6 @@
1
1
  import { membersAgree, sealedFieldsOf } from "#closed.ts"
2
2
  import { AuthoringError, SdkInvariantError } from "#errors.ts"
3
- import type { AnyClosedRoster, AnyField } from "#fields.ts"
3
+ import type { AnyClosedRoster, AnyField, IntervalField } from "#fields.ts"
4
4
  import {
5
5
  assertDeclarationOrderKey,
6
6
  f64 as f64Field,
@@ -13,6 +13,7 @@ import {
13
13
  u64 as u64Field
14
14
  } from "#fields.ts"
15
15
  import { snapshotData } from "#immutable.ts"
16
+ import type { Same } from "#judgment.ts"
16
17
  import type { ClassRecordOf, SchemaClasses } from "#law.ts"
17
18
  import type {
18
19
  AtomIr,
@@ -87,7 +88,8 @@ import {
87
88
  renderFieldKind,
88
89
  term
89
90
  } from "#query/scope.ts"
90
- import { literalWireOf } from "#scalar.ts"
91
+ import { difference, intersection, isSegments, segmentField } from "#query/segments.ts"
92
+ import { scalarWire } from "#scalar.ts"
91
93
  import type { AnySchema, Schema, SchemaRelations } from "#schema.ts"
92
94
  import { schemaDescriptor, schemasAgree } from "#schema.ts"
93
95
  import { arrayValue, recordValue, taggedValueOf } from "#values.ts"
@@ -102,6 +104,25 @@ type HeadShape = Readonly<Record<string, ClassedField>> | undefined
102
104
 
103
105
  type HeadOf<T> = InferredOf<T> extends { readonly head: infer H extends HeadShape } ? H : HeadShape
104
106
 
107
+ type HeadFieldMeet<A extends AnyField, B extends AnyField> = A extends IntervalField
108
+ ? B extends IntervalField
109
+ ? IntervalField<A["element"], Same<A["width"], B["width"]> extends true ? A["width"] : undefined>
110
+ : A
111
+ : A
112
+
113
+ /** The common refinements of the same output column across union arms. */
114
+ type HeadMeet<A extends HeadShape, B extends HeadShape> =
115
+ A extends Readonly<Record<string, ClassedField>>
116
+ ? B extends Readonly<Record<string, ClassedField>>
117
+ ? {
118
+ readonly [K in keyof A & keyof B]: {
119
+ readonly field: HeadFieldMeet<A[K]["field"], B[K]["field"]>
120
+ readonly class: Same<A[K]["class"], B[K]["class"]> extends true ? A[K]["class"] : undefined
121
+ }
122
+ }
123
+ : HeadShape
124
+ : HeadShape
125
+
105
126
  interface RuleValue<Row, P extends ParamsRecord, Head extends HeadShape = undefined> {
106
127
  readonly rule: RuleData
107
128
  readonly [inferred]?: { readonly row: Row; readonly params: P; readonly head: Head }
@@ -147,6 +168,8 @@ interface TermOps {
147
168
  readonly mean: typeof mean
148
169
  readonly min: typeof min
149
170
  readonly max: typeof max
171
+ readonly intersection: typeof intersection
172
+ readonly difference: typeof difference
150
173
  readonly pack: typeof pack
151
174
  }
152
175
 
@@ -409,7 +432,7 @@ interface Query<
409
432
 
410
433
  rule<RV extends AnyRuleValue>(
411
434
  build: (r: QueryRuleScope<Rels, Classes>) => RV
412
- ): Query<Rels, Row | RowOf<RV>, Flatten<Params & ParamsOf<RV>>, Classes, Head | HeadOf<RV>>
435
+ ): Query<Rels, Row | RowOf<RV>, Flatten<Params & ParamsOf<RV>>, Classes, HeadMeet<Head, HeadOf<RV>>>
413
436
 
414
437
  /** A diagnostic name for composition/tracing — never a schema relation. */
415
438
  named(label: string): Query<Rels, Row, Params, Classes, Head>
@@ -476,7 +499,9 @@ const termOps: TermOps = Object.freeze({
476
499
  mean,
477
500
  min,
478
501
  max,
479
- pack
502
+ pack,
503
+ intersection,
504
+ difference
480
505
  })
481
506
 
482
507
  interface RuleBuildState {
@@ -865,6 +890,8 @@ function findColumnOf(name: string, entry: unknown): FindColumn {
865
890
  message: `find ${name}: a ${entry[term]} is not projectable — find takes variables, aggregates or Compute expressions`
866
891
  })
867
892
  }
893
+ if (isSegments(entry))
894
+ return Object.freeze({ name, entry: Object.freeze({ ...entry }), closed: undefined, slot: undefined })
868
895
  if (isComputeExpr(entry)) {
869
896
  return Object.freeze({
870
897
  name,
@@ -940,6 +967,7 @@ function assertNumeric(where: string, position: string, ref: AnyVar): void {
940
967
  */
941
968
  function findColumnSlotOf(context: ChainContext, column: FindColumn): ClassedField | undefined {
942
969
  const entry = column.entry
970
+ if (entry.kind === "segments") return { field: segmentField(entry), class: undefined }
943
971
  if (entry.kind === "var") {
944
972
  return mintSlotOf(context, entry.over)
945
973
  }
@@ -957,13 +985,21 @@ function findColumnSlotOf(context: ChainContext, column: FindColumn): ClassedFie
957
985
  return { field: agg.over.field, class: undefined }
958
986
  }
959
987
  case "pack":
960
- return { field: agg.over.field, class: undefined }
988
+ return {
989
+ field: { kind: "interval", element: (agg.over.field as IntervalField).element, width: undefined },
990
+ class: undefined
991
+ }
961
992
  }
962
993
  }
963
994
 
964
995
  function validateColumn(context: ChainContext, bound: ReadonlySet<AnyVar>, column: FindColumn): void {
965
996
  const where = `${contextLabel(context)} find ${column.name}`
966
997
  const entry = column.entry
998
+ if (entry.kind === "segments") {
999
+ assertBound(where, bound, entry.left)
1000
+ assertBound(where, bound, entry.right)
1001
+ return
1002
+ }
967
1003
  if (entry.kind === "var") {
968
1004
  assertBound(where, bound, entry.over)
969
1005
  return
@@ -1085,6 +1121,13 @@ function completeRule(context: ChainContext, state: RuleBuildState, rawColumns:
1085
1121
  if (rawColumns.length === 0) {
1086
1122
  throw new AuthoringError({ message: `${label}: a find needs at least one entry` })
1087
1123
  }
1124
+ const aggregates = rawColumns.flatMap((column) => (column.entry.kind === "aggregate" ? [column.entry.agg] : []))
1125
+ const packs = aggregates.filter((agg) => agg.op === "pack").length
1126
+ if (packs > 1) throw new AuthoringError({ message: "a query stage can pack one interval column" })
1127
+ if (packs !== 0 && aggregates.length !== packs)
1128
+ throw new AuthoringError({ message: "pack intervals and compute numeric aggregates in separate query stages" })
1129
+ if (rawColumns.some((c) => c.entry.kind === "segments") && aggregates.length !== 0)
1130
+ throw new AuthoringError({ message: "aggregate generated segments in a following query stage" })
1088
1131
  const columns = rawColumns.map(function enrichColumn(column): FindColumn {
1089
1132
  assertDeclarationOrderKey(`${label} find column`, column.name)
1090
1133
  validateColumn(context, state.bound, column)
@@ -1519,19 +1562,8 @@ function renderClosedSlice(closed: AnyClosedRoster | undefined): string {
1519
1562
  return closed === undefined ? "a bare value" : `a ${closed.name} reference`
1520
1563
  }
1521
1564
 
1522
- function headSignature(column: FindColumn): string {
1523
- const entry = column.entry
1524
- if (entry.kind === "var") {
1525
- return `${column.name}:var`
1526
- }
1527
- if (entry.kind === "compute") {
1528
- return `${column.name}:compute`
1529
- }
1530
- const agg = entry.agg
1531
- if (agg.op === "fold") {
1532
- return `${column.name}:${agg.fold}`
1533
- }
1534
- return `${column.name}:${agg.op}`
1565
+ function headOperation(column: FindColumn): HeadOpIr | undefined {
1566
+ return column.entry.kind === "aggregate" ? headOpOf(column.entry.agg) : undefined
1535
1567
  }
1536
1568
 
1537
1569
  function renderParamAnchor(roster: AnyClosedRoster | undefined): string {
@@ -1668,12 +1700,22 @@ function alignedHeadOf(label: string, rules: readonly RuleData[]): readonly Find
1668
1700
  if (first === undefined) {
1669
1701
  throw new AuthoringError({ message: `${label} needs at least one rule` })
1670
1702
  }
1671
- const signature = first.finds.map(headSignature).join(", ")
1703
+ // Bare u64 admits either carrier, but does not make two different
1704
+ // carriers compatible. Retain one original classed slot per column;
1705
+ // checking only the first arm (or its weakened meet) loses that proof.
1706
+ const classed = new Map<number, { readonly slot: ClassedField; readonly rule: number }>()
1672
1707
  rules.forEach(function verifyHead(rule, index) {
1673
- const candidate = rule.finds.map(headSignature).join(", ")
1674
- if (candidate !== signature) {
1708
+ if (
1709
+ rule.finds.length !== first.finds.length ||
1710
+ rule.finds.some((column, position) => {
1711
+ const lead = first.finds[position]
1712
+ return lead === undefined || lead.name !== column.name || headOperation(lead) !== headOperation(column)
1713
+ })
1714
+ ) {
1715
+ const render = (columns: readonly FindColumn[]) =>
1716
+ columns.map((column) => `${column.name}:${headOperation(column) ?? "project"}`).join(", ")
1675
1717
  throw new AuthoringError({
1676
- message: `every rule of ${label} derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`
1718
+ message: `every rule of ${label} derives the same head — rule 0 finds (${render(first.finds)}), rule ${index} finds (${render(rule.finds)})`
1677
1719
  })
1678
1720
  }
1679
1721
  rule.finds.forEach(function verifyClosedSlice(column, position) {
@@ -1686,30 +1728,44 @@ function alignedHeadOf(label: string, rules: readonly RuleData[]): readonly Find
1686
1728
  if (lead === undefined) {
1687
1729
  return
1688
1730
  }
1689
- if (lead.slot !== undefined && column.slot !== undefined && !headFieldJoins(lead.slot, column.slot)) {
1731
+ const prior = classed.get(position) ?? (lead.slot === undefined ? undefined : { slot: lead.slot, rule: 0 })
1732
+ if (prior !== undefined && column.slot !== undefined && !headFieldJoins(prior.slot, column.slot)) {
1690
1733
  throw new AuthoringError({
1691
- message: `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 class-equal slots; same-wire u64 admits a bare side, and the merged head is bare)`
1734
+ message: `every rule of ${label} derives the same head — the head column ${lead.name} is bound at ${renderFieldKind(prior.slot)} in rule ${prior.rule} but at ${renderFieldKind(column.slot)} in rule ${index} (a head column joins class-equal slots; same-wire u64 admits a bare side, and the merged head is bare)`
1692
1735
  })
1693
1736
  }
1737
+ if (column.slot?.class !== undefined && !classed.has(position)) {
1738
+ classed.set(position, { slot: column.slot, rule: index })
1739
+ }
1694
1740
  })
1695
1741
  })
1696
1742
  const merged = first.finds.map(function meetColumn(lead, position) {
1697
1743
  const slot = lead.slot
1698
- if (slot === undefined || slot.class === undefined) {
1744
+ if (slot === undefined) {
1699
1745
  return lead
1700
1746
  }
1701
1747
  const demoted = rules.some(function bareElsewhere(rule) {
1702
1748
  const column = rule.finds[position]
1703
1749
  return column !== undefined && column.slot !== undefined && column.slot.class !== slot.class
1704
1750
  })
1705
- if (!demoted) {
1751
+ const field = slot.field
1752
+ const widened =
1753
+ field.kind === "interval" &&
1754
+ rules.some((rule) => {
1755
+ const other = rule.finds[position]?.slot?.field
1756
+ return other?.kind === "interval" && other.width !== field.width
1757
+ })
1758
+ if (!demoted && !widened) {
1706
1759
  return lead
1707
1760
  }
1708
1761
  return Object.freeze({
1709
1762
  name: lead.name,
1710
1763
  entry: lead.entry,
1711
1764
  closed: lead.closed,
1712
- slot: Object.freeze({ field: slot.field, class: undefined })
1765
+ slot: Object.freeze({
1766
+ field: widened ? Object.freeze({ ...field, width: undefined }) : field,
1767
+ class: demoted ? undefined : slot.class
1768
+ })
1713
1769
  })
1714
1770
  })
1715
1771
  return Object.freeze(merged)
@@ -2186,34 +2242,14 @@ function lowerComputeNode(node: QueryNode, ids: VarIds): ScalarExprIr {
2186
2242
  }
2187
2243
 
2188
2244
  function lowerComputeGrammar(node: QueryNode, ids: VarIds): ScalarExprIr {
2189
- switch (node.kind) {
2190
- case "var":
2191
- return { kind: "var", var: ids.of(node.ref as AnyVar) }
2192
- case "literal":
2193
- return { kind: "literal", value: literalWireOf(node.value) }
2194
- case "negate":
2195
- case "isNaN":
2196
- case "isFinite":
2197
- return { kind: node.kind, expr: lowerComputeGrammar(node.expr, ids) }
2198
- case "cast":
2199
- return { kind: "cast", cast: node.cast, expr: lowerComputeGrammar(node.expr, ids) }
2200
- case "add":
2201
- case "subtract":
2202
- case "multiply":
2203
- case "divide":
2204
- return {
2205
- kind: node.kind,
2206
- left: lowerComputeGrammar(node.left, ids),
2207
- right: lowerComputeGrammar(node.right, ids)
2208
- }
2209
- default:
2210
- throw new AuthoringError({
2211
- message: "query lowering: a query-var tree cannot carry a source-field leaf — use Compute over bound variables"
2212
- })
2213
- }
2245
+ return scalarWire(node, (leaf) => {
2246
+ return ids.of(leaf.ref as AnyVar)
2247
+ })
2214
2248
  }
2215
2249
 
2216
2250
  function lowerFind(entry: FindEntryData, ids: VarIds): FindTermIr {
2251
+ if (entry.kind === "segments")
2252
+ return { kind: entry.kind, op: entry.op, left: ids.of(entry.left), right: ids.of(entry.right) }
2217
2253
  if (entry.kind === "var") {
2218
2254
  return { kind: "var", var: ids.of(entry.over) }
2219
2255
  }
@@ -2244,6 +2280,7 @@ function headOpOf(agg: AggData): HeadOpIr {
2244
2280
 
2245
2281
  function headTermOf(column: FindColumn): HeadTermIr {
2246
2282
  const entry = column.entry
2283
+ if (entry.kind === "segments") return { kind: "compute" }
2247
2284
  if (entry.kind === "var") {
2248
2285
  return { kind: "var" }
2249
2286
  }
@@ -11,6 +11,7 @@ import type {
11
11
  ScalarExprIr,
12
12
  TermIr
13
13
  } from "#native.ts"
14
+ import { roundingMode } from "#scalar.ts"
14
15
  import { arrayValue as array, recordValue, valueDescriptor } from "#values.ts"
15
16
 
16
17
  function fail(context: string, expected: string): never {
@@ -59,6 +60,7 @@ function scalar(context: string, input: unknown, depth = 1): ScalarExprIr {
59
60
  case "literal":
60
61
  recordValue(context, raw, ["kind", "value"])
61
62
  return Object.freeze({ kind: raw.kind, value: valueDescriptor(`${context}.value`, raw.value) })
63
+ case "measure":
62
64
  case "negate":
63
65
  case "isNaN":
64
66
  case "isFinite":
@@ -74,6 +76,15 @@ function scalar(context: string, input: unknown, depth = 1): ScalarExprIr {
74
76
  left: scalar(`${context}.left`, raw.left, depth + 1),
75
77
  right: scalar(`${context}.right`, raw.right, depth + 1)
76
78
  })
79
+ case "mulDiv":
80
+ recordValue(context, raw, ["kind", "a", "b", "divisor", "rounding"])
81
+ return Object.freeze({
82
+ kind: raw.kind,
83
+ a: scalar(`${context}.a`, raw.a, depth + 1),
84
+ b: scalar(`${context}.b`, raw.b, depth + 1),
85
+ divisor: scalar(`${context}.divisor`, raw.divisor, depth + 1),
86
+ rounding: roundingMode(raw.rounding)
87
+ })
77
88
  case "cast": {
78
89
  recordValue(context, raw, ["kind", "cast", "expr"])
79
90
  const cast = raw.cast
@@ -102,6 +113,16 @@ function headTerm(context: string, input: unknown): HeadTermIr {
102
113
 
103
114
  function find(context: string, input: unknown): FindTermIr {
104
115
  const raw = tagged(context, input)
116
+ if (raw.kind === "segments") {
117
+ recordValue(context, raw, ["kind", "op", "left", "right"])
118
+ if (raw.op !== "intersection" && raw.op !== "difference") return fail(context, "unknown segment operator")
119
+ return Object.freeze({
120
+ kind: raw.kind,
121
+ op: raw.op,
122
+ left: ordinal(context, raw.left),
123
+ right: ordinal(context, raw.right)
124
+ })
125
+ }
105
126
  switch (raw.kind) {
106
127
  case "var":
107
128
  recordValue(context, raw, ["kind", "var"])
@@ -210,8 +231,8 @@ function align(context: string, head: readonly HeadTermIr[], rules: readonly Rul
210
231
  if (rule.finds.length !== head.length) fail(`${context}.rules[${index}]`, "finds width does not match head width")
211
232
  for (const [position, find] of rule.finds.entries()) {
212
233
  const term = head[position]
213
- const family = find.kind === "var" || find.kind === "compute" ? find.kind : "aggregate"
214
- if (term?.kind !== family)
234
+ const projects = find.kind === "var" || find.kind === "compute" || find.kind === "segments"
235
+ if (term === undefined || (term.kind === "aggregate") === projects)
215
236
  fail(`${context}.rules[${index}].finds[${position}]`, "find family does not match head")
216
237
  if (term.kind === "aggregate") {
217
238
  const op = find.kind === "aggregate" ? find.op.kind : find.kind
@@ -258,7 +279,10 @@ function parseQueryIr(input: unknown): ParsedQuery {
258
279
  if (rec.length === 0) return fail("parseQueryIr", "rec step is empty")
259
280
  align("rec base", headRec, base)
260
281
  align("rec step", headRec, rec)
261
- if (headRec.some((term) => term.kind !== "var") || [...base, ...rec].some((rule) => rule.negated.length !== 0))
282
+ if (
283
+ headRec.some((term) => term.kind !== "var") ||
284
+ [...base, ...rec].some((rule) => rule.negated.length !== 0 || rule.finds.some((find) => find.kind !== "var"))
285
+ )
262
286
  return fail("query.rec", "recursion requires projection-only heads and no negation")
263
287
  owned = { kind: raw.kind, interiors, head, rules, rec: Object.freeze({ head: headRec, base, rec }) }
264
288
  }
@@ -369,7 +369,14 @@ function fieldAntiJoins(a: ClassedField, b: ClassedField): boolean {
369
369
  * joins cannot inherit provenance a rule never proved.
370
370
  */
371
371
  function headFieldJoins(a: ClassedField, b: ClassedField): boolean {
372
- return fieldJoins(a, b) || u64BareWirePair(a, b)
372
+ return (
373
+ fieldJoins(a, b) ||
374
+ u64BareWirePair(a, b) ||
375
+ (a.class === b.class &&
376
+ a.field.kind === "interval" &&
377
+ b.field.kind === "interval" &&
378
+ a.field.element === b.field.element)
379
+ )
373
380
  }
374
381
 
375
382
  function renderFieldKind(slot: ClassedField): string {
@@ -0,0 +1,61 @@
1
+ import { AuthoringError } from "#errors.ts"
2
+ import type { IntervalElementKind, IntervalField } from "#fields.ts"
3
+ import { type AnyVar, isTerm, term } from "#query/scope.ts"
4
+ import { recordValue } from "#values.ts"
5
+
6
+ type IntervalVar<E extends IntervalElementKind = IntervalElementKind> = AnyVar & { readonly field: IntervalField<E> }
7
+ type SegmentOp = "intersection" | "difference"
8
+
9
+ /** A relational find output, using the native binary segment operator directly. */
10
+ interface Segments<E extends IntervalElementKind = IntervalElementKind> {
11
+ readonly kind: "segments"
12
+ readonly op: SegmentOp
13
+ readonly left: IntervalVar<E>
14
+ readonly right: IntervalVar<E>
15
+ }
16
+
17
+ function segments<E extends IntervalElementKind>(
18
+ op: SegmentOp,
19
+ left: IntervalVar<E>,
20
+ right: IntervalVar<NoInfer<E>>
21
+ ): Segments<E> {
22
+ if (
23
+ !isTerm(left) ||
24
+ left[term] !== "var" ||
25
+ !isTerm(right) ||
26
+ right[term] !== "var" ||
27
+ left.field.kind !== "interval" ||
28
+ right.field.kind !== "interval" ||
29
+ left.field.element !== right.field.element
30
+ )
31
+ throw new AuthoringError({ message: `${op}: expected interval variables of the same element kind` })
32
+ return Object.freeze({ kind: "segments", op, left, right })
33
+ }
34
+
35
+ function intersection<E extends IntervalElementKind>(
36
+ left: IntervalVar<E>,
37
+ right: IntervalVar<NoInfer<E>>
38
+ ): Segments<E> {
39
+ return segments("intersection", left, right)
40
+ }
41
+
42
+ function difference<E extends IntervalElementKind>(left: IntervalVar<E>, right: IntervalVar<NoInfer<E>>): Segments<E> {
43
+ return segments("difference", left, right)
44
+ }
45
+
46
+ function isSegments(input: unknown): input is Segments {
47
+ if (typeof input !== "object" || input === null || !("kind" in input) || input.kind !== "segments") return false
48
+ recordValue("segments", input, ["kind", "op", "left", "right"])
49
+ const value = input as Segments
50
+ if (value.op !== "intersection" && value.op !== "difference")
51
+ throw new AuthoringError({ message: "unknown segment operator" })
52
+ segments(value.op, value.left, value.right)
53
+ return true
54
+ }
55
+
56
+ function segmentField(value: Segments): IntervalField<IntervalElementKind, undefined> {
57
+ return Object.freeze({ kind: "interval", element: value.left.field.element, width: undefined })
58
+ }
59
+
60
+ export type { IntervalVar, SegmentOp, Segments }
61
+ export { difference, intersection, isSegments, segmentField }
@@ -3,6 +3,7 @@ export const runtimeErrorCodes = [
3
3
  "RuntimeAlreadyLive",
4
4
  "ForeignRuntime",
5
5
  "ClosedHandle",
6
+ "HandleBusy",
6
7
  "SpentHandle",
7
8
  "QueueFull",
8
9
  "InvalidArgument",