mirror of
https://github.com/Z3Prover/z3
synced 2025-05-15 11:44:44 +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
383
src/api/js/scripts/parse-api.ts
Normal file
383
src/api/js/scripts/parse-api.ts
Normal file
|
@ -0,0 +1,383 @@
|
|||
import assert from 'assert';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const 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',
|
||||
];
|
||||
|
||||
const aliases = {
|
||||
__proto__: null,
|
||||
Z3_bool: 'boolean',
|
||||
Z3_string: 'string',
|
||||
bool: 'boolean',
|
||||
signed: 'int',
|
||||
} as unknown as Record<string, string>;
|
||||
|
||||
const primitiveTypes = {
|
||||
__proto__: null,
|
||||
Z3_char_ptr: 'string',
|
||||
unsigned: 'number',
|
||||
int: 'number',
|
||||
uint64_t: 'bigint',
|
||||
int64_t: 'bigint',
|
||||
double: 'number',
|
||||
float: 'number',
|
||||
} as unknown as Record<string, string>;
|
||||
|
||||
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
|
||||
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',
|
||||
} 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;
|
||||
};
|
||||
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');
|
||||
|
||||
// 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) {
|
||||
assert(groups !== undefined);
|
||||
pytypes[groups.name] = groups.cname;
|
||||
}
|
||||
|
||||
// we don't have to pre-populate the types map with closure types
|
||||
// use the Z3_DECLARE_CLOSURE to identify closure types
|
||||
// for (let match of contents.matchAll(/Z3_DECLARE_CLOSURE\((?<type>[A-Za-z0-9_]+),/g)) {
|
||||
// types[match.groups.type] = match.groups.type
|
||||
// }
|
||||
|
||||
// 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|_fnptr|_inout_array)\([^)]+\)\s*,?\s*)*)\)\s*\)\s*$/,
|
||||
);
|
||||
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;
|
||||
let parsedParams = [];
|
||||
while (true) {
|
||||
text = eatWs(text);
|
||||
({ text, match } = eat(text, /^_(?<kind>in|out|in_array|out_array|inout_array|fnptr)\(/));
|
||||
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') {
|
||||
({ 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)) {
|
||||
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);
|
||||
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: 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_]+/));
|
||||
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) {
|
||||
assert(match.groups !== undefined);
|
||||
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)}`);
|
||||
}
|
||||
assert(match.groups !== undefined);
|
||||
|
||||
let { ret, name, params } = match.groups;
|
||||
let parsedParams = [];
|
||||
|
||||
if (params.trim() !== 'void') {
|
||||
for (let param of params.split(',')) {
|
||||
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_]+/));
|
||||
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: string) {
|
||||
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: 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: string) {
|
||||
return eat(text, /^\s*/).text;
|
||||
}
|
||||
|
||||
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)}`);
|
||||
}
|
||||
return { text, match };
|
||||
}
|
||||
|
||||
export { primitiveTypes, types, enums, functions };
|
Loading…
Add table
Add a link
Reference in a new issue