noir-idioms

Writing Idiomatic Noir

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "noir-idioms" with this command: npx skills add noir-lang/noir/noir-lang-noir-noir-idioms

Writing Idiomatic Noir

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:

pub fn any<Env>(self, predicate: fnEnv -> bool) -> bool { let mut ret = false; if is_unconstrained() { // Brillig path: use the actual length directly for i in 0..self.len { ret |= predicate(self.storage[i]); } } else { // 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?

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

extract-fuzzer-repro

No summary provided by upstream source.

Repository SourceNeeds Review
General

noir-optimize-acir

No summary provided by upstream source.

Repository SourceNeeds Review
General

bisect-ssa-pass

No summary provided by upstream source.

Repository SourceNeeds Review
General

debug-fuzzer-failure

No summary provided by upstream source.

Repository SourceNeeds Review