Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/noir-lang/noir/llms.txt

Use this file to discover all available pages before exploring further.

ACIR (Abstract Circuit Intermediate Representation) is the intermediate format that the Noir compiler produces. It sits between your Noir source code and the backend-specific constraint system used to generate proofs. Every proving backend that works with Noir consumes ACIR.

Why an intermediate representation?

Noir is designed to be backend-agnostic. Rather than targeting a specific proof system like PLONK or Groth16 directly, Noir compiles to ACIR, and each backend translates ACIR into its own native representation. This separation provides several benefits:
  • Backend portability — the same Noir program runs on any ACIR-compatible backend without rewriting.
  • Independent optimization — the Noir compiler can improve ACIR emission without affecting backend implementations, and backends can optimize their own translation independently.
  • Ecosystem composability — new backends only need to implement the ACVM interface to support the full Noir ecosystem.
The relationship is similar to LLVM IR for compiled languages: source languages target LLVM IR, and backends translate IR to native machine code.

The compiler pipeline

Noir source

    ▼  Lexing + Parsing
   AST (Abstract Syntax Tree)

    ▼  Name resolution + Type checking (Elaboration)
   HIR (High-level Intermediate Representation)

    ▼  Monomorphization
   Monomorphized AST

    ▼  SSA generation + Optimization passes
   SSA (Static Single Assignment)

    ├──▶  ACIR  (constrained circuit opcodes)
    └──▶  Brillig  (unconstrained bytecode)
The Noir compiler (noirc_evaluator) lowers from SSA to two output formats:
  • ACIR — encodes the constraints that must hold for the proof to be valid.
  • Brillig — encodes unconstrained computation that runs during witness generation but is not included in the proof.

ACIR opcodes

A compiled Noir program is a Program containing one or more Circuit values. Each Circuit holds a list of Opcode values that define the constraint system. The core opcode types are:
The fundamental constraint opcode. It asserts that a multivariate polynomial over witnesses evaluates to zero:
∑ q_M_{i,j} · w_i · w_j  +  ∑ q_i · w_i  +  q_c  =  0
This single opcode can express equality, inequality (via negation), and multiplication constraints. For example, asserting that witness z equals x * y is written as z - x*y = 0.
Calls to backend-implemented “gadgets” — specialized constraints for operations that are expensive to express with bare arithmetic.Examples of available black box functions:
FunctionPurpose
RANGEAssert a witness fits within a given bit width
AND, XORBitwise operations
Blake2s, Blake3Hash functions
EcdsaSecp256k1, EcdsaSecp256r1ECDSA signature verification
MultiScalarMulElliptic curve multi-scalar multiplication
Poseidon2PermutationZK-friendly hash permutation
Keccakf1600Keccak-f[1600] permutation
RecursiveAggregationRecursive proof verification
Backends must implement support for the black box functions they advertise.
ACIR supports arrays of witnesses via memory opcodes. MemoryInit declares an array with a fixed length, and MemoryOp performs reads and writes at a given index. This allows Noir’s array types to be represented efficiently in the circuit.
Invokes an unconstrained Brillig function during witness generation. Brillig execution does not add constraints — it computes witness values that are then used by surrounding constrained opcodes. See Brillig below.
Calls another ACIR function (circuit) within the same program. Enables function calls to be represented as separate sub-circuits, which is required for recursive proof schemes.

Brillig

Brillig is the bytecode format for unconstrained execution in Noir. When your program calls an unconstrained function, the Noir compiler emits Brillig bytecode instead of ACIR opcodes. Brillig runs on the Brillig VM during witness generation. It can perform arbitrary computation — loops over runtime-determined bounds, branching, division, sorting — without adding any constraints. The results it produces are then passed back into the constrained ACIR portion of the circuit, where they must be verified. The separation is intentional. Many operations that are expensive in a constraint system are cheap to compute and cheap to verify:
  • Division — computing q = a / b in Brillig and then asserting b * q == a in ACIR is far cheaper than implementing division as constraints.
  • Square root — computing in Brillig and squaring to verify.
  • Byte decomposition — computing the bytes of a field element in Brillig and asserting the reconstruction in ACIR.
From the ACIR representation, Brillig functions are stored separately in the Program struct and referenced by ID from BrilligCall opcodes:
pub struct Program<F> {
    pub functions: Vec<Circuit<F>>,                    // ACIR circuits
    pub unconstrained_functions: Vec<BrilligBytecode<F>>, // Brillig functions
}

The ACVM crate

The acvm crate in the ACVM repository is the reference executor for ACIR. It:
  • Walks ACIR opcodes and solves each one using provided witness values.
  • Invokes the Brillig VM for BrilligCall opcodes.
  • Delegates BlackBoxFuncCall opcodes to a pluggable BlackBoxFunctionSolver implementation.
Proving backends integrate with acvm by implementing the solver interface for the black box functions they support. This is the primary integration point between Noir’s output and a backend’s proving system.

Inspecting ACIR

You can inspect the ACIR your Noir program compiles to using nargo:
# Print ACIR opcodes
nargo info --print-acir

# Show circuit gate count (as reported by the backend)
nargo info
For a deeper look at the gate-level representation after backend translation, use the backend’s own tooling. For Barretenberg:
bb gates -b ./target/<project-name>.json

Build docs developers (and LLMs) love