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