Migrating from v1.0.0-rc.0
These notes cover what needs to change when moving a project from Noir v1.0.0-rc.0 to the latest Noir 1.0 toolchain.
The language itself is the same. Most of the work comes from the type checker, which now enforces trait and impl rules more strictly, so some code that compiled on rc.0 is rejected. A smaller number of changes affect how the toolchain is installed, and what some code and tools produce.
To upgrade:
- Update
noirupand install the new toolchain (P0). An out-of-datenoirupcan fail to install the new releases. - Run
nargo checkon every package, including your own dependencies, and fix each error using P1. Every item there gives the error you'll see and how to resolve it. - Review the code listed in P2, where the same source now means something different, and adjust it if you depended on the old behavior.
- Update formatting and tests for the tooling changes in P3: run
nargo fmt, and update tests that match on exact error messages. - Run your full test suite on the new toolchain before shipping.
Only changes that need action are listed. Bug fixes that need nothing from you are left out. Sections are ordered by priority.
P0 — Must act on: the upgrade fails without this
1. Update noirup before installing the new toolchain
Releases from rc.2 onwards ship only the noir-<target>.tar.gz bundle. The standalone nargo-<target>.tar.gz is no longer published (#13625). An out-of-date noirup can fail to install them.
Update noirup itself first, then install:
curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash
noirup --version <version>
nargo --version
noirup received three fixes on 2026-09-17 for this release layout:
- Bundle detection (noirup#69). Older
noirupused aHEADprobe to check for thenoir-bundle and fell back to the legacynargo-tarball on any probe failure. WhereHEADis rejected (seen as a401from GitHub's asset CDN on some networks), it falls back to a file that no longer exists and the install fails. The current script downloads the bundle and falls back only on a real404. - Missing bin directory (noirup#63). Older
noirupfailed to unpack a prebuilt release when$NARGO_HOME/bindidn't exist, e.g. a fresh CI runner or container. It still printeddoneand exited 0, so CI could carry on with nonargo(or a stale one) onPATH. noir-execute(noirup#65). Oldernoirupdoesn't installnoir-executefor source builds (--branch,-C,--pr,--path), and doesn't remove a stale one, so it can end up mismatched withnargo.
Also:
- Pin the
noirupinstall script (or its commit) in CI and Docker images, so runners don't keep using a cached old copy. Make CI fail ifnargo --versiondoesn't report the expected version. - Anything that downloads
nargo-<target>.tar.gzdirectly, withoutnoirup, will 404. Switch it tonoir-<target>.tar.gz. That bundle also containsnoir-execute,noir-inspectorandnoir-profiler. - The
acvmbinary (acvm_cli) was deleted (#13632). Usenoir-executeinstead. It can now also take a witness file as input with--witness-file(#13592).
P1 — New compile errors: code that compiled on rc.0 may be rejected
The trait solver was largely reworked between rc.4 and master. The changes below reject code that rc.0 accepted. Run nargo check on every package (including dependencies) first, and work through the errors using the notes below.
-
Overlapping impls are detected as rustc does it (#14085, #14099).
Two impls of the same trait now conflict if any type could satisfy both, even if no type in the program currently does. This covers:
- two blanket impls whose
whereclauses could both hold for one type; - a generic impl for
Wrap<U>alongside a blanket impl that someWrap<_>satisfies.
impl<T> Validate for T where T: Bounded { ... }impl<T> Validate for T where T: Audited { ... } // error: overlaps (a type could be both)impl Small for Wrap<u8> {}impl<T> Checked for T where T: Small { ... }impl<U> Checked for Wrap<U> { ... } // error: overlaps for Wrap<u8>Fix: Noir has no negative bounds, so the impls have to be restructured so they can't apply to the same type:
- Replace one blanket impl with concrete impls for the types that actually need it, e.g.
impl Validate for Amount { ... }. - Or merge the two blanket impls into one, behind a single trait that captures both behaviors.
- For the generic-wrapper case, implement
Checkedfor the specificWrap<...>types that don't already get it from the blanket impl, instead of for everyWrap<U>.
- two blanket impls whose
-
wherebounds that mention no generics are checked (#14029, #14039, #14037).A bound such as
where Field: Markerorwhere S: Markerdoesn't depend on any of the function's generics, so it is now checked at the declaration. If no impl satisfies it, it is an error, even if the function is never called. This applies to free functions,main, and inherent and trait methods.Fix: If the bound is meant to hold, add the missing impl (
impl Marker for S {}). If it isn't needed, remove it: a bound with no generics never constrains callers, so removing it doesn't change the function's signature for anyone calling it. -
Method calls that several inherent impls could serve are ambiguous (#14015, #14036).
When a struct has inherent methods with the same name in impls for different instantiations, and the receiver's generics aren't known yet at the call, the call is now rejected with "Multiple applicable methods named
min scope". rc.0 silently picked one of the impls.struct S<T> { v: T }impl S<u8> { fn m(self) -> Field { 8 } }impl S<u16> { fn m(self) -> Field { 16 } }let s = S { v: 5 }; // T not known yets.m(); // errorS::<_>::m(s); // errorFix: Make the receiver's type known before the call, e.g.
let s: S<u8> = S { v: 5 };,S::<u8> { v: 5 }, orS::<u8>::m(s). Check which impl rc.0 was picking: it may not be the one you intended. -
Impl methods may not require more than the trait method (#13655).
An impl method whose
whereclause equates two bounds' associated items, when the trait method doesn't, is rejected as "impl has stricter requirements than trait":trait Foo { fn foo<B, C>() where B: Bar, C: Bar; }impl Foo for Field {fn foo<B, C>() where B: Bar, C: Bar<N = <B as Bar>::N> {} // error}Fix: Either add the same requirement to the trait method's declaration (this then applies to every impl and every caller), or remove it from the impl. For associated constants, the relationship can be enforced in the body instead, e.g.
assert_eq(<B as Bar>::N, <C as Bar>::N). -
Numeric generics must divide exactly when solved (#13861).
rc.0 solved
A * 2 = 5asA = 2by truncating the division. Now there is no solution, so the call fails with "Type annotation needed":fn make<let A: u32>() -> [Field; A * 2] { [0; A * 2] }let x: [Field; 5] = make(); // error: no u32 A satisfies A * 2 = 5Fix: Use a length the expression can actually produce (
[Field; 4]or[Field; 6]here), or pass the generic explicitly (make::<2>()) and update the annotated type to match. Any code that relied on the truncated solution had a length mismatch, so check what it was doing. -
A trait listed as its own parent is a dependency cycle (#13934).
trait A: A {}ortrait A: A + Bar {}now reports "Dependency cycle found".Fix: Remove
Afrom its own supertrait list. If the intent was to require another trait, name that trait instead. -
Stricter rules for unconstrained functions inside unconstrained-call arguments (#13705, #13785).
-
An unconstrained function can be passed where a constrained
fnis expected only when it is directly an argument of an unconstrained call. Assigning it to a constrainedfnvariable or struct field somewhere inside the argument expression is now an error ("Converting an unconstrained fn to a non-unconstrained fn is unsafe"):let mut g: fn() -> () = foo;unsafe { sink({ g = bar; 0 }) }; // error: `bar` is unconstrainedFix: If
gshould hold unconstrained functions, declare it with the typeunconstrained fn() -> (). Otherwise, assign a constrained function. -
Only a lambda written directly as an argument of an unconstrained call is compiled as unconstrained. A lambda defined inside that lambda's body, or passed to a constrained method somewhere in the argument list, is constrained. Inside such a lambda, calls to unconstrained functions now report "Call to unconstrained function is unsafe".
Fix: Wrap those calls in
unsafe { ... }with a// Safety:comment, or move the logic into a namedunconstrained fn.
-
-
Binary operators are checked against the operands' resolved types (#14073).
When one operand's type is inferred, e.g. from a generic function's return type, the operator is checked against the type it resolves to.
zero() + true, wherezero()resolves tobool, is now "Cannot add aboolto abool".Fix: Use an operator the resolved type supports (
|,&or^forbool), or annotate the call so it resolves to the type intended, e.g.zero::<u8>(). -
Impl
whereclauses are checked more thoroughly. The solver now checks:- a trait impl's
whereclause even when the impl is used before it has been fully elaborated (#13939); - a trait's own
whereclause against the associated types an impl chooses (#13933).
Impls that didn't meet these requirements but weren't caught on rc.0 now produce "No matching impl found" errors.
Fix: Add the impl the error names, or add the missing bound to the impl's
whereclause so the requirement is passed on to its users. - a trait impl's
P2 — Code whose meaning changed
- Turbofish through a type alias (#13997). A struct literal or pattern's turbofish on a type alias is now resolved against the alias's own generics, not the aliased struct's. With
type Scaled<let SCALE: u32, let MAX: u32> = Capped<MAX, SCALE>;,Scaled::<1000, 8> { v }now meansCapped<8, 1000>. If you wrote turbofish arguments in the struct's order to match the old behavior, swap them into the alias's order.
P3 — Tooling changes
nargo fmtremoves emptyuselists (it used to turn them into imports) (#13678). If CI runsnargo fmt --check, runnargo fmtand commit the result.- Out-of-bounds error messages in ACIR now report the logical index, e.g.
Index out of bounds, array has size 3, but index was 9(#13770, #13776). Update any tests that match on exact error strings.