Skip to content

feat: annotate printed ACIR with Noir source locations and snippets - #13431

Open
asterite wants to merge 2 commits into
masterfrom
ab/print-acir-locations
Open

feat: annotate printed ACIR with Noir source locations and snippets#13431
asterite wants to merge 2 commits into
masterfrom
ab/print-acir-locations

Conversation

@asterite

Copy link
Copy Markdown
Collaborator

Problem

Everything needed to map a compiled constraint back to the Noir expression that produced it already ships in every artifact — DebugInfo records a full inlined call stack of byte-precise source spans per ACIR opcode, and file_map carries the source text — but none of it is exposed. --print-acir prints bare opcodes, so a reader (human or AI) auditing whether the constraints faithfully represent the source has to infer the mapping from scratch.

This adds a way to read that provenance directly.

What's added

nargo compile --print-acir --with-acir-locations annotates each run of ACIR opcodes with the source it was compiled from:

// src/main.nr:3:19: x * y
ASSERT w3 = w0*w1
BLACKBOX::RANGE input: w3, bits: 32 // attempt to multiply with overflow
// src/main.nr:4:9: assert(sum != 10)
BRILLIG CALL func: 0, predicate: 1, inputs: [w3 - 10], outputs: [w4]
ASSERT 0 = w3*w4 - 10*w4 - 1
// no source location
ASSERT w2 = w3

Inlined code carries a trail back to the user's call site, e.g. // poseidon/mod.nr:161:20: tt * tt (via mod.nr:60:17 <- perm.nr:14:13 <- src/main.nr:4:17).

noir-inspector print-acir <artifact.json> --with-locations produces the same output from a compiled artifact alone — artifacts embed both the debug symbols and the source file map, so no source tree or recompile is needed. This is the audit-a-thing-someone-handed-you case.

Both work for program and contract artifacts.

Behavior details

  • Deduplicated per run: one comment per group of opcodes sharing a call stack, so an expression that expands into several opcodes gets a single annotation.
  • Gaps are explicit: an unattributed opcode following an annotated one is marked // no source location rather than silently inheriting the previous comment. Leading unattributed opcodes (entry-point range checks) stay bare.
  • Snippets are whitespace-collapsed to one line and capped at 80 chars; paths are shown relative to the current directory when possible.
  • Parse-safe: annotations are // comments, which the ACIR parser already skips. A test asserts that annotated output parses back to the identical circuit.

Implementation

The acir crate has no access to Location/DebugInfo, so display_circuit/display_program take annotations as opaque BTreeMap<usize, String> and just print them. Resolution lives in the new noirc_artifacts::annotations module, shared by noirc_driver and noir-inspector so the two outputs can't drift.

No behavior change without the flag: existing --print-acir output is byte-identical (the Display impls pass None).

Testing

  • 5 new tests in noirc_driver/tests/print_acir.rs: run dedupe, inlined caller chain, multi-line snippet collapsing, gap marking, and the round-trip-parse guarantee.
  • 2 new CLI integration tests in tooling/inspector/tests/print_acir_tests.rs (with and without the flag). They use a different test program from info_tests.rs so the two test binaries don't race on artifact files.
  • Manually verified end-to-end on poseidon_bn254_hash_width_3 (deep stdlib inlining) and on a contract artifact with multiple entry points.

Why

The motivating use case is letting an AI validate that a circuit's constraints soundly encode its Noir source. As a check that the output is actually sufficient for that, I ran an audit sweep over 30 constrained test programs using this flag — the annotations were enough to verify signed-arithmetic overflow encodings, byte/bit-decomposition canonicity, predicate gating, and Brillig-hint binding in each case.

asterite and others added 2 commits July 29, 2026 10:03
Add a --with-acir-locations flag that, combined with --print-acir,
prints a `// file:line:col: snippet` comment above each run of ACIR
opcodes compiled from the same source span, using the call stacks
already recorded in DebugInfo. Inlined opcodes carry a compact
`(via caller1 <- caller2)` trail back to the user's call site, and an
unattributed opcode following an annotated run is marked explicitly so
it isn't mistaken for part of that run.

The acir display functions take the annotations as opaque strings since
the acir crate has no access to Location/DebugInfo. Annotations are `//`
comments, which the ACIR parser already skips, so annotated output still
parses back to the same circuit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the ACIR opcode annotation builder from noirc_driver into a new
noirc_artifacts::annotations module so it can work directly on compiled
artifacts, and use it in `noir-inspector print-acir --with-locations`.
Since artifacts embed both the debug symbols and the source file map,
the ACIR of an existing artifact can be annotated with the originating
Noir locations and snippets without the source tree or a recompile.
Works for both program and contract artifacts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@asterite
asterite requested a review from TomAFrench July 29, 2026 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant