@bjornpagen/bumbledb 1.1.0 → 1.2.1

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.
Files changed (152) hide show
  1. package/COOKBOOK.md +114 -11
  2. package/README.md +52 -9
  3. package/dist/capacity.d.ts +22 -59
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +66 -13
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/changes.d.ts +26 -3
  8. package/dist/changes.d.ts.map +1 -1
  9. package/dist/changes.js +89 -44
  10. package/dist/changes.js.map +1 -1
  11. package/dist/closed.d.ts +23 -51
  12. package/dist/closed.d.ts.map +1 -1
  13. package/dist/closed.js +110 -168
  14. package/dist/closed.js.map +1 -1
  15. package/dist/codec.d.ts +4 -9
  16. package/dist/codec.d.ts.map +1 -1
  17. package/dist/codec.js +108 -183
  18. package/dist/codec.js.map +1 -1
  19. package/dist/compile.d.ts +16 -16
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +36 -15
  22. package/dist/compile.js.map +1 -1
  23. package/dist/db-native.d.ts +39 -0
  24. package/dist/db-native.d.ts.map +1 -1
  25. package/dist/db-native.js +8 -0
  26. package/dist/db-native.js.map +1 -1
  27. package/dist/db.d.ts +28 -8
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +91 -38
  30. package/dist/db.js.map +1 -1
  31. package/dist/errors.d.ts +11 -3
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +13 -1
  34. package/dist/errors.js.map +1 -1
  35. package/dist/face.d.ts +19 -25
  36. package/dist/face.d.ts.map +1 -1
  37. package/dist/face.js +30 -24
  38. package/dist/face.js.map +1 -1
  39. package/dist/fields.d.ts +13 -26
  40. package/dist/fields.d.ts.map +1 -1
  41. package/dist/fields.js +89 -116
  42. package/dist/fields.js.map +1 -1
  43. package/dist/immutable.d.ts +10 -0
  44. package/dist/immutable.d.ts.map +1 -0
  45. package/dist/immutable.js +75 -0
  46. package/dist/immutable.js.map +1 -0
  47. package/dist/index.d.ts +16 -12
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +5 -3
  50. package/dist/index.js.map +1 -1
  51. package/dist/internal/log.d.ts +2 -0
  52. package/dist/internal/log.d.ts.map +1 -1
  53. package/dist/internal/log.js +2 -0
  54. package/dist/internal/log.js.map +1 -1
  55. package/dist/law.d.ts +13 -11
  56. package/dist/law.d.ts.map +1 -1
  57. package/dist/law.js +1 -1
  58. package/dist/law.js.map +1 -1
  59. package/dist/lower.d.ts.map +1 -1
  60. package/dist/lower.js +11 -6
  61. package/dist/lower.js.map +1 -1
  62. package/dist/native.d.ts +1 -1
  63. package/dist/native.d.ts.map +1 -1
  64. package/dist/native.js.map +1 -1
  65. package/dist/query/atom.d.ts +1 -1
  66. package/dist/query/atom.d.ts.map +1 -1
  67. package/dist/query/atom.js.map +1 -1
  68. package/dist/query/description.d.ts +33 -0
  69. package/dist/query/description.d.ts.map +1 -0
  70. package/dist/query/description.js +302 -0
  71. package/dist/query/description.js.map +1 -0
  72. package/dist/query/find.d.ts +21 -6
  73. package/dist/query/find.d.ts.map +1 -1
  74. package/dist/query/find.js.map +1 -1
  75. package/dist/query/lower.d.ts +75 -10
  76. package/dist/query/lower.d.ts.map +1 -1
  77. package/dist/query/lower.js +132 -163
  78. package/dist/query/lower.js.map +1 -1
  79. package/dist/query/parse-ir.d.ts +7 -2
  80. package/dist/query/parse-ir.d.ts.map +1 -1
  81. package/dist/query/parse-ir.js +240 -53
  82. package/dist/query/parse-ir.js.map +1 -1
  83. package/dist/query/run.d.ts.map +1 -1
  84. package/dist/query/run.js +8 -10
  85. package/dist/query/run.js.map +1 -1
  86. package/dist/query/scope.d.ts +24 -16
  87. package/dist/query/scope.d.ts.map +1 -1
  88. package/dist/query/scope.js +5 -10
  89. package/dist/query/scope.js.map +1 -1
  90. package/dist/relation.d.ts +22 -48
  91. package/dist/relation.d.ts.map +1 -1
  92. package/dist/relation.js +30 -82
  93. package/dist/relation.js.map +1 -1
  94. package/dist/rows.d.ts +7 -4
  95. package/dist/rows.d.ts.map +1 -1
  96. package/dist/rows.js +44 -198
  97. package/dist/rows.js.map +1 -1
  98. package/dist/runtime-errors.d.ts +26 -2
  99. package/dist/runtime-errors.d.ts.map +1 -1
  100. package/dist/runtime-errors.js +43 -3
  101. package/dist/runtime-errors.js.map +1 -1
  102. package/dist/schema.d.ts +9 -1
  103. package/dist/schema.d.ts.map +1 -1
  104. package/dist/schema.js +85 -24
  105. package/dist/schema.js.map +1 -1
  106. package/dist/selection.d.ts +27 -0
  107. package/dist/selection.d.ts.map +1 -0
  108. package/dist/selection.js +104 -0
  109. package/dist/selection.js.map +1 -0
  110. package/dist/shape.d.ts +12 -12
  111. package/dist/shape.d.ts.map +1 -1
  112. package/dist/statements.d.ts +17 -52
  113. package/dist/statements.d.ts.map +1 -1
  114. package/dist/statements.js +92 -111
  115. package/dist/statements.js.map +1 -1
  116. package/dist/values.d.ts +25 -0
  117. package/dist/values.d.ts.map +1 -0
  118. package/dist/values.js +203 -0
  119. package/dist/values.js.map +1 -0
  120. package/pack-provenance.json +3 -3
  121. package/package.json +4 -4
  122. package/src/capacity.ts +93 -80
  123. package/src/changes.ts +183 -54
  124. package/src/closed.ts +161 -314
  125. package/src/codec.ts +116 -190
  126. package/src/compile.ts +56 -32
  127. package/src/db-native.ts +42 -0
  128. package/src/db.ts +134 -52
  129. package/src/errors.ts +16 -1
  130. package/src/face.ts +44 -74
  131. package/src/fields.ts +100 -146
  132. package/src/immutable.ts +76 -0
  133. package/src/index.ts +36 -29
  134. package/src/internal/log.ts +2 -0
  135. package/src/law.ts +21 -18
  136. package/src/lower.ts +13 -10
  137. package/src/native.ts +2 -0
  138. package/src/query/atom.ts +1 -0
  139. package/src/query/description.ts +394 -0
  140. package/src/query/find.ts +20 -7
  141. package/src/query/lower.ts +177 -187
  142. package/src/query/parse-ir.ts +247 -54
  143. package/src/query/run.ts +15 -9
  144. package/src/query/scope.ts +42 -33
  145. package/src/relation.ts +58 -142
  146. package/src/rows.ts +62 -205
  147. package/src/runtime-errors.ts +47 -3
  148. package/src/schema.ts +99 -29
  149. package/src/selection.ts +124 -0
  150. package/src/shape.ts +18 -12
  151. package/src/statements.ts +138 -189
  152. package/src/values.ts +223 -0
package/src/statements.ts CHANGED
@@ -2,69 +2,44 @@ import {
2
2
  type BoundsOnTarget,
3
3
  type CapacityWeight,
4
4
  type CapacityWindow,
5
- isCapacityWeight,
6
- isCapacityWindow,
5
+ capacityWeight,
6
+ capacityWindow,
7
7
  type UnitDimensionBan,
8
8
  unitWeight,
9
9
  type WeightOnSource
10
10
  } from "#capacity.ts"
11
- import { isClosedMember, sealedFieldOf } from "#closed.ts"
11
+ import { isClosedMember, memberDescriptor, sealedFieldOf } from "#closed.ts"
12
12
  import { AuthoringError } from "#errors.ts"
13
- import { type AnyFace, type FaceData, renderFace, type SameArity, type SameShapes } from "#face.ts"
14
- import { type AnyClosedRoster, rosterOf, rostersAgree } from "#fields.ts"
13
+ import { type AnyFace, faceDescriptor, renderFace, type SameArity, type SameShapes } from "#face.ts"
14
+ import { type AnyClosedRoster, assertDeclarationRecord, rosterOf, rostersAgree, signaturesAgree } from "#fields.ts"
15
+ import { descriptorCache } from "#immutable.ts"
15
16
  import type { AnyRelation, RelationFields } from "#relation.ts"
16
17
  import { type CapacityWindowSpec, renderCapacityWindow, renderWeight, type WeightSpec } from "#spec.ts"
18
+ import { arrayValue, recordValue } from "#values.ts"
17
19
 
18
- interface KeyData<R extends AnyRelation, Projection extends readonly string[]> {
20
+ interface KeyStatement<R extends AnyRelation = AnyRelation, Projection extends readonly string[] = readonly string[]> {
19
21
  readonly kind: "key"
20
22
  readonly owner: R
21
23
  readonly projection: Projection
22
24
  }
23
-
24
- interface ContainmentData<Src extends FaceData = FaceData, Tgt extends FaceData = FaceData> {
25
+ interface ContainmentStatement<Src extends AnyFace = AnyFace, Tgt extends AnyFace = AnyFace> {
25
26
  readonly kind: "containment"
26
27
  readonly source: Src
27
28
  readonly target: Tgt
28
29
  }
29
-
30
- interface MirrorsData<Src extends FaceData = FaceData, Tgt extends FaceData = FaceData> {
30
+ interface MirrorsStatement<Src extends AnyFace = AnyFace, Tgt extends AnyFace = AnyFace> {
31
31
  readonly kind: "mirrors"
32
32
  readonly source: Src
33
33
  readonly target: Tgt
34
34
  }
35
-
36
- interface CapacityData<Tgt extends FaceData = FaceData, Src extends FaceData = FaceData> {
35
+ interface CapacityStatement<Tgt extends AnyFace = AnyFace, Src extends AnyFace = AnyFace> {
37
36
  readonly kind: "capacity"
38
37
  readonly target: Tgt
39
- readonly weight: WeightSpec
40
- readonly window: CapacityWindowSpec
38
+ readonly weight: CapacityWeight
39
+ readonly window: CapacityWindow
41
40
  readonly source: Src
42
41
  }
43
-
44
- type StatementData = KeyData<AnyRelation, readonly string[]> | ContainmentData | MirrorsData | CapacityData
45
-
46
- const admitted: unique symbol = Symbol("bumbledb.statement.admitted")
47
-
48
- interface Statement {
49
- readonly data: StatementData
50
- readonly [admitted]: true
51
- }
52
-
53
- function isStatement(value: unknown): value is Statement {
54
- return typeof value === "object" && value !== null && admitted in value
55
- }
56
-
57
- interface ContainedStatement<Src extends FaceData, Tgt extends FaceData> extends Statement {
58
- readonly data: ContainmentData<Src, Tgt> | MirrorsData<Src, Tgt>
59
- }
60
-
61
- interface CapacityStatement<Tgt extends FaceData, Src extends FaceData> extends Statement {
62
- readonly data: CapacityData<Tgt, Src>
63
- }
64
-
65
- interface KeyStatement<R extends AnyRelation, Projection extends readonly string[]> extends Statement {
66
- readonly data: KeyData<R, Projection>
67
- }
42
+ type Statement = KeyStatement | ContainmentStatement | MirrorsStatement | CapacityStatement
68
43
 
69
44
  function renderRosterSide(roster: AnyClosedRoster | undefined): string {
70
45
  return roster === undefined ? "a bare column" : `a ${roster.name} reference`
@@ -79,7 +54,7 @@ function renderRosterSide(roster: AnyClosedRoster | undefined): string {
79
54
  * `Db.create`'s colder engine refusal. The error carries the two faces'
80
55
  * own facts: names, arities, and the rendered statement.
81
56
  */
82
- function assertArityAgreement(source: FaceData, target: FaceData, statement: Statement): void {
57
+ function assertArityAgreement(source: AnyFace, target: AnyFace, statement: Statement): void {
83
58
  if (source.projection.length !== target.projection.length) {
84
59
  throw new AuthoringError({
85
60
  message: `${source.owner.name}(${source.projection.join(", ")}) and ${target.owner.name}(${target.projection.join(", ")}) project ${source.projection.length} vs ${target.projection.length} fields — positional pairing requires both faces to project equally many — ${renderStatement(statement)}`
@@ -87,7 +62,7 @@ function assertArityAgreement(source: FaceData, target: FaceData, statement: Sta
87
62
  }
88
63
  }
89
64
 
90
- function assertRosterAgreement(source: FaceData, target: FaceData, statement: Statement): void {
65
+ function assertRosterAgreement(source: AnyFace, target: AnyFace, statement: Statement): void {
91
66
  source.projection.forEach(function agreeAt(fieldName, position) {
92
67
  const targetField = target.projection[position]
93
68
  if (targetField === undefined) {
@@ -103,79 +78,7 @@ function assertRosterAgreement(source: FaceData, target: FaceData, statement: St
103
78
  })
104
79
  }
105
80
 
106
- /**
107
- * `R(X) -> R` — the FD key form, composite keys as tuples. No selection
108
- * parameter exists (the FD-with-selection shape is unrepresentable, as in
109
- * the grammar), and only ordinary relations are accepted: a closed
110
- * relation's key `R(id) -> R` is materialized by the engine, so an
111
- * explicit one would only ever be a duplicate. Every projected name is
112
- * checked against `R`'s field block in the type, and the tuple is carried
113
- * in the returned value's type ({@link KeyStatement}) — keyed point reads
114
- * through THIS statement are typed field-for-field, descriptors resolvable
115
- * through the owner's schema type. A DUPLICATE field in the projection is
116
- * refused here, at the mint (the engine's `FieldSet` refuses the same
117
- * duplicate at `Db.create`; canonical utterance says the twice-spelled
118
- * field is the once-spelled projection respelled) — without this wall a
119
- * `new Set`-collapsed duplicate could set-match a shorter target
120
- * projection the engine refuses.
121
- */
122
- function key<
123
- R extends AnyRelation,
124
- const Projection extends readonly [keyof RelationFields<R> & string, ...(keyof RelationFields<R> & string)[]]
125
- >(relation: R, fields: Projection): KeyStatement<R, Projection> {
126
- if (isClosedMember(relation)) {
127
- throw new AuthoringError({
128
- message: `key(${relation.name}, ...): closedness already materializes ${relation.name}(id) -> ${relation.name} — an explicit key on a closed relation is rejected as a duplicate`
129
- })
130
- }
131
- const seen = new Set<string>()
132
- for (const fieldName of fields) {
133
- if (seen.has(fieldName)) {
134
- throw new AuthoringError({
135
- message: `key(${relation.name}, ...): the projection spells ${fieldName} twice — write it once (the canonical-utterance law: one meaning, one spelling)`
136
- })
137
- }
138
- seen.add(fieldName)
139
- }
140
- const data: KeyData<R, Projection> = Object.freeze({
141
- kind: "key",
142
- owner: relation,
143
- projection: Object.freeze(fields)
144
- })
145
- return Object.freeze({ data, [admitted]: true as const })
146
- }
147
-
148
- function contained<A extends AnyFace, B extends AnyFace>(
149
- source: A,
150
- target: B & SameArity<A, B> & SameShapes<A, B>
151
- ): ContainedStatement<A["data"], B["data"]> {
152
- const data: ContainmentData<A["data"], B["data"]> = Object.freeze({
153
- kind: "containment",
154
- source: source.data,
155
- target: target.data
156
- })
157
- const statement = Object.freeze({ data, [admitted]: true as const })
158
- assertArityAgreement(data.source, data.target, statement)
159
- assertRosterAgreement(data.source, data.target, statement)
160
- return statement
161
- }
162
-
163
- function mirrors<A extends AnyFace, B extends AnyFace>(
164
- source: A,
165
- target: B & SameArity<A, B> & SameShapes<A, B>
166
- ): ContainedStatement<A["data"], B["data"]> {
167
- const data: MirrorsData<A["data"], B["data"]> = Object.freeze({
168
- kind: "mirrors",
169
- source: source.data,
170
- target: target.data
171
- })
172
- const statement = Object.freeze({ data, [admitted]: true as const })
173
- assertArityAgreement(data.source, data.target, statement)
174
- assertRosterAgreement(data.source, data.target, statement)
175
- return statement
176
- }
177
-
178
- function assertWeightOnSource(weight: WeightSpec, source: FaceData, statement: Statement): void {
81
+ function assertWeightOnSource(weight: WeightSpec, source: AnyFace, statement: Statement): void {
179
82
  if (weight.kind === "unit") {
180
83
  return
181
84
  }
@@ -197,7 +100,7 @@ function assertWeightOnSource(weight: WeightSpec, source: FaceData, statement: S
197
100
  }
198
101
  }
199
102
 
200
- function assertBoundsOnTarget(window: CapacityWindowSpec, target: FaceData, statement: Statement): void {
103
+ function assertBoundsOnTarget(window: CapacityWindowSpec, target: AnyFace, statement: Statement): void {
201
104
  const bounds = window.kind === "range" ? [window.lo, window.hi] : [window.kind === "exact" ? window.n : window.lo]
202
105
  for (const bound of bounds) {
203
106
  if (bound.kind === "lit") {
@@ -222,19 +125,112 @@ function assertBoundsOnTarget(window: CapacityWindowSpec, target: FaceData, stat
222
125
  }
223
126
  }
224
127
 
225
- /**
226
- * The chapter 34 NAMED-OPTIONS capacity call — it replaces the old four
227
- * positional arguments: `capacity(target, { from, weight?, within })`.
228
- * Unit weight is the default; `weigh(...)`/`within(...)` remain the only
229
- * mints for the weight/window values.
230
- */
128
+ function assertShapes(source: AnyFace, target: AnyFace): void {
129
+ for (let i = 0; i < source.projection.length; i++) {
130
+ const a = sealedFieldOf(source.owner, source.projection[i] ?? "")
131
+ const b = sealedFieldOf(target.owner, target.projection[i] ?? "")
132
+ if (a === undefined || b === undefined) throw new AuthoringError({ message: "statement: unknown projected field" })
133
+ const left = a.kind === "interval" ? { ...a, width: undefined } : a
134
+ const right = b.kind === "interval" ? { ...b, width: undefined } : b
135
+ if (!signaturesAgree(left, right))
136
+ throw new AuthoringError({ message: "statement: projected field shapes do not agree" })
137
+ }
138
+ }
139
+
140
+ /** Every authoring path checks and owns the same plain descriptor. */
141
+ function statementDescriptor<S extends Statement>(input: S): S
142
+ function statementDescriptor(input: unknown): Statement
143
+ function statementDescriptor(input: unknown): Statement {
144
+ return checkedStatement(input)
145
+ }
146
+
147
+ const checkedStatement = descriptorCache((raw): Statement => {
148
+ const input = recordValue("statement", raw, typeof raw === "object" && raw !== null ? Object.keys(raw) : [])
149
+ assertDeclarationRecord("statement", input)
150
+ if (input?.kind === "key") {
151
+ recordValue("key", input, ["kind", "owner", "projection"])
152
+ const owner = memberDescriptor(input.owner as unknown)
153
+ if (isClosedMember(owner))
154
+ throw new AuthoringError({
155
+ message: `key(${owner.name}, ...): closedness already materializes its key; explicit keys on closed relations are duplicates`
156
+ })
157
+ const projection = arrayValue("key projection", input.projection, (_, value) => {
158
+ if (typeof value !== "string") throw new AuthoringError({ message: "key: expected a field name" })
159
+ return value
160
+ })
161
+ if (projection.length === 0) throw new AuthoringError({ message: "key: expected a nonempty field projection" })
162
+ const seen = new Set<string>()
163
+ for (const field of projection) {
164
+ if (typeof field !== "string" || sealedFieldOf(owner, field) === undefined)
165
+ throw new AuthoringError({ message: `key(${owner.name}, ...): unknown field ${String(field)}` })
166
+ if (seen.has(field))
167
+ throw new AuthoringError({ message: `key(${owner.name}, ...): the projection spells ${field} twice` })
168
+ seen.add(field)
169
+ }
170
+ return Object.freeze({ kind: "key", owner, projection })
171
+ }
172
+ if (input?.kind !== "containment" && input?.kind !== "mirrors" && input?.kind !== "capacity") {
173
+ throw new AuthoringError({ message: "statement: unknown kind" })
174
+ }
175
+ recordValue(
176
+ "statement",
177
+ input,
178
+ input.kind === "capacity" ? ["kind", "target", "weight", "window", "source"] : ["kind", "source", "target"]
179
+ )
180
+ const source = faceDescriptor(input.source)
181
+ const target = faceDescriptor(input.target)
182
+ const statement =
183
+ input.kind === "capacity"
184
+ ? Object.freeze({
185
+ kind: input.kind,
186
+ source,
187
+ target,
188
+ weight: capacityWeight(input.weight),
189
+ window: capacityWindow(input.window)
190
+ })
191
+ : Object.freeze({ kind: input.kind, source, target })
192
+ assertArityAgreement(source, target, statement)
193
+ assertRosterAgreement(source, target, statement)
194
+ assertShapes(source, target)
195
+ if (statement.kind === "capacity") {
196
+ const { weight, window } = statement
197
+ if (weight.kind === "unit" && window.kind === "range" && window.hi.kind === "durationField") {
198
+ throw new AuthoringError({ message: "a unit (count) window against a duration bound mixes dimensions (C18)" })
199
+ }
200
+ assertWeightOnSource(weight, source, statement)
201
+ assertBoundsOnTarget(window, target, statement)
202
+ }
203
+ return statement
204
+ })
205
+
206
+ function key<
207
+ R extends AnyRelation,
208
+ const Projection extends readonly [keyof RelationFields<R> & string, ...(keyof RelationFields<R> & string)[]]
209
+ >(relation: R, fields: Projection): KeyStatement<R, Projection> {
210
+ return statementDescriptor({ kind: "key", owner: relation, projection: fields })
211
+ }
212
+
213
+ function contained<A extends AnyFace, B extends AnyFace>(
214
+ source: A,
215
+ target: B & SameArity<A, B> & SameShapes<A, B>
216
+ ): ContainmentStatement<A, B> {
217
+ return statementDescriptor({ kind: "containment", source, target })
218
+ }
219
+
220
+ function mirrors<A extends AnyFace, B extends AnyFace>(
221
+ source: A,
222
+ target: B & SameArity<A, B> & SameShapes<A, B>
223
+ ): MirrorsStatement<A, B> {
224
+ return statementDescriptor({ kind: "mirrors", source, target })
225
+ }
226
+
231
227
  function capacity<B extends AnyFace, W extends CapacityWindow, A extends AnyFace>(
232
228
  target: B,
233
229
  options: {
234
230
  readonly from: A & SameArity<B, A> & SameShapes<B, A>
235
231
  readonly within: W & UnitDimensionBan<W> & BoundsOnTarget<W, B>
236
232
  }
237
- ): CapacityStatement<B["data"], A["data"]>
233
+ ): CapacityStatement<B, A>
238
234
  function capacity<B extends AnyFace, M extends CapacityWeight, W extends CapacityWindow, A extends AnyFace>(
239
235
  target: B,
240
236
  options: {
@@ -242,84 +238,37 @@ function capacity<B extends AnyFace, M extends CapacityWeight, W extends Capacit
242
238
  readonly weight: M & WeightOnSource<M, A>
243
239
  readonly within: W & BoundsOnTarget<W, B>
244
240
  }
245
- ): CapacityStatement<B["data"], A["data"]>
241
+ ): CapacityStatement<B, A>
246
242
  function capacity(
247
243
  target: AnyFace,
248
- options: {
249
- readonly from: AnyFace
250
- readonly weight?: unknown
251
- readonly within: unknown
252
- }
253
- ): CapacityStatement<FaceData, FaceData> {
254
- if (typeof options !== "object" || options === null || !("from" in options) || !("within" in options)) {
255
- throw new AuthoringError({
256
- message:
257
- "capacity takes named options — capacity(target, { from, weight?, within }) (chapter 34's one spelling; the positional arguments are deleted)"
258
- })
259
- }
260
- const windowValue = options.within
261
- const source = options.from
262
- if (!isCapacityWindow(windowValue)) {
263
- throw new AuthoringError({
264
- message:
265
- "a capacity window is minted only by within() — a structural literal skips the ban table (the canonical-utterance law)"
266
- })
267
- }
268
- let weight: WeightSpec = unitWeight
269
- if (options.weight !== undefined) {
270
- if (!isCapacityWeight(options.weight)) {
271
- throw new AuthoringError({
272
- message: "a capacity weight is minted only by weigh() — a structural literal skips the row-local weight wall"
273
- })
274
- }
275
- weight = options.weight.weight
276
- }
277
- const window = windowValue.window
278
- // The unit-floor and vacuous-window ban tables are deleted:
279
- // `{1..*}`, `{N..*}` and `{0..*}` are accepted canonical laws now.
280
- // CapacityDimensionMixing remains — genuinely different semantics.
281
- if (weight.kind === "unit" && window.kind === "range" && window.hi.kind === "durationField") {
282
- throw new AuthoringError({
283
- message: `a unit (count) window against the duration() bound on ${window.hi.field} mixes dimensions (C18) — weigh the source with weigh(duration(field)), or bound by a u64 field or literal`
284
- })
285
- }
286
- const data: CapacityData = Object.freeze({
244
+ options: { readonly from: AnyFace; readonly weight?: unknown; readonly within: unknown }
245
+ ): CapacityStatement {
246
+ recordValue(
247
+ "capacity options",
248
+ options,
249
+ Object.hasOwn(options, "weight") ? ["from", "weight", "within"] : ["from", "within"]
250
+ )
251
+ return statementDescriptor({
287
252
  kind: "capacity",
288
- target: target.data,
289
- weight,
290
- window,
291
- source: source.data
253
+ target,
254
+ source: options.from,
255
+ weight: options.weight === undefined ? unitWeight : capacityWeight(options.weight),
256
+ window: capacityWindow(options.within)
292
257
  })
293
- const statement = Object.freeze({ data, [admitted]: true as const })
294
- assertArityAgreement(data.source, data.target, statement)
295
- assertRosterAgreement(data.source, data.target, statement)
296
- assertWeightOnSource(weight, data.source, statement)
297
- assertBoundsOnTarget(window, data.target, statement)
298
- return statement
299
258
  }
300
259
 
301
260
  function renderStatement(statement: Statement): string {
302
- const data = statement.data
303
- switch (data.kind) {
261
+ switch (statement.kind) {
304
262
  case "key":
305
- return `${data.owner.name}(${data.projection.join(", ")}) -> ${data.owner.name}`
263
+ return `${statement.owner.name}(${statement.projection.join(", ")}) -> ${statement.owner.name}`
306
264
  case "containment":
307
- return `${renderFace(data.source)} <= ${renderFace(data.target)}`
265
+ return `${renderFace(statement.source)} <= ${renderFace(statement.target)}`
308
266
  case "mirrors":
309
- return `${renderFace(data.source)} == ${renderFace(data.target)}`
267
+ return `${renderFace(statement.source)} == ${renderFace(statement.target)}`
310
268
  case "capacity":
311
- return `${renderFace(data.target)} <=${renderWeight(data.weight)}${renderCapacityWindow(data.window)} ${renderFace(data.source)}`
269
+ return `${renderFace(statement.target)} <=${renderWeight(statement.weight)}${renderCapacityWindow(statement.window)} ${renderFace(statement.source)}`
312
270
  }
313
271
  }
314
272
 
315
- export type {
316
- CapacityData,
317
- CapacityStatement,
318
- ContainedStatement,
319
- ContainmentData,
320
- KeyData,
321
- KeyStatement,
322
- Statement,
323
- StatementData
324
- }
325
- export { capacity, contained, isStatement, key, mirrors, renderStatement }
273
+ export type { CapacityStatement, ContainmentStatement, KeyStatement, MirrorsStatement, Statement }
274
+ export { capacity, contained, key, mirrors, renderStatement, statementDescriptor }
package/src/values.ts ADDED
@@ -0,0 +1,223 @@
1
+ import { type AuthoringDiagnostic, AuthoringError } from "#errors.ts"
2
+ import type { AnyField, Infer } from "#fields.ts"
3
+ import type { ValueSpec } from "#spec.ts"
4
+ import { Uuid } from "#uuid.ts"
5
+
6
+ const U64_MAX = 0xffffffffffffffffn
7
+ const I64_MIN = -0x8000000000000000n
8
+ const I64_MAX = 0x7fffffffffffffffn
9
+
10
+ function invalid(context: string, expected: string, code: AuthoringDiagnostic["code"] = "InvalidValue"): never {
11
+ throw new AuthoringError({ message: `${context}: expected ${expected}`, diagnostic: { code, context, expected } })
12
+ }
13
+
14
+ /** Validate the complete record shape without reading any field getter. */
15
+ function recordValue(context: string, input: unknown, fields: readonly string[]): Readonly<Record<string, unknown>> {
16
+ if (typeof input !== "object" || input === null) {
17
+ return invalid(context, "a plain record", "InvalidRecord")
18
+ }
19
+ const prototype = Object.getPrototypeOf(input)
20
+ if (prototype !== Object.prototype && prototype !== null) {
21
+ return invalid(context, "a plain record", "InvalidRecord")
22
+ }
23
+ for (const name of Reflect.ownKeys(input)) {
24
+ if (typeof name !== "string" || !fields.includes(name)) {
25
+ return invalid(`${context}.${String(name)}`, "only the declared fields", "UnknownField")
26
+ }
27
+ const property = Object.getOwnPropertyDescriptor(input, name)
28
+ if (property?.enumerable !== true || !("value" in property)) {
29
+ return invalid(`${context}.${name}`, "enumerable own data fields", "InvalidRecord")
30
+ }
31
+ }
32
+ for (const name of fields) {
33
+ if (!Object.hasOwn(input, name)) {
34
+ return invalid(`${context}.${name}`, "a required own field", "MissingField")
35
+ }
36
+ }
37
+ return input as Readonly<Record<string, unknown>>
38
+ }
39
+
40
+ /** Own a dense data array without invoking caller element accessors or methods. */
41
+ function arrayValue<A>(context: string, input: unknown, parse: (context: string, input: unknown) => A): readonly A[] {
42
+ if (!Array.isArray(input)) return invalid(context, "an array", "InvalidRecord")
43
+ for (const name of Reflect.ownKeys(input)) {
44
+ if (name === "length") continue
45
+ if (typeof name !== "string" || !/^(0|[1-9][0-9]*)$/.test(name) || Number(name) >= input.length)
46
+ return invalid(context, "only array elements", "InvalidRecord")
47
+ const property = Object.getOwnPropertyDescriptor(input, name)
48
+ if (property?.enumerable !== true || !("value" in property))
49
+ return invalid(context, "own data elements", "InvalidRecord")
50
+ }
51
+ const result: A[] = []
52
+ for (let index = 0; index < input.length; index++) {
53
+ if (!Object.hasOwn(input, index)) return invalid(`${context}[${index}]`, "a present element", "MissingField")
54
+ result.push(parse(`${context}[${index}]`, input[index]))
55
+ }
56
+ return Object.freeze(result)
57
+ }
58
+
59
+ function integerValue(context: string, kind: "u64" | "i64", input: unknown): bigint {
60
+ if (typeof input !== "bigint") return invalid(context, `${kind} bigint`)
61
+ const minimum = kind === "u64" ? 0n : I64_MIN
62
+ const maximum = kind === "u64" ? U64_MAX : I64_MAX
63
+ if (input < minimum || input > maximum) return invalid(context, `${kind} bigint in range`)
64
+ return input
65
+ }
66
+
67
+ function floatValue(context: string, input: unknown): number {
68
+ if (typeof input !== "number") return invalid(context, "number")
69
+ if (Number.isNaN(input)) return Number.NaN
70
+ return Object.is(input, -0) ? 0 : input
71
+ }
72
+
73
+ /** Read typed-array internal slots through the intrinsic getters, never a
74
+ * caller's shadowed buffer/length properties or custom iterator. */
75
+ function bytesValue(context: string, input: unknown): Uint8Array {
76
+ if (!(input instanceof Uint8Array)) return invalid(context, "Uint8Array")
77
+ const prototype = Object.getPrototypeOf(Uint8Array.prototype)
78
+ const buffer = Reflect.get(prototype, "buffer", input)
79
+ if (!(buffer instanceof ArrayBuffer))
80
+ throw new AuthoringError({
81
+ message: `${context}: SharedArrayBuffer-backed views are refused`,
82
+ diagnostic: { code: "InvalidValue", context, expected: "ArrayBuffer-backed bytes" }
83
+ })
84
+ const offset = Reflect.get(prototype, "byteOffset", input)
85
+ const length = Reflect.get(prototype, "byteLength", input)
86
+ try {
87
+ return new Uint8Array(new Uint8Array(buffer, offset, length))
88
+ } catch {
89
+ return invalid(context, "non-detached bytes")
90
+ }
91
+ }
92
+
93
+ /**
94
+ * The host interpreter of a field descriptor. All ingress paths use the
95
+ * same range, width, ownership and canonicalization rules. Native admission
96
+ * remains authoritative; this interpreter mirrors its value domain.
97
+ */
98
+ function fieldValue<F extends AnyField>(context: string, field: F, input: unknown): Infer<F>
99
+ function fieldValue(context: string, field: AnyField, input: unknown): Infer<AnyField> {
100
+ if ("closed" in field) {
101
+ if (typeof input !== "string" || !field.closed.handles.includes(input)) {
102
+ return invalid(context, `a ${field.closed.name} handle`)
103
+ }
104
+ return input
105
+ }
106
+ switch (field.kind) {
107
+ case "bool":
108
+ return typeof input === "boolean" ? input : invalid(context, "boolean")
109
+ case "str":
110
+ return typeof input === "string" && input.isWellFormed() ? input : invalid(context, "well-formed string")
111
+ case "u64":
112
+ case "i64":
113
+ return integerValue(context, field.kind, input)
114
+ case "f64":
115
+ return floatValue(context, input)
116
+ case "uuid":
117
+ return Uuid.isUuid(input) ? input : invalid(context, "canonical UUID text")
118
+ case "bytes": {
119
+ const owned = bytesValue(context, input)
120
+ if (owned.byteLength !== field.width) return invalid(context, `bytes<${field.width}>`)
121
+ return owned
122
+ }
123
+ case "interval": {
124
+ const record = recordValue(context, input, ["start", "end"])
125
+ if (field.element === "f64") {
126
+ const start = floatValue(`${context}.start`, record.start)
127
+ const end = floatValue(`${context}.end`, record.end)
128
+ if (!(start < end)) return invalid(context, "a nonempty float interval with non-NaN endpoints")
129
+ return Object.freeze({ start, end })
130
+ }
131
+ const start = integerValue(`${context}.start`, field.element, record.start)
132
+ const end = integerValue(`${context}.end`, field.element, record.end)
133
+ if (start >= end) return invalid(context, "a nonempty interval (start < end)")
134
+ const maximumEnd = field.element === "u64" ? U64_MAX : I64_MAX
135
+ if (field.width !== undefined && (end === maximumEnd || end - start !== field.width)) {
136
+ return invalid(context, `a finite interval of width ${field.width}`)
137
+ }
138
+ return Object.freeze({ start, end })
139
+ }
140
+ }
141
+ }
142
+
143
+ /** One checked host value to the native logical scalar description. */
144
+ function taggedValueOf(context: string, field: AnyField, input: unknown): ValueSpec {
145
+ if ("closed" in field) {
146
+ const handle = fieldValue(context, field, input)
147
+ return { kind: "u64", value: BigInt(field.closed.handles.indexOf(handle)) }
148
+ }
149
+ switch (field.kind) {
150
+ case "bool":
151
+ return { kind: "bool", value: fieldValue(context, field, input) }
152
+ case "str":
153
+ return { kind: "string", value: fieldValue(context, field, input) }
154
+ case "u64":
155
+ return { kind: "u64", value: fieldValue(context, field, input) }
156
+ case "i64":
157
+ return { kind: "i64", value: fieldValue(context, field, input) }
158
+ case "f64":
159
+ return { kind: "f64", value: fieldValue(context, field, input) }
160
+ case "uuid":
161
+ return { kind: "uuid", value: fieldValue(context, field, input) }
162
+ case "bytes":
163
+ return { kind: "fixedBytes", value: fieldValue(context, field, input) }
164
+ case "interval": {
165
+ if (field.element === "f64") {
166
+ const value = fieldValue(context, { ...field, element: "f64" }, input)
167
+ return { kind: "intervalF64", ...value }
168
+ }
169
+ const value = fieldValue(context, { ...field, element: field.element }, input)
170
+ return { kind: field.element === "u64" ? "intervalU64" : "intervalI64", ...value }
171
+ }
172
+ }
173
+ }
174
+
175
+ /** Check a generated logical value through the same field interpreter. */
176
+ function valueDescriptor(context: string, input: unknown): ValueSpec {
177
+ if (typeof input !== "object" || input === null) return invalid(context, "a tagged value", "InvalidRecord")
178
+ const tag = recordValue(context, input, Object.keys(input)).kind
179
+ const intervalElement = { intervalU64: "u64", intervalI64: "i64", intervalF64: "f64" } as const
180
+ if (tag === "intervalU64" || tag === "intervalI64" || tag === "intervalF64") {
181
+ const raw = recordValue(context, input, ["kind", "start", "end"])
182
+ return Object.freeze(
183
+ taggedValueOf(
184
+ context,
185
+ { kind: "interval", element: intervalElement[tag], width: undefined },
186
+ { start: raw.start, end: raw.end }
187
+ )
188
+ )
189
+ }
190
+ const raw = recordValue(context, input, ["kind", "value"])
191
+ if (tag === "fixedBytes") {
192
+ const bytes = bytesValue(context, raw.value)
193
+ if (bytes.byteLength < 1 || bytes.byteLength > 64) return invalid(context, "bytes with width 1 through 64")
194
+ return Object.freeze({ kind: "fixedBytes", value: bytes })
195
+ }
196
+ if (tag === "string") return Object.freeze(taggedValueOf(context, { kind: "str" }, raw.value))
197
+ switch (tag) {
198
+ case "bool":
199
+ return Object.freeze(taggedValueOf(context, { kind: tag }, raw.value))
200
+ case "u64":
201
+ return Object.freeze(taggedValueOf(context, { kind: tag }, raw.value))
202
+ case "i64":
203
+ return Object.freeze(taggedValueOf(context, { kind: tag }, raw.value))
204
+ case "f64":
205
+ return Object.freeze(taggedValueOf(context, { kind: tag }, raw.value))
206
+ case "uuid":
207
+ return Object.freeze(taggedValueOf(context, { kind: tag }, raw.value))
208
+ }
209
+ return invalid(context, "a supported value tag", "InvalidDeclaration")
210
+ }
211
+
212
+ export {
213
+ arrayValue,
214
+ bytesValue,
215
+ fieldValue,
216
+ I64_MAX,
217
+ I64_MIN,
218
+ integerValue,
219
+ recordValue,
220
+ taggedValueOf,
221
+ U64_MAX,
222
+ valueDescriptor
223
+ }