plonkish-cat
A PLONKish circuit system built on comp-cat-rs's categorical infrastructure.
Circuits are morphisms in a free category generated by a graph of primitive gates. Composition is sequential wiring; the tensor product (from the symmetric monoidal structure) is parallel placement. The compile function interprets composed circuits into polynomial constraint sets via the universal property of free categories.
The categorical model
Arithmetic circuits form a symmetric monoidal category:
| Categorical concept | Circuit meaning |
|---|---|
| Object | Wire count (number of field-element wires) |
| Morphism | Gadget: consumes input wires, produces output wires, generates constraints |
| Identity | Pass-through (no constraints) |
| Composition | Sequential wiring: output of one gadget feeds into the next |
| Tensor product | Parallel placement: side-by-side wire bundles |
| Braiding | Wire permutation |
The free category on a directed graph of primitive gates gives the circuit DSL. Vertices are wire counts (0, 1, 2, ...); edges are gates (Add, Mul, Const, Bool, Dup). A composed circuit is a path in this graph. The compile function is the universal property in action: a graph morphism (per-gate constraint generation) extending uniquely to a functor on paths.
This design is justified by comp-cat-rs's thesis that every constructive computation in category theory is a Kan extension. The free category is the left Kan extension of the graph inclusion along the forgetful functor from categories to graphs.
The PLONKish graph
The standard graph ships with three vertices and four edges:
Vertices (wire counts):
0 -> 0 wires (unit object, empty circuit)
1 -> 1 wire (single field element)
2 -> 2 wires (pair of field elements)
Edges (primitive gates):
0: Add 2 -> 1 out = in0 + in1
1: Mul 2 -> 1 out = in0 * in1
2: Bool 1 -> 1 in * (1 - in) = 0; out = in
3: Dup 1 -> 2 copy constraint: out0 = out1 = in
Constant gates are added dynamically via with_const(c), since they carry a field element:
4: Const(c) 0 -> 1 out = c
Compilation pipeline
Graph of primitive gates
|
v
Free category (paths = composed circuits)
|
| compile()
v
ConstraintSet<F> (polynomial + copy constraints)
|
| is_satisfied(assignment)
v
bool (all constraints hold?)
The compile function walks the path edge by edge, threading a WireAllocator that assigns fresh wire indices to each gate boundary. Each gate generates constraints referencing its allocated input and output wires. The result is a ConstraintSet<F> containing:
- Polynomial constraints: expressions over wires that must evaluate to zero.
- Copy constraints: pairs of wires that must carry equal values.
Quick start
use *;
use ;
Example: verifying x^2
Build a squaring circuit (dup ; mul : 1 -> 1), compile it, and check satisfaction over F101 (integers mod 101):
use *;
use compile_with_io;
use ;
Example: constant multiplication
Build a circuit that multiplies an input by 7: const(7) ; dup_input ; mul.
In graph terms, this requires composing gates with different arities. Since Const(7): 0 -> 1 produces a wire and Mul: 2 -> 1 consumes two, we need to prepare both inputs. One approach: build the constant and the input path separately, then connect them.
For the current version, the simplest encoding uses the standard gates directly:
use *;
use ;
Key types
| Type | Module | Purpose |
|---|---|---|
Field |
field |
Trait for finite field arithmetic (uses std::ops supertraits) |
F101 |
field |
Test field: integers mod 101 |
Wire |
wire |
Newtype for wire indices |
WireCount |
wire |
Newtype for wire bundle sizes (category objects) |
WireRange |
wire |
Contiguous block of allocated wires |
WireAllocator |
wire |
Functional state for fresh wire allocation |
Expression<F> |
expr |
Symbolic polynomial AST with operator overloads |
Constraint<F> |
constraint |
Polynomial expression that must equal zero |
CopyConstraint |
constraint |
Pair of wires that must be equal |
ConstraintSet<F> |
constraint |
Collection of constraints; supports merge and is_satisfied |
PrimitiveGate<F> |
gate |
Add, Mul, Const, Bool, Dup |
PlonkishGraph<F> |
gate |
Directed graph implementing comp-cat-rs Graph trait |
compile |
interpret |
Path -> ConstraintSet<F> (the compilation pipeline) |
compile_with_io |
interpret |
Same, but also returns input/output WireRange |
Implementing your own field
Field requires Add, Sub, Mul, Neg (all with Output = Self), plus Clone, Eq, and Debug. You provide zero(), one(), and inv():
use ;
;
Then use it: PlonkishGraph::<MyField>::standard().
Connection to comp-cat-rs
plonkish-cat uses the following comp-cat-rs infrastructure:
Graphtrait (collapse::free_category):PlonkishGraphimplements this, making primitive gates into graph edges and wire counts into vertices.Path(collapse::free_category): composed circuits are paths in the free category.Path::singletoncreates single-gate circuits;Path::composechains them.Vertex,Edge(collapse::free_category): newtypes indexing into the graph.FreeCategoryError: wrapped byplonkish_cat::Errorfor?propagation.
The categorical infrastructure in comp-cat-rs (monoidal categories, natural transformations, spans, pullbacks) provides the foundation for future extensions: parallel circuit composition via the tensor product, backend polymorphism via natural transformations, and lookup arguments via spans.
Building
RUSTFLAGS="-D warnings"
License
MIT