From df347c8a1817bcc67c6152609363159641d37140 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 16:49:50 -0300 Subject: [PATCH 01/18] fix: print `Self::item` for assumed trait items in `nargo expand` Inside a trait's default method body, a reference to another item of the same trait (e.g. `Self::static_method_2()`) was printed by falling back to the plain function path `ATrait::static_method_2()`. That form does not resolve when recompiled because the receiver type is unknown. Track the trait's `Self` type variable while printing its body and print any assumed trait item over that (still unbound) variable as `Self::item`. This un-ignores the `trait_function_calls` and `trait_static_methods` `nargo expand` integration tests. Co-Authored-By: Claude Fable 5 --- .../src/hir/printer/items/hir_def.rs | 10 +++- .../noirc_frontend/src/hir/printer/mod.rs | 10 +++- compiler/noirc_frontend/src/tests/expand.rs | 52 +++++++++++++++++++ tooling/nargo_cli/build.rs | 6 +-- .../execute__tests__expanded.snap | 24 ++++----- .../execute__tests__expanded.snap | 2 +- .../execute__tests__expanded.snap | 2 +- .../execute__tests__expanded.snap | 6 +-- 8 files changed, 88 insertions(+), 24 deletions(-) diff --git a/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs b/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs index 4b8cae410e6..5b3b2608bba 100644 --- a/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs +++ b/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs @@ -857,7 +857,15 @@ impl ItemPrinter<'_, '_> { } else { match &constraint.typ { Type::TypeVariable(type_var) if type_var.borrow().is_unbound() => { - // Don't show this as `AsTraitPath` + // The trait's own `Self` type variable can only stay unbound inside + // that trait's body, where the item is reachable as `Self::item`. + if self.trait_self_typevar == Some(type_var.id()) { + self.push_str("Self::"); + let name = self.interner.definition_name(trait_item.definition); + self.push_str(name); + return; + } + // Otherwise don't show this as `AsTraitPath` } _ => { self.push('<'); diff --git a/compiler/noirc_frontend/src/hir/printer/mod.rs b/compiler/noirc_frontend/src/hir/printer/mod.rs index de473e5daf9..900cdb9a820 100644 --- a/compiler/noirc_frontend/src/hir/printer/mod.rs +++ b/compiler/noirc_frontend/src/hir/printer/mod.rs @@ -6,7 +6,7 @@ use crate::hir::printer::items::ItemBuilder; use crate::hir::resolution::visibility::module_def_id_visibility; use crate::node_interner::TraitImplId; use crate::{ - DataType, Kind, NamedGeneric, ResolvedGenerics, Type, + DataType, Kind, NamedGeneric, ResolvedGenerics, Type, TypeVariableId, ast::{DocComment, Ident, ItemVisibility}, graph::Dependency, hir::{ @@ -89,6 +89,11 @@ struct ItemPrinter<'context, 'string> { imports: HashMap, self_type: Option, + /// When printing the body of a trait, this is the trait's `Self` type variable. + /// Any unbound occurrence of it (for example in `Self::method()` inside a default + /// method) must be printed as `Self`, which is the only name it has in source. + trait_self_typevar: Option, + /// Trait constraints in scope from an enclosing trait, trait impl, inherent impl, or /// function. A method's own where clause is filtered against these (see /// [`Self::parent_constraints_contain`]) so constraints already shown on the enclosing item @@ -122,6 +127,7 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { module_id, imports, self_type: None, + trait_self_typevar: None, trait_constraints: Vec::new(), trait_impls_printed: HashSet::new(), } @@ -448,6 +454,7 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { self.increase_indent(); self.trait_constraints = trait_.where_clause.clone(); + self.trait_self_typevar = Some(trait_.self_type_typevar.id()); let mut printed_type_or_function = false; @@ -498,6 +505,7 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { self.push('}'); self.trait_constraints.clear(); + self.trait_self_typevar = None; // Only show trait impls for types outside of the current crate: // trait impls for types in this crate are already shown alongside the type definition. diff --git a/compiler/noirc_frontend/src/tests/expand.rs b/compiler/noirc_frontend/src/tests/expand.rs index 82c70a66167..bdbe05865a7 100644 --- a/compiler/noirc_frontend/src/tests/expand.rs +++ b/compiler/noirc_frontend/src/tests/expand.rs @@ -562,3 +562,55 @@ fn expands_trait_method_call_shadowed_by_inherent_method() { } "); } + +#[test] +fn expands_self_static_trait_method_call_in_default_method() { + let src = r#" + trait ATrait { + fn static_method() -> Field { + Self::static_method_2() + } + + fn static_method_2() -> Field { + 100 + } + } + + struct Foo {} + + impl ATrait for Foo { + fn static_method_2() -> Field { + 200 + } + } + + fn main() { + let _ = Foo::static_method(); + } + "#; + let expanded = assert_no_errors_and_to_string(src); + insta::assert_snapshot!(expanded, @r" + trait ATrait { + fn static_method() -> Field { + Self::static_method_2() + } + + fn static_method_2() -> Field { + 100_Field + } + } + + struct Foo { + } + + impl ATrait for Foo { + fn static_method_2() -> Field { + 200_Field + } + } + + fn main() { + let _: Field = ::static_method(); + } + "); +} diff --git a/tooling/nargo_cli/build.rs b/tooling/nargo_cli/build.rs index 9fb8dedde38..b8eb8ad63de 100644 --- a/tooling/nargo_cli/build.rs +++ b/tooling/nargo_cli/build.rs @@ -250,7 +250,7 @@ const TESTS_WITHOUT_STDOUT_CHECK: [&str; 0] = []; /// These tests are ignored because of existing bugs in `nargo expand`. /// As the bugs are fixed these tests should be removed from this list. /// (some are ignored on purpose for the same reason as `IGNORED_NARGO_EXPAND_EXECUTION_TESTS`) -const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 11] = [ +const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 9] = [ // A generated associated constant resolves to `<[T; N] as Ser>::N`, which `nargo expand` // prints as `<(resolved type) as Ser>::N` — not valid syntax to recompile. "regression_10747_associated_constant", @@ -260,11 +260,7 @@ const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 11] = [ // because it references another project by a relative path "reexports", // bug - "trait_function_calls", - // bug "trait_method_mut_self", - // bug - "trait_static_methods", // There's no "src/main.nr" here so it's trickier to make this work "workspace_reexport_bug", // bug diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_function_calls/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_function_calls/execute__tests__expanded.snap index 777a0beb4b6..b6d7107e7f2 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_function_calls/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_function_calls/execute__tests__expanded.snap @@ -232,7 +232,7 @@ impl Trait1i for Struct1i { trait Trait2a { fn trait_method1(self) -> Field { - (Trait2a::trait_function2() * 2385_Field) - self.vl() + (Self::trait_function2() * 2385_Field) - self.vl() } fn trait_function2() -> Field { @@ -254,7 +254,7 @@ impl Trait2a for Struct2a { trait Trait2b { fn trait_method1(self) -> Field { - (Trait2b::trait_function2() * 6583_Field) - self.vl() + (Self::trait_function2() * 6583_Field) - self.vl() } fn trait_function2() -> Field { @@ -280,7 +280,7 @@ impl Trait2b for Struct2b { trait Trait2c { fn trait_method1(self) -> Field { - (Trait2c::trait_function2() * 2831_Field) - self.vl() + (Self::trait_function2() * 2831_Field) - self.vl() } fn trait_function2() -> Field; @@ -304,7 +304,7 @@ impl Trait2c for Struct2c { trait Trait2d { fn trait_method1(self) -> Field { - (Trait2d::trait_function2() * 924_Field) - self.vl() + (Self::trait_function2() * 924_Field) - self.vl() } fn trait_function2() -> Field { @@ -330,7 +330,7 @@ impl Trait2d for Struct2d { trait Trait2e { fn trait_method1(self) -> Field { - (Trait2e::trait_function2() * 3642_Field) - self.vl() + (Self::trait_function2() * 3642_Field) - self.vl() } fn trait_function2() -> Field { @@ -360,7 +360,7 @@ impl Trait2e for Struct2e { trait Trait2f { fn trait_method1(self) -> Field { - (Trait2f::trait_function2() * 2783_Field) - self.vl() + (Self::trait_function2() * 2783_Field) - self.vl() } fn trait_function2() -> Field; @@ -676,7 +676,7 @@ impl Trait3i for Struct3i { trait Trait4a { fn trait_function1() -> Field { - Trait4a::trait_function2() * 3842_Field + Self::trait_function2() * 3842_Field } fn trait_function2() -> Field { @@ -692,7 +692,7 @@ impl Trait4a for Struct4a {} trait Trait4b { fn trait_function1() -> Field { - Trait4b::trait_function2() * 3842_Field + Self::trait_function2() * 3842_Field } fn trait_function2() -> Field { @@ -712,7 +712,7 @@ impl Trait4b for Struct4b { trait Trait4c { fn trait_function1() -> Field { - Trait4c::trait_function2() * 7832_Field + Self::trait_function2() * 7832_Field } fn trait_function2() -> Field; @@ -730,7 +730,7 @@ impl Trait4c for Struct4c { trait Trait4d { fn trait_function1() -> Field { - Trait4d::trait_function2() * 2283_Field + Self::trait_function2() * 2283_Field } fn trait_function2() -> Field { @@ -750,7 +750,7 @@ impl Trait4d for Struct4d { trait Trait4e { fn trait_function1() -> Field { - Trait4e::trait_function2() * 94329_Field + Self::trait_function2() * 94329_Field } fn trait_function2() -> Field { @@ -774,7 +774,7 @@ impl Trait4e for Struct4e { trait Trait4f { fn trait_function1() -> Field { - Trait4f::trait_function2() * 23723_Field + Self::trait_function2() * 23723_Field } fn trait_function2() -> Field; diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_inheritence_call_method_on_self/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_inheritence_call_method_on_self/execute__tests__expanded.snap index ff9c80c304f..10044390e28 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_inheritence_call_method_on_self/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_inheritence_call_method_on_self/execute__tests__expanded.snap @@ -6,7 +6,7 @@ pub trait Empty: Eq { fn empty() -> Self; fn is_empty(self) -> bool { - self.eq(Empty::empty()) + self.eq(Self::empty()) } } diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_static_methods/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_static_methods/execute__tests__expanded.snap index 60bb0c53941..bcbdcac2662 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_static_methods/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_static_methods/execute__tests__expanded.snap @@ -6,7 +6,7 @@ trait ATrait { fn asd() -> Self; fn static_method() -> Field { - ATrait::static_method_2() + Self::static_method_2() } fn static_method_2() -> Field { diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/regression_8210/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/regression_8210/execute__tests__expanded.snap index 67b9143a76a..4b1c7a9e1de 100644 --- a/tooling/nargo_cli/tests/snapshots/execution_success/regression_8210/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/execution_success/regression_8210/execute__tests__expanded.snap @@ -6,15 +6,15 @@ trait Foo { let BAR: u32; fn via_trait(_: Self) -> u32 { - BAR + Self::BAR } fn via_self(_: Self) -> u32 { - BAR + Self::BAR } fn arith(_: Self) -> u32 { - BAR + 1_u32 + Self::BAR + 1_u32 } } From e020eb856526e459f1bed13dece80d9675b6bb90 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 17:02:17 -0300 Subject: [PATCH 02/18] fix: don't print `impl Trait` parameter constraints in where clauses An `impl Trait` parameter desugars to a hidden generic named `impl {Trait}` plus a trait constraint on it. `nargo expand` printed that constraint in the function's where clause as `where impl Trait: Trait`, which doesn't resolve when recompiled. The constraint is implied by the parameter type itself, so filter it out of the printed where clause. This un-ignores the `trait_method_mut_self` `nargo expand` integration test. Co-Authored-By: Claude Fable 5 --- .../noirc_frontend/src/hir/printer/mod.rs | 15 +++++- compiler/noirc_frontend/src/tests.rs | 14 ++++- compiler/noirc_frontend/src/tests/expand.rs | 53 ++++++++++++++++++- tooling/nargo_cli/build.rs | 4 +- .../execute__tests__expanded.snap | 5 +- 5 files changed, 80 insertions(+), 11 deletions(-) diff --git a/compiler/noirc_frontend/src/hir/printer/mod.rs b/compiler/noirc_frontend/src/hir/printer/mod.rs index 900cdb9a820..d96c5060ff0 100644 --- a/compiler/noirc_frontend/src/hir/printer/mod.rs +++ b/compiler/noirc_frontend/src/hir/printer/mod.rs @@ -713,7 +713,20 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { .cloned() .collect::>(); - self.show_where_clause(&func_trait_constraints); + // An `impl Trait` parameter desugars to a hidden generic named `impl {Trait}` with a + // trait constraint (see `desugar_impl_trait_arg`). That constraint is implied by the + // parameter type itself and its synthetic name is not valid in a where clause, so it + // must not be printed. + let shown_trait_constraints = func_trait_constraints + .iter() + .filter(|constraint| { + !matches!(&constraint.typ, + Type::NamedGeneric(generic) if generic.name.starts_with("impl ")) + }) + .cloned() + .collect::>(); + + self.show_where_clause(&shown_trait_constraints); let previous_trait_constraints_length = self.trait_constraints.len(); self.trait_constraints.extend(func_trait_constraints); diff --git a/compiler/noirc_frontend/src/tests.rs b/compiler/noirc_frontend/src/tests.rs index e3f17fe16da..4dbe100e19c 100644 --- a/compiler/noirc_frontend/src/tests.rs +++ b/compiler/noirc_frontend/src/tests.rs @@ -92,7 +92,17 @@ pub fn assert_no_errors_without_report(src: &str) -> Context<'_, '_> { } fn assert_no_errors_and_to_string(src: &str) -> String { - let context = assert_no_errors(src); + assert_no_errors_and_to_string_using_features( + src, + FrontendOptions::test_default().enabled_unstable_features, + ) +} + +fn assert_no_errors_and_to_string_using_features( + src: &str, + features: &[UnstableFeature], +) -> String { + let context = assert_no_errors_using_features(src, features); let expanded = display_crate( *context.crate_graph.root_crate_id(), &context.crate_graph, @@ -107,7 +117,7 @@ fn assert_no_errors_and_to_string(src: &str) -> String { // its module and break access to a module-private item. Only hard errors are checked: the // printer can legitimately produce code whose warnings (e.g. unused imports) differ from the // original. - let errors = get_program_errors(&expanded); + let errors = get_program_using_features(&expanded, features).2; let errors: Vec<_> = errors.iter().map(CustomDiagnostic::from).filter(CustomDiagnostic::is_error).collect(); if !errors.is_empty() { diff --git a/compiler/noirc_frontend/src/tests/expand.rs b/compiler/noirc_frontend/src/tests/expand.rs index bdbe05865a7..032657aba3d 100644 --- a/compiler/noirc_frontend/src/tests/expand.rs +++ b/compiler/noirc_frontend/src/tests/expand.rs @@ -1,7 +1,7 @@ //! Tests for `nargo expand` output (via the HIR printer), focusing on faithfully //! reconstructing impls: their generics and where clauses. -use crate::tests::assert_no_errors_and_to_string; +use crate::tests::{assert_no_errors_and_to_string, assert_no_errors_and_to_string_using_features}; #[test] fn expands_inherent_impl_with_where_clause() { @@ -614,3 +614,54 @@ fn expands_self_static_trait_method_call_in_default_method() { } "); } + +#[test] +fn expands_impl_trait_parameter_without_where_clause() { + let src = r#" + trait SomeTrait { + fn get_value(self) -> Field; + } + + struct AType {} + + impl SomeTrait for AType { + fn get_value(self) -> Field { + 1 + } + } + + fn take(x: impl SomeTrait) -> Field { + x.get_value() + } + + fn main() { + let _ = take(AType {}); + } + "#; + let expanded = assert_no_errors_and_to_string_using_features( + src, + &[crate::elaborator::UnstableFeature::TraitAsType], + ); + insta::assert_snapshot!(expanded, @r" + trait SomeTrait { + fn get_value(self) -> Field; + } + + struct AType { + } + + impl SomeTrait for AType { + fn get_value(self) -> Field { + 1_Field + } + } + + fn take(x: impl SomeTrait) -> Field { + x.get_value() + } + + fn main() { + let _: Field = take(AType { }); + } + "); +} diff --git a/tooling/nargo_cli/build.rs b/tooling/nargo_cli/build.rs index b8eb8ad63de..de796bce093 100644 --- a/tooling/nargo_cli/build.rs +++ b/tooling/nargo_cli/build.rs @@ -250,7 +250,7 @@ const TESTS_WITHOUT_STDOUT_CHECK: [&str; 0] = []; /// These tests are ignored because of existing bugs in `nargo expand`. /// As the bugs are fixed these tests should be removed from this list. /// (some are ignored on purpose for the same reason as `IGNORED_NARGO_EXPAND_EXECUTION_TESTS`) -const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 9] = [ +const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 8] = [ // A generated associated constant resolves to `<[T; N] as Ser>::N`, which `nargo expand` // prints as `<(resolved type) as Ser>::N` — not valid syntax to recompile. "regression_10747_associated_constant", @@ -259,8 +259,6 @@ const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 9] = [ // this one works, but copying its `Nargo.toml` file to somewhere else doesn't work // because it references another project by a relative path "reexports", - // bug - "trait_method_mut_self", // There's no "src/main.nr" here so it's trickier to make this work "workspace_reexport_bug", // bug diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_method_mut_self/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_method_mut_self/execute__tests__expanded.snap index 51c533df91f..fcbd2e84d49 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_method_mut_self/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_method_mut_self/execute__tests__expanded.snap @@ -40,10 +40,7 @@ impl SomeTrait for AType { } } -fn pass_trait_by_value_impl_param(mut a_mut_ref: impl SomeTrait, value: Field) -where - impl SomeTrait: SomeTrait, -{ +fn pass_trait_by_value_impl_param(mut a_mut_ref: impl SomeTrait, value: Field) { a_mut_ref.set_value(value); assert(a_mut_ref.get_value() == value); } From 17603281f70588beab84e6b1c99e7ace63bfd0e0 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 17:09:43 -0300 Subject: [PATCH 03/18] fix: print `impl Trait` parameters with their trait generic arguments The hidden generic minted for an `impl Trait` parameter is named `impl {trait_path}` without the trait's generic arguments, so `nargo expand` printed `_input: impl Foo` for `_input: impl Foo` and the expansion failed to recompile with "Foo expects 1 generic but 0 were given". Print such parameters from their resolved desugared trait constraint, which carries the generic arguments. This un-ignores the `regression_7648` `nargo expand` integration test. Co-Authored-By: Claude Fable 5 --- .../noirc_frontend/src/hir/printer/mod.rs | 33 ++++++++++++++++- compiler/noirc_frontend/src/tests/expand.rs | 35 +++++++++++++++++++ tooling/nargo_cli/build.rs | 4 +-- .../execute__tests__expanded.snap | 5 +-- 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/compiler/noirc_frontend/src/hir/printer/mod.rs b/compiler/noirc_frontend/src/hir/printer/mod.rs index d96c5060ff0..3a2421eff7b 100644 --- a/compiler/noirc_frontend/src/hir/printer/mod.rs +++ b/compiler/noirc_frontend/src/hir/printer/mod.rs @@ -16,6 +16,7 @@ use crate::{ }, hir_def::{ expr::HirExpression, + function::FuncMeta, stmt::{HirLetStatement, HirPattern}, traits::{ResolvedTraitBound, TraitConstraint}, }, @@ -685,7 +686,16 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { if matches!(visibility, Visibility::Public) { self.push_str("pub "); } - self.show_type(typ); + + // An `impl Trait` parameter desugars to a hidden generic whose synthetic name + // omits the trait's generic arguments (see `desugar_impl_trait_arg`), so it is + // printed from its resolved trait constraint instead. + if let Some(constraint) = impl_trait_parameter_constraint(func_meta, typ) { + self.push_str("impl "); + self.show_trait_bound(&constraint.trait_bound); + } else { + self.show_type(typ); + } } if index != parameters.len() - 1 { @@ -1441,3 +1451,24 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { self.string.push(char); } } + +/// If `typ` is the hidden generic minted for an `impl Trait` parameter, returns the trait +/// constraint that was desugared alongside it (see `desugar_impl_trait_arg`). The synthetic +/// generic's name omits the trait's generic arguments, so printing the parameter faithfully +/// requires the resolved bound instead. +fn impl_trait_parameter_constraint<'meta>( + func_meta: &'meta FuncMeta, + typ: &Type, +) -> Option<&'meta TraitConstraint> { + let Type::NamedGeneric(generic) = typ else { + return None; + }; + if !generic.name.starts_with("impl ") { + return None; + } + func_meta.trait_constraints.iter().find(|constraint| { + matches!(&constraint.typ, + Type::NamedGeneric(constraint_generic) + if constraint_generic.type_var.id() == generic.type_var.id()) + }) +} diff --git a/compiler/noirc_frontend/src/tests/expand.rs b/compiler/noirc_frontend/src/tests/expand.rs index 032657aba3d..b9290854fc7 100644 --- a/compiler/noirc_frontend/src/tests/expand.rs +++ b/compiler/noirc_frontend/src/tests/expand.rs @@ -665,3 +665,38 @@ fn expands_impl_trait_parameter_without_where_clause() { } "); } + +#[test] +fn expands_impl_trait_parameter_with_generics() { + let src = r#" + trait Foo {} + + impl Foo for [Field; N] {} + + fn my_fn(_input: impl Foo) {} + + fn main() { + my_fn::<0>([]); + } + "#; + let expanded = assert_no_errors_and_to_string_using_features( + src, + &[crate::elaborator::UnstableFeature::TraitAsType], + ); + insta::assert_snapshot!(expanded, @r" + trait Foo { + + } + + impl Foo for [Field; N] { + + } + + fn my_fn(_input: impl Foo) { + } + + fn main() { + my_fn::<0>([]); + } + "); +} diff --git a/tooling/nargo_cli/build.rs b/tooling/nargo_cli/build.rs index de796bce093..a8ba4c22b83 100644 --- a/tooling/nargo_cli/build.rs +++ b/tooling/nargo_cli/build.rs @@ -250,7 +250,7 @@ const TESTS_WITHOUT_STDOUT_CHECK: [&str; 0] = []; /// These tests are ignored because of existing bugs in `nargo expand`. /// As the bugs are fixed these tests should be removed from this list. /// (some are ignored on purpose for the same reason as `IGNORED_NARGO_EXPAND_EXECUTION_TESTS`) -const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 8] = [ +const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 7] = [ // A generated associated constant resolves to `<[T; N] as Ser>::N`, which `nargo expand` // prints as `<(resolved type) as Ser>::N` — not valid syntax to recompile. "regression_10747_associated_constant", @@ -263,8 +263,6 @@ const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 8] = [ "workspace_reexport_bug", // bug "trait_call_in_global", - // `nargo expand` drops the trait generic arguments on `impl Trait<...>` parameters - "regression_7648", // The expanded code names a transitive-only dependency (`leaflib`) by path, which isn't // directly importable when the expansion is recompiled as a standalone program. "comptime_as_typed_expr_public_type_trait_method", diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_7648/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_7648/execute__tests__expanded.snap index 5ac81de9765..510a2be0f8d 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_7648/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_7648/execute__tests__expanded.snap @@ -6,10 +6,7 @@ trait Foo {} impl Foo for [Field; N] {} -fn my_fn(_input: impl Foo) -where - impl Foo: Foo, -{} +fn my_fn(_input: impl Foo) {} fn main() { my_fn::<0>([]); From 6b3b93852c1efc5a12e1dd526675c83663860241 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 17:23:16 -0300 Subject: [PATCH 04/18] fix: don't embed "(resolved type)" in associated type names The synthetic name of an associated type's named generic (`<{object} as {trait}>::{name}`) was built by stringifying the object type as written. When a macro splices an already-resolved type (e.g. `impl<...> Ser for $typ` or a `$name: Ser` where clause), that `UnresolvedTypeData::Resolved` displays as the "(resolved type)" placeholder, which then surfaced in `nargo expand` output as `<(resolved type) as Ser>::N` and failed to reparse. Look through the resolution to the actual type when building these names. This un-ignores the `regression_10747_associated_constant` `nargo expand` integration test. Co-Authored-By: Claude Fable 5 --- .../src/elaborator/trait_impls.rs | 2 +- .../noirc_frontend/src/elaborator/traits.rs | 2 +- .../noirc_frontend/src/elaborator/types.rs | 14 ++++++ tooling/nargo_cli/build.rs | 5 +- .../execute__tests__expanded.snap | 49 ++++++++++++------- 5 files changed, 49 insertions(+), 23 deletions(-) diff --git a/compiler/noirc_frontend/src/elaborator/trait_impls.rs b/compiler/noirc_frontend/src/elaborator/trait_impls.rs index 15f37d5ca37..faa313dfa8f 100644 --- a/compiler/noirc_frontend/src/elaborator/trait_impls.rs +++ b/compiler/noirc_frontend/src/elaborator/trait_impls.rs @@ -1156,7 +1156,7 @@ impl Elaborator<'_> { // This way associated types can be referred to even if their actual value (for associated constants) // is not known yet. This is to allow associated constants to refer to associated constants // in other trait impls. - let object = trait_impl.object_type.to_string(); + let object = self.unresolved_type_name(&trait_impl.object_type); let trait_name = trait_id.map(|id| self.interner.get_trait(id).name.to_string()); let associated_types_behind_type_vars = vecmap(&associated_types, |(name, _typ, kind)| { let new_generic_id = self.interner.next_type_variable_id(); diff --git a/compiler/noirc_frontend/src/elaborator/traits.rs b/compiler/noirc_frontend/src/elaborator/traits.rs index f9714172763..1a5f63569f3 100644 --- a/compiler/noirc_frontend/src/elaborator/traits.rs +++ b/compiler/noirc_frontend/src/elaborator/traits.rs @@ -444,7 +444,7 @@ impl Elaborator<'_> { let the_trait = self.get_trait(trait_id); let trait_name = the_trait.name.to_string(); - let object_name = object.to_string(); + let object_name = self.unresolved_type_name(object); let associated_type_bounds = the_trait.associated_type_bounds.clone(); for associated_type in &the_trait.associated_types.clone() { diff --git a/compiler/noirc_frontend/src/elaborator/types.rs b/compiler/noirc_frontend/src/elaborator/types.rs index 08f26f989e1..adb88dcf9ac 100644 --- a/compiler/noirc_frontend/src/elaborator/types.rs +++ b/compiler/noirc_frontend/src/elaborator/types.rs @@ -127,6 +127,20 @@ impl Elaborator<'_> { ) } + /// Stringifies a type as written, for embedding in an associated type's name + /// (`"<{object} as {trait}>::{name}"`). A macro-spliced type arrives already resolved and + /// its `Display` is the "(resolved type)" placeholder, so look through the resolution to + /// the actual type. These names resurface in `nargo expand` output, where the placeholder + /// would not parse. + pub(super) fn unresolved_type_name(&self, typ: &UnresolvedType) -> String { + match &typ.typ { + UnresolvedTypeData::Resolved(quoted_type_id) => { + self.interner.get_quoted_type(*quoted_type_id).to_string() + } + _ => typ.to_string(), + } + } + /// Resolves an [`UnresolvedType`] to a [Type] with [`Kind::Normal`] and marks it, and any generic types it contains, as _used_. #[tracing::instrument(level = "trace", skip_all)] pub(crate) fn use_type( diff --git a/tooling/nargo_cli/build.rs b/tooling/nargo_cli/build.rs index a8ba4c22b83..c8577e10c99 100644 --- a/tooling/nargo_cli/build.rs +++ b/tooling/nargo_cli/build.rs @@ -250,10 +250,7 @@ const TESTS_WITHOUT_STDOUT_CHECK: [&str; 0] = []; /// These tests are ignored because of existing bugs in `nargo expand`. /// As the bugs are fixed these tests should be removed from this list. /// (some are ignored on purpose for the same reason as `IGNORED_NARGO_EXPAND_EXECUTION_TESTS`) -const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 7] = [ - // A generated associated constant resolves to `<[T; N] as Ser>::N`, which `nargo expand` - // prints as `<(resolved type) as Ser>::N` — not valid syntax to recompile. - "regression_10747_associated_constant", +const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 6] = [ // There's no "src/main.nr" here so it's trickier to make this work "overlapping_dep_and_mod", // this one works, but copying its `Nargo.toml` file to somewhere else doesn't work diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_10747_associated_constant/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_10747_associated_constant/execute__tests__expanded.snap index bdc6c87346b..2c5e89d4ea7 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_10747_associated_constant/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_10747_associated_constant/execute__tests__expanded.snap @@ -2,7 +2,6 @@ source: tooling/nargo_cli/tests/execute.rs expression: expanded_code --- - pub trait Ser { let N: u32; } @@ -11,7 +10,10 @@ impl Ser for Field { let N: u32 = 1; } -impl Ser for [T; M] where T: Ser { +impl Ser for [T; M] +where + T: Ser, +{ let N: u32 = M * ::N; } @@ -19,22 +21,37 @@ struct Wrapped { xs: [T; N], } -impl Ser for Wrapped where T: Ser { - let N: u32 = N * <(resolved type) as Ser>::N; +impl Ser for Wrapped +where + T: Ser, +{ + let N: u32 = N * ::N; } pub comptime fn derive_ser(s: TypeDefinition) -> Quoted { let typ: Type = s.as_type(); - let rhs: Quoted = s.fields_as_written().map(|(_, field_type, _): (Quoted, Type, Quoted)| -> Quoted quote { < $field_type as Ser > ::N }).join(quote { + }); - let generics: Quoted = s.generics().map(|(name, maybe_int): (Type, Option)| -> Quoted { - if maybe_int.is_some() { - let int_type: Type = maybe_int.unwrap(); - quote { let $name: $int_type } - } else { - quote { $name } - } - }).join(quote { , }); - let where_clause: Quoted = s.generics().filter(|(_, maybe_int): (Type, Option)| -> bool maybe_int.is_none()).map(|(name, _): (Type, Option)| -> Quoted quote { $name: Ser }).join(quote { , }); + let rhs: Quoted = s + .fields_as_written() + .map(|(_, field_type, _): (Quoted, Type, Quoted)| -> Quoted { + quote { <$field_type as Ser>::N } + }) + .join(quote { + }); + let generics: Quoted = s + .generics() + .map(|(name, maybe_int): (Type, Option)| -> Quoted { + if maybe_int.is_some() { + let int_type: Type = maybe_int.unwrap(); + quote { let $name: $int_type } + } else { + quote { $name } + } + }) + .join(quote { , }); + let where_clause: Quoted = s + .generics() + .filter(|(_, maybe_int): (Type, Option)| -> bool maybe_int.is_none()) + .map(|(name, _): (Type, Option)| -> Quoted quote { $name: Ser }) + .join(quote { , }); quote { impl < $generics > Ser for $typ where $where_clause { let N: u32 = $rhs; @@ -43,8 +60,6 @@ pub comptime fn derive_ser(s: TypeDefinition) -> Quoted { } fn main() { - let _wrapped: Wrapped = Wrapped:: { xs: [1_Field, 2_Field, 3_Field]}; + let _wrapped: Wrapped = Wrapped:: { xs: [1_Field, 2_Field, 3_Field] }; let _len: u32 = as Ser>::N; } - -// Warning: the generated code has syntax errors From 0de2f0b181c5edd8eaf19c023fcddc306ef41951 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 17:30:42 -0300 Subject: [PATCH 05/18] fix: print a global's initializer when its value can't be printed as code `nargo expand` printed a global's comptime-evaluated value, but some values don't reconstruct as compilable source: struct literals whose type or fields are private outside their defining module (e.g. `BoundedVec { len, storage }`, `Option { _is_some, _value }`), and comptime-only values that print as a `panic(...)` placeholder. For such values, print the global's original initializer expression instead, which is at least as visible as it was in the original program. Representable values keep printing as evaluated values, since a comptime-mutable global's final value can differ from what its initializer evaluates to. This un-ignores the `trait_call_in_global`, `function_registry` and `regression_10887` `nargo expand` integration tests. Co-Authored-By: Claude Fable 5 --- .../noirc_frontend/src/hir/printer/mod.rs | 117 +++++++++++++++++- compiler/noirc_frontend/src/tests/expand.rs | 39 ++++++ tooling/nargo_cli/build.rs | 8 +- .../execute__tests__expanded.snap | 2 +- .../execute__tests__expanded.snap | 20 +-- .../execute__tests__expanded.snap | 2 +- 6 files changed, 159 insertions(+), 29 deletions(-) diff --git a/compiler/noirc_frontend/src/hir/printer/mod.rs b/compiler/noirc_frontend/src/hir/printer/mod.rs index 3a2421eff7b..f3a4137e7d2 100644 --- a/compiler/noirc_frontend/src/hir/printer/mod.rs +++ b/compiler/noirc_frontend/src/hir/printer/mod.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use crate::graph::{CrateGraph, CrateId}; use crate::hir::comptime::FormatStringFragment; use crate::hir::printer::items::ItemBuilder; -use crate::hir::resolution::visibility::module_def_id_visibility; +use crate::hir::resolution::visibility::{module_def_id_visibility, struct_member_is_visible}; use crate::node_interner::TraitImplId; use crate::{ DataType, Kind, NamedGeneric, ResolvedGenerics, Type, TypeVariableId, @@ -21,7 +21,7 @@ use crate::{ traits::{ResolvedTraitBound, TraitConstraint}, }, modules::{get_parent_module, module_def_id_is_visible, module_def_id_to_reference_id}, - node_interner::{FuncId, GlobalId, GlobalValue, NodeInterner, ReferenceId, TypeAliasId}, + node_interner::{FuncId, GlobalId, GlobalValue, NodeInterner, ReferenceId, TypeAliasId, TypeId}, shared::Visibility, token::{FunctionAttributeKind, LocatedToken, SecondaryAttribute, SecondaryAttributeKind}, }; @@ -628,11 +628,122 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { self.show_type(&typ); if let GlobalValue::Resolved(value) = &global_info.value { self.push_str(" = "); - self.show_value(value); + // Prefer the evaluated value: a comptime-mutable global's final value can differ + // from what its initializer evaluates to. But some values can't be reconstructed + // as source code (private struct fields, comptime-only values): for those, print + // the original initializer expression, which is at least as visible as it was in + // the original program. + if self.value_is_representable(value) { + self.show_value(value); + } else if let Some(let_statement) = self.interner.get_global_let_statement(global_id) { + let expr_id = let_statement.expression; + let hir_expr = self.interner.expression(&expr_id); + self.show_hir_expression(hir_expr, expr_id); + } else { + self.show_value(value); + } } self.push_str(";"); } + /// Whether [`Self::show_value`] can print `value` as source code that compiles from the + /// current module. Some values have no code representation at all (they print as a + /// `panic(...)` placeholder), and a struct literal only compiles where the struct's type + /// and all of its fields are visible. + fn value_is_representable(&self, value: &Value) -> bool { + match value { + Value::Unit + | Value::Bool(_) + | Value::Integer(_) + | Value::String(_) + | Value::CtString(_) + | Value::Function(..) + | Value::Closure(_) + | Value::Quoted(_) + | Value::Zeroed(_) => true, + + Value::FormatString(fragments, ..) => fragments.iter().all(|fragment| match fragment { + FormatStringFragment::String(_) => true, + FormatStringFragment::Value { value, .. } => self.value_is_representable(value), + }), + + Value::Tuple(values) => { + values.iter().all(|value| self.value_is_representable(&value.borrow())) + } + + Value::Struct(fields, typ) => { + self.struct_literal_is_visible(typ) + && fields.values().all(|value| self.value_is_representable(&value.borrow())) + } + + Value::Enum(_, args, typ) => { + self.data_type_is_visible(typ) + && args.iter().all(|arg| self.value_is_representable(arg)) + } + + Value::Array(values, _) | Value::Vector(values, _) => { + values.iter().all(|value| self.value_is_representable(value)) + } + + Value::Pointer(value, ..) => self.value_is_representable(&value.borrow()), + + // These print as a `panic(...)` placeholder (see `show_value`). + Value::TypeDefinition(_) + | Value::TraitConstraint(..) + | Value::TraitDefinition(_) + | Value::TraitImpl(_) + | Value::FunctionDefinition(_) + | Value::ModuleDefinition(_) + | Value::Type(_) + | Value::Expr(_) + | Value::TypedExpr(_) + | Value::UnresolvedType(_) + | Value::Location(_) => false, + } + } + + /// Whether a struct literal of the given type compiles from the current module: + /// the type itself and every field must be visible. + fn struct_literal_is_visible(&self, typ: &Type) -> bool { + let typ = typ.follow_bindings(); + let Type::DataType(data_type, generics) = &typ else { + return true; + }; + let data_type = data_type.borrow(); + if !self.data_type_id_is_visible(data_type.id, data_type.visibility) { + return false; + } + let Some(fields) = data_type.get_fields(generics) else { + return false; + }; + fields.iter().all(|(_, _, visibility)| { + struct_member_is_visible(data_type.id, *visibility, self.module_id, self.def_maps) + }) + } + + /// Whether the given data type (by name) is visible from the current module. + fn data_type_is_visible(&self, typ: &Type) -> bool { + let typ = typ.follow_bindings(); + let Type::DataType(data_type, _) = &typ else { + return true; + }; + let data_type = data_type.borrow(); + self.data_type_id_is_visible(data_type.id, data_type.visibility) + } + + fn data_type_id_is_visible(&self, id: TypeId, visibility: ItemVisibility) -> bool { + let module_def_id = ModuleDefId::TypeId(id); + module_def_id_is_visible( + module_def_id, + self.module_id, + visibility, + None, + self.interner, + self.def_maps, + self.dependencies, + ) || !self.interner.get_reexports(module_def_id).is_empty() + } + /// Whether a constraint already in scope from an enclosing item subsumes the given one, so /// it shouldn't be repeated on a method's where clause. /// diff --git a/compiler/noirc_frontend/src/tests/expand.rs b/compiler/noirc_frontend/src/tests/expand.rs index b9290854fc7..569735e5c02 100644 --- a/compiler/noirc_frontend/src/tests/expand.rs +++ b/compiler/noirc_frontend/src/tests/expand.rs @@ -700,3 +700,42 @@ fn expands_impl_trait_parameter_with_generics() { } "); } + +#[test] +fn expands_global_whose_value_has_private_fields_as_its_initializer_expression() { + let src = r#" + mod foo { + pub struct Bar { + value: Field, + } + + pub fn make_bar() -> Bar { + Bar { value: 1 } + } + } + + global B: foo::Bar = foo::make_bar(); + + fn main() { + let _ = B; + } + "#; + let expanded = assert_no_errors_and_to_string(src); + insta::assert_snapshot!(expanded, @r" + mod foo { + pub struct Bar { + value: Field, + } + + pub fn make_bar() -> Bar { + Bar { value: 1_Field} + } + } + + global B: foo::Bar = foo::make_bar(); + + fn main() { + let _: foo::Bar = B; + } + "); +} diff --git a/tooling/nargo_cli/build.rs b/tooling/nargo_cli/build.rs index c8577e10c99..813196cabaa 100644 --- a/tooling/nargo_cli/build.rs +++ b/tooling/nargo_cli/build.rs @@ -250,7 +250,7 @@ const TESTS_WITHOUT_STDOUT_CHECK: [&str; 0] = []; /// These tests are ignored because of existing bugs in `nargo expand`. /// As the bugs are fixed these tests should be removed from this list. /// (some are ignored on purpose for the same reason as `IGNORED_NARGO_EXPAND_EXECUTION_TESTS`) -const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 6] = [ +const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 5] = [ // There's no "src/main.nr" here so it's trickier to make this work "overlapping_dep_and_mod", // this one works, but copying its `Nargo.toml` file to somewhere else doesn't work @@ -258,8 +258,6 @@ const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 6] = [ "reexports", // There's no "src/main.nr" here so it's trickier to make this work "workspace_reexport_bug", - // bug - "trait_call_in_global", // The expanded code names a transitive-only dependency (`leaflib`) by path, which isn't // directly importable when the expansion is recompiled as a standalone program. "comptime_as_typed_expr_public_type_trait_method", @@ -270,7 +268,7 @@ const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 6] = [ /// These tests are ignored because of existing bugs in `nargo expand`. /// As the bugs are fixed these tests should be removed from this list. -const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_NO_BUG_TESTS: [&str; 17] = [ +const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_NO_BUG_TESTS: [&str; 15] = [ "noirc_frontend_tests_check_trait_as_type_as_fn_parameter", "noirc_frontend_tests_check_trait_as_type_as_two_fn_parameters", "noirc_frontend_tests_enums_match_on_empty_enum", @@ -287,8 +285,6 @@ const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_NO_BUG_TESTS: [&str; 17] = [ "noirc_frontend_tests_aliases_type_alias_to_numeric_as_generic", "noirc_frontend_tests_aliases_type_alias_to_numeric_generic", "noirc_frontend_tests_traits_trait_bound_on_implementing_type", - "function_registry", - "regression_10887", // expands into global struct with private fields ]; const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_WITH_BUG_TESTS: [&str; 0] = []; diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_call_in_global/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_call_in_global/execute__tests__expanded.snap index f14a99900b7..7d83623a47d 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_call_in_global/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/trait_call_in_global/execute__tests__expanded.snap @@ -2,7 +2,7 @@ source: tooling/nargo_cli/tests/execute.rs expression: expanded_code --- -global s: BoundedVec = BoundedVec:: { len: 1, storage: [0x00, 0x00] }; +global s: BoundedVec = as From<[Field; 1]>>::from([0_Field]); fn main() { let _: BoundedVec = s; diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_no_bug/function_registry/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_no_bug/function_registry/execute__tests__expanded.snap index 0d25b7b83d2..1458a8fdedd 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_no_bug/function_registry/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_no_bug/function_registry/execute__tests__expanded.snap @@ -4,24 +4,8 @@ expression: expanded_code --- use std::{collections::umap::UHashMap, hash::{BuildHasherDefault, Hasher}}; -comptime mut global REGISTRY: UHashMap> = UHashMap::> { - _len: 1, - _table: @[ - std::collections::umap::Slot:: { - _value: @[panic(f"comptime value that cannot be represented with code")], - _is_present: true, - _key: panic(f"comptime value that cannot be represented with code"), - _is_deleted: false, - }, - std::collections::umap::Slot:: { - _value: @[], - _is_present: false, - _key: crate::mem::zeroed(), - _is_deleted: false, - }, - ], - _build_hasher: BuildHasherDefault:: {}, -}; +comptime mut global REGISTRY: UHashMap> = + UHashMap::>::default(); comptime fn add_to_registry( registry: &mut UHashMap>, diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_no_bug/regression_10887/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_no_bug/regression_10887/execute__tests__expanded.snap index e9e17d90290..f7850807339 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_no_bug/regression_10887/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_no_bug/regression_10887/execute__tests__expanded.snap @@ -2,7 +2,7 @@ source: tooling/nargo_cli/tests/execute.rs expression: expanded_code --- -global G_A: Option = Option:: { _is_some: false, _value: false }; +global G_A: Option = Option::::none(); fn main() { let a: Option = Option::::none(); From cb4f1d8b678a44b8ae8788c02854c5ab9c687b40 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 17:33:45 -0300 Subject: [PATCH 06/18] chore: drop stale entries from the `nargo expand` ignore lists The 15 `noirc_frontend_tests_*` entries referred to test programs that were generated from frontend unit tests and removed in #9706, so they no longer ignore anything. The remaining entries in the `compile_success_empty` list are all deliberate infrastructure limitations, not bugs, so its doc comment is updated to match. Co-Authored-By: Claude Fable 5 --- tooling/nargo_cli/build.rs | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/tooling/nargo_cli/build.rs b/tooling/nargo_cli/build.rs index 813196cabaa..e733ae7f44d 100644 --- a/tooling/nargo_cli/build.rs +++ b/tooling/nargo_cli/build.rs @@ -247,9 +247,9 @@ const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 12] = [ /// Tests for which we don't check that stdout matches the expected output. const TESTS_WITHOUT_STDOUT_CHECK: [&str; 0] = []; -/// These tests are ignored because of existing bugs in `nargo expand`. -/// As the bugs are fixed these tests should be removed from this list. -/// (some are ignored on purpose for the same reason as `IGNORED_NARGO_EXPAND_EXECUTION_TESTS`) +/// These tests are ignored on purpose for the same reason as +/// `IGNORED_NARGO_EXPAND_EXECUTION_TESTS`: making them work involves more complex test code +/// that might not be worth it. const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 5] = [ // There's no "src/main.nr" here so it's trickier to make this work "overlapping_dep_and_mod", @@ -268,24 +268,7 @@ const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_EMPTY_TESTS: [&str; 5] = [ /// These tests are ignored because of existing bugs in `nargo expand`. /// As the bugs are fixed these tests should be removed from this list. -const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_NO_BUG_TESTS: [&str; 15] = [ - "noirc_frontend_tests_check_trait_as_type_as_fn_parameter", - "noirc_frontend_tests_check_trait_as_type_as_two_fn_parameters", - "noirc_frontend_tests_enums_match_on_empty_enum", - "noirc_frontend_tests_traits_trait_alias_polymorphic_inheritance", - "noirc_frontend_tests_traits_trait_alias_single_member", - "noirc_frontend_tests_traits_trait_alias_two_members", - "noirc_frontend_tests_traits_trait_impl_with_where_clause_with_trait_with_associated_numeric", - "noirc_frontend_tests_traits_accesses_associated_type_inside_trait_impl_using_self", - "noirc_frontend_tests_traits_accesses_associated_type_inside_trait_using_self", - "noirc_frontend_tests_u32_globals_as_sizes_in_types", - // This creates a struct at comptime which, expanded, gives a visibility error - "noirc_frontend_tests_visibility_visibility_bug_inside_comptime", - "noirc_frontend_tests_aliases_identity_numeric_type_alias_works", - "noirc_frontend_tests_aliases_type_alias_to_numeric_as_generic", - "noirc_frontend_tests_aliases_type_alias_to_numeric_generic", - "noirc_frontend_tests_traits_trait_bound_on_implementing_type", -]; +const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_NO_BUG_TESTS: [&str; 0] = []; const IGNORED_NARGO_EXPAND_COMPILE_SUCCESS_WITH_BUG_TESTS: [&str; 0] = []; From 40c9aa80c672130a773441757dba87f4dc08b697 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 17:49:37 -0300 Subject: [PATCH 07/18] chore: un-ignore the `regression_9116` `nargo expand` execution test Its derive-macro-generated `Serialize` impls previously expanded with `<(resolved type) as Serialize>::N` associated-constant names; that was fixed when associated type names stopped embedding the "(resolved type)" placeholder, so the test passes now. Co-Authored-By: Claude Fable 5 --- tooling/nargo_cli/build.rs | 4 +- .../execute__tests__expanded.snap | 195 ++++++++++++++++++ 2 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 tooling/nargo_cli/tests/snapshots/execution_success/regression_9116/execute__tests__expanded.snap diff --git a/tooling/nargo_cli/build.rs b/tooling/nargo_cli/build.rs index e733ae7f44d..c3d7db143c7 100644 --- a/tooling/nargo_cli/build.rs +++ b/tooling/nargo_cli/build.rs @@ -216,7 +216,7 @@ const IGNORED_MINIMAL_EXECUTION_TESTS: [&str; 18] = [ /// might not be worth it. /// Others are ignored because of existing bugs in `nargo expand`. /// As the bugs are fixed these tests should be removed from this list. -const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 12] = [ +const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 11] = [ // `nargo expand` prints an associated-constant access by its bare name (e.g. `N`), // dropping the `Box::::` qualifier, so the expanded source no longer resolves. "comptime_resolve_associated_constant_scope", @@ -231,8 +231,6 @@ const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 12] = [ // There's no "src/main.nr" here so it's trickier to make this work "overlapping_dep_and_mod", // bug - "regression_9116", - // bug "regression_10466", // bug "trait_associated_constant", diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/regression_9116/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/regression_9116/execute__tests__expanded.snap new file mode 100644 index 00000000000..938d6bf113c --- /dev/null +++ b/tooling/nargo_cli/tests/snapshots/execution_success/regression_9116/execute__tests__expanded.snap @@ -0,0 +1,195 @@ +--- +source: tooling/nargo_cli/tests/execute.rs +expression: expanded_code +--- +use meta::{derive_deserialize, derive_serialize}; + +trait Serialize { + let N: u32; + + fn serialize(self) -> [Field; N]; +} + +impl Serialize for Field { + let N: u32 = 1; + + #[inline_always] + fn serialize(self) -> [Self; 1] { + [self] + } +} + +impl Serialize for [Field; M] { + let N: u32 = M; + + #[inline_always] + fn serialize(self) -> Self { + self + } +} + +trait Deserialize { + let N: u32; + + fn deserialize(fields: [Field; N]) -> Self; +} + +impl Deserialize for Field { + let N: u32 = 1; + + #[inline_always] + fn deserialize(fields: [Self; 1]) -> Self { + fields[0_u32] + } +} + +impl Deserialize for [Field; M] { + let N: u32 = M; + + #[inline_always] + fn deserialize(fields: Self) -> Self { + fields + } +} + +struct Foo { + x: Field, + y: [Field; 3], +} + +impl Eq for Foo { + fn eq(_self: Self, _other: Self) -> bool { + (_self.x == _other.x) & (_self.y == _other.y) + } +} + +impl Serialize for Foo { + let N: u32 = 4; + + #[inline_always] + fn serialize(self) -> [Field; 4] { + let mut result: [Field; 4] = [0_Field; 4]; + let mut offset: u32 = 0_u32; + let serialized_member: [Field; 1] = self.x.serialize(); + let serialized_member_len: u32 = ::N; + for i in 0_u32..serialized_member_len { + result[i + offset] = serialized_member[i]; + } + offset = offset + serialized_member_len; + let serialized_member: [Field; 3] = self.y.serialize(); + let serialized_member_len: u32 = <[Field; 3] as Serialize>::N; + for i in 0_u32..serialized_member_len { + result[i + offset] = serialized_member[i]; + } + offset = offset + serialized_member_len; + result + } +} + +impl Deserialize for Foo { + let N: u32 = 4; + + #[inline_always] + fn deserialize(serialized: [Field; 4]) -> Self { + let mut offset: u32 = 0_u32; + let mut member_fields: [Field; 1] = [0_Field; 1]; + for i in 0_u32..::N { + member_fields[i] = serialized[i + offset]; + } + let x: Field = ::deserialize(member_fields); + offset = offset + ::N; + let mut member_fields: [Field; 3] = [0_Field; 3]; + for i in 0_u32..<[Field; 3] as Deserialize>::N { + member_fields[i] = serialized[i + offset]; + } + let y: [Field; 3] = <[Field; 3] as Deserialize>::deserialize(member_fields); + offset = offset + <[Field; 3] as Deserialize>::N; + Self { x: x, y: y } + } +} + +fn main() { + let foo: Foo = Foo { x: 0_Field, y: [1_Field, 2_Field, 3_Field] }; + let serialized: [Field; 4] = foo.serialize(); + let deserialized: Foo = >::deserialize(serialized); + assert(foo == deserialized); +} + +mod meta { + pub comptime fn derive_serialize(s: TypeDefinition) -> Quoted { + let typ: Type = s.as_type(); + let nested_struct: (TypeDefinition, [Type]) = typ.as_data_type().unwrap(); + let params: [(Quoted, Type, Quoted)] = nested_struct.0.fields(nested_struct.1); + let components_of_definition_of_n: [Quoted] = params + .map(|(_, param_type, _): (Quoted, Type, Quoted)| -> Quoted { + quote { <$param_type as Serialize>::N } + }); + let right_hand_side_of_definition_of_n: Quoted = + components_of_definition_of_n.join(quote { + }); + let array_of_quotes_serializing_each_struct_member: [Quoted] = params + .map(|(param_name, param_type, _): (Quoted, Type, Quoted)| -> Quoted { + quote { + let serialized_member = self.$param_name.serialize(); + let serialized_member_len = < $param_type as Serialize > ::N; + for i in 0..serialized_member_len { + result[i + offset] = serialized_member[i]; + } + offset += serialized_member_len; + } + }); + let serialization_of_struct_members: Quoted = + array_of_quotes_serializing_each_struct_member.join(quote { }); + quote { + impl Serialize for $typ { + let N: u32 = $right_hand_side_of_definition_of_n; + #[inline_always]fn serialize(self) -> [Field; + Self::N] { + let mut result = [0; + _]; + let mut offset = 0; + $serialization_of_struct_members result + } + } + } + } + + pub(crate) comptime fn derive_deserialize(s: TypeDefinition) -> Quoted { + let typ: Type = s.as_type(); + let nested_struct: (TypeDefinition, [Type]) = typ.as_data_type().unwrap(); + let params: [(Quoted, Type, Quoted)] = nested_struct.0.fields(nested_struct.1); + let components_of_definition_of_n: [Quoted] = params + .map(|(_, param_type, _): (Quoted, Type, Quoted)| -> Quoted { + quote { <$param_type as Deserialize>::N } + }); + let serialized_len: Quoted = components_of_definition_of_n.join(quote { + }); + let array_of_quotes_deserializing_each_struct_member: [Quoted] = params + .map(|(param_name, param_type, _): (Quoted, Type, Quoted)| -> Quoted { + quote { + let mut member_fields = [0; + < $param_type as Deserialize > ::N]; + for i in 0.. < $param_type as Deserialize > ::N { + member_fields[i] = serialized[i + offset]; + } + let $param_name = < $param_type as Deserialize > ::deserialize(member_fields); + offset += < $param_type as Deserialize > ::N; + } + }); + let deserialization_of_struct_members: Quoted = + array_of_quotes_deserializing_each_struct_member.join(quote { }); + let struct_members: Quoted = params + .map(|(param_name, _, _): (Quoted, Type, Quoted)| -> Quoted quote { $param_name }) + .join(quote { , }); + quote { + impl Deserialize for $typ { + let N: u32 = $serialized_len; + #[inline_always]fn deserialize(serialized: [Field; + Self::N]) -> Self { + let mut offset = 0; + $deserialization_of_struct_members Self { + $struct_members + } + } + } + } + } +} From edf9a59b8930fd64c888b155393f328a6ac70785 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 18:09:35 -0300 Subject: [PATCH 08/18] fix: qualify associated constant references in `nargo expand` An expression reference to a trait impl's associated constant (e.g. `Self::N`) printed as the bare name `N`, which doesn't resolve when the expansion is recompiled. Print it as `Self::N` inside the defining impl and as `::N` elsewhere. This un-ignores the `trait_associated_constant` and `regression_10466` `nargo expand` integration tests. Co-Authored-By: Claude Fable 5 --- .../src/hir/printer/items/hir_def.rs | 28 +++++++++-- compiler/noirc_frontend/src/tests/expand.rs | 48 +++++++++++++++++++ tooling/nargo_cli/build.rs | 6 +-- .../execute__tests__expanded.snap | 34 +++++++++++++ .../execute__tests__expanded.snap | 25 ++++++++++ 5 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 tooling/nargo_cli/tests/snapshots/execution_success/regression_10466/execute__tests__expanded.snap create mode 100644 tooling/nargo_cli/tests/snapshots/execution_success/trait_associated_constant/execute__tests__expanded.snap diff --git a/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs b/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs index 5b3b2608bba..98eac6d12e0 100644 --- a/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs +++ b/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs @@ -958,9 +958,31 @@ impl ItemPrinter<'_, '_> { use_import, ); } - DefinitionKind::Local(..) - | DefinitionKind::NumericGeneric(..) - | DefinitionKind::AssociatedConstant(..) => { + DefinitionKind::AssociatedConstant(trait_impl_id, ref name) => { + // The bare name only resolves inside the trait impl that defines the constant, + // so qualify it: `Self::N` within that impl, `::N` elsewhere. + let trait_impl = self.interner.get_trait_implementation(trait_impl_id); + let trait_impl = trait_impl.borrow(); + if self.self_type.as_ref() == Some(&trait_impl.typ) { + self.push_str("Self::"); + } else { + self.push('<'); + self.show_type(&trait_impl.typ); + self.push_str(" as "); + let trait_ = self.interner.get_trait(trait_impl.trait_id); + self.show_reference_to_module_def_id( + ModuleDefId::TraitId(trait_impl.trait_id), + trait_.visibility, + true, + ); + let trait_generics = self.interner.get_trait_generics_for_impl(trait_impl_id); + let use_colons = false; + self.show_generic_types(&trait_generics.ordered, use_colons); + self.push_str(">::"); + } + self.push_str(name); + } + DefinitionKind::Local(..) | DefinitionKind::NumericGeneric(..) => { let name = self.interner.definition_name(ident.id); // The compiler uses '$' for some internal identifiers. diff --git a/compiler/noirc_frontend/src/tests/expand.rs b/compiler/noirc_frontend/src/tests/expand.rs index 569735e5c02..094505ddf30 100644 --- a/compiler/noirc_frontend/src/tests/expand.rs +++ b/compiler/noirc_frontend/src/tests/expand.rs @@ -739,3 +739,51 @@ fn expands_global_whose_value_has_private_fields_as_its_initializer_expression() } "); } + +#[test] +fn expands_associated_constant_reference_in_impl_method() { + let src = r#" + trait Trait { + let N: u32; + + fn foo() -> u32; + } + + struct Foo {} + + impl Trait for Foo { + let N: u32 = 30; + + fn foo() -> u32 { + Self::N + } + } + + fn main() { + let _ = Foo::foo(); + } + "#; + let expanded = assert_no_errors_and_to_string(src); + insta::assert_snapshot!(expanded, @r" + trait Trait { + let N: u32; + + fn foo() -> u32; + } + + struct Foo { + } + + impl Trait for Foo { + let N: u32 = 30; + + fn foo() -> u32 { + Self::N + } + } + + fn main() { + let _: u32 = Foo::foo(); + } + "); +} diff --git a/tooling/nargo_cli/build.rs b/tooling/nargo_cli/build.rs index c3d7db143c7..12954d4cacc 100644 --- a/tooling/nargo_cli/build.rs +++ b/tooling/nargo_cli/build.rs @@ -216,7 +216,7 @@ const IGNORED_MINIMAL_EXECUTION_TESTS: [&str; 18] = [ /// might not be worth it. /// Others are ignored because of existing bugs in `nargo expand`. /// As the bugs are fixed these tests should be removed from this list. -const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 11] = [ +const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 9] = [ // `nargo expand` prints an associated-constant access by its bare name (e.g. `N`), // dropping the `Box::::` qualifier, so the expanded source no longer resolves. "comptime_resolve_associated_constant_scope", @@ -230,10 +230,6 @@ const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 11] = [ "negative_associated_constants", // There's no "src/main.nr" here so it's trickier to make this work "overlapping_dep_and_mod", - // bug - "regression_10466", - // bug - "trait_associated_constant", // Globals evaluate to invalid utf-8 which don't display correctly in a source file "regression_12269", // There's no "src/main.nr" here so it's trickier to make this work diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/regression_10466/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/regression_10466/execute__tests__expanded.snap new file mode 100644 index 00000000000..a134c6d4eca --- /dev/null +++ b/tooling/nargo_cli/tests/snapshots/execution_success/regression_10466/execute__tests__expanded.snap @@ -0,0 +1,34 @@ +--- +source: tooling/nargo_cli/tests/execute.rs +expression: expanded_code +--- +pub trait Serialize { + let N: u32; + + fn serialize(self); +} + +impl Serialize for Field { + let N: u32 = 1; + + fn serialize(self) {} +} + +impl Serialize for [T; M] +where + T: Serialize, +{ + let N: u32 = M * ::N; + + fn serialize(self) { + println(Self::N); + println(T::N); + println(M); + assert((T::N * M) == Self::N); + } +} + +fn main() { + let nested_array: [[Field; 1]; 2] = [[1_Field], [2_Field]]; + let _: () = nested_array.serialize(); +} diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/trait_associated_constant/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/trait_associated_constant/execute__tests__expanded.snap new file mode 100644 index 00000000000..9ca93663659 --- /dev/null +++ b/tooling/nargo_cli/tests/snapshots/execution_success/trait_associated_constant/execute__tests__expanded.snap @@ -0,0 +1,25 @@ +--- +source: tooling/nargo_cli/tests/execute.rs +expression: expanded_code +--- +pub trait Trait { + let N: u32; + + fn foo() -> u32; +} + +struct Foo {} + +impl Trait for Foo { + let N: u32 = A + B; + + fn foo() -> u32 { + Self::N + } +} + +fn main() { + let x: u32 = Foo::<10, 20>::foo(); + assert(x == 30_u32); + () +} From 14d9fb80fd0f51e5eb69f0c2e4388ec0b5e1586c Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 18:12:38 -0300 Subject: [PATCH 09/18] fix: print numeric type aliases with their numeric type annotation `nargo expand` printed `type Double = N * 2;` without the `: u32` annotation, so the right-hand side was rejected as a type expression on recompile. Co-Authored-By: Claude Fable 5 --- .../noirc_frontend/src/hir/printer/mod.rs | 10 ++++++++- compiler/noirc_frontend/src/tests/expand.rs | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/compiler/noirc_frontend/src/hir/printer/mod.rs b/compiler/noirc_frontend/src/hir/printer/mod.rs index f3a4137e7d2..ce491bd03de 100644 --- a/compiler/noirc_frontend/src/hir/printer/mod.rs +++ b/compiler/noirc_frontend/src/hir/printer/mod.rs @@ -21,7 +21,9 @@ use crate::{ traits::{ResolvedTraitBound, TraitConstraint}, }, modules::{get_parent_module, module_def_id_is_visible, module_def_id_to_reference_id}, - node_interner::{FuncId, GlobalId, GlobalValue, NodeInterner, ReferenceId, TypeAliasId, TypeId}, + node_interner::{ + FuncId, GlobalId, GlobalValue, NodeInterner, ReferenceId, TypeAliasId, TypeId, + }, shared::Visibility, token::{FunctionAttributeKind, LocatedToken, SecondaryAttribute, SecondaryAttributeKind}, }; @@ -423,6 +425,12 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { self.push_str("type "); self.push_str(&type_alias.name.to_string()); self.show_generics(&type_alias.generics); + // A numeric type alias (`type Double: u32 = N * 2;`) must spell out its + // numeric type, otherwise the right-hand side is rejected as a type expression. + if let Kind::Numeric(numeric_type) = type_alias.typ.kind() { + self.push_str(": "); + self.show_type(&numeric_type); + } self.push_str(" = "); self.show_type(&type_alias.typ); self.push(';'); diff --git a/compiler/noirc_frontend/src/tests/expand.rs b/compiler/noirc_frontend/src/tests/expand.rs index 094505ddf30..d56e788bd14 100644 --- a/compiler/noirc_frontend/src/tests/expand.rs +++ b/compiler/noirc_frontend/src/tests/expand.rs @@ -787,3 +787,24 @@ fn expands_associated_constant_reference_in_impl_method() { } "); } + +#[test] +fn expands_numeric_type_alias_with_its_numeric_type() { + let src = r#" + type Double: u32 = N * 2; + + fn main() { + let arr: [Field; Double::<2>] = [0; 4]; + let _ = arr; + } + "#; + let expanded = assert_no_errors_and_to_string(src); + insta::assert_snapshot!(expanded, @r" + type Double: u32 = N * 2; + + fn main() { + let arr: [Field; 2 * 2] = [0_Field; 4]; + let _: [Field; 2 * 2] = arr; + } + "); +} From f09627e683320c5fad65754543111c9082b065e5 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 18:14:20 -0300 Subject: [PATCH 10/18] test: numeric type alias used as a value expands to the wrong ident Regression test for `nargo expand` printing `AliasN::<1>` as the bare name `N`, which silently rebinds to the global `N` at the use site. The accepted snapshot records the buggy output; the fix follows. Co-Authored-By: Claude Fable 5 --- compiler/noirc_frontend/src/tests/expand.rs | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/compiler/noirc_frontend/src/tests/expand.rs b/compiler/noirc_frontend/src/tests/expand.rs index d56e788bd14..8153cc2fe8b 100644 --- a/compiler/noirc_frontend/src/tests/expand.rs +++ b/compiler/noirc_frontend/src/tests/expand.rs @@ -808,3 +808,30 @@ fn expands_numeric_type_alias_with_its_numeric_type() { } "); } + +#[test] +fn expands_numeric_type_alias_used_as_value_with_turbofish() { + let src = r#" + type AliasN: u32 = N; + + global N: u32 = 100; + + fn main() { + let a: u32 = AliasN::<1>; + assert(a == 1); + assert(N == 100); + } + "#; + let expanded = assert_no_errors_and_to_string(src); + insta::assert_snapshot!(expanded, @r" + type AliasN: u32 = N; + + global N: u32 = 100; + + fn main() { + let a: u32 = N; + assert(a == 1_u32); + assert(N == 100_u32); + } + "); +} From a27a54db835d4a428f35f113ff457083c93318ed Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 18:23:53 -0300 Subject: [PATCH 11/18] fix: print instantiated numeric generics as their constant value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A numeric type alias's parameter used as a value (`AliasN::<1>`) printed as its bare name `N`, which doesn't resolve at the use site — or worse, silently resolves to an unrelated item with the same name (e.g. `global N`), changing the program's meaning. The definition's type variable is bound to the resolved value in this case, so print that value. Together with the numeric-type-annotation fix, this un-ignores the `numeric_type_alias` `nargo expand` integration test. Co-Authored-By: Claude Fable 5 --- .../src/hir/printer/items/hir_def.rs | 18 ++- compiler/noirc_frontend/src/tests/expand.rs | 2 +- tooling/nargo_cli/build.rs | 4 +- .../execute__tests__expanded.snap | 117 ++++++++++++++++++ 4 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias/execute__tests__expanded.snap diff --git a/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs b/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs index 98eac6d12e0..04e9a71e05b 100644 --- a/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs +++ b/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use itertools::Itertools; use crate::{ - NamedGeneric, Type, TypeBindings, + NamedGeneric, Type, TypeBinding, TypeBindings, ast::{ItemVisibility, UnaryOp}, hir::def_map::ModuleDefId, hir_def::{ @@ -982,7 +982,21 @@ impl ItemPrinter<'_, '_> { } self.push_str(name); } - DefinitionKind::Local(..) | DefinitionKind::NumericGeneric(..) => { + DefinitionKind::NumericGeneric(ref type_var, _) => { + // When a numeric type alias's parameter is used as a value (`AliasN::<1>`), + // the definition's type variable is bound to the resolved value and the bare + // name doesn't resolve at the use site (or worse, resolves to something else + // with the same name). Print the value instead. + if let TypeBinding::Bound(binding) = &*type_var.borrow() + && let Type::Constant(constant) = binding.follow_bindings() + { + self.push_str(&constant.to_string()); + return; + } + let name = self.interner.definition_name(ident.id); + self.push_str(name); + } + DefinitionKind::Local(..) => { let name = self.interner.definition_name(ident.id); // The compiler uses '$' for some internal identifiers. diff --git a/compiler/noirc_frontend/src/tests/expand.rs b/compiler/noirc_frontend/src/tests/expand.rs index 8153cc2fe8b..8c632848de9 100644 --- a/compiler/noirc_frontend/src/tests/expand.rs +++ b/compiler/noirc_frontend/src/tests/expand.rs @@ -829,7 +829,7 @@ fn expands_numeric_type_alias_used_as_value_with_turbofish() { global N: u32 = 100; fn main() { - let a: u32 = N; + let a: u32 = 1; assert(a == 1_u32); assert(N == 100_u32); } diff --git a/tooling/nargo_cli/build.rs b/tooling/nargo_cli/build.rs index 12954d4cacc..97ee753f637 100644 --- a/tooling/nargo_cli/build.rs +++ b/tooling/nargo_cli/build.rs @@ -216,7 +216,7 @@ const IGNORED_MINIMAL_EXECUTION_TESTS: [&str; 18] = [ /// might not be worth it. /// Others are ignored because of existing bugs in `nargo expand`. /// As the bugs are fixed these tests should be removed from this list. -const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 9] = [ +const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 8] = [ // `nargo expand` prints an associated-constant access by its bare name (e.g. `N`), // dropping the `Box::::` qualifier, so the expanded source no longer resolves. "comptime_resolve_associated_constant_scope", @@ -225,8 +225,6 @@ const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 9] = [ "custom_entry", // There's no "src/main.nr" here so it's trickier to make this work "diamond_deps_0", - // bug - "numeric_type_alias", "negative_associated_constants", // There's no "src/main.nr" here so it's trickier to make this work "overlapping_dep_and_mod", diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias/execute__tests__expanded.snap new file mode 100644 index 00000000000..35b2685a38e --- /dev/null +++ b/tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias/execute__tests__expanded.snap @@ -0,0 +1,117 @@ +--- +source: tooling/nargo_cli/tests/execute.rs +expression: expanded_code +--- +type Double: u32 = N * 2; + +type Quadruple: u32 = (N * 2) + (N * 2); + +type Mixed: u32 = N * ((N * 2) + 3); + +global One: u32 = 1; + +type Two: u32 = 2; + +type Three: u32 = 3; + +global N: u32 = 100; + +type AliasN: u32 = N; + +fn main(x: Field) { + let arr: [Field; (2 * 2) + (2 * 2)] = [0_Field; 8]; + assert(arr.len() == 8_u32); + let b: [u32; 12] = quadruple_array::<3>(); + assert(b[0_u32] == 0_u32); + let c: [u32; 14] = mixed_array::<2>(); + assert(c[0_u32] == 0_u32); + let two: u32 = One + One; + assert(two == 2_u32); + let three: u32 = (One + One) + One; + assert(three == 3_u32); + let arr2: [Field; 2] = [0_Field; 2]; + assert(arr2.len() == 2_u32); + let a: u32 = 1; + assert(a == 1_u32); + assert(N == 100_u32); + let b: u32 = 5; + assert(b == 5_u32); + let mut m: Matrix<3, 4> = new_matrix::<3, 4>(); + m.elements[(3_u32 * 2_u32) + 3_u32] = 10_Field; + m.elements[(3_u32 * 2_u32) + (x as u32)] = 5_Field + x; + assert(equal(m, m)); + let b: Matrix<4, 3> = transpose(m); + assert(b.elements != m.elements); + assert(trace(b) != trace(m)); + assert(sum(b) == sum(m)); + let a: BoundedVec = test_2::<4>(x); + assert(a.len() == 1_u32); +} + +fn quadruple_array() -> [u32; (N * 2) + (N * 2)] { + let mut a: [u32; (N * 2) + (N * 2)] = [0_u32; (N * 2) + (N * 2)]; + for i in 0_u32..(N * 2_u32) + (N * 2_u32) { + a[i] = i; + } + a +} + +fn mixed_array() -> [u32; N * ((N * 2) + 3)] { + let mut a: [u32; N * ((N * 2) + 3)] = [0_u32; N * ((N * 2) + 3)]; + for i in 0_u32..N * ((N * 2_u32) + 3_u32) { + a[i] = i; + } + a +} + +fn test_2(x: Field) -> BoundedVec> { + let mut a: BoundedVec = BoundedVec::::new(); + a.push(x); + a +} + +type MSize: u32 = N * M; + +struct Matrix { + elements: [Field; N * M], +} + +fn new_matrix() -> Matrix { + let mut a: [Field; N * M] = [0_Field; N * M]; + Matrix:: { elements: a } +} + +fn trace(A: Matrix) -> Field { + let n: u32 = if N > M { M } else { N }; + let mut s: Field = 0_Field; + for i in 0_u32..n { + s = s + A.elements[(i * N) + i]; + } + s +} + +fn sum(A: Matrix) -> Field { + let mut s: Field = 0_Field; + for i in 0_u32..N * M { + s = s + A.elements[i]; + } + s +} + +fn equal(A: Matrix, B: Matrix) -> bool { + let mut s: bool = true; + for i in 0_u32..N * M { + s = s | (A.elements[i] == B.elements[i]); + } + s +} + +fn transpose(a: Matrix) -> Matrix { + let mut b: Matrix = new_matrix::(); + for i in 0_u32..N { + for j in 0_u32..M { + b.elements[(j * N) + i] = a.elements[(i * M) + j]; + } + } + b +} From c1d76c65527323c53389a134f55e06d218a7c1d8 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 18:37:11 -0300 Subject: [PATCH 12/18] fix: print associated constant declarations that involve the self type Two problems with `let N: = ;` inside a trait impl: - When the numeric type is the impl's self type (`impl Foo for i32`), it printed as `let N: Self = ...`, and `Self` doesn't resolve in an associated constant's type annotation. - The value printed as an unsuffixed literal, which is checked as `u32` in this position, rejecting constants of other numeric types (e.g. `-12345` for an `i32` constant). Constants now print with their type suffix (`-12345_i32`). This un-ignores the `negative_associated_constants` `nargo expand` integration test. Co-Authored-By: Claude Fable 5 --- .../noirc_frontend/src/hir/printer/mod.rs | 15 ++++++- compiler/noirc_frontend/src/tests/expand.rs | 41 ++++++++++++++++++- tooling/nargo_cli/build.rs | 3 +- .../execute__tests__expanded.snap | 2 +- .../execute__tests__expanded.snap | 2 +- .../execute__tests__expanded.snap | 4 +- .../execute__tests__expanded.snap | 2 +- .../execute__tests__expanded.snap | 2 +- .../execute__tests__expanded.snap | 2 +- .../execute__tests__expanded.snap | 2 +- .../execute__tests__expanded.snap | 2 +- .../serialize_1/execute__tests__expanded.snap | 2 +- .../serialize_2/execute__tests__expanded.snap | 2 +- .../serialize_3/execute__tests__expanded.snap | 2 +- .../serialize_4/execute__tests__expanded.snap | 2 +- .../serialize_5/execute__tests__expanded.snap | 8 ++-- .../execute__tests__expanded.snap | 2 +- .../execute__tests__expanded.snap | 28 +++++++++++++ .../execute__tests__expanded.snap | 2 +- .../execute__tests__expanded.snap | 6 +-- .../execute__tests__expanded.snap | 4 +- 21 files changed, 107 insertions(+), 28 deletions(-) create mode 100644 tooling/nargo_cli/tests/snapshots/execution_success/negative_associated_constants/execute__tests__expanded.snap diff --git a/compiler/noirc_frontend/src/hir/printer/mod.rs b/compiler/noirc_frontend/src/hir/printer/mod.rs index ce491bd03de..887ce38936e 100644 --- a/compiler/noirc_frontend/src/hir/printer/mod.rs +++ b/compiler/noirc_frontend/src/hir/printer/mod.rs @@ -575,14 +575,27 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { self.push_str("let "); self.push_str(&named_type.name.to_string()); self.push_str(": "); + // `Self` doesn't resolve in an associated constant's numeric type annotation, + // so print the type itself even when it's the impl's self type. + let self_type = self.self_type.take(); self.show_type(&numeric_type); self.push_str(" = "); + // An unsuffixed literal in this position is checked as `u32`, so a constant + // of any other numeric type needs its type suffix. + if let Type::Constant(constant) = named_type.typ.follow_bindings() { + self.push_str(&constant.to_string()); + self.push('_'); + self.show_type(&numeric_type); + } else { + self.show_type(&named_type.typ); + } + self.self_type = self_type; } else { self.push_str("type "); self.push_str(&named_type.name.to_string()); self.push_str(" = "); + self.show_type(&named_type.typ); } - self.show_type(&named_type.typ); self.push_str(";"); printed_item = true; diff --git a/compiler/noirc_frontend/src/tests/expand.rs b/compiler/noirc_frontend/src/tests/expand.rs index 8c632848de9..36dcb525ab8 100644 --- a/compiler/noirc_frontend/src/tests/expand.rs +++ b/compiler/noirc_frontend/src/tests/expand.rs @@ -775,7 +775,7 @@ fn expands_associated_constant_reference_in_impl_method() { } impl Trait for Foo { - let N: u32 = 30; + let N: u32 = 30_u32; fn foo() -> u32 { Self::N @@ -835,3 +835,42 @@ fn expands_numeric_type_alias_used_as_value_with_turbofish() { } "); } + +#[test] +fn expands_associated_constant_over_self_type_with_concrete_annotation() { + let src = r#" + trait Foo { + let N: i32; + + fn n() -> i32 { + Self::N + } + } + + impl Foo for i32 { + let N: i32 = -12345i32; + } + + fn main() { + let _ = i32::n(); + } + "#; + let expanded = assert_no_errors_and_to_string(src); + insta::assert_snapshot!(expanded, @r" + trait Foo { + let N: i32; + + fn n() -> i32 { + Self::N + } + } + + impl Foo for i32 { + let N: i32 = -12345_i32; + } + + fn main() { + let _: i32 = ::n(); + } + "); +} diff --git a/tooling/nargo_cli/build.rs b/tooling/nargo_cli/build.rs index 97ee753f637..ee3bcfcb229 100644 --- a/tooling/nargo_cli/build.rs +++ b/tooling/nargo_cli/build.rs @@ -216,7 +216,7 @@ const IGNORED_MINIMAL_EXECUTION_TESTS: [&str; 18] = [ /// might not be worth it. /// Others are ignored because of existing bugs in `nargo expand`. /// As the bugs are fixed these tests should be removed from this list. -const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 8] = [ +const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 7] = [ // `nargo expand` prints an associated-constant access by its bare name (e.g. `N`), // dropping the `Box::::` qualifier, so the expanded source no longer resolves. "comptime_resolve_associated_constant_scope", @@ -225,7 +225,6 @@ const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 8] = [ "custom_entry", // There's no "src/main.nr" here so it's trickier to make this work "diamond_deps_0", - "negative_associated_constants", // There's no "src/main.nr" here so it's trickier to make this work "overlapping_dep_and_mod", // Globals evaluate to invalid utf-8 which don't display correctly in a source file diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/as_trait_path_type_expression/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/as_trait_path_type_expression/execute__tests__expanded.snap index a289f2edd41..e3e60148875 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/as_trait_path_type_expression/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/as_trait_path_type_expression/execute__tests__expanded.snap @@ -9,7 +9,7 @@ trait Trait { } impl Trait for Field { - let N: u32 = 10; + let N: u32 = 10_u32; } fn main() { diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/associated_constants_in_as_trait_expr/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/associated_constants_in_as_trait_expr/execute__tests__expanded.snap index 45f97f35aab..a53e2abbba8 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/associated_constants_in_as_trait_expr/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/associated_constants_in_as_trait_expr/execute__tests__expanded.snap @@ -7,7 +7,7 @@ trait Serialize { } impl Serialize for Field { - let N: u32 = 1; + let N: u32 = 1_u32; } fn main() { diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/nested_trait_associated_type_regression_8252/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/nested_trait_associated_type_regression_8252/execute__tests__expanded.snap index 217d01686ee..f079e9129ce 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/nested_trait_associated_type_regression_8252/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/nested_trait_associated_type_regression_8252/execute__tests__expanded.snap @@ -13,13 +13,13 @@ trait TraitWithAssociatedConstant { struct Foo {} impl TraitWithAssociatedConstant for Foo { - let N: u32 = 42; + let N: u32 = 42_u32; } struct Bar {} impl TraitWithAssociatedConstant for Bar { - let N: u32 = 43; + let N: u32 = 43_u32; } struct Wrapper { diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_10747_associated_constant/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_10747_associated_constant/execute__tests__expanded.snap index 2c5e89d4ea7..567e35b9532 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_10747_associated_constant/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_10747_associated_constant/execute__tests__expanded.snap @@ -7,7 +7,7 @@ pub trait Ser { } impl Ser for Field { - let N: u32 = 1; + let N: u32 = 1_u32; } impl Ser for [T; M] diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_10813/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_10813/execute__tests__expanded.snap index e92992f6114..aba6c97e16b 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_10813/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_10813/execute__tests__expanded.snap @@ -13,7 +13,7 @@ pub trait Foo { } impl Foo for () { - let Baz: u32 = 0; + let Baz: u32 = 0_u32; type Bar = Self; } diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_12659_associated_constant_projection_in_struct_field/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_12659_associated_constant_projection_in_struct_field/execute__tests__expanded.snap index c8c7bf4389b..e6a2a49aca5 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_12659_associated_constant_projection_in_struct_field/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_12659_associated_constant_projection_in_struct_field/execute__tests__expanded.snap @@ -9,7 +9,7 @@ pub trait HasSize { pub struct PublicMutable {} impl HasSize for PublicMutable { - let N: u32 = 1; + let N: u32 = 1_u32; } pub struct Wrapper { diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_9245/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_9245/execute__tests__expanded.snap index 600ed964bac..4b973a0ee13 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_9245/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_9245/execute__tests__expanded.snap @@ -9,7 +9,7 @@ pub trait Deserialize { } impl Deserialize for Field { - let N: u32 = 1; + let N: u32 = 1_u32; fn deserialize(fields: [Self; 1]) -> Self { fields[0_u32] diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_9248/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_9248/execute__tests__expanded.snap index beb2d3b55b6..789173efc90 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_9248/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/regression_9248/execute__tests__expanded.snap @@ -9,7 +9,7 @@ pub trait Deserialize { } impl Deserialize for Field { - let N: u32 = 1; + let N: u32 = 1_u32; fn deserialize(fields: [Self; 1]) -> Self { fields[0_u32] diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_1/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_1/execute__tests__expanded.snap index 3328d1e1f05..8c8dc2c814a 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_1/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_1/execute__tests__expanded.snap @@ -60,7 +60,7 @@ where } impl Serialize for Field { - let Size: u32 = 1; + let Size: u32 = 1_u32; fn serialize(self) -> [Self; 1] { [self] diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_2/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_2/execute__tests__expanded.snap index 163de61e9fd..05b74171081 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_2/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_2/execute__tests__expanded.snap @@ -9,7 +9,7 @@ trait Serialize { } impl Serialize for Field { - let Size: u32 = 1; + let Size: u32 = 1_u32; fn serialize(self) {} } diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_3/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_3/execute__tests__expanded.snap index a92f6580693..4aedb792cd3 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_3/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_3/execute__tests__expanded.snap @@ -33,7 +33,7 @@ where } impl Serialize for Field { - let Size: u32 = 1; + let Size: u32 = 1_u32; fn serialize(self) -> [Self; 1] { [self] diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_4/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_4/execute__tests__expanded.snap index c1df8155f22..2c4eb0a9a3b 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_4/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_4/execute__tests__expanded.snap @@ -60,7 +60,7 @@ where } impl Serialize for Field { - let Size: u32 = 1; + let Size: u32 = 1_u32; fn serialize(self) -> [Self; 1] { [self] diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_5/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_5/execute__tests__expanded.snap index 3d7f71c7d04..7698d9ca6d1 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_5/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_empty/serialize_5/execute__tests__expanded.snap @@ -9,7 +9,7 @@ trait Serialize { } impl Serialize for Field { - let N: u32 = 1; + let N: u32 = 1_u32; fn serialize(self) -> [Self; 1] { [self] @@ -21,7 +21,7 @@ pub struct Foo { } impl Serialize for Foo { - let N: u32 = 1; + let N: u32 = 1_u32; fn serialize(self) -> [Field; 1] { [0_Field; 1] @@ -31,7 +31,7 @@ impl Serialize for Foo { pub struct Bar {} impl Serialize for Bar { - let N: u32 = 1; + let N: u32 = 1_u32; fn serialize(self) -> [Field; 1] { [1_Field] @@ -43,7 +43,7 @@ pub struct Baz { } impl Serialize for Baz { - let N: u32 = 1; + let N: u32 = 1_u32; fn serialize(self) -> [Field; 1] { [0_Field; 1] diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/associated_constant_as_array_length/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/associated_constant_as_array_length/execute__tests__expanded.snap index 7beb393e16c..14fa0e15ae7 100644 --- a/tooling/nargo_cli/tests/snapshots/execution_success/associated_constant_as_array_length/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/execution_success/associated_constant_as_array_length/execute__tests__expanded.snap @@ -9,7 +9,7 @@ trait HasSize { struct Packet {} impl HasSize for Packet { - let SIZE: u32 = 4; + let SIZE: u32 = 4_u32; } global N: u32 = 4; diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/negative_associated_constants/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/negative_associated_constants/execute__tests__expanded.snap new file mode 100644 index 00000000000..96a467a4dc4 --- /dev/null +++ b/tooling/nargo_cli/tests/snapshots/execution_success/negative_associated_constants/execute__tests__expanded.snap @@ -0,0 +1,28 @@ +--- +source: tooling/nargo_cli/tests/execute.rs +expression: expanded_code +--- +trait Foo { + let N: i32; + + let M: i32; + + fn n() -> i32 { + Self::N + } + + fn m() -> i32 { + Self::M + } +} + +impl Foo for i32 { + let N: i32 = -12345_i32; + + let M: i32 = -7655_i32; +} + +fn main() { + println(::n()); + println(::m()); +} diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/regression_10466/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/regression_10466/execute__tests__expanded.snap index a134c6d4eca..3a939cf77ab 100644 --- a/tooling/nargo_cli/tests/snapshots/execution_success/regression_10466/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/execution_success/regression_10466/execute__tests__expanded.snap @@ -9,7 +9,7 @@ pub trait Serialize { } impl Serialize for Field { - let N: u32 = 1; + let N: u32 = 1_u32; fn serialize(self) {} } diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/regression_8210/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/regression_8210/execute__tests__expanded.snap index 4b1c7a9e1de..918ce6d6730 100644 --- a/tooling/nargo_cli/tests/snapshots/execution_success/regression_8210/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/execution_success/regression_8210/execute__tests__expanded.snap @@ -19,11 +19,11 @@ trait Foo { } impl Foo for Field { - let BAR: u32 = 254; + let BAR: u32 = 254_u32; } impl Foo for u8 { - let BAR: u32 = 7; + let BAR: u32 = 7_u32; } struct Wrapper { @@ -31,7 +31,7 @@ struct Wrapper { } impl Foo for Wrapper { - let BAR: u32 = 11; + let BAR: u32 = 11_u32; } fn main() { diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/regression_9116/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/regression_9116/execute__tests__expanded.snap index 938d6bf113c..b9f6ea4181f 100644 --- a/tooling/nargo_cli/tests/snapshots/execution_success/regression_9116/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/execution_success/regression_9116/execute__tests__expanded.snap @@ -11,7 +11,7 @@ trait Serialize { } impl Serialize for Field { - let N: u32 = 1; + let N: u32 = 1_u32; #[inline_always] fn serialize(self) -> [Self; 1] { @@ -35,7 +35,7 @@ trait Deserialize { } impl Deserialize for Field { - let N: u32 = 1; + let N: u32 = 1_u32; #[inline_always] fn deserialize(fields: [Self; 1]) -> Self { From 0dc6c5e16397a85d63d4504cb5708586d81e8c3f Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 18:42:51 -0300 Subject: [PATCH 13/18] fix: don't print non-UTF-8 string values lossily in `nargo expand` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `str` global whose bytes aren't valid UTF-8 (e.g. built with `as_str_unchecked`) printed via `from_utf8_lossy`, replacing invalid bytes with U+FFFD and changing the string's content and byte length — the expansion then failed to recompile with a length mismatch. Treat such values as unrepresentable so the global prints its initializer expression instead. This un-ignores the `regression_12269` `nargo expand` integration test. Co-Authored-By: Claude Fable 5 --- compiler/noirc_frontend/src/hir/printer/mod.rs | 6 ++++-- tooling/nargo_cli/build.rs | 4 +--- .../regression_12269/execute__tests__expanded.snap | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler/noirc_frontend/src/hir/printer/mod.rs b/compiler/noirc_frontend/src/hir/printer/mod.rs index 887ce38936e..a33366da730 100644 --- a/compiler/noirc_frontend/src/hir/printer/mod.rs +++ b/compiler/noirc_frontend/src/hir/printer/mod.rs @@ -676,13 +676,15 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { Value::Unit | Value::Bool(_) | Value::Integer(_) - | Value::String(_) - | Value::CtString(_) | Value::Function(..) | Value::Closure(_) | Value::Quoted(_) | Value::Zeroed(_) => true, + // A string that isn't valid UTF-8 can only be printed lossily, which changes both + // its content and its length. + Value::String(bytes) | Value::CtString(bytes) => std::str::from_utf8(bytes).is_ok(), + Value::FormatString(fragments, ..) => fragments.iter().all(|fragment| match fragment { FormatStringFragment::String(_) => true, FormatStringFragment::Value { value, .. } => self.value_is_representable(value), diff --git a/tooling/nargo_cli/build.rs b/tooling/nargo_cli/build.rs index ee3bcfcb229..aef97f9e0e0 100644 --- a/tooling/nargo_cli/build.rs +++ b/tooling/nargo_cli/build.rs @@ -216,7 +216,7 @@ const IGNORED_MINIMAL_EXECUTION_TESTS: [&str; 18] = [ /// might not be worth it. /// Others are ignored because of existing bugs in `nargo expand`. /// As the bugs are fixed these tests should be removed from this list. -const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 7] = [ +const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 6] = [ // `nargo expand` prints an associated-constant access by its bare name (e.g. `N`), // dropping the `Box::::` qualifier, so the expanded source no longer resolves. "comptime_resolve_associated_constant_scope", @@ -227,8 +227,6 @@ const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 7] = [ "diamond_deps_0", // There's no "src/main.nr" here so it's trickier to make this work "overlapping_dep_and_mod", - // Globals evaluate to invalid utf-8 which don't display correctly in a source file - "regression_12269", // There's no "src/main.nr" here so it's trickier to make this work "workspace", // There's no "src/main.nr" here so it's trickier to make this work diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/regression_12269/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/regression_12269/execute__tests__expanded.snap index 9cd4c7020d5..755c5a3216e 100644 --- a/tooling/nargo_cli/tests/snapshots/execution_success/regression_12269/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/execution_success/regression_12269/execute__tests__expanded.snap @@ -4,7 +4,7 @@ expression: expanded_code --- global BROKEN_BYTES: [u8; 4] = [102, 111, 128, 111]; -global S: str<4> = "fo�o"; +global S: str<4> = BROKEN_BYTES.as_str_unchecked(); fn main() { println(S); From aba823776e3e8b87213ce70627624b9bcb2f227d Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 18:49:41 -0300 Subject: [PATCH 14/18] fix: print an associated constant's value when its trait isn't visible A comptime `Expr::resolve` running in another module's scope can splice in a reference to an associated constant of a trait that is private to that module. The qualified `::N` form then names a trait that isn't visible from the printing module and fails to recompile. Since the constant's value is compile-time known, print the value in that case. This un-ignores the `comptime_resolve_associated_constant_scope` `nargo expand` integration test. Co-Authored-By: Claude Fable 5 --- .../src/hir/printer/items/hir_def.rs | 33 ++++++++++++++++++- tooling/nargo_cli/build.rs | 5 +-- .../execute__tests__expanded.snap | 32 ++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 tooling/nargo_cli/tests/snapshots/execution_success/comptime_resolve_associated_constant_scope/execute__tests__expanded.snap diff --git a/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs b/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs index 04e9a71e05b..d61a641d3a5 100644 --- a/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs +++ b/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs @@ -3,9 +3,10 @@ use std::borrow::Cow; use itertools::Itertools; use crate::{ - NamedGeneric, Type, TypeBinding, TypeBindings, + Kind, NamedGeneric, Type, TypeBinding, TypeBindings, ast::{ItemVisibility, UnaryOp}, hir::def_map::ModuleDefId, + modules::module_def_id_is_visible, hir_def::{ expr::{ Constructor, HirArrayLiteral, HirBlockExpression, HirCallExpression, HirExpression, @@ -966,6 +967,36 @@ impl ItemPrinter<'_, '_> { if self.self_type.as_ref() == Some(&trait_impl.typ) { self.push_str("Self::"); } else { + // The qualified form `::N` names the trait; when the trait + // isn't visible from this module (e.g. the reference was spliced in by a + // comptime `Expr::resolve` in another module's scope), print the constant's + // compile-time value instead. + let module_def_id = ModuleDefId::TraitId(trait_impl.trait_id); + let trait_ = self.interner.get_trait(trait_impl.trait_id); + let trait_is_visible = module_def_id_is_visible( + module_def_id, + self.module_id, + trait_.visibility, + None, + self.interner, + self.def_maps, + self.dependencies, + ) || !self.interner.get_reexports(module_def_id).is_empty(); + if !trait_is_visible + && let Some(named_type) = self + .interner + .get_associated_types_for_impl(trait_impl_id) + .iter() + .find(|named_type| named_type.name.as_str() == name) + && let Type::Constant(constant) = named_type.typ.follow_bindings() + { + self.push_str(&constant.to_string()); + if let Kind::Numeric(numeric_type) = named_type.typ.kind() { + self.push('_'); + self.show_type(&numeric_type); + } + return; + } self.push('<'); self.show_type(&trait_impl.typ); self.push_str(" as "); diff --git a/tooling/nargo_cli/build.rs b/tooling/nargo_cli/build.rs index aef97f9e0e0..447ebfce00e 100644 --- a/tooling/nargo_cli/build.rs +++ b/tooling/nargo_cli/build.rs @@ -216,10 +216,7 @@ const IGNORED_MINIMAL_EXECUTION_TESTS: [&str; 18] = [ /// might not be worth it. /// Others are ignored because of existing bugs in `nargo expand`. /// As the bugs are fixed these tests should be removed from this list. -const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 6] = [ - // `nargo expand` prints an associated-constant access by its bare name (e.g. `N`), - // dropping the `Box::::` qualifier, so the expanded source no longer resolves. - "comptime_resolve_associated_constant_scope", +const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 5] = [ // There's nothing special about this program but making it work with a custom entry would involve // having to parse the Nargo.toml file, etc., which is not worth it "custom_entry", diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/comptime_resolve_associated_constant_scope/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/comptime_resolve_associated_constant_scope/execute__tests__expanded.snap new file mode 100644 index 00000000000..964d80b8bcb --- /dev/null +++ b/tooling/nargo_cli/tests/snapshots/execution_success/comptime_resolve_associated_constant_scope/execute__tests__expanded.snap @@ -0,0 +1,32 @@ +--- +source: tooling/nargo_cli/tests/execute.rs +expression: expanded_code +--- +mod victim { + pub struct Box { + pub value: T, + } + + impl LocalConst for Box { + let N: u32 = 1_u32; + } + + impl crate::CallerConst for Box { + let N: u32 = 2_u32; + } + + trait LocalConst { + let N: u32; + } + + pub fn scope_anchor() {} +} + +trait CallerConst { + let N: u32; +} + +fn main() { + let selected: u32 = { 1_u32 }; + assert(selected == 1_u32); +} From 7ab33bae8a18121e7224559d6fec1c51d162fb6f Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 31 Jul 2026 18:50:15 -0300 Subject: [PATCH 15/18] chore: drop the stale bug note on `IGNORED_NARGO_EXPAND_EXECUTION_TESTS` Every remaining entry is a deliberate test-infrastructure limitation. Co-Authored-By: Claude Fable 5 --- .../src/hir/printer/items/hir_def.rs | 21 ++++++++++--------- tooling/nargo_cli/build.rs | 2 -- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs b/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs index d61a641d3a5..f9795a6802d 100644 --- a/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs +++ b/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs @@ -6,7 +6,6 @@ use crate::{ Kind, NamedGeneric, Type, TypeBinding, TypeBindings, ast::{ItemVisibility, UnaryOp}, hir::def_map::ModuleDefId, - modules::module_def_id_is_visible, hir_def::{ expr::{ Constructor, HirArrayLiteral, HirBlockExpression, HirCallExpression, HirExpression, @@ -14,6 +13,7 @@ use crate::{ }, stmt::{HirLValue, HirPattern, HirStatement}, }, + modules::module_def_id_is_visible, node_interner::{DefinitionId, DefinitionKind, ExprId, FuncId, StmtId}, token::FmtStrFragment, }; @@ -973,15 +973,16 @@ impl ItemPrinter<'_, '_> { // compile-time value instead. let module_def_id = ModuleDefId::TraitId(trait_impl.trait_id); let trait_ = self.interner.get_trait(trait_impl.trait_id); - let trait_is_visible = module_def_id_is_visible( - module_def_id, - self.module_id, - trait_.visibility, - None, - self.interner, - self.def_maps, - self.dependencies, - ) || !self.interner.get_reexports(module_def_id).is_empty(); + let trait_is_visible = + module_def_id_is_visible( + module_def_id, + self.module_id, + trait_.visibility, + None, + self.interner, + self.def_maps, + self.dependencies, + ) || !self.interner.get_reexports(module_def_id).is_empty(); if !trait_is_visible && let Some(named_type) = self .interner diff --git a/tooling/nargo_cli/build.rs b/tooling/nargo_cli/build.rs index 447ebfce00e..698f5cc3ce8 100644 --- a/tooling/nargo_cli/build.rs +++ b/tooling/nargo_cli/build.rs @@ -214,8 +214,6 @@ const IGNORED_MINIMAL_EXECUTION_TESTS: [&str; 18] = [ /// These tests are ignored because making them work involves a more complex test code that /// might not be worth it. -/// Others are ignored because of existing bugs in `nargo expand`. -/// As the bugs are fixed these tests should be removed from this list. const IGNORED_NARGO_EXPAND_EXECUTION_TESTS: [&str; 5] = [ // There's nothing special about this program but making it work with a custom entry would involve // having to parse the Nargo.toml file, etc., which is not worth it From fe28b5fabe5eff0ba206af43cc431936df075961 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Mon, 3 Aug 2026 18:23:13 -0300 Subject: [PATCH 16/18] fix: suffix instantiated numeric generics with their numeric type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An instantiated numeric generic printed as a bare integer literal, so wherever the surrounding context didn't pin the type the literal was inferred as a different one and could silently select a different trait impl — a wrong program that still compiles. Print the constant with the numeric type that the definition already carries (`200_u8`). Adds the `numeric_type_alias_inference` execution test, whose expansion picks the wrong impl at runtime without the suffix. Co-Authored-By: Claude Fable 5 --- .../src/hir/printer/items/hir_def.rs | 7 ++-- compiler/noirc_frontend/src/tests/expand.rs | 2 +- .../numeric_type_alias_inference/Nargo.toml | 5 +++ .../numeric_type_alias_inference/Prover.toml | 1 + .../numeric_type_alias_inference/src/main.nr | 32 ++++++++++++++++++ .../execute__tests__expanded.snap | 4 +-- .../execute__tests__expanded.snap | 33 +++++++++++++++++++ .../execute__tests__stdout.snap | 5 +++ 8 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 test_programs/execution_success/numeric_type_alias_inference/Nargo.toml create mode 100644 test_programs/execution_success/numeric_type_alias_inference/Prover.toml create mode 100644 test_programs/execution_success/numeric_type_alias_inference/src/main.nr create mode 100644 tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias_inference/execute__tests__expanded.snap create mode 100644 tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias_inference/execute__tests__stdout.snap diff --git a/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs b/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs index f9795a6802d..64b1592fade 100644 --- a/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs +++ b/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs @@ -1014,15 +1014,18 @@ impl ItemPrinter<'_, '_> { } self.push_str(name); } - DefinitionKind::NumericGeneric(ref type_var, _) => { + DefinitionKind::NumericGeneric(ref type_var, ref numeric_type) => { // When a numeric type alias's parameter is used as a value (`AliasN::<1>`), // the definition's type variable is bound to the resolved value and the bare // name doesn't resolve at the use site (or worse, resolves to something else - // with the same name). Print the value instead. + // with the same name). Print the value instead, suffixed with its numeric + // type so it can't be inferred as a different one. if let TypeBinding::Bound(binding) = &*type_var.borrow() && let Type::Constant(constant) = binding.follow_bindings() { self.push_str(&constant.to_string()); + self.push('_'); + self.show_type(numeric_type); return; } let name = self.interner.definition_name(ident.id); diff --git a/compiler/noirc_frontend/src/tests/expand.rs b/compiler/noirc_frontend/src/tests/expand.rs index 36dcb525ab8..9760b957edb 100644 --- a/compiler/noirc_frontend/src/tests/expand.rs +++ b/compiler/noirc_frontend/src/tests/expand.rs @@ -829,7 +829,7 @@ fn expands_numeric_type_alias_used_as_value_with_turbofish() { global N: u32 = 100; fn main() { - let a: u32 = 1; + let a: u32 = 1_u32; assert(a == 1_u32); assert(N == 100_u32); } diff --git a/test_programs/execution_success/numeric_type_alias_inference/Nargo.toml b/test_programs/execution_success/numeric_type_alias_inference/Nargo.toml new file mode 100644 index 00000000000..2586ed9caeb --- /dev/null +++ b/test_programs/execution_success/numeric_type_alias_inference/Nargo.toml @@ -0,0 +1,5 @@ +[package] +name = "numeric_type_alias_inference" +type = "bin" +authors = [""] +[dependencies] diff --git a/test_programs/execution_success/numeric_type_alias_inference/Prover.toml b/test_programs/execution_success/numeric_type_alias_inference/Prover.toml new file mode 100644 index 00000000000..0e5dfd5638d --- /dev/null +++ b/test_programs/execution_success/numeric_type_alias_inference/Prover.toml @@ -0,0 +1 @@ +x = "5" diff --git a/test_programs/execution_success/numeric_type_alias_inference/src/main.nr b/test_programs/execution_success/numeric_type_alias_inference/src/main.nr new file mode 100644 index 00000000000..e1040857bd9 --- /dev/null +++ b/test_programs/execution_success/numeric_type_alias_inference/src/main.nr @@ -0,0 +1,32 @@ +// A numeric type alias's parameter used as a value must keep its numeric type through +// `nargo expand` (which prints it as a constant): if the constant loses its `u8` type, +// inference picks `impl Which for Field` instead and `which` returns the wrong value. +trait Which { + fn which(self) -> Field; +} + +impl Which for u8 { + fn which(self) -> Field { + 1 + } +} + +impl Which for Field { + fn which(self) -> Field { + 2 + } +} + +type AliasN: u8 = N; + +fn takes(value: T) -> T +where + T: Which, +{ + value +} + +fn main(x: Field) { + assert(takes(AliasN::<200u8>).which() == 1); + assert(x == 5); +} diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias/execute__tests__expanded.snap index 35b2685a38e..c69bca1f60c 100644 --- a/tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias/execute__tests__expanded.snap @@ -31,10 +31,10 @@ fn main(x: Field) { assert(three == 3_u32); let arr2: [Field; 2] = [0_Field; 2]; assert(arr2.len() == 2_u32); - let a: u32 = 1; + let a: u32 = 1_u32; assert(a == 1_u32); assert(N == 100_u32); - let b: u32 = 5; + let b: u32 = 5_u32; assert(b == 5_u32); let mut m: Matrix<3, 4> = new_matrix::<3, 4>(); m.elements[(3_u32 * 2_u32) + 3_u32] = 10_Field; diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias_inference/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias_inference/execute__tests__expanded.snap new file mode 100644 index 00000000000..46d3e04b7cd --- /dev/null +++ b/tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias_inference/execute__tests__expanded.snap @@ -0,0 +1,33 @@ +--- +source: tooling/nargo_cli/tests/execute.rs +expression: expanded_code +--- +trait Which { + fn which(self) -> Field; +} + +impl Which for u8 { + fn which(self) -> Field { + 1_Field + } +} + +impl Which for Field { + fn which(self) -> Self { + 2_Field + } +} + +type AliasN: u8 = N; + +fn takes(value: T) -> T +where + T: Which, +{ + value +} + +fn main(x: Field) { + assert(takes(200_u8).which() == 1_Field); + assert(x == 5_Field); +} diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias_inference/execute__tests__stdout.snap b/tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias_inference/execute__tests__stdout.snap new file mode 100644 index 00000000000..e86e3de90e1 --- /dev/null +++ b/tooling/nargo_cli/tests/snapshots/execution_success/numeric_type_alias_inference/execute__tests__stdout.snap @@ -0,0 +1,5 @@ +--- +source: tooling/nargo_cli/tests/execute.rs +expression: stdout +--- + From 17b6d5131e0e7b18763432e6d1352a050acb0b32 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Mon, 3 Aug 2026 18:24:20 -0300 Subject: [PATCH 17/18] fix: harden `nargo expand`'s unrepresentable-value detection Addresses review findings on the value-printing commits: - A string value is now representable only if every character survives the round trip through Rust's `{:?}` formatting: valid UTF-8 isn't enough, since control characters and combining marks escape as `\u{..}`, which Noir's lexer doesn't accept. Format string fragments are stricter still (printed raw with only `"` re-escaped), so backslashes, braces and control characters make them unrepresentable. - A closure value prints as just its lambda, so a closure with captured variables is now unrepresentable (the captures would be dangling). - When a mutable global falls back to its initializer, the printed initializer doesn't reproduce the final mutated value (the mutating attributes are already expanded away), so a warning comment is appended to mark the divergence. - `Value::Function` now gets the same visibility check as structs and enums for plain (non-method) functions. - The `impl {Trait}` synthetic-name prefix shared by the elaborator and the printer is now a single constant with a `NamedGeneric` helper, so the sites can't silently drift apart. - Documented that `unresolved_type_name` renders through `Type`'s non-source-faithful `Display`, and that the visible-or-reexported checks deliberately over-approximate to match how references print. Adds the `comptime_mutated_global` execution test (pins that a runtime-observable comptime-mutated global expands to its final value) and the `global_string_control_char` execution test (a valid-UTF-8 string that can't print as a literal falls back to its initializer). Co-Authored-By: Claude Fable 5 --- .../noirc_frontend/src/elaborator/traits.rs | 5 +- .../noirc_frontend/src/elaborator/types.rs | 6 ++ .../src/hir/printer/items/hir_def.rs | 13 +-- .../noirc_frontend/src/hir/printer/mod.rs | 93 +++++++++++++++++-- compiler/noirc_frontend/src/hir_def/types.rs | 12 +++ compiler/noirc_frontend/src/tests/expand.rs | 31 +++++++ .../comptime_mutated_global/Nargo.toml | 5 + .../comptime_mutated_global/Prover.toml | 1 + .../comptime_mutated_global/src/main.nr | 18 ++++ .../global_string_control_char/Nargo.toml | 5 + .../global_string_control_char/Prover.toml | 1 + .../global_string_control_char/src/main.nr | 11 +++ .../execute__tests__expanded.snap | 2 +- .../execute__tests__expanded.snap | 18 ++++ .../execute__tests__stdout.snap | 5 + .../execute__tests__expanded.snap | 13 +++ .../execute__tests__stdout.snap | 5 + 17 files changed, 221 insertions(+), 23 deletions(-) create mode 100644 test_programs/execution_success/comptime_mutated_global/Nargo.toml create mode 100644 test_programs/execution_success/comptime_mutated_global/Prover.toml create mode 100644 test_programs/execution_success/comptime_mutated_global/src/main.nr create mode 100644 test_programs/execution_success/global_string_control_char/Nargo.toml create mode 100644 test_programs/execution_success/global_string_control_char/Prover.toml create mode 100644 test_programs/execution_success/global_string_control_char/src/main.nr create mode 100644 tooling/nargo_cli/tests/snapshots/execution_success/comptime_mutated_global/execute__tests__expanded.snap create mode 100644 tooling/nargo_cli/tests/snapshots/execution_success/comptime_mutated_global/execute__tests__stdout.snap create mode 100644 tooling/nargo_cli/tests/snapshots/execution_success/global_string_control_char/execute__tests__expanded.snap create mode 100644 tooling/nargo_cli/tests/snapshots/execution_success/global_string_control_char/execute__tests__stdout.snap diff --git a/compiler/noirc_frontend/src/elaborator/traits.rs b/compiler/noirc_frontend/src/elaborator/traits.rs index 1a5f63569f3..6e4b4f2d729 100644 --- a/compiler/noirc_frontend/src/elaborator/traits.rs +++ b/compiler/noirc_frontend/src/elaborator/traits.rs @@ -174,7 +174,8 @@ use itertools::Itertools; use noirc_errors::Location; use crate::{ - Kind, NamedGeneric, ResolvedGeneric, Type, TypeBindings, TypeVariable, + IMPL_TRAIT_PARAMETER_NAME_PREFIX, Kind, NamedGeneric, ResolvedGeneric, Type, TypeBindings, + TypeVariable, ast::{ FunctionDefinition, FunctionKind, GenericTypeArgs, Ident, NoirFunction, Path, TraitBound, TraitItem, UnresolvedGeneric, UnresolvedTraitConstraint, UnresolvedType, @@ -520,7 +521,7 @@ impl Elaborator<'_> { let new_generic = TypeVariable::unbound(new_generic_id, Kind::Normal); generics.push(new_generic.clone()); - let name = format!("impl {trait_path}"); + let name = format!("{IMPL_TRAIT_PARAMETER_NAME_PREFIX}{trait_path}"); let generic_type = new_generic.into_named_generic(&Rc::new(name), None); let trait_bound = TraitBound { trait_path, trait_generics }; diff --git a/compiler/noirc_frontend/src/elaborator/types.rs b/compiler/noirc_frontend/src/elaborator/types.rs index adb88dcf9ac..843d11cabff 100644 --- a/compiler/noirc_frontend/src/elaborator/types.rs +++ b/compiler/noirc_frontend/src/elaborator/types.rs @@ -132,6 +132,12 @@ impl Elaborator<'_> { /// its `Display` is the "(resolved type)" placeholder, so look through the resolution to /// the actual type. These names resurface in `nargo expand` output, where the placeholder /// would not parse. + /// + /// Note that `Display` for a resolved [Type] is not source-faithful: data types print as + /// their bare name (no module path, so a same-named type in scope at the printing site can + /// shadow it) and unbound type variables print as `_` or their kind's default. These names + /// are display-only — nothing semantic keys off them — but the printed projection may not + /// re-resolve in every context. pub(super) fn unresolved_type_name(&self, typ: &UnresolvedType) -> String { match &typ.typ { UnresolvedTypeData::Resolved(quoted_type_id) => { diff --git a/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs b/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs index 64b1592fade..b34d35de9a9 100644 --- a/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs +++ b/compiler/noirc_frontend/src/hir/printer/items/hir_def.rs @@ -13,7 +13,6 @@ use crate::{ }, stmt::{HirLValue, HirPattern, HirStatement}, }, - modules::module_def_id_is_visible, node_interner::{DefinitionId, DefinitionKind, ExprId, FuncId, StmtId}, token::FmtStrFragment, }; @@ -973,16 +972,8 @@ impl ItemPrinter<'_, '_> { // compile-time value instead. let module_def_id = ModuleDefId::TraitId(trait_impl.trait_id); let trait_ = self.interner.get_trait(trait_impl.trait_id); - let trait_is_visible = - module_def_id_is_visible( - module_def_id, - self.module_id, - trait_.visibility, - None, - self.interner, - self.def_maps, - self.dependencies, - ) || !self.interner.get_reexports(module_def_id).is_empty(); + let trait_is_visible = self + .module_def_id_is_visible_or_reexported(module_def_id, trait_.visibility); if !trait_is_visible && let Some(named_type) = self .interner diff --git a/compiler/noirc_frontend/src/hir/printer/mod.rs b/compiler/noirc_frontend/src/hir/printer/mod.rs index a33366da730..85cbface865 100644 --- a/compiler/noirc_frontend/src/hir/printer/mod.rs +++ b/compiler/noirc_frontend/src/hir/printer/mod.rs @@ -576,7 +576,9 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { self.push_str(&named_type.name.to_string()); self.push_str(": "); // `Self` doesn't resolve in an associated constant's numeric type annotation, - // so print the type itself even when it's the impl's self type. + // so print the type itself even when it's the impl's self type. This covers + // the whole declaration — annotation, value and the value's type suffix — + // since the value is a numeric type expression subject to the same rule. let self_type = self.self_type.take(); self.show_type(&numeric_type); self.push_str(" = "); @@ -647,6 +649,7 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { self.push_str(&global_info.ident.to_string()); self.push_str(": "); self.show_type(&typ); + let mut initializer_may_differ_from_value = false; if let GlobalValue::Resolved(value) = &global_info.value { self.push_str(" = "); // Prefer the evaluated value: a comptime-mutable global's final value can differ @@ -657,6 +660,10 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { if self.value_is_representable(value) { self.show_value(value); } else if let Some(let_statement) = self.interner.get_global_let_statement(global_id) { + // For a mutable global the mutations happened via attributes and comptime + // blocks that are already expanded away, so re-evaluating the initializer + // does not reproduce the final value. + initializer_may_differ_from_value = definition.mutable; let expr_id = let_statement.expression; let hir_expr = self.interner.expression(&expr_id); self.show_hir_expression(hir_expr, expr_id); @@ -665,6 +672,11 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { } } self.push_str(";"); + if initializer_may_differ_from_value { + self.push_str( + " // Warning: this global was mutated at compile time; its final value could not be printed, so this is its initializer", + ); + } } /// Whether [`Self::show_value`] can print `value` as source code that compiles from the @@ -676,17 +688,21 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { Value::Unit | Value::Bool(_) | Value::Integer(_) - | Value::Function(..) - | Value::Closure(_) | Value::Quoted(_) | Value::Zeroed(_) => true, - // A string that isn't valid UTF-8 can only be printed lossily, which changes both - // its content and its length. - Value::String(bytes) | Value::CtString(bytes) => std::str::from_utf8(bytes).is_ok(), + Value::Function(func_id, ..) => self.function_is_visible(*func_id), + + // A closure prints as just its lambda, so any captured variables would be + // dangling references at the global's scope. + Value::Closure(closure) => closure.lambda.captures.is_empty(), + + Value::String(bytes) | Value::CtString(bytes) => string_bytes_are_representable(bytes), Value::FormatString(fragments, ..) => fragments.iter().all(|fragment| match fragment { - FormatStringFragment::String(_) => true, + FormatStringFragment::String(string) => { + format_string_fragment_is_representable(string) + } FormatStringFragment::Value { value, .. } => self.value_is_representable(value), }), @@ -756,6 +772,33 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { fn data_type_id_is_visible(&self, id: TypeId, visibility: ItemVisibility) -> bool { let module_def_id = ModuleDefId::TypeId(id); + self.module_def_id_is_visible_or_reexported(module_def_id, visibility) + } + + /// Whether a reference to the given function compiles from the current module. Methods + /// (impl, trait impl or trait functions) are printed as `Type::method` or + /// `::method`, whose visibility follows the container, so only plain + /// functions are checked here. + fn function_is_visible(&self, func_id: FuncId) -> bool { + let func_meta = self.interner.function_meta(&func_id); + if func_meta.trait_impl.is_some() + || func_meta.trait_id.is_some() + || func_meta.type_id.is_some() + { + return true; + } + let visibility = self.interner.function_visibility(func_id); + self.module_def_id_is_visible_or_reexported(ModuleDefId::FunctionId(func_id), visibility) + } + + /// This over-approximates on purpose: a re-export *somewhere* isn't necessarily nameable + /// from this module, but it matches what `show_reference_to_module_def_id` does — when an + /// item isn't directly visible it prints through the first re-export it finds. + fn module_def_id_is_visible_or_reexported( + &self, + module_def_id: ModuleDefId, + visibility: ItemVisibility, + ) -> bool { module_def_id_is_visible( module_def_id, self.module_id, @@ -865,7 +908,7 @@ impl<'context, 'string> ItemPrinter<'context, 'string> { .iter() .filter(|constraint| { !matches!(&constraint.typ, - Type::NamedGeneric(generic) if generic.name.starts_with("impl ")) + Type::NamedGeneric(generic) if generic.is_impl_trait_parameter()) }) .cloned() .collect::>(); @@ -1597,7 +1640,7 @@ fn impl_trait_parameter_constraint<'meta>( let Type::NamedGeneric(generic) = typ else { return None; }; - if !generic.name.starts_with("impl ") { + if !generic.is_impl_trait_parameter() { return None; } func_meta.trait_constraints.iter().find(|constraint| { @@ -1606,3 +1649,35 @@ fn impl_trait_parameter_constraint<'meta>( if constraint_generic.type_var.id() == generic.type_var.id()) }) } + +/// Whether `show_value` can print these string bytes as a `"..."` literal that lexes back to +/// the same bytes. The bytes must be valid UTF-8, and every character must survive the trip +/// through Rust's `{:?}` formatting: Rust escapes control characters, combining marks and +/// other special characters as `\u{..}`, which Noir's lexer doesn't accept (it only knows +/// `\r \n \t \0 \" \\`). +fn string_bytes_are_representable(bytes: &[u8]) -> bool { + let Ok(string) = std::str::from_utf8(bytes) else { + return false; + }; + string.chars().all(char_survives_debug_formatting) +} + +fn char_survives_debug_formatting(char: char) -> bool { + // `{:?}` on a string prints `'` unescaped, even though `char::escape_debug` escapes it. + if char == '\'' { + return true; + } + let mut escaped = char.escape_debug(); + match escaped.next() { + // The escapes Noir's lexer accepts; anything else (i.e. `\u{..}`) doesn't re-lex. + Some('\\') => matches!(escaped.next(), Some('r' | 'n' | 't' | '0' | '"' | '\\')), + _ => true, + } +} + +/// Whether `show_value` can print this format string fragment inside an `f"..."` literal. +/// Fragments are printed raw with only `"` re-escaped, so any character that needs an escape, +/// or that the f-string syntax gives meaning to (braces), doesn't round-trip. +fn format_string_fragment_is_representable(string: &str) -> bool { + string.chars().all(|char| char != '\\' && char != '{' && char != '}' && !char.is_control()) +} diff --git a/compiler/noirc_frontend/src/hir_def/types.rs b/compiler/noirc_frontend/src/hir_def/types.rs index c2d5aacaed6..7000293891a 100644 --- a/compiler/noirc_frontend/src/hir_def/types.rs +++ b/compiler/noirc_frontend/src/hir_def/types.rs @@ -206,8 +206,20 @@ impl NamedGeneric { pub fn is_associated(&self) -> bool { self.name.contains("::") } + + /// Whether this is the hidden generic desugared from an `impl Trait` parameter + /// (see `desugar_impl_trait_arg`), recognizable by its synthetic name: a user-written + /// generic name can never contain a space. + pub fn is_impl_trait_parameter(&self) -> bool { + self.name.starts_with(IMPL_TRAIT_PARAMETER_NAME_PREFIX) + } } +/// Prefix of the synthetic name minted for the hidden generic that an `impl Trait` parameter +/// desugars to. Shared between the elaborator (which mints the name) and the HIR printer +/// (which recognizes such generics via [`NamedGeneric::is_impl_trait_parameter`]). +pub const IMPL_TRAIT_PARAMETER_NAME_PREFIX: &str = "impl "; + /// A Kind is the type of a Type. These are used since only certain kinds of types are allowed in /// certain positions. /// diff --git a/compiler/noirc_frontend/src/tests/expand.rs b/compiler/noirc_frontend/src/tests/expand.rs index 9760b957edb..c6259acd5f6 100644 --- a/compiler/noirc_frontend/src/tests/expand.rs +++ b/compiler/noirc_frontend/src/tests/expand.rs @@ -874,3 +874,34 @@ fn expands_associated_constant_over_self_type_with_concrete_annotation() { } "); } + +#[test] +fn expands_global_capturing_closure_as_its_initializer_expression() { + let src = r#" + fn make() -> fn[(Field,)](Field) -> Field { + let x: Field = 3; + |y: Field| -> Field { y + x } + } + + global F: fn[(Field,)](Field) -> Field = make(); + + fn main() { + let _ = F; + } + "#; + let expanded = assert_no_errors_and_to_string(src); + insta::assert_snapshot!(expanded, @r" + fn make() -> fn[(Field,)](Field) -> Field { + let x: Field = 3_Field; + |y: Field| -> Field { + y + x + } + } + + global F: fn[(Field,)](Field) -> Field = make(); + + fn main() { + let _: fn[(Field,)](Field) -> Field = F; + } + "); +} diff --git a/test_programs/execution_success/comptime_mutated_global/Nargo.toml b/test_programs/execution_success/comptime_mutated_global/Nargo.toml new file mode 100644 index 00000000000..7f493f7310a --- /dev/null +++ b/test_programs/execution_success/comptime_mutated_global/Nargo.toml @@ -0,0 +1,5 @@ +[package] +name = "comptime_mutated_global" +type = "bin" +authors = [""] +[dependencies] diff --git a/test_programs/execution_success/comptime_mutated_global/Prover.toml b/test_programs/execution_success/comptime_mutated_global/Prover.toml new file mode 100644 index 00000000000..0e5dfd5638d --- /dev/null +++ b/test_programs/execution_success/comptime_mutated_global/Prover.toml @@ -0,0 +1 @@ +x = "5" diff --git a/test_programs/execution_success/comptime_mutated_global/src/main.nr b/test_programs/execution_success/comptime_mutated_global/src/main.nr new file mode 100644 index 00000000000..5f91840da39 --- /dev/null +++ b/test_programs/execution_success/comptime_mutated_global/src/main.nr @@ -0,0 +1,18 @@ +// A comptime-mutable global observed at runtime must expand to its final mutated value, +// not its initializer: the attribute that mutates it is already expanded away, so +// re-evaluating the initializer would produce a different program. +comptime mut global COUNTER: u32 = 0; + +comptime fn bump(_f: FunctionDefinition) { + COUNTER += 1; +} + +#[bump] +fn foo() {} + +fn main(x: Field) { + foo(); + let counter = comptime { COUNTER }; + assert(counter == 1); + assert(x == 5); +} diff --git a/test_programs/execution_success/global_string_control_char/Nargo.toml b/test_programs/execution_success/global_string_control_char/Nargo.toml new file mode 100644 index 00000000000..dfa1027f18e --- /dev/null +++ b/test_programs/execution_success/global_string_control_char/Nargo.toml @@ -0,0 +1,5 @@ +[package] +name = "global_string_control_char" +type = "bin" +authors = [""] +[dependencies] diff --git a/test_programs/execution_success/global_string_control_char/Prover.toml b/test_programs/execution_success/global_string_control_char/Prover.toml new file mode 100644 index 00000000000..0e5dfd5638d --- /dev/null +++ b/test_programs/execution_success/global_string_control_char/Prover.toml @@ -0,0 +1 @@ +x = "5" diff --git a/test_programs/execution_success/global_string_control_char/src/main.nr b/test_programs/execution_success/global_string_control_char/src/main.nr new file mode 100644 index 00000000000..c259018e9d3 --- /dev/null +++ b/test_programs/execution_success/global_string_control_char/src/main.nr @@ -0,0 +1,11 @@ +// A string global containing a control character is valid UTF-8 but can't be printed as a +// Noir string literal (Rust's `{:?}` escapes it as `\u{..}`, which Noir doesn't lex), so +// `nargo expand` must print the initializer expression instead of the value. +global BYTES: [u8; 4] = [102, 7, 111, 111]; +global S: str<4> = BYTES.as_str_unchecked(); + +fn main(x: Field) { + let bytes = S.as_bytes(); + assert(bytes[1] as Field + x == 12); + assert(x == 5); +} diff --git a/tooling/nargo_cli/tests/snapshots/compile_success_no_bug/function_registry/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/compile_success_no_bug/function_registry/execute__tests__expanded.snap index 1458a8fdedd..2f67392c513 100644 --- a/tooling/nargo_cli/tests/snapshots/compile_success_no_bug/function_registry/execute__tests__expanded.snap +++ b/tooling/nargo_cli/tests/snapshots/compile_success_no_bug/function_registry/execute__tests__expanded.snap @@ -5,7 +5,7 @@ expression: expanded_code use std::{collections::umap::UHashMap, hash::{BuildHasherDefault, Hasher}}; comptime mut global REGISTRY: UHashMap> = - UHashMap::>::default(); + UHashMap::>::default(); // Warning: this global was mutated at compile time; its final value could not be printed, so this is its initializer comptime fn add_to_registry( registry: &mut UHashMap>, diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/comptime_mutated_global/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/comptime_mutated_global/execute__tests__expanded.snap new file mode 100644 index 00000000000..ad752270043 --- /dev/null +++ b/tooling/nargo_cli/tests/snapshots/execution_success/comptime_mutated_global/execute__tests__expanded.snap @@ -0,0 +1,18 @@ +--- +source: tooling/nargo_cli/tests/execute.rs +expression: expanded_code +--- +comptime mut global COUNTER: u32 = 1; + +comptime fn bump(_f: FunctionDefinition) { + COUNTER = COUNTER + 1_u32; +} + +fn foo() {} + +fn main(x: Field) { + foo(); + let counter: u32 = 1_u32; + assert(counter == 1_u32); + assert(x == 5_Field); +} diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/comptime_mutated_global/execute__tests__stdout.snap b/tooling/nargo_cli/tests/snapshots/execution_success/comptime_mutated_global/execute__tests__stdout.snap new file mode 100644 index 00000000000..e86e3de90e1 --- /dev/null +++ b/tooling/nargo_cli/tests/snapshots/execution_success/comptime_mutated_global/execute__tests__stdout.snap @@ -0,0 +1,5 @@ +--- +source: tooling/nargo_cli/tests/execute.rs +expression: stdout +--- + diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/global_string_control_char/execute__tests__expanded.snap b/tooling/nargo_cli/tests/snapshots/execution_success/global_string_control_char/execute__tests__expanded.snap new file mode 100644 index 00000000000..62dfd23fe35 --- /dev/null +++ b/tooling/nargo_cli/tests/snapshots/execution_success/global_string_control_char/execute__tests__expanded.snap @@ -0,0 +1,13 @@ +--- +source: tooling/nargo_cli/tests/execute.rs +expression: expanded_code +--- +global BYTES: [u8; 4] = [102, 7, 111, 111]; + +global S: str<4> = BYTES.as_str_unchecked(); + +fn main(x: Field) { + let bytes: [u8; 4] = S.as_bytes(); + assert(((bytes[1_u32] as Field) + x) == 12_Field); + assert(x == 5_Field); +} diff --git a/tooling/nargo_cli/tests/snapshots/execution_success/global_string_control_char/execute__tests__stdout.snap b/tooling/nargo_cli/tests/snapshots/execution_success/global_string_control_char/execute__tests__stdout.snap new file mode 100644 index 00000000000..e86e3de90e1 --- /dev/null +++ b/tooling/nargo_cli/tests/snapshots/execution_success/global_string_control_char/execute__tests__stdout.snap @@ -0,0 +1,5 @@ +--- +source: tooling/nargo_cli/tests/execute.rs +expression: stdout +--- + From 953941267bd893bc77578fb5c706e48fba26f407 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Mon, 3 Aug 2026 18:37:25 -0300 Subject: [PATCH 18/18] Format --- .../execution_success/numeric_type_alias_inference/src/main.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_programs/execution_success/numeric_type_alias_inference/src/main.nr b/test_programs/execution_success/numeric_type_alias_inference/src/main.nr index e1040857bd9..e46e8e7fa03 100644 --- a/test_programs/execution_success/numeric_type_alias_inference/src/main.nr +++ b/test_programs/execution_success/numeric_type_alias_inference/src/main.nr @@ -27,6 +27,6 @@ where } fn main(x: Field) { - assert(takes(AliasN::<200u8>).which() == 1); + assert(takes(AliasN::<200_u8>).which() == 1); assert(x == 5); }