mirror of
https://github.com/Z3Prover/z3
synced 2026-07-15 03:25:43 +00:00
Merge 1faba5c769 into 038b367d68
This commit is contained in:
commit
7d1bdf041e
11 changed files with 445 additions and 22 deletions
7
.github/workflows/wasm-release.yml
vendored
7
.github/workflows/wasm-release.yml
vendored
|
|
@ -48,7 +48,7 @@ jobs:
|
||||||
- name: Build TypeScript
|
- name: Build TypeScript
|
||||||
run: npm run build:ts
|
run: npm run build:ts
|
||||||
|
|
||||||
- name: Build wasm
|
- name: Build wasm (threaded)
|
||||||
run: |
|
run: |
|
||||||
emsdk install ${EM_VERSION}
|
emsdk install ${EM_VERSION}
|
||||||
emsdk activate ${EM_VERSION}
|
emsdk activate ${EM_VERSION}
|
||||||
|
|
@ -57,6 +57,11 @@ jobs:
|
||||||
which clang++
|
which clang++
|
||||||
npm run build:wasm
|
npm run build:wasm
|
||||||
|
|
||||||
|
- name: Build wasm (single-threaded)
|
||||||
|
run: |
|
||||||
|
source $(dirname $(which emsdk))/emsdk_env.sh
|
||||||
|
npm run build:wasm:singlethread
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|
||||||
|
|
|
||||||
36
.github/workflows/wasm.yml
vendored
36
.github/workflows/wasm.yml
vendored
|
|
@ -17,7 +17,7 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
name: Check
|
name: Check (threaded)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
@ -52,3 +52,37 @@ jobs:
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: npm 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
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,66 @@ const api = await init({
|
||||||
|
|
||||||
### Limitations
|
### 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.
|
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
|
## 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`
|
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`
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,18 @@
|
||||||
"browser": "build/browser.js",
|
"browser": "build/browser.js",
|
||||||
"main": "build/node.js",
|
"main": "build/node.js",
|
||||||
"types": "build/node.d.ts",
|
"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": [
|
"files": [
|
||||||
"build/**/*.{js,d.ts,wasm}"
|
"build/**/*.{js,d.ts,wasm}"
|
||||||
],
|
],
|
||||||
|
|
@ -26,6 +38,7 @@
|
||||||
"build:ts:tsc": "tsc --pretty --project tsconfig.build.json ",
|
"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: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": "ts-node --transpileOnly ./scripts/build-wasm.ts",
|
||||||
|
"build:wasm:singlethread": "ts-node --transpileOnly ./scripts/build-wasm-singlethread.ts",
|
||||||
"clean": "rimraf build 'src/**/*.__GENERATED__.*'",
|
"clean": "rimraf build 'src/**/*.__GENERATED__.*'",
|
||||||
"lint": "prettier -c '{./,src/,scripts/,examples/}**/*.{js,ts}'",
|
"lint": "prettier -c '{./,src/,scripts/,examples/}**/*.{js,ts}'",
|
||||||
"format": "prettier --write '{./,src/,scripts/}**/*.{js,ts}'",
|
"format": "prettier --write '{./,src/,scripts/}**/*.{js,ts}'",
|
||||||
|
|
|
||||||
86
src/api/js/scripts/build-wasm-singlethread.ts
Normal file
86
src/api/js/scripts/build-wasm-singlethread.ts
Normal 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');
|
||||||
|
|
@ -4,6 +4,34 @@ import path from 'path';
|
||||||
import { asyncFuncs } from './async-fns';
|
import { asyncFuncs } from './async-fns';
|
||||||
import { functions } from './parse-api';
|
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() {
|
export function makeCCWrapper() {
|
||||||
let wrappers = [];
|
let wrappers = [];
|
||||||
|
|
||||||
|
|
@ -213,30 +241,77 @@ void wrapper_str(Args&&... args) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Z3Exception : public std::exception {
|
${SHARED_ERROR_HANDLERS}
|
||||||
public:
|
|
||||||
const std::string m_msg;
|
${wrappers.join('\n\n')}
|
||||||
Z3Exception(const std::string& msg) : m_msg(msg) {}
|
`;
|
||||||
virtual const char* what() const throw () {
|
}
|
||||||
return m_msg.c_str();
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
return `// THIS FILE IS AUTOMATICALLY GENERATED BY ${path.basename(__filename)}
|
||||||
// pass
|
// DO NOT EDIT IT BY HAND
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void set_throwy_error_handler(Z3_context ctx) {
|
#include <stdexcept>
|
||||||
Z3_set_error_handler(ctx, throwy_error_handler);
|
#include <string>
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void set_noop_error_handler(Z3_context ctx) {
|
#include <emscripten.h>
|
||||||
Z3_set_error_handler(ctx, noop_error_handler);
|
|
||||||
}
|
#include "../../z3.h"
|
||||||
|
|
||||||
|
${SHARED_ERROR_HANDLERS}
|
||||||
|
|
||||||
${wrappers.join('\n\n')}
|
${wrappers.join('\n\n')}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
41
src/api/js/src/browser-singlethread.test.ts
Normal file
41
src/api/js/src/browser-singlethread.test.ts
Normal 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.');
|
||||||
|
});
|
||||||
|
});
|
||||||
35
src/api/js/src/browser-singlethread.ts
Normal file
35
src/api/js/src/browser-singlethread.ts
Normal 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 };
|
||||||
|
}
|
||||||
37
src/api/js/src/node-singlethread.test.ts
Normal file
37
src/api/js/src/node-singlethread.test.ts
Normal 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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
34
src/api/js/src/node-singlethread.ts
Normal file
34
src/api/js/src/node-singlethread.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
|
@ -87,6 +87,13 @@ scoped_timer::scoped_timer(unsigned ms, event_handler * eh) {
|
||||||
r->set_auto_cancel();
|
r->set_auto_cancel();
|
||||||
return;
|
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
|
#endif
|
||||||
workers.lock();
|
workers.lock();
|
||||||
if (available_workers.empty()) {
|
if (available_workers.empty()) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue