3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-05-27 12:26:25 +00:00

Allow JS/WASM init to accept Deno-friendly wasm load paths (#9621)

The JS bindings currently assume the wasm can be located via the default
Emscripten path resolution, which can force Deno users into
`--allow-read`. This change lets callers provide a custom wasm load path
through `init(...)`, so Deno can resolve the packaged wasm asset without
filesystem reads.

- **Public init API**
- Extend the JS entrypoints (`node` and `browser`) so `init(...)`
accepts optional Emscripten module overrides.
- Surface a typed `Z3ModuleOverrides` shape with explicit support for
`locateFile(path, prefix)`.

- **Low-level initialization**
- Thread module overrides through the generated low-level wrapper
instead of always calling the Emscripten module factory with no
arguments.
  - Keep the default behavior unchanged when no overrides are provided.

- **Docs**
  - Document the Deno usage pattern in the published JS README.
- Clarify the `locateFile` signature and show the intended Deno 2.1+
`import.meta.resolve(...)` flow.

- **Focused coverage**
- Add unit tests for `node` and `browser` init to verify module
overrides are forwarded correctly.

Example:

```ts
import { init } from 'npm:z3-solver';

const api = await init({
  locateFile: (file, _prefix): string =>
    import.meta.resolve(`npm:z3-solver/build/${file}`),
});
```

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
This commit is contained in:
Copilot 2026-05-26 14:02:04 -07:00 committed by GitHub
parent 2d827af322
commit 91205d2f60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 110 additions and 8 deletions

View file

@ -16,6 +16,17 @@ 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');`.
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. In Deno 2.1+, `import.meta.resolve(...)` returns a string synchronously, so it can be used directly in `locateFile`. For example:
```typescript
import { init } from 'npm:z3-solver';
const api = await init({
locateFile: (file, _prefix): string =>
import.meta.resolve(`npm:z3-solver/build/${file}`), // _prefix is unused here
});
```
### 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.

View file

@ -444,8 +444,8 @@ ${Object.entries(primitiveTypes)
.map(e => `type ${e[0]} = ${e[1]};`)
.join('\n')}
export async function init(initModule: any) {
let Mod = await initModule();
export async function init(initModule: any, moduleOverrides: Record<string, unknown> = {}) {
let Mod = await initModule(moduleOverrides);
// this works for both signed and unsigned, because JS will wrap for you when constructing the Uint32Array
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 { init as initWrapper, Z3LowLevel } from './low-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__';
export async function init(): Promise<Z3LowLevel & Z3HighLevel> {
export async function init(moduleOverrides: Z3ModuleOverrides = {}): Promise<Z3LowLevel & Z3HighLevel> {
const initZ3 = (global as any).initZ3;
if (initZ3 === undefined) {
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);
return { ...lowLevel, ...highLevel };
}

View file

@ -1,4 +1,8 @@
export * from './types.__GENERATED__';
export * from './wrapper.__GENERATED__';
export type Z3ModuleOverrides = {
locateFile?: (path: string, prefix: string) => string;
[key: string]: unknown;
};
export type Z3Core = Awaited<ReturnType<(typeof import('./wrapper.__GENERATED__'))['init']>>['Z3'];
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 { 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 { Z3Core, Z3LowLevel } from './low-level';
export * from './low-level/types.__GENERATED__';
@ -29,10 +29,17 @@ export * from './low-level/types.__GENERATED__';
*
* console.log(`x=${model.get(x)}, y=${model.get(y)}`);
* // 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, _prefix): string =>
* // import.meta.resolve(`npm:z3-solver/build/${file}`), // _prefix is unused here
* // });
* ```
* @category Global */
export async function init(): Promise<Z3HighLevel & Z3LowLevel> {
const lowLevel = await initWrapper(initModule);
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 };
}