Skip to content

Commit 7a8ba4b

Browse files
Add support for Algebraic Datatypes in JavaScript/TypeScript bindings (#7734)
* Initial plan * Add datatype type definitions to types.ts (work in progress) Co-authored-by: NikolajBjorner <[email protected]> * Complete datatype type definitions with working TypeScript compilation Co-authored-by: NikolajBjorner <[email protected]> * Implement core datatype functionality with TypeScript compilation success Co-authored-by: NikolajBjorner <[email protected]> * Complete datatype implementation with full Context integration and tests Co-authored-by: NikolajBjorner <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: NikolajBjorner <[email protected]>
1 parent d66fabe commit 7a8ba4b

File tree

3 files changed

+380
-2
lines changed

3 files changed

+380
-2
lines changed

src/api/js/src/high-level/high-level.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,4 +890,74 @@ describe('high-level', () => {
890890
expect(model.eval(z).eqIdentity(Int.val(5))).toBeTruthy();
891891
});
892892
});
893+
894+
describe('datatypes', () => {
895+
it('should create simple enum datatype', async () => {
896+
const { Datatype, Int, Bool, Solver } = api.Context('main');
897+
898+
// Create a simple Color enum datatype
899+
const Color = Datatype('Color');
900+
Color.declare('red');
901+
Color.declare('green');
902+
Color.declare('blue');
903+
904+
const ColorSort = Color.create();
905+
906+
// Test that we can access the constructors
907+
expect(typeof (ColorSort as any).red).not.toBe('undefined');
908+
expect(typeof (ColorSort as any).green).not.toBe('undefined');
909+
expect(typeof (ColorSort as any).blue).not.toBe('undefined');
910+
911+
// Test that we can access the recognizers
912+
expect(typeof (ColorSort as any).is_red).not.toBe('undefined');
913+
expect(typeof (ColorSort as any).is_green).not.toBe('undefined');
914+
expect(typeof (ColorSort as any).is_blue).not.toBe('undefined');
915+
});
916+
917+
it('should create recursive list datatype', async () => {
918+
const { Datatype, Int, Solver } = api.Context('main');
919+
920+
// Create a recursive List datatype like in the Python example
921+
const List = Datatype('List');
922+
List.declare('cons', ['car', Int.sort()], ['cdr', List]);
923+
List.declare('nil');
924+
925+
const ListSort = List.create();
926+
927+
// Test that constructors and accessors exist
928+
expect(typeof (ListSort as any).cons).not.toBe('undefined');
929+
expect(typeof (ListSort as any).nil).not.toBe('undefined');
930+
expect(typeof (ListSort as any).is_cons).not.toBe('undefined');
931+
expect(typeof (ListSort as any).is_nil).not.toBe('undefined');
932+
expect(typeof (ListSort as any).car).not.toBe('undefined');
933+
expect(typeof (ListSort as any).cdr).not.toBe('undefined');
934+
});
935+
936+
it('should create mutually recursive tree datatypes', async () => {
937+
const { Datatype, Int } = api.Context('main');
938+
939+
// Create mutually recursive Tree and TreeList datatypes
940+
const Tree = Datatype('Tree');
941+
const TreeList = Datatype('TreeList');
942+
943+
Tree.declare('leaf', ['value', Int.sort()]);
944+
Tree.declare('node', ['children', TreeList]);
945+
TreeList.declare('nil');
946+
TreeList.declare('cons', ['car', Tree], ['cdr', TreeList]);
947+
948+
const [TreeSort, TreeListSort] = Datatype.createDatatypes(Tree, TreeList);
949+
950+
// Test that both datatypes have their constructors
951+
expect(typeof (TreeSort as any).leaf).not.toBe('undefined');
952+
expect(typeof (TreeSort as any).node).not.toBe('undefined');
953+
expect(typeof (TreeListSort as any).nil).not.toBe('undefined');
954+
expect(typeof (TreeListSort as any).cons).not.toBe('undefined');
955+
956+
// Test accessors exist
957+
expect(typeof (TreeSort as any).value).not.toBe('undefined');
958+
expect(typeof (TreeSort as any).children).not.toBe('undefined');
959+
expect(typeof (TreeListSort as any).car).not.toBe('undefined');
960+
expect(typeof (TreeListSort as any).cdr).not.toBe('undefined');
961+
});
962+
});
893963
});

src/api/js/src/high-level/high-level.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
Z3_ast_print_mode,
1818
Z3_ast_vector,
1919
Z3_context,
20+
Z3_constructor,
21+
Z3_constructor_list,
2022
Z3_decl_kind,
2123
Z3_error_code,
2224
Z3_func_decl,
@@ -88,6 +90,10 @@ import {
8890
FuncEntry,
8991
SMTSetSort,
9092
SMTSet,
93+
Datatype,
94+
DatatypeSort,
95+
DatatypeExpr,
96+
DatatypeCreation,
9197
} from './types';
9298
import { allSatisfy, assert, assertExhaustive } from './utils';
9399

@@ -825,6 +831,17 @@ export function createApi(Z3: Z3Core): Z3HighLevel {
825831
}
826832
}
827833

834+
const Datatype = Object.assign(
835+
(name: string): DatatypeImpl => {
836+
return new DatatypeImpl(ctx, name);
837+
},
838+
{
839+
createDatatypes(...datatypes: DatatypeImpl[]): DatatypeSortImpl[] {
840+
return createDatatypes(...datatypes);
841+
}
842+
}
843+
);
844+
828845
////////////////
829846
// Operations //
830847
////////////////
@@ -2647,6 +2664,185 @@ export function createApi(Z3: Z3Core): Z3HighLevel {
26472664
}
26482665
}
26492666

2667+
////////////////////////////
2668+
// Datatypes
2669+
////////////////////////////
2670+
2671+
class DatatypeImpl implements Datatype<Name> {
2672+
readonly ctx: Context<Name>;
2673+
readonly name: string;
2674+
public constructors: Array<[string, Array<[string, Sort<Name> | Datatype<Name>]>]> = [];
2675+
2676+
constructor(ctx: Context<Name>, name: string) {
2677+
this.ctx = ctx;
2678+
this.name = name;
2679+
}
2680+
2681+
declare(name: string, ...fields: Array<[string, Sort<Name> | Datatype<Name>]>): this {
2682+
this.constructors.push([name, fields]);
2683+
return this;
2684+
}
2685+
2686+
create(): DatatypeSort<Name> {
2687+
const datatypes = createDatatypes(this);
2688+
return datatypes[0];
2689+
}
2690+
}
2691+
2692+
class DatatypeSortImpl extends SortImpl implements DatatypeSort<Name> {
2693+
declare readonly __typename: DatatypeSort['__typename'];
2694+
2695+
numConstructors(): number {
2696+
return Z3.get_datatype_sort_num_constructors(contextPtr, this.ptr);
2697+
}
2698+
2699+
constructorDecl(idx: number): FuncDecl<Name> {
2700+
const ptr = Z3.get_datatype_sort_constructor(contextPtr, this.ptr, idx);
2701+
return new FuncDeclImpl(ptr);
2702+
}
2703+
2704+
recognizer(idx: number): FuncDecl<Name> {
2705+
const ptr = Z3.get_datatype_sort_recognizer(contextPtr, this.ptr, idx);
2706+
return new FuncDeclImpl(ptr);
2707+
}
2708+
2709+
accessor(constructorIdx: number, accessorIdx: number): FuncDecl<Name> {
2710+
const ptr = Z3.get_datatype_sort_constructor_accessor(contextPtr, this.ptr, constructorIdx, accessorIdx);
2711+
return new FuncDeclImpl(ptr);
2712+
}
2713+
2714+
cast(other: CoercibleToExpr<Name>): DatatypeExpr<Name>;
2715+
cast(other: DatatypeExpr<Name>): DatatypeExpr<Name>;
2716+
cast(other: CoercibleToExpr<Name> | DatatypeExpr<Name>): DatatypeExpr<Name> {
2717+
if (isExpr(other)) {
2718+
assert(this.eqIdentity(other.sort), 'Value cannot be converted to this datatype');
2719+
return other as DatatypeExpr<Name>;
2720+
}
2721+
throw new Error('Cannot coerce value to datatype expression');
2722+
}
2723+
2724+
subsort(other: Sort<Name>) {
2725+
_assertContext(other.ctx);
2726+
return this.eqIdentity(other);
2727+
}
2728+
}
2729+
2730+
class DatatypeExprImpl extends ExprImpl<Z3_ast, DatatypeSortImpl> implements DatatypeExpr<Name> {
2731+
declare readonly __typename: DatatypeExpr['__typename'];
2732+
}
2733+
2734+
function createDatatypes(...datatypes: DatatypeImpl[]): DatatypeSortImpl[] {
2735+
if (datatypes.length === 0) {
2736+
throw new Error('At least one datatype must be provided');
2737+
}
2738+
2739+
// All datatypes must be from the same context
2740+
const dtCtx = datatypes[0].ctx;
2741+
for (const dt of datatypes) {
2742+
if (dt.ctx !== dtCtx) {
2743+
throw new Error('All datatypes must be from the same context');
2744+
}
2745+
}
2746+
2747+
const sortNames = datatypes.map(dt => dt.name);
2748+
const constructorLists: Z3_constructor_list[] = [];
2749+
const scopedConstructors: Z3_constructor[] = [];
2750+
2751+
try {
2752+
// Create constructor lists for each datatype
2753+
for (const dt of datatypes) {
2754+
const constructors: Z3_constructor[] = [];
2755+
2756+
for (const [constructorName, fields] of dt.constructors) {
2757+
const fieldNames: string[] = [];
2758+
const fieldSorts: Z3_sort[] = [];
2759+
const fieldRefs: number[] = [];
2760+
2761+
for (const [fieldName, fieldSort] of fields) {
2762+
fieldNames.push(fieldName);
2763+
2764+
if (fieldSort instanceof DatatypeImpl) {
2765+
// Reference to another datatype being defined
2766+
const refIndex = datatypes.indexOf(fieldSort);
2767+
if (refIndex === -1) {
2768+
throw new Error(`Referenced datatype "${fieldSort.name}" not found in datatypes being created`);
2769+
}
2770+
// For recursive references, we pass null and the ref index
2771+
fieldSorts.push(null as any); // null will be handled by the Z3 API
2772+
fieldRefs.push(refIndex);
2773+
} else {
2774+
// Regular sort
2775+
fieldSorts.push((fieldSort as Sort<Name>).ptr);
2776+
fieldRefs.push(0);
2777+
}
2778+
}
2779+
2780+
const constructor = Z3.mk_constructor(
2781+
contextPtr,
2782+
Z3.mk_string_symbol(contextPtr, constructorName),
2783+
Z3.mk_string_symbol(contextPtr, `is_${constructorName}`),
2784+
fieldNames.map(name => Z3.mk_string_symbol(contextPtr, name)),
2785+
fieldSorts,
2786+
fieldRefs
2787+
);
2788+
constructors.push(constructor);
2789+
scopedConstructors.push(constructor);
2790+
}
2791+
2792+
const constructorList = Z3.mk_constructor_list(contextPtr, constructors);
2793+
constructorLists.push(constructorList);
2794+
}
2795+
2796+
// Create the datatypes
2797+
const sortSymbols = sortNames.map(name => Z3.mk_string_symbol(contextPtr, name));
2798+
const resultSorts = Z3.mk_datatypes(contextPtr, sortSymbols, constructorLists);
2799+
2800+
// Create DatatypeSortImpl instances
2801+
const results: DatatypeSortImpl[] = [];
2802+
for (let i = 0; i < resultSorts.length; i++) {
2803+
const sortImpl = new DatatypeSortImpl(resultSorts[i]);
2804+
2805+
// Attach constructor, recognizer, and accessor functions dynamically
2806+
const numConstructors = sortImpl.numConstructors();
2807+
for (let j = 0; j < numConstructors; j++) {
2808+
const constructor = sortImpl.constructorDecl(j);
2809+
const recognizer = sortImpl.recognizer(j);
2810+
const constructorName = constructor.name().toString();
2811+
2812+
// Attach constructor function
2813+
if (constructor.arity() === 0) {
2814+
// Nullary constructor (constant)
2815+
(sortImpl as any)[constructorName] = constructor.call();
2816+
} else {
2817+
(sortImpl as any)[constructorName] = constructor;
2818+
}
2819+
2820+
// Attach recognizer function
2821+
(sortImpl as any)[`is_${constructorName}`] = recognizer;
2822+
2823+
// Attach accessor functions
2824+
for (let k = 0; k < constructor.arity(); k++) {
2825+
const accessor = sortImpl.accessor(j, k);
2826+
const accessorName = accessor.name().toString();
2827+
(sortImpl as any)[accessorName] = accessor;
2828+
}
2829+
}
2830+
2831+
results.push(sortImpl);
2832+
}
2833+
2834+
return results;
2835+
} finally {
2836+
// Clean up resources
2837+
for (const constructor of scopedConstructors) {
2838+
Z3.del_constructor(contextPtr, constructor);
2839+
}
2840+
for (const constructorList of constructorLists) {
2841+
Z3.del_constructor_list(contextPtr, constructorList);
2842+
}
2843+
}
2844+
}
2845+
26502846
class QuantifierImpl<
26512847
QVarSorts extends NonEmptySortArray<Name>,
26522848
QSort extends BoolSort<Name> | SMTArraySort<Name, QVarSorts>,
@@ -3029,6 +3225,7 @@ export function createApi(Z3: Z3Core): Z3HighLevel {
30293225
BitVec,
30303226
Array,
30313227
Set,
3228+
Datatype,
30323229

30333230
////////////////
30343231
// Operations //

0 commit comments

Comments
 (0)