@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.
- package/COOKBOOK.md +178 -0
- package/README.md +2 -2
- package/dist/alternatives.d.ts +25 -0
- package/dist/alternatives.d.ts.map +1 -0
- package/dist/alternatives.js +59 -0
- package/dist/alternatives.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/native.d.ts +14 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +2 -1
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js.map +1 -1
- package/dist/query/compute.d.ts +16 -1
- package/dist/query/compute.d.ts.map +1 -1
- package/dist/query/compute.js +15 -2
- package/dist/query/compute.js.map +1 -1
- package/dist/query/description.d.ts +10 -2
- package/dist/query/description.d.ts.map +1 -1
- package/dist/query/description.js +10 -1
- package/dist/query/description.js.map +1 -1
- package/dist/query/find.d.ts +13 -6
- package/dist/query/find.d.ts.map +1 -1
- package/dist/query/find.js +1 -1
- package/dist/query/find.js.map +1 -1
- package/dist/query/lower.d.ts +3 -0
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +37 -28
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts.map +1 -1
- package/dist/query/parse-ir.js +24 -1
- package/dist/query/parse-ir.js.map +1 -1
- package/dist/query/segments.d.ts +20 -0
- package/dist/query/segments.d.ts.map +1 -0
- package/dist/query/segments.js +35 -0
- package/dist/query/segments.js.map +1 -0
- package/dist/scalar.d.ts +30 -8
- package/dist/scalar.d.ts.map +1 -1
- package/dist/scalar.js +80 -2
- package/dist/scalar.js.map +1 -1
- package/pack-provenance.json +3 -3
- package/package.json +4 -4
- package/src/alternatives.ts +95 -0
- package/src/index.ts +12 -1
- package/src/native.ts +14 -0
- package/src/query/atom.ts +2 -0
- package/src/query/compute.ts +41 -2
- package/src/query/description.ts +34 -4
- package/src/query/find.ts +37 -26
- package/src/query/lower.ts +35 -29
- package/src/query/parse-ir.ts +23 -1
- package/src/query/segments.ts +61 -0
- package/src/scalar.ts +140 -7
package/src/query/description.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
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> =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
112
|
-
?
|
|
113
|
-
: F[K] extends
|
|
114
|
-
?
|
|
115
|
-
: F[K] extends
|
|
116
|
-
? { readonly field:
|
|
117
|
-
: F[K] extends
|
|
118
|
-
? { readonly field:
|
|
119
|
-
: F[K] extends Agg<
|
|
120
|
-
? { readonly field:
|
|
121
|
-
:
|
|
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 }
|
package/src/query/lower.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
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
|
}
|
package/src/query/parse-ir.ts
CHANGED
|
@@ -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
|
|
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 }
|