/** * helpers for testing equivalence of two implementations, one of them on bigints */ import { test, Random } from '../testing/property.js'; import { Provable } from '../provable/provable.js'; import { deepEqual } from 'node:assert/strict'; import { Bool, Field } from '../provable/wrapped.js'; import { AnyTuple, Tuple } from '../util/types.js'; import { provable } from '../provable/types/provable-derivers.js'; import { assert } from '../provable/gadgets/common.js'; import { synchronousRunners } from '../provable/core/provable-context.js'; export { equivalent, equivalentProvable, equivalentAsync, oneOf, throwError, handleErrors, deepEqual as defaultAssertEqual, id, }; export { spec, field, fieldWithRng, bigintField, bool, boolean, unit, array, record, map, onlyIf, fromRandom, first, second, constant, }; export { Spec, ToSpec, FromSpec, SpecFromFunctions, ProvableSpec, First, Second }; // TODO get rid of this top-level await by making `test` support async functions let { runAndCheckSync } = await synchronousRunners(); // a `Spec` tells us how to compare two functions type FromSpec = { // `rng` creates random inputs to the first function rng: Random; // `there` converts to inputs to the second function there: (x: In1) => In2; // `provable` tells us how to create witnesses, to test provable code // note: we only allow the second function to be provable; // the second because it's more natural to have non-provable types as random generator output provable?: Provable; }; type ToSpec = { // `back` converts outputs of the second function back to match the first function back: (x: Out2) => Out1; // `assertEqual` to compare outputs against each other; defaults to `deepEqual` assertEqual?: (x: Out1, y: Out1, message: string) => void; }; type Spec = FromSpec & ToSpec; type ProvableSpec = Spec & { provable: Provable }; type FuncSpec, Out1, In2 extends Tuple, Out2> = { from: { [k in keyof In1]: k extends keyof In2 ? FromSpec : never; }; to: ToSpec; }; type AnyTupleFunction = (...args: AnyTuple) => any; type SpecFromFunctions = FuncSpec< Parameters, ReturnType, Parameters, ReturnType >; function id(x: T) { return x; } // unions of specs, to cleanly model function parameters that are unions of types type FromSpecUnion = { _isUnion: true; specs: Tuple>; rng: Random<[number, T1]>; }; type OrUnion = FromSpec | FromSpecUnion; type Union = T[keyof T & number]; function oneOf>>( ...specs: In ): FromSpecUnion>, Union>> { // the randomly generated value from a union keeps track of which spec it came from let rng = Random.oneOf( ...specs.map((spec, i) => Random.map(spec.rng, (x) => [i, x] as [number, any])) ); return { _isUnion: true, specs, rng }; } function toUnion(spec: OrUnion): FromSpecUnion { let specAny = spec as any; return specAny._isUnion ? specAny : oneOf(specAny); } // equivalence tester function equivalent>, Out extends ToSpec>({ from, to, verbose, }: { from: In; to: Out; verbose?: boolean; }) { return function run( f1: (...args: Params1) => First, f2: (...args: Params2) => Second, label = 'expect equal results' ) { let generators = from.map((spec) => spec.rng); let assertEqual = to.assertEqual ?? deepEqual; let start = performance.now(); let nRuns = test(...(generators as any[]), (...args) => { args.pop(); let inputs = args as Params1; handleErrors( () => f1(...inputs), () => to.back(f2(...(inputs.map((x, i) => from[i].there(x)) as Params2))), (x, y) => assertEqual(x, y, label), label ); }); if (verbose) { let ms = (performance.now() - start).toFixed(1); let runs = nRuns.toString().padStart(2, ' '); console.log(`${label.padEnd(20, ' ')} success on ${runs} runs in ${ms}ms.`); } }; } // async equivalence function equivalentAsync>, Out extends ToSpec>( { from, to }: { from: In; to: Out }, { runs = 1 } = {} ) { return async function run( f1: (...args: Params1) => Promise> | First, f2: (...args: Params2) => Promise> | Second, label = 'expect equal results' ) { let generators = from.map((spec) => spec.rng); let assertEqual = to.assertEqual ?? deepEqual; let nexts = generators.map((g) => g.create()); for (let i = 0; i < runs; i++) { let args = nexts.map((next) => next()); let inputs = args as Params1; try { await handleErrorsAsync( () => f1(...inputs), async () => to.back(await f2(...(inputs.map((x, i) => from[i].there(x)) as Params2))), (x, y) => assertEqual(x, y, label), label ); } catch (err) { console.log(...inputs); throw err; } } }; } // equivalence tester for provable code function isProvable(spec: FromSpecUnion) { return spec.specs.some((spec) => spec.provable); } function equivalentProvable>, Out extends ToSpec>({ from: fromRaw, to, verbose, }: { from: In; to: Out; verbose?: boolean; }) { let fromUnions = fromRaw.map(toUnion); assert(fromUnions.some(isProvable), 'equivalentProvable: no provable input'); return function run( f1: (...args: Params1) => First, f2: (...args: Params2) => Second, label = 'expect equal results' ) { let generators = fromUnions.map((spec) => spec.rng); let assertEqual = to.assertEqual ?? deepEqual; let start = performance.now(); let nRuns = test.custom({ minRuns: 5 })(...generators, (...args) => { args.pop(); // figure out which spec to use for each argument let from = (args as [number, unknown][]).map(([j], i) => fromUnions[i].specs[j]); let inputs = (args as [number, unknown][]).map(([, x]) => x) as Params1; let inputs2 = inputs.map((x, i) => from[i].there(x)) as Params2; // outside provable code handleErrors( () => f1(...inputs), () => f2(...inputs2), (x, y) => assertEqual(x, to.back(y), label), label ); // inside provable code runAndCheckSync(() => { let inputWitnesses = inputs2.map((x, i) => { let provable = from[i].provable; return provable !== undefined ? Provable.witness(provable, () => x) : x; }) as Params2; handleErrors( () => f1(...inputs), () => f2(...inputWitnesses), (x, y) => Provable.asProver(() => assertEqual(x, to.back(y), label)), label ); }); }); if (verbose) { let ms = (performance.now() - start).toFixed(1); let runs = nRuns.toString().padStart(2, ' '); console.log(`${label.padEnd(20, ' ')} success on ${runs} runs in ${ms}ms.`); } }; } // creating specs function spec(spec: { rng: Random; there: (x: T) => S; back: (x: S) => T; assertEqual?: (x: T, y: T, message: string) => void; provable: Provable; }): ProvableSpec; function spec(spec: { rng: Random; there: (x: T) => S; back: (x: S) => T; assertEqual?: (x: T, y: T, message: string) => void; }): Spec; function spec(spec: { rng: Random; provable: Provable; assertEqual?: (x: T, y: T, message: string) => void; }): ProvableSpec; function spec(spec: { rng: Random; assertEqual?: (x: T, y: T, message: string) => void; }): Spec; function spec(spec: { rng: Random; there?: (x: T) => S; back?: (x: S) => T; assertEqual?: (x: T, y: T, message: string) => void; provable?: Provable; }): Spec { return { rng: spec.rng, there: spec.there ?? (id as any), back: spec.back ?? (id as any), assertEqual: spec.assertEqual, provable: spec.provable, }; } // some useful specs let unit: ToSpec = { back: id, assertEqual() {} }; let field: ProvableSpec = { rng: Random.field, there: Field, back: (x) => x.toBigInt(), provable: Field, }; let bigintField: Spec = { rng: Random.field, there: id, back: id, }; let bool: ProvableSpec = { rng: Random.boolean, there: Bool, back: (x) => x.toBoolean(), provable: Bool, }; let boolean: Spec = fromRandom(Random.boolean); function fieldWithRng(rng: Random): ProvableSpec { return { ...field, rng }; } // spec combinators function array(spec: ProvableSpec, n: number): ProvableSpec; function array(spec: Spec, n: Random | number): Spec; function array(spec: Spec, n: Random | number): Spec { return { rng: Random.array(spec.rng, n), there: (x) => x.map(spec.there), back: (x) => x.map(spec.back), provable: typeof n === 'number' && spec.provable ? Provable.Array(spec.provable, n) : undefined, }; } function record }>( specs: Specs ): Spec<{ [k in keyof Specs]: First }, { [k in keyof Specs]: Second }> { let isProvable = Object.values(specs).every((spec) => spec.provable); return { rng: Random.record(mapObject(specs, (spec) => spec.rng)) as any, there: (x) => mapObject(specs, (spec, k) => spec.there(x[k])) as any, back: (x) => mapObject(specs, (spec, k) => spec.back(x[k])) as any, provable: isProvable ? provable(mapObject(specs, (spec) => spec.provable) as any) : undefined, }; } function map( { from, to }: { from: ProvableSpec; to: ProvableSpec }, there: (t: T1) => S1 ): ProvableSpec; function map( { from, to }: { from: FromSpec; to: Spec }, there: (t: T1) => S1 ): Spec; function map( { from, to }: { from: FromSpec; to: Spec }, there: (t: T1) => S1 ): Spec { return { ...to, rng: Random.map(from.rng, there) }; } function onlyIf(spec: Spec, onlyIf: (t: T) => boolean): Spec { return { ...spec, rng: Random.reject(spec.rng, (x) => !onlyIf(x)) }; } function mapObject( t: { [k in K]: T }, map: (t: T, k: K) => S ): { [k in K]: S } { return Object.fromEntries(Object.entries(t).map(([k, v]) => [k, map(v, k as K)])) as any; } function fromRandom(rng: Random): Spec { return { rng, there: id, back: id }; } function first(spec: Spec): Spec { return { rng: spec.rng, there: id, back: id }; } function second(spec: Spec): Spec { return { rng: Random.map(spec.rng, spec.there), there: id, back: id, provable: spec.provable, }; } function constant(spec: Spec, value: T): Spec { return { ...spec, rng: Random.constant(value) }; } // helper to ensure two functions throw equivalent errors function handleErrors( op1: () => T, op2: () => S, useResults?: (a: T, b: S) => R, label?: string ): R | undefined { let result1: T, result2: S; let error1: Error | undefined; let error2: Error | undefined; try { result1 = op1(); } catch (err) { error1 = err as Error; } try { result2 = op2(); } catch (err) { error2 = err as Error; } if (!!error1 !== !!error2) { error1 && console.log(error1); error2 && console.log(error2); } let message = `${(label && `${label}: `) || ''}equivalent errors`; deepEqual(!!error1, !!error2, message); if (!(error1 || error2) && useResults !== undefined) { return useResults(result1!, result2!); } } async function handleErrorsAsync( op1: () => T, op2: () => S, useResults?: (a: Awaited, b: Awaited) => R, label?: string ): Promise { let result1: Awaited, result2: Awaited; let error1: Error | undefined; let error2: Error | undefined; try { result1 = await op1(); } catch (err) { error1 = err as Error; } try { result2 = await op2(); } catch (err) { error2 = err as Error; } if (!!error1 !== !!error2) { error1 && console.log(error1); error2 && console.log(error2); } let message = `${(label && `${label}: `) || ''}equivalent errors`; deepEqual(!!error1, !!error2, message); if (!(error1 || error2) && useResults !== undefined) { return useResults(result1!, result2!); } } function throwError(message?: string): any { throw Error(message); } // infer input types from specs type Param1> = In extends { there: (x: infer In) => any; } ? In : In extends FromSpecUnion ? T1 : never; type Param2> = In extends { there: (x: any) => infer In; } ? In : In extends FromSpecUnion ? T2 : never; type Params1>> = { [k in keyof Ins]: Param1; }; type Params2>> = { [k in keyof Ins]: Param2; }; type First> = Out extends ToSpec ? Out1 : never; type Second> = Out extends ToSpec ? Out2 : never;