@bjornpagen/bumbledb 0.5.0 → 0.6.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 +96 -131
- package/README.md +9 -9
- package/dist/db.js +2 -2
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/query/atom.d.ts +132 -201
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +30 -42
- package/dist/query/atom.js.map +1 -1
- package/dist/query/find.d.ts +116 -0
- package/dist/query/find.d.ts.map +1 -0
- package/dist/query/{select.js → find.js} +22 -22
- package/dist/query/find.js.map +1 -0
- package/dist/query/lower.d.ts +123 -158
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +437 -493
- package/dist/query/lower.js.map +1 -1
- package/dist/query/predicate.d.ts +22 -14
- package/dist/query/predicate.d.ts.map +1 -1
- package/dist/query/predicate.js +35 -42
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/run.d.ts +3 -3
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +9 -9
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +127 -72
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +80 -33
- package/dist/query/scope.js.map +1 -1
- package/package.json +2 -2
- package/src/db.ts +4 -4
- package/src/index.ts +11 -7
- package/src/query/atom.ts +181 -263
- package/src/query/find.ts +212 -0
- package/src/query/lower.ts +588 -722
- package/src/query/predicate.ts +37 -45
- package/src/query/run.ts +10 -10
- package/src/query/scope.ts +172 -87
- package/dist/query/select.d.ts +0 -128
- package/dist/query/select.d.ts.map +0 -1
- package/dist/query/select.js.map +0 -1
- package/src/query/select.ts +0 -215
package/src/query/lower.ts
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `query()` and the IR lowering,
|
|
3
|
-
* kysely-shaped —
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* name-
|
|
22
|
-
* the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
2
|
+
* `query()` and the IR lowering, REFERENCE-IDENTITY edition. A query is
|
|
3
|
+
* built kysely-shaped — variables minted by {@link v} outside the rule and
|
|
4
|
+
* reused by REFERENCE to join:
|
|
5
|
+
*
|
|
6
|
+
* query(S).rule((r) => {
|
|
7
|
+
* const acct = v(Account)
|
|
8
|
+
* const h = v(Holder)
|
|
9
|
+
* return r
|
|
10
|
+
* .match(Account, { id: acct.id, holder: acct.holder })
|
|
11
|
+
* .match(Holder, { id: acct.holder })
|
|
12
|
+
* .where(r.eq(acct.holder, r.param("root")))
|
|
13
|
+
* .find({ account: acct.id, holder: acct.holder })
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* — and is an INERT value: `Query<Rels, Row, Params>` with `Row` inferred
|
|
17
|
+
* from each rule's `.find` RECORD (its keys ARE the answer columns) and
|
|
18
|
+
* `Params` inferred to be EXACTLY the params the rules use (params are typed
|
|
19
|
+
* BY USE; a param no rule uses never registers). Variable IDENTITY is the
|
|
20
|
+
* object reference: reusing one value across binding positions IS the join,
|
|
21
|
+
* and a name-collision join is unrepresentable. Each binding position is
|
|
22
|
+
* judged against the variable's MINT slot — and because {@link JoinOk} is an
|
|
23
|
+
* equality, that alone makes every cross-binding join transitively
|
|
24
|
+
* class-equal. Lowering is a pure function of the query value down to the
|
|
25
|
+
* bridge's `ProgramIr` (`bumbledb/crates/bumbledb/src/ir.rs`): relations by
|
|
26
|
+
* declaration ordinal, variables by dense per-rule first-occurrence ids
|
|
27
|
+
* (keyed on the object REFERENCE — the discipline is unchanged, only the map
|
|
28
|
+
* key moved from name to reference), params by first-use order. Lowering is
|
|
29
|
+
* STABLE — the same query value lowers to deeply-equal IR every time, and
|
|
30
|
+
* two identically-written queries (fresh mints each) lower identically.
|
|
31
|
+
* Construction validates negation safety and boundness (typed by the var's
|
|
32
|
+
* label — object identity is invisible to the type tier, so these are
|
|
33
|
+
* construction-time walls); everything else (strata, types, aggregate
|
|
34
|
+
* rosters, rule caps) is the ENGINE's judge, surfacing at prepare.
|
|
26
35
|
*/
|
|
27
36
|
|
|
28
37
|
import * as errors from "@superbuilders/errors"
|
|
@@ -47,7 +56,6 @@ import type {
|
|
|
47
56
|
AggData,
|
|
48
57
|
AnyCond,
|
|
49
58
|
AtomData,
|
|
50
|
-
BindEnv,
|
|
51
59
|
BindingEntry,
|
|
52
60
|
BindParamsShape,
|
|
53
61
|
CheckBindings,
|
|
@@ -57,6 +65,8 @@ import type {
|
|
|
57
65
|
CmpTermData,
|
|
58
66
|
CondData,
|
|
59
67
|
CondParamsShape,
|
|
68
|
+
FindColumn,
|
|
69
|
+
FindEntryData,
|
|
60
70
|
MaskData,
|
|
61
71
|
MatchFields,
|
|
62
72
|
MatchOwner,
|
|
@@ -64,21 +74,20 @@ import type {
|
|
|
64
74
|
ParamUse,
|
|
65
75
|
RecData,
|
|
66
76
|
RuleData,
|
|
67
|
-
RuleItem
|
|
68
|
-
SelectColumn,
|
|
69
|
-
SelectEntryData,
|
|
70
|
-
TreeData
|
|
77
|
+
RuleItem
|
|
71
78
|
} from "#query/atom.ts"
|
|
72
79
|
import { allen, and, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts"
|
|
80
|
+
import type { CheckFind, CheckRecFind, FindShape, HeadRecordOf, RowOfFind } from "#query/find.ts"
|
|
81
|
+
import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/find.ts"
|
|
73
82
|
import type {
|
|
83
|
+
AnyVar,
|
|
74
84
|
ClassedField,
|
|
75
|
-
EnvShape,
|
|
76
85
|
Flatten,
|
|
77
86
|
InferredOf,
|
|
78
87
|
JoinOk,
|
|
88
|
+
MintSlotOf,
|
|
79
89
|
ParamEntry,
|
|
80
|
-
ParamsRecord
|
|
81
|
-
Var
|
|
90
|
+
ParamsRecord
|
|
82
91
|
} from "#query/scope.ts"
|
|
83
92
|
import {
|
|
84
93
|
fieldJoins,
|
|
@@ -88,28 +97,18 @@ import {
|
|
|
88
97
|
makeMaskParam,
|
|
89
98
|
makeParam,
|
|
90
99
|
makeSetParam,
|
|
91
|
-
makeVar,
|
|
92
100
|
renderFieldKind,
|
|
93
101
|
term
|
|
94
102
|
} 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 } from "#relation.ts"
|
|
98
103
|
import type { AnySchema, Schema, SchemaRelations } from "#schema.ts"
|
|
99
104
|
|
|
100
105
|
/**
|
|
101
106
|
* The matchable members of a schema's record — ordinary relations AND
|
|
102
|
-
* closed vocabularies (ψ query atoms
|
|
103
|
-
*
|
|
104
|
-
* plan-constant member set or joins the L1-resident virtual image — the
|
|
105
|
-
* SDK lowers pass-through and never knows which).
|
|
107
|
+
* closed vocabularies (ψ query atoms; the ENGINE decides folding vs virtual
|
|
108
|
+
* image, the SDK lowers pass-through).
|
|
106
109
|
*/
|
|
107
110
|
type QueryRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], MatchOwner>
|
|
108
111
|
|
|
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
112
|
/** Reads an inferred-params carrier off a rec reference or rule value. */
|
|
114
113
|
type ParamsOf<T> = InferredOf<T> extends { readonly params: infer P extends ParamsRecord } ? P : Record<never, never>
|
|
115
114
|
|
|
@@ -117,19 +116,16 @@ type ParamsOf<T> = InferredOf<T> extends { readonly params: infer P extends Para
|
|
|
117
116
|
type RowOf<T> = InferredOf<T> extends { readonly row: infer R } ? R : never
|
|
118
117
|
|
|
119
118
|
/**
|
|
120
|
-
* A recursive predicate's HEAD signature as classed slots
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* values that carry no head (a plain query rule, or an unthreaded rec
|
|
124
|
-
* handle before its first rule).
|
|
119
|
+
* A recursive predicate's HEAD signature as classed slots, keyed by column
|
|
120
|
+
* name — carried on the rec reference so an `idb` join can be judged against
|
|
121
|
+
* it; `undefined` on values that carry no head.
|
|
125
122
|
*/
|
|
126
|
-
type HeadShape =
|
|
123
|
+
type HeadShape = Readonly<Record<string, ClassedField>> | undefined
|
|
127
124
|
|
|
128
125
|
/**
|
|
129
126
|
* One finished rule as a plain value: the runtime data plus the inferred
|
|
130
|
-
* row/params carrier (and, for a RECURSIVE rule, the head
|
|
131
|
-
*
|
|
132
|
-
* `.rule(...)` consumes it; hosts never build one by hand.
|
|
127
|
+
* row/params carrier (and, for a RECURSIVE rule, the head record of classed
|
|
128
|
+
* slots `idb` pairs against). `.rule(...)` consumes it.
|
|
133
129
|
*/
|
|
134
130
|
interface RuleValue<Row, P extends ParamsRecord, Head extends HeadShape = undefined> {
|
|
135
131
|
readonly rule: RuleData
|
|
@@ -139,22 +135,14 @@ interface RuleValue<Row, P extends ParamsRecord, Head extends HeadShape = undefi
|
|
|
139
135
|
/** Any finished rule value. */
|
|
140
136
|
type AnyRuleValue = RuleValue<unknown, ParamsRecord, HeadShape>
|
|
141
137
|
|
|
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]
|
|
145
|
-
}
|
|
146
|
-
|
|
147
138
|
/** Reads an inferred-head carrier off a rule value or rec reference. */
|
|
148
|
-
type HeadOf<T> =
|
|
139
|
+
type HeadOf<T> =
|
|
140
|
+
InferredOf<T> extends { readonly head: infer H extends Readonly<Record<string, ClassedField>> } ? H : undefined
|
|
149
141
|
|
|
150
142
|
/**
|
|
151
143
|
* A recursive predicate REFERENCE — the shape `idb()` targets carry: the
|
|
152
|
-
* name (type-level identity
|
|
153
|
-
*
|
|
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).
|
|
144
|
+
* name (type-level identity), the runtime data (value identity), the params
|
|
145
|
+
* its rules have used, and the head signature its FIRST rule sealed.
|
|
158
146
|
*/
|
|
159
147
|
interface RecRef<Name extends string, P extends ParamsRecord, Head extends HeadShape = HeadShape> {
|
|
160
148
|
readonly name: Name
|
|
@@ -162,42 +150,44 @@ interface RecRef<Name extends string, P extends ParamsRecord, Head extends HeadS
|
|
|
162
150
|
readonly [inferred]?: { readonly params: P; readonly head: Head }
|
|
163
151
|
}
|
|
164
152
|
|
|
165
|
-
/** One `idb` position's judgment:
|
|
166
|
-
type
|
|
167
|
-
|
|
168
|
-
?
|
|
169
|
-
?
|
|
170
|
-
? JoinOk<Env[N], F>
|
|
171
|
-
: true
|
|
153
|
+
/** One `idb` position's judgment: a variable class-equal to the head slot when the head is carried. */
|
|
154
|
+
type IdbBindingOk<Classes extends SchemaClasses, HeadSlot, V> = V extends AnyVar
|
|
155
|
+
? HeadSlot extends ClassedField
|
|
156
|
+
? JoinOk<HeadSlot, MintSlotOf<Classes, V>> extends true
|
|
157
|
+
? true
|
|
172
158
|
: false
|
|
173
|
-
:
|
|
159
|
+
: true
|
|
160
|
+
: false
|
|
174
161
|
|
|
175
162
|
/**
|
|
176
|
-
* The validated `idb`
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* prepare (the engine's law stands behind both tiers).
|
|
163
|
+
* The validated `idb` bindings record: when the target carries its head
|
|
164
|
+
* (a threaded rec handle), the record's key set must EXACTLY equal the
|
|
165
|
+
* head's (a missing or extra key maps every property to `never`) and each
|
|
166
|
+
* variable must be class-equal to its head slot — the same wall `JoinOk`
|
|
167
|
+
* holds for EDB atoms. An unthreaded handle carries no head; every entry
|
|
168
|
+
* must still be a variable, arity/class judged at construction and prepare.
|
|
183
169
|
*/
|
|
184
|
-
type
|
|
185
|
-
|
|
186
|
-
?
|
|
187
|
-
?
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
170
|
+
type CheckIdbBindings<Classes extends SchemaClasses, Head, B> =
|
|
171
|
+
Head extends Readonly<Record<string, ClassedField>>
|
|
172
|
+
? [keyof B] extends [keyof Head]
|
|
173
|
+
? [keyof Head] extends [keyof B]
|
|
174
|
+
? {
|
|
175
|
+
readonly [K in keyof B]: K extends keyof Head
|
|
176
|
+
? IdbBindingOk<Classes, Head[K], B[K]> extends true
|
|
177
|
+
? B[K]
|
|
178
|
+
: never
|
|
179
|
+
: never
|
|
180
|
+
}
|
|
181
|
+
: { readonly [K in keyof B]: never }
|
|
182
|
+
: { readonly [K in keyof B]: never }
|
|
183
|
+
: { readonly [K in keyof B]: B[K] extends AnyVar ? B[K] : never }
|
|
191
184
|
|
|
192
185
|
/**
|
|
193
186
|
* The term/predicate/aggregate constructor vocabulary every rule builder
|
|
194
|
-
* carries — pure value builders
|
|
195
|
-
*
|
|
196
|
-
* environment.
|
|
187
|
+
* carries — pure value builders. Variables are minted by the free {@link v},
|
|
188
|
+
* outside the rule, and reused by reference; `r` no longer mints them.
|
|
197
189
|
*/
|
|
198
190
|
interface TermOps {
|
|
199
|
-
/** Declares/names one variable: typed by the field it first binds; reuse joins. */
|
|
200
|
-
readonly var: typeof makeVar
|
|
201
191
|
/** Names one scalar parameter: typed by its use; the key of the execute params object. */
|
|
202
192
|
readonly param: typeof makeParam
|
|
203
193
|
/** Names one ∈-set parameter (the IR's `ParamSet`): bound to a readonly array at execution. */
|
|
@@ -227,92 +217,68 @@ interface TermOps {
|
|
|
227
217
|
readonly pack: typeof pack
|
|
228
218
|
}
|
|
229
219
|
|
|
230
|
-
/** The rule builder a `query(S).rule(...)` callback receives
|
|
220
|
+
/** The rule builder a `query(S).rule(...)` callback receives (`Classes` — the join judge's authority). */
|
|
231
221
|
interface QueryRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> extends TermOps {
|
|
232
|
-
/** The first EDB atom of the rule: fields bind
|
|
222
|
+
/** The first EDB atom of the rule: fields bind variables, params, ∈-sets, or bare literals; absence is the wildcard. */
|
|
233
223
|
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
234
224
|
relation: R,
|
|
235
|
-
bindings: B & CheckBindings<
|
|
236
|
-
): QueryRuleChain<
|
|
237
|
-
Rels,
|
|
238
|
-
EnvOfMatch<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
239
|
-
BindParamsShape<MatchFields<R>, B>,
|
|
240
|
-
Classes
|
|
241
|
-
>
|
|
225
|
+
bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
226
|
+
): QueryRuleChain<Rels, BindParamsShape<MatchFields<R>, B>, Classes>
|
|
242
227
|
}
|
|
243
228
|
|
|
244
229
|
/** The chain of a plain query rule: more atoms, residual predicates, then the head. */
|
|
245
230
|
interface QueryRuleChain<
|
|
246
231
|
Rels extends SchemaRelations,
|
|
247
|
-
Env extends EnvShape,
|
|
248
232
|
P extends ParamsRecord,
|
|
249
233
|
Classes extends SchemaClasses = SchemaClasses
|
|
250
234
|
> {
|
|
251
|
-
/** One more positive EDB atom —
|
|
235
|
+
/** One more positive EDB atom — variable reuse joins, class-equal by the mint-slot judgment. */
|
|
252
236
|
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
253
237
|
relation: R,
|
|
254
|
-
bindings: B & CheckBindings<
|
|
255
|
-
): QueryRuleChain<
|
|
256
|
-
Rels,
|
|
257
|
-
EnvOfMatch<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
258
|
-
Flatten<P & BindParamsShape<MatchFields<R>, B>>,
|
|
259
|
-
Classes
|
|
260
|
-
>
|
|
238
|
+
bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
239
|
+
): QueryRuleChain<Rels, Flatten<P & BindParamsShape<MatchFields<R>, B>>, Classes>
|
|
261
240
|
/** One residual predicate: a comparison, an `and`/`or` tree, or a negated atom (`r.not`). */
|
|
262
241
|
where<const C extends AnyCond>(
|
|
263
|
-
cond: CheckCond<
|
|
264
|
-
): QueryRuleChain<Rels,
|
|
265
|
-
/** The head projection:
|
|
266
|
-
|
|
242
|
+
cond: CheckCond<Classes, C> & C
|
|
243
|
+
): QueryRuleChain<Rels, Flatten<P & CondParamsShape<C>>, Classes>
|
|
244
|
+
/** The head projection: a `find` RECORD whose keys name the answer columns. */
|
|
245
|
+
find<const F extends FindShape>(entries: F & CheckFind<F>): RuleValue<RowOfFind<F>, P>
|
|
267
246
|
}
|
|
268
247
|
|
|
269
248
|
/** The rule builder an OUTPUT rule of a `program()` receives: a query rule plus finished-stratum `idb` atoms. */
|
|
270
249
|
interface OutputRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> extends TermOps {
|
|
271
250
|
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
272
251
|
relation: R,
|
|
273
|
-
bindings: B & CheckBindings<
|
|
274
|
-
): OutputRuleChain<
|
|
275
|
-
Rels,
|
|
276
|
-
EnvOfMatch<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
277
|
-
BindParamsShape<MatchFields<R>, B>,
|
|
278
|
-
Classes
|
|
279
|
-
>
|
|
252
|
+
bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
253
|
+
): OutputRuleChain<Rels, BindParamsShape<MatchFields<R>, B>, Classes>
|
|
280
254
|
}
|
|
281
255
|
|
|
282
256
|
/** The chain of an output rule: atoms, predicates, `idb` joins over the program's recs, then the head. */
|
|
283
257
|
interface OutputRuleChain<
|
|
284
258
|
Rels extends SchemaRelations,
|
|
285
|
-
Env extends EnvShape,
|
|
286
259
|
P extends ParamsRecord,
|
|
287
260
|
Classes extends SchemaClasses = SchemaClasses
|
|
288
261
|
> {
|
|
289
262
|
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
290
263
|
relation: R,
|
|
291
|
-
bindings: B & CheckBindings<
|
|
292
|
-
): OutputRuleChain<
|
|
293
|
-
Rels,
|
|
294
|
-
EnvOfMatch<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
295
|
-
Flatten<P & BindParamsShape<MatchFields<R>, B>>,
|
|
296
|
-
Classes
|
|
297
|
-
>
|
|
264
|
+
bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
265
|
+
): OutputRuleChain<Rels, Flatten<P & BindParamsShape<MatchFields<R>, B>>, Classes>
|
|
298
266
|
where<const C extends AnyCond>(
|
|
299
|
-
cond: CheckCond<
|
|
300
|
-
): OutputRuleChain<Rels,
|
|
267
|
+
cond: CheckCond<Classes, C> & C
|
|
268
|
+
): OutputRuleChain<Rels, Flatten<P & CondParamsShape<C>>, Classes>
|
|
301
269
|
/**
|
|
302
270
|
* One `idb` atom over a FINISHED stratum (any rec of this program): a
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
* the rule
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
* AND its head signature, so the join is arity- and class-checked
|
|
309
|
-
* against the head at compile time.
|
|
271
|
+
* NAMED join against the rec's head — the bindings record's keys are the
|
|
272
|
+
* head columns, each bound to a variable positively bound by a relation
|
|
273
|
+
* atom of the rule. Threading the rec value the last `.rule(...)` returned
|
|
274
|
+
* carries its params into `Params` AND its head signature, so the join is
|
|
275
|
+
* key-exact and class-checked against the head at compile time.
|
|
310
276
|
*/
|
|
311
|
-
idb<Target extends RecRef<string, ParamsRecord>, const
|
|
277
|
+
idb<Target extends RecRef<string, ParamsRecord>, const B extends Readonly<Record<string, AnyVar>>>(
|
|
312
278
|
target: Target,
|
|
313
|
-
|
|
314
|
-
): OutputRuleChain<Rels,
|
|
315
|
-
|
|
279
|
+
bindings: B & CheckIdbBindings<Classes, HeadOf<Target>, B>
|
|
280
|
+
): OutputRuleChain<Rels, Flatten<P & ParamsOf<Target>>, Classes>
|
|
281
|
+
find<const F extends FindShape>(entries: F & CheckFind<F>): RuleValue<RowOfFind<F>, P>
|
|
316
282
|
}
|
|
317
283
|
|
|
318
284
|
/** The rule builder a RECURSIVE rule (`rec.rule(...)`) receives. */
|
|
@@ -320,53 +286,35 @@ interface RecRuleScope<Rels extends SchemaRelations, Self extends string, Classe
|
|
|
320
286
|
extends TermOps {
|
|
321
287
|
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
322
288
|
relation: R,
|
|
323
|
-
bindings: B & CheckBindings<
|
|
324
|
-
): RecRuleChain<
|
|
325
|
-
Rels,
|
|
326
|
-
Self,
|
|
327
|
-
EnvOfMatch<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
328
|
-
BindParamsShape<MatchFields<R>, B>,
|
|
329
|
-
Classes
|
|
330
|
-
>
|
|
289
|
+
bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
290
|
+
): RecRuleChain<Rels, Self, BindParamsShape<MatchFields<R>, B>, Classes>
|
|
331
291
|
}
|
|
332
292
|
|
|
333
293
|
/**
|
|
334
|
-
* The chain of a recursive rule. Its `idb` accepts ONLY the rec itself
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* and its `select` takes bound variable NAMES only: aggregates and the
|
|
338
|
-
* measure are unrepresentable in a recursive head (the strata judge's
|
|
339
|
-
* `AggregationThroughCycle`/`MeasureInRecursiveHead`, made unwritable).
|
|
294
|
+
* The chain of a recursive rule. Its `idb` accepts ONLY the rec itself (the
|
|
295
|
+
* self-recursion cut) and its `find` takes bound variables only — aggregates
|
|
296
|
+
* and the measure are unrepresentable in a recursive head.
|
|
340
297
|
*/
|
|
341
298
|
interface RecRuleChain<
|
|
342
299
|
Rels extends SchemaRelations,
|
|
343
300
|
Self extends string,
|
|
344
|
-
Env extends EnvShape,
|
|
345
301
|
P extends ParamsRecord,
|
|
346
302
|
Classes extends SchemaClasses = SchemaClasses
|
|
347
303
|
> {
|
|
348
304
|
match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
|
|
349
305
|
relation: R,
|
|
350
|
-
bindings: B & CheckBindings<
|
|
351
|
-
): RecRuleChain<
|
|
352
|
-
Rels,
|
|
353
|
-
Self,
|
|
354
|
-
EnvOfMatch<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
|
|
355
|
-
Flatten<P & BindParamsShape<MatchFields<R>, B>>,
|
|
356
|
-
Classes
|
|
357
|
-
>
|
|
306
|
+
bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
|
|
307
|
+
): RecRuleChain<Rels, Self, Flatten<P & BindParamsShape<MatchFields<R>, B>>, Classes>
|
|
358
308
|
where<const C extends AnyCond>(
|
|
359
|
-
cond: CheckCond<
|
|
360
|
-
): RecRuleChain<Rels, Self,
|
|
361
|
-
/** The self-recursive atom: `idb(self,
|
|
362
|
-
idb<Target extends RecRef<Self, ParamsRecord>, const
|
|
309
|
+
cond: CheckCond<Classes, C> & C
|
|
310
|
+
): RecRuleChain<Rels, Self, Flatten<P & CondParamsShape<C>>, Classes>
|
|
311
|
+
/** The self-recursive atom: `idb(self, { headKey: boundVar })` — only this rec's own reference is accepted. */
|
|
312
|
+
idb<Target extends RecRef<Self, ParamsRecord>, const B extends Readonly<Record<string, AnyVar>>>(
|
|
363
313
|
target: Target,
|
|
364
|
-
|
|
365
|
-
): RecRuleChain<Rels, Self,
|
|
366
|
-
/** The recursive head:
|
|
367
|
-
|
|
368
|
-
...names: CheckNameSelect<Env, S> & S
|
|
369
|
-
): RuleValue<RowOfNameSelect<Env, S>, P, HeadFieldsOf<Env, S>>
|
|
314
|
+
bindings: B & CheckIdbBindings<Classes, HeadOf<Target>, B>
|
|
315
|
+
): RecRuleChain<Rels, Self, P, Classes>
|
|
316
|
+
/** The recursive head: a `find` record of bound variables only; the value carries the head's classed slots for `idb` pairing. */
|
|
317
|
+
find<const F extends FindShape>(entries: F & CheckRecFind<F>): RuleValue<RowOfFind<F>, P, HeadRecordOf<Classes, F>>
|
|
370
318
|
}
|
|
371
319
|
|
|
372
320
|
/** A query's runtime description — everything lowering, the wire marshal, and answer decode read. */
|
|
@@ -376,16 +324,15 @@ interface QueryData {
|
|
|
376
324
|
/** The output rules in written order (multiple rules = set union). */
|
|
377
325
|
readonly rules: readonly RuleData[]
|
|
378
326
|
/** The head columns (every rule derives the same head; written order = answer column order). */
|
|
379
|
-
readonly
|
|
327
|
+
readonly finds: readonly FindColumn[]
|
|
380
328
|
/** The registered params in first-use order across the program walk (= dense `ParamId`s). */
|
|
381
329
|
readonly params: readonly ParamEntry[]
|
|
382
330
|
}
|
|
383
331
|
|
|
384
332
|
/**
|
|
385
333
|
* An inert query value. `Row` is the inferred answer-row object type;
|
|
386
|
-
* `Params` the inferred execute-params object type — exactly the params
|
|
387
|
-
*
|
|
388
|
-
* engine.
|
|
334
|
+
* `Params` the inferred execute-params object type — exactly the params the
|
|
335
|
+
* rules use. Prepare with `db.prepare(q)`.
|
|
389
336
|
*/
|
|
390
337
|
interface Query<
|
|
391
338
|
Rels extends SchemaRelations,
|
|
@@ -402,11 +349,7 @@ interface Query<
|
|
|
402
349
|
readonly [inferred]?: { readonly row: Row; readonly params: Params }
|
|
403
350
|
}
|
|
404
351
|
|
|
405
|
-
/**
|
|
406
|
-
* Any query value as lowering and the runtime consume it: the theory it
|
|
407
|
-
* was built against and its runtime description — every `Query` (typed or
|
|
408
|
-
* program-built) carries exactly this.
|
|
409
|
-
*/
|
|
352
|
+
/** Any query value as lowering and the runtime consume it. */
|
|
410
353
|
interface AnyQuery {
|
|
411
354
|
readonly schema: AnySchema
|
|
412
355
|
readonly data: QueryData
|
|
@@ -427,7 +370,6 @@ interface QueryStart<Rels extends SchemaRelations, Classes extends SchemaClasses
|
|
|
427
370
|
|
|
428
371
|
/** The frozen constructor vocabulary every rule builder spreads. */
|
|
429
372
|
const termOps: TermOps = Object.freeze({
|
|
430
|
-
var: makeVar,
|
|
431
373
|
param: makeParam,
|
|
432
374
|
inSet: makeSetParam,
|
|
433
375
|
maskParam: makeMaskParam,
|
|
@@ -453,41 +395,49 @@ const termOps: TermOps = Object.freeze({
|
|
|
453
395
|
pack
|
|
454
396
|
})
|
|
455
397
|
|
|
456
|
-
/** One rule under construction: immutable — every chain step is a fresh state. */
|
|
398
|
+
/** One rule under construction: immutable — every chain step is a fresh state. Boundness rides the `bound` set of var references. */
|
|
457
399
|
interface RuleBuildState {
|
|
458
400
|
readonly items: readonly RuleItem[]
|
|
459
|
-
readonly
|
|
401
|
+
readonly bound: ReadonlySet<AnyVar>
|
|
460
402
|
readonly paramUses: readonly ParamUse[]
|
|
461
403
|
}
|
|
462
404
|
|
|
463
405
|
/** The empty rule state. */
|
|
464
406
|
const EMPTY_RULE: RuleBuildState = Object.freeze({
|
|
465
407
|
items: Object.freeze([]),
|
|
466
|
-
|
|
408
|
+
bound: new Set<AnyVar>(),
|
|
467
409
|
paramUses: Object.freeze([])
|
|
468
410
|
})
|
|
469
411
|
|
|
470
|
-
/** One resolved bindings record: the atom entries, the
|
|
412
|
+
/** One resolved bindings record: the atom entries, the variable references it binds, and the params it uses. */
|
|
471
413
|
interface ResolvedBindings {
|
|
472
414
|
readonly atom: AtomData
|
|
473
|
-
readonly vars:
|
|
415
|
+
readonly vars: readonly AnyVar[]
|
|
474
416
|
readonly uses: readonly ParamUse[]
|
|
475
417
|
}
|
|
476
418
|
|
|
419
|
+
/**
|
|
420
|
+
* The MINT slot of a variable, the runtime twin of {@link MintSlotOf}: (i)
|
|
421
|
+
* verifies the mint owner is the schema's own member value — a variable
|
|
422
|
+
* minted from a foreign relation is refused, naming its label — and (ii)
|
|
423
|
+
* returns the descriptor it was minted at plus the law-computed class read
|
|
424
|
+
* off the schema's frozen class map. Because {@link fieldJoins} is an
|
|
425
|
+
* equality, judging every binding position against this one slot makes all
|
|
426
|
+
* cross-binding joins mutually class-equal by transitivity.
|
|
427
|
+
*/
|
|
428
|
+
function mintSlotOf(context: ChainContext, ref: AnyVar): ClassedField {
|
|
429
|
+
if (context.theory.relations[ref.owner.name] !== ref.owner) {
|
|
430
|
+
throw errors.new(
|
|
431
|
+
`the variable ${ref.label} was minted from a relation schema ${context.theory.name} does not declare — mint variables with v() from the schema's own relations`
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
return { field: ref.field, class: context.classes[ref.owner.name]?.[ref.column] }
|
|
435
|
+
}
|
|
436
|
+
|
|
477
437
|
/**
|
|
478
438
|
* Judges one membership ARRAY at a binding position — legal exactly at a
|
|
479
|
-
* CLOSED-reference field
|
|
480
|
-
*
|
|
481
|
-
* vocabulary's spelling), holding ≥ 2 DISTINCT handle names (the
|
|
482
|
-
* degenerate sets are refusals: empty selects nothing, one element is the
|
|
483
|
-
* bare literal respelled, and a duplicate member is the same respelling in
|
|
484
|
-
* disguise — write each member once). The returned name is
|
|
485
|
-
* CONTENT-ADDRESSED (vocabulary + the member SET — the key sorts a copy,
|
|
486
|
-
* so two spellings of one set, reordered or not, share one dense
|
|
487
|
-
* `ParamId`); the members are shape-checked strings here and
|
|
488
|
-
* roster-verified at the one verification point (`taggedHandleId`) when
|
|
489
|
-
* the SDK supplies the set at execute — the same moment a bound `r.inSet`
|
|
490
|
-
* param's members are judged.
|
|
439
|
+
* CLOSED-reference field, holding ≥ 2 DISTINCT handle names. The returned
|
|
440
|
+
* name is CONTENT-ADDRESSED (vocabulary + the member SET).
|
|
491
441
|
*/
|
|
492
442
|
function membershipSet(
|
|
493
443
|
context: string,
|
|
@@ -526,24 +476,23 @@ function membershipSet(
|
|
|
526
476
|
}
|
|
527
477
|
|
|
528
478
|
/**
|
|
529
|
-
* Resolves a bindings record against an atom owner's matchable fields
|
|
530
|
-
* relation's declared fields; a closed relation's sealed id + columns), in
|
|
479
|
+
* Resolves a bindings record against an atom owner's matchable fields, in
|
|
531
480
|
* the record's written order: terms classify by their runtime tag,
|
|
532
|
-
* everything else is a bare literal
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
481
|
+
* everything else is a bare literal. Every VARIABLE binding judges
|
|
482
|
+
* `fieldJoins(mintSlot, positionSlot)` and throws on a class-unequal reuse
|
|
483
|
+
* (the runtime twin of `CheckBindings`); the bound refs are collected for
|
|
484
|
+
* the rule's boundness set.
|
|
536
485
|
*/
|
|
537
486
|
function resolveBindings(
|
|
538
|
-
context:
|
|
487
|
+
context: ChainContext,
|
|
488
|
+
label: string,
|
|
539
489
|
relation: MatchOwner,
|
|
540
|
-
bindings: Readonly<Record<string, unknown
|
|
541
|
-
classes: SchemaClasses
|
|
490
|
+
bindings: Readonly<Record<string, unknown>>
|
|
542
491
|
): ResolvedBindings {
|
|
543
492
|
const entries: BindingEntry[] = []
|
|
544
|
-
const vars:
|
|
493
|
+
const vars: AnyVar[] = []
|
|
545
494
|
const uses: ParamUse[] = []
|
|
546
|
-
const relationClasses = classes[relation.name]
|
|
495
|
+
const relationClasses = context.classes[relation.name]
|
|
547
496
|
const ordered = sealedFieldsOf(relation)
|
|
548
497
|
for (const [fieldName, value] of Object.entries(bindings)) {
|
|
549
498
|
if (value === undefined) {
|
|
@@ -553,17 +502,23 @@ function resolveBindings(
|
|
|
553
502
|
return candidate.name === fieldName
|
|
554
503
|
})
|
|
555
504
|
if (declared === undefined) {
|
|
556
|
-
throw errors.new(`${
|
|
505
|
+
throw errors.new(`${label} has no field ${fieldName}`)
|
|
557
506
|
}
|
|
558
507
|
const fieldClass = relationClasses?.[fieldName]
|
|
559
508
|
let bound: BindingEntry["term"]
|
|
560
509
|
if (isTerm(value)) {
|
|
561
510
|
switch (value[term]) {
|
|
562
511
|
case "var": {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
)
|
|
512
|
+
const ref = value
|
|
513
|
+
const mint = mintSlotOf(context, ref)
|
|
514
|
+
const positionSlot: ClassedField = { field: declared.field, class: fieldClass }
|
|
515
|
+
if (!fieldJoins(mint, positionSlot)) {
|
|
516
|
+
throw errors.new(
|
|
517
|
+
`${label}: the variable ${ref.label} joins domain-unequal fields — minted at ${renderFieldKind(mint)}, reused at ${renderFieldKind(positionSlot)} (a var joins only class-equal slots; bare pairs only with bare)`
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
bound = Object.freeze({ kind: "var" as const, ref })
|
|
521
|
+
vars.push(ref)
|
|
567
522
|
break
|
|
568
523
|
}
|
|
569
524
|
case "param": {
|
|
@@ -594,15 +549,15 @@ function resolveBindings(
|
|
|
594
549
|
}
|
|
595
550
|
case "maskParam":
|
|
596
551
|
throw errors.new(
|
|
597
|
-
`${
|
|
552
|
+
`${label}.${fieldName}: an Allen-mask param is not a field-typed value — masks live in allen() conditions only`
|
|
598
553
|
)
|
|
599
554
|
case "duration":
|
|
600
555
|
throw errors.new(
|
|
601
|
-
`${
|
|
556
|
+
`${label}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and find entries`
|
|
602
557
|
)
|
|
603
558
|
}
|
|
604
559
|
} else if (Array.isArray(value)) {
|
|
605
|
-
const set = membershipSet(`${
|
|
560
|
+
const set = membershipSet(`${label}.${fieldName}`, declared.field, value)
|
|
606
561
|
bound = Object.freeze({ kind: "literalSet" as const, name: set.name, members: set.members })
|
|
607
562
|
uses.push(
|
|
608
563
|
Object.freeze({
|
|
@@ -618,57 +573,40 @@ function resolveBindings(
|
|
|
618
573
|
}
|
|
619
574
|
entries.push(Object.freeze({ field: fieldName, data: declared.field, class: fieldClass, term: bound }))
|
|
620
575
|
}
|
|
621
|
-
return {
|
|
622
|
-
atom: Object.freeze({ relation, bindings: Object.freeze(entries) }),
|
|
623
|
-
vars,
|
|
624
|
-
uses
|
|
625
|
-
}
|
|
576
|
+
return { atom: Object.freeze({ relation, bindings: Object.freeze(entries) }), vars, uses }
|
|
626
577
|
}
|
|
627
578
|
|
|
628
|
-
/**
|
|
629
|
-
* Extends a rule state with one positive atom. Vars bind on first
|
|
630
|
-
* occurrence; every LATER occurrence (a later atom's field or a same-record
|
|
631
|
-
* sibling) is a join and must be class-equal — the construction-time twin
|
|
632
|
-
* of the type tier's `JoinOk` (bare pairs only with bare), so the domain
|
|
633
|
-
* wall holds for untyped callers too.
|
|
634
|
-
*/
|
|
579
|
+
/** Extends a rule state with one positive atom; the bound variable references accumulate into the boundness set. */
|
|
635
580
|
function advanceMatch(
|
|
581
|
+
context: ChainContext,
|
|
636
582
|
state: RuleBuildState,
|
|
637
583
|
relation: MatchOwner,
|
|
638
|
-
bindings: Readonly<Record<string, unknown
|
|
639
|
-
classes: SchemaClasses
|
|
584
|
+
bindings: Readonly<Record<string, unknown>>
|
|
640
585
|
): RuleBuildState {
|
|
641
|
-
const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings
|
|
642
|
-
const
|
|
643
|
-
for (const
|
|
644
|
-
|
|
645
|
-
if (existing === undefined) {
|
|
646
|
-
varFields[bound.name] = bound.slot
|
|
647
|
-
} else if (!fieldJoins(existing, bound.slot)) {
|
|
648
|
-
throw errors.new(
|
|
649
|
-
`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)`
|
|
650
|
-
)
|
|
651
|
-
}
|
|
586
|
+
const resolved = resolveBindings(context, `relation ${relation.name}`, relation, bindings)
|
|
587
|
+
const bound = new Set(state.bound)
|
|
588
|
+
for (const ref of resolved.vars) {
|
|
589
|
+
bound.add(ref)
|
|
652
590
|
}
|
|
653
|
-
return {
|
|
591
|
+
return Object.freeze({
|
|
654
592
|
items: Object.freeze([...state.items, Object.freeze({ kind: "atom" as const, atom: resolved.atom })]),
|
|
655
|
-
|
|
593
|
+
bound,
|
|
656
594
|
paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
|
|
657
|
-
}
|
|
595
|
+
})
|
|
658
596
|
}
|
|
659
597
|
|
|
660
|
-
/** Resolves one comparison side to its runtime term. */
|
|
598
|
+
/** Resolves one comparison side to its runtime term (variables and the measure ride by reference). */
|
|
661
599
|
function cmpTermDataOf(op: string, value: unknown): CmpTermData {
|
|
662
600
|
if (isTerm(value)) {
|
|
663
601
|
switch (value[term]) {
|
|
664
602
|
case "var":
|
|
665
|
-
return Object.freeze({ kind: "var" as const,
|
|
603
|
+
return Object.freeze({ kind: "var" as const, ref: value })
|
|
666
604
|
case "param":
|
|
667
605
|
return Object.freeze({ kind: "param" as const, name: value.name })
|
|
668
606
|
case "setParam":
|
|
669
607
|
return Object.freeze({ kind: "setParam" as const, name: value.name })
|
|
670
608
|
case "duration":
|
|
671
|
-
return Object.freeze({ kind: "measure" as const,
|
|
609
|
+
return Object.freeze({ kind: "measure" as const, ref: value.over })
|
|
672
610
|
case "maskParam":
|
|
673
611
|
throw errors.new(`${op}: an Allen-mask param is not a comparison term — masks live in allen()'s mask position`)
|
|
674
612
|
}
|
|
@@ -678,23 +616,16 @@ function cmpTermDataOf(op: string, value: unknown): CmpTermData {
|
|
|
678
616
|
|
|
679
617
|
/**
|
|
680
618
|
* One comparison side's contribution to the param census: a param/set side
|
|
681
|
-
* anchors to its SIBLING — a
|
|
682
|
-
*
|
|
683
|
-
* anchor and must be anchored by some other use of the same name.
|
|
619
|
+
* anchors to its SIBLING — a variable's field descriptor or the measure; an
|
|
620
|
+
* unanchorable use records with no anchor.
|
|
684
621
|
*/
|
|
685
|
-
function sideUses(
|
|
686
|
-
op: CmpKind,
|
|
687
|
-
side: CmpTermData,
|
|
688
|
-
sibling: CmpTermData,
|
|
689
|
-
varFields: Readonly<Record<string, ClassedField>>,
|
|
690
|
-
uses: ParamUse[]
|
|
691
|
-
): void {
|
|
622
|
+
function sideUses(op: CmpKind, side: CmpTermData, sibling: CmpTermData, uses: ParamUse[]): void {
|
|
692
623
|
if (side.kind !== "param" && side.kind !== "setParam") {
|
|
693
624
|
return
|
|
694
625
|
}
|
|
695
626
|
let anchor: AnyField | "measure" | undefined
|
|
696
627
|
if (sibling.kind === "var") {
|
|
697
|
-
anchor =
|
|
628
|
+
anchor = sibling.ref.field
|
|
698
629
|
} else if (sibling.kind === "measure") {
|
|
699
630
|
anchor = "measure"
|
|
700
631
|
} else {
|
|
@@ -712,12 +643,12 @@ function sideUses(
|
|
|
712
643
|
}
|
|
713
644
|
|
|
714
645
|
/** Lowers one condition VALUE to its runtime data, recording param uses. */
|
|
715
|
-
function condDataOf(cond: AnyCond,
|
|
646
|
+
function condDataOf(cond: AnyCond, uses: ParamUse[]): CondData {
|
|
716
647
|
if (cond.cond === "cmp") {
|
|
717
648
|
const lhs = cmpTermDataOf(cond.op, cond.lhs)
|
|
718
649
|
const rhs = cmpTermDataOf(cond.op, cond.rhs)
|
|
719
|
-
sideUses(cond.op, lhs, rhs,
|
|
720
|
-
sideUses(cond.op, rhs, lhs,
|
|
650
|
+
sideUses(cond.op, lhs, rhs, uses)
|
|
651
|
+
sideUses(cond.op, rhs, lhs, uses)
|
|
721
652
|
let mask: MaskData | undefined
|
|
722
653
|
if (cond.op === "allen") {
|
|
723
654
|
const maskValue = cond.mask
|
|
@@ -738,15 +669,13 @@ function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, ClassedFie
|
|
|
738
669
|
throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam")
|
|
739
670
|
}
|
|
740
671
|
}
|
|
741
|
-
|
|
742
|
-
return data
|
|
672
|
+
return Object.freeze({ kind: "cmp" as const, op: cond.op, mask, lhs, rhs })
|
|
743
673
|
}
|
|
744
674
|
if (cond.cond === "tree") {
|
|
745
675
|
const children = cond.children.map(function lowerChild(child) {
|
|
746
|
-
return condDataOf(child,
|
|
676
|
+
return condDataOf(child, uses)
|
|
747
677
|
})
|
|
748
|
-
|
|
749
|
-
return data
|
|
678
|
+
return Object.freeze({ kind: "tree" as const, op: cond.op, children: Object.freeze(children) })
|
|
750
679
|
}
|
|
751
680
|
throw errors.new(
|
|
752
681
|
"a negated atom is not a condition-tree node — pass not(...) to where() directly, never inside and()/or()"
|
|
@@ -754,7 +683,7 @@ function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, ClassedFie
|
|
|
754
683
|
}
|
|
755
684
|
|
|
756
685
|
/** Extends a rule state with one `.where` item (a condition or a negated atom). */
|
|
757
|
-
function advanceWhere(state: RuleBuildState, cond: AnyCond
|
|
686
|
+
function advanceWhere(context: ChainContext, state: RuleBuildState, cond: AnyCond): RuleBuildState {
|
|
758
687
|
if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
|
|
759
688
|
throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom")
|
|
760
689
|
}
|
|
@@ -765,136 +694,139 @@ function advanceWhere(state: RuleBuildState, cond: AnyCond, classes: SchemaClass
|
|
|
765
694
|
return value !== undefined
|
|
766
695
|
})
|
|
767
696
|
)
|
|
768
|
-
const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings
|
|
769
|
-
return {
|
|
697
|
+
const resolved = resolveBindings(context, `negated relation ${relation.name}`, relation, bindings)
|
|
698
|
+
return Object.freeze({
|
|
770
699
|
items: Object.freeze([...state.items, Object.freeze({ kind: "negated" as const, atom: resolved.atom })]),
|
|
771
|
-
|
|
700
|
+
bound: state.bound,
|
|
772
701
|
paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
|
|
773
|
-
}
|
|
702
|
+
})
|
|
774
703
|
}
|
|
775
704
|
const uses: ParamUse[] = []
|
|
776
|
-
const data = condDataOf(cond,
|
|
777
|
-
return {
|
|
705
|
+
const data = condDataOf(cond, uses)
|
|
706
|
+
return Object.freeze({
|
|
778
707
|
items: Object.freeze([...state.items, Object.freeze({ kind: "cond" as const, cond: data })]),
|
|
779
|
-
|
|
708
|
+
bound: state.bound,
|
|
780
709
|
paramUses: Object.freeze([...state.paramUses, ...uses])
|
|
781
|
-
}
|
|
710
|
+
})
|
|
782
711
|
}
|
|
783
712
|
|
|
784
|
-
/** Extends a rule state with one `idb` atom (
|
|
785
|
-
function advanceIdb(state: RuleBuildState, rec: RecData,
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
713
|
+
/** Extends a rule state with one `idb` atom (a named record over head keys; vars validated at completion). */
|
|
714
|
+
function advanceIdb(state: RuleBuildState, rec: RecData, bindings: Readonly<Record<string, unknown>>): RuleBuildState {
|
|
715
|
+
const resolved: Array<{ readonly key: string; readonly ref: AnyVar }> = []
|
|
716
|
+
for (const [key, value] of Object.entries(bindings)) {
|
|
717
|
+
if (value === undefined) {
|
|
718
|
+
continue
|
|
789
719
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
720
|
+
if (!isTerm(value) || value[term] !== "var") {
|
|
721
|
+
throw errors.new(
|
|
722
|
+
`idb ${rec.name}: position ${key} takes a variable — bind literals and params through where()/match()`
|
|
723
|
+
)
|
|
724
|
+
}
|
|
725
|
+
resolved.push(Object.freeze({ key, ref: value }))
|
|
796
726
|
}
|
|
727
|
+
return Object.freeze({
|
|
728
|
+
items: Object.freeze([
|
|
729
|
+
...state.items,
|
|
730
|
+
Object.freeze({ kind: "idb" as const, rec, bindings: Object.freeze(resolved) })
|
|
731
|
+
]),
|
|
732
|
+
bound: state.bound,
|
|
733
|
+
paramUses: state.paramUses
|
|
734
|
+
})
|
|
797
735
|
}
|
|
798
736
|
|
|
799
|
-
/** Narrows a
|
|
737
|
+
/** Narrows a find entry to an aggregate value. */
|
|
800
738
|
function isAggregateEntry(
|
|
801
739
|
value: unknown
|
|
802
740
|
): value is { readonly agg: string; readonly over: unknown; readonly key: unknown } {
|
|
803
741
|
return typeof value === "object" && value !== null && "agg" in value
|
|
804
742
|
}
|
|
805
743
|
|
|
806
|
-
/**
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
* bare.
|
|
811
|
-
*/
|
|
812
|
-
function selectColumnOf(entry: unknown): SelectColumn {
|
|
813
|
-
if (typeof entry === "string") {
|
|
814
|
-
return Object.freeze({
|
|
815
|
-
name: entry,
|
|
816
|
-
entry: Object.freeze({ kind: "var" as const, over: entry }),
|
|
817
|
-
closed: undefined
|
|
818
|
-
})
|
|
744
|
+
/** Narrows a value to a variable term, else a pointed refusal. */
|
|
745
|
+
function asVarTerm(context: string, value: unknown): AnyVar {
|
|
746
|
+
if (isTerm(value) && value[term] === "var") {
|
|
747
|
+
return value
|
|
819
748
|
}
|
|
820
|
-
|
|
821
|
-
if (entry[term] === "duration") {
|
|
822
|
-
return Object.freeze({
|
|
823
|
-
name: entry.name,
|
|
824
|
-
entry: Object.freeze({ kind: "measure" as const, over: entry.name }),
|
|
825
|
-
closed: undefined
|
|
826
|
-
})
|
|
827
|
-
}
|
|
828
|
-
throw errors.new(
|
|
829
|
-
`query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`
|
|
830
|
-
)
|
|
831
|
-
}
|
|
832
|
-
if (isAggregateEntry(entry)) {
|
|
833
|
-
return aggregateColumnOf(entry)
|
|
834
|
-
}
|
|
835
|
-
throw errors.new("query select: not a select entry — select takes variable names, duration(v), or aggregates")
|
|
749
|
+
throw errors.new(`${context}: expected a variable`)
|
|
836
750
|
}
|
|
837
751
|
|
|
838
|
-
/** Classifies one aggregate
|
|
839
|
-
function
|
|
840
|
-
|
|
841
|
-
readonly over: unknown
|
|
842
|
-
|
|
843
|
-
}): SelectColumn {
|
|
844
|
-
function column(name: string, agg: AggData): SelectColumn {
|
|
845
|
-
return Object.freeze({
|
|
846
|
-
name,
|
|
847
|
-
entry: Object.freeze({ kind: "aggregate" as const, agg: Object.freeze(agg) }),
|
|
848
|
-
closed: undefined
|
|
849
|
-
})
|
|
850
|
-
}
|
|
752
|
+
/** Classifies one aggregate find entry into its runtime data (variables ride by reference). */
|
|
753
|
+
function aggDataOf(
|
|
754
|
+
name: string,
|
|
755
|
+
entry: { readonly agg: string; readonly over: unknown; readonly key: unknown }
|
|
756
|
+
): AggData {
|
|
851
757
|
const over = entry.over
|
|
852
758
|
switch (entry.agg) {
|
|
853
759
|
case "count":
|
|
854
|
-
return
|
|
855
|
-
case "countDistinct":
|
|
856
|
-
|
|
857
|
-
throw errors.new("countDistinct takes a variable name")
|
|
858
|
-
}
|
|
859
|
-
return column(over, { op: "countDistinct", over })
|
|
860
|
-
}
|
|
760
|
+
return Object.freeze({ op: "count" as const })
|
|
761
|
+
case "countDistinct":
|
|
762
|
+
return Object.freeze({ op: "countDistinct" as const, over: asVarTerm(`find ${name} (countDistinct)`, over) })
|
|
861
763
|
case "sum":
|
|
862
764
|
case "min":
|
|
863
765
|
case "max": {
|
|
864
|
-
if (
|
|
865
|
-
return
|
|
766
|
+
if (isTerm(over) && over[term] === "var") {
|
|
767
|
+
return Object.freeze({ op: "fold" as const, fold: entry.agg, over })
|
|
866
768
|
}
|
|
867
769
|
if (isTerm(over) && over[term] === "duration") {
|
|
868
|
-
return
|
|
770
|
+
return Object.freeze({ op: "fold" as const, fold: entry.agg, over: Object.freeze({ duration: over.over }) })
|
|
869
771
|
}
|
|
870
|
-
throw errors.new(
|
|
772
|
+
throw errors.new(`find ${name} (${entry.agg}): takes a variable or r.duration(v)`)
|
|
871
773
|
}
|
|
872
774
|
case "argMax":
|
|
873
|
-
case "argMin":
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
775
|
+
case "argMin":
|
|
776
|
+
return Object.freeze({
|
|
777
|
+
op: "arg" as const,
|
|
778
|
+
direction: entry.agg,
|
|
779
|
+
over: asVarTerm(`find ${name} (${entry.agg})`, over),
|
|
780
|
+
key: asVarTerm(`find ${name} (${entry.agg} key)`, entry.key)
|
|
781
|
+
})
|
|
782
|
+
case "pack":
|
|
783
|
+
return Object.freeze({ op: "pack" as const, over: asVarTerm(`find ${name} (pack)`, over) })
|
|
784
|
+
default:
|
|
785
|
+
throw errors.new(`find ${name}: unknown aggregate ${entry.agg}`)
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Classifies one find entry into its named answer column (the KEY names the
|
|
791
|
+
* column, `count` included). The `slot`/`closed` slices are resolved LATER,
|
|
792
|
+
* at rule completion, where boundness and the mint slots are in hand.
|
|
793
|
+
*/
|
|
794
|
+
function findColumnOf(name: string, entry: unknown): FindColumn {
|
|
795
|
+
if (isTerm(entry)) {
|
|
796
|
+
if (entry[term] === "var") {
|
|
797
|
+
return Object.freeze({
|
|
798
|
+
name,
|
|
799
|
+
entry: Object.freeze({ kind: "var" as const, over: entry }),
|
|
800
|
+
closed: undefined,
|
|
801
|
+
slot: undefined
|
|
802
|
+
})
|
|
878
803
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
804
|
+
if (entry[term] === "duration") {
|
|
805
|
+
return Object.freeze({
|
|
806
|
+
name,
|
|
807
|
+
entry: Object.freeze({ kind: "measure" as const, over: entry.over }),
|
|
808
|
+
closed: undefined,
|
|
809
|
+
slot: undefined
|
|
810
|
+
})
|
|
884
811
|
}
|
|
885
|
-
|
|
886
|
-
|
|
812
|
+
throw errors.new(
|
|
813
|
+
`find ${name}: a ${entry[term]} is not projectable — find takes variables, r.duration(v), or aggregates`
|
|
814
|
+
)
|
|
887
815
|
}
|
|
816
|
+
if (isAggregateEntry(entry)) {
|
|
817
|
+
return Object.freeze({
|
|
818
|
+
name,
|
|
819
|
+
entry: Object.freeze({ kind: "aggregate" as const, agg: aggDataOf(name, entry) }),
|
|
820
|
+
closed: undefined,
|
|
821
|
+
slot: undefined
|
|
822
|
+
})
|
|
823
|
+
}
|
|
824
|
+
throw errors.new(`find ${name}: not a find entry — find takes variables, r.duration(v), or aggregates`)
|
|
888
825
|
}
|
|
889
826
|
|
|
890
827
|
/**
|
|
891
828
|
* The orderable ban's pointed refusal (`docs/architecture/10-data-model.md`
|
|
892
|
-
* § orderability): a closed reference is equality-and-membership only
|
|
893
|
-
* its declaration-id order is an encoding accident, so every
|
|
894
|
-
* order-comparison and fold position refuses it. The construction-time
|
|
895
|
-
* twin of the type tier's `OrderVarOk` exclusion, so the wall holds for
|
|
896
|
-
* untyped callers too (the engine cannot backstop this one: the wire IR
|
|
897
|
-
* carries plain u64s, no rosters).
|
|
829
|
+
* § orderability): a closed reference is equality-and-membership only.
|
|
898
830
|
*/
|
|
899
831
|
function closedOrderError(context: string, position: string, vocabulary: string): Error {
|
|
900
832
|
return errors.new(
|
|
@@ -902,82 +834,62 @@ function closedOrderError(context: string, position: string, vocabulary: string)
|
|
|
902
834
|
)
|
|
903
835
|
}
|
|
904
836
|
|
|
905
|
-
/** The comparison ops under the orderable ban (order roster + point membership
|
|
837
|
+
/** The comparison ops under the orderable ban (order roster + point membership). */
|
|
906
838
|
function isOrderOp(op: CmpKind | "binding"): op is "lt" | "le" | "gt" | "ge" | "pointIn" {
|
|
907
839
|
return op === "lt" || op === "le" || op === "gt" || op === "ge" || op === "pointIn"
|
|
908
840
|
}
|
|
909
841
|
|
|
910
|
-
/** Requires a
|
|
911
|
-
function assertBound(
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
throw errors.new(`${context}: the variable ${name} is not bound by a relation atom of the rule`)
|
|
842
|
+
/** Requires a variable to be bound by a relation atom of the rule (the boundness wall — invisible to the type tier). */
|
|
843
|
+
function assertBound(where: string, bound: ReadonlySet<AnyVar>, ref: AnyVar): void {
|
|
844
|
+
if (!bound.has(ref)) {
|
|
845
|
+
throw errors.new(`${where}: the variable ${ref.label} is not bound by a relation atom of the rule`)
|
|
915
846
|
}
|
|
916
|
-
return slot
|
|
917
847
|
}
|
|
918
848
|
|
|
919
|
-
/** Requires a
|
|
920
|
-
function
|
|
921
|
-
|
|
922
|
-
if (slot.field.kind !== "interval") {
|
|
849
|
+
/** Requires a variable to be interval-typed (the measure's and pack's domain), off its own descriptor. */
|
|
850
|
+
function assertInterval(where: string, ref: AnyVar): void {
|
|
851
|
+
if (ref.field.kind !== "interval") {
|
|
923
852
|
throw errors.new(
|
|
924
|
-
`${
|
|
853
|
+
`${where}: ${ref.label} is not interval-typed — the measure is defined over interval-typed variables only`
|
|
925
854
|
)
|
|
926
855
|
}
|
|
927
856
|
}
|
|
928
857
|
|
|
858
|
+
/** Requires a variable's own field to be non-closed (the orderable ban's runtime twin). */
|
|
859
|
+
function assertNotClosed(where: string, position: string, ref: AnyVar): void {
|
|
860
|
+
const roster = rosterOf(ref.field)
|
|
861
|
+
if (roster !== undefined) {
|
|
862
|
+
throw closedOrderError(where, `${position} ${ref.label}`, roster.name)
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
929
866
|
/**
|
|
930
|
-
*
|
|
931
|
-
*
|
|
932
|
-
*
|
|
933
|
-
* match-reuse join must be (the construction-time twin of the type tier's
|
|
934
|
-
* `EqOk` → `JoinOk`; bare pairs only with bare). The engine cannot backstop
|
|
935
|
-
* this one — the query IR carries no domains — so the wall lives here for
|
|
936
|
-
* untyped callers too.
|
|
867
|
+
* The classed mint slot one answer column's VALUES flow from: a projected
|
|
868
|
+
* variable's mint slot, or an Arg-carried payload's. Counts, folds, `pack`
|
|
869
|
+
* and the measure derive numbers/intervals, so they resolve no slot.
|
|
937
870
|
*/
|
|
938
|
-
function
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
const slot = assertBound(context, varFields, side.name)
|
|
943
|
-
const roster = rosterOf(slot.field)
|
|
944
|
-
if (isOrderOp(cond.op) && roster !== undefined) {
|
|
945
|
-
throw closedOrderError(context, `the ${cond.op} side ${side.name}`, roster.name)
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
if (side.kind === "measure") {
|
|
949
|
-
assertIntervalBound(context, varFields, side.name)
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
|
|
953
|
-
const lhs = assertBound(context, varFields, cond.lhs.name)
|
|
954
|
-
const rhs = assertBound(context, varFields, cond.rhs.name)
|
|
955
|
-
if (!fieldJoins(lhs, rhs)) {
|
|
956
|
-
throw errors.new(
|
|
957
|
-
`${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)`
|
|
958
|
-
)
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
return
|
|
871
|
+
function findColumnSlotOf(context: ChainContext, column: FindColumn): ClassedField | undefined {
|
|
872
|
+
const entry = column.entry
|
|
873
|
+
if (entry.kind === "var") {
|
|
874
|
+
return mintSlotOf(context, entry.over)
|
|
962
875
|
}
|
|
963
|
-
|
|
964
|
-
|
|
876
|
+
if (entry.kind === "aggregate" && entry.agg.op === "arg") {
|
|
877
|
+
return mintSlotOf(context, entry.agg.over)
|
|
965
878
|
}
|
|
879
|
+
return undefined
|
|
966
880
|
}
|
|
967
881
|
|
|
968
|
-
/** Validates one
|
|
969
|
-
function validateColumn(
|
|
970
|
-
context
|
|
971
|
-
varFields: Readonly<Record<string, ClassedField>>,
|
|
972
|
-
column: SelectColumn
|
|
973
|
-
): void {
|
|
882
|
+
/** Validates one find column's variable references (boundness + the orderable/interval walls, off the var's own field). */
|
|
883
|
+
function validateColumn(context: ChainContext, bound: ReadonlySet<AnyVar>, column: FindColumn): void {
|
|
884
|
+
const where = `${contextLabel(context)} find ${column.name}`
|
|
974
885
|
const entry = column.entry
|
|
975
886
|
if (entry.kind === "var") {
|
|
976
|
-
assertBound(
|
|
887
|
+
assertBound(where, bound, entry.over)
|
|
977
888
|
return
|
|
978
889
|
}
|
|
979
890
|
if (entry.kind === "measure") {
|
|
980
|
-
|
|
891
|
+
assertBound(where, bound, entry.over)
|
|
892
|
+
assertInterval(where, entry.over)
|
|
981
893
|
return
|
|
982
894
|
}
|
|
983
895
|
const agg = entry.agg
|
|
@@ -985,145 +897,163 @@ function validateColumn(
|
|
|
985
897
|
case "count":
|
|
986
898
|
return
|
|
987
899
|
case "countDistinct":
|
|
988
|
-
assertBound(
|
|
900
|
+
assertBound(where, bound, agg.over)
|
|
989
901
|
return
|
|
990
902
|
case "fold": {
|
|
991
|
-
if (
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
if (roster !== undefined) {
|
|
995
|
-
throw closedOrderError(`${context} select ${column.name}`, `the ${agg.fold} input ${agg.over}`, roster.name)
|
|
996
|
-
}
|
|
903
|
+
if ("duration" in agg.over) {
|
|
904
|
+
assertBound(where, bound, agg.over.duration)
|
|
905
|
+
assertInterval(where, agg.over.duration)
|
|
997
906
|
return
|
|
998
907
|
}
|
|
999
|
-
|
|
908
|
+
assertBound(where, bound, agg.over)
|
|
909
|
+
assertNotClosed(where, `the ${agg.fold} input`, agg.over)
|
|
1000
910
|
return
|
|
1001
911
|
}
|
|
1002
912
|
case "arg": {
|
|
1003
|
-
assertBound(
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
if (keyRoster !== undefined) {
|
|
1007
|
-
throw closedOrderError(
|
|
1008
|
-
`${context} select ${column.name}`,
|
|
1009
|
-
`the ${agg.direction} key ${agg.key}`,
|
|
1010
|
-
keyRoster.name
|
|
1011
|
-
)
|
|
1012
|
-
}
|
|
913
|
+
assertBound(where, bound, agg.over)
|
|
914
|
+
assertBound(where, bound, agg.key)
|
|
915
|
+
assertNotClosed(where, `the ${agg.direction} key`, agg.key)
|
|
1013
916
|
return
|
|
1014
917
|
}
|
|
1015
918
|
case "pack":
|
|
1016
|
-
|
|
919
|
+
assertBound(where, bound, agg.over)
|
|
920
|
+
assertInterval(where, agg.over)
|
|
1017
921
|
return
|
|
1018
922
|
}
|
|
1019
923
|
}
|
|
1020
924
|
|
|
1021
925
|
/**
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
* machinery reads), and `decodeAnswers` lifts the column's row ids back to
|
|
1026
|
-
* handle NAMES through it — the runtime twin of the row type's `Infer`
|
|
1027
|
-
* claim. Every other entry decodes bare: counts are counts, the measure
|
|
1028
|
-
* and `pack` are never closed, and a closed FOLD is banned outright
|
|
1029
|
-
* ({@link closedOrderError}) before this resolution runs.
|
|
926
|
+
* Validates one condition's variable references against the rule's bound
|
|
927
|
+
* set — and, for `eq`/`ne` over two variables, holds the class wall through
|
|
928
|
+
* the mint slots (the unification IS a join; bare pairs only with bare).
|
|
1030
929
|
*/
|
|
1031
|
-
function
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
930
|
+
function validateCond(context: ChainContext, bound: ReadonlySet<AnyVar>, cond: CondData): void {
|
|
931
|
+
const label = contextLabel(context)
|
|
932
|
+
if (cond.kind === "cmp") {
|
|
933
|
+
for (const side of [cond.lhs, cond.rhs]) {
|
|
934
|
+
if (side.kind === "var") {
|
|
935
|
+
assertBound(label, bound, side.ref)
|
|
936
|
+
const roster = rosterOf(side.ref.field)
|
|
937
|
+
if (isOrderOp(cond.op) && roster !== undefined) {
|
|
938
|
+
throw closedOrderError(label, `the ${cond.op} side ${side.ref.label}`, roster.name)
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (side.kind === "measure") {
|
|
942
|
+
assertBound(label, bound, side.ref)
|
|
943
|
+
assertInterval(label, side.ref)
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
|
|
947
|
+
assertBound(label, bound, cond.lhs.ref)
|
|
948
|
+
assertBound(label, bound, cond.rhs.ref)
|
|
949
|
+
const lhs = mintSlotOf(context, cond.lhs.ref)
|
|
950
|
+
const rhs = mintSlotOf(context, cond.rhs.ref)
|
|
951
|
+
if (!fieldJoins(lhs, rhs)) {
|
|
952
|
+
throw errors.new(
|
|
953
|
+
`${label}: ${cond.op}(${cond.lhs.ref.label}, ${cond.rhs.ref.label}) unifies domain-unequal fields — ${cond.lhs.ref.label} bound at ${renderFieldKind(lhs)}, ${cond.rhs.ref.label} at ${renderFieldKind(rhs)} (a var joins only class-equal slots; bare pairs only with bare)`
|
|
954
|
+
)
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return
|
|
1042
958
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
959
|
+
for (const child of cond.children) {
|
|
960
|
+
validateCond(context, bound, child)
|
|
1045
961
|
}
|
|
1046
|
-
return rosterOf(varFields[over]?.field)
|
|
1047
962
|
}
|
|
1048
963
|
|
|
1049
964
|
/**
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1053
|
-
*
|
|
1054
|
-
*
|
|
965
|
+
* Validates one `idb` item: every head column of the rec is bound exactly
|
|
966
|
+
* once (a missing or extra key is a pointed error), every bound variable is
|
|
967
|
+
* positively bound by a relation atom of the rule, and each variable joins
|
|
968
|
+
* its head column's classed slot. When the rec's own rule 0 is in flight
|
|
969
|
+
* (`rec.rules[0]` absent), the completing rule's OWN find columns ARE the
|
|
970
|
+
* head.
|
|
1055
971
|
*/
|
|
1056
|
-
function
|
|
1057
|
-
|
|
1058
|
-
|
|
972
|
+
function validateIdb(
|
|
973
|
+
context: ChainContext,
|
|
974
|
+
bound: ReadonlySet<AnyVar>,
|
|
975
|
+
item: { readonly rec: RecData; readonly bindings: ReadonlyArray<{ readonly key: string; readonly ref: AnyVar }> },
|
|
976
|
+
columns: readonly FindColumn[]
|
|
977
|
+
): void {
|
|
978
|
+
const label = contextLabel(context)
|
|
979
|
+
const head = item.rec.rules[0]
|
|
980
|
+
const headColumns = head !== undefined ? head.finds : columns
|
|
981
|
+
const headNames = headColumns.map(function nameOf(column) {
|
|
982
|
+
return column.name
|
|
983
|
+
})
|
|
984
|
+
const keys = item.bindings.map(function keyOf(binding) {
|
|
985
|
+
return binding.key
|
|
986
|
+
})
|
|
987
|
+
for (const key of keys) {
|
|
988
|
+
if (!headNames.includes(key)) {
|
|
989
|
+
throw errors.new(
|
|
990
|
+
`${label}: idb ${item.rec.name} binds ${key}, not a head column of ${item.rec.name} (head columns: ${headNames.join(", ")})`
|
|
991
|
+
)
|
|
992
|
+
}
|
|
1059
993
|
}
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
994
|
+
for (const name of headNames) {
|
|
995
|
+
if (!keys.includes(name)) {
|
|
996
|
+
throw errors.new(
|
|
997
|
+
`${label}: idb ${item.rec.name} omits the head column ${name} — an idb join binds every head column of ${item.rec.name}`
|
|
998
|
+
)
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
for (const binding of item.bindings) {
|
|
1002
|
+
if (!bound.has(binding.ref)) {
|
|
1003
|
+
throw errors.new(
|
|
1004
|
+
`${label}: idb ${item.rec.name} names the variable ${binding.ref.label}, 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`
|
|
1005
|
+
)
|
|
1006
|
+
}
|
|
1007
|
+
const headColumn = headColumns.find(function byName(column) {
|
|
1008
|
+
return column.name === binding.key
|
|
1009
|
+
})
|
|
1010
|
+
if (headColumn === undefined || headColumn.slot === undefined) {
|
|
1011
|
+
continue
|
|
1012
|
+
}
|
|
1013
|
+
const mint = mintSlotOf(context, binding.ref)
|
|
1014
|
+
if (!fieldJoins(headColumn.slot, mint)) {
|
|
1015
|
+
throw errors.new(
|
|
1016
|
+
`${label}: idb ${item.rec.name} joins the variable ${binding.ref.label} (${renderFieldKind(mint)}) at head column ${binding.key} (${renderFieldKind(headColumn.slot)}) — a var joins only class-equal slots; bare pairs only with bare`
|
|
1017
|
+
)
|
|
1065
1018
|
}
|
|
1066
|
-
seen.add(column.name)
|
|
1067
|
-
validateColumn(context, state.varFields, column)
|
|
1068
1019
|
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Completes one rule: enriches the find columns (declaration-order-safe
|
|
1024
|
+
* keys, boundness validated, each column's classed slot and closed slice
|
|
1025
|
+
* resolved), then walks the body walls — negated-atom boundness safety, idb
|
|
1026
|
+
* head pairing, and condition validation.
|
|
1027
|
+
*/
|
|
1028
|
+
function completeRule(context: ChainContext, state: RuleBuildState, rawColumns: readonly FindColumn[]): RuleData {
|
|
1029
|
+
const label = contextLabel(context)
|
|
1030
|
+
if (rawColumns.length === 0) {
|
|
1031
|
+
throw errors.new(`${label}: a find needs at least one entry`)
|
|
1032
|
+
}
|
|
1033
|
+
const columns = rawColumns.map(function enrichColumn(column): FindColumn {
|
|
1034
|
+
assertDeclarationOrderKey(`${label} find column`, column.name)
|
|
1035
|
+
validateColumn(context, state.bound, column)
|
|
1036
|
+
const slot = findColumnSlotOf(context, column)
|
|
1037
|
+
return Object.freeze({ name: column.name, entry: column.entry, slot, closed: rosterOf(slot?.field) })
|
|
1038
|
+
})
|
|
1069
1039
|
for (const item of state.items) {
|
|
1070
1040
|
if (item.kind === "negated") {
|
|
1071
1041
|
for (const binding of item.atom.bindings) {
|
|
1072
|
-
if (binding.term.kind === "var") {
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
`${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)`
|
|
1077
|
-
)
|
|
1078
|
-
}
|
|
1079
|
-
const negatedSlot: ClassedField = { field: binding.data, class: binding.class }
|
|
1080
|
-
if (!fieldJoins(bound, negatedSlot)) {
|
|
1081
|
-
throw errors.new(
|
|
1082
|
-
`${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`
|
|
1083
|
-
)
|
|
1084
|
-
}
|
|
1042
|
+
if (binding.term.kind === "var" && !state.bound.has(binding.term.ref)) {
|
|
1043
|
+
throw errors.new(
|
|
1044
|
+
`${label}: negated ${item.atom.relation.name} atom binds the variable ${binding.term.ref.label} at position ${binding.field}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`
|
|
1045
|
+
)
|
|
1085
1046
|
}
|
|
1086
1047
|
}
|
|
1087
1048
|
}
|
|
1088
1049
|
if (item.kind === "idb") {
|
|
1089
|
-
|
|
1090
|
-
item.vars.forEach(function checkIdbVar(name, position) {
|
|
1091
|
-
const bound = state.varFields[name]
|
|
1092
|
-
if (bound === undefined) {
|
|
1093
|
-
throw errors.new(
|
|
1094
|
-
`${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`
|
|
1095
|
-
)
|
|
1096
|
-
}
|
|
1097
|
-
const column = head?.select[position]
|
|
1098
|
-
if (column === undefined || column.entry.kind !== "var") {
|
|
1099
|
-
return
|
|
1100
|
-
}
|
|
1101
|
-
const headSlot = head?.varFields[column.entry.over]
|
|
1102
|
-
if (headSlot !== undefined && !fieldJoins(headSlot, bound)) {
|
|
1103
|
-
throw errors.new(
|
|
1104
|
-
`${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`
|
|
1105
|
-
)
|
|
1106
|
-
}
|
|
1107
|
-
})
|
|
1050
|
+
validateIdb(context, state.bound, item, columns)
|
|
1108
1051
|
}
|
|
1109
1052
|
if (item.kind === "cond") {
|
|
1110
|
-
validateCond(context, state.
|
|
1053
|
+
validateCond(context, state.bound, item.cond)
|
|
1111
1054
|
}
|
|
1112
1055
|
}
|
|
1113
|
-
return Object.freeze({
|
|
1114
|
-
items: state.items,
|
|
1115
|
-
select: Object.freeze(
|
|
1116
|
-
columns.map(function enrichColumn(column): SelectColumn {
|
|
1117
|
-
return Object.freeze({
|
|
1118
|
-
name: column.name,
|
|
1119
|
-
entry: column.entry,
|
|
1120
|
-
closed: selectClosedOf(state.varFields, column.entry)
|
|
1121
|
-
})
|
|
1122
|
-
})
|
|
1123
|
-
),
|
|
1124
|
-
varFields: state.varFields,
|
|
1125
|
-
paramUses: state.paramUses
|
|
1126
|
-
})
|
|
1056
|
+
return Object.freeze({ items: state.items, finds: Object.freeze(columns), paramUses: state.paramUses })
|
|
1127
1057
|
}
|
|
1128
1058
|
|
|
1129
1059
|
/** Builds one typed rule value over completed rule data. */
|
|
@@ -1132,19 +1062,14 @@ function makeRuleValue<Row, P extends ParamsRecord>(rule: RuleData): RuleValue<R
|
|
|
1132
1062
|
}
|
|
1133
1063
|
|
|
1134
1064
|
/**
|
|
1135
|
-
* The one runtime chain every context shares — non-generic on purpose
|
|
1136
|
-
* typed chain interfaces
|
|
1137
|
-
* apply at the scope factories' boundaries, and the runtime beneath them is
|
|
1138
|
-
* one plain value walk. Context gates the two context-bound verbs: `idb`
|
|
1139
|
-
* (a program construct — self-only inside a rec, any rec of the program in
|
|
1140
|
-
* the output, refused in a plain query) and the recursive `select`
|
|
1141
|
-
* (bound variable names only — the creation quarantine).
|
|
1065
|
+
* The one runtime chain every context shares — non-generic on purpose. The
|
|
1066
|
+
* typed chain interfaces apply at the scope factories' boundaries.
|
|
1142
1067
|
*/
|
|
1143
1068
|
interface RawChain {
|
|
1144
1069
|
match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
|
|
1145
1070
|
where(cond: AnyCond): RawChain
|
|
1146
|
-
idb(target: RecRef<string, ParamsRecord>,
|
|
1147
|
-
|
|
1071
|
+
idb(target: RecRef<string, ParamsRecord>, bindings: Readonly<Record<string, unknown>>): RawChain
|
|
1072
|
+
find(entries: Readonly<Record<string, unknown>>): RuleValue<never, never>
|
|
1148
1073
|
}
|
|
1149
1074
|
|
|
1150
1075
|
/** The runtime rule-builder shape beneath every typed scope. */
|
|
@@ -1152,8 +1077,8 @@ interface RawScope extends TermOps {
|
|
|
1152
1077
|
match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
|
|
1153
1078
|
}
|
|
1154
1079
|
|
|
1155
|
-
/** Which rule family a chain builds —
|
|
1156
|
-
type ChainContext = { readonly classes: SchemaClasses } & (
|
|
1080
|
+
/** Which rule family a chain builds — plus the schema's runtime class map and theory value (the join judge's authority). */
|
|
1081
|
+
type ChainContext = { readonly classes: SchemaClasses; readonly theory: AnySchema } & (
|
|
1157
1082
|
| { readonly kind: "query" }
|
|
1158
1083
|
| { readonly kind: "rec"; readonly self: RecData }
|
|
1159
1084
|
| { readonly kind: "output"; readonly program: ProgramState }
|
|
@@ -1176,7 +1101,7 @@ function idbAdvance(
|
|
|
1176
1101
|
context: ChainContext,
|
|
1177
1102
|
state: RuleBuildState,
|
|
1178
1103
|
target: RecRef<string, ParamsRecord>,
|
|
1179
|
-
|
|
1104
|
+
bindings: Readonly<Record<string, unknown>>
|
|
1180
1105
|
): RuleBuildState {
|
|
1181
1106
|
if (context.kind === "query") {
|
|
1182
1107
|
throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()")
|
|
@@ -1187,42 +1112,47 @@ function idbAdvance(
|
|
|
1187
1112
|
`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)`
|
|
1188
1113
|
)
|
|
1189
1114
|
}
|
|
1190
|
-
return advanceIdb(state, context.self,
|
|
1115
|
+
return advanceIdb(state, context.self, bindings)
|
|
1191
1116
|
}
|
|
1192
1117
|
if (!context.program.recs.includes(target.data)) {
|
|
1193
1118
|
throw errors.new(
|
|
1194
1119
|
`idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`
|
|
1195
1120
|
)
|
|
1196
1121
|
}
|
|
1197
|
-
return advanceIdb(state, target.data,
|
|
1122
|
+
return advanceIdb(state, target.data, bindings)
|
|
1198
1123
|
}
|
|
1199
1124
|
|
|
1200
|
-
/** Classifies one
|
|
1201
|
-
function
|
|
1202
|
-
|
|
1203
|
-
|
|
1125
|
+
/** Classifies one find record per the context (a recursive head projects bound variables only). */
|
|
1126
|
+
function findColumns(context: ChainContext, entries: Readonly<Record<string, unknown>>): FindColumn[] {
|
|
1127
|
+
const columns: FindColumn[] = []
|
|
1128
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
1129
|
+
if (entry === undefined) {
|
|
1130
|
+
continue
|
|
1131
|
+
}
|
|
1132
|
+
if (context.kind === "rec" && !(isTerm(entry) && entry[term] === "var")) {
|
|
1204
1133
|
throw errors.new(
|
|
1205
|
-
`rec ${context.self.name}: a recursive head projects bound
|
|
1134
|
+
`rec ${context.self.name}: a recursive head projects bound variables only — aggregates and the measure read finished sets (the strata judge's quarantine, unwritable here)`
|
|
1206
1135
|
)
|
|
1207
1136
|
}
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1137
|
+
columns.push(findColumnOf(name, entry))
|
|
1138
|
+
}
|
|
1139
|
+
return columns
|
|
1210
1140
|
}
|
|
1211
1141
|
|
|
1212
1142
|
/** Builds one runtime chain (immutably — every step is a fresh chain over fresh state). */
|
|
1213
1143
|
function makeRawChain(context: ChainContext, state: RuleBuildState): RawChain {
|
|
1214
1144
|
const chain: RawChain = {
|
|
1215
1145
|
match(relation, bindings) {
|
|
1216
|
-
return makeRawChain(context, advanceMatch(state, relation, bindings
|
|
1146
|
+
return makeRawChain(context, advanceMatch(context, state, relation, bindings))
|
|
1217
1147
|
},
|
|
1218
1148
|
where(cond) {
|
|
1219
|
-
return makeRawChain(context, advanceWhere(state, cond
|
|
1149
|
+
return makeRawChain(context, advanceWhere(context, state, cond))
|
|
1220
1150
|
},
|
|
1221
|
-
idb(target,
|
|
1222
|
-
return makeRawChain(context, idbAdvance(context, state, target,
|
|
1151
|
+
idb(target, bindings) {
|
|
1152
|
+
return makeRawChain(context, idbAdvance(context, state, target, bindings))
|
|
1223
1153
|
},
|
|
1224
|
-
|
|
1225
|
-
return makeRuleValue<never, never>(completeRule(
|
|
1154
|
+
find(entries) {
|
|
1155
|
+
return makeRuleValue<never, never>(completeRule(context, state, findColumns(context, entries)))
|
|
1226
1156
|
}
|
|
1227
1157
|
}
|
|
1228
1158
|
Object.freeze(chain)
|
|
@@ -1234,7 +1164,7 @@ function makeRawScope(context: ChainContext): RawScope {
|
|
|
1234
1164
|
const scope: RawScope = {
|
|
1235
1165
|
...termOps,
|
|
1236
1166
|
match(relation, bindings) {
|
|
1237
|
-
return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings
|
|
1167
|
+
return makeRawChain(context, advanceMatch(context, EMPTY_RULE, relation, bindings))
|
|
1238
1168
|
}
|
|
1239
1169
|
}
|
|
1240
1170
|
Object.freeze(scope)
|
|
@@ -1243,14 +1173,12 @@ function makeRawScope(context: ChainContext): RawScope {
|
|
|
1243
1173
|
|
|
1244
1174
|
/**
|
|
1245
1175
|
* The rule builders' trusted admission seam — THE home of the
|
|
1246
|
-
* trusted-admission-seam pattern the other mint guards cite
|
|
1247
|
-
*
|
|
1248
|
-
*
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
1251
|
-
*
|
|
1252
|
-
* interfaces themselves; the runtime twin of every one of them is a
|
|
1253
|
-
* construction-time validation in this module.
|
|
1176
|
+
* trusted-admission-seam pattern the other mint guards cite: the raw builder
|
|
1177
|
+
* is one runtime shape for every context, and this guard verifies the
|
|
1178
|
+
* checkable fact — the builder verbs exist — before the value is admitted at
|
|
1179
|
+
* its TYPED face. The type-level judgments (class-equal joins, the recursion
|
|
1180
|
+
* cut) live in the interfaces themselves; boundness is a construction-time
|
|
1181
|
+
* validation in this module (object identity is invisible to the type tier).
|
|
1254
1182
|
*/
|
|
1255
1183
|
function isTypedScope<S>(scope: RawScope): scope is RawScope & S {
|
|
1256
1184
|
return typeof scope.match === "function"
|
|
@@ -1258,9 +1186,9 @@ function isTypedScope<S>(scope: RawScope): scope is RawScope & S {
|
|
|
1258
1186
|
|
|
1259
1187
|
/** Builds one query-rule builder (the typed face of the raw builder). */
|
|
1260
1188
|
function makeQueryRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
|
|
1261
|
-
|
|
1189
|
+
theory: AnySchema
|
|
1262
1190
|
): QueryRuleScope<Rels, Classes> {
|
|
1263
|
-
const raw = makeRawScope({ kind: "query", classes })
|
|
1191
|
+
const raw = makeRawScope({ kind: "query", classes: theory.classes, theory })
|
|
1264
1192
|
if (!isTypedScope<QueryRuleScope<Rels, Classes>>(raw)) {
|
|
1265
1193
|
throw errors.new("query rule builder construction incomplete")
|
|
1266
1194
|
}
|
|
@@ -1271,17 +1199,18 @@ function makeQueryRuleScope<Rels extends SchemaRelations, Classes extends Schema
|
|
|
1271
1199
|
function makeOutputRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
|
|
1272
1200
|
program: ProgramState
|
|
1273
1201
|
): OutputRuleScope<Rels, Classes> {
|
|
1274
|
-
const raw = makeRawScope({ kind: "output", program, classes: program.classes })
|
|
1202
|
+
const raw = makeRawScope({ kind: "output", program, classes: program.classes, theory: program.theory })
|
|
1275
1203
|
if (!isTypedScope<OutputRuleScope<Rels, Classes>>(raw)) {
|
|
1276
1204
|
throw errors.new("program output rule builder construction incomplete")
|
|
1277
1205
|
}
|
|
1278
1206
|
return raw
|
|
1279
1207
|
}
|
|
1280
1208
|
|
|
1281
|
-
/** One program's build-time registry: its recs in declaration order
|
|
1209
|
+
/** One program's build-time registry: its recs in declaration order, the theory value, and its class map. */
|
|
1282
1210
|
interface ProgramState {
|
|
1283
1211
|
readonly recs: RecData[]
|
|
1284
1212
|
readonly classes: SchemaClasses
|
|
1213
|
+
readonly theory: AnySchema
|
|
1285
1214
|
sealed: boolean
|
|
1286
1215
|
}
|
|
1287
1216
|
|
|
@@ -1291,7 +1220,7 @@ function renderClosedSlice(closed: ClosedRoster | undefined): string {
|
|
|
1291
1220
|
}
|
|
1292
1221
|
|
|
1293
1222
|
/** Renders one head column's signature for the rule-alignment check. */
|
|
1294
|
-
function headSignature(column:
|
|
1223
|
+
function headSignature(column: FindColumn): string {
|
|
1295
1224
|
const entry = column.entry
|
|
1296
1225
|
if (entry.kind === "var" || entry.kind === "measure") {
|
|
1297
1226
|
return `${column.name}:var`
|
|
@@ -1306,26 +1235,7 @@ function headSignature(column: SelectColumn): string {
|
|
|
1306
1235
|
return `${column.name}:${agg.op}`
|
|
1307
1236
|
}
|
|
1308
1237
|
|
|
1309
|
-
/**
|
|
1310
|
-
* The classed slot one answer column's VALUES flow from, resolved through
|
|
1311
|
-
* the rule's own binding environment: a projected var's first-binding slot,
|
|
1312
|
-
* or an Arg-carried payload's (`argMax`/`argMin` carry `over` verbatim —
|
|
1313
|
-
* the same two shapes the closed slice lifts). Counts, folds, `pack` and
|
|
1314
|
-
* the measure derive numbers/intervals rather than carrying a slot's ids,
|
|
1315
|
-
* so they resolve no slot (`undefined`).
|
|
1316
|
-
*/
|
|
1317
|
-
function headSlotOf(rule: RuleData, column: SelectColumn): ClassedField | undefined {
|
|
1318
|
-
const entry = column.entry
|
|
1319
|
-
if (entry.kind === "var") {
|
|
1320
|
-
return rule.varFields[entry.over]
|
|
1321
|
-
}
|
|
1322
|
-
if (entry.kind === "aggregate" && entry.agg.op === "arg") {
|
|
1323
|
-
return rule.varFields[entry.agg.over]
|
|
1324
|
-
}
|
|
1325
|
-
return undefined
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
/** The roster a param anchor carries: present exactly on a closed-reference field anchor (rides THE one `rosterOf` reader). */
|
|
1238
|
+
/** The roster a param anchor carries: present exactly on a closed-reference field anchor. */
|
|
1329
1239
|
function anchorRosterOf(anchor: AnyField | "measure" | undefined): ClosedRoster | undefined {
|
|
1330
1240
|
return anchor === "measure" ? undefined : rosterOf(anchor)
|
|
1331
1241
|
}
|
|
@@ -1337,20 +1247,9 @@ function renderParamAnchor(roster: ClosedRoster | undefined): string {
|
|
|
1337
1247
|
|
|
1338
1248
|
/**
|
|
1339
1249
|
* Folds every rule's param uses (recs in declaration order first, output
|
|
1340
|
-
* rules last — exactly the lowering walk) into the query's registry:
|
|
1341
|
-
*
|
|
1342
|
-
*
|
|
1343
|
-
* anchored use of one name must agree on the roster (value identity), so a
|
|
1344
|
-
* param anchored at a closed reference is GUARANTEED to ride the one
|
|
1345
|
-
* roster-verification point (`taggedHandleId`) at execute; a name anchored
|
|
1346
|
-
* both at a closed reference and at a non-closed position (or at two
|
|
1347
|
-
* vocabularies) is refused here, because the wire would translate only the
|
|
1348
|
-
* first anchor's reading (the type tier intersects the uses to `never`;
|
|
1349
|
-
* this is its runtime twin for untyped callers). A param whose anchor is a
|
|
1350
|
-
* CLOSED reference must never sit in an order-comparison position — the
|
|
1351
|
-
* anchor types its value a handle name and the engine would order the
|
|
1352
|
-
* translated row ids, so the pairing is refused here too (the registry is
|
|
1353
|
-
* the one place a name's every use and its anchoring field meet).
|
|
1250
|
+
* rules last — exactly the lowering walk) into the query's registry: first
|
|
1251
|
+
* use mints the dense `ParamId`, the first FIELD-ANCHORED use types the
|
|
1252
|
+
* wire, and one name keeps one shape AND one closedness.
|
|
1354
1253
|
*/
|
|
1355
1254
|
function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]): readonly ParamEntry[] {
|
|
1356
1255
|
const order: string[] = []
|
|
@@ -1439,49 +1338,43 @@ interface RawQuery {
|
|
|
1439
1338
|
/**
|
|
1440
1339
|
* Assembles the runtime query value over completed rules: every rule must
|
|
1441
1340
|
* derive the SAME head (name and aggregate shape, position for position —
|
|
1442
|
-
* the decode labels and the engine's alignment rule agree
|
|
1443
|
-
*
|
|
1341
|
+
* the decode labels and the engine's alignment rule agree), and the param
|
|
1342
|
+
* registry folds in program-walk order.
|
|
1444
1343
|
*/
|
|
1445
1344
|
function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readonly RuleData[]): RawQuery {
|
|
1446
1345
|
const first = rules[0]
|
|
1447
1346
|
if (first === undefined) {
|
|
1448
1347
|
throw errors.new("a query needs at least one rule")
|
|
1449
1348
|
}
|
|
1450
|
-
const signature = first.
|
|
1349
|
+
const signature = first.finds.map(headSignature).join(", ")
|
|
1451
1350
|
rules.forEach(function verifyHead(rule, index) {
|
|
1452
|
-
const candidate = rule.
|
|
1351
|
+
const candidate = rule.finds.map(headSignature).join(", ")
|
|
1453
1352
|
if (candidate !== signature) {
|
|
1454
1353
|
throw errors.new(
|
|
1455
|
-
`every rule of a query derives the same head — rule 0
|
|
1354
|
+
`every rule of a query derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`
|
|
1456
1355
|
)
|
|
1457
1356
|
}
|
|
1458
1357
|
// The closed slice is part of the head too: one answer column decodes
|
|
1459
|
-
// through one roster, so a union whose rules bind a column at
|
|
1460
|
-
//
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
rule.select.forEach(function verifyClosedSlice(column, position) {
|
|
1464
|
-
const lead = first.select[position]
|
|
1358
|
+
// through one roster, so a union whose rules bind a column at different
|
|
1359
|
+
// vocabularies (or one closed, one bare) is refused pointed.
|
|
1360
|
+
rule.finds.forEach(function verifyClosedSlice(column, position) {
|
|
1361
|
+
const lead = first.finds[position]
|
|
1465
1362
|
if (lead !== undefined && column.closed !== lead.closed) {
|
|
1466
1363
|
throw errors.new(
|
|
1467
1364
|
`every rule of a query derives the same head — the answer column ${lead.name} is ${renderClosedSlice(lead.closed)} in rule 0 but ${renderClosedSlice(column.closed)} in rule ${index} (one column decodes through one roster)`
|
|
1468
1365
|
)
|
|
1469
1366
|
}
|
|
1470
1367
|
// The law-class wall on the union head: one answer column is one
|
|
1471
|
-
// value space, so the classed slot each rule binds the column
|
|
1472
|
-
// must join across rules — the SAME fieldJoins judgment every
|
|
1473
|
-
// join/eq/negated-atom position enforces
|
|
1474
|
-
//
|
|
1475
|
-
// backstop it, and without it a union mixes (say) Holder ids and
|
|
1476
|
-
// Account ids in one column the consumer reads as one id space.
|
|
1368
|
+
// value space, so the classed mint slot each rule binds the column
|
|
1369
|
+
// at must join across rules — the SAME fieldJoins judgment every
|
|
1370
|
+
// join/eq/negated-atom position enforces (the SDK holds it because
|
|
1371
|
+
// the wire IR carries no domains).
|
|
1477
1372
|
if (lead === undefined) {
|
|
1478
1373
|
return
|
|
1479
1374
|
}
|
|
1480
|
-
|
|
1481
|
-
const slot = headSlotOf(rule, column)
|
|
1482
|
-
if (leadSlot !== undefined && slot !== undefined && !fieldJoins(leadSlot, slot)) {
|
|
1375
|
+
if (lead.slot !== undefined && column.slot !== undefined && !fieldJoins(lead.slot, column.slot)) {
|
|
1483
1376
|
throw errors.new(
|
|
1484
|
-
`every rule of a query derives the same head — the answer column ${lead.name} unions domain-unequal fields: bound at ${renderFieldKind(
|
|
1377
|
+
`every rule of a query derives the same head — the answer column ${lead.name} unions domain-unequal fields: bound at ${renderFieldKind(lead.slot)} in rule 0 but at ${renderFieldKind(column.slot)} in rule ${index} (a union column joins only class-equal slots; bare pairs only with bare)`
|
|
1485
1378
|
)
|
|
1486
1379
|
}
|
|
1487
1380
|
})
|
|
@@ -1489,14 +1382,14 @@ function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readon
|
|
|
1489
1382
|
const data: QueryData = Object.freeze({
|
|
1490
1383
|
recs: Object.freeze([...recs]),
|
|
1491
1384
|
rules: Object.freeze([...rules]),
|
|
1492
|
-
|
|
1385
|
+
finds: first.finds,
|
|
1493
1386
|
params: paramRegistryOf(recs, rules)
|
|
1494
1387
|
})
|
|
1495
1388
|
const value: RawQuery = {
|
|
1496
1389
|
schema: theory,
|
|
1497
1390
|
data,
|
|
1498
1391
|
rule(build) {
|
|
1499
|
-
const built = build(makeRawScope({ kind: "query", classes: theory.classes }))
|
|
1392
|
+
const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory }))
|
|
1500
1393
|
return makeRawQuery(theory, recs, [...rules, built.rule])
|
|
1501
1394
|
}
|
|
1502
1395
|
}
|
|
@@ -1530,11 +1423,10 @@ function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord, Cl
|
|
|
1530
1423
|
}
|
|
1531
1424
|
|
|
1532
1425
|
/**
|
|
1533
|
-
* Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule`
|
|
1534
|
-
*
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1537
|
-
* compare class names off it, at the type level and at construction alike.
|
|
1426
|
+
* Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule` adds
|
|
1427
|
+
* one conjunctive rule; multiple rules are the set union. The schema's
|
|
1428
|
+
* law-computed class map and theory value ride into every rule builder — the
|
|
1429
|
+
* join walls compare against the mint slots off it.
|
|
1538
1430
|
*/
|
|
1539
1431
|
function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
|
|
1540
1432
|
theory: Schema<Rels, Classes>
|
|
@@ -1543,7 +1435,7 @@ function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
|
|
|
1543
1435
|
rule<RV extends AnyRuleValue>(
|
|
1544
1436
|
build: (r: QueryRuleScope<Rels, Classes>) => RV
|
|
1545
1437
|
): Query<Rels, RowOf<RV>, ParamsOf<RV>, Classes> {
|
|
1546
|
-
const built = build(makeQueryRuleScope<Rels, Classes>(theory
|
|
1438
|
+
const built = build(makeQueryRuleScope<Rels, Classes>(theory))
|
|
1547
1439
|
return makeQuery<Rels, RowOf<RV>, ParamsOf<RV>, Classes>(theory, [], [built.rule])
|
|
1548
1440
|
}
|
|
1549
1441
|
}
|
|
@@ -1553,13 +1445,8 @@ function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
|
|
|
1553
1445
|
|
|
1554
1446
|
/**
|
|
1555
1447
|
* Tags one closed-reference literal: the handle NAME, verified against the
|
|
1556
|
-
* roster
|
|
1557
|
-
*
|
|
1558
|
-
* declaration-order row id, tagged u64 — queries cross ids, never handle
|
|
1559
|
-
* names; the wire is untouched. THE single roster-verification point of
|
|
1560
|
-
* the query surface: atom-binding literals, comparison literals,
|
|
1561
|
-
* execute-time params, and membership-array members all reach it (never
|
|
1562
|
-
* duplicate the check per call site).
|
|
1448
|
+
* roster and translated to its declaration-order row id, tagged u64. THE
|
|
1449
|
+
* single roster-verification point of the query surface.
|
|
1563
1450
|
*/
|
|
1564
1451
|
function taggedHandleId(
|
|
1565
1452
|
context: string,
|
|
@@ -1601,11 +1488,7 @@ function taggedAtElementDomain(context: string, element: "u64" | "i64", value: u
|
|
|
1601
1488
|
|
|
1602
1489
|
/**
|
|
1603
1490
|
* Tags one host literal at a FIELD position (atom bindings): the field's
|
|
1604
|
-
* structural kind directs the tag, never a guess.
|
|
1605
|
-
* bigint literal tags as the ELEMENT type — the IR's membership typing
|
|
1606
|
-
* rule (point membership), an interval-shaped literal as the interval
|
|
1607
|
-
* (value equality). A closed-reference literal is its bare handle id,
|
|
1608
|
-
* tagged u64 after a roster verification.
|
|
1491
|
+
* structural kind directs the tag, never a guess.
|
|
1609
1492
|
*/
|
|
1610
1493
|
function taggedLiteral(context: string, field: AnyField, value: unknown): TaggedValue {
|
|
1611
1494
|
const roster = rosterOf(field)
|
|
@@ -1635,15 +1518,6 @@ function taggedLiteral(context: string, field: AnyField, value: unknown): Tagged
|
|
|
1635
1518
|
if (typeof value !== "string") {
|
|
1636
1519
|
throw literalShapeError(context, "string", value)
|
|
1637
1520
|
}
|
|
1638
|
-
/**
|
|
1639
|
-
* The marshal's bijection law at the query seam (`marshal.ts`
|
|
1640
|
-
* cellOf): a lone surrogate would be lossily replaced with
|
|
1641
|
-
* U+FFFD at the bridge's UTF-8 crossing and silently match a
|
|
1642
|
-
* fact the typed write surface can never store — distinct JS
|
|
1643
|
-
* strings collapsing to one wire query. This is the single
|
|
1644
|
-
* seam every query string literal, string param
|
|
1645
|
-
* (`taggedCmpLiteral`), and membership member lowers through.
|
|
1646
|
-
*/
|
|
1647
1521
|
if (!value.isWellFormed()) {
|
|
1648
1522
|
throw literalShapeError(context, "well-formed string", value)
|
|
1649
1523
|
}
|
|
@@ -1661,17 +1535,13 @@ function taggedLiteral(context: string, field: AnyField, value: unknown): Tagged
|
|
|
1661
1535
|
}
|
|
1662
1536
|
|
|
1663
1537
|
/**
|
|
1664
|
-
* Tags one host literal at a COMPARISON or PARAM position, where the
|
|
1665
|
-
*
|
|
1666
|
-
*
|
|
1667
|
-
* `pointIn`
|
|
1668
|
-
*
|
|
1669
|
-
*
|
|
1670
|
-
*
|
|
1671
|
-
* `pointIn(t, span(...))` and tags as the interval of the sibling's
|
|
1672
|
-
* element domain; under every other operator an interval shape against a
|
|
1673
|
-
* scalar sibling stays refused (the engine's IllegalComparison — the
|
|
1674
|
-
* bug-hunt fix, preserved op-aware).
|
|
1538
|
+
* Tags one host literal at a COMPARISON or PARAM position, where the SIBLING
|
|
1539
|
+
* anchors the type: a measure sibling is u64, an interval-field sibling
|
|
1540
|
+
* contributes its element domain, a scalar sibling its own type. At
|
|
1541
|
+
* `pointIn` the operand order is interval-left, point-right, so an
|
|
1542
|
+
* interval-shaped literal beside a scalar element-typed sibling is the LEGAL
|
|
1543
|
+
* interval operand of `pointIn(t, span(...))`; under every other operator an
|
|
1544
|
+
* interval shape against a scalar sibling stays refused.
|
|
1675
1545
|
*/
|
|
1676
1546
|
function taggedCmpLiteral(
|
|
1677
1547
|
context: string,
|
|
@@ -1708,22 +1578,22 @@ interface LowerContext {
|
|
|
1708
1578
|
readonly params: ReadonlyMap<string, ParamEntry>
|
|
1709
1579
|
}
|
|
1710
1580
|
|
|
1711
|
-
/** One rule's dense variable numbering: first occurrence in written order. */
|
|
1581
|
+
/** One rule's dense variable numbering: first occurrence in written order, keyed on the object REFERENCE. */
|
|
1712
1582
|
interface VarIds {
|
|
1713
|
-
of(
|
|
1583
|
+
of(ref: AnyVar): number
|
|
1714
1584
|
}
|
|
1715
1585
|
|
|
1716
1586
|
/** Creates one rule-scoped variable numberer. */
|
|
1717
|
-
function
|
|
1718
|
-
const assigned = new Map<
|
|
1587
|
+
function freshVarIds(): VarIds {
|
|
1588
|
+
const assigned = new Map<AnyVar, number>()
|
|
1719
1589
|
return {
|
|
1720
|
-
of(
|
|
1721
|
-
const existing = assigned.get(
|
|
1590
|
+
of(ref) {
|
|
1591
|
+
const existing = assigned.get(ref)
|
|
1722
1592
|
if (existing !== undefined) {
|
|
1723
1593
|
return existing
|
|
1724
1594
|
}
|
|
1725
1595
|
const id = assigned.size
|
|
1726
|
-
assigned.set(
|
|
1596
|
+
assigned.set(ref, id)
|
|
1727
1597
|
return id
|
|
1728
1598
|
}
|
|
1729
1599
|
}
|
|
@@ -1740,10 +1610,7 @@ function paramIdOf(ctx: LowerContext, name: string): number {
|
|
|
1740
1610
|
|
|
1741
1611
|
/**
|
|
1742
1612
|
* Lowers one EDB atom (either polarity). A CLOSED owner lowers through the
|
|
1743
|
-
* same edb source
|
|
1744
|
-
* an ordinary relation's — with field ordinals over the SEALED shape: `id`
|
|
1745
|
-
* at 0, each payload column at its declared index + 1 (`sealedFieldsOf`
|
|
1746
|
-
* carries the shift; the lowering golden pins it).
|
|
1613
|
+
* same edb source, with field ordinals over the SEALED shape.
|
|
1747
1614
|
*/
|
|
1748
1615
|
function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
|
|
1749
1616
|
const member = ctx.theory.relations[atom.relation.name]
|
|
@@ -1769,17 +1636,12 @@ function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
|
|
|
1769
1636
|
return { source: { kind: "edb", relation: relationId }, bindings }
|
|
1770
1637
|
}
|
|
1771
1638
|
|
|
1772
|
-
/**
|
|
1773
|
-
* Lowers one binding term. A membership ARRAY (`literalSet`) lowers to the
|
|
1774
|
-
* existing param-set term over its content-addressed registry entry — the
|
|
1775
|
-
* program IR is byte-identical to the same set spelled `r.inSet`; the SDK
|
|
1776
|
-
* supplies the translated member set itself at execute (`wireParams`).
|
|
1777
|
-
*/
|
|
1639
|
+
/** Lowers one binding term. A membership ARRAY lowers to the existing param-set term over its content-addressed entry. */
|
|
1778
1640
|
function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEntry, ids: VarIds): TermIr {
|
|
1779
1641
|
const bound = binding.term
|
|
1780
1642
|
switch (bound.kind) {
|
|
1781
1643
|
case "var":
|
|
1782
|
-
return { kind: "var", var: ids.of(bound.
|
|
1644
|
+
return { kind: "var", var: ids.of(bound.ref) }
|
|
1783
1645
|
case "param":
|
|
1784
1646
|
return { kind: "param", param: paramIdOf(ctx, bound.name) }
|
|
1785
1647
|
case "setParam":
|
|
@@ -1791,42 +1653,51 @@ function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEn
|
|
|
1791
1653
|
}
|
|
1792
1654
|
}
|
|
1793
1655
|
|
|
1794
|
-
/**
|
|
1795
|
-
|
|
1656
|
+
/**
|
|
1657
|
+
* Lowers one idb atom: named bindings placed by HEAD order, `FieldId(i)` =
|
|
1658
|
+
* head position i. Every head column of the rec must be bound (a missing key
|
|
1659
|
+
* is refused pointed); the var-id assignment order is head order, so the
|
|
1660
|
+
* first-use numbering matches the name-keyed edition exactly.
|
|
1661
|
+
*/
|
|
1662
|
+
function lowerIdbAtom(
|
|
1663
|
+
ctx: LowerContext,
|
|
1664
|
+
rec: RecData,
|
|
1665
|
+
bindings: ReadonlyArray<{ readonly key: string; readonly ref: AnyVar }>,
|
|
1666
|
+
ids: VarIds
|
|
1667
|
+
): AtomIr {
|
|
1796
1668
|
const pred = ctx.recIds.get(rec)
|
|
1797
1669
|
if (pred === undefined) {
|
|
1798
1670
|
throw errors.new(`query lowering: rec ${rec.name} was declared by a different program`)
|
|
1799
1671
|
}
|
|
1800
|
-
const
|
|
1801
|
-
if (
|
|
1802
|
-
throw errors.new(`query lowering:
|
|
1672
|
+
const head = rec.rules[0]
|
|
1673
|
+
if (head === undefined) {
|
|
1674
|
+
throw errors.new(`query lowering: rec ${rec.name} has no rules`)
|
|
1803
1675
|
}
|
|
1804
|
-
const
|
|
1805
|
-
|
|
1676
|
+
const irBindings: Array<readonly [number, TermIr]> = head.finds.map(function lowerPosition(column, position) {
|
|
1677
|
+
const binding = bindings.find(function byKey(candidate) {
|
|
1678
|
+
return candidate.key === column.name
|
|
1679
|
+
})
|
|
1680
|
+
if (binding === undefined) {
|
|
1681
|
+
throw errors.new(`query lowering: idb ${rec.name} omits head column ${column.name}`)
|
|
1682
|
+
}
|
|
1683
|
+
return [position, { kind: "var", var: ids.of(binding.ref) } as const] as const
|
|
1806
1684
|
})
|
|
1807
|
-
return { source: { kind: "idb", pred }, bindings }
|
|
1685
|
+
return { source: { kind: "idb", pred }, bindings: irBindings }
|
|
1808
1686
|
}
|
|
1809
1687
|
|
|
1810
1688
|
/** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
|
|
1811
|
-
function lowerCmpTerm(
|
|
1812
|
-
ctx: LowerContext,
|
|
1813
|
-
rule: RuleData,
|
|
1814
|
-
side: CmpTermData,
|
|
1815
|
-
sibling: CmpTermData,
|
|
1816
|
-
ids: VarIds,
|
|
1817
|
-
op: CmpKind
|
|
1818
|
-
): TermIr {
|
|
1689
|
+
function lowerCmpTerm(ctx: LowerContext, side: CmpTermData, sibling: CmpTermData, ids: VarIds, op: CmpKind): TermIr {
|
|
1819
1690
|
switch (side.kind) {
|
|
1820
1691
|
case "var":
|
|
1821
|
-
return { kind: "var", var: ids.of(side.
|
|
1692
|
+
return { kind: "var", var: ids.of(side.ref) }
|
|
1822
1693
|
case "param":
|
|
1823
1694
|
return { kind: "param", param: paramIdOf(ctx, side.name) }
|
|
1824
1695
|
case "setParam":
|
|
1825
1696
|
return { kind: "paramSet", param: paramIdOf(ctx, side.name) }
|
|
1826
1697
|
case "measure":
|
|
1827
|
-
return { kind: "measure", var: ids.of(side.
|
|
1698
|
+
return { kind: "measure", var: ids.of(side.ref) }
|
|
1828
1699
|
case "literal": {
|
|
1829
|
-
const anchor = cmpAnchorOf(ctx,
|
|
1700
|
+
const anchor = cmpAnchorOf(ctx, sibling)
|
|
1830
1701
|
if (anchor === undefined) {
|
|
1831
1702
|
throw errors.new(
|
|
1832
1703
|
"query lowering: a comparison literal needs a bound-variable, measure, or anchored-param sibling to type it"
|
|
@@ -1837,10 +1708,10 @@ function lowerCmpTerm(
|
|
|
1837
1708
|
}
|
|
1838
1709
|
}
|
|
1839
1710
|
|
|
1840
|
-
/** Resolves the anchor a comparison literal tags by: the sibling's field, the measure, or an anchored param. */
|
|
1841
|
-
function cmpAnchorOf(ctx: LowerContext,
|
|
1711
|
+
/** Resolves the anchor a comparison literal tags by: the sibling variable's field, the measure, or an anchored param. */
|
|
1712
|
+
function cmpAnchorOf(ctx: LowerContext, sibling: CmpTermData): AnyField | "measure" | undefined {
|
|
1842
1713
|
if (sibling.kind === "var") {
|
|
1843
|
-
return
|
|
1714
|
+
return sibling.ref.field
|
|
1844
1715
|
}
|
|
1845
1716
|
if (sibling.kind === "measure") {
|
|
1846
1717
|
return "measure"
|
|
@@ -1852,7 +1723,7 @@ function cmpAnchorOf(ctx: LowerContext, rule: RuleData, sibling: CmpTermData): A
|
|
|
1852
1723
|
}
|
|
1853
1724
|
|
|
1854
1725
|
/** Lowers one comparison. */
|
|
1855
|
-
function lowerComparison(ctx: LowerContext,
|
|
1726
|
+
function lowerComparison(ctx: LowerContext, cmp: CmpData, ids: VarIds): ComparisonIr {
|
|
1856
1727
|
if (cmp.op === "allen") {
|
|
1857
1728
|
const maskData = cmp.mask
|
|
1858
1729
|
if (maskData === undefined) {
|
|
@@ -1864,32 +1735,32 @@ function lowerComparison(ctx: LowerContext, rule: RuleData, cmp: CmpData, ids: V
|
|
|
1864
1735
|
: { kind: "param" as const, param: paramIdOf(ctx, maskData.name) }
|
|
1865
1736
|
return {
|
|
1866
1737
|
op: { kind: "allen", mask },
|
|
1867
|
-
lhs: lowerCmpTerm(ctx,
|
|
1868
|
-
rhs: lowerCmpTerm(ctx,
|
|
1738
|
+
lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
|
|
1739
|
+
rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
|
|
1869
1740
|
}
|
|
1870
1741
|
}
|
|
1871
1742
|
return {
|
|
1872
1743
|
op: { kind: cmp.op },
|
|
1873
|
-
lhs: lowerCmpTerm(ctx,
|
|
1874
|
-
rhs: lowerCmpTerm(ctx,
|
|
1744
|
+
lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, cmp.op),
|
|
1745
|
+
rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, cmp.op)
|
|
1875
1746
|
}
|
|
1876
1747
|
}
|
|
1877
1748
|
|
|
1878
1749
|
/** Lowers one condition node (comparison leaf or and/or tree). */
|
|
1879
|
-
function lowerCondition(ctx: LowerContext,
|
|
1750
|
+
function lowerCondition(ctx: LowerContext, cond: CondData, ids: VarIds): ConditionTreeIr {
|
|
1880
1751
|
if (cond.kind === "cmp") {
|
|
1881
|
-
return { kind: "leaf", cmp: lowerComparison(ctx,
|
|
1752
|
+
return { kind: "leaf", cmp: lowerComparison(ctx, cond, ids) }
|
|
1882
1753
|
}
|
|
1883
1754
|
return {
|
|
1884
1755
|
kind: cond.op,
|
|
1885
1756
|
children: cond.children.map(function lowerChild(child) {
|
|
1886
|
-
return lowerCondition(ctx,
|
|
1757
|
+
return lowerCondition(ctx, child, ids)
|
|
1887
1758
|
})
|
|
1888
1759
|
}
|
|
1889
1760
|
}
|
|
1890
1761
|
|
|
1891
|
-
/** Lowers one
|
|
1892
|
-
function lowerFind(entry:
|
|
1762
|
+
/** Lowers one find entry to its per-rule find term. */
|
|
1763
|
+
function lowerFind(entry: FindEntryData, ids: VarIds): FindTermIr {
|
|
1893
1764
|
if (entry.kind === "var") {
|
|
1894
1765
|
return { kind: "var", var: ids.of(entry.over) }
|
|
1895
1766
|
}
|
|
@@ -1903,10 +1774,10 @@ function lowerFind(entry: SelectEntryData, ids: VarIds): FindTermIr {
|
|
|
1903
1774
|
case "countDistinct":
|
|
1904
1775
|
return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) }
|
|
1905
1776
|
case "fold": {
|
|
1906
|
-
if (
|
|
1907
|
-
return { kind: "
|
|
1777
|
+
if ("duration" in agg.over) {
|
|
1778
|
+
return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) }
|
|
1908
1779
|
}
|
|
1909
|
-
return { kind: "
|
|
1780
|
+
return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) }
|
|
1910
1781
|
}
|
|
1911
1782
|
case "arg":
|
|
1912
1783
|
return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) }
|
|
@@ -1931,8 +1802,8 @@ function headOpOf(agg: AggData): HeadOpIr {
|
|
|
1931
1802
|
}
|
|
1932
1803
|
}
|
|
1933
1804
|
|
|
1934
|
-
/** One
|
|
1935
|
-
function headTermOf(column:
|
|
1805
|
+
/** One find entry's var-free head shape. */
|
|
1806
|
+
function headTermOf(column: FindColumn): HeadTermIr {
|
|
1936
1807
|
const entry = column.entry
|
|
1937
1808
|
if (entry.kind === "var" || entry.kind === "measure") {
|
|
1938
1809
|
return { kind: "var" }
|
|
@@ -1942,7 +1813,7 @@ function headTermOf(column: SelectColumn): HeadTermIr {
|
|
|
1942
1813
|
|
|
1943
1814
|
/** Lowers one rule: body walked in written order (var ids by first occurrence), finds last. */
|
|
1944
1815
|
function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
|
|
1945
|
-
const ids =
|
|
1816
|
+
const ids = freshVarIds()
|
|
1946
1817
|
const atoms: AtomIr[] = []
|
|
1947
1818
|
const negated: AtomIr[] = []
|
|
1948
1819
|
const conditions: ConditionTreeIr[] = []
|
|
@@ -1957,17 +1828,17 @@ function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
|
|
|
1957
1828
|
break
|
|
1958
1829
|
}
|
|
1959
1830
|
case "idb": {
|
|
1960
|
-
atoms.push(lowerIdbAtom(ctx, item.rec, item.
|
|
1831
|
+
atoms.push(lowerIdbAtom(ctx, item.rec, item.bindings, ids))
|
|
1961
1832
|
break
|
|
1962
1833
|
}
|
|
1963
1834
|
case "cond": {
|
|
1964
|
-
conditions.push(lowerCondition(ctx,
|
|
1835
|
+
conditions.push(lowerCondition(ctx, item.cond, ids))
|
|
1965
1836
|
break
|
|
1966
1837
|
}
|
|
1967
1838
|
}
|
|
1968
1839
|
}
|
|
1969
1840
|
return {
|
|
1970
|
-
finds: rule.
|
|
1841
|
+
finds: rule.finds.map(function findOf(column) {
|
|
1971
1842
|
return lowerFind(column.entry, ids)
|
|
1972
1843
|
}),
|
|
1973
1844
|
atoms,
|
|
@@ -1979,11 +1850,7 @@ function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
|
|
|
1979
1850
|
/**
|
|
1980
1851
|
* Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
|
|
1981
1852
|
* recs in declaration order (`PredId` = index), the output predicate
|
|
1982
|
-
*
|
|
1983
|
-
* the law the engine's own manifest pins; `db.prepare` re-verifies the
|
|
1984
|
-
* alignment against the live manifest before sending. Every registered
|
|
1985
|
-
* param must carry a field anchor by now — an unanchorable param (its
|
|
1986
|
-
* every use beside a literal) is refused here, naming it.
|
|
1853
|
+
* appended last. Every registered param must carry a field anchor by now.
|
|
1987
1854
|
*/
|
|
1988
1855
|
function lowerQuery(q: AnyQuery): ProgramIr {
|
|
1989
1856
|
const theory = q.schema
|
|
@@ -2013,14 +1880,14 @@ function lowerQuery(q: AnyQuery): ProgramIr {
|
|
|
2013
1880
|
throw errors.new(`query lowering: rec ${rec.name} has no rules`)
|
|
2014
1881
|
}
|
|
2015
1882
|
return {
|
|
2016
|
-
head: head.
|
|
1883
|
+
head: head.finds.map(headTermOf),
|
|
2017
1884
|
rules: rec.rules.map(function lowerRecRule(rule) {
|
|
2018
1885
|
return lowerRule(ctx, rule)
|
|
2019
1886
|
})
|
|
2020
1887
|
}
|
|
2021
1888
|
})
|
|
2022
1889
|
predicates.push({
|
|
2023
|
-
head: q.data.
|
|
1890
|
+
head: q.data.finds.map(headTermOf),
|
|
2024
1891
|
rules: q.data.rules.map(function lowerOutputRule(rule) {
|
|
2025
1892
|
return lowerRule(ctx, rule)
|
|
2026
1893
|
})
|
|
@@ -2031,7 +1898,6 @@ function lowerQuery(q: AnyQuery): ProgramIr {
|
|
|
2031
1898
|
export type {
|
|
2032
1899
|
AnyQuery,
|
|
2033
1900
|
AnyRuleValue,
|
|
2034
|
-
HeadFieldsOf,
|
|
2035
1901
|
HeadOf,
|
|
2036
1902
|
HeadShape,
|
|
2037
1903
|
OutputRuleChain,
|