Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions compiler/noirc_frontend/src/elaborator/function_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,23 @@ impl Elaborator<'_> {

/// Push a type variable (its ID and type) as a required type variable: it must be
/// bound after type-checking the current function.
///
/// The type variable is only pushed if the elaborator is not in a comptime context.
/// The reason is that in a comptime context the type of a variable might change
/// across a loop's iterations, so a type can temporarily remain as `Type<_>` where
/// `_` is bound by the interpreter evaluating an expression's type being unified with
/// that type.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this open us up to regular unbound type variables though? E.g. the case this was meant to originally catch may no longer be caught in comptime code?

pub(super) fn push_required_type_variable(
&mut self,
type_variable_id: TypeVariableId,
typ: Type,
kind: BindableTypeVariableKind,
location: Location,
) {
if self.in_comptime_context() {
return;
}

let var = RequiredTypeVariable { type_variable_id, typ, kind, location };
self.get_function_context_mut().required_type_variables.push(var);
Comment on lines +103 to 108
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if self.in_comptime_context() {
return;
}
let var = RequiredTypeVariable { type_variable_id, typ, kind, location };
self.get_function_context_mut().required_type_variables.push(var);
if !self.in_comptime_context() {
let var = RequiredTypeVariable { type_variable_id, typ, kind, location };
self.get_function_context_mut().required_type_variables.push(var);
}

}
Expand Down
47 changes: 31 additions & 16 deletions compiler/noirc_frontend/src/hir/comptime/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ use iter_extended::{try_vecmap, vecmap};
use noirc_errors::Location;
use rustc_hash::FxHashMap as HashMap;

use crate::TypeVariable;
use crate::ast::{BinaryOpKind, FunctionKind, IntegerBitSize, UnaryOp};
use crate::elaborator::{ElaborateReason, Elaborator, ElaboratorOptions};
use crate::hir::Context;
Expand Down Expand Up @@ -75,6 +74,7 @@ use crate::{
},
node_interner::{DefinitionId, DefinitionKind, ExprId, FuncId, StmtId},
};
use crate::{TypeVariable, UnificationError};

use super::errors::{IResult, InterpreterError};
use super::value::{Closure, Value, unwrap_rc};
Expand Down Expand Up @@ -1163,7 +1163,36 @@ impl<'local, 'interner> Interpreter<'local, 'interner> {
// the macro result changes across loop iterations.
let expected_type = self.elaborator.interner.id_type(id);
let actual_type = result.get_type();
self.unify_without_binding(&actual_type, &expected_type, location);

// Undo any bindings (if any) from the last time we unified this expression's
// type against the actual type
if let Some(bindings) =
self.elaborator.interner.macro_call_expression_bindings.remove(&id)
{
for (var, kind, _typ) in bindings.values() {
var.unbind(var.id(), kind.clone());
}
}

let mut bindings = TypeBindings::default();
match actual_type.try_unify(&expected_type, &mut bindings) {
Ok(()) => {
// Store the bindings so we can undo them next time
self.elaborator
.interner
.macro_call_expression_bindings
.insert(id, bindings.clone());
Type::apply_type_bindings(bindings);
}
Err(UnificationError) => {
let error = TypeCheckError::TypeMismatch {
expected_typ: expected_type.to_string(),
expr_typ: actual_type.to_string(),
expr_location: location,
};
Comment on lines +1167 to +1192
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you factor out all or some of this code? It makes elaborate_call longer and more difficult to understand by getting very involved in a corner case.

self.elaborator.push_err(error);
}
}
}
Ok(result)
}
Expand All @@ -1175,20 +1204,6 @@ impl<'local, 'interner> Interpreter<'local, 'interner> {
}
}

/// This function is used by the interpreter for some comptime code
/// which can change types e.g. on each iteration of a for loop.
fn unify_without_binding(&mut self, actual: &Type, expected: &Type, location: Location) {
let mut bindings = TypeBindings::default();
if actual.try_unify(expected, &mut bindings).is_err() {
let error = TypeCheckError::TypeMismatch {
expected_typ: expected.to_string(),
expr_typ: actual.to_string(),
expr_location: location,
};
self.elaborator.push_err(error);
}
}

fn evaluate_cast(&mut self, cast: &HirCastExpression, id: ExprId) -> IResult<Value> {
let evaluated_lhs = self.evaluate(cast.lhs)?;
let location = self.elaborator.interner.expr_location(&id);
Expand Down
7 changes: 7 additions & 0 deletions compiler/noirc_frontend/src/node_interner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,12 @@ pub struct NodeInterner {
/// Tracks statements that encountered errors during elaboration.
/// Used by the interpreter to skip evaluation of errored statements.
pub(crate) stmts_with_errors: HashSet<StmtId>,

/// Associates type bindings that resulted from unifying the type of a macro call expression
/// with the expected type at the callsite.
/// Since a single macro call expression might end up having different types across loop
/// iterations, before unifying its type we undo bindings from the last time we unified it.
pub(crate) macro_call_expression_bindings: HashMap<ExprId, TypeBindings>,
}

/// A trait implementation is either a normal implementation that is present in the source
Expand Down Expand Up @@ -500,6 +506,7 @@ impl Default for NodeInterner {
primitive_docs: HashMap::default(),
exprs_with_errors: HashSet::default(),
stmts_with_errors: HashSet::default(),
macro_call_expression_bindings: HashMap::default(),
}
}
}
Expand Down
25 changes: 25 additions & 0 deletions compiler/noirc_frontend/src/tests/metaprogramming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1465,3 +1465,28 @@ fn escape_nested_unquote() {
"#;
assert_no_errors(src);
}

#[test]
fn unifies_macro_call_type_with_variable_type_in_comptime_block() {
let src = r#"
comptime fn unquote(code: Quoted) -> Quoted {
code
}

struct Foo<let N: u32> {}

impl<let N: u32> Foo<N> {
fn len(_self: Self) -> u32 {
N
}
}

fn main() -> pub u32 {
comptime {
let foo: Foo<_> = unquote!(quote { Foo::<10> {} });
foo.len()
}
}
"#;
assert_no_errors(src);
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.