These guidelines help you write Noir programs that are readable, idiomatic, and produce efficient circuits.
Core Principle: Hint and Verify
Computing a value is often more expensive in a circuit than verifying a claimed value is correct. Use
unconstrained
functions to compute results off-circuit, then verify them with cheap constraints.
// Expensive: sorting an array in-circuit requires many comparisons and swaps
let sorted = sort_in_circuit(arr);
// Cheaper: hint the sorted array, verify it's a valid permutation and is ordered
let sorted = unsafe { sort_hint(arr) };
// verify sorted order and that sorted is a permutation of arr
Note that the compiler already injects unconstrained helpers for some operations automatically (e.g., integer division). Don't hint what the compiler already optimizes — focus on higher-level computations like sorting, searching, and array construction where the compiler cannot automatically apply this pattern.
What to Hint
Hint the
final result
, not intermediate values. If your unconstrained function computes helper structures (masks, indices, accumulators) on the way to an answer, return only the answer and verify it directly against the inputs. Fewer hinted values means fewer constraints needed.
Safety Comments
Every
unsafe
block must have a
// Safety:
comment that explains
why
the constrained code makes the hint sound — not just
where
the verification happens, but what property it enforces:
// Safety: each result element is checked against a[i] or b[i] depending on
// whether i <= index, so a dishonest prover cannot substitute arbitrary values
let result = unsafe { my_hint(a, b, index) };
ACIR vs Brillig: Different Optimization Goals
Noir compiles to two different targets depending on context, and they have fundamentally different performance characteristics.
ACIR (constrained code)
— the default. Every operation becomes arithmetic constraints in a circuit. Optimize for
gate/constraint count
fewer constraints = faster proving.
Brillig (unconstrained code)
— functions marked
unconstrained
. Runs on a conventional VM. Optimize for
execution speed and bytecode size
familiar performance intuitions apply.
Key Differences
Aspect
ACIR (constrained)
Brillig (unconstrained)
Loops
Fully unrolled — bounds must be comptime-known
Native loop support — runtime-dynamic bounds are fine
Control flow
Flattened into conditional selects — both branches are always evaluated
Real branching — only the taken branch executes
Function calls
Always inlined
Preserved when beneficial
Comparisons
Inequality (
<
,
<=
) requires range checks (costs gates)
Native comparison instructions (cheap)
Branching with
is_unconstrained()
When a function may be called in either context, use
is_unconstrained()
to provide optimized implementations for each target. This is common in the standard library:
// ACIR path: iterate the full static capacity, guard with a flag
let mut exceeded_len = false;
for i in 0..MaxLen {
exceeded_len |= i == self.len;
if !exceeded_len {
ret |= predicate(self.storage[i]);
}
}
}
ret
}
The constrained path must iterate the full
MaxLen
because ACIR loops are unrolled at compile time — the compiler needs a static bound. The unconstrained path can loop over exactly
self.len
elements because Brillig supports runtime-dynamic loop bounds.
Practical Guidelines
Constrained code
minimize constraints — use hint-and-verify, avoid unnecessary comparisons, leverage type-system range guarantees to simplify constraints.
Unconstrained code
write for clarity and speed like normal imperative code — use dynamic loops, early returns, and mutable state freely.
Don't add constraint-style verification inside
unconstrained
functions — it wastes execution time without adding security (unconstrained results are verified by the constrained caller).
Don't use runtime-dynamic loop bounds in constrained code — the compiler must be able to unroll all loops.
Leveraging the Type System
Noir's type system provides range guarantees that make subsequent constraints cheaper — the compiler knows what values a type can hold and can emit simpler arithmetic as a result. Use typed values instead of manual field arithmetic.
Use
bool
Instead of Field Arithmetic
The
bool
type guarantees values are 0 or 1, so the compiler can use simpler constraints for operations on booleans. Prefer boolean operators over field multiplication for logical conditions:
// Prefer: readable, compiler knows switched[i] is 0 or 1
assert(!switched[i] | switched[i - 1]);
// Avoid: manual field encoding of the same logic
let s = switched[i] as Field;
let prev = switched[i - 1] as Field;
assert(s * (1 - prev) == 0);
Both compile to equivalent constraints, but the boolean version communicates intent.
Conditionals
Use
if/else
Expressions for Conditional Values
The compiler lowers
if cond { a } else { b }
into an optimized conditional select. Don't hand-roll the arithmetic:
// Prefer: clear intent
let val = if condition { x } else { y };
// Avoid: manual select
let c = condition as Field;
let val = c * (x - y) + y;
Hoist Assertions Out of Conditional Branches
When both branches of an
if/else
contain assertions, extract the condition into a value and assert once. The compiler optimizes a single assertion against a conditional value better than separate assertions in each branch:
// Prefer: one assertion, compiler optimizes the conditional select
let expected = if condition { a } else { b };
assert_eq(result, expected);
// Avoid: duplicated assertions in each branch
if condition {
assert_eq(result, a);
} else {
assert_eq(result, b);
}
Assertions
Use
assert_eq
over
assert(x == y)
. It provides better error messages on failure and reads more naturally:
assert_eq(result[i], expected);
Comparison Costs
Integer comparisons (
<
,
<=
,
>
,
>=
) require range checks, which cost gates. Equality checks (
==
) are cheaper. Strategies to reduce comparison costs:
Avoid redundant comparisons
If you need to check
i <= index
in a loop, do it once per iteration — don't check the same condition in multiple places.
Don't over-optimize comparisons
Replacing a simple
<=
with flag-tracking (
if i == index { flag = true }
) adds state and may produce
more
gates. Always measure before committing to a "cleverer" approach.
Summary Checklist
When writing or reviewing Noir code:
Can any in-circuit computation be replaced with hint-and-verify?
Are you hinting only final results, not intermediate scaffolding?
Are boolean conditions using
bool
types and operators, not Field arithmetic?
Are conditional values using
if/else
expressions, not manual selects?
Are assertions using
assert_eq
where applicable?
Are constrained and unconstrained paths optimized for their respective targets?
Do all loops in constrained code have comptime-known bounds?