diff --git a/lib/provable-generic.ts b/lib/provable-generic.ts index a8749ec6..f15c5fc1 100644 --- a/lib/provable-generic.ts +++ b/lib/provable-generic.ts @@ -44,65 +44,90 @@ function createDerivers(): { type HashInput = GenericHashInput; const HashInput = createHashInput(); + /** + * A function that gives us a hint that the input type is a `Provable` and we shouldn't continue + * recursing into its properties, when computing methods that aren't required by the `Provable` interface. + */ + function isProvable( + typeObj: object + ): typeObj is GenericProvable { + return ( + 'sizeInFields' in typeObj && + 'toFields' in typeObj && + 'fromFields' in typeObj && + 'check' in typeObj && + 'toValue' in typeObj && + 'fromValue' in typeObj && + 'toAuxiliary' in typeObj + ); + } + function provable( typeObj: A, - options?: { isPure?: boolean } + options?: { isPure?: boolean } // TODO: remove this option, it has no effect ): InferredProvable { type T = InferProvable; type V = InferValue; type J = InferJson; - let objectKeys = - typeof typeObj === 'object' && typeObj !== null - ? Object.keys(typeObj) - : []; - if (!primitives.has(typeObj as any) && !complexTypes.has(typeof typeObj)) { + if (!isPrimitive(typeObj) && !complexTypes.has(typeof typeObj)) { throw Error(`provable: unsupported type "${typeObj}"`); } - function sizeInFields(typeObj: any): number { - if (primitives.has(typeObj)) return 0; + function sizeInFields(typeObj: NestedProvable): number { + if (isPrimitive(typeObj)) return 0; + if (!complexTypes.has(typeof typeObj)) throw Error(`provable: unsupported type "${typeObj}"`); + if (Array.isArray(typeObj)) return typeObj.map(sizeInFields).reduce((a, b) => a + b, 0); - if ('sizeInFields' in typeObj) return typeObj.sizeInFields(); + + if (isProvable(typeObj)) return typeObj.sizeInFields(); + return Object.values(typeObj) .map(sizeInFields) .reduce((a, b) => a + b, 0); } - function toFields(typeObj: any, obj: any, isToplevel = false): Field[] { - if (primitives.has(typeObj)) return []; + + function toFields(typeObj: NestedProvable, obj: any): Field[] { + if (isPrimitive(typeObj)) return []; + if (!complexTypes.has(typeof typeObj)) throw Error(`provable: unsupported type "${typeObj}"`); + if (Array.isArray(typeObj)) return typeObj.map((t, i) => toFields(t, obj[i])).flat(); - if ('toFields' in typeObj) return typeObj.toFields(obj); - return (isToplevel ? objectKeys : Object.keys(typeObj)) + + if (isProvable(typeObj)) return typeObj.toFields(obj); + + return Object.keys(typeObj) .map((k) => toFields(typeObj[k], obj[k])) .flat(); } - function toAuxiliary(typeObj: any, obj?: any, isToplevel = false): any[] { + + function toAuxiliary(typeObj: NestedProvable, obj?: any): any[] { if (typeObj === Number) return [obj ?? 0]; if (typeObj === String) return [obj ?? '']; if (typeObj === Boolean) return [obj ?? false]; if (typeObj === BigInt) return [obj ?? 0n]; if (typeObj === undefined || typeObj === null) return []; - if (!complexTypes.has(typeof typeObj)) + + if (isPrimitive(typeObj) || !complexTypes.has(typeof typeObj)) throw Error(`provable: unsupported type "${typeObj}"`); + if (Array.isArray(typeObj)) return typeObj.map((t, i) => toAuxiliary(t, obj?.[i])); - if ('toAuxiliary' in typeObj) return typeObj.toAuxiliary(obj); - return (isToplevel ? objectKeys : Object.keys(typeObj)).map((k) => - toAuxiliary(typeObj[k], obj?.[k]) - ); + + if (isProvable(typeObj)) return typeObj.toAuxiliary(obj); + + return Object.keys(typeObj).map((k) => toAuxiliary(typeObj[k], obj?.[k])); } function fromFields( - typeObj: any, + typeObj: NestedProvable, fields: Field[], - aux: any[] = [], - isToplevel = false + aux: any[] = [] ): any { if ( typeObj === Number || @@ -112,8 +137,10 @@ function createDerivers(): { ) return aux[0]; if (typeObj === undefined || typeObj === null) return typeObj; - if (!complexTypes.has(typeof typeObj)) + + if (isPrimitive(typeObj) || !complexTypes.has(typeof typeObj)) throw Error(`provable: unsupported type "${typeObj}"`); + if (Array.isArray(typeObj)) { let array: any[] = []; let i = 0; @@ -128,8 +155,10 @@ function createDerivers(): { } return array; } - if ('fromFields' in typeObj) return typeObj.fromFields(fields, aux); - let keys = isToplevel ? objectKeys : Object.keys(typeObj); + + if (isProvable(typeObj)) return typeObj.fromFields(fields, aux); + + let keys = Object.keys(typeObj); let values = fromFields( keys.map((k) => typeObj[k]), fields, @@ -138,52 +167,35 @@ function createDerivers(): { return Object.fromEntries(keys.map((k, i) => [k, values[i]])); } - function check(typeObj: any, obj: any, isToplevel = false): void { - if (primitives.has(typeObj)) return; + function check(typeObj: NestedProvable, obj: any): void { + if (isPrimitive(typeObj)) return; + if (!complexTypes.has(typeof typeObj)) throw Error(`provable: unsupported type "${typeObj}"`); + if (Array.isArray(typeObj)) return typeObj.forEach((t, i) => check(t, obj[i])); - if ('check' in typeObj) return typeObj.check(obj); - return (isToplevel ? objectKeys : Object.keys(typeObj)).forEach((k) => - check(typeObj[k], obj[k]) - ); + + if (isProvable(typeObj)) return typeObj.check(obj); + + return Object.keys(typeObj).forEach((k) => check(typeObj[k], obj[k])); } const toValue = createMap('toValue'); const fromValue = createMap('fromValue'); - let { empty, fromJSON, toJSON, toInput } = signable(typeObj); + let { empty, fromJSON, toJSON, toInput } = signable(typeObj, isProvable); type S = InferSignable; - if (options?.isPure === true) { - return { - sizeInFields: () => sizeInFields(typeObj), - toFields: (obj: T) => toFields(typeObj, obj, true), - toAuxiliary: () => [], - fromFields: (fields: Field[]) => - fromFields(typeObj, fields, [], true) as T, - check: (obj: T) => check(typeObj, obj, true), - toValue(x) { - return toValue(typeObj, x); - }, - fromValue(v) { - return fromValue(typeObj, v); - }, - toInput: (obj: T) => toInput(obj as S), - toJSON: (obj: T) => toJSON(obj as S) satisfies J, - fromJSON: (json: J) => fromJSON(json) as T, - empty: () => empty() as T, - } satisfies ProvableExtended as InferredProvable; - } return { - sizeInFields: () => sizeInFields(typeObj), - toFields: (obj: T) => toFields(typeObj, obj, true), - toAuxiliary: (obj?: T) => toAuxiliary(typeObj, obj, true), + sizeInFields: () => sizeInFields(typeObj as NestedProvable), + toFields: (obj: T) => toFields(typeObj as NestedProvable, obj), + toAuxiliary: (obj?: T) => + toAuxiliary(typeObj as NestedProvable, obj), fromFields: (fields: Field[], aux: any[]) => - fromFields(typeObj, fields, aux, true) as T, - check: (obj: T) => check(typeObj, obj, true), + fromFields(typeObj as NestedProvable, fields, aux) as T, + check: (obj: T) => check(typeObj as NestedProvable, obj), toValue(x) { return toValue(typeObj, x); }, @@ -197,7 +209,10 @@ function createDerivers(): { } satisfies ProvableExtended as InferredProvable; } - function signable(typeObj: A): InferredSignable { + function signable( + typeObj: A, + shouldTerminate?: (typeObj: object) => boolean + ): InferredSignable { type T = InferSignable; type J = InferJson; let objectKeys = @@ -243,6 +258,11 @@ function createDerivers(): { if (Array.isArray(typeObj)) return typeObj.map((t, i) => toJSON(t, obj[i])); if ('toJSON' in typeObj) return typeObj.toJSON(obj); + + if (shouldTerminate?.(typeObj) === true) { + throw Error(`Expected \`toJSON()\` method on ${display(typeObj)}`); + } + return Object.fromEntries( (isToplevel ? objectKeys : Object.keys(typeObj)).map((k) => [ k, @@ -261,6 +281,11 @@ function createDerivers(): { if (Array.isArray(typeObj)) return typeObj.map((t, i) => fromJSON(t, json[i])); if ('fromJSON' in typeObj) return typeObj.fromJSON(json); + + if (shouldTerminate?.(typeObj) === true) { + throw Error(`Expected \`fromJSON()\` method on ${display(typeObj)}`); + } + let keys = isToplevel ? objectKeys : Object.keys(typeObj); let values = fromJSON( keys.map((k) => typeObj[k]), @@ -279,6 +304,11 @@ function createDerivers(): { throw Error(`provable: unsupported type "${typeObj}"`); if (Array.isArray(typeObj)) return typeObj.map(empty); if ('empty' in typeObj) return typeObj.empty(); + + if (shouldTerminate?.(typeObj) === true) { + throw Error(`Expected \`empty()\` method on ${display(typeObj)}`); + } + return Object.fromEntries( Object.keys(typeObj).map((k) => [k, empty(typeObj[k])]) ); @@ -292,6 +322,11 @@ function createDerivers(): { } satisfies Signable as InferredSignable; } + function display(typeObj: object) { + if ('name' in typeObj) return typeObj.name; + return 'anonymous type object'; + } + return { provable, signable }; } @@ -309,6 +344,10 @@ function createMap(name: S) { return map; } +function isPrimitive(typeObj: any): typeObj is Primitive { + return primitives.has(typeObj); +} + function createHashInput() { type HashInput = GenericHashInput; return { @@ -400,6 +439,13 @@ type InferPrimitiveJson

= P extends typeof String ? null : JSONValue; +type NestedProvable = + | Primitive + | GenericProvable + | [NestedProvable, ...NestedProvable[]] + | NestedProvable[] + | { [key: string]: NestedProvable }; + type InferProvable = A extends Constructor ? A extends GenericProvable ? U