@cavelang/solver-z3 0.34.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
@@ -0,0 +1,65 @@
1
+ import { errorMessage } from "./error-message.js";
2
+ /**
3
+ * The binding's finalizers call this instance's low-level API directly, outside
4
+ * its async mutex. Native AST deletion must not race a pthread solver check.
5
+ * Guard the two async native operations CAVE uses; normal synchronous releases
6
+ * remain immediate, and queued releases drain before a check returns to CAVE.
7
+ */
8
+ export const guardCleanup = (api) => {
9
+ const functions = api;
10
+ const pending = [];
11
+ let checking = 0;
12
+ for (const [name, value] of Object.entries(functions)) {
13
+ if (typeof value !== 'function')
14
+ continue;
15
+ if (name === 'dec_ref' || name.endsWith('_dec_ref') || name === 'del_context') {
16
+ functions[name] = (...args) => {
17
+ const release = () => { value.apply(api, args); };
18
+ if (checking > 0)
19
+ pending.push(release);
20
+ else
21
+ release();
22
+ };
23
+ }
24
+ }
25
+ for (const name of ['solver_check_assumptions', 'optimize_check']) {
26
+ const check = functions[name];
27
+ if (typeof check !== 'function')
28
+ throw new TypeError(`Z3 is missing ${name}`);
29
+ functions[name] = async (...args) => {
30
+ checking++;
31
+ let checkFailed = false;
32
+ let checkError;
33
+ try {
34
+ return await check.apply(api, args);
35
+ }
36
+ catch (error) {
37
+ checkFailed = true;
38
+ checkError = error;
39
+ throw error;
40
+ }
41
+ finally {
42
+ checking--;
43
+ if (checking === 0) {
44
+ const releases = pending.splice(0);
45
+ const failures = [];
46
+ for (const release of releases) {
47
+ try {
48
+ release();
49
+ }
50
+ catch (error) {
51
+ failures.push(error);
52
+ }
53
+ }
54
+ if (failures.length > 0) {
55
+ if (!checkFailed && failures.length === 1)
56
+ throw failures[0];
57
+ const errors = checkFailed ? [checkError, ...failures] : failures;
58
+ throw new AggregateError(errors, `Z3 native cleanup failed: ${errors.map(errorMessage).join('; ')}`, checkFailed ? { cause: checkError } : undefined);
59
+ }
60
+ }
61
+ }
62
+ };
63
+ }
64
+ };
65
+ //# sourceMappingURL=cleanup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../src/cleanup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ type Worker = {
2
+ terminate: () => Promise<unknown>;
3
+ };
4
+ type Pool = {
5
+ readonly runningWorkers: readonly Worker[];
6
+ readonly unusedWorkers: readonly Worker[];
7
+ terminateAllThreads: () => void;
8
+ };
9
+ /** Keep Emscripten's message handlers until Node confirms each worker has exited. */
10
+ export declare const closeWorkers: (pool: Pool | undefined) => Promise<void>;
11
+ export {};
12
+ //# sourceMappingURL=close-workers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"close-workers.d.ts","sourceRoot":"","sources":["../../src/close-workers.ts"],"names":[],"mappings":"AACA,KAAK,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAA;AACnD,KAAK,IAAI,GAAG;IACV,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1C,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAA;IACzC,mBAAmB,EAAE,MAAM,IAAI,CAAA;CAChC,CAAA;AAED,qFAAqF;AACrF,eAAO,MAAM,YAAY,GAAU,MAAM,IAAI,GAAG,SAAS,KAAG,OAAO,CAAC,IAAI,CAkBvE,CAAA"}
@@ -0,0 +1,27 @@
1
+ import { errorMessage } from "./error-message.js";
2
+ /** Keep Emscripten's message handlers until Node confirms each worker has exited. */
3
+ export const closeWorkers = async (pool) => {
4
+ if (pool === undefined)
5
+ return;
6
+ const stopped = new Set();
7
+ while (true) {
8
+ const workers = [...new Set([...pool.runningWorkers, ...pool.unusedWorkers])]
9
+ .filter(worker => !stopped.has(worker));
10
+ if (workers.length === 0)
11
+ break;
12
+ for (const worker of workers)
13
+ stopped.add(worker);
14
+ // A throwing termination must not prevent attempts on the remaining workers.
15
+ // Keep handlers alive until every attempted termination has settled.
16
+ const results = await Promise.allSettled(workers.map(async (worker) => worker.terminate()));
17
+ const failures = results.flatMap(result => result.status === 'rejected' ? [result.reason] : []);
18
+ if (failures.length === 1)
19
+ throw failures[0];
20
+ if (failures.length > 1)
21
+ throw new AggregateError(failures, `Z3 worker termination failed: ${failures.map(errorMessage).join('; ')}`);
22
+ }
23
+ // The binding replaces handlers with warnings when it clears the pool.
24
+ // Worker exit acknowledgments ensure pending cleanup messages came first.
25
+ pool.terminateAllThreads();
26
+ };
27
+ //# sourceMappingURL=close-workers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"close-workers.js","sourceRoot":"","sources":["../../src/close-workers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAQjD,qFAAqF;AACrF,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,IAAsB,EAAiB,EAAE;IAC1E,IAAI,IAAI,KAAK,SAAS;QAAE,OAAM;IAC9B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAA;IACjC,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC;aAC1E,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;QACzC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,MAAK;QAC/B,KAAK,MAAM,MAAM,IAAI,OAAO;YAAE,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QACjD,6EAA6E;QAC7E,qEAAqE;QACrE,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAC,MAAM,EAAC,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAA;QACzF,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QAC/F,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAA;QAC5C,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,IAAI,cAAc,CAAC,QAAQ,EAAE,iCAAiC,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACvI,CAAC;IACD,uEAAuE;IACvE,0EAA0E;IAC1E,IAAI,CAAC,mBAAmB,EAAE,CAAA;AAC5B,CAAC,CAAA"}
@@ -0,0 +1,7 @@
1
+ export declare const withDeadline: (context: {
2
+ interrupt: () => void;
3
+ }, timeoutMs: number, check: () => Promise<"sat" | "unsat" | "unknown">) => Promise<{
4
+ readonly status: "sat" | "unsat" | "unknown";
5
+ readonly interrupted: boolean;
6
+ }>;
7
+ //# sourceMappingURL=deadline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deadline.d.ts","sourceRoot":"","sources":["../../src/deadline.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,GACvB,SAAS;IAAE,SAAS,EAAE,MAAM,IAAI,CAAA;CAAE,EAClC,WAAW,MAAM,EACjB,OAAO,MAAM,OAAO,CAAC,KAAK,GAAG,OAAO,GAAG,SAAS,CAAC,KAChD,OAAO,CAAC;IAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,OAAO,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAkCzF,CAAA"}
@@ -0,0 +1,46 @@
1
+ export const withDeadline = async (context, timeoutMs, check) => {
2
+ let interrupted = false;
3
+ let interruptFailed = false;
4
+ let interruptError;
5
+ // Dispatch and native cancellation are asynchronous. Keep requesting cancellation
6
+ // until the check settles instead of assuming one interrupt was acknowledged.
7
+ const interrupt = () => {
8
+ interrupted = true;
9
+ try {
10
+ context.interrupt();
11
+ }
12
+ catch (error) {
13
+ // Timer failures belong to this check, not the host's uncaught-error
14
+ // handler. Retain one error while continuing cooperative cancellation.
15
+ if (!interruptFailed) {
16
+ interruptFailed = true;
17
+ interruptError = error;
18
+ }
19
+ }
20
+ timer = setTimeout(interrupt, 50);
21
+ timer.unref();
22
+ };
23
+ let timer = setTimeout(interrupt, timeoutMs);
24
+ timer.unref();
25
+ try {
26
+ let status;
27
+ try {
28
+ status = await check();
29
+ }
30
+ catch (error) {
31
+ if (interruptFailed) {
32
+ throw new AggregateError([error, interruptError], `Z3 check failed: ${errorMessage(error)}; deadline interruption also failed: ${errorMessage(interruptError)}`, { cause: error });
33
+ }
34
+ throw error;
35
+ }
36
+ // Wait for native work before the caller releases its solver/optimizer.
37
+ if (interruptFailed)
38
+ throw interruptError;
39
+ return { status, interrupted };
40
+ }
41
+ finally {
42
+ clearTimeout(timer);
43
+ }
44
+ };
45
+ import { errorMessage } from "./error-message.js";
46
+ //# sourceMappingURL=deadline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deadline.js","sourceRoot":"","sources":["../../src/deadline.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAC/B,OAAkC,EAClC,SAAiB,EACjB,KAAiD,EACyC,EAAE;IAC5F,IAAI,WAAW,GAAG,KAAK,CAAA;IACvB,IAAI,eAAe,GAAG,KAAK,CAAA;IAC3B,IAAI,cAAuB,CAAA;IAC3B,kFAAkF;IAClF,8EAA8E;IAC9E,MAAM,SAAS,GAAG,GAAS,EAAE;QAC3B,WAAW,GAAG,IAAI,CAAA;QAClB,IAAI,CAAC;YAAC,OAAO,CAAC,SAAS,EAAE,CAAA;QAAC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACzC,qEAAqE;YACrE,uEAAuE;YACvE,IAAI,CAAC,eAAe,EAAE,CAAC;gBAAC,eAAe,GAAG,IAAI,CAAC;gBAAC,cAAc,GAAG,KAAK,CAAA;YAAC,CAAC;QAC1E,CAAC;QACD,KAAK,GAAG,UAAU,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;QACjC,KAAK,CAAC,KAAK,EAAE,CAAA;IACf,CAAC,CAAA;IACD,IAAI,KAAK,GAAG,UAAU,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;IAC5C,KAAK,CAAC,KAAK,EAAE,CAAA;IACb,IAAI,CAAC;QACH,IAAI,MAAmC,CAAA;QACvC,IAAI,CAAC;YAAC,MAAM,GAAG,MAAM,KAAK,EAAE,CAAA;QAAC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAC5C,IAAI,eAAe,EAAE,CAAC;gBACpB,MAAM,IAAI,cAAc,CAAC,CAAC,KAAK,EAAE,cAAc,CAAC,EAC9C,oBAAoB,YAAY,CAAC,KAAK,CAAC,wCAAwC,YAAY,CAAC,cAAc,CAAC,EAAE,EAC7G,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;YACrB,CAAC;YACD,MAAM,KAAK,CAAA;QACb,CAAC;QACD,wEAAwE;QACxE,IAAI,eAAe;YAAE,MAAM,cAAc,CAAA;QACzC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAA;IAChC,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAA;IACrB,CAAC;AACH,CAAC,CAAA;AACD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA"}
@@ -0,0 +1,3 @@
1
+ /** Formatting a failure must not prevent runtime cleanup or replace its diagnostic. */
2
+ export declare const errorMessage: (error: unknown) => string;
3
+ //# sourceMappingURL=error-message.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-message.d.ts","sourceRoot":"","sources":["../../src/error-message.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAG7C,CAAA"}
@@ -0,0 +1,10 @@
1
+ /** Formatting a failure must not prevent runtime cleanup or replace its diagnostic. */
2
+ export const errorMessage = (error) => {
3
+ try {
4
+ return error instanceof Error ? String(error.message) : String(error);
5
+ }
6
+ catch {
7
+ return '[unprintable thrown value]';
8
+ }
9
+ };
10
+ //# sourceMappingURL=error-message.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-message.js","sourceRoot":"","sources":["../../src/error-message.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAc,EAAU,EAAE;IACrD,IAAI,CAAC;QAAC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAAC,CAAC;IAC7E,MAAM,CAAC;QAAC,OAAO,4BAA4B,CAAA;IAAC,CAAC;AAC/C,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAgB,MAAM,kBAAkB,CAAA;AA+BxD,MAAM,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,GAAG;IAChC,8DAA8D;IAC9D,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACpC,CAAA;AAqbD,+DAA+D;AAC/D,eAAO,MAAM,MAAM,QAAO,OAAO,CAAC,OAAO,CAGvC,CAAA"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,OAAO,EAAgB,MAAM,kBAAkB,CAAA;AAiCxD,MAAM,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,GAAG;IAChC,8DAA8D;IAC9D,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACpC,CAAA;AA8eD,+DAA+D;AAC/D,eAAO,MAAM,MAAM,QAAO,OAAO,CAAC,OAAO,CAGvC,CAAA"}
@@ -1,10 +1,15 @@
1
+ import { withDeadline } from "./deadline.js";
2
+ import { withRelease } from "./with-release.js";
3
+ import { errorMessage } from "./error-message.js";
1
4
  import { performance } from 'node:perf_hooks';
2
5
  import { Adapter, Exact, Model } from '@cavelang/solver';
6
+ import { guardCleanup } from "./cleanup.js";
7
+ import { closeWorkers } from "./close-workers.js";
3
8
  const backendError = (backend, started, error) => ({
4
9
  status: 'unknown',
5
10
  reason: {
6
11
  kind: 'backend-error',
7
- message: error instanceof Error ? error.message : String(error)
12
+ message: errorMessage(error)
8
13
  },
9
14
  backend,
10
15
  diagnostics: [],
@@ -29,6 +34,7 @@ const compiler = (context, model) => {
29
34
  domain.id,
30
35
  [...domain.values].sort(compareText)
31
36
  ]));
37
+ const enumCodes = new Map([...enums].map(([domain, values]) => [domain, new Map(values.map((value, index) => [value, index]))]));
32
38
  const variables = new Map();
33
39
  const bounds = [];
34
40
  for (const variable of model.variables) {
@@ -62,47 +68,110 @@ const compiler = (context, model) => {
62
68
  }
63
69
  }
64
70
  }
65
- const compile = (expression) => {
71
+ const expressions = new WeakMap();
72
+ const compiledChild = (expression) => expressions.get(expression);
73
+ const compileBoolean = (kind, operands) => {
74
+ const combine = (values) => kind === 'and' ? context.And(...values) : context.Or(...values);
75
+ let values = operands.map(value => asBool(compiledChild(value)));
76
+ // The binding also spreads arguments internally, including its AstVector
77
+ // overload. Bound every call and retain all operands through associativity.
78
+ const width = 1024;
79
+ while (values.length > width) {
80
+ const groups = [];
81
+ for (let index = 0; index < values.length; index += width) {
82
+ groups.push(combine(values.slice(index, index + width)));
83
+ }
84
+ values = groups;
85
+ }
86
+ return combine(values);
87
+ };
88
+ const compileNode = (expression) => {
66
89
  switch (expression.kind) {
67
90
  case 'literal':
68
91
  switch (expression.sort) {
69
92
  case 'bool': return context.Bool.val(expression.value);
70
93
  case 'int': return context.Int.val(Exact.integer(expression.value));
71
94
  case 'real': return context.Real.val(exact(expression.value));
72
- case 'enum': return context.Int.val(enums.get(expression.domain).indexOf(expression.value));
95
+ case 'enum': return context.Int.val(enumCodes.get(expression.domain).get(expression.value));
73
96
  }
74
97
  case 'variable': return variables.get(expression.id);
75
- case 'not': return asBool(compile(expression.value)).not();
76
- case 'and': return context.And(...expression.operands.map(value => asBool(compile(value))));
77
- case 'or': return context.Or(...expression.operands.map(value => asBool(compile(value))));
78
- case 'implies': return context.Implies(asBool(compile(expression.left)), asBool(compile(expression.right)));
79
- case 'eq': return compile(expression.left).eq(compile(expression.right));
80
- case 'neq': return compile(expression.left).neq(compile(expression.right));
81
- case 'lt': return asArith(compile(expression.left)).lt(asArith(compile(expression.right)));
82
- case 'lte': return asArith(compile(expression.left)).le(asArith(compile(expression.right)));
83
- case 'gt': return asArith(compile(expression.left)).gt(asArith(compile(expression.right)));
84
- case 'gte': return asArith(compile(expression.left)).ge(asArith(compile(expression.right)));
98
+ case 'not': return asBool(compiledChild(expression.value)).not();
99
+ case 'and':
100
+ case 'or': return compileBoolean(expression.kind, expression.operands);
101
+ case 'implies': return context.Implies(asBool(compiledChild(expression.left)), asBool(compiledChild(expression.right)));
102
+ case 'eq': return compiledChild(expression.left).eq(compiledChild(expression.right));
103
+ case 'neq': return compiledChild(expression.left).neq(compiledChild(expression.right));
104
+ case 'lt': return asArith(compiledChild(expression.left)).lt(asArith(compiledChild(expression.right)));
105
+ case 'lte': return asArith(compiledChild(expression.left)).le(asArith(compiledChild(expression.right)));
106
+ case 'gt': return asArith(compiledChild(expression.left)).gt(asArith(compiledChild(expression.right)));
107
+ case 'gte': return asArith(compiledChild(expression.left)).ge(asArith(compiledChild(expression.right)));
85
108
  case 'add': {
86
- const [first, ...rest] = expression.operands.map(value => asArith(compile(value)));
109
+ const [first, ...rest] = expression.operands.map(value => asArith(compiledChild(value)));
87
110
  return rest.reduce((left, right) => left.add(right), first);
88
111
  }
89
112
  case 'multiply': {
90
- const [first, ...rest] = expression.operands.map(value => asArith(compile(value)));
113
+ const [first, ...rest] = expression.operands.map(value => asArith(compiledChild(value)));
91
114
  return rest.reduce((left, right) => left.mul(right), first);
92
115
  }
93
- case 'subtract': return asArith(compile(expression.left)).sub(asArith(compile(expression.right)));
116
+ case 'subtract': return asArith(compiledChild(expression.left)).sub(asArith(compiledChild(expression.right)));
94
117
  case 'divide': {
95
- const left = asArith(compile(expression.left));
96
- const right = asArith(compile(expression.right));
118
+ const left = asArith(compiledChild(expression.left));
119
+ const right = asArith(compiledChild(expression.right));
97
120
  // Portable division is exact-real division even when both operands are
98
121
  // integers; Z3's native Int / Int operation would truncate instead.
99
122
  const real = (value) => context.isInt(value) ? context.ToReal(value) : value;
100
123
  return real(left).div(real(right));
101
124
  }
102
- case 'negate': return asArith(compile(expression.value)).neg();
103
- case 'if': return context.If(asBool(compile(expression.condition)), compile(expression.then), compile(expression.else));
125
+ case 'negate': return asArith(compiledChild(expression.value)).neg();
126
+ case 'if': return context.If(asBool(compiledChild(expression.condition)), compiledChild(expression.then), compiledChild(expression.else));
104
127
  }
105
128
  };
129
+ function* children(expression) {
130
+ switch (expression.kind) {
131
+ case 'literal':
132
+ case 'variable': return;
133
+ case 'not':
134
+ case 'negate':
135
+ yield expression.value;
136
+ return;
137
+ case 'and':
138
+ case 'or':
139
+ case 'add':
140
+ case 'multiply':
141
+ yield* expression.operands;
142
+ return;
143
+ case 'if':
144
+ yield expression.condition;
145
+ yield expression.then;
146
+ yield expression.else;
147
+ return;
148
+ default:
149
+ yield expression.left;
150
+ yield expression.right;
151
+ }
152
+ }
153
+ const compile = (expression) => {
154
+ if (expressions.has(expression))
155
+ return expressions.get(expression);
156
+ const stack = [{ expression, children: children(expression) }];
157
+ const active = new WeakSet([expression]);
158
+ while (stack.length > 0) {
159
+ const frame = stack[stack.length - 1];
160
+ const child = frame.children.next();
161
+ if (child.done) {
162
+ expressions.set(frame.expression, compileNode(frame.expression));
163
+ active.delete(frame.expression);
164
+ stack.pop();
165
+ }
166
+ else if (!expressions.has(child.value)) {
167
+ if (active.has(child.value))
168
+ throw new TypeError('cyclic solver expression');
169
+ active.add(child.value);
170
+ stack.push({ expression: child.value, children: children(child.value) });
171
+ }
172
+ }
173
+ return expressions.get(expression);
174
+ };
106
175
  return {
107
176
  variables,
108
177
  enums,
@@ -164,20 +233,6 @@ const numericValue = (context, value) => {
164
233
  };
165
234
  throw new TypeError(`Z3 returned a non-rational objective value: ${value.sexpr()}`);
166
235
  };
167
- const withDeadline = async (context, timeoutMs, check) => {
168
- let interrupted = false;
169
- const timer = setTimeout(() => {
170
- interrupted = true;
171
- context.interrupt();
172
- }, timeoutMs);
173
- timer.unref();
174
- try {
175
- return { status: await check(), interrupted };
176
- }
177
- finally {
178
- clearTimeout(timer);
179
- }
180
- };
181
236
  const unknown = (backend, started, reason, interrupted) => {
182
237
  const timeout = interrupted || /timeout|canceled|cancelled/i.test(reason);
183
238
  const memory = /memory|memout/i.test(reason);
@@ -194,12 +249,13 @@ const unknown = (backend, started, reason, interrupted) => {
194
249
  };
195
250
  };
196
251
  const addHard = (solver, compiled, track) => {
197
- solver.add(...compiled.bounds);
252
+ for (let index = 0; index < compiled.bounds.length; index += 1024) {
253
+ solver.add(...compiled.bounds.slice(index, index + 1024));
254
+ }
198
255
  const trackers = new Map();
199
256
  for (const [id, expression] of compiled.constraints) {
200
257
  if (track) {
201
- const symbol = `cave.constraint.${id}`;
202
- const tracker = solver.ctx.Bool.const(symbol);
258
+ const tracker = solver.ctx.Bool.fresh('cave.constraint');
203
259
  solver.addAndTrack(expression, tracker);
204
260
  trackers.set(tracker.id(), id);
205
261
  }
@@ -211,7 +267,7 @@ const addHard = (solver, compiled, track) => {
211
267
  };
212
268
  const coreFor = async (context, compiled, limits) => {
213
269
  const solver = new context.Solver();
214
- try {
270
+ return withRelease(async () => {
215
271
  solver.set('timeout', limits.timeoutMs);
216
272
  const trackers = addHard(solver, compiled, true);
217
273
  if ((await withDeadline(context, limits.timeoutMs, () => solver.check())).status !== 'unsat')
@@ -220,10 +276,7 @@ const coreFor = async (context, compiled, limits) => {
220
276
  .map(value => trackers.get(value.id()))
221
277
  .filter((value) => value !== undefined)
222
278
  .sort();
223
- }
224
- finally {
225
- solver.release();
226
- }
279
+ }, () => solver.release());
227
280
  };
228
281
  const limitOutput = (result, limits) => Buffer.byteLength(JSON.stringify(result)) <= limits.maxOutputBytes
229
282
  ? result
@@ -240,13 +293,16 @@ const limitOutput = (result, limits) => Buffer.byteLength(JSON.stringify(result)
240
293
  };
241
294
  const solveModel = async (api, context, backend, model, request) => {
242
295
  const started = performance.now();
296
+ if (!Number.isSafeInteger(request.limits.timeoutMs) || request.limits.timeoutMs < 1 || request.limits.timeoutMs > 2147483647) {
297
+ return backendError(backend, started, new TypeError('Z3 timeoutMs must be in 1..2147483647'));
298
+ }
243
299
  try {
244
300
  api.setParam('memory_max_size', Math.max(1, Math.floor(request.limits.maxMemoryBytes / 1024 / 1024)));
245
301
  const compiled = compiler(context, model);
246
302
  const optimize = compiled.objectives.length > 0 || compiled.softConstraints.length > 0;
247
303
  if (!optimize) {
248
304
  const solver = new context.Solver();
249
- try {
305
+ return await withRelease(async () => {
250
306
  solver.set('timeout', request.limits.timeoutMs);
251
307
  const trackers = addHard(solver, compiled, request.unsatCore);
252
308
  const checked = await withDeadline(context, request.limits.timeoutMs, () => solver.check());
@@ -276,22 +332,16 @@ const solveModel = async (api, context, backend, model, request) => {
276
332
  diagnostics: [],
277
333
  elapsedMs: Math.round(performance.now() - started)
278
334
  }, request.limits);
279
- }
280
- finally {
281
- solver.release();
282
- }
335
+ }, () => solver.release());
283
336
  }
284
337
  const optimizer = new context.Optimize();
285
- try {
338
+ return await withRelease(async () => {
286
339
  optimizer.set('timeout', request.limits.timeoutMs);
287
340
  optimizer.set('priority', 'lex');
288
341
  addHard(optimizer, compiled, false);
289
- for (const objective of compiled.objectives) {
290
- if (objective.direction === 'minimize')
291
- optimizer.minimize(objective.expression);
292
- else
293
- optimizer.maximize(objective.expression);
294
- }
342
+ const handles = compiled.objectives.map(objective => objective.direction === 'minimize'
343
+ ? optimizer.minimize(objective.expression)
344
+ : optimizer.maximize(objective.expression));
295
345
  // Soft preferences are deliberately the lowest-priority objective. Their
296
346
  // weights are explicit model data and never derived from CAVE confidence.
297
347
  for (const constraint of compiled.softConstraints) {
@@ -313,6 +363,25 @@ const solveModel = async (api, context, backend, model, request) => {
313
363
  }, request.limits);
314
364
  }
315
365
  const z3Model = optimizer.model();
366
+ const rational = (value) => {
367
+ if (context.isIntVal(value))
368
+ return value.value().toString();
369
+ if (context.isRealVal(value))
370
+ return {
371
+ numerator: value.value().numerator.toString(),
372
+ denominator: value.value().denominator.toString()
373
+ };
374
+ return undefined;
375
+ };
376
+ for (const [index, objective] of compiled.objectives.entries()) {
377
+ const lower = rational(optimizer.getLower(handles[index]));
378
+ const upper = rational(optimizer.getUpper(handles[index]));
379
+ const attained = rational(z3Model.eval(objective.expression, true));
380
+ if (lower === undefined || upper === undefined || attained === undefined ||
381
+ Exact.compare(lower, upper) !== 0 || Exact.compare(lower, attained) !== 0) {
382
+ return unknown(backend, started, `objective ${JSON.stringify(objective.id)} has no proved finite attained optimum`, false);
383
+ }
384
+ }
316
385
  return limitOutput({
317
386
  status: 'optimal',
318
387
  assignment: assignment(context, compiled, model, z3Model),
@@ -325,10 +394,7 @@ const solveModel = async (api, context, backend, model, request) => {
325
394
  diagnostics: [],
326
395
  elapsedMs: Math.round(performance.now() - started)
327
396
  }, request.limits);
328
- }
329
- finally {
330
- optimizer.release();
331
- }
397
+ }, () => optimizer.release());
332
398
  }
333
399
  catch (error) {
334
400
  if (api.Z3.get_estimated_alloc_size() > BigInt(request.limits.maxMemoryBytes)) {
@@ -348,16 +414,18 @@ const solveModel = async (api, context, backend, model, request) => {
348
414
  }
349
415
  };
350
416
  let singleton;
417
+ let shutdown;
351
418
  const initialize = async () => {
352
419
  const started = performance.now();
353
420
  // Keep this as the only runtime import: importing @cavelang/solver-z3 itself
354
421
  // must not load the 34 MB Wasm artifact or start worker threads.
355
422
  const api = await import('z3-solver').then(module => module.init());
423
+ guardCleanup(api.Z3);
356
424
  const context = api.Context('cave');
357
425
  const backend = Object.freeze({ name: 'z3-wasm', version: api.Z3.get_full_version() });
358
426
  let tail = Promise.resolve();
359
427
  let closing = false;
360
- let closed = false;
428
+ let closePromise;
361
429
  const enqueue = (work) => {
362
430
  const result = tail.then(work, work);
363
431
  tail = result.then(() => undefined, () => undefined);
@@ -372,27 +440,24 @@ const initialize = async () => {
372
440
  return Promise.resolve(backendError(backend, performance.now(), new Error('Z3 runtime is closed')));
373
441
  return enqueue(() => solveModel(api, context, backend, model, request));
374
442
  },
375
- close: async () => {
376
- if (closed)
377
- return;
443
+ close: () => {
444
+ if (closePromise)
445
+ return closePromise;
378
446
  closing = true;
379
- await enqueue(async () => {
380
- if (closed)
381
- return;
382
- // Let the completed pthread post its cleanup message before forcibly
383
- // terminating the pool. Without this turn, Emscripten can report a
384
- // late `cleanupThread` command from a worker we just terminated.
385
- await new Promise(resolve => setTimeout(resolve, 0));
386
- api.em?.PThread?.terminateAllThreads?.();
387
- closed = true;
447
+ closePromise = enqueue(async () => {
448
+ // Await worker exits before Emscripten discards their message handlers.
449
+ await closeWorkers(api.em?.PThread);
388
450
  singleton = undefined;
451
+ shutdown = undefined;
389
452
  });
453
+ shutdown = closePromise;
454
+ return closePromise;
390
455
  }
391
456
  };
392
457
  return runtime;
393
458
  };
394
459
  /** Lazily initialize and reuse one process-wide Z3 runtime. */
395
- export const create = () => singleton ??= initialize().catch(error => {
460
+ export const create = () => shutdown ? shutdown.then(create) : singleton ??= initialize().catch(error => {
396
461
  singleton = undefined;
397
462
  throw error;
398
463
  });