3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-07-14 19:15:41 +00:00
This commit is contained in:
Copilot 2026-07-13 16:32:39 +00:00 committed by GitHub
commit 7d1bdf041e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 445 additions and 22 deletions

View file

@ -48,7 +48,7 @@ jobs:
- name: Build TypeScript
run: npm run build:ts
- name: Build wasm
- name: Build wasm (threaded)
run: |
emsdk install ${EM_VERSION}
emsdk activate ${EM_VERSION}
@ -57,6 +57,11 @@ jobs:
which clang++
npm run build:wasm
- name: Build wasm (single-threaded)
run: |
source $(dirname $(which emsdk))/emsdk_env.sh
npm run build:wasm:singlethread
- name: Test
run: npm test

View file

@ -17,7 +17,7 @@ permissions:
jobs:
check:
name: Check
name: Check (threaded)
runs-on: ubuntu-latest
steps:
- name: Checkout
@ -52,3 +52,37 @@ jobs:
- name: Test
run: npm test
check-singlethread:
name: Check (single-threaded)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: "lts/*"
- name: Setup emscripten
uses: mymindstorm/setup-emsdk@v16
with:
no-install: true
version: ${{env.EM_VERSION}}
actions-cache-folder: "emsdk-cache"
- name: Install dependencies
run: npm ci
- name: Build TypeScript
run: npm run build:ts
- name: Build single-threaded wasm
run: |
emsdk install ${EM_VERSION}
emsdk activate ${EM_VERSION}
source $(dirname $(which emsdk))/emsdk_env.sh
which node
which clang++
npm run build:wasm:singlethread

View file

@ -29,10 +29,66 @@ const api = await init({
### Limitations
The package requires threads, which means you'll need to be running in an environment which supports `SharedArrayBuffer`. In browsers, in addition to ensuring the browser has implemented `SharedArrayBuffer`, you'll need to serve your page with [special headers](https://web.dev/coop-coep/). There's a [neat trick](https://github.com/gzuidhof/coi-serviceworker) for doing that client-side on e.g. Github Pages, though you shouldn't use that trick in more complex applications.
The default package requires threads, which means you'll need to be running in an environment which supports `SharedArrayBuffer`. In browsers, in addition to ensuring the browser has implemented `SharedArrayBuffer`, you'll need to serve your page with [special headers](https://web.dev/coop-coep/). There's a [neat trick](https://github.com/gzuidhof/coi-serviceworker) for doing that client-side on e.g. Github Pages, though you shouldn't use that trick in more complex applications.
The Emscripten worker model will spawn multiple instances of `z3-built.js` for long-running operations. When building for the web, you should include that file as its own script on the page - using a bundler like webpack will prevent it from loading correctly.
#### Single-threaded build (no SharedArrayBuffer required)
If you cannot set the COOP/COEP headers required by the default threaded build (for example because your application embeds cross-origin iframes such as YouTube), you can use the single-threaded variant instead:
```javascript
// Node.js / bundler (no SharedArrayBuffer needed)
const { init } = require('z3-solver/singlethread');
```
```typescript
// TypeScript
import { init } from 'z3-solver/singlethread';
```
The API is identical to the default export. The key difference is that all Z3 operations run **synchronously in the calling thread** rather than in a background thread. To avoid blocking the UI, run your Z3 code inside a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers):
```javascript
// my-z3-worker.js (loaded as a classic Web Worker)
// 1. Load the single-threaded WASM artifact (exposes initZ3SingleThread globally).
importScripts('z3-built-singlethread.js');
// 2. Load the single-threaded entry point as a classic script bundle.
// Most bundlers can produce a CJS/UMD build that can be loaded with importScripts.
// Alternatively, switch to a module worker (new Worker(..., { type: 'module' }))
// and use ES module import syntax instead.
importScripts('z3-solver-singlethread-bundle.js'); // exposes Z3SolverSingleThread
const { init } = Z3SolverSingleThread;
const { Context } = await init();
const { Solver, Int } = new Context('main');
// 3. Use Z3 normally calls block this worker thread, not the main thread.
const x = Int.const('x');
const solver = new Solver();
solver.add(x.ge(0));
self.postMessage(await solver.check()); // 'sat'
```
Or with a **module worker** (supported in modern browsers):
```javascript
// my-z3-worker.mjs (loaded as new Worker('...', { type: 'module' }))
import { init } from 'z3-solver/singlethread';
const { Context } = await init();
const { Solver, Int } = new Context('main');
const x = Int.const('x');
const solver = new Solver();
solver.add(x.ge(0));
self.postMessage(await solver.check()); // 'sat'
```
The single-threaded WASM file (`z3-built-singlethread.js` / `z3-built-singlethread.wasm`) is distributed alongside the default build in the npm package.
## High-level
You can find the documentation for the high-level Z3 API [here](https://z3prover.github.io/api/html/js/index.html). There are some usage examples in `src/high-level/high-level.test.ts`

View file

@ -18,6 +18,18 @@
"browser": "build/browser.js",
"main": "build/node.js",
"types": "build/node.d.ts",
"exports": {
".": {
"types": "./build/node.d.ts",
"browser": "./build/browser.js",
"default": "./build/node.js"
},
"./singlethread": {
"types": "./build/node-singlethread.d.ts",
"browser": "./build/browser-singlethread.js",
"default": "./build/node-singlethread.js"
}
},
"files": [
"build/**/*.{js,d.ts,wasm}"
],
@ -26,6 +38,7 @@
"build:ts:tsc": "tsc --pretty --project tsconfig.build.json ",
"build:ts:generate": "ts-node --transpileOnly scripts/make-ts-wrapper.ts src/low-level/wrapper.__GENERATED__.ts src/low-level/types.__GENERATED__.ts",
"build:wasm": "ts-node --transpileOnly ./scripts/build-wasm.ts",
"build:wasm:singlethread": "ts-node --transpileOnly ./scripts/build-wasm-singlethread.ts",
"clean": "rimraf build 'src/**/*.__GENERATED__.*'",
"lint": "prettier -c '{./,src/,scripts/,examples/}**/*.{js,ts}'",
"format": "prettier --write '{./,src/,scripts/}**/*.{js,ts}'",

View file

@ -0,0 +1,86 @@
import assert from 'assert';
import { SpawnOptions, spawnSync as originalSpawnSync } from 'child_process';
import fs, { existsSync } from 'fs';
import os from 'os';
import path from 'path';
import process from 'process';
import { asyncFuncs } from './async-fns';
import { makeCCSinglethreadWrapper } from './make-cc-wrapper';
import { functions } from './parse-api';
console.log('--- Building single-threaded WASM (no SharedArrayBuffer required)');
// Single-threaded build: no -pthread / USE_PTHREADS.
// POLLING_TIMER gives timeout support via wall-clock polling rather than threads.
const SWAP_OPTS: SpawnOptions = {
shell: true,
stdio: 'inherit',
env: {
...process.env,
CXXFLAGS: '-DSINGLE_THREAD -DPOLLING_TIMER -fexceptions -s DISABLE_EXCEPTION_CATCHING=0',
LDFLAGS: '-s WASM_BIGINT',
FPMATH_ENABLED: 'False', // Until Safari supports WASM SSE, we have to disable fast FP support
},
};
function spawnSync(command: string, opts: SpawnOptions = {}) {
console.log(`- ${command}`);
// TODO(ritave): Create a splitter that keeps track of quoted strings
const [cmd, ...args] = command.split(' ');
const { error, ...rest } = originalSpawnSync(cmd, args, { ...SWAP_OPTS, ...opts });
if (error !== undefined || rest.status !== 0) {
if (error !== undefined) {
console.error(error.message);
} else {
console.error(`Process exited with status ${rest.status}`);
}
process.exit(1);
}
return rest;
}
function exportedFuncs(): string[] {
const extras = [
'_malloc',
'_free',
'_set_throwy_error_handler',
'_set_noop_error_handler',
...asyncFuncs.map(f => '_async_' + f),
];
return [...extras, ...(functions as any[]).map(f => '_' + f.name)];
}
assert(fs.existsSync('./package.json'), 'Not in the root directory of js api');
const z3RootDir = path.join(process.cwd(), '../../../');
// Single-threaded libz3.a is built in a separate directory to avoid
// conflicting with the threaded build in 'build/'.
const singlethreadBuildDir = 'build-singlethread';
const singlethreadBuildPath = path.join(z3RootDir, singlethreadBuildDir);
if (!existsSync(path.join(singlethreadBuildPath, 'Makefile'))) {
spawnSync(
`emconfigure python scripts/mk_make.py --staticlib --single-threaded --arm64=false --build=${singlethreadBuildDir}`,
{ cwd: z3RootDir },
);
}
spawnSync(`emmake make -j${os.cpus().length} libz3.a`, { cwd: singlethreadBuildPath });
const ccWrapperPath = 'build/async-fns-singlethread.cc';
console.log(`- Building ${ccWrapperPath}`);
fs.mkdirSync(path.dirname(ccWrapperPath), { recursive: true });
fs.writeFileSync(ccWrapperPath, makeCCSinglethreadWrapper());
const fns = JSON.stringify(exportedFuncs());
// No PThread in exported methods for the single-threaded build.
const methods = '["ccall","FS","UTF8ToString","intArrayFromString","addFunction","removeFunction"]';
const libz3a = path.normalize(`../../../${singlethreadBuildDir}/libz3.a`);
spawnSync(
`emcc build/async-fns-singlethread.cc ${libz3a} --std=c++20 --pre-js src/low-level/async-wrapper.js -fexceptions -s WASM_BIGINT -s MODULARIZE=1 -s 'EXPORT_NAME="initZ3SingleThread"' -s EXPORTED_RUNTIME_METHODS=${methods} -s EXPORTED_FUNCTIONS=${fns} -s DISABLE_EXCEPTION_CATCHING=0 -s SAFE_HEAP=0 -s ALLOW_MEMORY_GROWTH=1 -s INITIAL_MEMORY=64MB -s TOTAL_STACK=20MB -s ALLOW_TABLE_GROWTH=1 -I z3/src/api/ -o build/z3-built-singlethread.js`,
);
fs.rmSync(ccWrapperPath);
console.log('--- Single-threaded WASM build finished');

View file

@ -4,6 +4,34 @@ import path from 'path';
import { asyncFuncs } from './async-fns';
import { functions } from './parse-api';
// Shared error handler code used by both threaded and single-threaded wrappers
const SHARED_ERROR_HANDLERS = `
class Z3Exception : public std::exception {
public:
const std::string m_msg;
Z3Exception(const std::string& msg) : m_msg(msg) {}
virtual const char* what() const throw () {
return m_msg.c_str();
}
};
void throwy_error_handler(Z3_context ctx, Z3_error_code c) {
throw Z3Exception(Z3_get_error_msg(ctx, c));
}
void noop_error_handler(Z3_context ctx, Z3_error_code c) {
// pass
}
extern "C" void set_throwy_error_handler(Z3_context ctx) {
Z3_set_error_handler(ctx, throwy_error_handler);
}
extern "C" void set_noop_error_handler(Z3_context ctx) {
Z3_set_error_handler(ctx, noop_error_handler);
}
`.trim();
export function makeCCWrapper() {
let wrappers = [];
@ -213,30 +241,77 @@ void wrapper_str(Args&&... args) {
class Z3Exception : public std::exception {
public:
const std::string m_msg;
Z3Exception(const std::string& msg) : m_msg(msg) {}
virtual const char* what() const throw () {
return m_msg.c_str();
${SHARED_ERROR_HANDLERS}
${wrappers.join('\n\n')}
`;
}
/**
* Generates a C++ wrapper file for async Z3 functions that runs each function
* synchronously in the calling thread. This version does NOT use pthreads and
* therefore does NOT require SharedArrayBuffer / COOP-COEP headers.
*
* Intended to be paired with the Emscripten build flags:
* -DSINGLE_THREAD -DPOLLING_TIMER (no -pthread / USE_PTHREADS)
*/
export function makeCCSinglethreadWrapper() {
const wrappers: string[] = [];
for (const fnName of asyncFuncs) {
const fn = functions.find(f => f.name === fnName);
if (fn == null) {
throw new Error(`could not find definition for ${fnName}`);
}
const isString = fn.cRet === 'Z3_string';
const returnType = isString ? 'auto' : fn.cRet;
const paramList = fn.params
.map(p => `${p.isConst ? 'const ' : ''}${p.cType}${p.isPtr ? '*' : ''} ${p.name}${p.isArray ? '[]' : ''}`)
.join(', ');
const callArgs = fn.params.map(p => p.name).join(', ');
const resolveExpr = isString
? `EM_ASM({
resolve_async(UTF8ToString($0));
}, result);`
: `EM_ASM({
resolve_async($0);
}, result);`;
wrappers.push(
`
extern "C" void async_${fn.name}(${paramList}) {
try {
${returnType} result = ${fn.name}(${callArgs});
${resolveExpr}
} catch (std::exception& e) {
EM_ASM({
reject_async(new Error(UTF8ToString($0)));
}, e.what());
} catch (...) {
EM_ASM({
reject_async(new Error('failed with unknown exception'));
});
}
};
void throwy_error_handler(Z3_context ctx, Z3_error_code c) {
throw Z3Exception(Z3_get_error_msg(ctx, c));
}
`.trim(),
);
}
void noop_error_handler(Z3_context ctx, Z3_error_code c) {
// pass
}
return `// THIS FILE IS AUTOMATICALLY GENERATED BY ${path.basename(__filename)}
// DO NOT EDIT IT BY HAND
extern "C" void set_throwy_error_handler(Z3_context ctx) {
Z3_set_error_handler(ctx, throwy_error_handler);
}
#include <stdexcept>
#include <string>
extern "C" void set_noop_error_handler(Z3_context ctx) {
Z3_set_error_handler(ctx, noop_error_handler);
}
#include <emscripten.h>
#include "../../z3.h"
${SHARED_ERROR_HANDLERS}
${wrappers.join('\n\n')}
`;

View file

@ -0,0 +1,41 @@
const mockInitWrapper = jest.fn();
const mockCreateApi = jest.fn();
jest.mock('./low-level', () => ({
init: mockInitWrapper,
Z3Core: undefined,
Z3LowLevel: undefined,
}));
jest.mock('./high-level', () => ({
createApi: mockCreateApi,
}));
import { init } from './browser-singlethread';
describe('browser-singlethread init', () => {
beforeEach(() => {
delete (global as any).initZ3SingleThread;
mockInitWrapper.mockReset();
mockCreateApi.mockReset();
});
it('passes module overrides to the browser-singlethread initializer', async () => {
const initZ3SingleThread = jest.fn();
const locateFile = jest.fn((file: string) => `https://example.test/${file}`);
const lowLevel = { Z3: { low: true }, em: { module: true } };
const highLevel = { Context: jest.fn() };
(global as any).initZ3SingleThread = initZ3SingleThread;
mockInitWrapper.mockResolvedValue(lowLevel);
mockCreateApi.mockReturnValue(highLevel);
const api = await init({ locateFile });
expect(mockInitWrapper).toHaveBeenCalledWith(initZ3SingleThread, { locateFile });
expect(mockCreateApi).toHaveBeenCalledWith(lowLevel.Z3, lowLevel.em);
expect(api).toEqual({ ...lowLevel, ...highLevel });
});
it('throws when initZ3SingleThread is unavailable', async () => {
await expect(init()).rejects.toThrow('initZ3SingleThread was not found.');
});
});

View file

@ -0,0 +1,35 @@
import { createApi, Z3HighLevel } from './high-level';
import { init as initWrapper, Z3LowLevel, Z3ModuleOverrides } from './low-level';
export * from './high-level/types';
export { Z3Core, Z3LowLevel } from './low-level';
export * from './low-level/types.__GENERATED__';
// Name of the global exposed by z3-built-singlethread.js when loaded as a script.
const INIT_GLOBAL = 'initZ3SingleThread';
/**
* Browser entry point for the single-threaded Z3 build.
*
* This variant does NOT require SharedArrayBuffer or the COOP/COEP HTTP headers
* that the default (threaded) build needs. All Z3 operations run synchronously
* in the calling thread, so this build should be used inside a Web Worker to
* avoid blocking the main browser thread.
*
* Before calling `init()`, make sure the `z3-built-singlethread.js` script has
* been loaded (e.g. via `importScripts('z3-built-singlethread.js')` inside the
* worker), so that `initZ3SingleThread` is available on the global object.
* @category Global */
export async function init(moduleOverrides: Z3ModuleOverrides = {}): Promise<Z3LowLevel & Z3HighLevel> {
const initZ3 = (global as any)[INIT_GLOBAL];
if (initZ3 === undefined) {
throw new Error(
`${INIT_GLOBAL} was not found. ` +
'Load z3-built-singlethread.js before calling init() ' +
'(e.g. importScripts("z3-built-singlethread.js") in a Web Worker).',
);
}
const lowLevel = await initWrapper(initZ3, moduleOverrides);
const highLevel = createApi(lowLevel.Z3, lowLevel.em);
return { ...lowLevel, ...highLevel };
}

View file

@ -0,0 +1,37 @@
const mockInitModule = jest.fn();
const mockInitWrapper = jest.fn();
const mockCreateApi = jest.fn();
jest.mock('./z3-built-singlethread', () => mockInitModule, { virtual: true });
jest.mock('./low-level', () => ({
init: mockInitWrapper,
Z3Core: undefined,
Z3LowLevel: undefined,
}));
jest.mock('./high-level', () => ({
createApi: mockCreateApi,
}));
import { init } from './node-singlethread';
describe('node-singlethread init', () => {
beforeEach(() => {
mockInitModule.mockReset();
mockInitWrapper.mockReset();
mockCreateApi.mockReset();
});
it('passes module overrides to the low-level initializer', async () => {
const locateFile = jest.fn((file: string) => `npm:z3-solver/build/${file}`);
const lowLevel = { Z3: { low: true }, em: { module: true } };
const highLevel = { Context: jest.fn() };
mockInitWrapper.mockResolvedValue(lowLevel);
mockCreateApi.mockReturnValue(highLevel);
const api = await init({ locateFile });
expect(mockInitWrapper).toHaveBeenCalledWith(mockInitModule, { locateFile });
expect(mockCreateApi).toHaveBeenCalledWith(lowLevel.Z3, lowLevel.em);
expect(api).toEqual({ ...lowLevel, ...highLevel });
});
});

View file

@ -0,0 +1,34 @@
// @ts-ignore no-implicit-any
import initModule = require('./z3-built-singlethread');
import { createApi, Z3HighLevel } from './high-level';
import { init as initWrapper, Z3LowLevel, Z3ModuleOverrides } from './low-level';
export * from './high-level/types';
export { Z3Core, Z3LowLevel } from './low-level';
export * from './low-level/types.__GENERATED__';
/**
* The main entry point to the single-threaded Z3 API.
*
* This variant does NOT require SharedArrayBuffer or the COOP/COEP HTTP headers
* that the default (threaded) build needs. All Z3 operations run synchronously
* in the calling thread, so this build should be used inside a Web Worker to
* avoid blocking the main browser thread.
*
* ```typescript
* import { init } from 'z3-solver/singlethread';
*
* const { Context } = await init();
* const { Solver, Int } = new Context('main');
*
* const x = Int.const('x');
* const solver = new Solver();
* solver.add(x.ge(0));
* console.log(await solver.check()); // 'sat'
* ```
* @category Global */
export async function init(moduleOverrides: Z3ModuleOverrides = {}): Promise<Z3HighLevel & Z3LowLevel> {
const lowLevel = await initWrapper(initModule, moduleOverrides);
const highLevel = createApi(lowLevel.Z3, lowLevel.em);
return { ...lowLevel, ...highLevel };
}

View file

@ -87,6 +87,13 @@ scoped_timer::scoped_timer(unsigned ms, event_handler * eh) {
r->set_auto_cancel();
return;
}
#endif
// In single-threaded Emscripten builds (without pthreads), std::thread is not
// usable. Timeout support is provided via POLLING_TIMER for the primary use
// case (cancel_eh<reslimit>) above. Any other event-handler type simply gets
// no timeout rather than a crash.
#if defined(SINGLE_THREAD) && !defined(__EMSCRIPTEN_PTHREADS__)
return;
#endif
workers.lock();
if (available_workers.empty()) {