@cavelang/solver-z3 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/BENCHMARK.md +56 -3
- package/README.md +267 -9
- package/dist/scripts/benchmark.js +35 -13
- package/dist/scripts/benchmark.js.map +1 -1
- package/dist/scripts/memory-stress.d.ts +2 -0
- package/dist/scripts/memory-stress.d.ts.map +1 -0
- package/dist/scripts/memory-stress.js +74 -0
- package/dist/scripts/memory-stress.js.map +1 -0
- package/dist/src/cleanup.d.ts +8 -0
- package/dist/src/cleanup.d.ts.map +1 -0
- package/dist/src/cleanup.js +65 -0
- package/dist/src/cleanup.js.map +1 -0
- package/dist/src/close-workers.d.ts +12 -0
- package/dist/src/close-workers.d.ts.map +1 -0
- package/dist/src/close-workers.js +27 -0
- package/dist/src/close-workers.js.map +1 -0
- package/dist/src/deadline.d.ts +7 -0
- package/dist/src/deadline.d.ts.map +1 -0
- package/dist/src/deadline.js +46 -0
- package/dist/src/deadline.js.map +1 -0
- package/dist/src/error-message.d.ts +3 -0
- package/dist/src/error-message.d.ts.map +1 -0
- package/dist/src/error-message.js +10 -0
- package/dist/src/error-message.js.map +1 -0
- package/dist/src/runtime.d.ts.map +1 -1
- package/dist/src/runtime.js +137 -72
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/with-release.d.ts +3 -0
- package/dist/src/with-release.d.ts.map +1 -0
- package/dist/src/with-release.js +25 -0
- package/dist/src/with-release.js.map +1 -0
- package/dist/src/workflow-fixture.d.ts.map +1 -1
- package/dist/src/workflow-fixture.js +27 -10
- package/dist/src/workflow-fixture.js.map +1 -1
- package/package.json +4 -3
- package/scripts/benchmark.ts +35 -12
- package/scripts/memory-stress.ts +72 -0
- package/src/cleanup.ts +55 -0
- package/src/close-workers.ts +28 -0
- package/src/deadline.ts +40 -0
- package/src/error-message.ts +5 -0
- package/src/runtime.ts +132 -70
- package/src/with-release.ts +21 -0
- package/src/workflow-fixture.ts +30 -11
package/src/runtime.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { withDeadline } from './deadline.ts'
|
|
2
|
+
import { withRelease } from './with-release.ts'
|
|
3
|
+
import { errorMessage } from './error-message.ts'
|
|
1
4
|
import { performance } from 'node:perf_hooks'
|
|
2
5
|
import { Adapter, Exact, Model } from '@cavelang/solver'
|
|
6
|
+
import { guardCleanup } from './cleanup.ts'
|
|
7
|
+
import { closeWorkers } from './close-workers.ts'
|
|
3
8
|
import type {
|
|
4
9
|
AnyExpr,
|
|
5
10
|
Arith,
|
|
@@ -45,7 +50,7 @@ const backendError = (
|
|
|
45
50
|
status: 'unknown',
|
|
46
51
|
reason: {
|
|
47
52
|
kind: 'backend-error',
|
|
48
|
-
message:
|
|
53
|
+
message: errorMessage(error)
|
|
49
54
|
},
|
|
50
55
|
backend,
|
|
51
56
|
diagnostics: [],
|
|
@@ -77,6 +82,8 @@ const compiler = (
|
|
|
77
82
|
domain.id,
|
|
78
83
|
[...domain.values].sort(compareText)
|
|
79
84
|
] as const))
|
|
85
|
+
const enumCodes = new Map([...enums].map(([domain, values]) =>
|
|
86
|
+
[domain, new Map(values.map((value, index) => [value, index]))] as const))
|
|
80
87
|
const variables = new Map<string, Expression>()
|
|
81
88
|
const bounds: Bool<Name>[] = []
|
|
82
89
|
|
|
@@ -110,52 +117,110 @@ const compiler = (
|
|
|
110
117
|
}
|
|
111
118
|
}
|
|
112
119
|
|
|
113
|
-
const
|
|
120
|
+
const expressions = new WeakMap<Model.Expression, Expression>()
|
|
121
|
+
const compiledChild = (expression: Model.Expression): Expression => expressions.get(expression)!
|
|
122
|
+
const compileBoolean = (kind: 'and' | 'or', operands: readonly Model.Expression[]): Bool<Name> => {
|
|
123
|
+
const combine = (values: Bool<Name>[]): Bool<Name> => kind === 'and' ? context.And(...values) : context.Or(...values)
|
|
124
|
+
let values = operands.map(value => asBool(compiledChild(value)))
|
|
125
|
+
// The binding also spreads arguments internally, including its AstVector
|
|
126
|
+
// overload. Bound every call and retain all operands through associativity.
|
|
127
|
+
const width = 1024
|
|
128
|
+
while (values.length > width) {
|
|
129
|
+
const groups: Bool<Name>[] = []
|
|
130
|
+
for (let index = 0; index < values.length; index += width) {
|
|
131
|
+
groups.push(combine(values.slice(index, index + width)))
|
|
132
|
+
}
|
|
133
|
+
values = groups
|
|
134
|
+
}
|
|
135
|
+
return combine(values)
|
|
136
|
+
}
|
|
137
|
+
const compileNode = (expression: Model.Expression): Expression => {
|
|
114
138
|
switch (expression.kind) {
|
|
115
139
|
case 'literal':
|
|
116
140
|
switch (expression.sort) {
|
|
117
141
|
case 'bool': return context.Bool.val(expression.value)
|
|
118
142
|
case 'int': return context.Int.val(Exact.integer(expression.value))
|
|
119
143
|
case 'real': return context.Real.val(exact(expression.value))
|
|
120
|
-
case 'enum': return context.Int.val(
|
|
144
|
+
case 'enum': return context.Int.val(enumCodes.get(expression.domain)!.get(expression.value)!)
|
|
121
145
|
}
|
|
122
146
|
case 'variable': return variables.get(expression.id)!
|
|
123
|
-
case 'not': return asBool(
|
|
124
|
-
case 'and':
|
|
125
|
-
case 'or': return
|
|
126
|
-
case 'implies': return context.Implies(asBool(
|
|
127
|
-
case 'eq': return
|
|
128
|
-
case 'neq': return
|
|
129
|
-
case 'lt': return asArith(
|
|
130
|
-
case 'lte': return asArith(
|
|
131
|
-
case 'gt': return asArith(
|
|
132
|
-
case 'gte': return asArith(
|
|
147
|
+
case 'not': return asBool(compiledChild(expression.value)).not()
|
|
148
|
+
case 'and':
|
|
149
|
+
case 'or': return compileBoolean(expression.kind, expression.operands)
|
|
150
|
+
case 'implies': return context.Implies(asBool(compiledChild(expression.left)), asBool(compiledChild(expression.right)))
|
|
151
|
+
case 'eq': return compiledChild(expression.left).eq(compiledChild(expression.right))
|
|
152
|
+
case 'neq': return compiledChild(expression.left).neq(compiledChild(expression.right))
|
|
153
|
+
case 'lt': return asArith(compiledChild(expression.left)).lt(asArith(compiledChild(expression.right)))
|
|
154
|
+
case 'lte': return asArith(compiledChild(expression.left)).le(asArith(compiledChild(expression.right)))
|
|
155
|
+
case 'gt': return asArith(compiledChild(expression.left)).gt(asArith(compiledChild(expression.right)))
|
|
156
|
+
case 'gte': return asArith(compiledChild(expression.left)).ge(asArith(compiledChild(expression.right)))
|
|
133
157
|
case 'add': {
|
|
134
|
-
const [first, ...rest] = expression.operands.map(value => asArith(
|
|
158
|
+
const [first, ...rest] = expression.operands.map(value => asArith(compiledChild(value)))
|
|
135
159
|
return rest.reduce((left, right) => left.add(right), first!)
|
|
136
160
|
}
|
|
137
161
|
case 'multiply': {
|
|
138
|
-
const [first, ...rest] = expression.operands.map(value => asArith(
|
|
162
|
+
const [first, ...rest] = expression.operands.map(value => asArith(compiledChild(value)))
|
|
139
163
|
return rest.reduce((left, right) => left.mul(right), first!)
|
|
140
164
|
}
|
|
141
|
-
case 'subtract': return asArith(
|
|
165
|
+
case 'subtract': return asArith(compiledChild(expression.left)).sub(asArith(compiledChild(expression.right)))
|
|
142
166
|
case 'divide': {
|
|
143
|
-
const left = asArith(
|
|
144
|
-
const right = asArith(
|
|
167
|
+
const left = asArith(compiledChild(expression.left))
|
|
168
|
+
const right = asArith(compiledChild(expression.right))
|
|
145
169
|
// Portable division is exact-real division even when both operands are
|
|
146
170
|
// integers; Z3's native Int / Int operation would truncate instead.
|
|
147
171
|
const real = (value: Arith<Name>): Arith<Name> => context.isInt(value) ? context.ToReal(value) : value
|
|
148
172
|
return real(left).div(real(right))
|
|
149
173
|
}
|
|
150
|
-
case 'negate': return asArith(
|
|
174
|
+
case 'negate': return asArith(compiledChild(expression.value)).neg()
|
|
151
175
|
case 'if': return context.If(
|
|
152
|
-
asBool(
|
|
153
|
-
|
|
154
|
-
|
|
176
|
+
asBool(compiledChild(expression.condition)),
|
|
177
|
+
compiledChild(expression.then),
|
|
178
|
+
compiledChild(expression.else)
|
|
155
179
|
) as Expression
|
|
156
180
|
}
|
|
157
181
|
}
|
|
158
182
|
|
|
183
|
+
function* children(expression: Model.Expression): Generator<Model.Expression> {
|
|
184
|
+
switch (expression.kind) {
|
|
185
|
+
case 'literal':
|
|
186
|
+
case 'variable': return
|
|
187
|
+
case 'not':
|
|
188
|
+
case 'negate': yield expression.value; return
|
|
189
|
+
case 'and':
|
|
190
|
+
case 'or':
|
|
191
|
+
case 'add':
|
|
192
|
+
case 'multiply': yield* expression.operands; return
|
|
193
|
+
case 'if':
|
|
194
|
+
yield expression.condition
|
|
195
|
+
yield expression.then
|
|
196
|
+
yield expression.else
|
|
197
|
+
return
|
|
198
|
+
default:
|
|
199
|
+
yield expression.left
|
|
200
|
+
yield expression.right
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const compile = (expression: Model.Expression): Expression => {
|
|
205
|
+
if (expressions.has(expression)) return expressions.get(expression)!
|
|
206
|
+
const stack = [{ expression, children: children(expression) }]
|
|
207
|
+
const active = new WeakSet<Model.Expression>([expression])
|
|
208
|
+
while (stack.length > 0) {
|
|
209
|
+
const frame = stack[stack.length - 1]!
|
|
210
|
+
const child = frame.children.next()
|
|
211
|
+
if (child.done) {
|
|
212
|
+
expressions.set(frame.expression, compileNode(frame.expression))
|
|
213
|
+
active.delete(frame.expression)
|
|
214
|
+
stack.pop()
|
|
215
|
+
} else if (!expressions.has(child.value)) {
|
|
216
|
+
if (active.has(child.value)) throw new TypeError('cyclic solver expression')
|
|
217
|
+
active.add(child.value)
|
|
218
|
+
stack.push({ expression: child.value, children: children(child.value) })
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return expressions.get(expression)!
|
|
222
|
+
}
|
|
223
|
+
|
|
159
224
|
return {
|
|
160
225
|
variables,
|
|
161
226
|
enums,
|
|
@@ -219,23 +284,6 @@ const numericValue = (context: Context<Name>, value: Arith<Name>): Model.Value =
|
|
|
219
284
|
throw new TypeError(`Z3 returned a non-rational objective value: ${value.sexpr()}`)
|
|
220
285
|
}
|
|
221
286
|
|
|
222
|
-
const withDeadline = async (
|
|
223
|
-
context: Context<Name>,
|
|
224
|
-
timeoutMs: number,
|
|
225
|
-
check: () => Promise<'sat' | 'unsat' | 'unknown'>
|
|
226
|
-
): Promise<{ readonly status: 'sat' | 'unsat' | 'unknown', readonly interrupted: boolean }> => {
|
|
227
|
-
let interrupted = false
|
|
228
|
-
const timer = setTimeout(() => {
|
|
229
|
-
interrupted = true
|
|
230
|
-
context.interrupt()
|
|
231
|
-
}, timeoutMs)
|
|
232
|
-
timer.unref()
|
|
233
|
-
try {
|
|
234
|
-
return { status: await check(), interrupted }
|
|
235
|
-
} finally {
|
|
236
|
-
clearTimeout(timer)
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
287
|
|
|
240
288
|
const unknown = (
|
|
241
289
|
backend: Adapter.Backend,
|
|
@@ -263,12 +311,13 @@ const addHard = (
|
|
|
263
311
|
compiled: Compiled,
|
|
264
312
|
track: boolean
|
|
265
313
|
): ReadonlyMap<number, string> => {
|
|
266
|
-
|
|
314
|
+
for (let index = 0; index < compiled.bounds.length; index += 1024) {
|
|
315
|
+
solver.add(...compiled.bounds.slice(index, index + 1024))
|
|
316
|
+
}
|
|
267
317
|
const trackers = new Map<number, string>()
|
|
268
318
|
for (const [id, expression] of compiled.constraints) {
|
|
269
319
|
if (track) {
|
|
270
|
-
const
|
|
271
|
-
const tracker = solver.ctx.Bool.const(symbol)
|
|
320
|
+
const tracker = solver.ctx.Bool.fresh('cave.constraint')
|
|
272
321
|
solver.addAndTrack(expression, tracker)
|
|
273
322
|
trackers.set(tracker.id(), id)
|
|
274
323
|
} else {
|
|
@@ -284,7 +333,7 @@ const coreFor = async (
|
|
|
284
333
|
limits: Adapter.Limits
|
|
285
334
|
): Promise<readonly string[] | undefined> => {
|
|
286
335
|
const solver = new context.Solver()
|
|
287
|
-
|
|
336
|
+
return withRelease<readonly string[] | undefined>(async () => {
|
|
288
337
|
solver.set('timeout', limits.timeoutMs)
|
|
289
338
|
const trackers = addHard(solver, compiled, true)
|
|
290
339
|
if ((await withDeadline(context, limits.timeoutMs, () => solver.check())).status !== 'unsat') return undefined
|
|
@@ -292,9 +341,7 @@ const coreFor = async (
|
|
|
292
341
|
.map(value => trackers.get(value.id()))
|
|
293
342
|
.filter((value): value is string => value !== undefined)
|
|
294
343
|
.sort()
|
|
295
|
-
}
|
|
296
|
-
solver.release()
|
|
297
|
-
}
|
|
344
|
+
}, () => solver.release())
|
|
298
345
|
}
|
|
299
346
|
|
|
300
347
|
const limitOutput = (
|
|
@@ -322,6 +369,9 @@ const solveModel = async (
|
|
|
322
369
|
request: Adapter.Request
|
|
323
370
|
): Promise<Adapter.Result> => {
|
|
324
371
|
const started = performance.now()
|
|
372
|
+
if (!Number.isSafeInteger(request.limits.timeoutMs) || request.limits.timeoutMs < 1 || request.limits.timeoutMs > 2147483647) {
|
|
373
|
+
return backendError(backend, started, new TypeError('Z3 timeoutMs must be in 1..2147483647'))
|
|
374
|
+
}
|
|
325
375
|
try {
|
|
326
376
|
api.setParam('memory_max_size', Math.max(1, Math.floor(request.limits.maxMemoryBytes / 1024 / 1024)))
|
|
327
377
|
const compiled = compiler(context, model)
|
|
@@ -329,7 +379,7 @@ const solveModel = async (
|
|
|
329
379
|
|
|
330
380
|
if (!optimize) {
|
|
331
381
|
const solver = new context.Solver()
|
|
332
|
-
|
|
382
|
+
return await withRelease<Adapter.Result>(async () => {
|
|
333
383
|
solver.set('timeout', request.limits.timeoutMs)
|
|
334
384
|
const trackers = addHard(solver, compiled, request.unsatCore)
|
|
335
385
|
const checked = await withDeadline(context, request.limits.timeoutMs, () => solver.check())
|
|
@@ -359,20 +409,17 @@ const solveModel = async (
|
|
|
359
409
|
diagnostics: [],
|
|
360
410
|
elapsedMs: Math.round(performance.now() - started)
|
|
361
411
|
}, request.limits)
|
|
362
|
-
}
|
|
363
|
-
solver.release()
|
|
364
|
-
}
|
|
412
|
+
}, () => solver.release())
|
|
365
413
|
}
|
|
366
414
|
|
|
367
415
|
const optimizer = new context.Optimize()
|
|
368
|
-
|
|
416
|
+
return await withRelease<Adapter.Result>(async () => {
|
|
369
417
|
optimizer.set('timeout', request.limits.timeoutMs)
|
|
370
418
|
optimizer.set('priority', 'lex')
|
|
371
419
|
addHard(optimizer, compiled, false)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
420
|
+
const handles = compiled.objectives.map(objective => objective.direction === 'minimize'
|
|
421
|
+
? optimizer.minimize(objective.expression)
|
|
422
|
+
: optimizer.maximize(objective.expression))
|
|
376
423
|
// Soft preferences are deliberately the lowest-priority objective. Their
|
|
377
424
|
// weights are explicit model data and never derived from CAVE confidence.
|
|
378
425
|
for (const constraint of compiled.softConstraints) {
|
|
@@ -394,6 +441,23 @@ const solveModel = async (
|
|
|
394
441
|
}, request.limits)
|
|
395
442
|
}
|
|
396
443
|
const z3Model = optimizer.model()
|
|
444
|
+
const rational = (value: unknown): Model.Rational | undefined => {
|
|
445
|
+
if (context.isIntVal(value)) return value.value().toString()
|
|
446
|
+
if (context.isRealVal(value)) return {
|
|
447
|
+
numerator: value.value().numerator.toString(),
|
|
448
|
+
denominator: value.value().denominator.toString()
|
|
449
|
+
}
|
|
450
|
+
return undefined
|
|
451
|
+
}
|
|
452
|
+
for (const [index, objective] of compiled.objectives.entries()) {
|
|
453
|
+
const lower = rational(optimizer.getLower(handles[index]!))
|
|
454
|
+
const upper = rational(optimizer.getUpper(handles[index]!))
|
|
455
|
+
const attained = rational(z3Model.eval(objective.expression, true))
|
|
456
|
+
if (lower === undefined || upper === undefined || attained === undefined ||
|
|
457
|
+
Exact.compare(lower, upper) !== 0 || Exact.compare(lower, attained) !== 0) {
|
|
458
|
+
return unknown(backend, started, `objective ${JSON.stringify(objective.id)} has no proved finite attained optimum`, false)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
397
461
|
return limitOutput({
|
|
398
462
|
status: 'optimal',
|
|
399
463
|
assignment: assignment(context, compiled, model, z3Model),
|
|
@@ -406,9 +470,7 @@ const solveModel = async (
|
|
|
406
470
|
diagnostics: [],
|
|
407
471
|
elapsedMs: Math.round(performance.now() - started)
|
|
408
472
|
}, request.limits)
|
|
409
|
-
}
|
|
410
|
-
optimizer.release()
|
|
411
|
-
}
|
|
473
|
+
}, () => optimizer.release())
|
|
412
474
|
} catch (error) {
|
|
413
475
|
if (api.Z3.get_estimated_alloc_size() > BigInt(request.limits.maxMemoryBytes)) {
|
|
414
476
|
return {
|
|
@@ -428,17 +490,19 @@ const solveModel = async (
|
|
|
428
490
|
}
|
|
429
491
|
|
|
430
492
|
let singleton: Promise<Runtime> | undefined
|
|
493
|
+
let shutdown: Promise<void> | undefined
|
|
431
494
|
|
|
432
495
|
const initialize = async (): Promise<Runtime> => {
|
|
433
496
|
const started = performance.now()
|
|
434
497
|
// Keep this as the only runtime import: importing @cavelang/solver-z3 itself
|
|
435
498
|
// must not load the 34 MB Wasm artifact or start worker threads.
|
|
436
499
|
const api = await import('z3-solver').then(module => module.init())
|
|
500
|
+
guardCleanup(api.Z3)
|
|
437
501
|
const context = api.Context('cave')
|
|
438
502
|
const backend = Object.freeze({ name: 'z3-wasm', version: api.Z3.get_full_version() })
|
|
439
503
|
let tail: Promise<void> = Promise.resolve()
|
|
440
504
|
let closing = false
|
|
441
|
-
let
|
|
505
|
+
let closePromise: Promise<void> | undefined
|
|
442
506
|
|
|
443
507
|
const enqueue = <T>(work: () => Promise<T>): Promise<T> => {
|
|
444
508
|
const result = tail.then(work, work)
|
|
@@ -454,26 +518,24 @@ const initialize = async (): Promise<Runtime> => {
|
|
|
454
518
|
if (closing) return Promise.resolve(backendError(backend, performance.now(), new Error('Z3 runtime is closed')))
|
|
455
519
|
return enqueue(() => solveModel(api, context, backend, model, request))
|
|
456
520
|
},
|
|
457
|
-
close:
|
|
458
|
-
if (
|
|
521
|
+
close: () => {
|
|
522
|
+
if (closePromise) return closePromise
|
|
459
523
|
closing = true
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
// terminating the pool. Without this turn, Emscripten can report a
|
|
464
|
-
// late `cleanupThread` command from a worker we just terminated.
|
|
465
|
-
await new Promise<void>(resolve => setTimeout(resolve, 0))
|
|
466
|
-
api.em?.PThread?.terminateAllThreads?.()
|
|
467
|
-
closed = true
|
|
524
|
+
closePromise = enqueue(async () => {
|
|
525
|
+
// Await worker exits before Emscripten discards their message handlers.
|
|
526
|
+
await closeWorkers(api.em?.PThread)
|
|
468
527
|
singleton = undefined
|
|
528
|
+
shutdown = undefined
|
|
469
529
|
})
|
|
530
|
+
shutdown = closePromise
|
|
531
|
+
return closePromise
|
|
470
532
|
}
|
|
471
533
|
}
|
|
472
534
|
return runtime
|
|
473
535
|
}
|
|
474
536
|
|
|
475
537
|
/** Lazily initialize and reuse one process-wide Z3 runtime. */
|
|
476
|
-
export const create = (): Promise<Runtime> => singleton ??= initialize().catch(error => {
|
|
538
|
+
export const create = (): Promise<Runtime> => shutdown ? shutdown.then(create) : singleton ??= initialize().catch(error => {
|
|
477
539
|
singleton = undefined
|
|
478
540
|
throw error
|
|
479
541
|
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { errorMessage } from './error-message.ts'
|
|
2
|
+
|
|
3
|
+
/** Await the operation before releasing its owned native resource. */
|
|
4
|
+
export const withRelease = async <T>(operation: () => Promise<T>, release: () => void): Promise<T> => {
|
|
5
|
+
let failed = false
|
|
6
|
+
let operationError: unknown
|
|
7
|
+
try {
|
|
8
|
+
return await operation()
|
|
9
|
+
} catch (error) {
|
|
10
|
+
failed = true
|
|
11
|
+
operationError = error
|
|
12
|
+
throw error
|
|
13
|
+
} finally {
|
|
14
|
+
try { release() } catch (error) {
|
|
15
|
+
if (!failed) throw error
|
|
16
|
+
throw new AggregateError([operationError, error],
|
|
17
|
+
`Z3 operation failed: ${errorMessage(operationError)}; native release also failed: ${errorMessage(error)}`,
|
|
18
|
+
{ cause: operationError })
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/workflow-fixture.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { errorMessage } from './error-message.ts'
|
|
1
2
|
/** Named CLI fixture for the solver workflow API. No raw model input is accepted. */
|
|
2
3
|
|
|
3
4
|
import { Adapter, Model, Workflow } from '@cavelang/solver'
|
|
4
|
-
import { create } from './runtime.ts'
|
|
5
|
+
import { create, type Runtime } from './runtime.ts'
|
|
5
6
|
|
|
6
7
|
export type Output = { readonly code: number, readonly out: string, readonly err: string }
|
|
7
8
|
|
|
@@ -18,7 +19,9 @@ Options:
|
|
|
18
19
|
--deployment-frequency <1..12> Default: 6
|
|
19
20
|
--from <1..12> Sensitivity start; default: 1
|
|
20
21
|
--to <1..12> Sensitivity end; default: 12
|
|
21
|
-
--timeout-ms <
|
|
22
|
+
--timeout-ms <1..2147483647> Per solver run; default: 10000
|
|
23
|
+
--max-explanation-bits <integer> Local evaluation; default: ${Adapter.defaultLimits.maxExplanationBits}
|
|
24
|
+
--max-explanation-work <integer> Cumulative local evaluation; default: ${Adapter.defaultLimits.maxExplanationWork}
|
|
22
25
|
|
|
23
26
|
The fixture is the only selectable model. Inputs are bounded and typed before
|
|
24
27
|
the optional Z3 adapter runs; arbitrary expressions and SMT-LIB are rejected.
|
|
@@ -121,6 +124,8 @@ type Parsed = {
|
|
|
121
124
|
readonly from: number
|
|
122
125
|
readonly to: number
|
|
123
126
|
readonly timeoutMs: number
|
|
127
|
+
readonly maxExplanationBits: number
|
|
128
|
+
readonly maxExplanationWork: number
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
const parseInteger = (name: string, value: string | undefined, min: number, max?: number): number => {
|
|
@@ -143,7 +148,7 @@ const parse = (argv: readonly string[]): Parsed | Output => {
|
|
|
143
148
|
for (let index = 2; index < argv.length; index += 2) {
|
|
144
149
|
const flag = argv[index]
|
|
145
150
|
const value = argv[index + 1]
|
|
146
|
-
if (flag === undefined || !['--team-size', '--deployment-frequency', '--from', '--to', '--timeout-ms'].includes(flag)) {
|
|
151
|
+
if (flag === undefined || !['--team-size', '--deployment-frequency', '--from', '--to', '--timeout-ms', '--max-explanation-bits', '--max-explanation-work'].includes(flag)) {
|
|
147
152
|
return fail(`unknown workflow option ${JSON.stringify(flag ?? '')}`)
|
|
148
153
|
}
|
|
149
154
|
if (value === undefined || value.startsWith('--')) return fail(`${flag} needs a value`)
|
|
@@ -162,10 +167,12 @@ const parse = (argv: readonly string[]): Parsed | Output => {
|
|
|
162
167
|
),
|
|
163
168
|
from,
|
|
164
169
|
to,
|
|
165
|
-
timeoutMs: parseInteger('--timeout-ms', values.get('--timeout-ms') ?? '10000', 1)
|
|
170
|
+
timeoutMs: parseInteger('--timeout-ms', values.get('--timeout-ms') ?? '10000', 1, 2147483647),
|
|
171
|
+
maxExplanationBits: parseInteger('--max-explanation-bits', values.get('--max-explanation-bits') ?? String(Adapter.defaultLimits.maxExplanationBits), 1),
|
|
172
|
+
maxExplanationWork: parseInteger('--max-explanation-work', values.get('--max-explanation-work') ?? String(Adapter.defaultLimits.maxExplanationWork), 1)
|
|
166
173
|
}
|
|
167
174
|
} catch (error) {
|
|
168
|
-
return fail(
|
|
175
|
+
return fail(errorMessage(error))
|
|
169
176
|
}
|
|
170
177
|
}
|
|
171
178
|
|
|
@@ -176,11 +183,9 @@ export const runWorkflowFixture = async (
|
|
|
176
183
|
): Promise<Output> => {
|
|
177
184
|
const parsed = parse(argv)
|
|
178
185
|
if ('code' in parsed) return parsed
|
|
179
|
-
const
|
|
180
|
-
const adapter = supplied ?? runtime!
|
|
181
|
-
const options: Adapter.Options = { limits: { timeoutMs: parsed.timeoutMs } }
|
|
186
|
+
const options: Adapter.Options = { limits: { timeoutMs: parsed.timeoutMs, maxExplanationBits: parsed.maxExplanationBits, maxExplanationWork: parsed.maxExplanationWork } }
|
|
182
187
|
const context = { snapshot: { transactionTime: null } } as const
|
|
183
|
-
|
|
188
|
+
const execute = async (adapter: Adapter.t): Promise<Output> => {
|
|
184
189
|
switch (parsed.operation) {
|
|
185
190
|
case 'feasibility':
|
|
186
191
|
return ok(await Workflow.feasibility(
|
|
@@ -219,9 +224,23 @@ export const runWorkflowFixture = async (
|
|
|
219
224
|
}, options, context))
|
|
220
225
|
}
|
|
221
226
|
}
|
|
227
|
+
}
|
|
228
|
+
let runtime: Runtime | undefined
|
|
229
|
+
let result: Output
|
|
230
|
+
try {
|
|
231
|
+
runtime = supplied === undefined ? await create() : undefined
|
|
232
|
+
result = await execute(supplied ?? runtime!)
|
|
222
233
|
} catch (error) {
|
|
223
|
-
|
|
224
|
-
}
|
|
234
|
+
result = { code: 1, out: '', err: `${errorMessage(error)}\n` }
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
225
237
|
await runtime?.close()
|
|
238
|
+
} catch (error) {
|
|
239
|
+
result = {
|
|
240
|
+
code: 1,
|
|
241
|
+
out: '',
|
|
242
|
+
err: `${result.err}Z3 shutdown failed: ${errorMessage(error)}\n`
|
|
243
|
+
}
|
|
226
244
|
}
|
|
245
|
+
return result
|
|
227
246
|
}
|