3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2025-05-07 07:45:46 +00:00

Add WebAssembly/TypeScript bindings (#5762)

* Add TypeScript bindings

* mark Z3_eval_smtlib2_string as async
This commit is contained in:
Kevin Gibbons 2022-01-09 17:16:38 -08:00 committed by GitHub
parent 9ac57fc510
commit 2b934b601d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1722 additions and 33 deletions

View file

@ -0,0 +1,22 @@
'use strict';
// things which you probably want to do off-thread
// from https://github.com/Z3Prover/z3/issues/5746#issuecomment-1006289146
module.exports = [
'Z3_eval_smtlib2_string',
'Z3_simplify',
'Z3_simplify_ex',
'Z3_solver_check',
'Z3_solver_check_assumptions',
'Z3_solver_cube',
'Z3_solver_get_consequences',
'Z3_tactic_apply',
'Z3_tactic_apply_ex',
'Z3_optimize_check',
'Z3_algebraic_roots',
'Z3_algebraic_eval',
'Z3_fixedpoint_query',
'Z3_fixedpoint_query_relations',
'Z3_fixedpoint_query_from_lvl',
'Z3_polynomial_subresultants',
];

View file

@ -0,0 +1,11 @@
'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 = asyncFns.map(f => '_async_' + f);
let fns = functions.filter(f => !asyncFns.includes(f.name));
console.log(JSON.stringify([...extras, ...functions.map(f => '_' + f.name)]));

View file

@ -0,0 +1,82 @@
'use strict';
// generates c wrappers with off-thread versions of specified functions
let path = require('path');
let { functions } = require('./parse-api.js');
let asyncFns = require('./async-fns.js');
let wrappers = [];
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(
`
extern "C" void async_${fn.name}(${fn.params
.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)}
// DO NOT EDIT IT BY HAND
#include <thread>
#include <emscripten.h>
#include "../../z3.h"
template<typename Fn, Fn fn, typename... Args>
void wrapper(Args&&... args) {
std::thread t([...args = std::forward<Args>(args)] {
try {
auto result = fn(args...);
MAIN_THREAD_ASYNC_EM_ASM({
resolve_async($0);
}, result);
} catch (...) {
MAIN_THREAD_ASYNC_EM_ASM({
reject_async('failed with unknown exception');
});
throw;
}
});
t.detach();
}
template<typename Fn, Fn fn, typename... Args>
void wrapper_str(Args&&... args) {
std::thread t([...args = std::forward<Args>(args)] {
try {
auto result = fn(args...);
MAIN_THREAD_ASYNC_EM_ASM({
resolve_async(UTF8ToString($0));
}, result);
} catch (...) {
MAIN_THREAD_ASYNC_EM_ASM({
reject_async('failed with unknown exception');
});
throw;
}
});
t.detach();
}
${wrappers.join('\n\n')}`);

View file

@ -0,0 +1,422 @@
'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;
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) {
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();
}
if (isAsync) {
}
// 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
// @ts-ignore no-implicit-any
import initModule = require('./z3-built.js');
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() {
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: {
${functions
.map(wrapFunction)
.filter(f => f != null)
.join(',\n')}
}
};
}
`;
console.log(prettier.format(out, { singleQuote: true, parser: 'typescript' }));

View file

@ -0,0 +1,354 @@
'use strict';
let fs = require('fs');
let path = require('path');
let files = [
'z3_api.h',
'z3_algebraic.h',
'z3_ast_containers.h',
'z3_fixedpoint.h',
'z3_fpa.h',
'z3_optimization.h',
'z3_polynomial.h',
'z3_rcf.h',
'z3_spacer.h',
];
let aliases = {
__proto__: null,
Z3_bool: 'boolean',
Z3_string: 'string',
bool: 'boolean',
signed: 'int',
};
let primitiveTypes = {
__proto__: null,
Z3_char_ptr: 'string',
unsigned: 'number',
int: 'number',
uint64_t: 'bigint',
int64_t: 'bigint',
double: 'number',
float: 'number',
};
let optTypes = {
__proto__: null,
Z3_sort_opt: 'Z3_sort',
Z3_ast_opt: 'Z3_ast',
Z3_func_interp_opt: 'Z3_func_interp',
};
// parse type declarations
let 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',
};
let defApis = Object.create(null);
let functions = [];
let enums = Object.create(null);
for (let file of files) {
let contents = fs.readFileSync(path.join(__dirname, '..', '..', file), 'utf8');
// we _could_ use an actual C++ parser, which accounted for macros and everything
// but that's super painful
// and the files are regular enough that we can get away without it
// we could also do this by modifying the `update_api.py` script
// which we should probably do eventually
// but this is easier while this remains not upstreamed
// we need to parse the `def_API` stuff so we know which things are out parameters
// unfortunately we also need to parse the actual declarations so we know the parameter names also
let pytypes = Object.create(null);
let typeMatches = contents.matchAll(
/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) {
pytypes[groups.name] = groups.cname;
}
// we filter first to ensure our regex isn't too strict
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|_inout_array)\([^)]+\)\s*,?\s*)*)\)\s*\)\s*$/,
);
if (match == null) {
throw new Error(`failed to match def_API call ${JSON.stringify(line)}`);
}
let { name, ret, def } = match.groups;
let params = match.groups.params.trim();
let text = params;
let parsedParams = [];
while (true) {
text = eatWs(text);
({ text, match } = eat(text, /^_(?<kind>in|out|in_array|out_array|inout_array)\(/));
if (match == null) {
break;
}
let kind = match.groups.kind;
if (kind === 'inout_array') kind = 'in_array'; // https://github.com/Z3Prover/z3/discussions/5761
if (kind === 'in' || kind === 'out') {
({ text, match } = expect(text, /^[A-Za-z0-9_]+/));
parsedParams.push({ kind, type: match[0] });
} else {
({ text, match } = expect(text, /^(\d+),/));
let sizeIndex = Number(match[1]);
text = eatWs(text);
({ text, match } = expect(text, /^[A-Za-z0-9_]+/));
parsedParams.push({ kind, sizeIndex, type: match[0] });
}
({ text, match } = expect(text, /^\)/));
text = eatWs(text);
({ text, match } = eat(text, /^,/));
}
if (text !== '') {
throw new Error(`extra text in parameter list ${JSON.stringify(text)}`);
}
if (name in defApis) {
throw new Error(`multiple defApi calls for ${name}`);
}
defApis[name] = { params: parsedParams, ret, extra: def === 'extra_API' };
}
for (let match of contents.matchAll(/DEFINE_TYPE\((?<type>[A-Za-z0-9_]+)\)/g)) {
types[match.groups.type] = match.groups.type;
}
// parse enum declarations
for (let idx = 0; idx < contents.length; ) {
let nextIdx = contents.indexOf('typedef enum', idx);
if (nextIdx === -1) {
break;
}
let lineStart = contents.lastIndexOf('\n', nextIdx);
let lineEnd = contents.indexOf(';', nextIdx);
if (lineStart === -1 || lineEnd === -1) {
throw new Error(`could not parse enum at index ${nextIdx}`);
}
idx = lineEnd;
let slice = contents.substring(lineStart, lineEnd);
let { match, text } = eat(slice, /^\s*typedef enum\s*\{/);
if (match === null) {
throw new Error(`could not parse enum ${JSON.stringify(slice)}`);
}
let vals = Object.create(null);
let next = 0;
while (true) {
let blank = true;
while (blank) {
({ match, text } = eat(text, /^\s*(\/\/[^\n]*\n)?/));
blank = match[0].length > 0;
}
({ match, text } = eat(text, /^[A-Za-z0-9_]+/));
if (match === null) {
throw new Error(`could not parse enum value in ${slice}`);
}
let name = match[0];
text = eatWs(text);
({ match, text } = eat(text, /^= *(?<val>[^\n,\s]+)/));
if (match !== null) {
let parsedVal = Number(match.groups.val);
if (Object.is(parsedVal, NaN)) {
throw new Error('unknown value ' + match.groups.val);
}
vals[name] = parsedVal;
next = parsedVal;
} else {
vals[name] = next;
}
text = eatWs(text);
({ match, text } = eat(text, /^,?\s*}/));
if (match !== null) {
break;
}
({ match, text } = expect(text, /^,/));
++next;
}
text = eatWs(text);
({ match, text } = expect(text, /^[A-Za-z0-9_]+/));
if (match[0] in enums) {
throw new Error(`duplicate enum definition ${match[0]}`);
}
enums[match[0]] = vals;
text = eatWs(text);
if (text !== '') {
throw new Error('expected end of definition, got ' + text);
}
}
// parse function declarations
for (let idx = 0; idx < contents.length; ) {
let nextIdx = contents.indexOf(' Z3_API ', idx);
if (nextIdx === -1) {
break;
}
let lineStart = contents.lastIndexOf('\n', nextIdx);
let lineEnd = contents.indexOf(';', nextIdx);
if (lineStart === -1 || lineEnd === -1) {
throw new Error(`could not parse definition at index ${nextIdx}`);
}
idx = lineEnd;
let slice = contents.substring(lineStart, lineEnd);
let match = slice.match(/^\s*(?<ret>[A-Za-z0-9_]+) +Z3_API +(?<name>[A-Za-z0-9_]+)\s*\((?<params>[^)]*)\)/);
if (match == null) {
throw new Error(`failed to match c definition: ${JSON.stringify(slice)}`);
}
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 { match, text } = eat(param, /^\s*/);
({ match, text } = eat(text, /^[A-Za-z0-9_]+/));
if (match === null) {
throw new Error(`failed to parse param type in ${JSON.stringify(slice)} for param ${JSON.stringify(param)}`);
}
paramType = match[0];
text = eatWs(text);
({ match, text } = eat(text, /^const(?![A-Za-z0-9_])/));
isConst = match !== null;
({ match, text } = eat(text, /^\s*\*/));
isPtr = match !== null;
text = eatWs(text);
if (text === '') {
paramName = 'UNNAMED';
isArray = false;
} else {
({ match, text } = eat(text, /^[A-Za-z0-9_]+/));
if (match === null) {
throw new Error(
`failed to parse param name in ${JSON.stringify(slice)} for param ${JSON.stringify(param)}`,
);
}
paramName = match[0];
text = eatWs(text);
({ match, text } = eat(text, /^\[\]/));
isArray = match !== null;
text = eatWs(text);
if (text !== '') {
throw new Error(`excess text in param in ${JSON.stringify(slice)} for param ${JSON.stringify(param)}`);
}
}
if (paramType === 'Z3_string_ptr' && !isPtr) {
paramType = 'Z3_string';
isPtr = true;
}
let nullable = false;
if (paramType in optTypes) {
nullable = true;
paramType = optTypes[paramType];
}
let cType = paramType;
paramType = aliases[paramType] ?? paramType;
parsedParams.push({ type: paramType, cType, name: paramName, isConst, isPtr, isArray, nullable });
}
}
let nullableRet = false;
if (ret in optTypes) {
nullableRet = true;
ret = optTypes[ret];
}
let cRet = ret;
ret = aliases[ret] ?? ret;
if (name in defApis) {
functions.push({ ret, cRet, name, params: parsedParams, nullableRet });
}
// only a few things are missing `def_API`; we'll skip those
}
}
function isKnownType(t) {
return t in enums || t in types || t in primitiveTypes || ['string', 'boolean', 'void'].includes(t);
}
for (let fn of functions) {
if (!isKnownType(fn.ret)) {
throw new Error(`unknown type ${fn.ret}`);
}
let defParams = defApis[fn.name].params;
if (fn.params.length !== defParams.length) {
throw new Error(`parameter length mismatch for ${fn.name}`);
}
let idx = 0;
for (let param of fn.params) {
if (!isKnownType(param.type)) {
throw new Error(`unknown type ${param.type}`);
}
param.kind = defParams[idx].kind;
if (param.kind === 'in_array' || param.kind === 'out_array') {
if (defParams[idx].sizeIndex == null) {
throw new Error(`function ${fn.name} parameter ${idx} is marked as ${param.kind} but has no index`);
}
param.sizeIndex = defParams[idx].sizeIndex;
if (!param.isArray && param.isPtr) {
// not clear why some things are written as `int * x` and others `int x[]`
// but we can jsut cast
param.isArray = true;
param.isPtr = false;
}
if (!param.isArray) {
throw new Error(`function ${fn.name} parameter ${idx} is marked as ${param.kind} but not typed as array`);
}
}
++idx;
}
}
function eat(str, regex) {
const match = str.match(regex);
if (match == null) {
return { match, text: str };
}
return { match, text: str.substring(match[0].length) };
}
function eatWs(text) {
return eat(text, /^\s*/).text;
}
function expect(str, regex) {
let { text, match } = eat(str, regex);
if (match === null) {
throw new Error(`expected ${regex}, got ${JSON.stringify(text)}`);
}
return { text, match };
}
module.exports = { primitiveTypes, types, enums, functions };