@bjornpagen/bumbledb 1.2.1 → 1.3.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 (56) hide show
  1. package/COOKBOOK.md +178 -0
  2. package/README.md +2 -2
  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/index.d.ts +4 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/native.d.ts +14 -0
  12. package/dist/native.d.ts.map +1 -1
  13. package/dist/native.js.map +1 -1
  14. package/dist/query/atom.d.ts +2 -1
  15. package/dist/query/atom.d.ts.map +1 -1
  16. package/dist/query/atom.js.map +1 -1
  17. package/dist/query/compute.d.ts +16 -1
  18. package/dist/query/compute.d.ts.map +1 -1
  19. package/dist/query/compute.js +15 -2
  20. package/dist/query/compute.js.map +1 -1
  21. package/dist/query/description.d.ts +10 -2
  22. package/dist/query/description.d.ts.map +1 -1
  23. package/dist/query/description.js +10 -1
  24. package/dist/query/description.js.map +1 -1
  25. package/dist/query/find.d.ts +13 -6
  26. package/dist/query/find.d.ts.map +1 -1
  27. package/dist/query/find.js +1 -1
  28. package/dist/query/find.js.map +1 -1
  29. package/dist/query/lower.d.ts +3 -0
  30. package/dist/query/lower.d.ts.map +1 -1
  31. package/dist/query/lower.js +37 -28
  32. package/dist/query/lower.js.map +1 -1
  33. package/dist/query/parse-ir.d.ts.map +1 -1
  34. package/dist/query/parse-ir.js +24 -1
  35. package/dist/query/parse-ir.js.map +1 -1
  36. package/dist/query/segments.d.ts +20 -0
  37. package/dist/query/segments.d.ts.map +1 -0
  38. package/dist/query/segments.js +35 -0
  39. package/dist/query/segments.js.map +1 -0
  40. package/dist/scalar.d.ts +30 -8
  41. package/dist/scalar.d.ts.map +1 -1
  42. package/dist/scalar.js +80 -2
  43. package/dist/scalar.js.map +1 -1
  44. package/pack-provenance.json +3 -3
  45. package/package.json +4 -4
  46. package/src/alternatives.ts +95 -0
  47. package/src/index.ts +12 -1
  48. package/src/native.ts +14 -0
  49. package/src/query/atom.ts +2 -0
  50. package/src/query/compute.ts +41 -2
  51. package/src/query/description.ts +34 -4
  52. package/src/query/find.ts +37 -26
  53. package/src/query/lower.ts +35 -29
  54. package/src/query/parse-ir.ts +23 -1
  55. package/src/query/segments.ts +61 -0
  56. package/src/scalar.ts +140 -7
@@ -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")
@@ -259,6 +280,10 @@ function replayRule(
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,
@@ -87,7 +87,8 @@ import {
87
87
  renderFieldKind,
88
88
  term
89
89
  } from "#query/scope.ts"
90
- import { literalWireOf } from "#scalar.ts"
90
+ import { difference, intersection, isSegments, segmentField } from "#query/segments.ts"
91
+ import { scalarWire } from "#scalar.ts"
91
92
  import type { AnySchema, Schema, SchemaRelations } from "#schema.ts"
92
93
  import { schemaDescriptor, schemasAgree } from "#schema.ts"
93
94
  import { arrayValue, recordValue, taggedValueOf } from "#values.ts"
@@ -147,6 +148,8 @@ interface TermOps {
147
148
  readonly mean: typeof mean
148
149
  readonly min: typeof min
149
150
  readonly max: typeof max
151
+ readonly intersection: typeof intersection
152
+ readonly difference: typeof difference
150
153
  readonly pack: typeof pack
151
154
  }
152
155
 
@@ -476,7 +479,9 @@ const termOps: TermOps = Object.freeze({
476
479
  mean,
477
480
  min,
478
481
  max,
479
- pack
482
+ pack,
483
+ intersection,
484
+ difference
480
485
  })
481
486
 
482
487
  interface RuleBuildState {
@@ -865,6 +870,8 @@ function findColumnOf(name: string, entry: unknown): FindColumn {
865
870
  message: `find ${name}: a ${entry[term]} is not projectable — find takes variables, aggregates or Compute expressions`
866
871
  })
867
872
  }
873
+ if (isSegments(entry))
874
+ return Object.freeze({ name, entry: Object.freeze({ ...entry }), closed: undefined, slot: undefined })
868
875
  if (isComputeExpr(entry)) {
869
876
  return Object.freeze({
870
877
  name,
@@ -940,6 +947,7 @@ function assertNumeric(where: string, position: string, ref: AnyVar): void {
940
947
  */
941
948
  function findColumnSlotOf(context: ChainContext, column: FindColumn): ClassedField | undefined {
942
949
  const entry = column.entry
950
+ if (entry.kind === "segments") return { field: segmentField(entry), class: undefined }
943
951
  if (entry.kind === "var") {
944
952
  return mintSlotOf(context, entry.over)
945
953
  }
@@ -957,13 +965,21 @@ function findColumnSlotOf(context: ChainContext, column: FindColumn): ClassedFie
957
965
  return { field: agg.over.field, class: undefined }
958
966
  }
959
967
  case "pack":
960
- return { field: agg.over.field, class: undefined }
968
+ return {
969
+ field: { kind: "interval", element: (agg.over.field as IntervalField).element, width: undefined },
970
+ class: undefined
971
+ }
961
972
  }
962
973
  }
963
974
 
964
975
  function validateColumn(context: ChainContext, bound: ReadonlySet<AnyVar>, column: FindColumn): void {
965
976
  const where = `${contextLabel(context)} find ${column.name}`
966
977
  const entry = column.entry
978
+ if (entry.kind === "segments") {
979
+ assertBound(where, bound, entry.left)
980
+ assertBound(where, bound, entry.right)
981
+ return
982
+ }
967
983
  if (entry.kind === "var") {
968
984
  assertBound(where, bound, entry.over)
969
985
  return
@@ -1085,6 +1101,13 @@ function completeRule(context: ChainContext, state: RuleBuildState, rawColumns:
1085
1101
  if (rawColumns.length === 0) {
1086
1102
  throw new AuthoringError({ message: `${label}: a find needs at least one entry` })
1087
1103
  }
1104
+ const aggregates = rawColumns.flatMap((column) => (column.entry.kind === "aggregate" ? [column.entry.agg] : []))
1105
+ const packs = aggregates.filter((agg) => agg.op === "pack").length
1106
+ if (packs > 1) throw new AuthoringError({ message: "a query stage can pack one interval column" })
1107
+ if (packs !== 0 && aggregates.length !== packs)
1108
+ throw new AuthoringError({ message: "pack intervals and compute numeric aggregates in separate query stages" })
1109
+ if (rawColumns.some((c) => c.entry.kind === "segments") && aggregates.length !== 0)
1110
+ throw new AuthoringError({ message: "aggregate generated segments in a following query stage" })
1088
1111
  const columns = rawColumns.map(function enrichColumn(column): FindColumn {
1089
1112
  assertDeclarationOrderKey(`${label} find column`, column.name)
1090
1113
  validateColumn(context, state.bound, column)
@@ -1521,6 +1544,7 @@ function renderClosedSlice(closed: AnyClosedRoster | undefined): string {
1521
1544
 
1522
1545
  function headSignature(column: FindColumn): string {
1523
1546
  const entry = column.entry
1547
+ if (entry.kind === "segments") return `${column.name}:compute`
1524
1548
  if (entry.kind === "var") {
1525
1549
  return `${column.name}:var`
1526
1550
  }
@@ -2186,34 +2210,15 @@ function lowerComputeNode(node: QueryNode, ids: VarIds): ScalarExprIr {
2186
2210
  }
2187
2211
 
2188
2212
  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
- }
2213
+ return scalarWire(node, (leaf) => {
2214
+ if (leaf.kind !== "var") throw new AuthoringError({ message: "query expressions require bound variables" })
2215
+ return ids.of(leaf.ref as AnyVar)
2216
+ })
2214
2217
  }
2215
2218
 
2216
2219
  function lowerFind(entry: FindEntryData, ids: VarIds): FindTermIr {
2220
+ if (entry.kind === "segments")
2221
+ return { kind: entry.kind, op: entry.op, left: ids.of(entry.left), right: ids.of(entry.right) }
2217
2222
  if (entry.kind === "var") {
2218
2223
  return { kind: "var", var: ids.of(entry.over) }
2219
2224
  }
@@ -2244,6 +2249,7 @@ function headOpOf(agg: AggData): HeadOpIr {
2244
2249
 
2245
2250
  function headTermOf(column: FindColumn): HeadTermIr {
2246
2251
  const entry = column.entry
2252
+ if (entry.kind === "segments") return { kind: "compute" }
2247
2253
  if (entry.kind === "var") {
2248
2254
  return { kind: "var" }
2249
2255
  }
@@ -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,7 +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"
234
+ const plainFamily = find.kind === "var" || find.kind === "compute" ? find.kind : "aggregate"
235
+ const family = find.kind === "segments" ? "compute" : plainFamily
214
236
  if (term?.kind !== family)
215
237
  fail(`${context}.rules[${index}].finds[${position}]`, "find family does not match head")
216
238
  if (term.kind === "aggregate") {
@@ -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 }