reduce-ssa-repro
Reduce SSA Reproduction
Minimize an SSA file that triggers a bug in the noir-ssa pipeline. Use after the bisect-ssa-pass skill has identified which passes cause the issue.
Setup
-
Build the
noir-ssaCLI as a debug binary:cargo build -p noir_ssa_cliAlways use debug builds (
target/debug/noir-ssa). Many SSA invariant checks are#[cfg(debug_assertions)]-guarded and compiled out of release builds. -
Set
SKILL_DIRto this skill's directory (for script references):SKILL_DIR=<path-to-repo>/.claude/skills/reduce-ssa-repro -
Gather from bisection:
- The SSA file (
input.ssa) - The passes that trigger the bug (corruption passes + detection pass)
- The error pattern from stderr
- The SSA file (
1. Minimize the Pass Pipeline
Reduce the number of passes needed to trigger the bug. For each non-detection pass in the pipeline, try removing it:
# If the pipeline is: A → B → C → Detection
# Try removing each non-detection pass:
noir-ssa transform --source-path input.ssa --ssa-pass "B" --ssa-pass "C" --ssa-pass "Detection" -o /dev/null
noir-ssa transform --source-path input.ssa --ssa-pass "A" --ssa-pass "C" --ssa-pass "Detection" -o /dev/null
noir-ssa transform --source-path input.ssa --ssa-pass "A" --ssa-pass "B" --ssa-pass "Detection" -o /dev/null
When a pass produces valid output (not the buggy pass), bake it into the input to shrink both input and pipeline:
noir-ssa transform --source-path input.ssa --ssa-pass "A" -o after_A.ssa
# Verify crash reproduces without A:
noir-ssa transform --source-path after_A.ssa --ssa-pass "B" --ssa-pass "Detection" -o /dev/null
# If it still crashes:
cp after_A.ssa input.ssa
Repeat until no more passes can be removed.
2. Set Up the Reproduction Script
Use $SKILL_DIR/scripts/reproduce_crash.sh. It has two modes depending on whether SSA_PASSES is set:
Multi-pass mode — when corruption passes precede the detection pass:
SSA_PASSES="<pass>" DETECTION_PASS="<detection-pass>" $SKILL_DIR/scripts/reproduce_crash.sh input.ssa
SSA_PASSES="<pass-1>:<pass-2>" DETECTION_PASS="<detection-pass>" $SKILL_DIR/scripts/reproduce_crash.sh input.ssa
Validates: (1) input parses, (2) detection pass alone succeeds, (3) full pipeline crashes.
Single-pass mode — when a single pass crashes directly on the input (all intermediate passes were removed in step 1):
DETECTION_PASS="<crashing-pass>" $SKILL_DIR/scripts/reproduce_crash.sh input.ssa
Validates: (1) input parses, (2) the pass crashes.
Choosing a detection pass
The detection pass is appended after the buggy passes. noir-ssa transform calls normalize_ids when formatting output after every pass, so any pass works for detecting corruption caught by normalize_ids (e.g., "Unmapped value" panics). Simplifying or Inlining Brillig Calls are lightweight choices.
The script defaults to target/debug/noir-ssa relative to the repo root. Override with NOIR_SSA=/path/to/noir-ssa.
Run reproduce_crash.sh after every change to input.ssa to verify the crash still reproduces.
3. Automated Reduction
Run reducer scripts from the same directory as input.ssa. Both --passes and --error-pattern are required. Pass all passes including the detection pass.
Phase 1: Remove unused instructions
python3 $SKILL_DIR/scripts/reduce_instructions.py --passes "<pass>" "<detection-pass>" --error-pattern "<error text>"
Iterates over instructions, removes those whose results aren't referenced elsewhere, keeps only removals where the crash still reproduces.
Phase 2: Collapse control flow
python3 $SKILL_DIR/scripts/reduce_branches.py --passes "<pass>" "<detection-pass>" --error-pattern "<error text>"
Converts jmpif to unconditional jmp (tries both targets). Re-runs Phase 1 after each successful collapse.
Phase 3: Apply cleanup passes
Apply simplification passes one at a time. After each, verify the same error pattern still triggers:
noir-ssa transform --source-path input.ssa --ssa-pass "Simplifying" -o candidate.ssa
# Then verify:
SSA_PASSES="<pass>" DETECTION_PASS="<detection-pass>" $SKILL_DIR/scripts/reproduce_crash.sh candidate.ssa
# If same error, replace:
cp candidate.ssa input.ssa
Try these passes in order:
Simplifying— collapses trivial block chainsMem2Reg— eliminates store/load pairsDead Instruction Elimination— removes dead instructions
If a cleanup pass triggers a different error: save that SSA separately as a potential second bug, but do not use it as input.ssa.
Phase 4: Repeat
Re-run phases 1–3 until no more reductions are possible.
Phase 5: Manual simplifications
Draw a CFG diagram first. Write an ASCII diagram of the control flow graph to a file (e.g., cfg_diagram.txt). This makes the meaningful structure visible — nested diamonds, loop nesting, jmp chains vs. actual branch points — and guides which simplifications preserve the structure that triggers the bug vs. which just remove padding. Update the diagram after significant reductions.
Try each change one at a time, running reproduce_crash.sh after each:
- Collapse jmp chains: if
bA → bB → bCare all unconditional jumps with no instructions/params, redirectbA → bCand removebB. This is the single most effective manual reduction for CFG-heavy inputs - Remove nested structure levels: in nested diamonds or loops, try rewiring an outer level to point directly at an inner level's targets, removing the intermediate blocks entirely
- Make jmpif converge: try pointing both branches of a
jmpifto the same target (e.g.,jmpif v0 then: bX, else: bX). This tests whether the branch matters or just the block count - Remove unused globals: delete
gNdefinitions not referenced in any function body - Simplify constants: replace large numeric constants (
u128 671967...) with small values (u128 1) - Remove unused functions: if
f0just callsf1, tryreturninf0; or inlinef1intof0 - Reduce loop bounds: e.g.,
lt vN, u32 3→lt vN, u32 1. Test one change at a time — reductions that work individually may not compose - Remove function arguments: remove the parameter from the signature, replace uses with a constant, remove the argument from all call sites. Arguments feeding control flow (
jmpifconditions, loop bounds) are less likely to be removable - Remove return values: replace
return vNwithreturn, update the function signature and callers - Simplify function attributes: try removing
predicate_pureand other attributes
4. Completion Criteria
Reduction is done when:
- No automated reducer makes further progress
- No cleanup pass shrinks the input
- Manual simplifications have been attempted
- The minimized SSA is small enough to read and understand the bug pattern
5. Common Bug Patterns
After reduction, the minimized SSA reveals the structural trigger:
- Unreachable value references: a pass leaves instructions in unreachable blocks whose values are still referenced in reachable blocks. Detected by
normalize_value_idspanicking with "Unmapped value". - Missing stores: a pass removes a
store, leaving aloadthat reads uninitialized memory. Detected by "loaded before it was first stored". - Changed semantics: a pass changes program output. Detected by comparing interpreter results before and after.
Reference
noir-ssa check: parses SSA, normalizes IDs, removes unreachable blocks, prints canonical form to stdout (not-o). Use to clean up after manual edits:noir-ssa check --source-path input.ssa > normalized.ssa.--ssa-passuses substring matching (contains()), always matching the first pass with that name. Passes appearing multiple times may have different implementations at different pipeline positions.- Serialize to heal state:
noir-ssa transform -o file.ssathen re-read — isolates whether corruption is in DFG state or logical SSA structure.
More from noir-lang/noir
noir-idioms
Guidelines for writing idiomatic, efficient Noir programs. Use when writing or reviewing Noir code.
43extract-fuzzer-repro
Extract a Noir reproduction project from fuzzer failure logs in GitHub Actions. Use when a CI fuzzer test fails and you need to create a local reproduction.
37noir-optimize-acir
Workflow for measuring and optimizing the ACIR circuit size of a constrained Noir program. Use when asked to optimize a Noir program's gate count or circuit size.
36bisect-ssa-pass
Workflow for debugging SSA pass semantic preservation using the noir-ssa CLI. Use when a program's behavior changes incorrectly during the SSA pipeline - bisects passes to identify which one breaks semantics. The `pass_vs_prev` fuzzer finds such issues automatically.
32debug-fuzzer-failure
End-to-end workflow for debugging SSA fuzzer failures from CI. Extracts a reproduction case from GitHub Actions logs, then bisects SSA passes to identify the bug. Use when a `pass_vs_prev` or similar fuzzer test fails in CI.
30noir-frontend-tests
Guide for writing noirc_frontend unit tests. Use when adding, writing, or reviewing frontend tests — regression tests, reproduction tests, error-checking tests, or should_panic tests in the compiler frontend.
1