@bjornpagen/bumbledb 1.2.0 → 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 (72) 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/capacity.d.ts +7 -4
  8. package/dist/capacity.d.ts.map +1 -1
  9. package/dist/capacity.js +3 -1
  10. package/dist/capacity.js.map +1 -1
  11. package/dist/index.d.ts +6 -4
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/law.d.ts +3 -1
  16. package/dist/law.d.ts.map +1 -1
  17. package/dist/law.js.map +1 -1
  18. package/dist/native.d.ts +14 -0
  19. package/dist/native.d.ts.map +1 -1
  20. package/dist/native.js.map +1 -1
  21. package/dist/query/atom.d.ts +2 -1
  22. package/dist/query/atom.d.ts.map +1 -1
  23. package/dist/query/atom.js.map +1 -1
  24. package/dist/query/compute.d.ts +16 -1
  25. package/dist/query/compute.d.ts.map +1 -1
  26. package/dist/query/compute.js +15 -2
  27. package/dist/query/compute.js.map +1 -1
  28. package/dist/query/description.d.ts +10 -2
  29. package/dist/query/description.d.ts.map +1 -1
  30. package/dist/query/description.js +10 -1
  31. package/dist/query/description.js.map +1 -1
  32. package/dist/query/find.d.ts +32 -10
  33. package/dist/query/find.d.ts.map +1 -1
  34. package/dist/query/find.js +1 -1
  35. package/dist/query/find.js.map +1 -1
  36. package/dist/query/lower.d.ts +18 -6
  37. package/dist/query/lower.d.ts.map +1 -1
  38. package/dist/query/lower.js +43 -30
  39. package/dist/query/lower.js.map +1 -1
  40. package/dist/query/parse-ir.d.ts.map +1 -1
  41. package/dist/query/parse-ir.js +24 -1
  42. package/dist/query/parse-ir.js.map +1 -1
  43. package/dist/query/scope.d.ts +21 -12
  44. package/dist/query/scope.d.ts.map +1 -1
  45. package/dist/query/scope.js.map +1 -1
  46. package/dist/query/segments.d.ts +20 -0
  47. package/dist/query/segments.d.ts.map +1 -0
  48. package/dist/query/segments.js +35 -0
  49. package/dist/query/segments.js.map +1 -0
  50. package/dist/scalar.d.ts +30 -8
  51. package/dist/scalar.d.ts.map +1 -1
  52. package/dist/scalar.js +80 -2
  53. package/dist/scalar.js.map +1 -1
  54. package/dist/shape.d.ts +6 -3
  55. package/dist/shape.d.ts.map +1 -1
  56. package/pack-provenance.json +3 -3
  57. package/package.json +4 -4
  58. package/src/alternatives.ts +95 -0
  59. package/src/capacity.ts +7 -4
  60. package/src/index.ts +14 -2
  61. package/src/law.ts +8 -5
  62. package/src/native.ts +14 -0
  63. package/src/query/atom.ts +2 -0
  64. package/src/query/compute.ts +41 -2
  65. package/src/query/description.ts +34 -4
  66. package/src/query/find.ts +47 -23
  67. package/src/query/lower.ts +73 -43
  68. package/src/query/parse-ir.ts +23 -1
  69. package/src/query/scope.ts +32 -15
  70. package/src/query/segments.ts +61 -0
  71. package/src/scalar.ts +140 -7
  72. package/src/shape.ts +9 -3
package/src/native.ts CHANGED
@@ -89,6 +89,14 @@ type HeadOpIr = "sum" | "mean" | "min" | "max" | "count" | "pack"
89
89
  type NumericCastIr = "toF64" | "toF64Exact" | "toI64Exact" | "toU64Exact"
90
90
 
91
91
  type ScalarExprIr =
92
+ | { readonly kind: "measure"; readonly expr: ScalarExprIr }
93
+ | {
94
+ readonly kind: "mulDiv"
95
+ readonly a: ScalarExprIr
96
+ readonly b: ScalarExprIr
97
+ readonly divisor: ScalarExprIr
98
+ readonly rounding: import("#scalar.ts").Rounding
99
+ }
92
100
  | { readonly kind: "var"; readonly var: number }
93
101
  | { readonly kind: "literal"; readonly value: TaggedValue }
94
102
  | { readonly kind: "negate"; readonly expr: ScalarExprIr }
@@ -114,6 +122,12 @@ type FoldOpIr =
114
122
  | { readonly kind: "max" }
115
123
 
116
124
  type FindTermIr =
125
+ | {
126
+ readonly kind: "segments"
127
+ readonly op: import("#query/segments.ts").SegmentOp
128
+ readonly left: number
129
+ readonly right: number
130
+ }
117
131
  | { readonly kind: "var"; readonly var: number }
118
132
  | { readonly kind: "compute"; readonly expr: ScalarExprIr }
119
133
  | { readonly kind: "count" }
package/src/query/atom.ts CHANGED
@@ -16,6 +16,7 @@ import type {
16
16
  ShapeOf
17
17
  } from "#query/scope.ts"
18
18
  import { isTerm, term } from "#query/scope.ts"
19
+ import type { Segments } from "#query/segments.ts"
19
20
  import type { FieldsShape } from "#relation.ts"
20
21
  import type { ScalarKind } from "#scalar.ts"
21
22
  import type { Uuid } from "#uuid.ts"
@@ -75,6 +76,7 @@ type AggData =
75
76
 
76
77
  /** Compute `expr` is the shared `#scalar.ts` query-var node (one grammar). */
77
78
  type FindEntryData =
79
+ | Segments
78
80
  | { readonly kind: "var"; readonly over: AnyVar }
79
81
  | { readonly kind: "aggregate"; readonly agg: AggData }
80
82
  | { readonly kind: "compute"; readonly expr: QueryNode; readonly result: ScalarKind }
@@ -13,7 +13,7 @@ import type { AnyField } from "#fields.ts"
13
13
  import { bool as boolField, f64 as f64Field, i64 as i64Field, rosterOf, u64 as u64Field } from "#fields.ts"
14
14
  import type { AnyVar } from "#query/scope.ts"
15
15
  import { isTerm, term } from "#query/scope.ts"
16
- import type { ScalarKind, ScalarLiteral, ScalarNode } from "#scalar.ts"
16
+ import type { Rounding, ScalarKind, ScalarLiteral, ScalarNode } from "#scalar.ts"
17
17
  import {
18
18
  checkBool,
19
19
  checkF64,
@@ -27,6 +27,8 @@ import {
27
27
  scalarCast,
28
28
  scalarFloatPredicate,
29
29
  scalarLiteral,
30
+ scalarMeasure,
31
+ scalarMulDiv,
30
32
  scalarNegate
31
33
  } from "#scalar.ts"
32
34
 
@@ -61,7 +63,11 @@ type OperandKind<O> =
61
63
  : never
62
64
 
63
65
  function isComputeExpr(value: unknown): value is AnyComputeExpr {
64
- return isScalarNode(value) && value.scope === "query-var" && value.result !== "unresolved"
66
+ return (
67
+ isScalarNode(value) &&
68
+ value.scope === "query-var" &&
69
+ (value.result === "u64" || value.result === "i64" || value.result === "f64" || value.result === "bool")
70
+ )
65
71
  }
66
72
 
67
73
  function varKindOf(where: string, ref: AnyVar): ScalarKind {
@@ -167,6 +173,37 @@ function negate<O extends SignedOperand>(operand: O): ComputeExpr<OperandKind<O>
167
173
  return scalarNegate(where, asQueryNode(where, operand)) as ComputeExpr<OperandKind<O> & ("i64" | "f64")>
168
174
  }
169
175
 
176
+ type IntegerOperand =
177
+ | ComputeExpr<"u64">
178
+ | ComputeExpr<"i64">
179
+ | (AnyVar & { readonly field: { readonly kind: "i64" | "u64" } })
180
+
181
+ function mulDiv<A extends IntegerOperand, B extends IntegerOperand, D extends IntegerOperand>(
182
+ a: A,
183
+ b: B & (OperandKind<A> extends OperandKind<B> ? unknown : never),
184
+ divisor: D & (OperandKind<A> extends OperandKind<D> ? unknown : never),
185
+ rounding: Rounding
186
+ ): ComputeExpr<OperandKind<A>> {
187
+ return scalarMulDiv(
188
+ "Compute.mulDiv",
189
+ asQueryNode("Compute.mulDiv", a),
190
+ asQueryNode("Compute.mulDiv", b),
191
+ asQueryNode("Compute.mulDiv", divisor),
192
+ rounding
193
+ ) as ComputeExpr<OperandKind<A>>
194
+ }
195
+
196
+ function measure<V extends AnyVar & { readonly field: { readonly kind: "interval" } }>(
197
+ interval: V
198
+ ): ComputeExpr<V["field"] extends { readonly element: "f64" } ? "f64" : "u64"> {
199
+ if (!isTerm(interval) || interval[term] !== "var" || interval.field.kind !== "interval")
200
+ throw new AuthoringError({ message: "Compute.measure: expected an interval variable" })
201
+ const kind = { u64: "intervalU64", i64: "intervalI64", f64: "intervalF64" } as const
202
+ return scalarMeasure("Compute.measure", queryVarLeaf(interval, kind[interval.field.element])) as ComputeExpr<
203
+ V["field"] extends { readonly element: "f64" } ? "f64" : "u64"
204
+ >
205
+ }
206
+
170
207
  function toF64(operand: ArithmeticOperand): ComputeExpr<"f64"> {
171
208
  const where = "Compute.toF64"
172
209
  return scalarCast(where, "toF64", "f64", asQueryNode(where, operand)) as ComputeExpr<"f64">
@@ -225,6 +262,8 @@ const Compute = Object.freeze({
225
262
  subtract,
226
263
  multiply,
227
264
  divide,
265
+ mulDiv,
266
+ measure,
228
267
  toF64,
229
268
  toF64Exact,
230
269
  toI64Exact,
@@ -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 { AnyField, Infer } 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,31 +82,54 @@ 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
 
102
+ type ComputeFields = {
103
+ readonly u64: U64Field
104
+ readonly i64: I64Field
105
+ readonly f64: F64Field
106
+ readonly bool: BoolField
107
+ }
108
+
98
109
  /**
99
- * The head signature of a recursive rule's find record as classed mint
100
- * slots (descriptor + law-computed class), keyed by column name — the
101
- * signature an interior join pairs against (`F` is variable-only there).
110
+ * The head signature as classed slots, shared by derived-stage and imported
111
+ * query variables. Projection preserves its carrier; aggregates/computations
112
+ * produce bare values, matching findColumnSlotOf at runtime.
102
113
  */
103
114
  type HeadRecordOf<Classes extends SchemaClasses, F extends FindShape> = {
104
- readonly [K in keyof F]: F[K] extends AnyVar
105
- ? MintSlotOf<Classes, F[K]>
106
- : F[K] extends AnyComputeExpr
107
- ? { readonly field: AnyField; readonly class: undefined }
108
- : 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
109
133
  }
110
134
 
111
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"
@@ -100,6 +101,8 @@ type RowOf<T> = InferredOf<T> extends { readonly row: infer R } ? R : never
100
101
 
101
102
  type HeadShape = Readonly<Record<string, ClassedField>> | undefined
102
103
 
104
+ type HeadOf<T> = InferredOf<T> extends { readonly head: infer H extends HeadShape } ? H : HeadShape
105
+
103
106
  interface RuleValue<Row, P extends ParamsRecord, Head extends HeadShape = undefined> {
104
107
  readonly rule: RuleData
105
108
  readonly [inferred]?: { readonly row: Row; readonly params: P; readonly head: Head }
@@ -145,6 +148,8 @@ interface TermOps {
145
148
  readonly mean: typeof mean
146
149
  readonly min: typeof min
147
150
  readonly max: typeof max
151
+ readonly intersection: typeof intersection
152
+ readonly difference: typeof difference
148
153
  readonly pack: typeof pack
149
154
  }
150
155
 
@@ -225,7 +230,12 @@ interface QueryRuleChain<
225
230
  bindings: B & CheckInteriorBindings<B>
226
231
  ): QueryRuleChain<Rels, P, Classes>
227
232
 
228
- find<const F extends FindShape>(entries: F & CheckFind<F>): RuleValue<RowOfFind<F>, P>
233
+ /** A record of variables is already a valid projection, including v(rel)
234
+ * for a generic relation. Aggregate judgments remain on the general form. */
235
+ find<const F extends Readonly<Record<string, AnyVar>>>(
236
+ entries: F
237
+ ): RuleValue<RowOfFind<F>, P, HeadRecordOf<Classes, F>>
238
+ find<const F extends FindShape>(entries: F & CheckFind<F>): RuleValue<RowOfFind<F>, P, HeadRecordOf<Classes, F>>
229
239
  }
230
240
 
231
241
  interface InteriorRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses>
@@ -292,6 +302,9 @@ interface InteriorRuleChain<
292
302
  * Nonrecursive derived stages emit aggregate/computed outputs too
293
303
  * (C05): only the RECURSIVE head stays projection-only.
294
304
  */
305
+ find<const F extends Readonly<Record<string, AnyVar>>>(
306
+ entries: F
307
+ ): RuleValue<RowOfFind<F>, P, HeadRecordOf<Classes, F>>
295
308
  find<const F extends FindShape>(entries: F & CheckFind<F>): RuleValue<RowOfFind<F>, P, HeadRecordOf<Classes, F>>
296
309
  }
297
310
 
@@ -355,6 +368,9 @@ interface RecRuleChain<
355
368
  name: string,
356
369
  bindings: B & CheckInteriorBindings<B>
357
370
  ): RecRuleChain<Rels, P, Classes>
371
+ find<const F extends Readonly<Record<string, AnyVar>>>(
372
+ entries: F
373
+ ): RuleValue<RowOfFind<F>, P, HeadRecordOf<Classes, F>>
358
374
  find<const F extends FindShape>(entries: F & CheckRecFind<F>): RuleValue<RowOfFind<F>, P, HeadRecordOf<Classes, F>>
359
375
  }
360
376
 
@@ -388,22 +404,23 @@ interface Query<
388
404
  Rels extends SchemaRelations,
389
405
  Row,
390
406
  Params extends ParamsRecord,
391
- Classes extends SchemaClasses = SchemaClasses
407
+ Classes extends SchemaClasses = SchemaClasses,
408
+ Head extends HeadShape = HeadShape
392
409
  > {
393
410
  readonly schema: Schema<Rels, Classes>
394
411
  readonly data: QueryData
395
412
 
396
413
  rule<RV extends AnyRuleValue>(
397
414
  build: (r: QueryRuleScope<Rels, Classes>) => RV
398
- ): Query<Rels, Row | RowOf<RV>, Flatten<Params & ParamsOf<RV>>, Classes>
415
+ ): Query<Rels, Row | RowOf<RV>, Flatten<Params & ParamsOf<RV>>, Classes, Head | HeadOf<RV>>
399
416
 
400
417
  /** A diagnostic name for composition/tracing — never a schema relation. */
401
- named(label: string): Query<Rels, Row, Params, Classes>
418
+ named(label: string): Query<Rels, Row, Params, Classes, Head>
402
419
 
403
420
  interior(name: string, ...builds: never[]): never
404
421
 
405
422
  reach(name: string, arms: never): never
406
- readonly [inferred]?: { readonly row: Row; readonly params: Params }
423
+ readonly [inferred]?: { readonly row: Row; readonly params: Params; readonly head: Head }
407
424
  }
408
425
 
409
426
  interface AnyQuery {
@@ -422,7 +439,7 @@ type QueryStart<
422
439
  > = {
423
440
  rule<RV extends AnyRuleValue>(
424
441
  build: (r: QueryRuleScope<Rels, Classes>) => RV
425
- ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes>
442
+ ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes, HeadOf<RV>>
426
443
  interior<const Builds extends readonly InteriorBuild<Rels, Classes>[]>(
427
444
  name: string,
428
445
  ...builds: Builds
@@ -440,7 +457,7 @@ type QueryReachStart<
440
457
  > = {
441
458
  rule<RV extends AnyRuleValue>(
442
459
  build: (r: QueryRuleScope<Rels, Classes>) => RV
443
- ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes>
460
+ ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes, HeadOf<RV>>
444
461
  }
445
462
 
446
463
  const termOps: TermOps = Object.freeze({
@@ -462,7 +479,9 @@ const termOps: TermOps = Object.freeze({
462
479
  mean,
463
480
  min,
464
481
  max,
465
- pack
482
+ pack,
483
+ intersection,
484
+ difference
466
485
  })
467
486
 
468
487
  interface RuleBuildState {
@@ -851,6 +870,8 @@ function findColumnOf(name: string, entry: unknown): FindColumn {
851
870
  message: `find ${name}: a ${entry[term]} is not projectable — find takes variables, aggregates or Compute expressions`
852
871
  })
853
872
  }
873
+ if (isSegments(entry))
874
+ return Object.freeze({ name, entry: Object.freeze({ ...entry }), closed: undefined, slot: undefined })
854
875
  if (isComputeExpr(entry)) {
855
876
  return Object.freeze({
856
877
  name,
@@ -926,6 +947,7 @@ function assertNumeric(where: string, position: string, ref: AnyVar): void {
926
947
  */
927
948
  function findColumnSlotOf(context: ChainContext, column: FindColumn): ClassedField | undefined {
928
949
  const entry = column.entry
950
+ if (entry.kind === "segments") return { field: segmentField(entry), class: undefined }
929
951
  if (entry.kind === "var") {
930
952
  return mintSlotOf(context, entry.over)
931
953
  }
@@ -943,13 +965,21 @@ function findColumnSlotOf(context: ChainContext, column: FindColumn): ClassedFie
943
965
  return { field: agg.over.field, class: undefined }
944
966
  }
945
967
  case "pack":
946
- 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
+ }
947
972
  }
948
973
  }
949
974
 
950
975
  function validateColumn(context: ChainContext, bound: ReadonlySet<AnyVar>, column: FindColumn): void {
951
976
  const where = `${contextLabel(context)} find ${column.name}`
952
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
+ }
953
983
  if (entry.kind === "var") {
954
984
  assertBound(where, bound, entry.over)
955
985
  return
@@ -1071,6 +1101,13 @@ function completeRule(context: ChainContext, state: RuleBuildState, rawColumns:
1071
1101
  if (rawColumns.length === 0) {
1072
1102
  throw new AuthoringError({ message: `${label}: a find needs at least one entry` })
1073
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" })
1074
1111
  const columns = rawColumns.map(function enrichColumn(column): FindColumn {
1075
1112
  assertDeclarationOrderKey(`${label} find column`, column.name)
1076
1113
  validateColumn(context, state.bound, column)
@@ -1507,6 +1544,7 @@ function renderClosedSlice(closed: AnyClosedRoster | undefined): string {
1507
1544
 
1508
1545
  function headSignature(column: FindColumn): string {
1509
1546
  const entry = column.entry
1547
+ if (entry.kind === "segments") return `${column.name}:compute`
1510
1548
  if (entry.kind === "var") {
1511
1549
  return `${column.name}:var`
1512
1550
  }
@@ -1774,13 +1812,19 @@ function makeRawQuery(
1774
1812
  return value
1775
1813
  }
1776
1814
 
1777
- function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
1815
+ function makeQuery<
1816
+ Rels extends SchemaRelations,
1817
+ Row,
1818
+ P extends ParamsRecord,
1819
+ Classes extends SchemaClasses,
1820
+ Head extends HeadShape
1821
+ >(
1778
1822
  theory: Schema<Rels, Classes>,
1779
1823
  interiors: readonly InteriorData[],
1780
1824
  rec: RecData | undefined,
1781
1825
  rules: readonly RuleData[]
1782
- ): Query<Rels, Row, P, Classes> {
1783
- return makeRawQuery(theory, interiors, rec, rules) as unknown as Query<Rels, Row, P, Classes>
1826
+ ): Query<Rels, Row, P, Classes, Head> {
1827
+ return makeRawQuery(theory, interiors, rec, rules) as unknown as Query<Rels, Row, P, Classes, Head>
1784
1828
  }
1785
1829
 
1786
1830
  function collectInterior<Rels extends SchemaRelations, Classes extends SchemaClasses>(
@@ -1899,9 +1943,11 @@ function makeQueryStart<Rels extends SchemaRelations, Classes extends SchemaClas
1899
1943
  },
1900
1944
  rule<RV extends AnyRuleValue>(
1901
1945
  build: (r: QueryRuleScope<Rels, Classes>) => RV
1902
- ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes> {
1946
+ ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes, HeadOf<RV>> {
1903
1947
  const built = build(makeQueryRuleScope<Rels, Classes>(theory, env))
1904
- return makeQuery<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes>(theory, interiors, undefined, [built.rule])
1948
+ return makeQuery<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes, HeadOf<RV>>(theory, interiors, undefined, [
1949
+ built.rule
1950
+ ])
1905
1951
  }
1906
1952
  }
1907
1953
  Object.freeze(start)
@@ -1917,9 +1963,11 @@ function makeQueryReachStart<Rels extends SchemaRelations, Classes extends Schem
1917
1963
  const start = {
1918
1964
  rule<RV extends AnyRuleValue>(
1919
1965
  build: (r: QueryRuleScope<Rels, Classes>) => RV
1920
- ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes> {
1966
+ ): Query<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes, HeadOf<RV>> {
1921
1967
  const built = build(makeQueryRuleScope<Rels, Classes>(theory, env))
1922
- return makeQuery<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes>(theory, interiors, rec, [built.rule])
1968
+ return makeQuery<Rels, RowOf<RV>, Flatten<P & ParamsOf<RV>>, Classes, HeadOf<RV>>(theory, interiors, rec, [
1969
+ built.rule
1970
+ ])
1923
1971
  }
1924
1972
  }
1925
1973
  Object.freeze(start)
@@ -2162,34 +2210,15 @@ function lowerComputeNode(node: QueryNode, ids: VarIds): ScalarExprIr {
2162
2210
  }
2163
2211
 
2164
2212
  function lowerComputeGrammar(node: QueryNode, ids: VarIds): ScalarExprIr {
2165
- switch (node.kind) {
2166
- case "var":
2167
- return { kind: "var", var: ids.of(node.ref as AnyVar) }
2168
- case "literal":
2169
- return { kind: "literal", value: literalWireOf(node.value) }
2170
- case "negate":
2171
- case "isNaN":
2172
- case "isFinite":
2173
- return { kind: node.kind, expr: lowerComputeGrammar(node.expr, ids) }
2174
- case "cast":
2175
- return { kind: "cast", cast: node.cast, expr: lowerComputeGrammar(node.expr, ids) }
2176
- case "add":
2177
- case "subtract":
2178
- case "multiply":
2179
- case "divide":
2180
- return {
2181
- kind: node.kind,
2182
- left: lowerComputeGrammar(node.left, ids),
2183
- right: lowerComputeGrammar(node.right, ids)
2184
- }
2185
- default:
2186
- throw new AuthoringError({
2187
- message: "query lowering: a query-var tree cannot carry a source-field leaf — use Compute over bound variables"
2188
- })
2189
- }
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
+ })
2190
2217
  }
2191
2218
 
2192
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) }
2193
2222
  if (entry.kind === "var") {
2194
2223
  return { kind: "var", var: ids.of(entry.over) }
2195
2224
  }
@@ -2220,6 +2249,7 @@ function headOpOf(agg: AggData): HeadOpIr {
2220
2249
 
2221
2250
  function headTermOf(column: FindColumn): HeadTermIr {
2222
2251
  const entry = column.entry
2252
+ if (entry.kind === "segments") return { kind: "compute" }
2223
2253
  if (entry.kind === "var") {
2224
2254
  return { kind: "var" }
2225
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") {