diff --git a/compiler/noirc_frontend/src/elaborator/comptime.rs b/compiler/noirc_frontend/src/elaborator/comptime.rs index cb681de606d..2aba18af63a 100644 --- a/compiler/noirc_frontend/src/elaborator/comptime.rs +++ b/compiler/noirc_frontend/src/elaborator/comptime.rs @@ -682,7 +682,7 @@ impl<'context> Elaborator<'context> { Some(DependencyId::Function(function)) => Some(function), _ => None, }; - Interpreter::new(self, self.crate_id, current_function) + Interpreter::new(self, current_function) } /// Debug helper to print comptime evaluation results. diff --git a/compiler/noirc_frontend/src/hir/comptime/interpreter.rs b/compiler/noirc_frontend/src/hir/comptime/interpreter.rs index cd9a8d5f616..c61bf451d65 100644 --- a/compiler/noirc_frontend/src/hir/comptime/interpreter.rs +++ b/compiler/noirc_frontend/src/hir/comptime/interpreter.rs @@ -1,3 +1,29 @@ +//! The comptime interpreter is a tree-walking interpreter used for evaluating Noir code +//! at compile-time. It is typically triggered (by the elaborator) in one of four scenarios: +//! 1. A `comptime {}` block +//! - Everything in the block is interpreted +//! 2. A macro call `foo!()` +//! - The interpreter calls the function `foo` and inlines the resulting `Quoted` code at the callsite. +//! 3. An attribute call `#[my_attr] struct Foo {}` +//! - The interpreter calls the function `my_attr` and, if `foo`returns a `Quoted` value, +//! inlines the resulting `Quoted` code. +//! 4. A global `global FOO = expr;` +//! - The interpreter evaluates `expr` to simplify the global to a constant. +//! - This means any side-effects in `expr` will be performed at compile-time (!). +//! - This may change in the future. +//! +//! The interpreter operates on the HIR which only requires interpreted code to be elaborated +//! before-hand, it does not need to be translated into another IR. Operating on high-level +//! code like this makes the interpreter more predictable, hopefully limiting bugs, but does +//! make it rather slow in practice. +//! +//! Since unquoting macros may result in new variables in scope, the elaborator must run on that +//! code after the interpreter is run. Yet the requirement that the interpreter runs on HIR means +//! the interpreter must run in the middle of the elaborator. The usual flow is for the elaborator +//! to elaborate as it goes, creating new HIR. Then when it sees a `comptime {}` block or other +//! item that must be interpreted, it elaborates the entire item, creates and runs an [Interpreter] +//! on it, inlines the result, and continues elaborating the rest of the code. + use std::collections::VecDeque; use std::{collections::hash_map::Entry, rc::Rc}; @@ -10,7 +36,6 @@ use rustc_hash::FxHashMap as HashMap; use crate::TypeVariable; use crate::ast::{BinaryOpKind, FunctionKind, IntegerBitSize, UnaryOp}; use crate::elaborator::{ElaborateReason, Elaborator}; -use crate::graph::CrateId; use crate::hir::def_map::ModuleId; use crate::hir::type_check::TypeCheckError; use crate::hir_def::expr::TraitItem; @@ -55,30 +80,34 @@ pub struct Interpreter<'local, 'interner> { /// To expand macros the Interpreter needs access to the Elaborator pub elaborator: &'local mut Elaborator<'interner>, - crate_id: CrateId, - + /// True if the interpreter is currently in a loop (in the current function). + /// Used only to error if break/continue are used outside a loop. in_loop: bool, + /// The current function being interpreted. This may be `None` if we're interpreting + /// the rhs of a global. current_function: Option, - /// Maps each bound generic to each binding it has in the current callstack. + /// Maps each generic to the binding it has in the current callstack. /// Since the interpreter monomorphizes as it interprets, we can bind over the same generic - /// multiple times. Without this map, when one of these inner functions exits we would + /// multiple times. Without the outer Vec, when one of these inner functions exits we would /// unbind the generic completely instead of resetting it to its previous binding. bound_generics: Vec>, } -#[allow(unused)] impl<'local, 'interner> Interpreter<'local, 'interner> { pub(crate) fn new( elaborator: &'local mut Elaborator<'interner>, - crate_id: CrateId, current_function: Option, ) -> Self { - let pedantic_solving = elaborator.pedantic_solving(); - Self { elaborator, crate_id, current_function, bound_generics: Vec::new(), in_loop: false } + Self { elaborator, current_function, bound_generics: Vec::new(), in_loop: false } } + /// Call the given function with the given arguments and return the result. + /// + /// This will handle internal details like binding generics and error handling. + /// Note that running code which resulted in previous errors during elaboration + /// may result in similar errors being issued again by the interpreter. pub(crate) fn call_function( &mut self, function: FuncId, @@ -114,6 +143,7 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { result } + /// Helper to check parameter count and dispatch on the function kind to run the function. fn call_function_inner( &mut self, function: FuncId, @@ -136,7 +166,7 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { // Don't change the current function scope if we're in a #[use_callers_scope] function. // This will affect where `Expression::resolve`, `Quoted::as_type`, and similar functions resolve. - let mut old_function = self.current_function; + let old_function = self.current_function; let modifiers = self.elaborator.interner.function_modifiers(&function); if !modifiers.attributes.has_use_callers_scope() { self.current_function = Some(function); @@ -201,18 +231,24 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { } } + /// Helper to elaborate the given item in the given function's context. If None is passed, + /// the global context is used. This function will temporarily unbind any generics from the + /// previous function call if they exist. fn elaborate_in_function( &mut self, function: Option, reason: Option, f: impl FnOnce(&mut Elaborator) -> T, ) -> T { + // Why do we only unbind generics from the previous function here? self.unbind_generics_from_previous_function(); let result = self.elaborator.elaborate_item_from_comptime_in_function(function, reason, f); self.rebind_generics_from_previous_function(); result } + /// Run the given function with an elaborator in the context of the given module. + /// Temporarily undoes any generics from the previous function. fn elaborate_in_module( &mut self, module: ModuleId, @@ -225,6 +261,9 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { result } + /// Calls a builtin, foreign, or oracle function (not all oracles are supported). + /// + /// This will ignore any oracles starting with "__debug" fn call_special( &mut self, function: FuncId, @@ -425,7 +464,7 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { } } } - HirPattern::Struct(struct_type, pattern_fields, _) => { + HirPattern::Struct(_struct_type, pattern_fields, _) => { self.push_scope(); let res = match argument { @@ -541,8 +580,8 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { HirExpression::Call(call) => self.evaluate_call(call, id), HirExpression::Constrain(constrain) => self.evaluate_constrain(constrain), HirExpression::Cast(cast) => self.evaluate_cast(&cast, id), - HirExpression::If(if_) => self.evaluate_if(if_, id), - HirExpression::Match(match_) => todo!("Evaluate match in comptime code"), + HirExpression::If(if_) => self.evaluate_if(if_), + HirExpression::Match(_) => todo!("Evaluate match in comptime code"), HirExpression::Tuple(tuple) => self.evaluate_tuple(tuple), HirExpression::Lambda(lambda) => self.evaluate_lambda(lambda, id), HirExpression::Quote(tokens) => self.evaluate_quote(tokens), @@ -550,7 +589,7 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { HirExpression::EnumConstructor(constructor) => { self.evaluate_enum_constructor(constructor, id) } - HirExpression::Unquote(tokens) => { + HirExpression::Unquote(_) => { // An Unquote expression being found is indicative of a macro being // expanded within another comptime fn which we don't currently support. let location = self.elaborator.interner.expr_location(&id); @@ -585,7 +624,6 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { // Avoid resetting the value if it is already known let global_id = *global_id; let global_info = self.elaborator.interner.get_global(global_id); - let global_crate_id = global_info.crate_id; match &global_info.value { GlobalValue::Resolved(value) => Ok(value.clone()), GlobalValue::Resolving => { @@ -622,9 +660,7 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { .iter() .find(|typ| typ.name.as_str() == name) .expect("Expected to find associated type"); - let Kind::Numeric(numeric_type) = associated_type.typ.kind() else { - unreachable!("Expected associated type to be numeric"); - }; + let location = self.elaborator.interner.expr_location(&id); match associated_type .typ @@ -691,8 +727,6 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { id: ExprId, ) -> IResult { let mut result = String::new(); - let mut escaped = false; - let mut consuming = false; let mut values: VecDeque<_> = captures.into_iter().map(|capture| self.evaluate(capture)).collect::>()?; @@ -870,7 +904,6 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { ) -> IResult { let method = prefix.trait_method_id.expect("ice: expected prefix operator trait at this point"); - let operator = prefix.operator; let method_id = resolve_trait_item(self.elaborator.interner, method, id)?.unwrap_method(); let type_bindings = self.elaborator.interner.get_instantiation_bindings(id).clone(); @@ -1055,7 +1088,7 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { cast::evaluate_cast_one_step(&cast.r#type, location, evaluated_lhs) } - fn evaluate_if(&mut self, if_: HirIfExpression, id: ExprId) -> IResult { + fn evaluate_if(&mut self, if_: HirIfExpression) -> IResult { let condition = match self.evaluate(if_.condition)? { Value::Bool(value) => value, value => { @@ -1106,7 +1139,7 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { Ok(Value::Closure(Box::new(closure))) } - fn evaluate_quote(&mut self, mut tokens: Tokens) -> IResult { + fn evaluate_quote(&mut self, tokens: Tokens) -> IResult { let tokens = self.substitute_unquoted_values_into_tokens(tokens)?; Ok(Value::Quoted(Rc::new(tokens))) } @@ -1167,7 +1200,7 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { fn store_lvalue(&mut self, lvalue: HirLValue, rhs: Value) -> IResult<()> { match lvalue { - HirLValue::Ident(ident, typ) => self.mutate(ident.id, rhs, ident.location), + HirLValue::Ident(ident, _typ) => self.mutate(ident.id, rhs, ident.location), HirLValue::Dereference { lvalue, element_type: _, location, implicitly_added: _ } => { match self.evaluate_lvalue(&lvalue)? { Value::Pointer(value, _, _) => { @@ -1258,7 +1291,7 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { Value::Pointer(elem, true, _) => Ok(elem.borrow().clone()), other => Ok(other), }, - HirLValue::Dereference { lvalue, element_type, location, implicitly_added: _ } => { + HirLValue::Dereference { lvalue, element_type: _, location, implicitly_added: _ } => { match self.evaluate_lvalue(lvalue)? { Value::Pointer(value, _, _) => Ok(value.borrow().clone()), value => { @@ -1309,7 +1342,7 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { Value::I16(_) => |i| Value::I16(i as i16), Value::I32(_) => |i| Value::I32(i as i32), Value::I64(_) => |i| Value::I64(i as i64), - value => unreachable!("Checked above that value is signed type"), + _ => unreachable!("Checked above that value is signed type"), }; // i128 can store all values from i8 - u64