diff --git a/compiler/noirc_frontend/src/elaborator/types.rs b/compiler/noirc_frontend/src/elaborator/types.rs index 6be18df7b52..264b83956f8 100644 --- a/compiler/noirc_frontend/src/elaborator/types.rs +++ b/compiler/noirc_frontend/src/elaborator/types.rs @@ -633,7 +633,7 @@ impl<'context> Elaborator<'context> { 0 } - pub fn unify( + pub(super) fn unify( &mut self, actual: &Type, expected: &Type, @@ -644,6 +644,22 @@ impl<'context> Elaborator<'context> { } } + /// Do not apply type bindings even after a successful unification. + /// This function is used by the interpreter for some comptime code + /// which can change types e.g. on each iteration of a for loop. + pub fn unify_without_applying_bindings( + &mut self, + actual: &Type, + expected: &Type, + file: fm::FileId, + make_error: impl FnOnce() -> TypeCheckError, + ) { + let mut bindings = TypeBindings::new(); + if actual.try_unify(expected, &mut bindings).is_err() { + self.errors.push((make_error().into(), file)); + } + } + /// Wrapper of Type::unify_with_coercions using self.errors pub(super) fn unify_with_coercions( &mut self, diff --git a/compiler/noirc_frontend/src/hir/comptime/interpreter.rs b/compiler/noirc_frontend/src/hir/comptime/interpreter.rs index b5ed8126e33..e920073b453 100644 --- a/compiler/noirc_frontend/src/hir/comptime/interpreter.rs +++ b/compiler/noirc_frontend/src/hir/comptime/interpreter.rs @@ -1303,9 +1303,11 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { // Macro calls are typed as type variables during type checking. // Now that we know the type we need to further unify it in case there // are inconsistencies or the type needs to be known. + // We don't commit any type bindings made this way in case the type of + // the macro result changes across loop iterations. let expected_type = self.elaborator.interner.id_type(id); let actual_type = result.get_type(); - self.unify(&actual_type, &expected_type, location); + self.unify_without_binding(&actual_type, &expected_type, location); } Ok(result) } @@ -1319,16 +1321,14 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { } } - fn unify(&mut self, actual: &Type, expected: &Type, location: Location) { - // We need to swap out the elaborator's file since we may be - // in a different one currently, and it uses that for the error location. - let old_file = std::mem::replace(&mut self.elaborator.file, location.file); - self.elaborator.unify(actual, expected, || TypeCheckError::TypeMismatch { - expected_typ: expected.to_string(), - expr_typ: actual.to_string(), - expr_span: location.span, + fn unify_without_binding(&mut self, actual: &Type, expected: &Type, location: Location) { + self.elaborator.unify_without_applying_bindings(actual, expected, location.file, || { + TypeCheckError::TypeMismatch { + expected_typ: expected.to_string(), + expr_typ: actual.to_string(), + expr_span: location.span, + } }); - self.elaborator.file = old_file; } fn evaluate_method_call( diff --git a/compiler/noirc_frontend/src/tests.rs b/compiler/noirc_frontend/src/tests.rs index e21db0790d7..dc54fd624e4 100644 --- a/compiler/noirc_frontend/src/tests.rs +++ b/compiler/noirc_frontend/src/tests.rs @@ -3725,3 +3725,26 @@ fn use_numeric_generic_in_trait_method() { println!("{errors:?}"); assert_eq!(errors.len(), 0); } + +#[test] +fn macro_result_type_mismatch() { + let src = r#" + fn main() { + comptime { + let x = unquote!(quote { "test" }); + let _: Field = x; + } + } + + comptime fn unquote(q: Quoted) -> Quoted { + q + } + "#; + + let errors = get_program_errors(src); + assert_eq!(errors.len(), 1); + assert!(matches!( + errors[0].0, + CompilationError::TypeError(TypeCheckError::TypeMismatch { .. }) + )); +} diff --git a/test_programs/compile_success_empty/macro_result_type/Nargo.toml b/test_programs/compile_failure/macro_result_type/Nargo.toml similarity index 100% rename from test_programs/compile_success_empty/macro_result_type/Nargo.toml rename to test_programs/compile_failure/macro_result_type/Nargo.toml diff --git a/test_programs/compile_success_empty/macro_result_type/src/main.nr b/test_programs/compile_failure/macro_result_type/src/main.nr similarity index 100% rename from test_programs/compile_success_empty/macro_result_type/src/main.nr rename to test_programs/compile_failure/macro_result_type/src/main.nr diff --git a/test_programs/compile_success_empty/comptime_change_type_each_iteration/Nargo.toml b/test_programs/compile_success_empty/comptime_change_type_each_iteration/Nargo.toml new file mode 100644 index 00000000000..38e72395bb5 --- /dev/null +++ b/test_programs/compile_success_empty/comptime_change_type_each_iteration/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "comptime_change_type_each_iteration" +type = "bin" +authors = [""] +compiler_version = ">=0.34.0" + +[dependencies] diff --git a/test_programs/compile_success_empty/comptime_change_type_each_iteration/src/main.nr b/test_programs/compile_success_empty/comptime_change_type_each_iteration/src/main.nr new file mode 100644 index 00000000000..7b34c112d4f --- /dev/null +++ b/test_programs/compile_success_empty/comptime_change_type_each_iteration/src/main.nr @@ -0,0 +1,19 @@ +fn main() { + comptime + { + for i in 9..11 { + // Lengths are different on each iteration: + // foo9, foo10 + let name = f"foo{i}".as_ctstring().as_quoted_str!(); + + // So to call `from_signature` we need to delay the type check + // by quoting the function call so that we re-typecheck on each iteration + let hash = std::meta::unquote!(quote { from_signature($name) }); + assert(hash > 3); + } + } +} + +fn from_signature(_signature: str) -> u32 { + N +} diff --git a/test_programs/compile_success_empty/macro_result_type/t.rs b/test_programs/compile_success_empty/macro_result_type/t.rs deleted file mode 100644 index bcd91d7bf5d..00000000000 --- a/test_programs/compile_success_empty/macro_result_type/t.rs +++ /dev/null @@ -1,12 +0,0 @@ - -trait Foo { - fn foo() {} -} - -impl Foo<3> for () { - fn foo() {} -} - -fn main() { - let _ = Foo::foo(); -}