@cavelang/solver-z3 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Authors.md +5 -0
- package/BENCHMARK.md +44 -0
- package/License.md +6 -0
- package/README.md +105 -0
- package/dist/scripts/benchmark.d.ts +2 -0
- package/dist/scripts/benchmark.d.ts.map +1 -0
- package/dist/scripts/benchmark.js +96 -0
- package/dist/scripts/benchmark.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +9 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/runtime.d.ts +10 -0
- package/dist/src/runtime.d.ts.map +1 -0
- package/dist/src/runtime.js +399 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/workflow-fixture.d.ts +20 -0
- package/dist/src/workflow-fixture.d.ts.map +1 -0
- package/dist/src/workflow-fixture.js +185 -0
- package/dist/src/workflow-fixture.js.map +1 -0
- package/dist/src/workflow-main.d.ts +3 -0
- package/dist/src/workflow-main.d.ts.map +1 -0
- package/dist/src/workflow-main.js +9 -0
- package/dist/src/workflow-main.js.map +1 -0
- package/package.json +67 -0
- package/scripts/benchmark.ts +101 -0
- package/src/index.ts +10 -0
- package/src/runtime.ts +479 -0
- package/src/workflow-fixture.ts +227 -0
- package/src/workflow-main.ts +8 -0
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks'
|
|
2
|
+
import { Adapter, Exact, Model } from '@cavelang/solver'
|
|
3
|
+
import type {
|
|
4
|
+
AnyExpr,
|
|
5
|
+
Arith,
|
|
6
|
+
Bool,
|
|
7
|
+
Context,
|
|
8
|
+
Model as Z3Model,
|
|
9
|
+
Optimize,
|
|
10
|
+
Solver
|
|
11
|
+
} from 'z3-solver'
|
|
12
|
+
|
|
13
|
+
type Z3 = Awaited<ReturnType<typeof import('z3-solver')['init']>>
|
|
14
|
+
type Name = 'cave'
|
|
15
|
+
type Expression = AnyExpr<Name>
|
|
16
|
+
|
|
17
|
+
type Compiled = {
|
|
18
|
+
readonly variables: ReadonlyMap<string, Expression>
|
|
19
|
+
readonly enums: ReadonlyMap<string, readonly string[]>
|
|
20
|
+
readonly bounds: readonly Bool<Name>[]
|
|
21
|
+
readonly constraints: ReadonlyMap<string, Bool<Name>>
|
|
22
|
+
readonly softConstraints: readonly {
|
|
23
|
+
readonly expression: Bool<Name>
|
|
24
|
+
readonly weight: string
|
|
25
|
+
}[]
|
|
26
|
+
readonly objectives: readonly {
|
|
27
|
+
readonly id: string
|
|
28
|
+
readonly direction: 'minimize' | 'maximize'
|
|
29
|
+
readonly expression: Arith<Name>
|
|
30
|
+
}[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type Runtime = Adapter.t & {
|
|
34
|
+
/** Time spent loading and initializing the Z3 Wasm module. */
|
|
35
|
+
readonly initializationMs: number
|
|
36
|
+
/** Wait for queued checks and terminate Z3's Emscripten workers. Idempotent. */
|
|
37
|
+
readonly close: () => Promise<void>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const backendError = (
|
|
41
|
+
backend: Adapter.Backend,
|
|
42
|
+
started: number,
|
|
43
|
+
error: unknown
|
|
44
|
+
): Adapter.Result => ({
|
|
45
|
+
status: 'unknown',
|
|
46
|
+
reason: {
|
|
47
|
+
kind: 'backend-error',
|
|
48
|
+
message: error instanceof Error ? error.message : String(error)
|
|
49
|
+
},
|
|
50
|
+
backend,
|
|
51
|
+
diagnostics: [],
|
|
52
|
+
elapsedMs: Math.round(performance.now() - started)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const exact = (value: Model.Rational): { readonly numerator: bigint, readonly denominator: bigint } => {
|
|
56
|
+
const normalized = Exact.rational(value)
|
|
57
|
+
return {
|
|
58
|
+
numerator: BigInt(normalized.numerator),
|
|
59
|
+
denominator: BigInt(normalized.denominator)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const rationalText = (value: Model.Rational): string => {
|
|
64
|
+
const normalized = Exact.rational(value)
|
|
65
|
+
return `${normalized.numerator}/${normalized.denominator}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const asBool = (value: Expression): Bool<Name> => value as Bool<Name>
|
|
69
|
+
const asArith = (value: Expression): Arith<Name> => value as Arith<Name>
|
|
70
|
+
|
|
71
|
+
const compiler = (
|
|
72
|
+
context: Context<Name>,
|
|
73
|
+
model: Model.t
|
|
74
|
+
): Compiled => {
|
|
75
|
+
const compareText = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0
|
|
76
|
+
const enums = new Map((model.enums ?? []).map(domain => [
|
|
77
|
+
domain.id,
|
|
78
|
+
[...domain.values].sort(compareText)
|
|
79
|
+
] as const))
|
|
80
|
+
const variables = new Map<string, Expression>()
|
|
81
|
+
const bounds: Bool<Name>[] = []
|
|
82
|
+
|
|
83
|
+
for (const variable of model.variables) {
|
|
84
|
+
switch (variable.sort) {
|
|
85
|
+
case 'bool':
|
|
86
|
+
variables.set(variable.id, context.Bool.const(variable.id))
|
|
87
|
+
break
|
|
88
|
+
case 'int': {
|
|
89
|
+
const expression = context.Int.const(variable.id)
|
|
90
|
+
variables.set(variable.id, expression)
|
|
91
|
+
bounds.push(expression.ge(Exact.integer(variable.min)), expression.le(Exact.integer(variable.max)))
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
case 'real': {
|
|
95
|
+
const expression = context.Real.const(variable.id)
|
|
96
|
+
variables.set(variable.id, expression)
|
|
97
|
+
if (variable.min !== undefined) bounds.push(expression.ge(exact(variable.min)))
|
|
98
|
+
if (variable.max !== undefined) bounds.push(expression.le(exact(variable.max)))
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
case 'enum': {
|
|
102
|
+
// A finite enum is encoded as a bounded integer. The mapping is stable
|
|
103
|
+
// across semantically irrelevant declaration reordering.
|
|
104
|
+
const values = enums.get(variable.domain)!
|
|
105
|
+
const expression = context.Int.const(variable.id)
|
|
106
|
+
variables.set(variable.id, expression)
|
|
107
|
+
bounds.push(expression.ge(0), expression.lt(values.length))
|
|
108
|
+
break
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const compile = (expression: Model.Expression): Expression => {
|
|
114
|
+
switch (expression.kind) {
|
|
115
|
+
case 'literal':
|
|
116
|
+
switch (expression.sort) {
|
|
117
|
+
case 'bool': return context.Bool.val(expression.value)
|
|
118
|
+
case 'int': return context.Int.val(Exact.integer(expression.value))
|
|
119
|
+
case 'real': return context.Real.val(exact(expression.value))
|
|
120
|
+
case 'enum': return context.Int.val(enums.get(expression.domain)!.indexOf(expression.value))
|
|
121
|
+
}
|
|
122
|
+
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)))
|
|
133
|
+
case 'add': {
|
|
134
|
+
const [first, ...rest] = expression.operands.map(value => asArith(compile(value)))
|
|
135
|
+
return rest.reduce((left, right) => left.add(right), first!)
|
|
136
|
+
}
|
|
137
|
+
case 'multiply': {
|
|
138
|
+
const [first, ...rest] = expression.operands.map(value => asArith(compile(value)))
|
|
139
|
+
return rest.reduce((left, right) => left.mul(right), first!)
|
|
140
|
+
}
|
|
141
|
+
case 'subtract': return asArith(compile(expression.left)).sub(asArith(compile(expression.right)))
|
|
142
|
+
case 'divide': {
|
|
143
|
+
const left = asArith(compile(expression.left))
|
|
144
|
+
const right = asArith(compile(expression.right))
|
|
145
|
+
// Portable division is exact-real division even when both operands are
|
|
146
|
+
// integers; Z3's native Int / Int operation would truncate instead.
|
|
147
|
+
const real = (value: Arith<Name>): Arith<Name> => context.isInt(value) ? context.ToReal(value) : value
|
|
148
|
+
return real(left).div(real(right))
|
|
149
|
+
}
|
|
150
|
+
case 'negate': return asArith(compile(expression.value)).neg()
|
|
151
|
+
case 'if': return context.If(
|
|
152
|
+
asBool(compile(expression.condition)),
|
|
153
|
+
compile(expression.then),
|
|
154
|
+
compile(expression.else)
|
|
155
|
+
) as Expression
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
variables,
|
|
161
|
+
enums,
|
|
162
|
+
bounds,
|
|
163
|
+
constraints: new Map(model.constraints.map(constraint => [constraint.id, asBool(compile(constraint.expression))])),
|
|
164
|
+
softConstraints: (model.softConstraints ?? []).map(constraint => ({
|
|
165
|
+
expression: asBool(compile(constraint.expression)),
|
|
166
|
+
weight: rationalText(constraint.weight)
|
|
167
|
+
})),
|
|
168
|
+
objectives: (model.objectives ?? []).map(objective => ({
|
|
169
|
+
id: objective.id,
|
|
170
|
+
direction: objective.direction,
|
|
171
|
+
expression: asArith(compile(objective.expression))
|
|
172
|
+
}))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const assignment = (
|
|
177
|
+
context: Context<Name>,
|
|
178
|
+
compiled: Compiled,
|
|
179
|
+
model: Model.t,
|
|
180
|
+
z3Model: Z3Model<Name>
|
|
181
|
+
): Model.Assignment => Object.fromEntries(
|
|
182
|
+
[...model.variables]
|
|
183
|
+
.sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0)
|
|
184
|
+
.map(variable => {
|
|
185
|
+
const value = z3Model.eval(compiled.variables.get(variable.id)!, true)
|
|
186
|
+
switch (variable.sort) {
|
|
187
|
+
case 'bool':
|
|
188
|
+
if (context.isTrue(value)) return [variable.id, { sort: 'bool', value: true }]
|
|
189
|
+
if (context.isFalse(value)) return [variable.id, { sort: 'bool', value: false }]
|
|
190
|
+
throw new TypeError(`Z3 returned a non-Boolean value for ${JSON.stringify(variable.id)}`)
|
|
191
|
+
case 'int':
|
|
192
|
+
if (!context.isIntVal(value)) throw new TypeError(`Z3 returned a non-integer value for ${JSON.stringify(variable.id)}`)
|
|
193
|
+
return [variable.id, { sort: 'int', value: value.value().toString() }]
|
|
194
|
+
case 'real':
|
|
195
|
+
if (!context.isRealVal(value)) throw new TypeError(`Z3 returned a non-rational value for ${JSON.stringify(variable.id)}`)
|
|
196
|
+
return [variable.id, {
|
|
197
|
+
sort: 'real',
|
|
198
|
+
numerator: value.value().numerator.toString(),
|
|
199
|
+
denominator: value.value().denominator.toString()
|
|
200
|
+
}]
|
|
201
|
+
case 'enum': {
|
|
202
|
+
if (!context.isIntVal(value)) throw new TypeError(`Z3 returned a non-enum value for ${JSON.stringify(variable.id)}`)
|
|
203
|
+
const index = Number(value.value())
|
|
204
|
+
const member = compiled.enums.get(variable.domain)?.[index]
|
|
205
|
+
if (member === undefined) throw new TypeError(`Z3 returned an out-of-domain enum value for ${JSON.stringify(variable.id)}`)
|
|
206
|
+
return [variable.id, { sort: 'enum', domain: variable.domain, value: member }]
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
) as Model.Assignment
|
|
211
|
+
|
|
212
|
+
const numericValue = (context: Context<Name>, value: Arith<Name>): Model.Value => {
|
|
213
|
+
if (context.isIntVal(value)) return { sort: 'int', value: value.value().toString() }
|
|
214
|
+
if (context.isRealVal(value)) return {
|
|
215
|
+
sort: 'real',
|
|
216
|
+
numerator: value.value().numerator.toString(),
|
|
217
|
+
denominator: value.value().denominator.toString()
|
|
218
|
+
}
|
|
219
|
+
throw new TypeError(`Z3 returned a non-rational objective value: ${value.sexpr()}`)
|
|
220
|
+
}
|
|
221
|
+
|
|
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
|
+
|
|
240
|
+
const unknown = (
|
|
241
|
+
backend: Adapter.Backend,
|
|
242
|
+
started: number,
|
|
243
|
+
reason: string,
|
|
244
|
+
interrupted: boolean
|
|
245
|
+
): Adapter.Result => {
|
|
246
|
+
const timeout = interrupted || /timeout|canceled|cancelled/i.test(reason)
|
|
247
|
+
const memory = /memory|memout/i.test(reason)
|
|
248
|
+
return {
|
|
249
|
+
status: 'unknown',
|
|
250
|
+
reason: timeout
|
|
251
|
+
? { kind: 'timeout', message: reason || 'Z3 deadline reached', limit: 'timeoutMs' }
|
|
252
|
+
: memory
|
|
253
|
+
? { kind: 'resource-limit', message: reason, limit: 'maxMemoryBytes' }
|
|
254
|
+
: { kind: 'indeterminate', message: reason || 'Z3 returned unknown' },
|
|
255
|
+
backend,
|
|
256
|
+
diagnostics: [],
|
|
257
|
+
elapsedMs: Math.round(performance.now() - started)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const addHard = (
|
|
262
|
+
solver: Solver<Name> | Optimize<Name>,
|
|
263
|
+
compiled: Compiled,
|
|
264
|
+
track: boolean
|
|
265
|
+
): ReadonlyMap<number, string> => {
|
|
266
|
+
solver.add(...compiled.bounds)
|
|
267
|
+
const trackers = new Map<number, string>()
|
|
268
|
+
for (const [id, expression] of compiled.constraints) {
|
|
269
|
+
if (track) {
|
|
270
|
+
const symbol = `cave.constraint.${id}`
|
|
271
|
+
const tracker = solver.ctx.Bool.const(symbol)
|
|
272
|
+
solver.addAndTrack(expression, tracker)
|
|
273
|
+
trackers.set(tracker.id(), id)
|
|
274
|
+
} else {
|
|
275
|
+
solver.add(expression)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return trackers
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const coreFor = async (
|
|
282
|
+
context: Context<Name>,
|
|
283
|
+
compiled: Compiled,
|
|
284
|
+
limits: Adapter.Limits
|
|
285
|
+
): Promise<readonly string[] | undefined> => {
|
|
286
|
+
const solver = new context.Solver()
|
|
287
|
+
try {
|
|
288
|
+
solver.set('timeout', limits.timeoutMs)
|
|
289
|
+
const trackers = addHard(solver, compiled, true)
|
|
290
|
+
if ((await withDeadline(context, limits.timeoutMs, () => solver.check())).status !== 'unsat') return undefined
|
|
291
|
+
return [...solver.unsatCore()]
|
|
292
|
+
.map(value => trackers.get(value.id()))
|
|
293
|
+
.filter((value): value is string => value !== undefined)
|
|
294
|
+
.sort()
|
|
295
|
+
} finally {
|
|
296
|
+
solver.release()
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const limitOutput = (
|
|
301
|
+
result: Adapter.Result,
|
|
302
|
+
limits: Adapter.Limits
|
|
303
|
+
): Adapter.Result => Buffer.byteLength(JSON.stringify(result)) <= limits.maxOutputBytes
|
|
304
|
+
? result
|
|
305
|
+
: {
|
|
306
|
+
status: 'unknown',
|
|
307
|
+
reason: {
|
|
308
|
+
kind: 'resource-limit',
|
|
309
|
+
message: 'solver result exceeds maxOutputBytes',
|
|
310
|
+
limit: 'maxOutputBytes'
|
|
311
|
+
},
|
|
312
|
+
backend: result.backend,
|
|
313
|
+
diagnostics: result.diagnostics,
|
|
314
|
+
elapsedMs: result.elapsedMs
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const solveModel = async (
|
|
318
|
+
api: Z3,
|
|
319
|
+
context: Context<Name>,
|
|
320
|
+
backend: Adapter.Backend,
|
|
321
|
+
model: Model.t,
|
|
322
|
+
request: Adapter.Request
|
|
323
|
+
): Promise<Adapter.Result> => {
|
|
324
|
+
const started = performance.now()
|
|
325
|
+
try {
|
|
326
|
+
api.setParam('memory_max_size', Math.max(1, Math.floor(request.limits.maxMemoryBytes / 1024 / 1024)))
|
|
327
|
+
const compiled = compiler(context, model)
|
|
328
|
+
const optimize = compiled.objectives.length > 0 || compiled.softConstraints.length > 0
|
|
329
|
+
|
|
330
|
+
if (!optimize) {
|
|
331
|
+
const solver = new context.Solver()
|
|
332
|
+
try {
|
|
333
|
+
solver.set('timeout', request.limits.timeoutMs)
|
|
334
|
+
const trackers = addHard(solver, compiled, request.unsatCore)
|
|
335
|
+
const checked = await withDeadline(context, request.limits.timeoutMs, () => solver.check())
|
|
336
|
+
if (checked.status === 'unknown') {
|
|
337
|
+
return unknown(backend, started, solver.reasonUnknown(), checked.interrupted)
|
|
338
|
+
}
|
|
339
|
+
if (checked.status === 'unsat') {
|
|
340
|
+
const core = request.unsatCore
|
|
341
|
+
? [...solver.unsatCore()]
|
|
342
|
+
.map(value => trackers.get(value.id()))
|
|
343
|
+
.filter((value): value is string => value !== undefined)
|
|
344
|
+
.sort()
|
|
345
|
+
: undefined
|
|
346
|
+
return limitOutput({
|
|
347
|
+
status: 'unsatisfied',
|
|
348
|
+
...(core === undefined ? {} : { core }),
|
|
349
|
+
infeasibilityProved: true,
|
|
350
|
+
backend,
|
|
351
|
+
diagnostics: [],
|
|
352
|
+
elapsedMs: Math.round(performance.now() - started)
|
|
353
|
+
}, request.limits)
|
|
354
|
+
}
|
|
355
|
+
return limitOutput({
|
|
356
|
+
status: 'satisfied',
|
|
357
|
+
assignment: assignment(context, compiled, model, solver.model()),
|
|
358
|
+
backend,
|
|
359
|
+
diagnostics: [],
|
|
360
|
+
elapsedMs: Math.round(performance.now() - started)
|
|
361
|
+
}, request.limits)
|
|
362
|
+
} finally {
|
|
363
|
+
solver.release()
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const optimizer = new context.Optimize()
|
|
368
|
+
try {
|
|
369
|
+
optimizer.set('timeout', request.limits.timeoutMs)
|
|
370
|
+
optimizer.set('priority', 'lex')
|
|
371
|
+
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
|
+
}
|
|
376
|
+
// Soft preferences are deliberately the lowest-priority objective. Their
|
|
377
|
+
// weights are explicit model data and never derived from CAVE confidence.
|
|
378
|
+
for (const constraint of compiled.softConstraints) {
|
|
379
|
+
optimizer.addSoft(constraint.expression, constraint.weight)
|
|
380
|
+
}
|
|
381
|
+
const checked = await withDeadline(context, request.limits.timeoutMs, () => optimizer.check())
|
|
382
|
+
if (checked.status === 'unknown') {
|
|
383
|
+
const reason = api.Z3.optimize_get_reason_unknown(context.ptr, optimizer.ptr)
|
|
384
|
+
return unknown(backend, started, reason, checked.interrupted)
|
|
385
|
+
}
|
|
386
|
+
if (checked.status === 'unsat') {
|
|
387
|
+
return limitOutput({
|
|
388
|
+
status: 'unsatisfied',
|
|
389
|
+
...(request.unsatCore ? { core: await coreFor(context, compiled, request.limits) } : {}),
|
|
390
|
+
infeasibilityProved: true,
|
|
391
|
+
backend,
|
|
392
|
+
diagnostics: [],
|
|
393
|
+
elapsedMs: Math.round(performance.now() - started)
|
|
394
|
+
}, request.limits)
|
|
395
|
+
}
|
|
396
|
+
const z3Model = optimizer.model()
|
|
397
|
+
return limitOutput({
|
|
398
|
+
status: 'optimal',
|
|
399
|
+
assignment: assignment(context, compiled, model, z3Model),
|
|
400
|
+
objectives: compiled.objectives.map(objective => ({
|
|
401
|
+
objectiveId: objective.id,
|
|
402
|
+
value: numericValue(context, z3Model.eval(objective.expression, true))
|
|
403
|
+
})),
|
|
404
|
+
optimalityProved: true,
|
|
405
|
+
backend,
|
|
406
|
+
diagnostics: [],
|
|
407
|
+
elapsedMs: Math.round(performance.now() - started)
|
|
408
|
+
}, request.limits)
|
|
409
|
+
} finally {
|
|
410
|
+
optimizer.release()
|
|
411
|
+
}
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (api.Z3.get_estimated_alloc_size() > BigInt(request.limits.maxMemoryBytes)) {
|
|
414
|
+
return {
|
|
415
|
+
status: 'unknown',
|
|
416
|
+
reason: {
|
|
417
|
+
kind: 'resource-limit',
|
|
418
|
+
message: 'Z3 exceeded maxMemoryBytes',
|
|
419
|
+
limit: 'maxMemoryBytes'
|
|
420
|
+
},
|
|
421
|
+
backend,
|
|
422
|
+
diagnostics: [],
|
|
423
|
+
elapsedMs: Math.round(performance.now() - started)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return backendError(backend, started, error)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let singleton: Promise<Runtime> | undefined
|
|
431
|
+
|
|
432
|
+
const initialize = async (): Promise<Runtime> => {
|
|
433
|
+
const started = performance.now()
|
|
434
|
+
// Keep this as the only runtime import: importing @cavelang/solver-z3 itself
|
|
435
|
+
// must not load the 34 MB Wasm artifact or start worker threads.
|
|
436
|
+
const api = await import('z3-solver').then(module => module.init())
|
|
437
|
+
const context = api.Context('cave')
|
|
438
|
+
const backend = Object.freeze({ name: 'z3-wasm', version: api.Z3.get_full_version() })
|
|
439
|
+
let tail: Promise<void> = Promise.resolve()
|
|
440
|
+
let closing = false
|
|
441
|
+
let closed = false
|
|
442
|
+
|
|
443
|
+
const enqueue = <T>(work: () => Promise<T>): Promise<T> => {
|
|
444
|
+
const result = tail.then(work, work)
|
|
445
|
+
tail = result.then(() => undefined, () => undefined)
|
|
446
|
+
return result
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const runtime: Runtime = {
|
|
450
|
+
backend,
|
|
451
|
+
capabilities: new Set(Adapter.capabilities),
|
|
452
|
+
initializationMs: Math.round(performance.now() - started),
|
|
453
|
+
solve: (model, request) => {
|
|
454
|
+
if (closing) return Promise.resolve(backendError(backend, performance.now(), new Error('Z3 runtime is closed')))
|
|
455
|
+
return enqueue(() => solveModel(api, context, backend, model, request))
|
|
456
|
+
},
|
|
457
|
+
close: async () => {
|
|
458
|
+
if (closed) return
|
|
459
|
+
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
|
|
468
|
+
singleton = undefined
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return runtime
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Lazily initialize and reuse one process-wide Z3 runtime. */
|
|
476
|
+
export const create = (): Promise<Runtime> => singleton ??= initialize().catch(error => {
|
|
477
|
+
singleton = undefined
|
|
478
|
+
throw error
|
|
479
|
+
})
|