mirror of
https://github.com/Z3Prover/z3
synced 2025-05-07 07:45:46 +00:00
Add high level bindings for js (#6048)
* [Draft] Added unfinished code for high level bindings for js * * Rewrote structure of js api files * Added more high level apis * Minor fixes * Fixed wasm github action * Fix JS test * Removed ContextOptions type * * Added Ints to JS Api * Added tests to JS Api * Added run-time checks for contexts * Removed default contexts * Merged Context and createContext so that the api behaves the sames as in other constructors * Added a test for Solver * Added Reals * Added classes for IntVals and RealVals * Added abillity to specify logic for solver * Try to make CI tests not fail * Changed APIs after a round of review * Fix test * Added BitVectors * Made sort into getter * Added initial JS docs * Added more coercible types * Removed done TODOs
This commit is contained in:
parent
3d00d1d56b
commit
7fdcbbaee9
37 changed files with 15973 additions and 643 deletions
|
@ -1,8 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
// things which you probably want to do off-thread
|
||||
// from https://github.com/Z3Prover/z3/issues/5746#issuecomment-1006289146
|
||||
module.exports = [
|
||||
export const asyncFuncs = [
|
||||
'Z3_eval_smtlib2_string',
|
||||
'Z3_simplify',
|
||||
'Z3_simplify_ex',
|
77
src/api/js/scripts/build-wasm.ts
Normal file
77
src/api/js/scripts/build-wasm.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
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 { makeCCWrapper } from './make-cc-wrapper';
|
||||
import { functions } from './parse-api';
|
||||
|
||||
console.log('--- Building WASM');
|
||||
|
||||
const SWAP_OPTS: SpawnOptions = {
|
||||
shell: true,
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
CXXFLAGS: '-pthread -s USE_PTHREADS=1 -s DISABLE_EXCEPTION_CATCHING=0',
|
||||
LDFLAGS: '-s WASM_BIGINT -s -pthread -s USE_PTHREADS=1',
|
||||
FPMATH_ENABLED: 'False', // Until Safari supports WASM SSE, we have to disable fast FP support
|
||||
// TODO(ritave): Setting EM_CACHE breaks compiling on M1 MacBook
|
||||
//EM_CACHE: path.join(os.homedir(), '.emscripten/'),
|
||||
},
|
||||
};
|
||||
|
||||
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 = ['_set_throwy_error_handler', '_set_noop_error_handler', ...asyncFuncs.map(f => '_async_' + f)];
|
||||
|
||||
// TODO(ritave): This variable is unused in original script, find out if it's important
|
||||
const fns: any[] = (functions as any[]).filter(f => !asyncFuncs.includes(f.name));
|
||||
|
||||
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(), '../../../');
|
||||
|
||||
// TODO(ritave): Detect if it's in the configuration we need
|
||||
if (!existsSync(path.join(z3RootDir, 'build/Makefile'))) {
|
||||
spawnSync('emconfigure python scripts/mk_make.py --staticlib --single-threaded --arm64=false', {
|
||||
cwd: z3RootDir,
|
||||
});
|
||||
}
|
||||
|
||||
spawnSync(`emmake make -j${os.cpus().length} libz3.a`, { cwd: path.join(z3RootDir, 'build') });
|
||||
|
||||
const ccWrapperPath = 'build/async-fns.cc';
|
||||
console.log(`- Building ${ccWrapperPath}`);
|
||||
fs.mkdirSync(path.dirname(ccWrapperPath), { recursive: true });
|
||||
fs.writeFileSync(ccWrapperPath, makeCCWrapper());
|
||||
|
||||
const fns = JSON.stringify(exportedFuncs());
|
||||
const methods = '["ccall","FS","allocate","UTF8ToString","intArrayFromString","ALLOC_NORMAL"]';
|
||||
const libz3a = path.normalize('../../../build/libz3.a');
|
||||
spawnSync(
|
||||
`emcc build/async-fns.cc ${libz3a} --std=c++20 --pre-js src/low-level/async-wrapper.js -g2 -pthread -fexceptions -s WASM_BIGINT -s USE_PTHREADS=1 -s PTHREAD_POOL_SIZE=0 -s PTHREAD_POOL_SIZE_STRICT=0 -s MODULARIZE=1 -s 'EXPORT_NAME="initZ3"' -s EXPORTED_RUNTIME_METHODS=${methods} -s EXPORTED_FUNCTIONS=${fns} -s DISABLE_EXCEPTION_CATCHING=0 -s SAFE_HEAP=0 -s DEMANGLE_SUPPORT=1 -s TOTAL_MEMORY=1GB -I z3/src/api/ -o build/z3-built.js`,
|
||||
);
|
||||
|
||||
fs.rmSync(ccWrapperPath);
|
||||
|
||||
console.log('--- WASM build finished');
|
|
@ -1,11 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
// this is called by build.sh to generate the names of the bindings to export
|
||||
|
||||
let { functions } = require('./parse-api.js');
|
||||
let asyncFns = require('./async-fns.js');
|
||||
|
||||
let extras = ['_set_throwy_error_handler', '_set_noop_error_handler', ...asyncFns.map(f => '_async_' + f)];
|
||||
let fns = functions.filter(f => !asyncFns.includes(f.name));
|
||||
|
||||
console.log(JSON.stringify([...extras, ...functions.map(f => '_' + f.name)]));
|
|
@ -1,40 +1,38 @@
|
|||
'use strict';
|
||||
|
||||
// generates c wrappers with off-thread versions of specified functions
|
||||
|
||||
let path = require('path');
|
||||
import path from 'path';
|
||||
import { asyncFuncs } from './async-fns';
|
||||
import { functions } from './parse-api';
|
||||
|
||||
let { functions } = require('./parse-api.js');
|
||||
let asyncFns = require('./async-fns.js');
|
||||
export function makeCCWrapper() {
|
||||
let wrappers = [];
|
||||
|
||||
let wrappers = [];
|
||||
for (let fnName of asyncFuncs) {
|
||||
let fn = functions.find(f => f.name === fnName);
|
||||
if (fn == null) {
|
||||
throw new Error(`could not find definition for ${fnName}`);
|
||||
}
|
||||
let wrapper;
|
||||
if (fn.cRet === 'Z3_string') {
|
||||
wrapper = `wrapper_str`;
|
||||
} else if (['int', 'unsigned', 'void'].includes(fn.cRet) || fn.cRet.startsWith('Z3_')) {
|
||||
wrapper = `wrapper`;
|
||||
} else {
|
||||
throw new Error(`async function with unknown return type ${fn.cRet}`);
|
||||
}
|
||||
|
||||
for (let fnName of asyncFns) {
|
||||
let fn = functions.find(f => f.name === fnName);
|
||||
if (fn == null) {
|
||||
throw new Error(`could not find definition for ${fnName}`);
|
||||
}
|
||||
let wrapper;
|
||||
if (fn.cRet === 'Z3_string') {
|
||||
wrapper = `wrapper_str`;
|
||||
} else if (['int', 'unsigned', 'void'].includes(fn.cRet) || fn.cRet.startsWith('Z3_')) {
|
||||
wrapper = `wrapper`;
|
||||
} else {
|
||||
throw new Error(`async function with unknown return type ${fn.cRet}`);
|
||||
}
|
||||
|
||||
wrappers.push(
|
||||
`
|
||||
wrappers.push(
|
||||
`
|
||||
extern "C" void async_${fn.name}(${fn.params
|
||||
.map(p => `${p.isConst ? 'const ' : ''}${p.cType}${p.isPtr ? '*' : ''} ${p.name}${p.isArray ? '[]' : ''}`)
|
||||
.join(', ')}) {
|
||||
.map(p => `${p.isConst ? 'const ' : ''}${p.cType}${p.isPtr ? '*' : ''} ${p.name}${p.isArray ? '[]' : ''}`)
|
||||
.join(', ')}) {
|
||||
${wrapper}<decltype(&${fn.name}), &${fn.name}>(${fn.params.map(p => `${p.name}`).join(', ')});
|
||||
}
|
||||
`.trim(),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`// THIS FILE IS AUTOMATICALLY GENERATED BY ${path.basename(__filename)}
|
||||
return `// THIS FILE IS AUTOMATICALLY GENERATED BY ${path.basename(__filename)}
|
||||
// DO NOT EDIT IT BY HAND
|
||||
|
||||
#include <thread>
|
||||
|
@ -112,4 +110,10 @@ extern "C" void set_noop_error_handler(Z3_context ctx) {
|
|||
Z3_set_error_handler(ctx, noop_error_handler);
|
||||
}
|
||||
|
||||
${wrappers.join('\n\n')}`);
|
||||
${wrappers.join('\n\n')}
|
||||
`;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
console.log(makeCCWrapper());
|
||||
}
|
|
@ -1,434 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let path = require('path');
|
||||
let prettier = require('prettier');
|
||||
|
||||
let { primitiveTypes, types, enums, functions } = require('./parse-api.js');
|
||||
let asyncFns = require('./async-fns.js');
|
||||
|
||||
let subtypes = {
|
||||
__proto__: null,
|
||||
Z3_sort: 'Z3_ast',
|
||||
Z3_func_decl: 'Z3_ast',
|
||||
};
|
||||
|
||||
let makePointerType = t =>
|
||||
`export type ${t} = ` + (t in subtypes ? `Subpointer<'${t}', '${subtypes[t]}'>;` : `Pointer<'${t}'>;`);
|
||||
|
||||
// this supports a up to 6 out intergers/pointers
|
||||
// or up to 3 out int64s
|
||||
const BYTES_TO_ALLOCATE_FOR_OUT_PARAMS = 24;
|
||||
|
||||
const CUSTOM_IMPLEMENTATIONS = ['Z3_mk_context', 'Z3_mk_context_rc'];
|
||||
|
||||
function toEmType(type) {
|
||||
if (type in primitiveTypes) {
|
||||
type = primitiveTypes[type];
|
||||
}
|
||||
if (['boolean', 'number', 'string', 'bigint', 'void'].includes(type)) {
|
||||
return type;
|
||||
}
|
||||
if (type.startsWith('Z3_')) {
|
||||
return 'number';
|
||||
}
|
||||
throw new Error(`unknown parameter type ${type}`);
|
||||
}
|
||||
|
||||
function isZ3PointerType(type) {
|
||||
return type.startsWith('Z3_');
|
||||
}
|
||||
|
||||
function toEm(p) {
|
||||
if (typeof p === 'string') {
|
||||
// we've already set this, e.g. by replacing it with an expression
|
||||
return p;
|
||||
}
|
||||
let { type } = p;
|
||||
if (p.kind === 'out') {
|
||||
throw new Error(`unknown out parameter type ${JSON.stringify(p)}`);
|
||||
}
|
||||
if (p.isArray) {
|
||||
if (isZ3PointerType(type) || type === 'unsigned' || type === 'int') {
|
||||
// this works for nullables also because null coerces to 0
|
||||
return `intArrayToByteArr(${p.name} as unknown as number[])`;
|
||||
} else if (type === 'boolean') {
|
||||
return `boolArrayToByteArr(${p.name})`;
|
||||
} else {
|
||||
throw new Error(`only know how to deal with arrays of int/bool (got ${type})`);
|
||||
}
|
||||
}
|
||||
if (type in primitiveTypes) {
|
||||
type = primitiveTypes[type];
|
||||
}
|
||||
|
||||
if (['boolean', 'number', 'bigint', 'string'].includes(type)) {
|
||||
return p.name;
|
||||
}
|
||||
if (type.startsWith('Z3_')) {
|
||||
return p.name;
|
||||
}
|
||||
throw new Error(`unknown parameter type ${JSON.stringify(p)}`);
|
||||
}
|
||||
|
||||
let isInParam = p => ['in', 'in_array'].includes(p.kind);
|
||||
function wrapFunction(fn) {
|
||||
if (CUSTOM_IMPLEMENTATIONS.includes(fn.name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let inParams = fn.params.filter(isInParam);
|
||||
let outParams = fn.params.map((p, idx) => ({ ...p, idx })).filter(p => !isInParam(p));
|
||||
|
||||
// we'll figure out how to deal with these cases later
|
||||
let unknownInParam = inParams.find(
|
||||
p =>
|
||||
p.isPtr ||
|
||||
p.type === 'Z3_char_ptr' ||
|
||||
(p.isArray && !(isZ3PointerType(p.type) || p.type === 'unsigned' || p.type === 'int' || p.type === 'boolean')),
|
||||
);
|
||||
if (unknownInParam) {
|
||||
console.error(`skipping ${fn.name} - unknown in parameter ${JSON.stringify(unknownInParam)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fn.ret === 'Z3_char_ptr') {
|
||||
console.error(`skipping ${fn.name} - returns a string or char pointer`);
|
||||
return null;
|
||||
}
|
||||
// console.error(fn.name);
|
||||
|
||||
let isAsync = asyncFns.includes(fn.name);
|
||||
let trivial =
|
||||
!['string', 'boolean'].includes(fn.ret) &&
|
||||
!fn.nullableRet &&
|
||||
outParams.length === 0 &&
|
||||
!inParams.some(p => p.type === 'string' || p.isArray || p.nullable);
|
||||
|
||||
let name = fn.name.startsWith('Z3_') ? fn.name.substring(3) : fn.name;
|
||||
|
||||
let params = inParams.map(p => {
|
||||
let type = p.type;
|
||||
if (p.isArray && p.nullable) {
|
||||
type = `(${type} | null)[]`;
|
||||
} else if (p.isArray) {
|
||||
type = `${type}[]`;
|
||||
} else if (p.nullable) {
|
||||
type = `${type} | null`;
|
||||
}
|
||||
return `${p.name}: ${type}`;
|
||||
});
|
||||
|
||||
if (trivial && isAsync) {
|
||||
// i.e. and async
|
||||
return `${name}: function (${params.join(', ')}): Promise<${fn.ret}> {
|
||||
return Mod.async_call(Mod._async_${fn.name}, ${fn.params.map(toEm).join(', ')});
|
||||
}`;
|
||||
}
|
||||
|
||||
if (trivial) {
|
||||
return `${name}: Mod._${fn.name} as ((${params.join(', ')}) => ${fn.ret})`;
|
||||
}
|
||||
|
||||
// otherwise fall back to ccall
|
||||
|
||||
let ctypes = fn.params.map(p =>
|
||||
p.kind === 'in_array' ? 'array' : p.kind === 'out_array' ? 'number' : p.isPtr ? 'number' : toEmType(p.type),
|
||||
);
|
||||
|
||||
let prefix = '';
|
||||
let infix = '';
|
||||
let rv = 'ret';
|
||||
let suffix = '';
|
||||
|
||||
let args = fn.params;
|
||||
|
||||
let arrayLengthParams = new Map();
|
||||
for (let p of inParams) {
|
||||
if (p.nullable && !p.isArray) {
|
||||
// this would be easy to implement - just map null to 0 - but nothing actually uses nullable non-array input parameters, so we can't ensure we've done it right
|
||||
console.error(`skipping ${fn.name} - nullable input parameter`);
|
||||
return null;
|
||||
}
|
||||
if (!p.isArray) {
|
||||
continue;
|
||||
}
|
||||
let { sizeIndex } = p;
|
||||
if (arrayLengthParams.has(sizeIndex)) {
|
||||
let otherParam = arrayLengthParams.get(sizeIndex);
|
||||
prefix += `
|
||||
if (${otherParam}.length !== ${p.name}.length) {
|
||||
throw new TypeError(\`${otherParam} and ${p.name} must be the same length (got \${${otherParam}.length} and \{${p.name}.length})\`);
|
||||
}
|
||||
`.trim();
|
||||
continue;
|
||||
}
|
||||
arrayLengthParams.set(sizeIndex, p.name);
|
||||
|
||||
let sizeParam = fn.params[sizeIndex];
|
||||
if (!(sizeParam.kind === 'in' && sizeParam.type === 'unsigned' && !sizeParam.isPtr && !sizeParam.isArray)) {
|
||||
throw new Error(
|
||||
`size index is not unsigned int (for fn ${fn.name} parameter ${sizeIndex} got ${sizeParam.type})`,
|
||||
);
|
||||
}
|
||||
args[sizeIndex] = `${p.name}.length`;
|
||||
params[sizeIndex] = null;
|
||||
}
|
||||
|
||||
let returnType = fn.ret;
|
||||
let cReturnType = toEmType(fn.ret);
|
||||
if (outParams.length > 0) {
|
||||
let mapped = [];
|
||||
let memIdx = 0; // offset from `outAddress` where the data should get written, in units of 4 bytes
|
||||
|
||||
for (let outParam of outParams) {
|
||||
if (outParam.isArray) {
|
||||
if (isZ3PointerType(outParam.type) || outParam.type === 'unsigned') {
|
||||
let { sizeIndex } = outParam;
|
||||
|
||||
let count;
|
||||
if (arrayLengthParams.has(sizeIndex)) {
|
||||
// i.e. this is also the length of an input array
|
||||
count = args[sizeIndex];
|
||||
} else {
|
||||
let sizeParam = fn.params[sizeIndex];
|
||||
if (!(sizeParam.kind === 'in' && sizeParam.type === 'unsigned' && !sizeParam.isPtr && !sizeParam.isArray)) {
|
||||
throw new Error(
|
||||
`size index is not unsigned int (for fn ${fn.name} parameter ${sizeIndex} got ${sizeParam.type})`,
|
||||
);
|
||||
}
|
||||
count = sizeParam.name;
|
||||
}
|
||||
let outArrayAddress = `outArray_${outParam.name}`;
|
||||
prefix += `
|
||||
let ${outArrayAddress} = Mod._malloc(4 * ${count});
|
||||
try {
|
||||
`.trim();
|
||||
suffix =
|
||||
`
|
||||
} finally {
|
||||
Mod._free(${outArrayAddress});
|
||||
}
|
||||
`.trim() + suffix;
|
||||
args[outParam.idx] = outArrayAddress;
|
||||
mapped.push({
|
||||
name: outParam.name,
|
||||
read:
|
||||
`readUintArray(${outArrayAddress}, ${count})` +
|
||||
(outParam.type === 'unsigned' ? '' : `as unknown as ${outParam.type}[]`),
|
||||
type: `${outParam.type}[]`,
|
||||
});
|
||||
} else {
|
||||
console.error(`skipping ${fn.name} - out array of ${outParam.type}`);
|
||||
return null;
|
||||
}
|
||||
} else if (outParam.isPtr) {
|
||||
function setArg() {
|
||||
args[outParam.idx] = memIdx === 0 ? 'outAddress' : `outAddress + ${memIdx * 4}`;
|
||||
}
|
||||
let read, type;
|
||||
if (outParam.type === 'string') {
|
||||
read = `Mod.UTF8ToString(getOutUint(${memIdx}))`;
|
||||
setArg();
|
||||
++memIdx;
|
||||
} else if (isZ3PointerType(outParam.type)) {
|
||||
read = `getOutUint(${memIdx}) as unknown as ${outParam.type}`;
|
||||
setArg();
|
||||
++memIdx;
|
||||
} else if (outParam.type === 'unsigned') {
|
||||
read = `getOutUint(${memIdx})`;
|
||||
setArg();
|
||||
++memIdx;
|
||||
} else if (outParam.type === 'int') {
|
||||
read = `getOutInt(${memIdx})`;
|
||||
setArg();
|
||||
++memIdx;
|
||||
} else if (outParam.type === 'uint64_t') {
|
||||
if (memIdx % 2 === 1) {
|
||||
++memIdx;
|
||||
}
|
||||
read = `getOutUint64(${memIdx / 2})`;
|
||||
setArg();
|
||||
memIdx += 2;
|
||||
} else if (outParam.type === 'int64_t') {
|
||||
if (memIdx % 2 === 1) {
|
||||
++memIdx;
|
||||
}
|
||||
read = `getOutInt64(${memIdx / 2})`;
|
||||
setArg();
|
||||
memIdx += 2;
|
||||
} else {
|
||||
console.error(`skipping ${fn.name} - unknown out parameter type ${outParam.type}`);
|
||||
return null;
|
||||
}
|
||||
if (memIdx > Math.floor(BYTES_TO_ALLOCATE_FOR_OUT_PARAMS / 4)) {
|
||||
// prettier-ignore
|
||||
console.error(`skipping ${fn.name} - out parameter sizes sum to ${memIdx * 4}, which is > ${BYTES_TO_ALLOCATE_FOR_OUT_PARAMS}`);
|
||||
return null;
|
||||
}
|
||||
mapped.push({
|
||||
name: outParam.name,
|
||||
read,
|
||||
type: outParam.type,
|
||||
});
|
||||
} else {
|
||||
console.error(`skipping ${fn.name} - out param is neither pointer nor array`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let ignoreReturn = fn.ret === 'boolean' || fn.ret === 'void';
|
||||
if (outParams.length === 1) {
|
||||
let outParam = mapped[0];
|
||||
if (ignoreReturn) {
|
||||
returnType = outParam.type;
|
||||
rv = outParam.read;
|
||||
} else {
|
||||
returnType = `{ rv: ${fn.ret}, ${outParam.name} : ${outParam.type} }`;
|
||||
rv = `{ rv: ret, ${outParam.name} : ${outParam.read} }`;
|
||||
}
|
||||
} else {
|
||||
if (ignoreReturn) {
|
||||
returnType = `{ ${mapped.map(p => `${p.name} : ${p.type}`).join(', ')} }`;
|
||||
rv = `{ ${mapped.map(p => `${p.name}: ${p.read}`).join(', ')} }`;
|
||||
} else {
|
||||
returnType = `{ rv: ${fn.ret}, ${mapped.map(p => `${p.name} : ${p.type}`).join(', ')} }`;
|
||||
rv = `{ rv: ret, ${mapped.map(p => `${p.name}: ${p.read}`).join(', ')} }`;
|
||||
}
|
||||
}
|
||||
|
||||
if (fn.ret === 'boolean') {
|
||||
// assume the boolean indicates success
|
||||
infix += `
|
||||
if (!ret) {
|
||||
return null;
|
||||
}
|
||||
`.trim();
|
||||
cReturnType = 'boolean';
|
||||
returnType += ' | null';
|
||||
} else if (fn.ret === 'void') {
|
||||
cReturnType = 'void';
|
||||
} else if (isZ3PointerType(fn.ret) || fn.ret === 'unsigned') {
|
||||
cReturnType = 'number';
|
||||
} else {
|
||||
console.error(`skipping ${fn.name} - out parameter for function which returns non-boolean`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (fn.nullableRet) {
|
||||
returnType += ' | null';
|
||||
infix += `
|
||||
if (ret === 0) {
|
||||
return null;
|
||||
}
|
||||
`.trim();
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
let invocation = `Mod.ccall('${isAsync ? 'async_' : ''}${fn.name}', '${cReturnType}', ${JSON.stringify(ctypes)}, [${args.map(toEm).join(', ')}])`;
|
||||
|
||||
if (isAsync) {
|
||||
invocation = `await Mod.async_call(() => ${invocation})`;
|
||||
returnType = `Promise<${returnType}>`;
|
||||
}
|
||||
|
||||
let out = `${name}: ${isAsync ? 'async' : ''} function(${params.filter(p => p != null).join(', ')}): ${returnType} {
|
||||
${prefix}`;
|
||||
if (infix === '' && suffix === '' && rv === 'ret') {
|
||||
out += `return ${invocation};`;
|
||||
} else {
|
||||
out += `
|
||||
let ret = ${invocation};
|
||||
${infix}return ${rv};${suffix}
|
||||
`.trim();
|
||||
}
|
||||
out += '}';
|
||||
return out;
|
||||
}
|
||||
|
||||
function wrapEnum(name, values) {
|
||||
let enumEntries = Object.entries(values);
|
||||
return `export enum ${name} {
|
||||
${enumEntries.map(([k, v], i) => k + (v === (enumEntries[i - 1]?.[1] ?? -1) + 1 ? '' : ` = ${v}`) + ',').join('\n')}
|
||||
};`;
|
||||
}
|
||||
|
||||
function getValidOutArrayIndexes(size) {
|
||||
return Array.from({ length: Math.floor(BYTES_TO_ALLOCATE_FOR_OUT_PARAMS / size) }, (_, i) => i).join(' | ');
|
||||
}
|
||||
|
||||
let out = `
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY ${path.basename(__filename)}
|
||||
// DO NOT EDIT IT BY HAND
|
||||
|
||||
interface Pointer<T extends string> extends Number {
|
||||
readonly __typeName: T;
|
||||
}
|
||||
interface Subpointer<T extends string, S extends string> extends Pointer<S> {
|
||||
readonly __typeName2: T;
|
||||
}
|
||||
|
||||
${Object.entries(primitiveTypes)
|
||||
.filter(e => e[0] !== 'void')
|
||||
.map(e => `type ${e[0]} = ${e[1]};`)
|
||||
.join('\n')}
|
||||
|
||||
${Object.keys(types)
|
||||
.filter(k => k.startsWith('Z3'))
|
||||
.map(makePointerType)
|
||||
.join('\n')}
|
||||
|
||||
${Object.entries(enums)
|
||||
.map(e => wrapEnum(e[0], e[1]))
|
||||
.join('\n\n')}
|
||||
|
||||
export async function init(initModule: any) {
|
||||
let Mod = await initModule();
|
||||
|
||||
// this works for both signed and unsigned, because JS will wrap for you when constructing the Uint32Array
|
||||
function intArrayToByteArr(ints: number[]) {
|
||||
return new Uint8Array((new Uint32Array(ints)).buffer);
|
||||
}
|
||||
|
||||
function boolArrayToByteArr(bools: boolean[]) {
|
||||
return bools.map(b => b ? 1 : 0);
|
||||
}
|
||||
|
||||
function readUintArray(address: number, count: number) {
|
||||
return Array.from(new Uint32Array(Mod.HEAPU32.buffer, address, count));
|
||||
}
|
||||
|
||||
let outAddress = Mod._malloc(${BYTES_TO_ALLOCATE_FOR_OUT_PARAMS});
|
||||
let outUintArray = (new Uint32Array(Mod.HEAPU32.buffer, outAddress, 4));
|
||||
let getOutUint = (i: ${getValidOutArrayIndexes(4)}) => outUintArray[i];
|
||||
let outIntArray = (new Int32Array(Mod.HEAPU32.buffer, outAddress, 4));
|
||||
let getOutInt = (i: ${getValidOutArrayIndexes(4)}) => outIntArray[i];
|
||||
let outUint64Array = (new BigUint64Array(Mod.HEAPU32.buffer, outAddress, 2));
|
||||
let getOutUint64 = (i: ${getValidOutArrayIndexes(8)}) => outUint64Array[i];
|
||||
let outInt64Array = (new BigInt64Array(Mod.HEAPU32.buffer, outAddress, 2));
|
||||
let getOutInt64 = (i: ${getValidOutArrayIndexes(8)}) => outInt64Array[i];
|
||||
|
||||
return {
|
||||
em: Mod,
|
||||
Z3: {
|
||||
mk_context: function(c: Z3_config): Z3_context {
|
||||
let ctx = Mod._Z3_mk_context(c);
|
||||
Mod._set_noop_error_handler(ctx);
|
||||
return ctx;
|
||||
},
|
||||
mk_context_rc: function(c: Z3_config): Z3_context {
|
||||
let ctx = Mod._Z3_mk_context_rc(c);
|
||||
Mod._set_noop_error_handler(ctx);
|
||||
return ctx;
|
||||
},
|
||||
${functions
|
||||
.map(wrapFunction)
|
||||
.filter(f => f != null)
|
||||
.join(',\n')}
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
||||
console.log(prettier.format(out, { singleQuote: true, parser: 'typescript' }));
|
468
src/api/js/scripts/make-ts-wrapper.ts
Normal file
468
src/api/js/scripts/make-ts-wrapper.ts
Normal file
|
@ -0,0 +1,468 @@
|
|||
import assert from 'assert';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import prettier from 'prettier';
|
||||
import { asyncFuncs } from './async-fns';
|
||||
import { enums, Func, FuncParam, functions, primitiveTypes, types } from './parse-api';
|
||||
|
||||
assert(process.argv.length === 4, `Usage: ${process.argv[0]} ${process.argv[1]} wrapperFilePath typesFilePath`);
|
||||
|
||||
const wrapperFilePath = process.argv[2];
|
||||
const typesFilePath = process.argv[3];
|
||||
|
||||
function makeTsWrapper() {
|
||||
const subtypes = {
|
||||
__proto__: null,
|
||||
Z3_sort: 'Z3_ast',
|
||||
Z3_func_decl: 'Z3_ast',
|
||||
} as unknown as Record<string, string>;
|
||||
|
||||
const makePointerType = (t: string) =>
|
||||
`export type ${t} = ` + (t in subtypes ? `Subpointer<'${t}', '${subtypes[t]}'>;` : `Pointer<'${t}'>;`);
|
||||
|
||||
// this supports a up to 6 out integers/pointers
|
||||
// or up to 3 out int64s
|
||||
const BYTES_TO_ALLOCATE_FOR_OUT_PARAMS = 24;
|
||||
|
||||
const CUSTOM_IMPLEMENTATIONS = ['Z3_mk_context', 'Z3_mk_context_rc'];
|
||||
|
||||
function toEmType(type: string) {
|
||||
if (type in primitiveTypes) {
|
||||
type = primitiveTypes[type];
|
||||
}
|
||||
if (['boolean', 'number', 'string', 'bigint', 'void'].includes(type)) {
|
||||
return type;
|
||||
}
|
||||
if (type.startsWith('Z3_')) {
|
||||
return 'number';
|
||||
}
|
||||
throw new Error(`unknown parameter type ${type}`);
|
||||
}
|
||||
|
||||
function isZ3PointerType(type: string) {
|
||||
return type.startsWith('Z3_');
|
||||
}
|
||||
|
||||
function toEm(p: string | FuncParam) {
|
||||
if (typeof p === 'string') {
|
||||
// we've already set this, e.g. by replacing it with an expression
|
||||
return p;
|
||||
}
|
||||
let { type } = p;
|
||||
if (p.kind === 'out') {
|
||||
throw new Error(`unknown out parameter type ${JSON.stringify(p)}`);
|
||||
}
|
||||
if (p.isArray) {
|
||||
if (isZ3PointerType(type) || type === 'unsigned' || type === 'int') {
|
||||
// this works for nullables also because null coerces to 0
|
||||
return `intArrayToByteArr(${p.name} as unknown as number[])`;
|
||||
} else if (type === 'boolean') {
|
||||
return `boolArrayToByteArr(${p.name})`;
|
||||
} else {
|
||||
throw new Error(`only know how to deal with arrays of int/bool (got ${type})`);
|
||||
}
|
||||
}
|
||||
if (type in primitiveTypes) {
|
||||
type = primitiveTypes[type];
|
||||
}
|
||||
|
||||
if (['boolean', 'number', 'bigint', 'string'].includes(type)) {
|
||||
return p.name;
|
||||
}
|
||||
if (type.startsWith('Z3_')) {
|
||||
return p.name;
|
||||
}
|
||||
throw new Error(`unknown parameter type ${JSON.stringify(p)}`);
|
||||
}
|
||||
|
||||
const isInParam = (p: FuncParam) => p.kind !== undefined && ['in', 'in_array'].includes(p.kind);
|
||||
function wrapFunction(fn: Func) {
|
||||
if (CUSTOM_IMPLEMENTATIONS.includes(fn.name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let inParams = fn.params.filter(isInParam);
|
||||
let outParams = fn.params.map((p, idx) => ({ ...p, idx })).filter(p => !isInParam(p));
|
||||
|
||||
// we'll figure out how to deal with these cases later
|
||||
let unknownInParam = inParams.find(
|
||||
p =>
|
||||
p.isPtr ||
|
||||
p.type === 'Z3_char_ptr' ||
|
||||
(p.isArray && !(isZ3PointerType(p.type) || p.type === 'unsigned' || p.type === 'int' || p.type === 'boolean')),
|
||||
);
|
||||
if (unknownInParam) {
|
||||
console.error(`skipping ${fn.name} - unknown in parameter ${JSON.stringify(unknownInParam)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fn.ret === 'Z3_char_ptr') {
|
||||
console.error(`skipping ${fn.name} - returns a string or char pointer`);
|
||||
return null;
|
||||
}
|
||||
// console.error(fn.name);
|
||||
|
||||
let isAsync = asyncFuncs.includes(fn.name);
|
||||
let trivial =
|
||||
!['string', 'boolean'].includes(fn.ret) &&
|
||||
!fn.nullableRet &&
|
||||
outParams.length === 0 &&
|
||||
!inParams.some(p => p.type === 'string' || p.isArray || p.nullable);
|
||||
|
||||
let name = fn.name.startsWith('Z3_') ? fn.name.substring(3) : fn.name;
|
||||
|
||||
const params: (string | null)[] = inParams.map(p => {
|
||||
let type = p.type;
|
||||
if (p.isArray && p.nullable) {
|
||||
type = `(${type} | null)[]`;
|
||||
} else if (p.isArray) {
|
||||
type = `${type}[]`;
|
||||
} else if (p.nullable) {
|
||||
type = `${type} | null`;
|
||||
}
|
||||
return `${p.name}: ${type}`;
|
||||
});
|
||||
|
||||
if (trivial && isAsync) {
|
||||
// i.e. and async
|
||||
return `${name}: function (${params.join(', ')}): Promise<${fn.ret}> {
|
||||
return Mod.async_call(Mod._async_${fn.name}, ${fn.params.map(toEm).join(', ')});
|
||||
}`;
|
||||
}
|
||||
|
||||
if (trivial) {
|
||||
return `${name}: Mod._${fn.name} as ((${params.join(', ')}) => ${fn.ret})`;
|
||||
}
|
||||
|
||||
// otherwise fall back to ccall
|
||||
|
||||
const ctypes = fn.params.map(p =>
|
||||
p.kind === 'in_array' ? 'array' : p.kind === 'out_array' ? 'number' : p.isPtr ? 'number' : toEmType(p.type),
|
||||
);
|
||||
|
||||
let prefix = '';
|
||||
let infix = '';
|
||||
let rv = 'ret';
|
||||
let suffix = '';
|
||||
|
||||
const args: (string | FuncParam)[] = fn.params;
|
||||
|
||||
let arrayLengthParams = new Map();
|
||||
for (let p of inParams) {
|
||||
if (p.nullable && !p.isArray) {
|
||||
// this would be easy to implement - just map null to 0 - but nothing actually uses nullable non-array input parameters, so we can't ensure we've done it right
|
||||
console.error(`skipping ${fn.name} - nullable input parameter`);
|
||||
return null;
|
||||
}
|
||||
if (!p.isArray) {
|
||||
continue;
|
||||
}
|
||||
let { sizeIndex } = p;
|
||||
assert(sizeIndex !== undefined);
|
||||
if (arrayLengthParams.has(sizeIndex)) {
|
||||
let otherParam = arrayLengthParams.get(sizeIndex);
|
||||
prefix += `
|
||||
if (${otherParam}.length !== ${p.name}.length) {
|
||||
throw new TypeError(\`${otherParam} and ${p.name} must be the same length (got \${${otherParam}.length} and \{${p.name}.length})\`);
|
||||
}
|
||||
`.trim();
|
||||
continue;
|
||||
}
|
||||
arrayLengthParams.set(sizeIndex, p.name);
|
||||
|
||||
const sizeParam = fn.params[sizeIndex];
|
||||
if (!(sizeParam.kind === 'in' && sizeParam.type === 'unsigned' && !sizeParam.isPtr && !sizeParam.isArray)) {
|
||||
throw new Error(
|
||||
`size index is not unsigned int (for fn ${fn.name} parameter ${sizeIndex} got ${sizeParam.type})`,
|
||||
);
|
||||
}
|
||||
args[sizeIndex] = `${p.name}.length`;
|
||||
params[sizeIndex] = null;
|
||||
}
|
||||
|
||||
let returnType = fn.ret;
|
||||
let cReturnType = toEmType(fn.ret);
|
||||
if (outParams.length > 0) {
|
||||
let mapped = [];
|
||||
let memIdx = 0; // offset from `outAddress` where the data should get written, in units of 4 bytes
|
||||
|
||||
for (let outParam of outParams) {
|
||||
if (outParam.isArray) {
|
||||
if (isZ3PointerType(outParam.type) || outParam.type === 'unsigned') {
|
||||
let { sizeIndex } = outParam;
|
||||
assert(sizeIndex !== undefined);
|
||||
|
||||
let count;
|
||||
if (arrayLengthParams.has(sizeIndex)) {
|
||||
// i.e. this is also the length of an input array
|
||||
count = args[sizeIndex];
|
||||
} else {
|
||||
let sizeParam = fn.params[sizeIndex];
|
||||
if (
|
||||
!(sizeParam.kind === 'in' && sizeParam.type === 'unsigned' && !sizeParam.isPtr && !sizeParam.isArray)
|
||||
) {
|
||||
throw new Error(
|
||||
`size index is not unsigned int (for fn ${fn.name} parameter ${sizeIndex} got ${sizeParam.type})`,
|
||||
);
|
||||
}
|
||||
count = sizeParam.name;
|
||||
}
|
||||
let outArrayAddress = `outArray_${outParam.name}`;
|
||||
prefix += `
|
||||
let ${outArrayAddress} = Mod._malloc(4 * ${count});
|
||||
try {
|
||||
`.trim();
|
||||
suffix =
|
||||
`
|
||||
} finally {
|
||||
Mod._free(${outArrayAddress});
|
||||
}
|
||||
`.trim() + suffix;
|
||||
args[outParam.idx] = outArrayAddress;
|
||||
mapped.push({
|
||||
name: outParam.name,
|
||||
read:
|
||||
`readUintArray(${outArrayAddress}, ${count})` +
|
||||
(outParam.type === 'unsigned' ? '' : `as unknown as ${outParam.type}[]`),
|
||||
type: `${outParam.type}[]`,
|
||||
});
|
||||
} else {
|
||||
console.error(`skipping ${fn.name} - out array of ${outParam.type}`);
|
||||
return null;
|
||||
}
|
||||
} else if (outParam.isPtr) {
|
||||
function setArg() {
|
||||
args[outParam.idx] = memIdx === 0 ? 'outAddress' : `outAddress + ${memIdx * 4}`;
|
||||
}
|
||||
let read, type;
|
||||
if (outParam.type === 'string') {
|
||||
read = `Mod.UTF8ToString(getOutUint(${memIdx}))`;
|
||||
setArg();
|
||||
++memIdx;
|
||||
} else if (isZ3PointerType(outParam.type)) {
|
||||
read = `getOutUint(${memIdx}) as unknown as ${outParam.type}`;
|
||||
setArg();
|
||||
++memIdx;
|
||||
} else if (outParam.type === 'unsigned') {
|
||||
read = `getOutUint(${memIdx})`;
|
||||
setArg();
|
||||
++memIdx;
|
||||
} else if (outParam.type === 'int') {
|
||||
read = `getOutInt(${memIdx})`;
|
||||
setArg();
|
||||
++memIdx;
|
||||
} else if (outParam.type === 'uint64_t') {
|
||||
if (memIdx % 2 === 1) {
|
||||
++memIdx;
|
||||
}
|
||||
read = `getOutUint64(${memIdx / 2})`;
|
||||
setArg();
|
||||
memIdx += 2;
|
||||
} else if (outParam.type === 'int64_t') {
|
||||
if (memIdx % 2 === 1) {
|
||||
++memIdx;
|
||||
}
|
||||
read = `getOutInt64(${memIdx / 2})`;
|
||||
setArg();
|
||||
memIdx += 2;
|
||||
} else {
|
||||
console.error(`skipping ${fn.name} - unknown out parameter type ${outParam.type}`);
|
||||
return null;
|
||||
}
|
||||
if (memIdx > Math.floor(BYTES_TO_ALLOCATE_FOR_OUT_PARAMS / 4)) {
|
||||
// prettier-ignore
|
||||
console.error(`skipping ${fn.name} - out parameter sizes sum to ${memIdx * 4}, which is > ${BYTES_TO_ALLOCATE_FOR_OUT_PARAMS}`);
|
||||
return null;
|
||||
}
|
||||
mapped.push({
|
||||
name: outParam.name,
|
||||
read,
|
||||
type: outParam.type,
|
||||
});
|
||||
} else {
|
||||
console.error(`skipping ${fn.name} - out param is neither pointer nor array`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let ignoreReturn = fn.ret === 'boolean' || fn.ret === 'void';
|
||||
if (outParams.length === 1) {
|
||||
let outParam = mapped[0];
|
||||
if (ignoreReturn) {
|
||||
returnType = outParam.type;
|
||||
rv = outParam.read;
|
||||
} else {
|
||||
returnType = `{ rv: ${fn.ret}, ${outParam.name} : ${outParam.type} }`;
|
||||
rv = `{ rv: ret, ${outParam.name} : ${outParam.read} }`;
|
||||
}
|
||||
} else {
|
||||
if (ignoreReturn) {
|
||||
returnType = `{ ${mapped.map(p => `${p.name} : ${p.type}`).join(', ')} }`;
|
||||
rv = `{ ${mapped.map(p => `${p.name}: ${p.read}`).join(', ')} }`;
|
||||
} else {
|
||||
returnType = `{ rv: ${fn.ret}, ${mapped.map(p => `${p.name} : ${p.type}`).join(', ')} }`;
|
||||
rv = `{ rv: ret, ${mapped.map(p => `${p.name}: ${p.read}`).join(', ')} }`;
|
||||
}
|
||||
}
|
||||
|
||||
if (fn.ret === 'boolean') {
|
||||
// assume the boolean indicates success
|
||||
infix += `
|
||||
if (!ret) {
|
||||
return null;
|
||||
}
|
||||
`.trim();
|
||||
cReturnType = 'boolean';
|
||||
returnType += ' | null';
|
||||
} else if (fn.ret === 'void') {
|
||||
cReturnType = 'void';
|
||||
} else if (isZ3PointerType(fn.ret) || fn.ret === 'unsigned') {
|
||||
cReturnType = 'number';
|
||||
} else {
|
||||
console.error(`skipping ${fn.name} - out parameter for function which returns non-boolean`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (fn.nullableRet) {
|
||||
returnType += ' | null';
|
||||
infix += `
|
||||
if (ret === 0) {
|
||||
return null;
|
||||
}
|
||||
`.trim();
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
let invocation = `Mod.ccall('${isAsync ? 'async_' : ''}${fn.name}', '${cReturnType}', ${JSON.stringify(ctypes)}, [${args.map(toEm).join(', ')}])`;
|
||||
|
||||
if (isAsync) {
|
||||
invocation = `await Mod.async_call(() => ${invocation})`;
|
||||
returnType = `Promise<${returnType}>`;
|
||||
}
|
||||
|
||||
let out = `${name}: ${isAsync ? 'async' : ''} function(${params.filter(p => p != null).join(', ')}): ${returnType} {
|
||||
${prefix}`;
|
||||
if (infix === '' && suffix === '' && rv === 'ret') {
|
||||
out += `return ${invocation};`;
|
||||
} else {
|
||||
out += `
|
||||
let ret = ${invocation};
|
||||
${infix}return ${rv};${suffix}
|
||||
`.trim();
|
||||
}
|
||||
out += '}';
|
||||
return out;
|
||||
}
|
||||
|
||||
function wrapEnum(name: string, values: Record<string, number>) {
|
||||
let enumEntries = Object.entries(values);
|
||||
return `export enum ${name} {
|
||||
${enumEntries.map(([k, v], i) => k + (v === (enumEntries[i - 1]?.[1] ?? -1) + 1 ? '' : ` = ${v}`) + ',').join('\n')}
|
||||
};`;
|
||||
}
|
||||
|
||||
function getValidOutArrayIndexes(size: number) {
|
||||
return Array.from({ length: Math.floor(BYTES_TO_ALLOCATE_FOR_OUT_PARAMS / size) }, (_, i) => i).join(' | ');
|
||||
}
|
||||
|
||||
const typesDocument = `// THIS FILE IS AUTOMATICALLY GENERATED BY ${path.basename(__filename)}
|
||||
// DO NOT EDIT IT BY HAND
|
||||
|
||||
interface Pointer<T extends string> extends Number {
|
||||
readonly __typeName: T;
|
||||
}
|
||||
interface Subpointer<T extends string, S extends string> extends Pointer<S> {
|
||||
readonly __typeName2: T;
|
||||
}
|
||||
|
||||
${Object.keys(types)
|
||||
.filter(k => k.startsWith('Z3'))
|
||||
.map(makePointerType)
|
||||
.join('\n')}
|
||||
|
||||
${Object.entries(enums)
|
||||
.map(e => wrapEnum(e[0], e[1]))
|
||||
.join('\n\n')}
|
||||
`;
|
||||
|
||||
const relativePath: string = path.relative(path.dirname(wrapperFilePath), path.dirname(typesFilePath)) || './';
|
||||
const ext: string = path.extname(typesFilePath);
|
||||
const basename: string = path.basename(typesFilePath);
|
||||
const importPath = relativePath + basename.slice(0, -ext.length);
|
||||
|
||||
const wrapperDocument = `// THIS FILE IS AUTOMATICALLY GENERATED BY ${path.basename(__filename)}
|
||||
// DO NOT EDIT IT BY HAND
|
||||
|
||||
import {
|
||||
${Object.keys(types)
|
||||
.filter(k => k.startsWith('Z3'))
|
||||
.join(',\n')},
|
||||
${Object.keys(enums).join(',\n')},
|
||||
} from '${importPath}';
|
||||
|
||||
${Object.entries(primitiveTypes)
|
||||
.filter(e => e[0] !== 'void')
|
||||
.map(e => `type ${e[0]} = ${e[1]};`)
|
||||
.join('\n')}
|
||||
|
||||
export async function init(initModule: any) {
|
||||
let Mod = await initModule();
|
||||
|
||||
// this works for both signed and unsigned, because JS will wrap for you when constructing the Uint32Array
|
||||
function intArrayToByteArr(ints: number[]) {
|
||||
return new Uint8Array((new Uint32Array(ints)).buffer);
|
||||
}
|
||||
|
||||
function boolArrayToByteArr(bools: boolean[]) {
|
||||
return bools.map(b => b ? 1 : 0);
|
||||
}
|
||||
|
||||
function readUintArray(address: number, count: number) {
|
||||
return Array.from(new Uint32Array(Mod.HEAPU32.buffer, address, count));
|
||||
}
|
||||
|
||||
let outAddress = Mod._malloc(${BYTES_TO_ALLOCATE_FOR_OUT_PARAMS});
|
||||
let outUintArray = (new Uint32Array(Mod.HEAPU32.buffer, outAddress, 4));
|
||||
let getOutUint = (i: ${getValidOutArrayIndexes(4)}) => outUintArray[i];
|
||||
let outIntArray = (new Int32Array(Mod.HEAPU32.buffer, outAddress, 4));
|
||||
let getOutInt = (i: ${getValidOutArrayIndexes(4)}) => outIntArray[i];
|
||||
let outUint64Array = (new BigUint64Array(Mod.HEAPU32.buffer, outAddress, 2));
|
||||
let getOutUint64 = (i: ${getValidOutArrayIndexes(8)}) => outUint64Array[i];
|
||||
let outInt64Array = (new BigInt64Array(Mod.HEAPU32.buffer, outAddress, 2));
|
||||
let getOutInt64 = (i: ${getValidOutArrayIndexes(8)}) => outInt64Array[i];
|
||||
|
||||
return {
|
||||
em: Mod,
|
||||
Z3: {
|
||||
mk_context: function(c: Z3_config): Z3_context {
|
||||
let ctx = Mod._Z3_mk_context(c);
|
||||
Mod._set_noop_error_handler(ctx);
|
||||
return ctx;
|
||||
},
|
||||
mk_context_rc: function(c: Z3_config): Z3_context {
|
||||
let ctx = Mod._Z3_mk_context_rc(c);
|
||||
Mod._set_noop_error_handler(ctx);
|
||||
return ctx;
|
||||
},
|
||||
${functions
|
||||
.map(wrapFunction)
|
||||
.filter(f => f != null)
|
||||
.join(',\n')}
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
||||
return {
|
||||
wrapperDocument: prettier.format(wrapperDocument, { singleQuote: true, parser: 'typescript' }),
|
||||
typesDocument: prettier.format(typesDocument, { singleQuote: true, parser: 'typescript' }),
|
||||
};
|
||||
}
|
||||
|
||||
const { wrapperDocument, typesDocument } = makeTsWrapper();
|
||||
fs.mkdirSync(path.dirname(wrapperFilePath), { recursive: true });
|
||||
fs.writeFileSync(wrapperFilePath, wrapperDocument);
|
||||
fs.mkdirSync(path.dirname(typesFilePath), { recursive: true });
|
||||
fs.writeFileSync(typesFilePath, typesDocument);
|
|
@ -1,9 +1,8 @@
|
|||
'use strict';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
let fs = require('fs');
|
||||
let path = require('path');
|
||||
|
||||
let files = [
|
||||
const files = [
|
||||
'z3_api.h',
|
||||
'z3_algebraic.h',
|
||||
'z3_ast_containers.h',
|
||||
|
@ -15,15 +14,15 @@ let files = [
|
|||
'z3_spacer.h',
|
||||
];
|
||||
|
||||
let aliases = {
|
||||
const aliases = {
|
||||
__proto__: null,
|
||||
Z3_bool: 'boolean',
|
||||
Z3_string: 'string',
|
||||
bool: 'boolean',
|
||||
signed: 'int',
|
||||
};
|
||||
} as unknown as Record<string, string>;
|
||||
|
||||
let primitiveTypes = {
|
||||
const primitiveTypes = {
|
||||
__proto__: null,
|
||||
Z3_char_ptr: 'string',
|
||||
unsigned: 'number',
|
||||
|
@ -32,35 +31,49 @@ let primitiveTypes = {
|
|||
int64_t: 'bigint',
|
||||
double: 'number',
|
||||
float: 'number',
|
||||
};
|
||||
} as unknown as Record<string, string>;
|
||||
|
||||
let optTypes = {
|
||||
const optTypes = {
|
||||
__proto__: null,
|
||||
|
||||
Z3_sort_opt: 'Z3_sort',
|
||||
Z3_ast_opt: 'Z3_ast',
|
||||
Z3_func_interp_opt: 'Z3_func_interp',
|
||||
};
|
||||
} as unknown as Record<string, string>;
|
||||
|
||||
// parse type declarations
|
||||
let types = {
|
||||
__proto__: null,
|
||||
const types = {
|
||||
__proto__: null,
|
||||
|
||||
// these are function types I can't be bothered to parse
|
||||
Z3_error_handler: 'Z3_error_handler',
|
||||
Z3_push_eh: 'Z3_push_eh',
|
||||
Z3_pop_eh: 'Z3_pop_eh',
|
||||
Z3_fresh_eh: 'Z3_fresh_eh',
|
||||
Z3_fixed_eh: 'Z3_fixed_eh',
|
||||
Z3_eq_eh: 'Z3_eq_eh',
|
||||
Z3_final_eh: 'Z3_final_eh',
|
||||
Z3_created_eh: 'Z3_created_eh',
|
||||
Z3_decide_eh: 'Z3_decide_eh'
|
||||
// these are function types I can't be bothered to parse
|
||||
Z3_error_handler: 'Z3_error_handler',
|
||||
Z3_push_eh: 'Z3_push_eh',
|
||||
Z3_pop_eh: 'Z3_pop_eh',
|
||||
Z3_fresh_eh: 'Z3_fresh_eh',
|
||||
Z3_fixed_eh: 'Z3_fixed_eh',
|
||||
Z3_eq_eh: 'Z3_eq_eh',
|
||||
Z3_final_eh: 'Z3_final_eh',
|
||||
Z3_created_eh: 'Z3_created_eh',
|
||||
Z3_decide_eh: 'Z3_decide_eh',
|
||||
} as unknown as Record<string, string>;
|
||||
|
||||
export type ApiParam = { kind: string; sizeIndex?: number; type: string };
|
||||
export type Api = { params: ApiParam[]; ret: string; extra: boolean };
|
||||
const defApis: Record<string, Api> = Object.create(null);
|
||||
export type FuncParam = {
|
||||
type: string;
|
||||
cType: string;
|
||||
name: string;
|
||||
isConst: boolean;
|
||||
isPtr: boolean;
|
||||
isArray: boolean;
|
||||
nullable: boolean;
|
||||
kind?: string;
|
||||
sizeIndex?: number;
|
||||
};
|
||||
|
||||
let defApis = Object.create(null);
|
||||
let functions = [];
|
||||
let enums = Object.create(null);
|
||||
export type Func = { ret: string; cRet: string; name: string; params: FuncParam[]; nullableRet: boolean };
|
||||
const functions: Func[] = [];
|
||||
let enums: Record<string, Record<string, number>> = Object.create(null);
|
||||
for (let file of files) {
|
||||
let contents = fs.readFileSync(path.join(__dirname, '..', '..', file), 'utf8');
|
||||
|
||||
|
@ -80,6 +93,7 @@ for (let file of files) {
|
|||
/def_Type\(\s*'(?<name>[A-Za-z0-9_]+)',\s*'(?<cname>[A-Za-z0-9_]+)',\s*'(?<pname>[A-Za-z0-9_]+)'\)/g,
|
||||
);
|
||||
for (let { groups } of typeMatches) {
|
||||
assert(groups !== undefined);
|
||||
pytypes[groups.name] = groups.cname;
|
||||
}
|
||||
|
||||
|
@ -93,11 +107,12 @@ for (let file of files) {
|
|||
let apiLines = contents.split('\n').filter(l => /def_API|extra_API/.test(l));
|
||||
for (let line of apiLines) {
|
||||
let match = line.match(
|
||||
/^\s*(?<def>def_API|extra_API) *\(\s*'(?<name>[A-Za-z0-9_]+)'\s*,\s*(?<ret>[A-Za-z0-9_]+)\s*,\s*\((?<params>((_in|_out|_in_array|_out_array|_fnptr|_inout_array)\([^)]+\)\s*,?\s*)*)\)\s*\)\s*$/,
|
||||
/^\s*(?<def>def_API|extra_API) *\(\s*'(?<name>[A-Za-z0-9_]+)'\s*,\s*(?<ret>[A-Za-z0-9_]+)\s*,\s*\((?<params>((_in|_out|_in_array|_out_array|_fnptr|_inout_array)\([^)]+\)\s*,?\s*)*)\)\s*\)\s*$/,
|
||||
);
|
||||
if (match == null) {
|
||||
if (match === null) {
|
||||
throw new Error(`failed to match def_API call ${JSON.stringify(line)}`);
|
||||
}
|
||||
assert(match.groups !== undefined);
|
||||
let { name, ret, def } = match.groups;
|
||||
let params = match.groups.params.trim();
|
||||
let text = params;
|
||||
|
@ -108,6 +123,7 @@ for (let file of files) {
|
|||
if (match == null) {
|
||||
break;
|
||||
}
|
||||
assert(match.groups !== undefined);
|
||||
let kind = match.groups.kind;
|
||||
if (kind === 'inout_array') kind = 'in_array'; // https://github.com/Z3Prover/z3/discussions/5761
|
||||
if (kind === 'in' || kind === 'out' || kind == 'fnptr') {
|
||||
|
@ -135,10 +151,10 @@ for (let file of files) {
|
|||
}
|
||||
|
||||
for (let match of contents.matchAll(/DEFINE_TYPE\((?<type>[A-Za-z0-9_]+)\)/g)) {
|
||||
assert(match.groups !== undefined);
|
||||
types[match.groups.type] = match.groups.type;
|
||||
}
|
||||
|
||||
|
||||
// parse enum declarations
|
||||
for (let idx = 0; idx < contents.length; ) {
|
||||
let nextIdx = contents.indexOf('typedef enum', idx);
|
||||
|
@ -156,12 +172,13 @@ for (let file of files) {
|
|||
if (match === null) {
|
||||
throw new Error(`could not parse enum ${JSON.stringify(slice)}`);
|
||||
}
|
||||
let vals = Object.create(null);
|
||||
let vals: Record<string, number> = Object.create(null);
|
||||
let next = 0;
|
||||
while (true) {
|
||||
let blank = true;
|
||||
while (blank) {
|
||||
({ match, text } = eat(text, /^\s*(\/\/[^\n]*\n)?/));
|
||||
assert(match !== null);
|
||||
blank = match[0].length > 0;
|
||||
}
|
||||
({ match, text } = eat(text, /^[A-Za-z0-9_]+/));
|
||||
|
@ -173,6 +190,7 @@ for (let file of files) {
|
|||
|
||||
({ match, text } = eat(text, /^= *(?<val>[^\n,\s]+)/));
|
||||
if (match !== null) {
|
||||
assert(match.groups !== undefined);
|
||||
let parsedVal = Number(match.groups.val);
|
||||
if (Object.is(parsedVal, NaN)) {
|
||||
throw new Error('unknown value ' + match.groups.val);
|
||||
|
@ -222,12 +240,14 @@ for (let file of files) {
|
|||
if (match == null) {
|
||||
throw new Error(`failed to match c definition: ${JSON.stringify(slice)}`);
|
||||
}
|
||||
assert(match.groups !== undefined);
|
||||
|
||||
let { ret, name, params } = match.groups;
|
||||
let parsedParams = [];
|
||||
|
||||
if (params.trim() !== 'void') {
|
||||
for (let param of params.split(',')) {
|
||||
let paramType, paramName, isConst, isPtr, isArray;
|
||||
let paramType: string, paramName: string, isConst: boolean, isPtr: boolean, isArray: boolean;
|
||||
|
||||
let { match, text } = eat(param, /^\s*/);
|
||||
({ match, text } = eat(text, /^[A-Za-z0-9_]+/));
|
||||
|
@ -303,7 +323,7 @@ for (let file of files) {
|
|||
}
|
||||
}
|
||||
|
||||
function isKnownType(t) {
|
||||
function isKnownType(t: string) {
|
||||
return t in enums || t in types || t in primitiveTypes || ['string', 'boolean', 'void'].includes(t);
|
||||
}
|
||||
|
||||
|
@ -340,19 +360,19 @@ for (let fn of functions) {
|
|||
}
|
||||
}
|
||||
|
||||
function eat(str, regex) {
|
||||
const match = str.match(regex);
|
||||
if (match == null) {
|
||||
function eat(str: string, regex: string | RegExp) {
|
||||
const match: RegExpMatchArray | null = str.match(regex);
|
||||
if (match === null) {
|
||||
return { match, text: str };
|
||||
}
|
||||
return { match, text: str.substring(match[0].length) };
|
||||
}
|
||||
|
||||
function eatWs(text) {
|
||||
function eatWs(text: string) {
|
||||
return eat(text, /^\s*/).text;
|
||||
}
|
||||
|
||||
function expect(str, regex) {
|
||||
function expect(str: string, regex: string | RegExp) {
|
||||
let { text, match } = eat(str, regex);
|
||||
if (match === null) {
|
||||
throw new Error(`expected ${regex}, got ${JSON.stringify(text)}`);
|
||||
|
@ -360,4 +380,4 @@ function expect(str, regex) {
|
|||
return { text, match };
|
||||
}
|
||||
|
||||
module.exports = { primitiveTypes, types, enums, functions };
|
||||
export { primitiveTypes, types, enums, functions };
|
Loading…
Add table
Add a link
Reference in a new issue