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