Minimize an SSA file that triggers a bug in the noir-ssa pipeline, producing the smallest possible reproduction case. Use after bisecting to identify which SSA passes cause the issue.
Minimize an SSA file that triggers a bug in the noir-ssa pipeline, producing the smallest possible reproduction case. Use after bisecting to identify which SSA passes cause the issue.
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-ssa CLI as a debug binary:
cargo build -p noir_ssa_cli
Always use debug builds (target/debug/noir-ssa). Many SSA invariant checks are #[cfg(debug_assertions)]-guarded and compiled out of release builds.
Set SKILL_DIR to this skill's directory (for script references):
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.
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 chains
Mem2Reg — eliminates store/load pairs
Dead 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 → bC are all unconditional jumps with no instructions/params, redirect bA → bC and remove bB. 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 jmpif to 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 gN definitions not referenced in any function body
Simplify constants: replace large numeric constants (u128 671967...) with small values (u128 1)
Remove unused functions: if f0 just calls f1, try return in f0; or inline f1 into f0
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 (jmpif conditions, loop bounds) are less likely to be removable
Remove return values: replace return vN with return, update the function signature and callers
Simplify function attributes: try removing predicate_pure and 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_ids panicking with "Unmapped value".
Missing stores: a pass removes a store, leaving a load that 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-pass uses 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.ssa then re-read — isolates whether corruption is in DFG state or logical SSA structure.