@cavelang/solver 0.35.0 → 0.36.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.
- package/README.md +2155 -3
- package/dist/src/adapter.d.ts +6 -0
- package/dist/src/adapter.d.ts.map +1 -1
- package/dist/src/adapter.js +3 -0
- package/dist/src/adapter.js.map +1 -1
- package/dist/src/balanced-reduce.d.ts +3 -0
- package/dist/src/balanced-reduce.d.ts.map +1 -0
- package/dist/src/balanced-reduce.js +10 -0
- package/dist/src/balanced-reduce.js.map +1 -0
- package/dist/src/canonical-owned.d.ts +5 -0
- package/dist/src/canonical-owned.d.ts.map +1 -0
- package/dist/src/canonical-owned.js +206 -0
- package/dist/src/canonical-owned.js.map +1 -0
- package/dist/src/canonical.d.ts +3 -2
- package/dist/src/canonical.d.ts.map +1 -1
- package/dist/src/canonical.js +12 -85
- package/dist/src/canonical.js.map +1 -1
- package/dist/src/capability.d.ts.map +1 -1
- package/dist/src/capability.js +43 -33
- package/dist/src/capability.js.map +1 -1
- package/dist/src/clone.d.ts +3 -0
- package/dist/src/clone.d.ts.map +1 -0
- package/dist/src/clone.js +54 -0
- package/dist/src/clone.js.map +1 -0
- package/dist/src/constant-sign.d.ts +5 -0
- package/dist/src/constant-sign.d.ts.map +1 -0
- package/dist/src/constant-sign.js +54 -0
- package/dist/src/constant-sign.js.map +1 -0
- package/dist/src/context.d.ts +6 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +151 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/diagnostic-text.d.ts +3 -0
- package/dist/src/diagnostic-text.d.ts.map +1 -0
- package/dist/src/diagnostic-text.js +5 -0
- package/dist/src/diagnostic-text.js.map +1 -0
- package/dist/src/evaluate-expression.d.ts +4 -0
- package/dist/src/evaluate-expression.d.ts.map +1 -0
- package/dist/src/evaluate-expression.js +24 -0
- package/dist/src/evaluate-expression.js.map +1 -0
- package/dist/src/exact.d.ts +2 -0
- package/dist/src/exact.d.ts.map +1 -1
- package/dist/src/exact.js +125 -28
- package/dist/src/exact.js.map +1 -1
- package/dist/src/explain.d.ts +4 -0
- package/dist/src/explain.d.ts.map +1 -1
- package/dist/src/explain.js +330 -91
- package/dist/src/explain.js.map +1 -1
- package/dist/src/explanation-budget.d.ts +14 -0
- package/dist/src/explanation-budget.d.ts.map +1 -0
- package/dist/src/explanation-budget.js +46 -0
- package/dist/src/explanation-budget.js.map +1 -0
- package/dist/src/expression-key.d.ts +8 -0
- package/dist/src/expression-key.d.ts.map +1 -0
- package/dist/src/expression-key.js +54 -0
- package/dist/src/expression-key.js.map +1 -0
- package/dist/src/fraction-product.d.ts +3 -0
- package/dist/src/fraction-product.d.ts.map +1 -0
- package/dist/src/fraction-product.js +13 -0
- package/dist/src/fraction-product.js.map +1 -0
- package/dist/src/fraction-sum.d.ts +3 -0
- package/dist/src/fraction-sum.d.ts.map +1 -0
- package/dist/src/fraction-sum.js +23 -0
- package/dist/src/fraction-sum.js.map +1 -0
- package/dist/src/integer-gcd.d.ts +3 -0
- package/dist/src/integer-gcd.d.ts.map +1 -0
- package/dist/src/integer-gcd.js +50 -0
- package/dist/src/integer-gcd.js.map +1 -0
- package/dist/src/linear.d.ts +2 -1
- package/dist/src/linear.d.ts.map +1 -1
- package/dist/src/linear.js +85 -17
- package/dist/src/linear.js.map +1 -1
- package/dist/src/numeric-size.d.ts +2 -0
- package/dist/src/numeric-size.d.ts.map +1 -0
- package/dist/src/numeric-size.js +33 -0
- package/dist/src/numeric-size.js.map +1 -0
- package/dist/src/options.d.ts +4 -0
- package/dist/src/options.d.ts.map +1 -0
- package/dist/src/options.js +18 -0
- package/dist/src/options.js.map +1 -0
- package/dist/src/result.d.ts +4 -0
- package/dist/src/result.d.ts.map +1 -0
- package/dist/src/result.js +29 -0
- package/dist/src/result.js.map +1 -0
- package/dist/src/solve.d.ts.map +1 -1
- package/dist/src/solve.js +34 -3
- package/dist/src/solve.js.map +1 -1
- package/dist/src/validate.d.ts +5 -0
- package/dist/src/validate.d.ts.map +1 -1
- package/dist/src/validate.js +323 -65
- package/dist/src/validate.js.map +1 -1
- package/dist/src/workflow.d.ts.map +1 -1
- package/dist/src/workflow.js +98 -26
- package/dist/src/workflow.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +9 -0
- package/src/balanced-reduce.ts +9 -0
- package/src/canonical-owned.ts +186 -0
- package/src/canonical.ts +14 -90
- package/src/capability.ts +41 -30
- package/src/clone.ts +47 -0
- package/src/constant-sign.ts +59 -0
- package/src/context.ts +114 -0
- package/src/diagnostic-text.ts +4 -0
- package/src/evaluate-expression.ts +21 -0
- package/src/exact.ts +122 -27
- package/src/explain.ts +342 -94
- package/src/explanation-budget.ts +47 -0
- package/src/expression-key.ts +45 -0
- package/src/fraction-product.ts +10 -0
- package/src/fraction-sum.ts +20 -0
- package/src/integer-gcd.ts +47 -0
- package/src/linear.ts +82 -16
- package/src/numeric-size.ts +26 -0
- package/src/options.ts +18 -0
- package/src/result.ts +28 -0
- package/src/solve.ts +34 -3
- package/src/validate.ts +312 -66
- package/src/workflow.ts +92 -26
package/src/explain.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import { ExplanationBudget } from './explanation-budget.ts'
|
|
2
|
+
import { expressionKey } from './expression-key.ts'
|
|
3
|
+
import { constantSign, type Sign } from './constant-sign.ts'
|
|
4
|
+
import { captureResult } from './result.ts'
|
|
5
|
+
import { balancedReduce } from './balanced-reduce.ts'
|
|
6
|
+
import { sum } from './fraction-sum.ts'
|
|
7
|
+
import { product } from './fraction-product.ts'
|
|
8
|
+
import { gcd } from './integer-gcd.ts'
|
|
9
|
+
import { clone } from './clone.ts'
|
|
10
|
+
import { stringifyInput, validateContext } from './context.ts'
|
|
11
|
+
import { mergeLimits } from './validate.ts'
|
|
1
12
|
import type { Backend, Diagnostic, Limits, ObjectiveValue, Result, UnknownReason } from './adapter.ts'
|
|
2
|
-
import
|
|
13
|
+
import { digestOwned } from './canonical-owned.ts'
|
|
3
14
|
import * as Exact from './exact.ts'
|
|
4
15
|
import type {
|
|
5
16
|
Assignment,
|
|
@@ -79,10 +90,14 @@ export type AssignmentValue = Element & {
|
|
|
79
90
|
|
|
80
91
|
export type Constraint = Element & {
|
|
81
92
|
readonly evaluation: 'satisfied' | 'violated' | 'indeterminate'
|
|
93
|
+
/** Human-readable local evaluation failure; absent for determinate evaluations. */
|
|
94
|
+
readonly evaluationReason?: string
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
export type SoftConstraintResult = Element & {
|
|
85
98
|
readonly evaluation: 'accepted' | 'violated' | 'indeterminate'
|
|
99
|
+
/** Human-readable local evaluation failure; absent for determinate evaluations. */
|
|
100
|
+
readonly evaluationReason?: string
|
|
86
101
|
readonly weight: ReturnType<typeof Exact.rational>
|
|
87
102
|
}
|
|
88
103
|
|
|
@@ -133,13 +148,30 @@ type Evaluated =
|
|
|
133
148
|
| NumberValue
|
|
134
149
|
| { readonly kind: 'enum', readonly domain: string, readonly value: string }
|
|
135
150
|
|
|
151
|
+
type EvaluationContext = {
|
|
152
|
+
readonly expressionResults: WeakMap<Expression, Evaluation>
|
|
153
|
+
readonly evaluations: Map<string, Evaluation>
|
|
154
|
+
readonly expressionStrings: Map<string, number>
|
|
155
|
+
readonly assignment: Assignment
|
|
156
|
+
readonly budget: ExplanationBudget
|
|
157
|
+
readonly signs: WeakMap<Expression, Sign>
|
|
158
|
+
readonly assigned: Map<string, { readonly value: Evaluated } | { readonly error: unknown }>
|
|
159
|
+
readonly variables: ReadonlyMap<string, Variable>
|
|
160
|
+
readonly enumDomains: ReadonlyMap<string, ReadonlySet<string>>
|
|
161
|
+
}
|
|
162
|
+
|
|
136
163
|
const normalize = (numerator: bigint, denominator: bigint): NumberValue => {
|
|
137
164
|
if (denominator === 0n) throw new TypeError('cannot explain division by zero')
|
|
138
|
-
|
|
139
|
-
|
|
165
|
+
if (denominator < 0n) {
|
|
166
|
+
numerator = -numerator
|
|
167
|
+
denominator = -denominator
|
|
168
|
+
}
|
|
169
|
+
const divisor = gcd(numerator, denominator)
|
|
170
|
+
return { kind: 'number', numerator: numerator / divisor, denominator: denominator / divisor }
|
|
140
171
|
}
|
|
141
172
|
|
|
142
|
-
const number = (value: Rational): NumberValue => {
|
|
173
|
+
const number = (value: Rational, budget: ExplanationBudget): NumberValue => {
|
|
174
|
+
budget.input(value)
|
|
143
175
|
const exact = Exact.rational(value)
|
|
144
176
|
return { kind: 'number', numerator: BigInt(exact.numerator), denominator: BigInt(exact.denominator) }
|
|
145
177
|
}
|
|
@@ -154,78 +186,214 @@ const bool = (value: Evaluated): boolean => {
|
|
|
154
186
|
return value.value
|
|
155
187
|
}
|
|
156
188
|
|
|
157
|
-
const add = (left: NumberValue, right: NumberValue): NumberValue =>
|
|
158
|
-
left.numerator
|
|
159
|
-
|
|
160
|
-
)
|
|
189
|
+
const add = (left: NumberValue, right: NumberValue, budget: ExplanationBudget): NumberValue => {
|
|
190
|
+
if (left.numerator === 0n) return right
|
|
191
|
+
if (right.numerator === 0n) return left
|
|
192
|
+
if (left.denominator === right.denominator) {
|
|
193
|
+
budget.check(budget.sum(left.numerator, right.numerator), budget.bits(left.denominator))
|
|
194
|
+
} else {
|
|
195
|
+
budget.check(Math.max(budget.product(left.numerator, right.denominator),
|
|
196
|
+
budget.product(right.numerator, left.denominator)) + 1,
|
|
197
|
+
budget.product(left.denominator, right.denominator))
|
|
198
|
+
}
|
|
199
|
+
// Adding an integer preserves gcd(numerator, denominator) = 1.
|
|
200
|
+
if (left.denominator === 1n) return { kind: 'number',
|
|
201
|
+
numerator: left.numerator * right.denominator + right.numerator, denominator: right.denominator }
|
|
202
|
+
if (right.denominator === 1n) return { kind: 'number',
|
|
203
|
+
numerator: left.numerator + right.numerator * left.denominator, denominator: left.denominator }
|
|
204
|
+
return normalize(...sum(left.numerator, left.denominator, right.numerator, right.denominator))
|
|
205
|
+
}
|
|
161
206
|
|
|
162
|
-
const multiply = (left: NumberValue, right: NumberValue): NumberValue =>
|
|
163
|
-
|
|
207
|
+
const multiply = (left: NumberValue, right: NumberValue, budget: ExplanationBudget): NumberValue => {
|
|
208
|
+
budget.check(budget.product(left.numerator, right.numerator), budget.product(left.denominator, right.denominator))
|
|
209
|
+
// Evaluation values are reduced with positive denominators. Squaring preserves
|
|
210
|
+
// coprimality; otherwise cross-cancellation leaves a reduced product directly.
|
|
211
|
+
const [numerator, denominator] = left.numerator === right.numerator && left.denominator === right.denominator
|
|
212
|
+
? [left.numerator * right.numerator, left.denominator * right.denominator]
|
|
213
|
+
: product(left.numerator, left.denominator, right.numerator, right.denominator)
|
|
214
|
+
return { kind: 'number', numerator, denominator }
|
|
215
|
+
}
|
|
164
216
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
217
|
+
const negate = (value: NumberValue): NumberValue =>
|
|
218
|
+
({ kind: 'number', numerator: -value.numerator, denominator: value.denominator })
|
|
219
|
+
|
|
220
|
+
const compare = (left: NumberValue, right: NumberValue, budget: ExplanationBudget): -1 | 0 | 1 => {
|
|
221
|
+
// Evaluation values are normalized, so both denominators are positive.
|
|
222
|
+
if (left.denominator === right.denominator || left.numerator === 0n || right.numerator === 0n ||
|
|
223
|
+
(left.numerator < 0n) !== (right.numerator < 0n)) {
|
|
224
|
+
return left.numerator < right.numerator ? -1 : left.numerator > right.numerator ? 1 : 0
|
|
225
|
+
}
|
|
226
|
+
if (left.numerator === right.numerator) {
|
|
227
|
+
return (left.denominator < right.denominator) === (left.numerator > 0n) ? 1 : -1
|
|
228
|
+
}
|
|
229
|
+
if ((left.numerator < right.numerator) ===
|
|
230
|
+
(left.numerator > 0n ? left.denominator > right.denominator : left.denominator < right.denominator)) {
|
|
231
|
+
return left.numerator < right.numerator ? -1 : 1
|
|
232
|
+
}
|
|
233
|
+
budget.check(budget.product(left.numerator, right.denominator), budget.product(right.numerator, left.denominator))
|
|
234
|
+
const a = left.numerator * right.denominator
|
|
235
|
+
const b = right.numerator * left.denominator
|
|
236
|
+
return a < b ? -1 : a > b ? 1 : 0
|
|
168
237
|
}
|
|
169
238
|
|
|
170
239
|
const equal = (left: Evaluated, right: Evaluated): boolean => {
|
|
171
|
-
if (left.kind === 'number' && right.kind === 'number')
|
|
240
|
+
if (left.kind === 'number' && right.kind === 'number') {
|
|
241
|
+
// Every evaluated number is reduced with a positive denominator.
|
|
242
|
+
return left.numerator === right.numerator && left.denominator === right.denominator
|
|
243
|
+
}
|
|
172
244
|
if (left.kind !== right.kind) return false
|
|
173
245
|
if (left.kind === 'bool' && right.kind === 'bool') return left.value === right.value
|
|
174
246
|
if (left.kind === 'enum' && right.kind === 'enum') return left.domain === right.domain && left.value === right.value
|
|
175
247
|
return false
|
|
176
248
|
}
|
|
177
249
|
|
|
178
|
-
const assigned = (value: Value): Evaluated => {
|
|
250
|
+
const assigned = (value: Value, budget: ExplanationBudget): Evaluated => {
|
|
179
251
|
switch (value.sort) {
|
|
180
|
-
case 'bool':
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
case '
|
|
252
|
+
case 'bool':
|
|
253
|
+
if (typeof value.value !== 'boolean') throw new TypeError('boolean assignment value must be a boolean')
|
|
254
|
+
return { kind: 'bool', value: value.value }
|
|
255
|
+
case 'int': return number({ numerator: value.value, denominator: '1' }, budget)
|
|
256
|
+
case 'real': return number({ numerator: value.numerator, denominator: value.denominator }, budget)
|
|
257
|
+
case 'enum':
|
|
258
|
+
if (typeof value.domain !== 'string' || typeof value.value !== 'string') throw new TypeError('enum assignment domain and value must be strings')
|
|
259
|
+
return { kind: 'enum', domain: value.domain, value: value.value }
|
|
184
260
|
}
|
|
185
261
|
}
|
|
186
262
|
|
|
187
|
-
|
|
188
|
-
|
|
263
|
+
/** A captured assignment has one validation result for the whole report. */
|
|
264
|
+
const evaluateAssignment = (id: string, context: EvaluationContext): Evaluated => {
|
|
265
|
+
const cached = context.assigned.get(id)
|
|
266
|
+
if (cached !== undefined) {
|
|
267
|
+
if ('error' in cached) throw cached.error
|
|
268
|
+
return cached.value
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
const value = context.assignment[id]
|
|
272
|
+
if (value === undefined) throw new TypeError(`assignment omits ${JSON.stringify(id)}`)
|
|
273
|
+
const evaluated = assigned(value, context.budget)
|
|
274
|
+
const variable = context.variables.get(id)!
|
|
275
|
+
const kind = variable.sort === 'int' || variable.sort === 'real' ? 'number' : variable.sort
|
|
276
|
+
if (evaluated.kind !== kind) throw new TypeError('assignment kind must match the declared variable')
|
|
277
|
+
if (variable.sort === 'int' && evaluated.kind === 'number' && evaluated.denominator !== 1n) {
|
|
278
|
+
throw new TypeError('integer variable assignment must be integral')
|
|
279
|
+
}
|
|
280
|
+
if (evaluated.kind === 'number' && (variable.sort === 'int' || variable.sort === 'real')) {
|
|
281
|
+
const min = variable.min === undefined ? undefined : variable.sort === 'int'
|
|
282
|
+
? number({ numerator: variable.min, denominator: '1' }, context.budget) : number(variable.min, context.budget)
|
|
283
|
+
const max = variable.max === undefined ? undefined : variable.sort === 'int'
|
|
284
|
+
? number({ numerator: variable.max, denominator: '1' }, context.budget) : number(variable.max, context.budget)
|
|
285
|
+
if ((min !== undefined && compare(evaluated, min, context.budget) < 0) || (max !== undefined && compare(evaluated, max, context.budget) > 0)) {
|
|
286
|
+
throw new TypeError('numeric assignment must lie within the declared bounds')
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (variable.sort === 'enum' && (evaluated.kind !== 'enum' || evaluated.domain !== variable.domain || !context.enumDomains.get(variable.domain)!.has(evaluated.value))) {
|
|
290
|
+
throw new TypeError('enum assignment must belong to the declared domain')
|
|
291
|
+
}
|
|
292
|
+
context.assigned.set(id, { value: evaluated })
|
|
293
|
+
return evaluated
|
|
294
|
+
} catch (error) {
|
|
295
|
+
context.assigned.set(id, { error })
|
|
296
|
+
throw error
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function* evaluateNode(expression: Expression, context: EvaluationContext): Generator<Expression, Evaluated, Evaluated> {
|
|
301
|
+
const signOf = (value: Expression): Sign => constantSign(value, context.signs, literal => {
|
|
302
|
+
if (literal.kind !== 'literal' || (literal.sort !== 'int' && literal.sort !== 'real')) throw new TypeError('expected a numeric literal')
|
|
303
|
+
const input = literal.sort === 'int' ? { numerator: literal.value, denominator: '1' } : literal.value
|
|
304
|
+
context.budget.input(input)
|
|
305
|
+
return Exact.rational(input)
|
|
306
|
+
})
|
|
307
|
+
if (expression.kind === 'eq' || expression.kind === 'neq' || expression.kind === 'lt' ||
|
|
308
|
+
expression.kind === 'lte' || expression.kind === 'gt' || expression.kind === 'gte') {
|
|
309
|
+
const zero = (value: Expression): boolean => value.kind === 'literal' &&
|
|
310
|
+
(value.sort === 'int' || value.sort === 'real') && signOf(value) === 0
|
|
311
|
+
// A proof reads only captured constants. Unknown signs (including undefined
|
|
312
|
+
// arithmetic or any variable) retain ordinary assignment/error evaluation.
|
|
313
|
+
const sign = zero(expression.right) ? signOf(expression.left)
|
|
314
|
+
: zero(expression.left) ? signOf(expression.right) : undefined
|
|
315
|
+
if (sign !== undefined) {
|
|
316
|
+
const order = zero(expression.right) ? sign : -sign
|
|
317
|
+
return { kind: 'bool', value: expression.kind === 'eq' ? order === 0
|
|
318
|
+
: expression.kind === 'neq' ? order !== 0 : expression.kind === 'lt' ? order < 0
|
|
319
|
+
: expression.kind === 'lte' ? order <= 0 : expression.kind === 'gt' ? order > 0 : order >= 0 }
|
|
320
|
+
}
|
|
321
|
+
}
|
|
189
322
|
switch (expression.kind) {
|
|
190
323
|
case 'literal':
|
|
191
324
|
switch (expression.sort) {
|
|
192
325
|
case 'bool': return { kind: 'bool', value: expression.value }
|
|
193
|
-
case 'int': return number({ numerator:
|
|
194
|
-
case 'real': return number(expression.value)
|
|
326
|
+
case 'int': return number({ numerator: expression.value, denominator: '1' }, context.budget)
|
|
327
|
+
case 'real': return number(expression.value, context.budget)
|
|
195
328
|
case 'enum': return { kind: 'enum', domain: expression.domain, value: expression.value }
|
|
196
329
|
}
|
|
197
|
-
case 'variable':
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
330
|
+
case 'variable': return evaluateAssignment(expression.id, context)
|
|
331
|
+
case 'not': return { kind: 'bool', value: !bool((yield expression.value)) }
|
|
332
|
+
case 'and':
|
|
333
|
+
for (const value of expression.operands) if (!bool(yield value)) return { kind: 'bool', value: false }
|
|
334
|
+
return { kind: 'bool', value: true }
|
|
335
|
+
case 'or':
|
|
336
|
+
for (const value of expression.operands) if (bool(yield value)) return { kind: 'bool', value: true }
|
|
337
|
+
return { kind: 'bool', value: false }
|
|
338
|
+
case 'implies': return { kind: 'bool', value: !bool((yield expression.left)) || bool((yield expression.right)) }
|
|
339
|
+
case 'eq': return { kind: 'bool', value: equal((yield expression.left), (yield expression.right)) }
|
|
340
|
+
case 'neq': return { kind: 'bool', value: !equal((yield expression.left), (yield expression.right)) }
|
|
341
|
+
case 'lt': return { kind: 'bool', value: compare(numeric((yield expression.left)), numeric((yield expression.right)), context.budget) < 0 }
|
|
342
|
+
case 'lte': return { kind: 'bool', value: compare(numeric((yield expression.left)), numeric((yield expression.right)), context.budget) <= 0 }
|
|
343
|
+
case 'gt': return { kind: 'bool', value: compare(numeric((yield expression.left)), numeric((yield expression.right)), context.budget) > 0 }
|
|
344
|
+
case 'gte': return { kind: 'bool', value: compare(numeric((yield expression.left)), numeric((yield expression.right)), context.budget) >= 0 }
|
|
345
|
+
case 'add':
|
|
346
|
+
case 'multiply': {
|
|
347
|
+
const values: Evaluated[] = []
|
|
348
|
+
for (const value of expression.operands) values.push(yield value)
|
|
349
|
+
const numbers = values.map(numeric)
|
|
350
|
+
// Evaluate and type-check every operand before applying the zero identity:
|
|
351
|
+
// zero must not hide undefined arithmetic in another operand.
|
|
352
|
+
if (expression.kind === 'multiply' && numbers.some(value => value.numerator === 0n)) {
|
|
353
|
+
return normalize(0n, 1n)
|
|
354
|
+
}
|
|
355
|
+
return balancedReduce(numbers, (left, right) => expression.kind === 'add'
|
|
356
|
+
? add(left, right, context.budget) : multiply(left, right, context.budget))
|
|
201
357
|
}
|
|
202
|
-
case 'not': return { kind: 'bool', value: !bool(run(expression.value)) }
|
|
203
|
-
case 'and': return { kind: 'bool', value: expression.operands.every(value => bool(run(value))) }
|
|
204
|
-
case 'or': return { kind: 'bool', value: expression.operands.some(value => bool(run(value))) }
|
|
205
|
-
case 'implies': return { kind: 'bool', value: !bool(run(expression.left)) || bool(run(expression.right)) }
|
|
206
|
-
case 'eq': return { kind: 'bool', value: equal(run(expression.left), run(expression.right)) }
|
|
207
|
-
case 'neq': return { kind: 'bool', value: !equal(run(expression.left), run(expression.right)) }
|
|
208
|
-
case 'lt': return { kind: 'bool', value: compare(numeric(run(expression.left)), numeric(run(expression.right))) < 0 }
|
|
209
|
-
case 'lte': return { kind: 'bool', value: compare(numeric(run(expression.left)), numeric(run(expression.right))) <= 0 }
|
|
210
|
-
case 'gt': return { kind: 'bool', value: compare(numeric(run(expression.left)), numeric(run(expression.right))) > 0 }
|
|
211
|
-
case 'gte': return { kind: 'bool', value: compare(numeric(run(expression.left)), numeric(run(expression.right))) >= 0 }
|
|
212
|
-
case 'add': return expression.operands.map(run).map(numeric).reduce(add)
|
|
213
|
-
case 'multiply': return expression.operands.map(run).map(numeric).reduce(multiply)
|
|
214
358
|
case 'subtract': {
|
|
215
|
-
const left = numeric(
|
|
216
|
-
const right = numeric(
|
|
217
|
-
return add(left,
|
|
359
|
+
const left = numeric((yield expression.left))
|
|
360
|
+
const right = numeric((yield expression.right))
|
|
361
|
+
return add(left, negate(right), context.budget)
|
|
218
362
|
}
|
|
219
363
|
case 'divide': {
|
|
220
|
-
const left = numeric(
|
|
221
|
-
const right = numeric(
|
|
222
|
-
|
|
364
|
+
const left = numeric((yield expression.left))
|
|
365
|
+
const right = numeric((yield expression.right))
|
|
366
|
+
if (right.numerator === 0n) throw new TypeError('cannot explain division by zero')
|
|
367
|
+
context.budget.check(context.budget.product(left.numerator, right.denominator),
|
|
368
|
+
context.budget.product(left.denominator, right.numerator))
|
|
369
|
+
// Cross-cancellation preserves reduction; only denominator sign can change.
|
|
370
|
+
const [numerator, denominator] = product(left.numerator, left.denominator, right.denominator, right.numerator)
|
|
371
|
+
return denominator < 0n
|
|
372
|
+
? { kind: 'number', numerator: -numerator, denominator: -denominator }
|
|
373
|
+
: { kind: 'number', numerator, denominator }
|
|
223
374
|
}
|
|
224
375
|
case 'negate': {
|
|
225
|
-
const value = numeric(
|
|
226
|
-
return
|
|
376
|
+
const value = numeric((yield expression.value))
|
|
377
|
+
return negate(value)
|
|
378
|
+
}
|
|
379
|
+
case 'if': return bool((yield expression.condition)) ? (yield expression.then) : (yield expression.else)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
const evaluate = (expression: Expression, context: EvaluationContext): Evaluated => {
|
|
385
|
+
const stack = [evaluateNode(expression, context)]
|
|
386
|
+
let step = stack[0]!.next()
|
|
387
|
+
while (true) {
|
|
388
|
+
if (step.done) {
|
|
389
|
+
stack.pop()
|
|
390
|
+
if (stack.length === 0) return step.value
|
|
391
|
+
step = stack[stack.length - 1]!.next(step.value)
|
|
392
|
+
} else {
|
|
393
|
+
const child = evaluateNode(step.value, context)
|
|
394
|
+
stack.push(child)
|
|
395
|
+
step = child.next()
|
|
227
396
|
}
|
|
228
|
-
case 'if': return bool(run(expression.condition)) ? run(expression.then) : run(expression.else)
|
|
229
397
|
}
|
|
230
398
|
}
|
|
231
399
|
|
|
@@ -240,25 +408,50 @@ const element = (value: { readonly id: string } & Provenance): Element => ({
|
|
|
240
408
|
scenarioInputIds: sorted(value.scenarioInputIds)
|
|
241
409
|
})
|
|
242
410
|
|
|
243
|
-
|
|
244
|
-
|
|
411
|
+
type Evaluation = {
|
|
412
|
+
readonly value?: boolean
|
|
413
|
+
readonly evaluationReason?: string
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const evaluation = (expression: Expression, context: EvaluationContext): Evaluation => {
|
|
417
|
+
const repeated = context.expressionResults.get(expression)
|
|
418
|
+
if (repeated !== undefined) return repeated
|
|
419
|
+
const key = expressionKey(expression, context.expressionStrings)
|
|
420
|
+
const cached = context.evaluations.get(key)
|
|
421
|
+
if (cached !== undefined) {
|
|
422
|
+
context.expressionResults.set(expression, cached)
|
|
423
|
+
return cached
|
|
424
|
+
}
|
|
425
|
+
let result: Evaluation
|
|
245
426
|
try {
|
|
246
|
-
|
|
247
|
-
} catch {
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
})
|
|
427
|
+
result = { value: bool(evaluate(expression, context)) }
|
|
428
|
+
} catch (error) {
|
|
429
|
+
// Local arithmetic/assignment failure does not reclassify the backend result.
|
|
430
|
+
result = { evaluationReason: error instanceof Error ? error.message : 'constraint evaluation failed' }
|
|
431
|
+
}
|
|
432
|
+
context.evaluations.set(key, result)
|
|
433
|
+
context.expressionResults.set(expression, result)
|
|
434
|
+
return result
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const constraint = (value: HardConstraint, context: EvaluationContext): Constraint => {
|
|
438
|
+
const result = evaluation(value.expression, context)
|
|
439
|
+
return {
|
|
440
|
+
...element(value),
|
|
441
|
+
evaluation: result.value === undefined ? 'indeterminate' : result.value ? 'satisfied' : 'violated',
|
|
442
|
+
...(result.evaluationReason === undefined ? {} : { evaluationReason: result.evaluationReason })
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const softConstraint = (value: SoftConstraint, context: EvaluationContext): SoftConstraintResult => {
|
|
447
|
+
const result = evaluation(value.expression, context)
|
|
448
|
+
return {
|
|
449
|
+
...element(value),
|
|
450
|
+
evaluation: result.value === undefined ? 'indeterminate' : result.value ? 'accepted' : 'violated',
|
|
451
|
+
...(result.evaluationReason === undefined ? {} : { evaluationReason: result.evaluationReason }),
|
|
452
|
+
weight: Exact.rational(value.weight)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
262
455
|
|
|
263
456
|
const assignments = (model: Model, assignment: Assignment): readonly AssignmentValue[] => {
|
|
264
457
|
const variables = new Map(model.variables.map(variable => [variable.id, variable]))
|
|
@@ -270,11 +463,19 @@ const assignments = (model: Model, assignment: Assignment): readonly AssignmentV
|
|
|
270
463
|
})
|
|
271
464
|
}
|
|
272
465
|
|
|
273
|
-
const feasible = (model: Model, assignment: Assignment): Feasible =>
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
466
|
+
const feasible = (model: Model, assignment: Assignment, limits: Limits): Feasible => {
|
|
467
|
+
// The report owns a captured assignment; normalized values live only for this report.
|
|
468
|
+
const context: EvaluationContext = {
|
|
469
|
+
assignment, budget: new ExplanationBudget(limits.maxExplanationBits, limits.maxExplanationWork), assigned: new Map(), signs: new WeakMap(), expressionResults: new WeakMap(), evaluations: new Map(), expressionStrings: new Map(),
|
|
470
|
+
variables: new Map(model.variables.map(variable => [variable.id, variable])),
|
|
471
|
+
enumDomains: new Map((model.enums ?? []).map(domain => [domain.id, new Set(domain.values)]))
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
assignments: assignments(model, assignment),
|
|
475
|
+
hardConstraints: model.constraints.map(value => constraint(value, context)),
|
|
476
|
+
softConstraints: (model.softConstraints ?? []).map(value => softConstraint(value, context))
|
|
477
|
+
}
|
|
478
|
+
}
|
|
278
479
|
|
|
279
480
|
const objective = (declaration: Objective | undefined, result: ObjectiveValue): ObjectiveResult => ({
|
|
280
481
|
...element(declaration ?? { id: result.objectiveId }),
|
|
@@ -284,14 +485,14 @@ const objective = (declaration: Objective | undefined, result: ObjectiveValue):
|
|
|
284
485
|
...(result.bound === undefined ? {} : { bound: result.bound })
|
|
285
486
|
})
|
|
286
487
|
|
|
287
|
-
const outcome = (model: Model, result: Result): Outcome => {
|
|
488
|
+
const outcome = (model: Model, result: Result, limits: Limits): Outcome => {
|
|
288
489
|
switch (result.status) {
|
|
289
|
-
case 'satisfied': return { status: result.status, ...feasible(model, result.assignment) }
|
|
490
|
+
case 'satisfied': return { status: result.status, ...feasible(model, result.assignment, limits) }
|
|
290
491
|
case 'optimal': {
|
|
291
492
|
const declarations = new Map((model.objectives ?? []).map(value => [value.id, value]))
|
|
292
493
|
return {
|
|
293
494
|
status: result.status,
|
|
294
|
-
...feasible(model, result.assignment),
|
|
495
|
+
...feasible(model, result.assignment, limits),
|
|
295
496
|
objectives: result.objectives.map(value => objective(declarations.get(value.objectiveId), value)),
|
|
296
497
|
optimalityProved: result.optimalityProved
|
|
297
498
|
}
|
|
@@ -316,10 +517,22 @@ const outcome = (model: Model, result: Result): Outcome => {
|
|
|
316
517
|
|
|
317
518
|
/** Build stable JSON data without mutating the model, scenario, or CAVE store. */
|
|
318
519
|
export const report = (model: Model, result: Result, limits: Limits, context: Context = {}): Report => {
|
|
319
|
-
|
|
520
|
+
limits = mergeLimits(limits)
|
|
521
|
+
model = clone(model)
|
|
522
|
+
result = captureResult(result)
|
|
523
|
+
validateContext(context)
|
|
524
|
+
context = clone({
|
|
525
|
+
modelDigest: context.modelDigest,
|
|
526
|
+
scenario: context.scenario,
|
|
527
|
+
snapshot: context.snapshot,
|
|
528
|
+
inputs: context.inputs,
|
|
529
|
+
})
|
|
530
|
+
validateContext(context)
|
|
531
|
+
const modelDigest = digestOwned(model, limits)
|
|
320
532
|
if (context.modelDigest !== undefined && context.modelDigest !== modelDigest) {
|
|
321
533
|
throw new TypeError(`scenario model digest ${context.modelDigest} does not match ${modelDigest}`)
|
|
322
534
|
}
|
|
535
|
+
// All referenced data is owned by the captures above or constructed in this call.
|
|
323
536
|
return {
|
|
324
537
|
schema,
|
|
325
538
|
run: {
|
|
@@ -332,69 +545,104 @@ export const report = (model: Model, result: Result, limits: Limits, context: Co
|
|
|
332
545
|
...(context.snapshot === undefined ? {} : { snapshot: context.snapshot }),
|
|
333
546
|
inputs: [...(context.inputs ?? [])].sort((left, right) => compareText(left.id, right.id))
|
|
334
547
|
},
|
|
335
|
-
outcome: outcome(model, result)
|
|
548
|
+
outcome: outcome(model, result, limits)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const sourceLocation = (declaration: Declaration): string => {
|
|
553
|
+
if (declaration.line !== undefined) {
|
|
554
|
+
return `${inlineText(declaration.uri)}:${declaration.line}${declaration.column === undefined ? '' : `:${declaration.column}`}`
|
|
336
555
|
}
|
|
556
|
+
return `${inlineText(declaration.uri)}${declaration.column === undefined ? '' : ` (column ${declaration.column})`}`
|
|
337
557
|
}
|
|
338
558
|
|
|
339
559
|
const evidence = (value: Element): string => {
|
|
340
560
|
const parts = [
|
|
341
|
-
value.declaration === undefined ? undefined :
|
|
342
|
-
value.evidenceRowIds.length === 0 ? undefined : `rows ${value.evidenceRowIds.join(', ')}`,
|
|
343
|
-
value.scenarioInputIds.length === 0 ? undefined : `inputs ${value.scenarioInputIds.join(', ')}`
|
|
561
|
+
value.declaration === undefined ? undefined : sourceLocation(value.declaration),
|
|
562
|
+
value.evidenceRowIds.length === 0 ? undefined : `rows ${value.evidenceRowIds.map(inlineText).join(', ')}`,
|
|
563
|
+
value.scenarioInputIds.length === 0 ? undefined : `inputs ${value.scenarioInputIds.map(inlineText).join(', ')}`
|
|
344
564
|
].filter((part): part is string => part !== undefined)
|
|
345
565
|
return parts.length === 0 ? '' : ` — ${parts.join('; ')}`
|
|
346
566
|
}
|
|
347
567
|
|
|
348
568
|
const valueText = (value: Value): string => {
|
|
569
|
+
const invalid = '(invalid backend value)'
|
|
570
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return invalid
|
|
349
571
|
switch (value.sort) {
|
|
350
|
-
case 'bool': return String(value.value)
|
|
351
|
-
case 'int': return value.value
|
|
352
|
-
case 'real': return
|
|
353
|
-
|
|
572
|
+
case 'bool': return typeof value.value === 'boolean' ? String(value.value) : invalid
|
|
573
|
+
case 'int': return typeof value.value === 'string' ? inlineText(value.value) : invalid
|
|
574
|
+
case 'real': return typeof value.numerator === 'string' && typeof value.denominator === 'string'
|
|
575
|
+
? inlineText(`${value.numerator}/${value.denominator}`) : invalid
|
|
576
|
+
case 'enum': return typeof value.domain === 'string' && typeof value.value === 'string' ? inlineText(value.value) : invalid
|
|
577
|
+
default: return invalid
|
|
354
578
|
}
|
|
355
579
|
}
|
|
356
580
|
|
|
581
|
+
const jsonControlEscapes = new Map(
|
|
582
|
+
[...Array.from({ length: 33 }, (_, index) => index + 0x7f), 0x2028, 0x2029]
|
|
583
|
+
.map(code => [String.fromCharCode(code), `\\u${code.toString(16).padStart(4, '0')}`])
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
const escapeJsonControls = (text: string): string =>
|
|
587
|
+
text.replace(/[\u007f-\u009f\u2028\u2029]/g, character => jsonControlEscapes.get(character)!)
|
|
588
|
+
|
|
589
|
+
const quotedText = (message: string): string => escapeJsonControls(JSON.stringify(message))
|
|
590
|
+
|
|
591
|
+
const inlineText = (message: string): string =>
|
|
592
|
+
/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/.test(message) ? quotedText(message) : message
|
|
593
|
+
|
|
357
594
|
/** Render the same report as concise, deterministic plain text. */
|
|
358
595
|
export const render = (value: Report): string => {
|
|
359
596
|
const lines = [
|
|
360
597
|
`Solver result: ${value.outcome.status}`,
|
|
361
|
-
`Model: ${value.run.modelDigest}`,
|
|
362
|
-
`Backend: ${value.run.backend.name} ${value.run.backend.version}`,
|
|
598
|
+
`Model: ${inlineText(value.run.modelDigest)}`,
|
|
599
|
+
`Backend: ${inlineText(value.run.backend.name)} ${inlineText(value.run.backend.version)}`,
|
|
363
600
|
`Elapsed: ${value.run.elapsedMs} ms`
|
|
364
601
|
]
|
|
365
602
|
if (value.run.snapshot !== undefined) {
|
|
366
|
-
|
|
603
|
+
const snapshot = value.run.snapshot
|
|
604
|
+
const policies: string[] = []
|
|
605
|
+
if (snapshot.aliases !== undefined) policies.push(`aliases ${snapshot.aliases}`)
|
|
606
|
+
if (snapshot.resolution !== undefined) policies.push(`resolution ${snapshot.resolution}`)
|
|
607
|
+
if (snapshot.minimumConfidence !== undefined) policies.push(`minimum confidence ${snapshot.minimumConfidence}`)
|
|
608
|
+
lines.push(`Snapshot: transaction ${inlineText(snapshot.transactionTime ?? 'empty')}${snapshot.validTime === undefined ? '' : `, valid ${inlineText(snapshot.validTime)}`}${policies.length === 0 ? '' : `; ${policies.join('; ')}`}`)
|
|
367
609
|
}
|
|
368
|
-
if (value.run.scenario !== undefined) lines.push(`Scenario: ${value.run.scenario.id} (${value.run.scenario.inputDigest})`)
|
|
610
|
+
if (value.run.scenario !== undefined) lines.push(`Scenario: ${inlineText(value.run.scenario.id)} (input ${inlineText(value.run.scenario.inputDigest)}; overlay ${inlineText(value.run.scenario.overlayDigest)})`)
|
|
369
611
|
for (const input of value.run.inputs) {
|
|
370
|
-
|
|
612
|
+
const values: string[] = []
|
|
613
|
+
if (input.value !== undefined) values.push(escapeJsonControls(stringifyInput(input.value)))
|
|
614
|
+
if (input.authoredValue !== undefined) values.push(`authored ${escapeJsonControls(stringifyInput(input.authoredValue))}`)
|
|
615
|
+
const sources: string[] = []
|
|
616
|
+
if (input.evidenceRowIds.length > 0) sources.push(`rows ${sorted(input.evidenceRowIds).map(inlineText).join(', ')}`)
|
|
617
|
+
if (input.scenarioClaimIds.length > 0) sources.push(`scenario claims ${sorted(input.scenarioClaimIds).map(inlineText).join(', ')}`)
|
|
618
|
+
lines.push(`Input ${inlineText(input.id)}: ${values.length === 0 ? '(no value)' : values.join('; ')}${input.query === undefined ? '' : ` via ${inlineText(input.query)}`}${sources.length === 0 ? '' : ` — ${sources.join('; ')}`}`)
|
|
371
619
|
}
|
|
372
620
|
switch (value.outcome.status) {
|
|
373
621
|
case 'satisfied':
|
|
374
622
|
case 'optimal':
|
|
375
623
|
for (const assignment of value.outcome.assignments) {
|
|
376
|
-
lines.push(`Assignment ${assignment.id} = ${valueText(assignment.value)}${assignment.declared ? '' : ' (undeclared backend ID)'}${evidence(assignment)}`)
|
|
624
|
+
lines.push(`Assignment ${inlineText(assignment.id)} = ${valueText(assignment.value)}${assignment.declared ? '' : ' (undeclared backend ID)'}${evidence(assignment)}`)
|
|
377
625
|
}
|
|
378
626
|
for (const constraint of value.outcome.hardConstraints) {
|
|
379
|
-
lines.push(`Hard constraint ${constraint.id}: ${constraint.evaluation}${evidence(constraint)}`)
|
|
627
|
+
lines.push(`Hard constraint ${inlineText(constraint.id)}: ${constraint.evaluation}${constraint.evaluationReason === undefined ? '' : ` (${quotedText(constraint.evaluationReason)})`}${evidence(constraint)}`)
|
|
380
628
|
}
|
|
381
629
|
for (const constraint of value.outcome.softConstraints) {
|
|
382
|
-
lines.push(`Soft constraint ${constraint.id}: ${constraint.evaluation}, weight ${constraint.weight.numerator}/${constraint.weight.denominator}${evidence(constraint)}`)
|
|
630
|
+
lines.push(`Soft constraint ${inlineText(constraint.id)}: ${constraint.evaluation}${constraint.evaluationReason === undefined ? '' : ` (${quotedText(constraint.evaluationReason)})`}, weight ${constraint.weight.numerator}/${constraint.weight.denominator}${evidence(constraint)}`)
|
|
383
631
|
}
|
|
384
632
|
if (value.outcome.status === 'optimal') {
|
|
385
633
|
for (const objective of value.outcome.objectives) {
|
|
386
|
-
lines.push(`Objective ${objective.id} (${objective.direction}) = ${valueText(objective.value)}${objective.declared ? '' : ' (undeclared backend ID)'}${evidence(objective)}`)
|
|
634
|
+
lines.push(`Objective ${inlineText(objective.id)} (${objective.direction}) = ${valueText(objective.value)}${objective.declared ? '' : ' (undeclared backend ID)'}${evidence(objective)}`)
|
|
387
635
|
}
|
|
388
636
|
}
|
|
389
637
|
break
|
|
390
638
|
case 'unsatisfied':
|
|
391
639
|
lines.push('Infeasibility proved. Unsatisfiable cores are not necessarily minimal.')
|
|
392
640
|
for (const constraint of value.outcome.core ?? []) {
|
|
393
|
-
lines.push(`Core constraint ${constraint.id}${constraint.declared ? '' : ' (undeclared backend ID)'}${evidence(constraint)}`)
|
|
641
|
+
lines.push(`Core constraint ${inlineText(constraint.id)}${constraint.declared ? '' : ' (undeclared backend ID)'}${evidence(constraint)}`)
|
|
394
642
|
}
|
|
395
643
|
break
|
|
396
644
|
case 'unknown':
|
|
397
|
-
lines.push(`Unknown: ${value.outcome.reason.kind} — ${value.outcome.reason.message}`)
|
|
645
|
+
lines.push(`Unknown: ${value.outcome.reason.kind} — ${inlineText(value.outcome.reason.message)}`)
|
|
398
646
|
break
|
|
399
647
|
}
|
|
400
648
|
return `${lines.join('\n')}\n`
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Rational } from './model.ts'
|
|
2
|
+
import { numericDigits } from './numeric-size.ts'
|
|
3
|
+
|
|
4
|
+
/** Conservative pre-allocation bounds for local fraction arithmetic, not a time or heap limit. */
|
|
5
|
+
export class ExplanationBudget {
|
|
6
|
+
readonly maximum: number
|
|
7
|
+
readonly maximumWork: number
|
|
8
|
+
private used = 0
|
|
9
|
+
constructor(maximum: number, maximumWork = Number.MAX_SAFE_INTEGER) {
|
|
10
|
+
this.maximum = maximum
|
|
11
|
+
this.maximumWork = maximumWork
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
check(...bounds: number[]): void {
|
|
15
|
+
const actual = Math.max(0, ...bounds)
|
|
16
|
+
if (actual > this.maximum) {
|
|
17
|
+
throw new RangeError(`explanation exceeds maxExplanationBits: estimated ${actual} > ${this.maximum}`)
|
|
18
|
+
}
|
|
19
|
+
if (actual > this.maximumWork - this.used) {
|
|
20
|
+
throw new RangeError(`explanation exceeds maxExplanationWork: estimated ${actual} with ${this.maximumWork - this.used} remaining`)
|
|
21
|
+
}
|
|
22
|
+
this.used += actual
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
input(value: Rational): void {
|
|
26
|
+
// Four bits per decimal digit also bounds either member of an unreduced pair.
|
|
27
|
+
// Charge text before Exact can allocate powers of ten or parse large BigInts.
|
|
28
|
+
this.check(numericDigits(value, 'real') * 4)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
bits(value: bigint): number {
|
|
32
|
+
return value === 0n ? 0 : (value < 0n ? -value : value).toString(2).length
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
product(left: bigint, right: bigint): number {
|
|
36
|
+
if (left === 0n || right === 0n) return 0
|
|
37
|
+
if (left === 1n || left === -1n) return this.bits(right)
|
|
38
|
+
if (right === 1n || right === -1n) return this.bits(left)
|
|
39
|
+
return this.bits(left) + this.bits(right)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
sum(left: bigint, right: bigint): number {
|
|
43
|
+
if (left === 0n) return this.bits(right)
|
|
44
|
+
if (right === 0n) return this.bits(left)
|
|
45
|
+
return Math.max(this.bits(left), this.bits(right)) + 1
|
|
46
|
+
}
|
|
47
|
+
}
|