@bjornpagen/bumbledb 0.1.0 → 0.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 +1450 -0
- package/README.md +69 -30
- package/dist/closed.d.ts +157 -38
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +232 -47
- package/dist/closed.js.map +1 -1
- package/dist/count.d.ts +64 -17
- package/dist/count.d.ts.map +1 -1
- package/dist/count.js +37 -14
- package/dist/count.js.map +1 -1
- package/dist/db.d.ts +27 -8
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +33 -10
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts +12 -11
- package/dist/exhume.d.ts.map +1 -1
- package/dist/exhume.js +7 -6
- package/dist/exhume.js.map +1 -1
- package/dist/face.d.ts +135 -30
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +44 -19
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +130 -130
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +60 -94
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +29 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -14
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +224 -0
- package/dist/law.d.ts.map +1 -0
- package/dist/law.js +224 -0
- package/dist/law.js.map +1 -0
- package/dist/lower.d.ts +17 -7
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +66 -24
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +36 -21
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +41 -25
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +6 -2
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +2 -2
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +370 -174
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +75 -172
- package/dist/query/atom.js.map +1 -1
- package/dist/query/lower.d.ts +312 -109
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +906 -387
- package/dist/query/lower.js.map +1 -1
- package/dist/query/predicate.d.ts +70 -88
- package/dist/query/predicate.d.ts.map +1 -1
- package/dist/query/predicate.js +118 -72
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/run.d.ts +19 -16
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +31 -24
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +178 -127
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +111 -108
- package/dist/query/scope.js.map +1 -1
- package/dist/query/select.d.ts +102 -80
- package/dist/query/select.d.ts.map +1 -1
- package/dist/query/select.js +39 -34
- package/dist/query/select.js.map +1 -1
- package/dist/relation.d.ts +48 -38
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +24 -19
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts +41 -3
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +34 -9
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +7 -5
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +91 -36
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +42 -12
- package/dist/statements.js.map +1 -1
- package/package.json +4 -3
- package/src/closed.ts +448 -95
- package/src/count.ts +112 -18
- package/src/db.ts +45 -17
- package/src/exhume.ts +12 -11
- package/src/face.ts +211 -41
- package/src/fields.ts +199 -234
- package/src/index.ts +78 -61
- package/src/law.ts +519 -0
- package/src/lower.ts +82 -27
- package/src/marshal.ts +48 -30
- package/src/native.ts +8 -4
- package/src/query/atom.ts +525 -374
- package/src/query/lower.ts +1469 -538
- package/src/query/predicate.ts +208 -161
- package/src/query/run.ts +35 -25
- package/src/query/scope.ts +264 -218
- package/src/query/select.ts +168 -93
- package/src/relation.ts +47 -47
- package/src/schema.ts +69 -18
- package/src/spec.ts +7 -5
- package/src/statements.ts +124 -41
- package/dist/brand.d.ts +0 -59
- package/dist/brand.d.ts.map +0 -1
- package/dist/brand.js +0 -47
- package/dist/brand.js.map +0 -1
- package/src/brand.ts +0 -82
package/src/query/lower.ts
CHANGED
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `query()` and the IR lowering
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
2
|
+
* `query()` and the IR lowering, STRUCTURAL edition. A query is built
|
|
3
|
+
* kysely-shaped — `query(S).rule(r => r.match(Rel, { f: r.var("x") })
|
|
4
|
+
* .where(r.eq(r.var("x"), r.param("p"))).select("x"))` — and is an INERT
|
|
5
|
+
* value: `Query<Rels, Row, Params>` with `Row` inferred from each rule's
|
|
6
|
+
* `.select` and `Params` inferred to be EXACTLY the params the rules use
|
|
7
|
+
* (params are typed BY USE; a param value no rule uses never registers, so
|
|
8
|
+
* every query executes under its own inferred type). Vars are string
|
|
9
|
+
* names, domain-typed by the field they first bind and joined by reuse —
|
|
10
|
+
* the rule builder's environment carries name → field descriptor through
|
|
11
|
+
* the chain, checked structurally at every reuse (`JoinOk`), so the old
|
|
12
|
+
* brand-equal join is now the domain-equal compile error. Lowering is a
|
|
13
|
+
* pure function of the query value down to the bridge's `ProgramIr`
|
|
14
|
+
* (`bumbledb/crates/bumbledb/src/ir.rs`, the bijection target): relations
|
|
15
|
+
* by declaration ordinal (the declaration-order-is-ids law the engine's
|
|
16
|
+
* manifest pins), variables by dense per-rule first-occurrence ids
|
|
17
|
+
* (rule-scoped, exactly as the IR scopes them), params by first-use order
|
|
18
|
+
* across the program walk. Lowering is STABLE — the same query value
|
|
19
|
+
* lowers to deeply-equal IR every time, and two identically-written
|
|
20
|
+
* queries lower identically. Construction validates negation safety and
|
|
21
|
+
* name-boundness (typed, naming the variable — earlier and warmer than
|
|
22
|
+
* the engine's refusal); everything else (strata, types, aggregate
|
|
23
|
+
* rosters, rule caps) is the ENGINE's judge, surfacing its typed errors
|
|
24
|
+
* at prepare. No invented limits: rule and predicate counts are never
|
|
25
|
+
* pre-checked here.
|
|
19
26
|
*/
|
|
20
27
|
|
|
21
28
|
import * as errors from "@superbuilders/errors"
|
|
22
|
-
import {
|
|
23
|
-
import { assertDeclarationOrderKey
|
|
29
|
+
import type { AnyField } from "#fields.ts"
|
|
30
|
+
import { assertDeclarationOrderKey } from "#fields.ts"
|
|
31
|
+
import type { ClassRecordOf, SchemaClasses } from "#law.ts"
|
|
24
32
|
import type {
|
|
25
33
|
AtomIr,
|
|
26
34
|
ComparisonIr,
|
|
@@ -34,298 +42,1249 @@ import type {
|
|
|
34
42
|
TaggedValue,
|
|
35
43
|
TermIr
|
|
36
44
|
} from "#native.ts"
|
|
37
|
-
|
|
38
45
|
import type {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
AggData,
|
|
47
|
+
AnyCond,
|
|
48
|
+
AtomData,
|
|
49
|
+
BindEnv,
|
|
42
50
|
BindingEntry,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
BindParamsShape,
|
|
52
|
+
CheckBindings,
|
|
53
|
+
CheckCond,
|
|
54
|
+
CmpData,
|
|
55
|
+
CmpKind,
|
|
56
|
+
CmpTermData,
|
|
57
|
+
CondData,
|
|
58
|
+
CondParamsShape,
|
|
59
|
+
MaskData,
|
|
60
|
+
MatchFields,
|
|
61
|
+
MatchOwner,
|
|
62
|
+
MatchShape,
|
|
63
|
+
ParamUse,
|
|
64
|
+
RecData,
|
|
65
|
+
RuleData,
|
|
66
|
+
RuleItem,
|
|
67
|
+
SelectColumn,
|
|
68
|
+
SelectEntryData,
|
|
69
|
+
TreeData
|
|
47
70
|
} from "#query/atom.ts"
|
|
48
|
-
|
|
49
|
-
import type {
|
|
50
|
-
ColumnValues,
|
|
51
|
-
Predicate,
|
|
52
|
-
PredicateColumnsInput,
|
|
53
|
-
PredicateData,
|
|
54
|
-
PredicateParams,
|
|
55
|
-
PredicateRuleInput,
|
|
56
|
-
PredicateSelf
|
|
57
|
-
} from "#query/predicate.ts"
|
|
58
|
-
import { makePredicate } from "#query/predicate.ts"
|
|
71
|
+
import { allen, and, covers, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts"
|
|
59
72
|
import type {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
ClassedField,
|
|
74
|
+
EnvShape,
|
|
75
|
+
Flatten,
|
|
76
|
+
InferredOf,
|
|
77
|
+
JoinOk,
|
|
78
|
+
ParamEntry,
|
|
66
79
|
ParamsRecord,
|
|
67
|
-
ParamsShape,
|
|
68
|
-
QueryRegistry,
|
|
69
80
|
Var
|
|
70
81
|
} from "#query/scope.ts"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
import {
|
|
83
|
+
fieldJoins,
|
|
84
|
+
inferred,
|
|
85
|
+
isTerm,
|
|
86
|
+
makeDuration,
|
|
87
|
+
makeMaskParam,
|
|
88
|
+
makeParam,
|
|
89
|
+
makeSetParam,
|
|
90
|
+
makeVar,
|
|
91
|
+
makeVars,
|
|
92
|
+
renderFieldKind,
|
|
93
|
+
term
|
|
94
|
+
} from "#query/scope.ts"
|
|
95
|
+
import type { CheckNameSelect, CheckSelect, RowOfNameSelect, RowOfSelect, SelectEntry } from "#query/select.ts"
|
|
96
|
+
import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/select.ts"
|
|
97
|
+
import type { FieldsShape, RelationField } from "#relation.ts"
|
|
75
98
|
import type { AnySchema, Schema, SchemaRelations } from "#schema.ts"
|
|
76
99
|
|
|
77
100
|
/**
|
|
78
|
-
* The
|
|
79
|
-
*
|
|
101
|
+
* The matchable members of a schema's record — ordinary relations AND
|
|
102
|
+
* closed vocabularies (ψ query atoms: a closed atom is an ordinary EDB
|
|
103
|
+
* atom over the sealed extension; the ENGINE decides whether it folds to a
|
|
104
|
+
* plan-constant member set or joins the L1-resident virtual image — the
|
|
105
|
+
* SDK lowers pass-through and never knows which).
|
|
80
106
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
type QueryRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], MatchOwner>
|
|
108
|
+
|
|
109
|
+
/** The environment after one bindings record: the incoming env plus every var the record binds (as classed slots). */
|
|
110
|
+
type EnvOfMatch<Env extends EnvShape, F extends FieldsShape, CR, B> =
|
|
111
|
+
Flatten<Env & BindEnv<F, CR, B>> extends infer E extends EnvShape ? E : never
|
|
112
|
+
|
|
113
|
+
/** Reads an inferred-params carrier off a rec reference or rule value. */
|
|
114
|
+
type ParamsOf<T> = InferredOf<T> extends { readonly params: infer P extends ParamsRecord } ? P : Record<never, never>
|
|
115
|
+
|
|
116
|
+
/** Reads an inferred-row carrier off a rule value or query. */
|
|
117
|
+
type RowOf<T> = InferredOf<T> extends { readonly row: infer R } ? R : never
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A recursive predicate's HEAD signature as classed slots (descriptor +
|
|
121
|
+
* law-computed class), position for position — carried on the rec
|
|
122
|
+
* reference so an `idb` join can be judged against it; `undefined` on
|
|
123
|
+
* values that carry no head (a plain query rule, or an unthreaded rec
|
|
124
|
+
* handle before its first rule).
|
|
125
|
+
*/
|
|
126
|
+
type HeadShape = readonly ClassedField[] | undefined
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* One finished rule as a plain value: the runtime data plus the inferred
|
|
130
|
+
* row/params carrier (and, for a RECURSIVE rule, the head's positional
|
|
131
|
+
* field descriptors — the signature `idb` joins pair against).
|
|
132
|
+
* `.rule(...)` consumes it; hosts never build one by hand.
|
|
133
|
+
*/
|
|
134
|
+
interface RuleValue<Row, P extends ParamsRecord, Head extends HeadShape = undefined> {
|
|
135
|
+
readonly rule: RuleData
|
|
136
|
+
readonly [inferred]?: { readonly row: Row; readonly params: P; readonly head: Head }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Any finished rule value. */
|
|
140
|
+
type AnyRuleValue = RuleValue<unknown, ParamsRecord, HeadShape>
|
|
141
|
+
|
|
142
|
+
/** The positional head-field tuple of a recursive rule's names-only select. */
|
|
143
|
+
type HeadFieldsOf<Env extends EnvShape, S extends readonly string[]> = {
|
|
144
|
+
readonly [I in keyof S]: Env[S[I] & keyof Env]
|
|
117
145
|
}
|
|
118
146
|
|
|
147
|
+
/** Reads an inferred-head carrier off a rule value or rec reference. */
|
|
148
|
+
type HeadOf<T> = InferredOf<T> extends { readonly head: infer H extends readonly ClassedField[] } ? H : undefined
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A recursive predicate REFERENCE — the shape `idb()` targets carry: the
|
|
152
|
+
* name (type-level identity: a recursive rule's own `idb` accepts only its
|
|
153
|
+
* own name — the self-recursion cut), the runtime data (value identity),
|
|
154
|
+
* the params its attached rules have used so far, and the head signature
|
|
155
|
+
* its FIRST rule sealed (thread the value `.rule(...)` returns into an
|
|
156
|
+
* `idb` and the program's `Params` type stays exact AND the idb join is
|
|
157
|
+
* arity- and domain-checked against the head).
|
|
158
|
+
*/
|
|
159
|
+
interface RecRef<Name extends string, P extends ParamsRecord, Head extends HeadShape = HeadShape> {
|
|
160
|
+
readonly name: Name
|
|
161
|
+
readonly data: RecData
|
|
162
|
+
readonly [inferred]?: { readonly params: P; readonly head: Head }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** One `idb` position's judgment: the var must be bound by a relation atom, class-equal to the head slot when the head is carried. */
|
|
166
|
+
type IdbVarOk<Env extends EnvShape, T, F> =
|
|
167
|
+
T extends Var<infer N extends string>
|
|
168
|
+
? N extends keyof Env
|
|
169
|
+
? F extends ClassedField
|
|
170
|
+
? JoinOk<Env[N], F>
|
|
171
|
+
: true
|
|
172
|
+
: false
|
|
173
|
+
: false
|
|
174
|
+
|
|
119
175
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* the
|
|
123
|
-
*
|
|
176
|
+
* The validated `idb` variable tuple: every var must already be bound by a
|
|
177
|
+
* relation atom of the rule; and when the target carries its head
|
|
178
|
+
* signature (the threaded rec handle), the tuple must match the head's
|
|
179
|
+
* arity and every position must be class-equal to its head slot — the
|
|
180
|
+
* same wall `JoinOk` holds for EDB atoms. An unthreaded handle carries no
|
|
181
|
+
* head; its joins stay boundness-checked here and arity/class-judged at
|
|
182
|
+
* prepare (the engine's law stands behind both tiers).
|
|
124
183
|
*/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
184
|
+
type CheckIdbVars<Env extends EnvShape, V, Head extends HeadShape = undefined> = Head extends readonly ClassedField[]
|
|
185
|
+
? V extends readonly unknown[]
|
|
186
|
+
? V["length"] extends Head["length"]
|
|
187
|
+
? { readonly [I in keyof V]: IdbVarOk<Env, V[I], Head[I & keyof Head]> extends true ? V[I] : never }
|
|
188
|
+
: { readonly [I in keyof V]: never }
|
|
189
|
+
: never
|
|
190
|
+
: { readonly [I in keyof V]: IdbVarOk<Env, V[I], undefined> extends true ? V[I] : never }
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The term/predicate/aggregate constructor vocabulary every rule builder
|
|
194
|
+
* carries — pure value builders, environment-free: the chain's `.where`,
|
|
195
|
+
* `.match`, and `.select` seams judge their output against the rule
|
|
196
|
+
* environment.
|
|
197
|
+
*/
|
|
198
|
+
interface TermOps {
|
|
199
|
+
/** Declares/names one variable: typed by the field it first binds; reuse joins. */
|
|
200
|
+
readonly var: typeof makeVar
|
|
201
|
+
/** Mints several variables at once — `const { service, w } = r.vars("service", "w")`: each name typed exactly; duplicates refuse. */
|
|
202
|
+
readonly vars: typeof makeVars
|
|
203
|
+
/** Names one scalar parameter: typed by its use; the key of the execute params object. */
|
|
204
|
+
readonly param: typeof makeParam
|
|
205
|
+
/** Names one ∈-set parameter (the IR's `ParamSet`): bound to a readonly array at execution. */
|
|
206
|
+
readonly inSet: typeof makeSetParam
|
|
207
|
+
/** Names one Allen-mask parameter (`MaskTerm::Param`): a bind-time 13-bit mask number. */
|
|
208
|
+
readonly maskParam: typeof makeMaskParam
|
|
209
|
+
/** The measure of an interval-typed variable: `|[s, e)| = e − s`, u64. */
|
|
210
|
+
readonly duration: typeof makeDuration
|
|
211
|
+
readonly eq: typeof eq
|
|
212
|
+
readonly ne: typeof ne
|
|
213
|
+
readonly lt: typeof lt
|
|
214
|
+
readonly le: typeof le
|
|
215
|
+
readonly gt: typeof gt
|
|
216
|
+
readonly ge: typeof ge
|
|
217
|
+
readonly pointIn: typeof pointIn
|
|
218
|
+
readonly covers: typeof covers
|
|
219
|
+
readonly allen: typeof allen
|
|
220
|
+
readonly and: typeof and
|
|
221
|
+
readonly or: typeof or
|
|
222
|
+
readonly not: typeof not
|
|
223
|
+
readonly count: typeof count
|
|
224
|
+
readonly countDistinct: typeof countDistinct
|
|
225
|
+
readonly sum: typeof sum
|
|
226
|
+
readonly min: typeof min
|
|
227
|
+
readonly max: typeof max
|
|
228
|
+
readonly argMax: typeof argMax
|
|
229
|
+
readonly argMin: typeof argMin
|
|
230
|
+
readonly pack: typeof pack
|
|
128
231
|
}
|
|
129
232
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
233
|
+
/** The rule builder a `query(S).rule(...)` callback receives: the ops plus the first atom (`Classes` — the schema type's class map, the join judge's authority). */
|
|
234
|
+
interface QueryRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> extends TermOps {
|
|
235
|
+
/** The first EDB atom of the rule: fields bind vars, params, ∈-sets, or bare literals; absence is the wildcard (same-named vars within the record join class-equal). */
|
|
236
|
+
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
237
|
+
relation: R,
|
|
238
|
+
bindings: B & CheckBindings<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
239
|
+
): QueryRuleChain<
|
|
240
|
+
Rels,
|
|
241
|
+
EnvOfMatch<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
242
|
+
BindParamsShape<MatchFields<R>, B>,
|
|
243
|
+
Classes
|
|
244
|
+
>
|
|
245
|
+
}
|
|
135
246
|
|
|
136
|
-
/**
|
|
137
|
-
interface
|
|
138
|
-
|
|
139
|
-
|
|
247
|
+
/** The chain of a plain query rule: more atoms, residual predicates, then the head. */
|
|
248
|
+
interface QueryRuleChain<
|
|
249
|
+
Rels extends SchemaRelations,
|
|
250
|
+
Env extends EnvShape,
|
|
251
|
+
P extends ParamsRecord,
|
|
252
|
+
Classes extends SchemaClasses = SchemaClasses
|
|
253
|
+
> {
|
|
254
|
+
/** One more positive EDB atom — var reuse joins, class-equal by the environment check. */
|
|
255
|
+
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
256
|
+
relation: R,
|
|
257
|
+
bindings: B & CheckBindings<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
258
|
+
): QueryRuleChain<
|
|
259
|
+
Rels,
|
|
260
|
+
EnvOfMatch<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
261
|
+
Flatten<P & BindParamsShape<MatchFields<R>, B>>,
|
|
262
|
+
Classes
|
|
263
|
+
>
|
|
264
|
+
/** One residual predicate: a comparison, an `and`/`or` tree, or a negated atom (`r.not`). */
|
|
265
|
+
where<const C extends AnyCond>(
|
|
266
|
+
cond: CheckCond<Env, Classes, C> & C
|
|
267
|
+
): QueryRuleChain<Rels, Env, Flatten<P & CondParamsShape<Env, C>>, Classes>
|
|
268
|
+
/** The head projection: var names, the measure, and aggregates; written order = answer column order. */
|
|
269
|
+
select<const S extends readonly SelectEntry[]>(...entries: CheckSelect<Env, S> & S): RuleValue<RowOfSelect<Env, S>, P>
|
|
140
270
|
}
|
|
141
271
|
|
|
142
|
-
/**
|
|
272
|
+
/** The rule builder an OUTPUT rule of a `program()` receives: a query rule plus finished-stratum `idb` atoms. */
|
|
273
|
+
interface OutputRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> extends TermOps {
|
|
274
|
+
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
275
|
+
relation: R,
|
|
276
|
+
bindings: B & CheckBindings<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
277
|
+
): OutputRuleChain<
|
|
278
|
+
Rels,
|
|
279
|
+
EnvOfMatch<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
280
|
+
BindParamsShape<MatchFields<R>, B>,
|
|
281
|
+
Classes
|
|
282
|
+
>
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The chain of an output rule: atoms, predicates, `idb` joins over the program's recs, then the head. */
|
|
286
|
+
interface OutputRuleChain<
|
|
287
|
+
Rels extends SchemaRelations,
|
|
288
|
+
Env extends EnvShape,
|
|
289
|
+
P extends ParamsRecord,
|
|
290
|
+
Classes extends SchemaClasses = SchemaClasses
|
|
291
|
+
> {
|
|
292
|
+
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
293
|
+
relation: R,
|
|
294
|
+
bindings: B & CheckBindings<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
295
|
+
): OutputRuleChain<
|
|
296
|
+
Rels,
|
|
297
|
+
EnvOfMatch<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
298
|
+
Flatten<P & BindParamsShape<MatchFields<R>, B>>,
|
|
299
|
+
Classes
|
|
300
|
+
>
|
|
301
|
+
where<const C extends AnyCond>(
|
|
302
|
+
cond: CheckCond<Env, Classes, C> & C
|
|
303
|
+
): OutputRuleChain<Rels, Env, Flatten<P & CondParamsShape<Env, C>>, Classes>
|
|
304
|
+
/**
|
|
305
|
+
* One `idb` atom over a FINISHED stratum (any rec of this program): a
|
|
306
|
+
* positional join against the rec's head. An idb atom is a join
|
|
307
|
+
* position — every variable must already be bound by a relation atom of
|
|
308
|
+
* the rule (the theory's own domain relation; the rec's answers are
|
|
309
|
+
* theory values, so the join is identity). Threading the rec value the
|
|
310
|
+
* last `.rule(...)` returned carries its rules' params into `Params`
|
|
311
|
+
* AND its head signature, so the join is arity- and class-checked
|
|
312
|
+
* against the head at compile time.
|
|
313
|
+
*/
|
|
314
|
+
idb<Target extends RecRef<string, ParamsRecord>, const V extends readonly Var<string>[]>(
|
|
315
|
+
target: Target,
|
|
316
|
+
...vars: CheckIdbVars<Env, V, HeadOf<Target>> & V
|
|
317
|
+
): OutputRuleChain<Rels, Env, Flatten<P & ParamsOf<Target>>, Classes>
|
|
318
|
+
select<const S extends readonly SelectEntry[]>(...entries: CheckSelect<Env, S> & S): RuleValue<RowOfSelect<Env, S>, P>
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The rule builder a RECURSIVE rule (`rec.rule(...)`) receives. */
|
|
322
|
+
interface RecRuleScope<Rels extends SchemaRelations, Self extends string, Classes extends SchemaClasses = SchemaClasses>
|
|
323
|
+
extends TermOps {
|
|
324
|
+
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
325
|
+
relation: R,
|
|
326
|
+
bindings: B & CheckBindings<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
327
|
+
): RecRuleChain<
|
|
328
|
+
Rels,
|
|
329
|
+
Self,
|
|
330
|
+
EnvOfMatch<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
331
|
+
BindParamsShape<MatchFields<R>, B>,
|
|
332
|
+
Classes
|
|
333
|
+
>
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The chain of a recursive rule. Its `idb` accepts ONLY the rec itself —
|
|
338
|
+
* the self-recursion cut as a type-level boundary (mutual recursion is
|
|
339
|
+
* unwritable; a finished lower stratum is folded by the OUTPUT rules) —
|
|
340
|
+
* and its `select` takes bound variable NAMES only: aggregates and the
|
|
341
|
+
* measure are unrepresentable in a recursive head (the strata judge's
|
|
342
|
+
* `AggregationThroughCycle`/`MeasureInRecursiveHead`, made unwritable).
|
|
343
|
+
*/
|
|
344
|
+
interface RecRuleChain<
|
|
345
|
+
Rels extends SchemaRelations,
|
|
346
|
+
Self extends string,
|
|
347
|
+
Env extends EnvShape,
|
|
348
|
+
P extends ParamsRecord,
|
|
349
|
+
Classes extends SchemaClasses = SchemaClasses
|
|
350
|
+
> {
|
|
351
|
+
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
352
|
+
relation: R,
|
|
353
|
+
bindings: B & CheckBindings<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
354
|
+
): RecRuleChain<
|
|
355
|
+
Rels,
|
|
356
|
+
Self,
|
|
357
|
+
EnvOfMatch<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
358
|
+
Flatten<P & BindParamsShape<MatchFields<R>, B>>,
|
|
359
|
+
Classes
|
|
360
|
+
>
|
|
361
|
+
where<const C extends AnyCond>(
|
|
362
|
+
cond: CheckCond<Env, Classes, C> & C
|
|
363
|
+
): RecRuleChain<Rels, Self, Env, Flatten<P & CondParamsShape<Env, C>>, Classes>
|
|
364
|
+
/** The self-recursive atom: `idb(self, ...boundVars)` — only this rec's own reference is accepted (threaded, its head arity- and class-checks the join). */
|
|
365
|
+
idb<Target extends RecRef<Self, ParamsRecord>, const V extends readonly Var<string>[]>(
|
|
366
|
+
target: Target,
|
|
367
|
+
...vars: CheckIdbVars<Env, V, HeadOf<Target>> & V
|
|
368
|
+
): RecRuleChain<Rels, Self, Env, P, Classes>
|
|
369
|
+
/** The recursive head: bound variable names only (the creation quarantine, restated for fixpoint topology); the value carries the head's classed slots for `idb` pairing. */
|
|
370
|
+
select<const S extends readonly string[]>(
|
|
371
|
+
...names: CheckNameSelect<Env, S> & S
|
|
372
|
+
): RuleValue<RowOfNameSelect<Env, S>, P, HeadFieldsOf<Env, S>>
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** A query's runtime description — everything lowering, the wire marshal, and answer decode read. */
|
|
143
376
|
interface QueryData {
|
|
144
|
-
|
|
145
|
-
readonly
|
|
377
|
+
/** The program's recursive predicates in declaration order (empty for a plain query); `PredId` = index. */
|
|
378
|
+
readonly recs: readonly RecData[]
|
|
379
|
+
/** The output rules in written order (multiple rules = set union). */
|
|
380
|
+
readonly rules: readonly RuleData[]
|
|
381
|
+
/** The head columns (every rule derives the same head; written order = answer column order). */
|
|
146
382
|
readonly select: readonly SelectColumn[]
|
|
383
|
+
/** The registered params in first-use order across the program walk (= dense `ParamId`s). */
|
|
384
|
+
readonly params: readonly ParamEntry[]
|
|
147
385
|
}
|
|
148
386
|
|
|
149
387
|
/**
|
|
150
388
|
* An inert query value. `Row` is the inferred answer-row object type;
|
|
151
|
-
* `Params` the inferred execute-params object type
|
|
152
|
-
* `db.prepare(q)`; nothing here touches an
|
|
389
|
+
* `Params` the inferred execute-params object type — exactly the params
|
|
390
|
+
* the rules use. Prepare with `db.prepare(q)`; nothing here touches an
|
|
391
|
+
* engine.
|
|
153
392
|
*/
|
|
154
|
-
interface Query<
|
|
155
|
-
|
|
393
|
+
interface Query<
|
|
394
|
+
Rels extends SchemaRelations,
|
|
395
|
+
Row,
|
|
396
|
+
Params extends ParamsRecord,
|
|
397
|
+
Classes extends SchemaClasses = SchemaClasses
|
|
398
|
+
> {
|
|
399
|
+
readonly schema: Schema<Rels, Classes>
|
|
156
400
|
readonly data: QueryData
|
|
157
|
-
|
|
401
|
+
/** One more rule — the query's answers are the SET UNION of its rules' answers; every rule derives the same head. */
|
|
402
|
+
rule<RV extends AnyRuleValue>(
|
|
403
|
+
build: (r: QueryRuleScope<Rels, Classes>) => RV
|
|
404
|
+
): Query<Rels, Row | RowOf<RV>, Flatten<Params & ParamsOf<RV>>, Classes>
|
|
405
|
+
readonly [inferred]?: { readonly row: Row; readonly params: Params }
|
|
158
406
|
}
|
|
159
407
|
|
|
160
|
-
/**
|
|
161
|
-
|
|
408
|
+
/**
|
|
409
|
+
* Any query value as lowering and the runtime consume it: the theory it
|
|
410
|
+
* was built against and its runtime description — every `Query` (typed or
|
|
411
|
+
* program-built) carries exactly this.
|
|
412
|
+
*/
|
|
413
|
+
interface AnyQuery {
|
|
414
|
+
readonly schema: AnySchema
|
|
415
|
+
readonly data: QueryData
|
|
416
|
+
}
|
|
162
417
|
|
|
163
418
|
/** Extracts a query value's inferred answer-row type. */
|
|
164
|
-
type QueryRow<Q extends AnyQuery> =
|
|
419
|
+
type QueryRow<Q extends AnyQuery> = RowOf<Q>
|
|
165
420
|
|
|
166
421
|
/** Extracts a query value's inferred execute-params type. */
|
|
167
|
-
type QueryParams<Q extends AnyQuery> =
|
|
422
|
+
type QueryParams<Q extends AnyQuery> = ParamsOf<Q>
|
|
168
423
|
|
|
169
|
-
/** The
|
|
170
|
-
|
|
424
|
+
/** The entry value of `query(S)`: the first `.rule` mints the query. */
|
|
425
|
+
interface QueryStart<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> {
|
|
426
|
+
rule<RV extends AnyRuleValue>(
|
|
427
|
+
build: (r: QueryRuleScope<Rels, Classes>) => RV
|
|
428
|
+
): Query<Rels, RowOf<RV>, ParamsOf<RV>, Classes>
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** The frozen constructor vocabulary every rule builder spreads. */
|
|
432
|
+
const termOps: TermOps = Object.freeze({
|
|
433
|
+
var: makeVar,
|
|
434
|
+
vars: makeVars,
|
|
435
|
+
param: makeParam,
|
|
436
|
+
inSet: makeSetParam,
|
|
437
|
+
maskParam: makeMaskParam,
|
|
438
|
+
duration: makeDuration,
|
|
439
|
+
eq,
|
|
440
|
+
ne,
|
|
441
|
+
lt,
|
|
442
|
+
le,
|
|
443
|
+
gt,
|
|
444
|
+
ge,
|
|
445
|
+
pointIn,
|
|
446
|
+
covers,
|
|
447
|
+
allen,
|
|
448
|
+
and,
|
|
449
|
+
or,
|
|
450
|
+
not,
|
|
451
|
+
count,
|
|
452
|
+
countDistinct,
|
|
453
|
+
sum,
|
|
454
|
+
min,
|
|
455
|
+
max,
|
|
456
|
+
argMax,
|
|
457
|
+
argMin,
|
|
458
|
+
pack
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
/** One rule under construction: immutable — every chain step is a fresh state. */
|
|
462
|
+
interface RuleBuildState {
|
|
463
|
+
readonly items: readonly RuleItem[]
|
|
464
|
+
readonly varFields: Readonly<Record<string, ClassedField>>
|
|
465
|
+
readonly paramUses: readonly ParamUse[]
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** The empty rule state. */
|
|
469
|
+
const EMPTY_RULE: RuleBuildState = Object.freeze({
|
|
470
|
+
items: Object.freeze([]),
|
|
471
|
+
varFields: Object.freeze({}),
|
|
472
|
+
paramUses: Object.freeze([])
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
/** One resolved bindings record: the atom entries, the vars it binds (as classed slots), and the params it uses. */
|
|
476
|
+
interface ResolvedBindings {
|
|
477
|
+
readonly atom: AtomData
|
|
478
|
+
readonly vars: ReadonlyArray<{ readonly name: string; readonly slot: ClassedField }>
|
|
479
|
+
readonly uses: readonly ParamUse[]
|
|
480
|
+
}
|
|
171
481
|
|
|
172
482
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
483
|
+
* The ordered matchable fields of an atom owner — the runtime twin of the
|
|
484
|
+
* type tier's `MatchFields`: a relation's declared fields; a closed
|
|
485
|
+
* relation's SEALED shape, the synthetic `id` (the value's own
|
|
486
|
+
* roster-carrying descriptor, by identity) at ordinal 0 and the declared
|
|
487
|
+
* payload columns at declared index + 1 (the sealed shift, mirroring
|
|
488
|
+
* `spec.rs`'s resolver — a `ClosedColumn` is structurally a
|
|
489
|
+
* {@link RelationField}). The lowering golden pins this mapping.
|
|
176
490
|
*/
|
|
177
|
-
function
|
|
178
|
-
|
|
491
|
+
function matchFieldsOf(owner: MatchOwner): readonly RelationField[] {
|
|
492
|
+
if ("axioms" in owner) {
|
|
493
|
+
return [Object.freeze({ name: "id", field: owner.id }), ...owner.data.columns]
|
|
494
|
+
}
|
|
495
|
+
return owner.data.fields
|
|
179
496
|
}
|
|
180
497
|
|
|
181
|
-
/**
|
|
182
|
-
|
|
183
|
-
|
|
498
|
+
/**
|
|
499
|
+
* Resolves a bindings record against an atom owner's matchable fields (a
|
|
500
|
+
* relation's declared fields; a closed relation's sealed id + columns), in
|
|
501
|
+
* the record's written order: terms classify by their runtime tag,
|
|
502
|
+
* everything else is a bare literal (typed by the FIELD at lowering — the
|
|
503
|
+
* membership typing rule included). Every bound field carries its
|
|
504
|
+
* law-computed class, read off the schema value's frozen class map — the
|
|
505
|
+
* runtime twin of the type tier's `SlotAt` lookups.
|
|
506
|
+
*/
|
|
507
|
+
function resolveBindings(
|
|
508
|
+
context: string,
|
|
509
|
+
relation: MatchOwner,
|
|
510
|
+
bindings: Readonly<Record<string, unknown>>,
|
|
511
|
+
classes: SchemaClasses
|
|
512
|
+
): ResolvedBindings {
|
|
513
|
+
const entries: BindingEntry[] = []
|
|
514
|
+
const vars: Array<{ readonly name: string; readonly slot: ClassedField }> = []
|
|
515
|
+
const uses: ParamUse[] = []
|
|
516
|
+
const relationClasses = classes[relation.name]
|
|
517
|
+
const ordered = matchFieldsOf(relation)
|
|
518
|
+
for (const [fieldName, value] of Object.entries(bindings)) {
|
|
519
|
+
if (value === undefined) {
|
|
520
|
+
continue
|
|
521
|
+
}
|
|
522
|
+
const declared = ordered.find(function byName(candidate) {
|
|
523
|
+
return candidate.name === fieldName
|
|
524
|
+
})
|
|
525
|
+
if (declared === undefined) {
|
|
526
|
+
throw errors.new(`${context} has no field ${fieldName}`)
|
|
527
|
+
}
|
|
528
|
+
const fieldClass = relationClasses?.[fieldName]
|
|
529
|
+
let bound: BindingEntry["term"]
|
|
530
|
+
if (isTerm(value)) {
|
|
531
|
+
switch (value[term]) {
|
|
532
|
+
case "var": {
|
|
533
|
+
bound = Object.freeze({ kind: "var" as const, name: value.name })
|
|
534
|
+
vars.push(
|
|
535
|
+
Object.freeze({ name: value.name, slot: Object.freeze({ field: declared.field, class: fieldClass }) })
|
|
536
|
+
)
|
|
537
|
+
break
|
|
538
|
+
}
|
|
539
|
+
case "param": {
|
|
540
|
+
bound = Object.freeze({ kind: "param" as const, name: value.name })
|
|
541
|
+
uses.push(
|
|
542
|
+
Object.freeze({ name: value.name, shape: "value" as const, anchor: declared.field, op: "binding" as const })
|
|
543
|
+
)
|
|
544
|
+
break
|
|
545
|
+
}
|
|
546
|
+
case "setParam": {
|
|
547
|
+
bound = Object.freeze({ kind: "setParam" as const, name: value.name })
|
|
548
|
+
uses.push(
|
|
549
|
+
Object.freeze({ name: value.name, shape: "set" as const, anchor: declared.field, op: "binding" as const })
|
|
550
|
+
)
|
|
551
|
+
break
|
|
552
|
+
}
|
|
553
|
+
case "maskParam":
|
|
554
|
+
throw errors.new(
|
|
555
|
+
`${context}.${fieldName}: an Allen-mask param is not a field-typed value — masks live in allen() conditions only`
|
|
556
|
+
)
|
|
557
|
+
case "duration":
|
|
558
|
+
throw errors.new(
|
|
559
|
+
`${context}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and select entries`
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
} else {
|
|
563
|
+
bound = Object.freeze({ kind: "literal" as const, value })
|
|
564
|
+
}
|
|
565
|
+
entries.push(Object.freeze({ field: fieldName, data: declared.field, class: fieldClass, term: bound }))
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
atom: Object.freeze({ relation, bindings: Object.freeze(entries) }),
|
|
569
|
+
vars,
|
|
570
|
+
uses
|
|
571
|
+
}
|
|
184
572
|
}
|
|
185
573
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
574
|
+
/**
|
|
575
|
+
* Extends a rule state with one positive atom. Vars bind on first
|
|
576
|
+
* occurrence; every LATER occurrence (a later atom's field or a same-record
|
|
577
|
+
* sibling) is a join and must be class-equal — the construction-time twin
|
|
578
|
+
* of the type tier's `JoinOk` (bare pairs only with bare), so the domain
|
|
579
|
+
* wall holds for untyped callers too.
|
|
580
|
+
*/
|
|
581
|
+
function advanceMatch(
|
|
582
|
+
state: RuleBuildState,
|
|
583
|
+
relation: MatchOwner,
|
|
584
|
+
bindings: Readonly<Record<string, unknown>>,
|
|
585
|
+
classes: SchemaClasses
|
|
586
|
+
): RuleBuildState {
|
|
587
|
+
const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings, classes)
|
|
588
|
+
const varFields: Record<string, ClassedField> = { ...state.varFields }
|
|
589
|
+
for (const bound of resolved.vars) {
|
|
590
|
+
const existing = varFields[bound.name]
|
|
591
|
+
if (existing === undefined) {
|
|
592
|
+
varFields[bound.name] = bound.slot
|
|
593
|
+
} else if (!fieldJoins(existing, bound.slot)) {
|
|
190
594
|
throw errors.new(
|
|
191
|
-
`
|
|
595
|
+
`relation ${relation.name}: the variable ${bound.name} joins domain-unequal fields — first bound at ${renderFieldKind(existing)}, reused at ${renderFieldKind(bound.slot)} (a var joins only class-equal slots; bare pairs only with bare)`
|
|
192
596
|
)
|
|
193
597
|
}
|
|
194
|
-
return Object.freeze({ kind: "var" as const, over: value })
|
|
195
598
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
599
|
+
return {
|
|
600
|
+
items: Object.freeze([...state.items, Object.freeze({ kind: "atom" as const, atom: resolved.atom })]),
|
|
601
|
+
varFields: Object.freeze(varFields),
|
|
602
|
+
paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Resolves one comparison side to its runtime term. */
|
|
607
|
+
function cmpTermDataOf(op: string, value: unknown): CmpTermData {
|
|
608
|
+
if (isTerm(value)) {
|
|
609
|
+
switch (value[term]) {
|
|
610
|
+
case "var":
|
|
611
|
+
return Object.freeze({ kind: "var" as const, name: value.name })
|
|
612
|
+
case "param":
|
|
613
|
+
return Object.freeze({ kind: "param" as const, name: value.name })
|
|
614
|
+
case "setParam":
|
|
615
|
+
return Object.freeze({ kind: "setParam" as const, name: value.name })
|
|
616
|
+
case "duration":
|
|
617
|
+
return Object.freeze({ kind: "measure" as const, name: value.name })
|
|
618
|
+
case "maskParam":
|
|
619
|
+
throw errors.new(`${op}: an Allen-mask param is not a comparison term — masks live in allen()'s mask position`)
|
|
199
620
|
}
|
|
200
|
-
|
|
201
|
-
|
|
621
|
+
}
|
|
622
|
+
return Object.freeze({ kind: "literal" as const, value })
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* One comparison side's contribution to the param census: a param/set side
|
|
627
|
+
* anchors to its SIBLING — a bound variable's field descriptor or the
|
|
628
|
+
* measure; an unanchorable use (literal or param sibling) records with no
|
|
629
|
+
* anchor and must be anchored by some other use of the same name.
|
|
630
|
+
*/
|
|
631
|
+
function sideUses(
|
|
632
|
+
op: CmpKind,
|
|
633
|
+
side: CmpTermData,
|
|
634
|
+
sibling: CmpTermData,
|
|
635
|
+
varFields: Readonly<Record<string, ClassedField>>,
|
|
636
|
+
uses: ParamUse[]
|
|
637
|
+
): void {
|
|
638
|
+
if (side.kind !== "param" && side.kind !== "setParam") {
|
|
639
|
+
return
|
|
640
|
+
}
|
|
641
|
+
let anchor: AnyField | "measure" | undefined
|
|
642
|
+
if (sibling.kind === "var") {
|
|
643
|
+
anchor = varFields[sibling.name]?.field
|
|
644
|
+
} else if (sibling.kind === "measure") {
|
|
645
|
+
anchor = "measure"
|
|
646
|
+
} else {
|
|
647
|
+
anchor = undefined
|
|
648
|
+
}
|
|
649
|
+
uses.push(
|
|
650
|
+
Object.freeze({
|
|
651
|
+
name: side.name,
|
|
652
|
+
shape: side.kind === "param" ? ("value" as const) : ("set" as const),
|
|
653
|
+
anchor,
|
|
654
|
+
op
|
|
655
|
+
})
|
|
656
|
+
)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** Lowers one condition VALUE to its runtime data, recording param uses. */
|
|
660
|
+
function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, ClassedField>>, uses: ParamUse[]): CondData {
|
|
661
|
+
if (cond.cond === "cmp") {
|
|
662
|
+
const lhs = cmpTermDataOf(cond.op, cond.lhs)
|
|
663
|
+
const rhs = cmpTermDataOf(cond.op, cond.rhs)
|
|
664
|
+
sideUses(cond.op, lhs, rhs, varFields, uses)
|
|
665
|
+
sideUses(cond.op, rhs, lhs, varFields, uses)
|
|
666
|
+
let mask: MaskData | undefined
|
|
667
|
+
if (cond.op === "allen") {
|
|
668
|
+
const maskValue = cond.mask
|
|
669
|
+
if (typeof maskValue === "number") {
|
|
670
|
+
mask = Object.freeze({ kind: "literal" as const, mask: maskValue })
|
|
671
|
+
} else if (isTerm(maskValue) && maskValue[term] === "maskParam") {
|
|
672
|
+
mask = Object.freeze({ kind: "param" as const, name: maskValue.name })
|
|
673
|
+
uses.push(
|
|
674
|
+
Object.freeze({ name: maskValue.name, shape: "mask" as const, anchor: undefined, op: "allen" as const })
|
|
675
|
+
)
|
|
676
|
+
} else {
|
|
677
|
+
throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam")
|
|
678
|
+
}
|
|
202
679
|
}
|
|
680
|
+
const data: CmpData = Object.freeze({ kind: "cmp" as const, op: cond.op, mask, lhs, rhs })
|
|
681
|
+
return data
|
|
682
|
+
}
|
|
683
|
+
if (cond.cond === "tree") {
|
|
684
|
+
const children = cond.children.map(function lowerChild(child) {
|
|
685
|
+
return condDataOf(child, varFields, uses)
|
|
686
|
+
})
|
|
687
|
+
const data: TreeData = Object.freeze({ kind: "tree" as const, op: cond.op, children: Object.freeze(children) })
|
|
688
|
+
return data
|
|
203
689
|
}
|
|
204
690
|
throw errors.new(
|
|
205
|
-
|
|
691
|
+
"a negated atom is not a condition-tree node — pass not(...) to where() directly, never inside and()/or()"
|
|
206
692
|
)
|
|
207
693
|
}
|
|
208
694
|
|
|
209
|
-
/**
|
|
210
|
-
function
|
|
211
|
-
if (
|
|
212
|
-
|
|
695
|
+
/** Extends a rule state with one `.where` item (a condition or a negated atom). */
|
|
696
|
+
function advanceWhere(state: RuleBuildState, cond: AnyCond, classes: SchemaClasses): RuleBuildState {
|
|
697
|
+
if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
|
|
698
|
+
throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom")
|
|
699
|
+
}
|
|
700
|
+
if (cond.cond === "not") {
|
|
701
|
+
const relation: MatchOwner = cond.relation
|
|
702
|
+
const bindings: Readonly<Record<string, unknown>> = Object.fromEntries(
|
|
703
|
+
Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
|
|
704
|
+
return value !== undefined
|
|
705
|
+
})
|
|
706
|
+
)
|
|
707
|
+
const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings, classes)
|
|
708
|
+
return {
|
|
709
|
+
items: Object.freeze([...state.items, Object.freeze({ kind: "negated" as const, atom: resolved.atom })]),
|
|
710
|
+
varFields: state.varFields,
|
|
711
|
+
paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const uses: ParamUse[] = []
|
|
715
|
+
const data = condDataOf(cond, state.varFields, uses)
|
|
716
|
+
return {
|
|
717
|
+
items: Object.freeze([...state.items, Object.freeze({ kind: "cond" as const, cond: data })]),
|
|
718
|
+
varFields: state.varFields,
|
|
719
|
+
paramUses: Object.freeze([...state.paramUses, ...uses])
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** Extends a rule state with one `idb` atom (vars must be bound — validated at completion). */
|
|
724
|
+
function advanceIdb(state: RuleBuildState, rec: RecData, vars: readonly Var<string>[]): RuleBuildState {
|
|
725
|
+
const names = vars.map(function nameOf(variable) {
|
|
726
|
+
if (!isTerm(variable) || variable[term] !== "var") {
|
|
727
|
+
throw errors.new(`idb ${rec.name}: positions take variables — bind literals and params through where()/match()`)
|
|
728
|
+
}
|
|
729
|
+
return variable.name
|
|
730
|
+
})
|
|
731
|
+
return {
|
|
732
|
+
items: Object.freeze([...state.items, Object.freeze({ kind: "idb" as const, rec, vars: Object.freeze(names) })]),
|
|
733
|
+
varFields: state.varFields,
|
|
734
|
+
paramUses: state.paramUses
|
|
213
735
|
}
|
|
214
|
-
return `predicate ${source.pred.name}`
|
|
215
736
|
}
|
|
216
737
|
|
|
217
|
-
/**
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
738
|
+
/** Narrows a select entry to an aggregate value. */
|
|
739
|
+
function isAggregateEntry(
|
|
740
|
+
value: unknown
|
|
741
|
+
): value is { readonly agg: string; readonly over: unknown; readonly key: unknown } {
|
|
742
|
+
return typeof value === "object" && value !== null && "agg" in value
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** Classifies one select entry into its named answer column. */
|
|
746
|
+
function selectColumnOf(entry: unknown): SelectColumn {
|
|
747
|
+
if (typeof entry === "string") {
|
|
748
|
+
return Object.freeze({ name: entry, entry: Object.freeze({ kind: "var" as const, over: entry }) })
|
|
749
|
+
}
|
|
750
|
+
if (isTerm(entry)) {
|
|
751
|
+
if (entry[term] === "duration") {
|
|
752
|
+
return Object.freeze({ name: entry.name, entry: Object.freeze({ kind: "measure" as const, over: entry.name }) })
|
|
753
|
+
}
|
|
754
|
+
throw errors.new(
|
|
755
|
+
`query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`
|
|
756
|
+
)
|
|
757
|
+
}
|
|
758
|
+
if (isAggregateEntry(entry)) {
|
|
759
|
+
return aggregateColumnOf(entry)
|
|
760
|
+
}
|
|
761
|
+
throw errors.new("query select: not a select entry — select takes variable names, duration(v), or aggregates")
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** Classifies one aggregate select entry. */
|
|
765
|
+
function aggregateColumnOf(entry: {
|
|
766
|
+
readonly agg: string
|
|
767
|
+
readonly over: unknown
|
|
768
|
+
readonly key: unknown
|
|
769
|
+
}): SelectColumn {
|
|
770
|
+
function column(name: string, agg: AggData): SelectColumn {
|
|
771
|
+
return Object.freeze({ name, entry: Object.freeze({ kind: "aggregate" as const, agg: Object.freeze(agg) }) })
|
|
772
|
+
}
|
|
773
|
+
const over = entry.over
|
|
774
|
+
switch (entry.agg) {
|
|
775
|
+
case "count":
|
|
776
|
+
return column("count", { op: "count" })
|
|
777
|
+
case "countDistinct": {
|
|
778
|
+
if (typeof over !== "string") {
|
|
779
|
+
throw errors.new("countDistinct takes a variable name")
|
|
780
|
+
}
|
|
781
|
+
return column(over, { op: "countDistinct", over })
|
|
782
|
+
}
|
|
783
|
+
case "sum":
|
|
784
|
+
case "min":
|
|
785
|
+
case "max": {
|
|
786
|
+
if (typeof over === "string") {
|
|
787
|
+
return column(over, { op: "fold", fold: entry.agg, over })
|
|
788
|
+
}
|
|
789
|
+
if (isTerm(over) && over[term] === "duration") {
|
|
790
|
+
return column(over.name, { op: "fold", fold: entry.agg, over: Object.freeze({ duration: over.name }) })
|
|
791
|
+
}
|
|
792
|
+
throw errors.new(`${entry.agg} takes a variable name or duration(v)`)
|
|
793
|
+
}
|
|
794
|
+
case "argMax":
|
|
795
|
+
case "argMin": {
|
|
796
|
+
if (typeof over !== "string" || typeof entry.key !== "string") {
|
|
797
|
+
throw errors.new(`${entry.agg} takes a carried variable name and an orderable key variable name`)
|
|
798
|
+
}
|
|
799
|
+
return column(over, { op: "arg", direction: entry.agg, over, key: entry.key })
|
|
223
800
|
}
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
801
|
+
case "pack": {
|
|
802
|
+
if (typeof over !== "string") {
|
|
803
|
+
throw errors.new("pack takes a variable name")
|
|
227
804
|
}
|
|
805
|
+
return column(over, { op: "pack", over })
|
|
228
806
|
}
|
|
807
|
+
default:
|
|
808
|
+
throw errors.new(`unknown aggregate ${entry.agg}`)
|
|
229
809
|
}
|
|
230
|
-
return positive
|
|
231
810
|
}
|
|
232
811
|
|
|
233
|
-
/**
|
|
234
|
-
function
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
812
|
+
/** Requires a var name to be bound by a relation atom of the rule. */
|
|
813
|
+
function assertBound(context: string, varFields: Readonly<Record<string, ClassedField>>, name: string): ClassedField {
|
|
814
|
+
const slot = varFields[name]
|
|
815
|
+
if (slot === undefined) {
|
|
816
|
+
throw errors.new(`${context}: the variable ${name} is not bound by a relation atom of the rule`)
|
|
817
|
+
}
|
|
818
|
+
return slot
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** Requires a var name to be bound at an interval field (the measure's and pack's domain). */
|
|
822
|
+
function assertIntervalBound(context: string, varFields: Readonly<Record<string, ClassedField>>, name: string): void {
|
|
823
|
+
const slot = assertBound(context, varFields, name)
|
|
824
|
+
if (slot.field.kind !== "interval") {
|
|
825
|
+
throw errors.new(
|
|
826
|
+
`${context}: ${name} is not interval-typed — the measure is defined over interval-typed variables only`
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Validates one condition's variable references against the rule's bound
|
|
833
|
+
* names — and, for `eq`/`ne` over two variables, holds the class wall: the
|
|
834
|
+
* unification IS a join, so the two slots must be class-equal exactly as a
|
|
835
|
+
* match-reuse join must be (the construction-time twin of the type tier's
|
|
836
|
+
* `EqOk` → `JoinOk`; bare pairs only with bare). The engine cannot backstop
|
|
837
|
+
* this one — the query IR carries no domains — so the wall lives here for
|
|
838
|
+
* untyped callers too.
|
|
839
|
+
*/
|
|
840
|
+
function validateCond(context: string, varFields: Readonly<Record<string, ClassedField>>, cond: CondData): void {
|
|
841
|
+
if (cond.kind === "cmp") {
|
|
842
|
+
for (const side of [cond.lhs, cond.rhs]) {
|
|
843
|
+
if (side.kind === "var") {
|
|
844
|
+
assertBound(context, varFields, side.name)
|
|
845
|
+
}
|
|
846
|
+
if (side.kind === "measure") {
|
|
847
|
+
assertIntervalBound(context, varFields, side.name)
|
|
848
|
+
}
|
|
240
849
|
}
|
|
241
|
-
if (
|
|
242
|
-
|
|
850
|
+
if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
|
|
851
|
+
const lhs = assertBound(context, varFields, cond.lhs.name)
|
|
852
|
+
const rhs = assertBound(context, varFields, cond.rhs.name)
|
|
853
|
+
if (!fieldJoins(lhs, rhs)) {
|
|
854
|
+
throw errors.new(
|
|
855
|
+
`${context}: ${cond.op}(${cond.lhs.name}, ${cond.rhs.name}) unifies domain-unequal fields — ${cond.lhs.name} bound at ${renderFieldKind(lhs)}, ${cond.rhs.name} at ${renderFieldKind(rhs)} (a var joins only class-equal slots; bare pairs only with bare)`
|
|
856
|
+
)
|
|
857
|
+
}
|
|
243
858
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
859
|
+
return
|
|
860
|
+
}
|
|
861
|
+
for (const child of cond.children) {
|
|
862
|
+
validateCond(context, varFields, child)
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/** Validates one select column's variable references. */
|
|
867
|
+
function validateColumn(
|
|
868
|
+
context: string,
|
|
869
|
+
varFields: Readonly<Record<string, ClassedField>>,
|
|
870
|
+
column: SelectColumn
|
|
871
|
+
): void {
|
|
872
|
+
const entry = column.entry
|
|
873
|
+
if (entry.kind === "var") {
|
|
874
|
+
assertBound(`${context} select ${column.name}`, varFields, entry.over)
|
|
875
|
+
return
|
|
876
|
+
}
|
|
877
|
+
if (entry.kind === "measure") {
|
|
878
|
+
assertIntervalBound(`${context} select ${column.name}`, varFields, entry.over)
|
|
879
|
+
return
|
|
880
|
+
}
|
|
881
|
+
const agg = entry.agg
|
|
882
|
+
switch (agg.op) {
|
|
883
|
+
case "count":
|
|
884
|
+
return
|
|
885
|
+
case "countDistinct":
|
|
886
|
+
assertBound(`${context} select ${column.name}`, varFields, agg.over)
|
|
887
|
+
return
|
|
888
|
+
case "fold": {
|
|
889
|
+
if (typeof agg.over === "string") {
|
|
890
|
+
assertBound(`${context} select ${column.name}`, varFields, agg.over)
|
|
891
|
+
return
|
|
892
|
+
}
|
|
893
|
+
assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over.duration)
|
|
894
|
+
return
|
|
249
895
|
}
|
|
896
|
+
case "arg":
|
|
897
|
+
assertBound(`${context} select ${column.name}`, varFields, agg.over)
|
|
898
|
+
assertBound(`${context} select ${column.name}`, varFields, agg.key)
|
|
899
|
+
return
|
|
900
|
+
case "pack":
|
|
901
|
+
assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over)
|
|
902
|
+
return
|
|
250
903
|
}
|
|
251
904
|
}
|
|
252
905
|
|
|
253
906
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* atom
|
|
907
|
+
* Completes one rule: classifies the select record (written order = answer
|
|
908
|
+
* column order, names must be declaration-order-safe keys), and validates
|
|
909
|
+
* boundness — every condition/select/idb variable bound by a relation atom,
|
|
910
|
+
* and every NEGATED atom's variable positively bound (the safety rule: a
|
|
911
|
+
* negated atom binds nothing, only rejects).
|
|
258
912
|
*/
|
|
259
|
-
function
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
913
|
+
function completeRule(context: string, state: RuleBuildState, columns: readonly SelectColumn[]): RuleData {
|
|
914
|
+
if (columns.length === 0) {
|
|
915
|
+
throw errors.new(`${context}: a select needs at least one entry`)
|
|
916
|
+
}
|
|
917
|
+
const seen = new Set<string>()
|
|
918
|
+
for (const column of columns) {
|
|
919
|
+
assertDeclarationOrderKey(`${context} select column`, column.name)
|
|
920
|
+
if (seen.has(column.name)) {
|
|
921
|
+
throw errors.new(`${context}: select names the answer column ${column.name} twice`)
|
|
264
922
|
}
|
|
923
|
+
seen.add(column.name)
|
|
924
|
+
validateColumn(context, state.varFields, column)
|
|
265
925
|
}
|
|
926
|
+
for (const item of state.items) {
|
|
927
|
+
if (item.kind === "negated") {
|
|
928
|
+
for (const binding of item.atom.bindings) {
|
|
929
|
+
if (binding.term.kind === "var") {
|
|
930
|
+
const bound = state.varFields[binding.term.name]
|
|
931
|
+
if (bound === undefined) {
|
|
932
|
+
throw errors.new(
|
|
933
|
+
`${context}: negated ${item.atom.relation.name} atom binds the variable ${binding.term.name} at position ${binding.field}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`
|
|
934
|
+
)
|
|
935
|
+
}
|
|
936
|
+
const negatedSlot: ClassedField = { field: binding.data, class: binding.class }
|
|
937
|
+
if (!fieldJoins(bound, negatedSlot)) {
|
|
938
|
+
throw errors.new(
|
|
939
|
+
`${context}: negated ${item.atom.relation.name} atom reuses the variable ${binding.term.name} at ${binding.field} (${renderFieldKind(negatedSlot)}), but the rule binds it at ${renderFieldKind(bound)} — a var joins only class-equal slots; bare pairs only with bare`
|
|
940
|
+
)
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (item.kind === "idb") {
|
|
946
|
+
const head = item.rec.rules[0]
|
|
947
|
+
item.vars.forEach(function checkIdbVar(name, position) {
|
|
948
|
+
const bound = state.varFields[name]
|
|
949
|
+
if (bound === undefined) {
|
|
950
|
+
throw errors.new(
|
|
951
|
+
`${context}: idb ${item.rec.name} names the variable ${name}, but no relation atom of the rule binds it — an idb atom is a join position; bind the variable through the theory's own relation first`
|
|
952
|
+
)
|
|
953
|
+
}
|
|
954
|
+
const column = head?.select[position]
|
|
955
|
+
if (column === undefined || column.entry.kind !== "var") {
|
|
956
|
+
return
|
|
957
|
+
}
|
|
958
|
+
const headSlot = head?.varFields[column.entry.over]
|
|
959
|
+
if (headSlot !== undefined && !fieldJoins(headSlot, bound)) {
|
|
960
|
+
throw errors.new(
|
|
961
|
+
`${context}: idb ${item.rec.name} joins the variable ${name} (${renderFieldKind(bound)}) at head position ${position} (${column.name}: ${renderFieldKind(headSlot)}) — a var joins only class-equal slots; bare pairs only with bare`
|
|
962
|
+
)
|
|
963
|
+
}
|
|
964
|
+
})
|
|
965
|
+
}
|
|
966
|
+
if (item.kind === "cond") {
|
|
967
|
+
validateCond(context, state.varFields, item.cond)
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return Object.freeze({
|
|
971
|
+
items: state.items,
|
|
972
|
+
select: Object.freeze([...columns]),
|
|
973
|
+
varFields: state.varFields,
|
|
974
|
+
paramUses: state.paramUses
|
|
975
|
+
})
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/** Builds one typed rule value over completed rule data. */
|
|
979
|
+
function makeRuleValue<Row, P extends ParamsRecord>(rule: RuleData): RuleValue<Row, P> {
|
|
980
|
+
return Object.freeze({ rule })
|
|
266
981
|
}
|
|
267
982
|
|
|
268
983
|
/**
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
984
|
+
* The one runtime chain every context shares — non-generic on purpose: the
|
|
985
|
+
* typed chain interfaces (`QueryRuleChain`/`OutputRuleChain`/`RecRuleChain`)
|
|
986
|
+
* apply at the scope factories' boundaries, and the runtime beneath them is
|
|
987
|
+
* one plain value walk. Context gates the two context-bound verbs: `idb`
|
|
988
|
+
* (a program construct — self-only inside a rec, any rec of the program in
|
|
989
|
+
* the output, refused in a plain query) and the recursive `select`
|
|
990
|
+
* (bound variable names only — the creation quarantine).
|
|
274
991
|
*/
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
992
|
+
interface RawChain {
|
|
993
|
+
match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
|
|
994
|
+
where(cond: AnyCond): RawChain
|
|
995
|
+
idb(target: RecRef<string, ParamsRecord>, ...vars: readonly Var<string>[]): RawChain
|
|
996
|
+
select(...entries: readonly SelectEntry[]): RuleValue<never, never>
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/** The runtime rule-builder shape beneath every typed scope. */
|
|
1000
|
+
interface RawScope extends TermOps {
|
|
1001
|
+
match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/** Which rule family a chain builds — gates `idb` and the recursive select — plus the schema's runtime class map (the join judge's authority). */
|
|
1005
|
+
type ChainContext = { readonly classes: SchemaClasses } & (
|
|
1006
|
+
| { readonly kind: "query" }
|
|
1007
|
+
| { readonly kind: "rec"; readonly self: RecData }
|
|
1008
|
+
| { readonly kind: "output"; readonly program: ProgramState }
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
/** The diagnostic label of a chain context. */
|
|
1012
|
+
function contextLabel(context: ChainContext): string {
|
|
1013
|
+
switch (context.kind) {
|
|
1014
|
+
case "query":
|
|
1015
|
+
return "query rule"
|
|
1016
|
+
case "rec":
|
|
1017
|
+
return `rec ${context.self.name} rule`
|
|
1018
|
+
case "output":
|
|
1019
|
+
return "program output rule"
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/** Validates and records one `idb` atom per the context's cut. */
|
|
1024
|
+
function idbAdvance(
|
|
1025
|
+
context: ChainContext,
|
|
1026
|
+
state: RuleBuildState,
|
|
1027
|
+
target: RecRef<string, ParamsRecord>,
|
|
1028
|
+
vars: readonly Var<string>[]
|
|
1029
|
+
): RuleBuildState {
|
|
1030
|
+
if (context.kind === "query") {
|
|
1031
|
+
throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()")
|
|
1032
|
+
}
|
|
1033
|
+
if (context.kind === "rec") {
|
|
1034
|
+
if (target.data !== context.self) {
|
|
1035
|
+
throw errors.new(
|
|
1036
|
+
`rec ${context.self.name}: a recursive rule's idb target must be the rec itself — the self-recursion-only cut (mutual recursion is unwritable; fold a finished stratum in the output rules)`
|
|
1037
|
+
)
|
|
1038
|
+
}
|
|
1039
|
+
return advanceIdb(state, context.self, vars)
|
|
1040
|
+
}
|
|
1041
|
+
if (!context.program.recs.includes(target.data)) {
|
|
1042
|
+
throw errors.new(
|
|
1043
|
+
`idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`
|
|
1044
|
+
)
|
|
1045
|
+
}
|
|
1046
|
+
return advanceIdb(state, target.data, vars)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/** Classifies one select tuple per the context (a recursive head projects bound NAMES only). */
|
|
1050
|
+
function selectColumns(context: ChainContext, entries: readonly SelectEntry[]): SelectColumn[] {
|
|
1051
|
+
return entries.map(function columnOf(entry): SelectColumn {
|
|
1052
|
+
if (context.kind === "rec" && typeof entry !== "string") {
|
|
1053
|
+
throw errors.new(
|
|
1054
|
+
`rec ${context.self.name}: a recursive head projects bound variable NAMES only — aggregates and the measure read finished sets (the strata judge's quarantine, unwritable here)`
|
|
1055
|
+
)
|
|
1056
|
+
}
|
|
1057
|
+
return selectColumnOf(entry)
|
|
1058
|
+
})
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/** Builds one runtime chain (immutably — every step is a fresh chain over fresh state). */
|
|
1062
|
+
function makeRawChain(context: ChainContext, state: RuleBuildState): RawChain {
|
|
1063
|
+
const chain: RawChain = {
|
|
1064
|
+
match(relation, bindings) {
|
|
1065
|
+
return makeRawChain(context, advanceMatch(state, relation, bindings, context.classes))
|
|
286
1066
|
},
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
field: FieldRef<keyof Rels & string, string, V>
|
|
290
|
-
): ParamSet<Name, V> {
|
|
291
|
-
return scopeParamSet(registry, name, field)
|
|
1067
|
+
where(cond) {
|
|
1068
|
+
return makeRawChain(context, advanceWhere(state, cond, context.classes))
|
|
292
1069
|
},
|
|
293
|
-
|
|
294
|
-
return
|
|
1070
|
+
idb(target, ...vars) {
|
|
1071
|
+
return makeRawChain(context, idbAdvance(context, state, target, vars))
|
|
295
1072
|
},
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const Rules extends readonly PredicateRuleInput<ColumnValues<Cols>>[]
|
|
299
|
-
>(
|
|
300
|
-
name: string,
|
|
301
|
-
columns: Cols,
|
|
302
|
-
rules: (self: PredicateSelf<ColumnValues<Cols>>) => Rules
|
|
303
|
-
): Predicate<ColumnValues<Cols>, PredicateParams<Rules>> {
|
|
304
|
-
return makePredicate(registry, name, columns, rules)
|
|
1073
|
+
select(...entries) {
|
|
1074
|
+
return makeRuleValue<never, never>(completeRule(contextLabel(context), state, selectColumns(context, entries)))
|
|
305
1075
|
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
1076
|
+
}
|
|
1077
|
+
Object.freeze(chain)
|
|
1078
|
+
return chain
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/** Builds one runtime rule-builder over a context. */
|
|
1082
|
+
function makeRawScope(context: ChainContext): RawScope {
|
|
1083
|
+
const scope: RawScope = {
|
|
1084
|
+
...termOps,
|
|
1085
|
+
match(relation, bindings) {
|
|
1086
|
+
return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings, context.classes))
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
Object.freeze(scope)
|
|
1090
|
+
return scope
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* The one trusted admission seam of the rule builders (the `relation()`
|
|
1095
|
+
* `refsComplete` pattern): the raw builder is one runtime shape for every
|
|
1096
|
+
* context, and this guard verifies the checkable fact — the builder verbs
|
|
1097
|
+
* exist — before the value is admitted at its TYPED face. The type-level
|
|
1098
|
+
* judgments (domain-equal joins, boundness, the recursion cut) live in the
|
|
1099
|
+
* interfaces themselves; the runtime twin of every one of them is a
|
|
1100
|
+
* construction-time validation in this module.
|
|
1101
|
+
*/
|
|
1102
|
+
function isTypedScope<S>(scope: RawScope): scope is RawScope & S {
|
|
1103
|
+
return typeof scope.match === "function"
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/** Builds one query-rule builder (the typed face of the raw builder). */
|
|
1107
|
+
function makeQueryRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
|
|
1108
|
+
classes: SchemaClasses
|
|
1109
|
+
): QueryRuleScope<Rels, Classes> {
|
|
1110
|
+
const raw = makeRawScope({ kind: "query", classes })
|
|
1111
|
+
if (!isTypedScope<QueryRuleScope<Rels, Classes>>(raw)) {
|
|
1112
|
+
throw errors.new("query rule builder construction incomplete")
|
|
1113
|
+
}
|
|
1114
|
+
return raw
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/** Builds one output-rule builder over a program's recs. */
|
|
1118
|
+
function makeOutputRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
|
|
1119
|
+
program: ProgramState
|
|
1120
|
+
): OutputRuleScope<Rels, Classes> {
|
|
1121
|
+
const raw = makeRawScope({ kind: "output", program, classes: program.classes })
|
|
1122
|
+
if (!isTypedScope<OutputRuleScope<Rels, Classes>>(raw)) {
|
|
1123
|
+
throw errors.new("program output rule builder construction incomplete")
|
|
1124
|
+
}
|
|
1125
|
+
return raw
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/** One program's build-time registry: its recs in declaration order (sealed when the output is declared) and the theory's class map. */
|
|
1129
|
+
interface ProgramState {
|
|
1130
|
+
readonly recs: RecData[]
|
|
1131
|
+
readonly classes: SchemaClasses
|
|
1132
|
+
sealed: boolean
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/** Renders one head column's signature for the rule-alignment check. */
|
|
1136
|
+
function headSignature(column: SelectColumn): string {
|
|
1137
|
+
const entry = column.entry
|
|
1138
|
+
if (entry.kind === "var" || entry.kind === "measure") {
|
|
1139
|
+
return `${column.name}:var`
|
|
1140
|
+
}
|
|
1141
|
+
const agg = entry.agg
|
|
1142
|
+
if (agg.op === "fold") {
|
|
1143
|
+
return `${column.name}:${agg.fold}`
|
|
1144
|
+
}
|
|
1145
|
+
if (agg.op === "arg") {
|
|
1146
|
+
return `${column.name}:${agg.direction}`
|
|
1147
|
+
}
|
|
1148
|
+
return `${column.name}:${agg.op}`
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Folds every rule's param uses (recs in declaration order first, output
|
|
1153
|
+
* rules last — exactly the lowering walk) into the query's registry:
|
|
1154
|
+
* first use mints the dense `ParamId`, the first FIELD-ANCHORED use types
|
|
1155
|
+
* the wire, and one name must keep one shape.
|
|
1156
|
+
*/
|
|
1157
|
+
function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]): readonly ParamEntry[] {
|
|
1158
|
+
const order: string[] = []
|
|
1159
|
+
const byName = new Map<string, { shape: ParamEntry["shape"]; anchor: ParamEntry["anchor"]; op: ParamEntry["op"] }>()
|
|
1160
|
+
function fold(uses: readonly ParamUse[]): void {
|
|
1161
|
+
for (const use of uses) {
|
|
1162
|
+
const existing = byName.get(use.name)
|
|
1163
|
+
if (existing === undefined) {
|
|
1164
|
+
order.push(use.name)
|
|
1165
|
+
byName.set(use.name, { shape: use.shape, anchor: use.anchor, op: use.op })
|
|
1166
|
+
continue
|
|
1167
|
+
}
|
|
1168
|
+
if (existing.shape !== use.shape) {
|
|
1169
|
+
throw errors.new(
|
|
1170
|
+
`query param ${use.name} is used both as a ${existing.shape} param and a ${use.shape} param — one name, one shape`
|
|
1171
|
+
)
|
|
1172
|
+
}
|
|
1173
|
+
if (existing.anchor === undefined && use.anchor !== undefined) {
|
|
1174
|
+
existing.anchor = use.anchor
|
|
1175
|
+
existing.op = use.op
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
for (const rec of recs) {
|
|
1180
|
+
for (const rule of rec.rules) {
|
|
1181
|
+
fold(rule.paramUses)
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
for (const rule of rules) {
|
|
1185
|
+
fold(rule.paramUses)
|
|
1186
|
+
}
|
|
1187
|
+
return Object.freeze(
|
|
1188
|
+
order.map(function entryOf(name): ParamEntry {
|
|
1189
|
+
const entry = byName.get(name)
|
|
1190
|
+
if (entry === undefined) {
|
|
1191
|
+
throw errors.new(`query param ${name} lost its registry entry`)
|
|
1192
|
+
}
|
|
1193
|
+
return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op })
|
|
316
1194
|
})
|
|
1195
|
+
)
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/** The runtime query shape beneath the typed `Query` face. */
|
|
1199
|
+
interface RawQuery {
|
|
1200
|
+
readonly schema: AnySchema
|
|
1201
|
+
readonly data: QueryData
|
|
1202
|
+
rule(build: (r: RawScope) => RuleValue<never, never>): RawQuery
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Assembles the runtime query value over completed rules: every rule must
|
|
1207
|
+
* derive the SAME head (name and aggregate shape, position for position —
|
|
1208
|
+
* the decode labels and the engine's alignment rule agree by
|
|
1209
|
+
* construction), and the param registry folds in program-walk order.
|
|
1210
|
+
*/
|
|
1211
|
+
function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readonly RuleData[]): RawQuery {
|
|
1212
|
+
const first = rules[0]
|
|
1213
|
+
if (first === undefined) {
|
|
1214
|
+
throw errors.new("a query needs at least one rule")
|
|
317
1215
|
}
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
1216
|
+
const signature = first.select.map(headSignature).join(", ")
|
|
1217
|
+
rules.forEach(function verifyHead(rule, index) {
|
|
1218
|
+
const candidate = rule.select.map(headSignature).join(", ")
|
|
1219
|
+
if (candidate !== signature) {
|
|
1220
|
+
throw errors.new(
|
|
1221
|
+
`every rule of a query derives the same head — rule 0 selects (${signature}), rule ${index} selects (${candidate})`
|
|
1222
|
+
)
|
|
1223
|
+
}
|
|
321
1224
|
})
|
|
322
|
-
Object.freeze(
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
data: Object.freeze({ registry, rules: Object.freeze(rules), select: Object.freeze(select) })
|
|
1225
|
+
const data: QueryData = Object.freeze({
|
|
1226
|
+
recs: Object.freeze([...recs]),
|
|
1227
|
+
rules: Object.freeze([...rules]),
|
|
1228
|
+
select: first.select,
|
|
1229
|
+
params: paramRegistryOf(recs, rules)
|
|
328
1230
|
})
|
|
1231
|
+
const value: RawQuery = {
|
|
1232
|
+
schema: theory,
|
|
1233
|
+
data,
|
|
1234
|
+
rule(build) {
|
|
1235
|
+
const built = build(makeRawScope({ kind: "query", classes: theory.classes }))
|
|
1236
|
+
return makeRawQuery(theory, recs, [...rules, built.rule])
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
Object.freeze(value)
|
|
1240
|
+
return value
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* The query values' trusted admission seam (the `refsComplete` pattern):
|
|
1245
|
+
* the checkable fact — the value was assembled over the identical theory —
|
|
1246
|
+
* is verified before the raw value is admitted at its typed face.
|
|
1247
|
+
*/
|
|
1248
|
+
function isQueryValue<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
|
|
1249
|
+
theory: Schema<Rels, Classes>,
|
|
1250
|
+
value: RawQuery
|
|
1251
|
+
): value is RawQuery & Query<Rels, Row, P, Classes> {
|
|
1252
|
+
return value.schema === theory
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/** Assembles one typed query value (rules already completed). */
|
|
1256
|
+
function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
|
|
1257
|
+
theory: Schema<Rels, Classes>,
|
|
1258
|
+
recs: readonly RecData[],
|
|
1259
|
+
rules: readonly RuleData[]
|
|
1260
|
+
): Query<Rels, Row, P, Classes> {
|
|
1261
|
+
const raw = makeRawQuery(theory, recs, rules)
|
|
1262
|
+
if (!isQueryValue<Rels, Row, P, Classes>(theory, raw)) {
|
|
1263
|
+
throw errors.new("query value construction incomplete")
|
|
1264
|
+
}
|
|
1265
|
+
return raw
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule`
|
|
1270
|
+
* adds one conjunctive rule; multiple rules are the set union (answers are
|
|
1271
|
+
* SETS — no order or limit exists anywhere; hosts sort). The schema's
|
|
1272
|
+
* law-computed class map rides into every rule builder — the join walls
|
|
1273
|
+
* compare class names off it, at the type level and at construction alike.
|
|
1274
|
+
*/
|
|
1275
|
+
function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
|
|
1276
|
+
theory: Schema<Rels, Classes>
|
|
1277
|
+
): QueryStart<Rels, Classes> {
|
|
1278
|
+
const start: QueryStart<Rels, Classes> = {
|
|
1279
|
+
rule<RV extends AnyRuleValue>(
|
|
1280
|
+
build: (r: QueryRuleScope<Rels, Classes>) => RV
|
|
1281
|
+
): Query<Rels, RowOf<RV>, ParamsOf<RV>, Classes> {
|
|
1282
|
+
const built = build(makeQueryRuleScope<Rels, Classes>(theory.classes))
|
|
1283
|
+
return makeQuery<Rels, RowOf<RV>, ParamsOf<RV>, Classes>(theory, [], [built.rule])
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
Object.freeze(start)
|
|
1287
|
+
return start
|
|
329
1288
|
}
|
|
330
1289
|
|
|
331
1290
|
/** The typed shape refusal of the literal tagger — a genuine failure, never data. */
|
|
@@ -345,20 +1304,62 @@ function isIntervalShaped(value: unknown): value is { readonly start: bigint; re
|
|
|
345
1304
|
)
|
|
346
1305
|
}
|
|
347
1306
|
|
|
1307
|
+
/**
|
|
1308
|
+
* Tags one closed-reference literal: the bare handle id, verified against
|
|
1309
|
+
* the roster (the belt the type level cannot provide — structural values
|
|
1310
|
+
* make any bigint spellable here) and tagged u64 — queries cross ids,
|
|
1311
|
+
* never handle names.
|
|
1312
|
+
*/
|
|
1313
|
+
function taggedHandleId(
|
|
1314
|
+
context: string,
|
|
1315
|
+
closed: { readonly name: string; readonly handles: readonly string[] },
|
|
1316
|
+
value: unknown
|
|
1317
|
+
): TaggedValue {
|
|
1318
|
+
if (typeof value !== "bigint") {
|
|
1319
|
+
throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, value)
|
|
1320
|
+
}
|
|
1321
|
+
if (closed.handles[Number(value)] === undefined) {
|
|
1322
|
+
throw errors.new(
|
|
1323
|
+
`${context}: closed relation ${closed.name} has no handle with id ${value} (roster holds ${closed.handles.length})`
|
|
1324
|
+
)
|
|
1325
|
+
}
|
|
1326
|
+
return { kind: "u64", value }
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Tags one literal in an interval element domain: a bigint tags as the
|
|
1331
|
+
* element (the membership typing rule's point side), an interval-shaped
|
|
1332
|
+
* value as the interval (value equality).
|
|
1333
|
+
*/
|
|
1334
|
+
function taggedAtElementDomain(context: string, element: "u64" | "i64", value: unknown): TaggedValue {
|
|
1335
|
+
if (typeof value === "bigint") {
|
|
1336
|
+
if (element === "u64") {
|
|
1337
|
+
return { kind: "u64", value }
|
|
1338
|
+
}
|
|
1339
|
+
return { kind: "i64", value }
|
|
1340
|
+
}
|
|
1341
|
+
if (isIntervalShaped(value)) {
|
|
1342
|
+
if (element === "u64") {
|
|
1343
|
+
return { kind: "intervalU64", start: value.start, end: value.end }
|
|
1344
|
+
}
|
|
1345
|
+
return { kind: "intervalI64", start: value.start, end: value.end }
|
|
1346
|
+
}
|
|
1347
|
+
throw literalShapeError(context, "bigint (point) or { start, end } (interval)", value)
|
|
1348
|
+
}
|
|
1349
|
+
|
|
348
1350
|
/**
|
|
349
1351
|
* Tags one host literal at a FIELD position (atom bindings): the field's
|
|
350
|
-
* structural
|
|
1352
|
+
* structural kind directs the tag, never a guess. At an interval field a
|
|
351
1353
|
* bigint literal tags as the ELEMENT type — the IR's membership typing
|
|
352
1354
|
* rule (point membership), an interval-shaped literal as the interval
|
|
353
|
-
* (value equality). A closed-reference literal is its
|
|
354
|
-
* u64 after a roster
|
|
355
|
-
* names).
|
|
1355
|
+
* (value equality). A closed-reference literal is its bare handle id,
|
|
1356
|
+
* tagged u64 after a roster verification.
|
|
356
1357
|
*/
|
|
357
|
-
function taggedLiteral(context: string, field:
|
|
358
|
-
if (
|
|
1358
|
+
function taggedLiteral(context: string, field: AnyField, value: unknown): TaggedValue {
|
|
1359
|
+
if ("closed" in field) {
|
|
359
1360
|
return taggedHandleId(context, field.closed, value)
|
|
360
1361
|
}
|
|
361
|
-
switch (field.
|
|
1362
|
+
switch (field.kind) {
|
|
362
1363
|
case "bool": {
|
|
363
1364
|
if (typeof value !== "boolean") {
|
|
364
1365
|
throw literalShapeError(context, "boolean", value)
|
|
@@ -377,79 +1378,41 @@ function taggedLiteral(context: string, field: FieldData, value: unknown): Tagge
|
|
|
377
1378
|
}
|
|
378
1379
|
return { kind: "i64", value }
|
|
379
1380
|
}
|
|
380
|
-
case "
|
|
1381
|
+
case "str": {
|
|
381
1382
|
if (typeof value !== "string") {
|
|
382
1383
|
throw literalShapeError(context, "string", value)
|
|
383
1384
|
}
|
|
384
1385
|
return { kind: "string", value }
|
|
385
1386
|
}
|
|
386
|
-
case "
|
|
1387
|
+
case "bytes": {
|
|
387
1388
|
if (!(value instanceof Uint8Array)) {
|
|
388
1389
|
throw literalShapeError(context, "Uint8Array", value)
|
|
389
1390
|
}
|
|
390
1391
|
return { kind: "fixedBytes", value }
|
|
391
1392
|
}
|
|
392
1393
|
case "interval":
|
|
393
|
-
return taggedAtElementDomain(context, field.
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Tags one closed-reference literal: the branded id, re-verified against
|
|
399
|
-
* the roster (the belt the type level cannot provide against forged
|
|
400
|
-
* brands) and tagged u64 — queries cross ids, never handle names.
|
|
401
|
-
*/
|
|
402
|
-
function taggedHandleId(context: string, closed: NonNullable<FieldData["closed"]>, value: unknown): TaggedValue {
|
|
403
|
-
if (typeof value !== "bigint") {
|
|
404
|
-
throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, value)
|
|
405
|
-
}
|
|
406
|
-
if (closed.handles[Number(value)] === undefined) {
|
|
407
|
-
throw errors.new(
|
|
408
|
-
`${context}: closed relation ${closed.name} has no handle with id ${value} (roster holds ${closed.handles.length})`
|
|
409
|
-
)
|
|
410
|
-
}
|
|
411
|
-
return { kind: "u64", value }
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Tags one literal in an interval element domain: a bigint tags as the
|
|
416
|
-
* element (the membership typing rule's point side), an interval-shaped
|
|
417
|
-
* value as the interval (value equality).
|
|
418
|
-
*/
|
|
419
|
-
function taggedAtElementDomain(context: string, element: "u64" | "i64", value: unknown): TaggedValue {
|
|
420
|
-
if (typeof value === "bigint") {
|
|
421
|
-
if (element === "u64") {
|
|
422
|
-
return { kind: "u64", value }
|
|
423
|
-
}
|
|
424
|
-
return { kind: "i64", value }
|
|
425
|
-
}
|
|
426
|
-
if (isIntervalShaped(value)) {
|
|
427
|
-
if (element === "u64") {
|
|
428
|
-
return { kind: "intervalU64", start: value.start, end: value.end }
|
|
429
|
-
}
|
|
430
|
-
return { kind: "intervalI64", start: value.start, end: value.end }
|
|
1394
|
+
return taggedAtElementDomain(context, field.element, value)
|
|
431
1395
|
}
|
|
432
|
-
throw literalShapeError(context, "bigint (point) or { start, end } (interval)", value)
|
|
433
1396
|
}
|
|
434
1397
|
|
|
435
1398
|
/**
|
|
436
|
-
* Tags one host literal at a COMPARISON position, where
|
|
437
|
-
*
|
|
438
|
-
* sibling
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
1399
|
+
* Tags one host literal at a COMPARISON or PARAM position, where the
|
|
1400
|
+
* SIBLING anchors the type: a measure sibling is u64, an interval-field
|
|
1401
|
+
* sibling contributes its element domain (so both a point literal in
|
|
1402
|
+
* `covers` and a `span` literal in `allen` tag correctly), a scalar
|
|
1403
|
+
* sibling its own type. At `pointIn` the operand order is interval-left,
|
|
1404
|
+
* point-right (`ir::CmpOp::PointIn`), so an interval-shaped literal
|
|
1405
|
+
* beside a scalar element-typed sibling is the LEGAL lhs of
|
|
1406
|
+
* `covers(span(...), t)` and tags as the interval of the sibling's
|
|
1407
|
+
* element domain; under every other operator an interval shape against a
|
|
1408
|
+
* scalar sibling stays refused (the engine's IllegalComparison — the
|
|
1409
|
+
* bug-hunt fix, preserved op-aware).
|
|
447
1410
|
*/
|
|
448
1411
|
function taggedCmpLiteral(
|
|
449
1412
|
context: string,
|
|
450
|
-
sibling:
|
|
1413
|
+
sibling: AnyField | "measure",
|
|
451
1414
|
value: unknown,
|
|
452
|
-
op:
|
|
1415
|
+
op: CmpKind | "binding"
|
|
453
1416
|
): TaggedValue {
|
|
454
1417
|
if (sibling === "measure") {
|
|
455
1418
|
if (typeof value !== "bigint") {
|
|
@@ -457,11 +1420,16 @@ function taggedCmpLiteral(
|
|
|
457
1420
|
}
|
|
458
1421
|
return { kind: "u64", value }
|
|
459
1422
|
}
|
|
460
|
-
if (sibling.
|
|
461
|
-
return taggedAtElementDomain(context, sibling.
|
|
1423
|
+
if (!("closed" in sibling) && sibling.kind === "interval") {
|
|
1424
|
+
return taggedAtElementDomain(context, sibling.element, value)
|
|
462
1425
|
}
|
|
463
|
-
if (
|
|
464
|
-
|
|
1426
|
+
if (
|
|
1427
|
+
op === "pointIn" &&
|
|
1428
|
+
!("closed" in sibling) &&
|
|
1429
|
+
(sibling.kind === "u64" || sibling.kind === "i64") &&
|
|
1430
|
+
isIntervalShaped(value)
|
|
1431
|
+
) {
|
|
1432
|
+
return taggedAtElementDomain(context, sibling.kind, value)
|
|
465
1433
|
}
|
|
466
1434
|
return taggedLiteral(context, sibling, value)
|
|
467
1435
|
}
|
|
@@ -470,225 +1438,180 @@ function taggedCmpLiteral(
|
|
|
470
1438
|
interface LowerContext {
|
|
471
1439
|
readonly theory: AnySchema
|
|
472
1440
|
readonly relationIds: ReadonlyMap<string, number>
|
|
473
|
-
readonly
|
|
474
|
-
readonly
|
|
475
|
-
|
|
476
|
-
readonly usedParams: Set<number>
|
|
1441
|
+
readonly recIds: ReadonlyMap<RecData, number>
|
|
1442
|
+
readonly paramIds: ReadonlyMap<string, number>
|
|
1443
|
+
readonly params: ReadonlyMap<string, ParamEntry>
|
|
477
1444
|
}
|
|
478
1445
|
|
|
479
1446
|
/** One rule's dense variable numbering: first occurrence in written order. */
|
|
480
1447
|
interface VarIds {
|
|
481
|
-
of(
|
|
482
|
-
synthetic(): number
|
|
1448
|
+
of(name: string): number
|
|
483
1449
|
}
|
|
484
1450
|
|
|
485
1451
|
/** Creates one rule-scoped variable numberer. */
|
|
486
|
-
function makeVarIds(
|
|
487
|
-
const assigned = new Map<
|
|
488
|
-
const state = { next: 0 }
|
|
1452
|
+
function makeVarIds(): VarIds {
|
|
1453
|
+
const assigned = new Map<string, number>()
|
|
489
1454
|
return {
|
|
490
|
-
of(
|
|
491
|
-
const existing = assigned.get(
|
|
1455
|
+
of(name) {
|
|
1456
|
+
const existing = assigned.get(name)
|
|
492
1457
|
if (existing !== undefined) {
|
|
493
1458
|
return existing
|
|
494
1459
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
`query lowering: the variable declared from ${variable.relation}.${variable.field} belongs to a different query scope`
|
|
498
|
-
)
|
|
499
|
-
}
|
|
500
|
-
const id = state.next
|
|
501
|
-
state.next += 1
|
|
502
|
-
assigned.set(variable, id)
|
|
503
|
-
return id
|
|
504
|
-
},
|
|
505
|
-
synthetic() {
|
|
506
|
-
const id = state.next
|
|
507
|
-
state.next += 1
|
|
1460
|
+
const id = assigned.size
|
|
1461
|
+
assigned.set(name, id)
|
|
508
1462
|
return id
|
|
509
1463
|
}
|
|
510
1464
|
}
|
|
511
1465
|
}
|
|
512
1466
|
|
|
513
|
-
/** Resolves a param
|
|
514
|
-
function paramIdOf(ctx: LowerContext,
|
|
515
|
-
const
|
|
516
|
-
if (
|
|
517
|
-
throw errors.new(`query lowering: param ${name}
|
|
1467
|
+
/** Resolves a param name to its dense positional id. */
|
|
1468
|
+
function paramIdOf(ctx: LowerContext, name: string): number {
|
|
1469
|
+
const id = ctx.paramIds.get(name)
|
|
1470
|
+
if (id === undefined) {
|
|
1471
|
+
throw errors.new(`query lowering: param ${name} is not in the query's registry`)
|
|
518
1472
|
}
|
|
519
|
-
|
|
520
|
-
return index
|
|
1473
|
+
return id
|
|
521
1474
|
}
|
|
522
1475
|
|
|
523
|
-
/**
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
1476
|
+
/**
|
|
1477
|
+
* Lowers one EDB atom (either polarity). A CLOSED owner lowers through the
|
|
1478
|
+
* same edb source — its ordinal is its record-declaration slot exactly like
|
|
1479
|
+
* an ordinary relation's — with field ordinals over the SEALED shape: `id`
|
|
1480
|
+
* at 0, each payload column at its declared index + 1 (`matchFieldsOf`
|
|
1481
|
+
* carries the shift; the lowering golden pins it).
|
|
1482
|
+
*/
|
|
1483
|
+
function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
|
|
1484
|
+
const member = ctx.theory.relations[atom.relation.name]
|
|
1485
|
+
if (member !== atom.relation) {
|
|
1486
|
+
throw errors.new(
|
|
1487
|
+
`query lowering: relation ${atom.relation.name} is not the relation value schema ${ctx.theory.name} declares`
|
|
1488
|
+
)
|
|
1489
|
+
}
|
|
1490
|
+
const relationId = ctx.relationIds.get(atom.relation.name)
|
|
1491
|
+
if (relationId === undefined) {
|
|
1492
|
+
throw errors.new(`query lowering: relation ${atom.relation.name} has no ordinal`)
|
|
1493
|
+
}
|
|
1494
|
+
const ordered = matchFieldsOf(atom.relation)
|
|
531
1495
|
const bindings: Array<readonly [number, TermIr]> = atom.bindings.map(function lowerBinding(binding) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1496
|
+
const ordinal = ordered.findIndex(function byName(candidate) {
|
|
1497
|
+
return candidate.name === binding.field
|
|
1498
|
+
})
|
|
1499
|
+
if (ordinal < 0) {
|
|
1500
|
+
throw errors.new(`query lowering: relation ${atom.relation.name} has no field ${binding.field}`)
|
|
1501
|
+
}
|
|
1502
|
+
return [ordinal, lowerBindingTerm(ctx, `${atom.relation.name}.${binding.field}`, binding, ids)] as const
|
|
536
1503
|
})
|
|
537
|
-
return { source, bindings }
|
|
1504
|
+
return { source: { kind: "edb", relation: relationId }, bindings }
|
|
538
1505
|
}
|
|
539
1506
|
|
|
540
|
-
/** Lowers
|
|
541
|
-
function
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
)
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
return { kind: "edb", relation: id }
|
|
554
|
-
}
|
|
555
|
-
const pred = ctx.predicateIds.get(source.pred)
|
|
556
|
-
if (pred === undefined) {
|
|
557
|
-
throw errors.new(`query lowering: predicate ${source.pred.name} was declared in a different query scope`)
|
|
1507
|
+
/** Lowers one binding term. */
|
|
1508
|
+
function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEntry, ids: VarIds): TermIr {
|
|
1509
|
+
const bound = binding.term
|
|
1510
|
+
switch (bound.kind) {
|
|
1511
|
+
case "var":
|
|
1512
|
+
return { kind: "var", var: ids.of(bound.name) }
|
|
1513
|
+
case "param":
|
|
1514
|
+
return { kind: "param", param: paramIdOf(ctx, bound.name) }
|
|
1515
|
+
case "setParam":
|
|
1516
|
+
return { kind: "paramSet", param: paramIdOf(ctx, bound.name) }
|
|
1517
|
+
case "literal":
|
|
1518
|
+
return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) }
|
|
558
1519
|
}
|
|
559
|
-
return { kind: "idb", pred }
|
|
560
1520
|
}
|
|
561
1521
|
|
|
562
|
-
/**
|
|
563
|
-
function
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
})
|
|
568
|
-
if (ordinal < 0) {
|
|
569
|
-
throw errors.new(`query lowering: relation ${source.relation.name} has no field ${binding.field}`)
|
|
570
|
-
}
|
|
571
|
-
return ordinal
|
|
1522
|
+
/** Lowers one idb atom: positional head bindings, `FieldId(i)` = head position i. */
|
|
1523
|
+
function lowerIdbAtom(ctx: LowerContext, rec: RecData, vars: readonly string[], ids: VarIds): AtomIr {
|
|
1524
|
+
const pred = ctx.recIds.get(rec)
|
|
1525
|
+
if (pred === undefined) {
|
|
1526
|
+
throw errors.new(`query lowering: rec ${rec.name} was declared by a different program`)
|
|
572
1527
|
}
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (ordinal < 0) {
|
|
577
|
-
throw errors.new(`query lowering: predicate ${source.pred.name} has no column ${binding.field}`)
|
|
1528
|
+
const arity = rec.rules[0]?.select.length
|
|
1529
|
+
if (arity !== undefined && vars.length !== arity) {
|
|
1530
|
+
throw errors.new(`query lowering: idb ${rec.name} takes ${arity} positions, got ${vars.length}`)
|
|
578
1531
|
}
|
|
579
|
-
|
|
1532
|
+
const bindings: Array<readonly [number, TermIr]> = vars.map(function lowerPosition(name, position) {
|
|
1533
|
+
return [position, { kind: "var", var: ids.of(name) } as const] as const
|
|
1534
|
+
})
|
|
1535
|
+
return { source: { kind: "idb", pred }, bindings }
|
|
580
1536
|
}
|
|
581
1537
|
|
|
582
|
-
/** Lowers one
|
|
583
|
-
function
|
|
1538
|
+
/** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
|
|
1539
|
+
function lowerCmpTerm(
|
|
584
1540
|
ctx: LowerContext,
|
|
585
|
-
|
|
586
|
-
|
|
1541
|
+
rule: RuleData,
|
|
1542
|
+
side: CmpTermData,
|
|
1543
|
+
sibling: CmpTermData,
|
|
587
1544
|
ids: VarIds,
|
|
588
|
-
|
|
1545
|
+
op: CmpKind
|
|
589
1546
|
): TermIr {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
throw errors.new(`${context}: an Allen-mask param is not a field-typed binding`)
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
if (bound.kind === "oneOf") {
|
|
606
|
-
const minted = ids.synthetic()
|
|
607
|
-
const leaves: ConditionTreeIr[] = bound.values.map(function equalityLeaf(candidate) {
|
|
608
|
-
return {
|
|
609
|
-
kind: "leaf",
|
|
610
|
-
cmp: {
|
|
611
|
-
op: { kind: "eq" },
|
|
612
|
-
lhs: { kind: "var", var: minted },
|
|
613
|
-
rhs: { kind: "literal", value: taggedLiteral(context, binding.data, candidate) }
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
})
|
|
617
|
-
extraConditions.push({ kind: "or", children: leaves })
|
|
618
|
-
return { kind: "var", var: minted }
|
|
619
|
-
}
|
|
620
|
-
return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) }
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/** Lowers one comparison side; literals tag by the sibling's element domain (op-aware at `pointIn`). */
|
|
624
|
-
function lowerCmpTerm(ctx: LowerContext, side: CmpTerm, sibling: CmpTerm, ids: VarIds, op: CmpOpData["kind"]): TermIr {
|
|
625
|
-
if (side.kind === "term") {
|
|
626
|
-
const value = side.value
|
|
627
|
-
switch (value[term]) {
|
|
628
|
-
case "var":
|
|
629
|
-
return { kind: "var", var: ids.of(value) }
|
|
630
|
-
case "param":
|
|
631
|
-
return { kind: "param", param: paramIdOf(ctx, value, value.name) }
|
|
632
|
-
case "paramSet":
|
|
633
|
-
return { kind: "paramSet", param: paramIdOf(ctx, value, value.name) }
|
|
634
|
-
case "maskParam":
|
|
1547
|
+
switch (side.kind) {
|
|
1548
|
+
case "var":
|
|
1549
|
+
return { kind: "var", var: ids.of(side.name) }
|
|
1550
|
+
case "param":
|
|
1551
|
+
return { kind: "param", param: paramIdOf(ctx, side.name) }
|
|
1552
|
+
case "setParam":
|
|
1553
|
+
return { kind: "paramSet", param: paramIdOf(ctx, side.name) }
|
|
1554
|
+
case "measure":
|
|
1555
|
+
return { kind: "measure", var: ids.of(side.name) }
|
|
1556
|
+
case "literal": {
|
|
1557
|
+
const anchor = cmpAnchorOf(ctx, rule, sibling)
|
|
1558
|
+
if (anchor === undefined) {
|
|
635
1559
|
throw errors.new(
|
|
636
|
-
"query lowering:
|
|
1560
|
+
"query lowering: a comparison literal needs a bound-variable, measure, or anchored-param sibling to type it"
|
|
637
1561
|
)
|
|
1562
|
+
}
|
|
1563
|
+
return { kind: "literal", value: taggedCmpLiteral("comparison literal", anchor, side.value, op) }
|
|
638
1564
|
}
|
|
639
1565
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
throw errors.new("query lowering: an Allen-mask param cannot type a literal side")
|
|
647
|
-
}
|
|
648
|
-
return {
|
|
649
|
-
kind: "literal",
|
|
650
|
-
value: taggedCmpLiteral("comparison literal", value.data, side.value, op)
|
|
651
|
-
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/** Resolves the anchor a comparison literal tags by: the sibling's field, the measure, or an anchored param. */
|
|
1569
|
+
function cmpAnchorOf(ctx: LowerContext, rule: RuleData, sibling: CmpTermData): AnyField | "measure" | undefined {
|
|
1570
|
+
if (sibling.kind === "var") {
|
|
1571
|
+
return rule.varFields[sibling.name]?.field
|
|
652
1572
|
}
|
|
653
1573
|
if (sibling.kind === "measure") {
|
|
654
|
-
return
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1574
|
+
return "measure"
|
|
1575
|
+
}
|
|
1576
|
+
if (sibling.kind === "param" || sibling.kind === "setParam") {
|
|
1577
|
+
return ctx.params.get(sibling.name)?.anchor
|
|
658
1578
|
}
|
|
659
|
-
|
|
1579
|
+
return undefined
|
|
660
1580
|
}
|
|
661
1581
|
|
|
662
1582
|
/** Lowers one comparison. */
|
|
663
|
-
function lowerComparison(ctx: LowerContext, cmp:
|
|
664
|
-
|
|
665
|
-
|
|
1583
|
+
function lowerComparison(ctx: LowerContext, rule: RuleData, cmp: CmpData, ids: VarIds): ComparisonIr {
|
|
1584
|
+
if (cmp.op === "allen") {
|
|
1585
|
+
const maskData = cmp.mask
|
|
1586
|
+
if (maskData === undefined) {
|
|
1587
|
+
throw errors.new("query lowering: an allen comparison lost its mask")
|
|
1588
|
+
}
|
|
666
1589
|
const mask =
|
|
667
|
-
|
|
668
|
-
?
|
|
669
|
-
:
|
|
1590
|
+
maskData.kind === "literal"
|
|
1591
|
+
? { kind: "literal" as const, mask: maskData.mask }
|
|
1592
|
+
: { kind: "param" as const, param: paramIdOf(ctx, maskData.name) }
|
|
670
1593
|
return {
|
|
671
1594
|
op: { kind: "allen", mask },
|
|
672
|
-
lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
|
|
673
|
-
rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
|
|
1595
|
+
lhs: lowerCmpTerm(ctx, rule, cmp.lhs, cmp.rhs, ids, "allen"),
|
|
1596
|
+
rhs: lowerCmpTerm(ctx, rule, cmp.rhs, cmp.lhs, ids, "allen")
|
|
674
1597
|
}
|
|
675
1598
|
}
|
|
676
1599
|
return {
|
|
677
|
-
op: { kind: op
|
|
678
|
-
lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, op
|
|
679
|
-
rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, op
|
|
1600
|
+
op: { kind: cmp.op },
|
|
1601
|
+
lhs: lowerCmpTerm(ctx, rule, cmp.lhs, cmp.rhs, ids, cmp.op),
|
|
1602
|
+
rhs: lowerCmpTerm(ctx, rule, cmp.rhs, cmp.lhs, ids, cmp.op)
|
|
680
1603
|
}
|
|
681
1604
|
}
|
|
682
1605
|
|
|
683
1606
|
/** Lowers one condition node (comparison leaf or and/or tree). */
|
|
684
|
-
function lowerCondition(ctx: LowerContext,
|
|
685
|
-
if (
|
|
686
|
-
return { kind: "leaf", cmp: lowerComparison(ctx,
|
|
1607
|
+
function lowerCondition(ctx: LowerContext, rule: RuleData, cond: CondData, ids: VarIds): ConditionTreeIr {
|
|
1608
|
+
if (cond.kind === "cmp") {
|
|
1609
|
+
return { kind: "leaf", cmp: lowerComparison(ctx, rule, cond, ids) }
|
|
687
1610
|
}
|
|
688
1611
|
return {
|
|
689
|
-
kind:
|
|
690
|
-
children:
|
|
691
|
-
return lowerCondition(ctx, child, ids)
|
|
1612
|
+
kind: cond.op,
|
|
1613
|
+
children: cond.children.map(function lowerChild(child) {
|
|
1614
|
+
return lowerCondition(ctx, rule, child, ids)
|
|
692
1615
|
})
|
|
693
1616
|
}
|
|
694
1617
|
}
|
|
@@ -701,41 +1624,36 @@ function lowerFind(entry: SelectEntryData, ids: VarIds): FindTermIr {
|
|
|
701
1624
|
if (entry.kind === "measure") {
|
|
702
1625
|
return { kind: "measure", var: ids.of(entry.over) }
|
|
703
1626
|
}
|
|
704
|
-
const
|
|
705
|
-
switch (
|
|
1627
|
+
const agg = entry.agg
|
|
1628
|
+
switch (agg.op) {
|
|
706
1629
|
case "count":
|
|
707
1630
|
return { kind: "aggregate", op: { kind: "count" } }
|
|
708
1631
|
case "countDistinct":
|
|
709
|
-
return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(
|
|
1632
|
+
return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) }
|
|
710
1633
|
case "fold": {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
return { kind: "aggregate", op: { kind: aggregate.fold }, over: ids.of(over) }
|
|
1634
|
+
if (typeof agg.over === "string") {
|
|
1635
|
+
return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) }
|
|
714
1636
|
}
|
|
715
|
-
return { kind: "aggregateMeasure", op: { kind:
|
|
1637
|
+
return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) }
|
|
716
1638
|
}
|
|
717
1639
|
case "arg":
|
|
718
|
-
return {
|
|
719
|
-
kind: "aggregate",
|
|
720
|
-
op: { kind: aggregate.direction, key: ids.of(aggregate.key) },
|
|
721
|
-
over: ids.of(aggregate.over)
|
|
722
|
-
}
|
|
1640
|
+
return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) }
|
|
723
1641
|
case "pack":
|
|
724
|
-
return { kind: "aggregate", op: { kind: "pack" }, over: ids.of(
|
|
1642
|
+
return { kind: "aggregate", op: { kind: "pack" }, over: ids.of(agg.over) }
|
|
725
1643
|
}
|
|
726
1644
|
}
|
|
727
1645
|
|
|
728
1646
|
/** One aggregate's var-free head-op kind (`AggOp::head_op`). */
|
|
729
|
-
function headOpOf(
|
|
730
|
-
switch (
|
|
1647
|
+
function headOpOf(agg: AggData): HeadOpIr {
|
|
1648
|
+
switch (agg.op) {
|
|
731
1649
|
case "count":
|
|
732
1650
|
return "count"
|
|
733
1651
|
case "countDistinct":
|
|
734
1652
|
return "countDistinct"
|
|
735
1653
|
case "fold":
|
|
736
|
-
return
|
|
1654
|
+
return agg.fold
|
|
737
1655
|
case "arg":
|
|
738
|
-
return
|
|
1656
|
+
return agg.direction
|
|
739
1657
|
case "pack":
|
|
740
1658
|
return "pack"
|
|
741
1659
|
}
|
|
@@ -747,43 +1665,53 @@ function headTermOf(column: SelectColumn): HeadTermIr {
|
|
|
747
1665
|
if (entry.kind === "var" || entry.kind === "measure") {
|
|
748
1666
|
return { kind: "var" }
|
|
749
1667
|
}
|
|
750
|
-
return { kind: "aggregate", op: headOpOf(entry.
|
|
1668
|
+
return { kind: "aggregate", op: headOpOf(entry.agg) }
|
|
751
1669
|
}
|
|
752
1670
|
|
|
753
|
-
/** Lowers one rule: body walked in written order, finds
|
|
754
|
-
function lowerRule(ctx: LowerContext,
|
|
755
|
-
const ids = makeVarIds(
|
|
1671
|
+
/** Lowers one rule: body walked in written order (var ids by first occurrence), finds last. */
|
|
1672
|
+
function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
|
|
1673
|
+
const ids = makeVarIds()
|
|
756
1674
|
const atoms: AtomIr[] = []
|
|
757
1675
|
const negated: AtomIr[] = []
|
|
758
1676
|
const conditions: ConditionTreeIr[] = []
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1677
|
+
for (const item of rule.items) {
|
|
1678
|
+
switch (item.kind) {
|
|
1679
|
+
case "atom": {
|
|
1680
|
+
atoms.push(lowerAtom(ctx, item.atom, ids))
|
|
1681
|
+
break
|
|
1682
|
+
}
|
|
1683
|
+
case "negated": {
|
|
1684
|
+
negated.push(lowerAtom(ctx, item.atom, ids))
|
|
1685
|
+
break
|
|
1686
|
+
}
|
|
1687
|
+
case "idb": {
|
|
1688
|
+
atoms.push(lowerIdbAtom(ctx, item.rec, item.vars, ids))
|
|
1689
|
+
break
|
|
1690
|
+
}
|
|
1691
|
+
case "cond": {
|
|
1692
|
+
conditions.push(lowerCondition(ctx, rule, item.cond, ids))
|
|
1693
|
+
break
|
|
767
1694
|
}
|
|
768
|
-
continue
|
|
769
1695
|
}
|
|
770
|
-
conditions.push(lowerCondition(ctx, item, ids))
|
|
771
1696
|
}
|
|
772
1697
|
return {
|
|
773
|
-
finds:
|
|
1698
|
+
finds: rule.select.map(function findOf(column) {
|
|
1699
|
+
return lowerFind(column.entry, ids)
|
|
1700
|
+
}),
|
|
774
1701
|
atoms,
|
|
775
1702
|
negated,
|
|
776
|
-
conditions
|
|
1703
|
+
conditions
|
|
777
1704
|
}
|
|
778
1705
|
}
|
|
779
1706
|
|
|
780
1707
|
/**
|
|
781
1708
|
* Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
1709
|
+
* recs in declaration order (`PredId` = index), the output predicate
|
|
1710
|
+
* (rules + head) appended last. Relations lower by declaration ordinal,
|
|
1711
|
+
* the law the engine's own manifest pins; `db.prepare` re-verifies the
|
|
1712
|
+
* alignment against the live manifest before sending. Every registered
|
|
1713
|
+
* param must carry a field anchor by now — an unanchorable param (its
|
|
1714
|
+
* every use beside a literal) is refused here, naming it.
|
|
787
1715
|
*/
|
|
788
1716
|
function lowerQuery(q: AnyQuery): ProgramIr {
|
|
789
1717
|
const theory = q.schema
|
|
@@ -791,65 +1719,68 @@ function lowerQuery(q: AnyQuery): ProgramIr {
|
|
|
791
1719
|
Object.keys(theory.relations).forEach(function assignOrdinal(name, index) {
|
|
792
1720
|
relationIds.set(name, index)
|
|
793
1721
|
})
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
predicateIds.set(pred, index)
|
|
1722
|
+
const recIds = new Map<RecData, number>()
|
|
1723
|
+
q.data.recs.forEach(function assignPredId(rec, index) {
|
|
1724
|
+
recIds.set(rec, index)
|
|
798
1725
|
})
|
|
799
|
-
const
|
|
800
|
-
const
|
|
1726
|
+
const paramIds = new Map<string, number>()
|
|
1727
|
+
const params = new Map<string, ParamEntry>()
|
|
1728
|
+
q.data.params.forEach(function assignParamId(entry, index) {
|
|
1729
|
+
if (entry.anchor === undefined && entry.shape !== "mask") {
|
|
1730
|
+
throw errors.new(
|
|
1731
|
+
`query param ${entry.name} has no field-anchored use — bind it in an atom or compare it against a bound variable`
|
|
1732
|
+
)
|
|
1733
|
+
}
|
|
1734
|
+
paramIds.set(entry.name, index)
|
|
1735
|
+
params.set(entry.name, entry)
|
|
1736
|
+
})
|
|
1737
|
+
const ctx: LowerContext = { theory, relationIds, recIds, paramIds, params }
|
|
1738
|
+
const predicates: PredicateDefIr[] = q.data.recs.map(function lowerRec(rec) {
|
|
1739
|
+
const head = rec.rules[0]
|
|
1740
|
+
if (head === undefined) {
|
|
1741
|
+
throw errors.new(`query lowering: rec ${rec.name} has no rules`)
|
|
1742
|
+
}
|
|
801
1743
|
return {
|
|
802
|
-
head:
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
rules: pred.rules.map(function lowerClause(rule) {
|
|
806
|
-
return lowerRule(ctx, rule.body, function clauseFinds(ids) {
|
|
807
|
-
return rule.finds.map(function findVar(variable): FindTermIr {
|
|
808
|
-
return { kind: "var", var: ids.of(variable) }
|
|
809
|
-
})
|
|
810
|
-
})
|
|
1744
|
+
head: head.select.map(headTermOf),
|
|
1745
|
+
rules: rec.rules.map(function lowerRecRule(rule) {
|
|
1746
|
+
return lowerRule(ctx, rule)
|
|
811
1747
|
})
|
|
812
1748
|
}
|
|
813
1749
|
})
|
|
814
1750
|
predicates.push({
|
|
815
1751
|
head: q.data.select.map(headTermOf),
|
|
816
|
-
rules: q.data.rules.map(function lowerOutputRule(
|
|
817
|
-
return lowerRule(ctx,
|
|
818
|
-
return q.data.select.map(function findOf(column) {
|
|
819
|
-
return lowerFind(column.entry, ids)
|
|
820
|
-
})
|
|
821
|
-
})
|
|
1752
|
+
rules: q.data.rules.map(function lowerOutputRule(rule) {
|
|
1753
|
+
return lowerRule(ctx, rule)
|
|
822
1754
|
})
|
|
823
1755
|
})
|
|
824
|
-
|
|
825
|
-
* The dead-declaration refusal: a declared param no rule reached is
|
|
826
|
-
* unexecutable BOTH ways — its inferred `Params` contribution is nothing
|
|
827
|
-
* (params ride item phantoms), yet the wire marshal is the full registry
|
|
828
|
-
* in declaration order, while the ENGINE's arity is usage-derived. The
|
|
829
|
-
* contradiction is refused here, the earliest seam that knows usage, so
|
|
830
|
-
* every query that lowers has registry == used set (and the dense-id
|
|
831
|
-
* hole a skipped middle declaration would open never exists).
|
|
832
|
-
*/
|
|
833
|
-
registry.params.forEach(function assertUsed(entry, index) {
|
|
834
|
-
if (!ctx.usedParams.has(index)) {
|
|
835
|
-
throw errors.new(
|
|
836
|
-
`query declares param ${entry.name} but no rule uses it — remove the declaration or reference it`
|
|
837
|
-
)
|
|
838
|
-
}
|
|
839
|
-
})
|
|
840
|
-
return { predicates, output: registry.predicates.length }
|
|
1756
|
+
return { predicates, output: q.data.recs.length }
|
|
841
1757
|
}
|
|
842
1758
|
|
|
843
1759
|
export type {
|
|
844
1760
|
AnyQuery,
|
|
845
|
-
|
|
1761
|
+
AnyRuleValue,
|
|
1762
|
+
HeadFieldsOf,
|
|
1763
|
+
HeadOf,
|
|
1764
|
+
HeadShape,
|
|
1765
|
+
OutputRuleChain,
|
|
1766
|
+
OutputRuleScope,
|
|
1767
|
+
ParamsOf,
|
|
1768
|
+
ProgramState,
|
|
846
1769
|
Query,
|
|
847
|
-
QueryBuild,
|
|
848
1770
|
QueryData,
|
|
849
1771
|
QueryParams,
|
|
1772
|
+
QueryRelation,
|
|
850
1773
|
QueryRow,
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1774
|
+
QueryRuleChain,
|
|
1775
|
+
QueryRuleScope,
|
|
1776
|
+
QueryStart,
|
|
1777
|
+
RawChain,
|
|
1778
|
+
RawScope,
|
|
1779
|
+
RecRef,
|
|
1780
|
+
RecRuleChain,
|
|
1781
|
+
RecRuleScope,
|
|
1782
|
+
RowOf,
|
|
1783
|
+
RuleValue,
|
|
1784
|
+
TermOps
|
|
854
1785
|
}
|
|
855
|
-
export { lowerQuery, query, taggedLiteral }
|
|
1786
|
+
export { lowerQuery, makeOutputRuleScope, makeQuery, makeRawScope, query, taggedCmpLiteral, taggedLiteral }
|