ProveIR
Pre-compiled circuit template format: types, pipeline phases, and serialization.
Overview
ProveIR is the pre-compiled circuit template format that decouples compile-time circuit definition from runtime circuit instantiation. prove {…} blocks and Circom templates both produce ProveIR; both feed into the same instantiation → IR-SSA → backend pipeline.
Key crates:
ir-forge— ProveIR data types, AST→ProveIR compiler, Lysis liftir— orchestrates ProveIR, hostsCircomLibraryHandletrait, owns the module loaderir-core— leaf SSA types (Instruction<F>,SsaVar) shared byirandir-forge
Why pre-compile templates
- Avoids re-parsing/re-typechecking every prove invocation.
- Lets bytecode embed circuit definitions in the constant pool (
TAG_BYTES). - Allows Circom and
.achto share a single instantiation/optimization stack. - Permits structural deduplication via the Lysis VM (in-progress).
Pipeline phases (A–H)
| Phase | Name | Input | Output |
|---|---|---|---|
| A | Compile | AST prove {} block + outer scope | ProveIR (typed circuit expressions) |
| B | Serialize | ProveIR | Bytes (stored in bytecode constant pool, TAG_BYTES) |
| C | Deserialize | Bytes (at VM runtime) | ProveIR |
| D | Instantiate | ProveIR + captured scope values | SSA IrProgram<F> |
| E | Optimize | IrProgram<F> | Optimized IrProgram<F> |
| F | Backend | IrProgram<F> | R1CS or Plonkish constraints |
| G | Witness | Captured values + constraint trace | Witness vector |
| H | Prove | Witness + constraints | Cryptographic proof |
Phases A–B run at compile time; C–H run at runtime (VM mode) or directly in ach circuit mode (skipping serialize/deserialize).
Container types
ProveIR (file: ir-forge/src/types/prove_ir.rs:17-43):
pub struct ProveIR {
pub name: Option<String>,
pub public_inputs: Vec<ProveInputDecl>,
pub witness_inputs: Vec<ProveInputDecl>,
pub captures: Vec<CaptureDef>,
pub body: Vec<CircuitNode>,
pub capture_arrays: Vec<CaptureArrayDef>,
pub component_bodies: HashMap<String, Vec<CircuitNode>>,
}
pub const PROVE_IR_MAGIC: &[u8; 4] = b"ACHP";
pub const PROVE_IR_FORMAT_VERSION: u8 = 6;
Serialization layout:
[MAGIC: 4 = "ACHP"][VERSION: 1][PRIME_ID: 1][BINCODE_PAYLOAD]
Version history (only v6 is currently accepted):
- v2 — added
capture_arrays - v3 — added
messagetoCircuitNode::AssertEq - v4 — added
PrimeIdbyte (multi-prime) - v5 —
CircuitExpr::Constswitched toFieldConst(32-byte canonical LE) - v6 — added the
component_bodiestable for deferred component instances
CircuitExpr (expression tree)
File: ir-forge/src/types/expressions.rs:14-132.
Leaves:
| Variant | Meaning |
|---|---|
Const(FieldConst) | Field constant (32-byte canonical LE) |
Input(name) | Public or witness input by name |
Capture(name) | Captured outer-scope value |
Var(name) | Local SSA-like variable |
ArrayLen(name) | Array length lookup |
ArrayIndex { array, index } | Element access |
Arithmetic:
| Variant | Notes |
|---|---|
BinOp { op, lhs, rhs } | op ∈ {Add, Sub, Mul, Div} |
UnaryOp { op, operand } | op ∈ {Neg, Not} |
Pow { base, exp: u64 } | Literal exponent only |
Comparison:
| Variant | Notes |
|---|---|
Comparison { op, lhs, rhs } | op ∈ {Eq, Neq, Lt, Le, Gt, Ge} |
Signed-range semantics — comparisons treat operands as signed values in (−p/2, p/2).
Boolean:
| Variant | Notes |
|---|---|
BoolOp { op, lhs, rhs } | op ∈ {And, Or} |
Mux { cond, if_true, if_false } | Branch selection |
Crypto:
| Variant | Notes |
|---|---|
PoseidonHash { left, right } | 2-input Poseidon |
PoseidonMany(Vec<CircuitExpr>) | Variadic Poseidon |
MerkleVerify { root, leaf, path, indices } | Membership proof |
RangeCheck { value, bits } | value ∈ [0, 2^bits) |
Integer ops:
| Variant | Notes |
|---|---|
IntDiv { lhs, rhs, max_bits } | Bounded integer division |
IntMod { lhs, rhs, max_bits } | Bounded integer modulo |
Bitwise:
| Variant | Notes |
|---|---|
BitAnd, BitOr, BitXor, BitNot | Each carries num_bits |
ShiftR, ShiftL | Carry num_bits; accept CircuitExpr shift amounts (not just constants), enabling in >> i in unrolled loops |
CircuitNode (statement-level)
File: ir-forge/src/types/nodes.rs:17-159.
pub enum CircuitNode {
Let { name, value: CircuitExpr, span },
LetArray { name, elements: Vec<CircuitExpr>, span },
AssertEq { lhs, rhs, message: Option<String>, span },
Assert { expr, message, span },
For { var, range: ForRange, body: Vec<CircuitNode>, span },
If { cond, then_body, else_body, span },
Expr { expr, span }, // builtin call w/ side effect
Decompose { name, value, num_bits: u32, span },
WitnessHint { name, hint: CircuitExpr, span },
LetIndexed { array, index, value, span }, // arr[i] = expr
WitnessHintIndexed { array, index, hint, span }, // arr[i] <-- expr
WitnessCall { output_bindings, input_signals, program_bytes, span }, // Artik dispatch
ComponentCall { body_key, comp_name, param_subs, span }, // deferred component instance
}
pub enum ForRange {
Literal { start: u64, end: u64 },
WithCapture { start: u64, end_capture: String }, // bound is a template param
WithExpr { start: u64, end_expr: Box<CircuitExpr> }, // computed bound (e.g., n+1)
Array(String), // iterate by name
}
Decompose is the bit-decomposition primitive: name is bound as a length-num_bits array of bool signals, with witness hints derived from value.
WitnessCall carries an embedded Artik bytecode blob — see Artik Witness VM. ComponentCall points into ProveIR::component_bodies; instantiation expands the shared body with the component name and parameter substitutions instead of serializing another inlined copy.
Captures
A prove {} block captures variables from the enclosing scope; Circom templates capture their parameters. Both use the same CaptureDef machinery:
pub struct CaptureDef {
pub name: String,
pub value: FieldConst, // resolved at instantiation, not compile
}
pub struct CaptureArrayDef {
pub name: String,
pub elements: Vec<FieldConst>,
}
Captures stay symbolic in ProveIR (the Capture(name) leaf). They are bound by the Lysis-backed instantiation entry points such as ProveIR::instantiate_lysis(captures) and ProveIR::instantiate_lysis_with_outputs(captures, output_names).
Compiler entry point
File: ir-forge/src/ast_lower/api.rs.
ProveIrCompiler::compile_prove_block(
block: &Block,
outer_scope: &OuterScope,
resolver_state: &ResolverState,
) -> Result<ProveIR, ProveIrError>
Submodules:
ir-forge/src/ast_lower/exprs/—CircuitExprlowering (literals, ops, calls)ir-forge/src/ast_lower/calls/— function/component dispatch (resolves throughBuiltinRegistry+ module loader)ir-forge/src/ast_lower/stmts/—CircuitNodelowering (let, mut, for, if, assert)ir-forge/src/ast_lower/methods.rs— method desugaring (x.abs()→ builtin-call)ir-forge/src/ast_lower/state.rs— compiler state (scope, witness auto-inference)
Instantiation
ProveIR::instantiate_extended(captures) first walks the ProveIR body into ExtendedInstruction<F>. The production entry points, instantiate_lysis and instantiate_lysis_with_outputs, then lower that extended form through Lysis into a flat IrProgram<F>. Loop handling, capture substitution, array index resolution, deferred component expansion, and Circom output projection all happen in this instantiation layer.
For Circom template libraries, circom/src/library/instantiate.rs handles component composition and parameter passing before producing the ProveIR nodes that use the same instantiation layer.
ExtendedInstruction (Lysis bridge)
File: ir-forge/src/extended.rs:63-112.
pub enum ExtendedInstruction<F: FieldBackend = Bn254Fr> {
Plain(Instruction<F>),
TemplateBody { id: TemplateId, frame_size: u8, n_params: u8, body: Vec<ExtendedInstruction<F>> },
TemplateCall { template_id: TemplateId, captures: Vec<SsaVar>, outputs: Vec<SsaVar> },
LoopUnroll { iter_var: SsaVar, start: i64, end: i64, body: Vec<ExtendedInstruction<F>> },
}
This wrapper layer enables the Lysis VM walker to lift loops and templates without inlining them in IR — a prerequisite for structural deduplication via the interner. Instantiation in this path is Bytecode-Oriented: ProveIR is lifted to a Lysis bytecode program whose execution emits the SSA IR. See Bytecode-Oriented Compilation for the architectural framing.
Source files
| Component | File |
|---|---|
| ProveIR types | ir-forge/src/types/prove_ir.rs |
CircuitExpr | ir-forge/src/types/expressions.rs |
CircuitNode | ir-forge/src/types/nodes.rs |
| Compiler API | ir-forge/src/ast_lower/api.rs |
| Instantiation | ir-forge/src/instantiate/ |
| ExtendedInstruction | ir-forge/src/extended.rs |
| Format constants | ir-forge/src/types/prove_ir.rs |
See Pipeline Overview for the full compile path. See IR & Optimization for what runs after instantiation. See Circom Frontend for how .circom files reach ProveIR.