Skip to content

Commit 0b5ea5f

Browse files
committed
Add a Variant class and to lib/provable
`Variant`s (alternatively known as "sum types", "discriminated unions", or "tagged unions") are a way to define a type that can be one of several different types, but only one at a time. This is useful for representing data that can take on multiple forms, such as for JSON data that can be one of several different types (e.g. a string, number, object, array, etc.).
1 parent ea3f901 commit 0b5ea5f

File tree

1 file changed

+214
-0
lines changed

1 file changed

+214
-0
lines changed

src/lib/provable/types/variant.ts

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// variant.ts
2+
3+
import { InferValue } from '../../../bindings/lib/provable-generic.js';
4+
import { provable } from "./provable-derivers.js";
5+
import type { HashInput, InferJson, InferProvable } from './provable-derivers.js';
6+
import { Field } from '../wrapped.js';
7+
8+
// From https://stackoverflow.com/questions/62158066/typescript-type-where-an-object-consists-of-exactly-a-single-property-of-a-set-o
9+
type Explode<T> = keyof T extends infer K
10+
? K extends unknown
11+
? { [I in keyof T]: I extends K ? T[I] : never }
12+
: never
13+
: never;
14+
type AtMostOne<T> = Explode<Partial<T>>;
15+
type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]
16+
17+
/** This type indicates that exactly one key is present in the object.
18+
*
19+
* @remarks Although useful for polymorphic code or large variants, this will give somewhat poor type errors
20+
* if you make a mistake, due to the way it is implemented. This can be avoided by making your own union type.
21+
*
22+
* @example
23+
* ```typescript
24+
*
25+
* // Define a struct for RGB colors.
26+
* class Rgb extends Struct({
27+
* r: Field,
28+
* g: Field,
29+
* b: Field,
30+
* }) {}
31+
*
32+
* // A color is either an RGB color or a well-known 'named' color, like "pink" or "blue".
33+
* type ColorVariant = ExactlyOne<{
34+
* rgb: Rgb,
35+
* named: CircuitString,
36+
* }>;
37+
*
38+
*
39+
* const reddish: ColorVariant = { rgb: new Rgb({ r: Field(255), g: Field(10), b: Field(0) }) };
40+
* const pink: ColorVariant = { named: CircuitString.fromString("pink") };
41+
*
42+
* const bad1: ColorVariant = { rgb: new Rgb({ r: Field(255), g: Field(10), b: Field(0) }), named: CircuitString.fromString("pink") }; // This will cause a type error because both keys are present.
43+
* const bad2: ColorVariant = { }; // This will also cause a type error, there aren't any keys.
44+
* const bad3: ColorVariant = { blarg: Field(13n) }; // This will also cause a type error, since `blarg` is not a key in `ColorVariant`.
45+
* const bad4: ColorVariant = { rgb: CircuitString.fromString("pink") }; // This will also cause a type error, since `rgb` must be an instance of `Rgb`.
46+
* ```
47+
*
48+
*/
49+
type ExactlyOne<T> = AtMostOne<T> & AtLeastOne<T>
50+
51+
// Helper to get the single key from an ExactlyOne type
52+
function getKey<T>(value: ExactlyOne<T>): keyof T {
53+
const keys = Object.keys(value);
54+
if (keys.length !== 1) {
55+
throw new Error("Invalid variant: expected exactly one key, got: " + keys.length);
56+
}
57+
return keys[0] as keyof T;
58+
}
59+
60+
/**
61+
* Creates a variant type that can hold exactly one of the provided types.
62+
*
63+
* @param variants - An object where each key is a variant name and the value is the type of that variant.
64+
*
65+
* @returns A class that represents the variant type, with methods for serialization, deserialization, and other operations.
66+
*
67+
* @remarks sizeInFields returns the maximum size of the fields required to represent any of the variants _plus one for the index_.
68+
*
69+
* @example
70+
* ```typescript
71+
* // Define a struct for RGB colors.
72+
* class Rgb extends Struct({
73+
* t: Field,
74+
* g: Field,
75+
* b: Field,
76+
* }) {}
77+
*
78+
* // Define a variant type that can be either an RGB color or a named color.
79+
* class Color extends Variant({
80+
* rgb: Rgb,
81+
* named: CircuitString,
82+
* }) {}
83+
*
84+
* // Each variant is represented by a single key-value pair.
85+
* const reddish = { rgb: new Rgb({ r: Field(255), g: Field(10), b: Field(0) }) };
86+
* const pink = { named: CircuitString.fromString("pink") };
87+
*
88+
* // Create a Merkle list of colors.
89+
* class ColorList extends MerkleList.create(Color) {}
90+
*
91+
* const colorList = ColorList.empty();
92+
*
93+
* // Add colors to the list.
94+
* colorList.push(reddish); //
95+
* colorList.push(pink);
96+
* ```
97+
*/
98+
99+
function Variant<
100+
As,
101+
Ts extends InferProvable<As>,
102+
Vs extends InferValue<As>,
103+
Js extends InferJson<As>,
104+
>(
105+
variants: As
106+
) {
107+
108+
class Variant_ {
109+
static keys = Object.keys(variants as object) as (keyof As)[];
110+
static indices = Object.fromEntries(
111+
this.keys.map((k, i) => [k, i])
112+
) as {[K in keyof As]: number};
113+
static provables = this.keys.map(k => provable(variants[k]));
114+
static types = variants
115+
static _isVariant: true = true;
116+
117+
118+
constructor(value: ExactlyOne<Ts>) {
119+
Object.assign(this, value);
120+
}
121+
122+
static sizeInFields() {
123+
return Math.max(...this.provables.map(p => p.sizeInFields())) + 1;
124+
}
125+
126+
static toFields(value: ExactlyOne<Ts>): Field[] {
127+
const key = getKey(value) as keyof As;
128+
const typedValue = value as any;
129+
return [Field(this.indices[key]), ...this.provables[this.indices[key]].toFields(typedValue[key])];
130+
}
131+
132+
static toAuxiliary(value?: ExactlyOne<Ts>): any[] {
133+
if (value === undefined) {
134+
return [];
135+
}
136+
const key = getKey(value) as keyof As;
137+
const typedValue = value as any;
138+
return [this.indices[key], ...this.provables[this.indices[key]].toAuxiliary(typedValue[key])];
139+
}
140+
141+
static toInput(value: ExactlyOne<Ts>): HashInput {
142+
const key = getKey(value) as keyof As;
143+
const typedValue = value as any;
144+
return this.provables[this.indices[key]].toInput(typedValue[key]);
145+
}
146+
147+
static toJSON(value: ExactlyOne<Ts>): { tag: string; value: Js[keyof Js] } {
148+
const key = getKey(value) as string;
149+
const typedValue = value as any;
150+
return { tag: key, value: this.provables[this.indices[key as keyof As]].toJSON(typedValue[key]) as Js[keyof Js] };
151+
}
152+
153+
static fromJSON(json: { tag: keyof As; value: Js[keyof Js] }): ExactlyOne<Ts> {
154+
const key = json.tag;
155+
// FIXME: This is correct as long as `tag` is the index into `Js`, but I'm using `any` to avoid type issues for now.
156+
const value = this.provables[this.indices[key]].fromJSON(json.value as any);
157+
return { [key]: value } as ExactlyOne<Ts>;
158+
}
159+
160+
static empty(): ExactlyOne<Ts> {
161+
const key = this.keys[0];
162+
const value = this.provables[this.indices[key]].empty();
163+
const obj = Object.create(this.prototype);
164+
return Object.assign(obj, { [key]: value });
165+
}
166+
167+
static check(value: ExactlyOne<Ts>) {
168+
const key = getKey(value) as keyof As;
169+
const typedValue = value as any;
170+
this.provables[this.indices[key]].check(typedValue[key]);
171+
}
172+
173+
static toCanonical(value: ExactlyOne<Ts>): ExactlyOne<Ts> {
174+
const key = getKey(value) as keyof As;
175+
const typedValue = value as any;
176+
const canonical = this.provables[this.indices[key]].toCanonical?.(typedValue[key]) ?? typedValue[key];
177+
const obj = Object.create(this.prototype);
178+
return Object.assign(obj, { [key]: canonical });
179+
}
180+
181+
static toValue(value: ExactlyOne<Ts>): ExactlyOne<Vs> {
182+
const key = getKey(value) as keyof As;
183+
const typedValue = value as any;
184+
const val = this.provables[this.indices[key]].toValue(typedValue[key]) as Vs[keyof Vs];
185+
const obj = Object.create(this.prototype);
186+
return Object.assign(obj, { [key]: val });
187+
}
188+
189+
static fromValue(value: ExactlyOne<Vs>): ExactlyOne<Ts> {
190+
const key = getKey(value) as keyof As;
191+
const typedValue = value as any;
192+
const val = this.provables[this.indices[key]].fromValue(typedValue[key]) as Ts[keyof Ts];
193+
const obj = Object.create(this.prototype);
194+
return Object.assign(obj, { [key]: val });
195+
}
196+
197+
static fromFields(fields: Field[], aux: any[]): ExactlyOne<Ts> {
198+
const index = fields[0].toBigInt();
199+
if (index < 0 || index >= this.keys.length) {
200+
throw new Error("Invalid variant index");
201+
}
202+
const key = this.keys[Number(index)];
203+
const value = this.provables[this.indices[key]].fromFields(fields.slice(1), aux.slice(1));
204+
const obj = Object.create(this.prototype);
205+
return Object.assign(obj, { [key]: value });
206+
}
207+
208+
}
209+
return Variant_;
210+
}
211+
212+
export { Variant }
213+
214+
export type { ExactlyOne }

0 commit comments

Comments
 (0)