@cavelang/solver-z3 0.35.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/BENCHMARK.md +56 -3
  2. package/README.md +267 -9
  3. package/dist/scripts/benchmark.js +35 -13
  4. package/dist/scripts/benchmark.js.map +1 -1
  5. package/dist/scripts/memory-stress.d.ts +2 -0
  6. package/dist/scripts/memory-stress.d.ts.map +1 -0
  7. package/dist/scripts/memory-stress.js +74 -0
  8. package/dist/scripts/memory-stress.js.map +1 -0
  9. package/dist/src/cleanup.d.ts +8 -0
  10. package/dist/src/cleanup.d.ts.map +1 -0
  11. package/dist/src/cleanup.js +65 -0
  12. package/dist/src/cleanup.js.map +1 -0
  13. package/dist/src/close-workers.d.ts +12 -0
  14. package/dist/src/close-workers.d.ts.map +1 -0
  15. package/dist/src/close-workers.js +27 -0
  16. package/dist/src/close-workers.js.map +1 -0
  17. package/dist/src/deadline.d.ts +7 -0
  18. package/dist/src/deadline.d.ts.map +1 -0
  19. package/dist/src/deadline.js +46 -0
  20. package/dist/src/deadline.js.map +1 -0
  21. package/dist/src/error-message.d.ts +3 -0
  22. package/dist/src/error-message.d.ts.map +1 -0
  23. package/dist/src/error-message.js +10 -0
  24. package/dist/src/error-message.js.map +1 -0
  25. package/dist/src/runtime.d.ts.map +1 -1
  26. package/dist/src/runtime.js +137 -72
  27. package/dist/src/runtime.js.map +1 -1
  28. package/dist/src/with-release.d.ts +3 -0
  29. package/dist/src/with-release.d.ts.map +1 -0
  30. package/dist/src/with-release.js +25 -0
  31. package/dist/src/with-release.js.map +1 -0
  32. package/dist/src/workflow-fixture.d.ts.map +1 -1
  33. package/dist/src/workflow-fixture.js +27 -10
  34. package/dist/src/workflow-fixture.js.map +1 -1
  35. package/package.json +4 -3
  36. package/scripts/benchmark.ts +35 -12
  37. package/scripts/memory-stress.ts +72 -0
  38. package/src/cleanup.ts +55 -0
  39. package/src/close-workers.ts +28 -0
  40. package/src/deadline.ts +40 -0
  41. package/src/error-message.ts +5 -0
  42. package/src/runtime.ts +132 -70
  43. package/src/with-release.ts +21 -0
  44. 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: error instanceof Error ? error.message : String(error)
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 compile = (expression: Model.Expression): Expression => {
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(enums.get(expression.domain)!.indexOf(expression.value))
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(compile(expression.value)).not()
124
- case 'and': return context.And(...expression.operands.map(value => asBool(compile(value))))
125
- case 'or': return context.Or(...expression.operands.map(value => asBool(compile(value))))
126
- case 'implies': return context.Implies(asBool(compile(expression.left)), asBool(compile(expression.right)))
127
- case 'eq': return compile(expression.left).eq(compile(expression.right))
128
- case 'neq': return compile(expression.left).neq(compile(expression.right))
129
- case 'lt': return asArith(compile(expression.left)).lt(asArith(compile(expression.right)))
130
- case 'lte': return asArith(compile(expression.left)).le(asArith(compile(expression.right)))
131
- case 'gt': return asArith(compile(expression.left)).gt(asArith(compile(expression.right)))
132
- case 'gte': return asArith(compile(expression.left)).ge(asArith(compile(expression.right)))
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(compile(value)))
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(compile(value)))
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(compile(expression.left)).sub(asArith(compile(expression.right)))
165
+ case 'subtract': return asArith(compiledChild(expression.left)).sub(asArith(compiledChild(expression.right)))
142
166
  case 'divide': {
143
- const left = asArith(compile(expression.left))
144
- const right = asArith(compile(expression.right))
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(compile(expression.value)).neg()
174
+ case 'negate': return asArith(compiledChild(expression.value)).neg()
151
175
  case 'if': return context.If(
152
- asBool(compile(expression.condition)),
153
- compile(expression.then),
154
- compile(expression.else)
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
- solver.add(...compiled.bounds)
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 symbol = `cave.constraint.${id}`
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
- try {
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
- } finally {
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
- try {
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
- } finally {
363
- solver.release()
364
- }
412
+ }, () => solver.release())
365
413
  }
366
414
 
367
415
  const optimizer = new context.Optimize()
368
- try {
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
- for (const objective of compiled.objectives) {
373
- if (objective.direction === 'minimize') optimizer.minimize(objective.expression)
374
- else optimizer.maximize(objective.expression)
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
- } finally {
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 closed = false
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: async () => {
458
- if (closed) return
521
+ close: () => {
522
+ if (closePromise) return closePromise
459
523
  closing = true
460
- await enqueue(async () => {
461
- if (closed) return
462
- // Let the completed pthread post its cleanup message before forcibly
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
+ }
@@ -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 <positive integer> Per solver run; default: 10000
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(error instanceof Error ? error.message : String(error))
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 runtime = supplied === undefined ? await create() : undefined
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
- try {
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
- return { code: 1, out: '', err: `${error instanceof Error ? error.message : String(error)}\n` }
224
- } finally {
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
  }