Skip to content

docs(aztec-nr): flag host-dependent ordering in external_functions_registry#23899

Closed
AztecBot wants to merge 1 commit into
merge-train/fairies-v5from
cb/aztec-nr-external-registry-determinism
Closed

docs(aztec-nr): flag host-dependent ordering in external_functions_registry#23899
AztecBot wants to merge 1 commit into
merge-train/fairies-v5from
cb/aztec-nr-external-registry-determinism

Conversation

@AztecBot

@AztecBot AztecBot commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Background

PR #23896 (contract-snapshots regen for merge-train/fairies-v5) failed CI on one snapshot (expand::test_avm_test_contract) even though the author's local run reported 60/61 → 61/61 pass. Diagnosis from that CI run is in the gist.

This PR documents what the investigation found and opens the conversation on where to put the fix. The only change in code so far is a module-level doc note in external_functions_registry.nr.

What's actually happening

The expand::test_avm_test_contract snapshot diff is purely a function-pair swap (bodies byte-identical):

Section in nargo expand output Source order in avm_test_contract/src/main.nr Snapshot committed in #23896 Fresh nargo expand on this host
Forward decls (top) nested_call_large_calldata (839) then call_fee_juice (850) matches source order matches source order
Impl blocks (4 sections: PublicCall builder, inline-call wrapper, etc.) (same as above) reversed matches source order

Both runs use the same nargo 1.0.0-beta.22+c57152f. Two back-to-back fresh nargo expand invocations on this host produce byte-identical output, so it isn't per-process flakiness. Different hosts get different answers.

Root cause path

The impl-block macro is calls_generation::external_functions::generate_external_function_calls in aztec-nr/aztec/src/macros/calls_generation/external_functions.nr. It iterates over external_functions_registry::get_public_functions(m) etc.

That registry is CHashMap<Module, [FunctionDefinition]> (utils/cmap.nr). CHashMap is backed by a plain slice — insertion-order preserving. So get_* returns entries in the order add_* was called.

add_public / add_private / add_utility are called from the body of the #[external(...)] attribute macro (macros/functions/mod.nr:341). Which is once per #[external(...)]-decorated function, in whatever order noirc invokes the attribute macro across the contract module's functions.

That iteration order — noirc's attribute application order across a module — is not guaranteed to be host-independent. Different hosts with the same nargo binary apply the attributes in different orders, and the registry faithfully preserves whatever order it was given. The forward-decl section of nargo expand happens to traverse functions in source order via a different code path, which is why those forward decls are consistent across hosts while the impl-block sections aren't.

Fix difficulty

Two ways to make this deterministic:

  1. In aztec-nr (local fix). Have get_*_functions sort entries by a host-independent key (FunctionDefinition::name() or ::location()) before returning. Requires a comptime ordering primitive that the current Noir stdlib doesn't expose: Quoted has only Eq, no Ord; Location has only Eq and Hash (no file/line accessors). A pure aztec-nr workaround is possible but messy: it would need to thread function names through f"{...}"-style fmtstrs and implement a byte-level string compare for variable-length names, or sort by Hash<Location> (which is itself host-dependent and so doesn't actually help).
  2. In noir-lang/noir (upstream fix). Make noirc's attribute-application order across a module deterministic, e.g. by iterating in source order rather than via a hash-based collection. This is likely a small targeted change in the relevant pass and removes the need for any aztec-nr workaround. It also benefits any other comptime registry pattern that someone else might write.

My recommendation is (2) upstream for the long-term fix, plus a separate trivial noir-stdlib PR adding Ord on Quoted/Location and Location::file()/line() accessors so that aztec-nr-side sorts of comptime registries are cheap to write going forward.

What this PR does

  • Adds a # Determinism block to the module doc of external_functions_registry.nr recording the invariant ("the iteration order of get_*_functions is not guaranteed to be host-independent") so the next person who writes a snapshot test that depends on this order is forewarned.
  • Does not change behavior.

What this PR does NOT do

  • Does not fix PR chore: regenerate contract snapshots for fairies-v5 #23896. That PR can unblock by regenerating the avm_test_contract expand snapshot on a Linux x86_64 host so its order matches CI's (or by waiting on whichever fix path below lands first).
  • Does not implement the sort. The aztec-nr-side workaround is feasible but ugly with current Noir comptime primitives, and a noirc-side fix subsumes it.

Reproduction evidence

From this session, on Linux x86_64 with nargo 1.0.0-beta.22+c57152f:

$ grep -n "fn call_fee_juice\|fn nested_call_large_calldata" \
    noir-projects/contract-snapshots/tests/snapshots/expand/avm_test_contract/snapshots__expanded.snap | head -8
670:    fn nested_call_large_calldata(arr: [Field; 300]) -> pub Field;
678:    fn call_fee_juice();
1380:        pub fn call_fee_juice(self) -> ...        # snap (impl block): call_fee_juice first
1428:        pub fn nested_call_large_calldata(self, arr: [Field; 300]) -> ...
...

$ nargo expand --package avm_test_contract | grep -n "fn call_fee_juice\|fn nested_call_large_calldata"
667:    fn nested_call_large_calldata(arr: [Field; 300]) -> pub Field;
675:    fn call_fee_juice();
1377:        pub fn nested_call_large_calldata(self, arr: [Field; 300]) -> ...   # fresh: nested first
1407:        pub fn call_fee_juice(self) -> ...
...

Same binary, same source, two different impl-block orderings.

Suggested next steps

  • File the noir-lang/noir issue tracking the attribute-application order guarantee (or a stdlib issue for the Quoted/Location ordering primitives, depending on which path the team chooses).
  • Use this PR as the link target from PR chore: regenerate contract snapshots for fairies-v5 #23896's review thread when the question "why did this pass locally but fail in CI" comes up next time.

Created by claudebox · group: slackbot

…gistry

The PUBLIC/PRIVATE/UTILITY registries hand back entries in the order
#[external(...)] attribute macros were applied, which the noir compiler
does not guarantee to be host-independent. Downstream macros emit code
in that order, so identical nargo binaries on different hosts can
produce different expand snapshots for the same contract source.

This commit only documents the invariant. The fix (sort entries by a
host-independent key on read, or fix the order upstream in noirc) is
follow-up work.
@AztecBot

Copy link
Copy Markdown
Collaborator Author

Automatically closing this stale claudebox draft PR (no updates for 5+ days). Re-open if still needed.

@AztecBot AztecBot closed this Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-noir Component: Noir/Nargo ci-draft Run CI on draft PRs. claudebox Owned by claudebox. it can push to this PR. private-port-next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant