3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-05-29 21:36:30 +00:00

feat: allow custom wasm load paths for js init

Agent-Logs-Url: https://github.com/Z3Prover/z3/sessions/3070f139-a412-4e40-8446-bd569f9ffa55

Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-25 16:45:59 +00:00 committed by GitHub
parent d19585bd03
commit a30faacba1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 105 additions and 8 deletions

View file

@ -16,6 +16,16 @@ const {
This package has different initialization for browser and node. Your bundler and node should choose good version automatically, but you can import the one you need manually - `const { init } = require('z3-solver/node');` or `const { init } = require('z3-solver/browser');`. This package has different initialization for browser and node. Your bundler and node should choose good version automatically, but you can import the one you need manually - `const { init } = require('z3-solver/node');` or `const { init } = require('z3-solver/browser');`.
The `init` function also accepts an optional Emscripten module overrides object. This is useful in runtimes such as Deno where you may want to provide a wasm load path explicitly instead of relying on filesystem reads. For example:
```typescript
import { init } from 'npm:z3-solver';
const api = await init({
locateFile: (file) => import.meta.resolve(`npm:z3-solver/build/${file}`),
});
```
### 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 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.

View file

@ -444,8 +444,8 @@ ${Object.entries(primitiveTypes)
.map(e => `type ${e[0]} = ${e[1]};`) .map(e => `type ${e[0]} = ${e[1]};`)
.join('\n')} .join('\n')}
export async function init(initModule: any) { export async function init(initModule: any, moduleOverrides: Record<string, unknown> = {}) {
let Mod = await initModule(); let Mod = await initModule(moduleOverrides);
// this works for both signed and unsigned, because JS will wrap for you when constructing the Uint32Array // this works for both signed and unsigned, because JS will wrap for you when constructing the Uint32Array
function intArrayToByteArr(ints: number[]) { function intArrayToByteArr(ints: number[]) {

View file

@ -0,0 +1,43 @@
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';
describe('browser init', () => {
beforeEach(() => {
delete (global as any).initZ3;
mockInitWrapper.mockReset();
mockCreateApi.mockReset();
});
it('passes module overrides to the browser initializer', async () => {
const initZ3 = 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).initZ3 = initZ3;
mockInitWrapper.mockResolvedValue(lowLevel);
mockCreateApi.mockReturnValue(highLevel);
const api = await init({ locateFile });
expect(mockInitWrapper).toHaveBeenCalledWith(initZ3, { locateFile });
expect(mockCreateApi).toHaveBeenCalledWith(lowLevel.Z3, lowLevel.em);
expect(api).toEqual({ ...lowLevel, ...highLevel });
});
it('throws when initZ3 is unavailable', async () => {
await expect(init()).rejects.toThrow(
'initZ3 was not imported correctly. Please consult documentation on how to load Z3 in browser',
);
});
});

View file

@ -1,16 +1,16 @@
import { createApi, Z3HighLevel } from './high-level'; import { createApi, Z3HighLevel } from './high-level';
import { init as initWrapper, Z3LowLevel } from './low-level'; import { init as initWrapper, Z3LowLevel, Z3ModuleOverrides } from './low-level';
export * from './high-level/types'; export * from './high-level/types';
export { Z3Core, Z3LowLevel } from './low-level'; export { Z3Core, Z3LowLevel } from './low-level';
export * from './low-level/types.__GENERATED__'; export * from './low-level/types.__GENERATED__';
export async function init(): Promise<Z3LowLevel & Z3HighLevel> { export async function init(moduleOverrides: Z3ModuleOverrides = {}): Promise<Z3LowLevel & Z3HighLevel> {
const initZ3 = (global as any).initZ3; const initZ3 = (global as any).initZ3;
if (initZ3 === undefined) { if (initZ3 === undefined) {
throw new Error('initZ3 was not imported correctly. Please consult documentation on how to load Z3 in browser'); throw new Error('initZ3 was not imported correctly. Please consult documentation on how to load Z3 in browser');
} }
const lowLevel = await initWrapper(initZ3); const lowLevel = await initWrapper(initZ3, moduleOverrides);
const highLevel = createApi(lowLevel.Z3, lowLevel.em); const highLevel = createApi(lowLevel.Z3, lowLevel.em);
return { ...lowLevel, ...highLevel }; return { ...lowLevel, ...highLevel };
} }

View file

@ -1,4 +1,5 @@
export * from './types.__GENERATED__'; export * from './types.__GENERATED__';
export * from './wrapper.__GENERATED__'; export * from './wrapper.__GENERATED__';
export type Z3ModuleOverrides = Record<string, unknown>;
export type Z3Core = Awaited<ReturnType<(typeof import('./wrapper.__GENERATED__'))['init']>>['Z3']; export type Z3Core = Awaited<ReturnType<(typeof import('./wrapper.__GENERATED__'))['init']>>['Z3'];
export type Z3LowLevel = Awaited<ReturnType<(typeof import('./wrapper.__GENERATED__'))['init']>>; export type Z3LowLevel = Awaited<ReturnType<(typeof import('./wrapper.__GENERATED__'))['init']>>;

View file

@ -0,0 +1,37 @@
const mockInitModule = jest.fn();
const mockInitWrapper = jest.fn();
const mockCreateApi = jest.fn();
jest.mock('./z3-built', () => mockInitModule, { virtual: true });
jest.mock('./low-level', () => ({
init: mockInitWrapper,
Z3Core: undefined,
Z3LowLevel: undefined,
}));
jest.mock('./high-level', () => ({
createApi: mockCreateApi,
}));
import { init } from './node';
describe('node 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

@ -2,7 +2,7 @@
import initModule = require('./z3-built'); import initModule = require('./z3-built');
import { createApi, Z3HighLevel } from './high-level'; import { createApi, Z3HighLevel } from './high-level';
import { init as initWrapper, Z3LowLevel } from './low-level'; import { init as initWrapper, Z3LowLevel, Z3ModuleOverrides } from './low-level';
export * from './high-level/types'; export * from './high-level/types';
export { Z3Core, Z3LowLevel } from './low-level'; export { Z3Core, Z3LowLevel } from './low-level';
export * from './low-level/types.__GENERATED__'; export * from './low-level/types.__GENERATED__';
@ -29,10 +29,16 @@ export * from './low-level/types.__GENERATED__';
* *
* console.log(`x=${model.get(x)}, y=${model.get(y)}`); * console.log(`x=${model.get(x)}, y=${model.get(y)}`);
* // x=0, y=12 * // x=0, y=12
*
* // Deno users can provide an Emscripten locateFile hook to load the wasm
* // through npm's asset resolution instead of filesystem reads.
* // const api = await init({
* // locateFile: (file) => import.meta.resolve(`npm:z3-solver/build/${file}`),
* // });
* ``` * ```
* @category Global */ * @category Global */
export async function init(): Promise<Z3HighLevel & Z3LowLevel> { export async function init(moduleOverrides: Z3ModuleOverrides = {}): Promise<Z3HighLevel & Z3LowLevel> {
const lowLevel = await initWrapper(initModule); const lowLevel = await initWrapper(initModule, moduleOverrides);
const highLevel = createApi(lowLevel.Z3, lowLevel.em); const highLevel = createApi(lowLevel.Z3, lowLevel.em);
return { ...lowLevel, ...highLevel }; return { ...lowLevel, ...highLevel };
} }