From de0eec21dbd35eb0a38755d0bcfb365c125bff70 Mon Sep 17 00:00:00 2001 From: jfecher Date: Mon, 6 Mar 2023 08:45:21 -0600 Subject: [PATCH] chore: Document type checking (#927) * Document name resolution * Document type checking * Fix typos * More docs --- crates/noirc_frontend/src/hir/mod.rs | 4 +- .../noirc_frontend/src/hir/resolution/mod.rs | 2 +- .../noirc_frontend/src/hir/type_check/expr.rs | 8 +++ .../noirc_frontend/src/hir/type_check/mod.rs | 30 +++++++----- .../noirc_frontend/src/hir/type_check/stmt.rs | 8 +++ crates/noirc_frontend/src/hir_def/types.rs | 49 +++++++++++++++++-- 6 files changed, 85 insertions(+), 16 deletions(-) diff --git a/crates/noirc_frontend/src/hir/mod.rs b/crates/noirc_frontend/src/hir/mod.rs index 64c12451318..984231545be 100644 --- a/crates/noirc_frontend/src/hir/mod.rs +++ b/crates/noirc_frontend/src/hir/mod.rs @@ -11,7 +11,9 @@ use def_map::CrateDefMap; use fm::FileManager; use std::collections::HashMap; -/// Global context that is accessible during each stage +/// Helper object which groups together several useful context objects used +/// during name resolution. Once name resolution is finished, only the +/// def_interner is required for type inference and monomorphization. #[derive(Default)] pub struct Context { pub def_interner: NodeInterner, diff --git a/crates/noirc_frontend/src/hir/resolution/mod.rs b/crates/noirc_frontend/src/hir/resolution/mod.rs index 32d6e0b07bd..601e78015ca 100644 --- a/crates/noirc_frontend/src/hir/resolution/mod.rs +++ b/crates/noirc_frontend/src/hir/resolution/mod.rs @@ -1,5 +1,5 @@ //! This set of modules implements the second half of the name resolution pass. -//! After all definitions are collected by def_collector, resovler::Resolvers are +//! After all definitions are collected by def_collector, resolver::Resolvers are //! created to resolve all names within a definition. In this context 'resolving' //! a name means validating that it has a valid definition, and that it was not //! redefined multiple times in the same scope. Once this is validated, it is linked diff --git a/crates/noirc_frontend/src/hir/type_check/expr.rs b/crates/noirc_frontend/src/hir/type_check/expr.rs index 88f768a71af..4f5ddbca9d8 100644 --- a/crates/noirc_frontend/src/hir/type_check/expr.rs +++ b/crates/noirc_frontend/src/hir/type_check/expr.rs @@ -12,6 +12,14 @@ use crate::{ use super::{bind_pattern, errors::TypeCheckError}; +/// Infers a type for a given expression, and return this type. +/// As a side-effect, this function will also remember this type in the NodeInterner +/// for the given expr_id key. +/// +/// This function also converts any HirExpression::MethodCalls `a.foo(b, c)` into +/// an equivalent HirExpression::Call in the form `foo(a, b, c)`. This cannot +/// be done earlier since we need to know the type of the object `a` to resolve which +/// function `foo` to refer to. pub(crate) fn type_check_expression( interner: &mut NodeInterner, expr_id: &ExprId, diff --git a/crates/noirc_frontend/src/hir/type_check/mod.rs b/crates/noirc_frontend/src/hir/type_check/mod.rs index 29b1d7e0061..357e9a6f62b 100644 --- a/crates/noirc_frontend/src/hir/type_check/mod.rs +++ b/crates/noirc_frontend/src/hir/type_check/mod.rs @@ -1,11 +1,19 @@ +//! This file contains type_check_func, the entry point to the type checking pass (for each function). +//! +//! The pass structure of type checking is relatively straightforward. It is a single pass through +//! the HIR of each function and outputs the inferred type of each HIR node into the NodeInterner, +//! keyed by the ID of the node. +//! +//! The algorithm for checking and inferring types itself is somewhat ad-hoc. It includes both +//! unification and subtyping, with the only difference between the two being how CompTime +//! is handled (See note on CompTime and make_subtype_of for details). Additionally, although +//! this algorithm features inference via TypeVariables, there is no generalization step as +//! all functions are required to give their full signatures. Closures are inferred but are +//! never generalized and thus cannot be used polymorphically. mod errors; mod expr; mod stmt; -// Type checking at the moment is very simple due to what is supported in the grammar. -// If polymorphism is never need, then Wands algorithm should be powerful enough to accommodate -// all foreseeable types, if it is needed then we would need to switch to Hindley-Milner type or maybe bidirectional - pub use errors::TypeCheckError; use expr::type_check_expression; @@ -16,23 +24,23 @@ pub(crate) use self::stmt::{bind_pattern, type_check}; /// Type checks a function and assigns the /// appropriate types to expressions in a side table pub fn type_check_func(interner: &mut NodeInterner, func_id: FuncId) -> Vec { - // First fetch the metadata and add the types for parameters - // Note that we do not look for the defining Identifier for a parameter, - // since we know that it is the parameter itself let meta = interner.function_meta(&func_id); let declared_return_type = meta.return_type().clone(); let can_ignore_ret = meta.can_ignore_return_type(); + // Bind each parameter to its annotated type. + // This is locally obvious, but it must be bound here so that the + // Definition object of the parameter in the NodeInterner is given the correct type. let mut errors = vec![]; for param in meta.parameters.into_iter() { bind_pattern(interner, ¶m.0, param.1, &mut errors); } // Fetch the HirFunction and iterate all of it's statements - let hir_func = interner.function(&func_id); - let func_as_expr = hir_func.as_expr(); + let function_body = interner.function(&func_id); + let function_body_id = function_body.as_expr(); - let function_last_type = type_check_expression(interner, func_as_expr, &mut errors); + let function_last_type = type_check_expression(interner, function_body_id, &mut errors); // Go through any delayed type checking errors to see if they are resolved, or error otherwise. for type_check_fn in interner.take_delayed_type_check_functions() { @@ -43,7 +51,7 @@ pub fn type_check_func(interner: &mut NodeInterner, func_id: FuncId) -> Vec, Box), // Array(4, Field) = [Field; 4] - Integer(CompTime, Signedness, u32), // u32 = Integer(unsigned, 32) - PolymorphicInteger(CompTime, TypeVariable), + + /// Array(N, E) is an array of N elements of type E. It is expected that N + /// is either a type variable of some kind or a Type::Constant. + Array(Box, Box), + + /// A primitive integer type with the given sign, bit count, and whether it is known at compile-time. + /// E.g. `u32` would be `Integer(CompTime::No(None), Unsigned, 32)` + Integer(CompTime, Signedness, u32), + + /// The primitive `bool` type. Like other primitive types, whether bools are known at CompTime + /// is also tracked. Unlike the other primitives however, it isn't as useful since it is + /// primarily only used when converting between a bool and an integer type for indexing arrays. Bool(CompTime), + + /// String(N) is an array of characters of length N. It is expected that N + /// is either a type variable of some kind or a Type::Constant. String(Box), + + /// The unit type `()`. Unit, + + /// A user-defined struct type. The `Shared` field here refers to + /// the shared definition for each instance of this struct type. The `Vec` + /// represents the generic arguments (if any) to this struct type. Struct(Shared, Vec), + + /// A tuple type with the given list of fields in the order they appear in source code. Tuple(Vec), + + /// TypeVariables are stand-in variables for some type which is not yet known. + /// They are not to be confused with NamedGenerics. While the later mostly works + /// as with normal types (ie. for two NamedGenerics T and U, T != U), TypeVariables + /// will be automatically rebound as necessary to satisfy any calls to unify + /// and make_subtype_of. + /// + /// TypeVariables are often created when a generic function is instantiated. This + /// is a process that replaces each NamedGeneric in a generic function with a TypeVariable. + /// Doing this at each call site of a generic function is how they can be called with + /// different argument types each time. TypeVariable(TypeVariable), + /// A generic integer or field type. This is a more specific kind of TypeVariable + /// that can only be bound to Type::Field, Type::Integer, or other PolymorphicIntegers. + /// This is the type of undecorated integer literals like `46`. Typing them in this way + /// allows them to be polymorphic over the actual integer/field type used without requiring + /// type annotations on each integer literal. + PolymorphicInteger(CompTime, TypeVariable), + /// NamedGenerics are the 'T' or 'U' in a user-defined generic function /// like `fn foo(...) {}`. Unlike TypeVariables, they cannot be bound over. NamedGeneric(TypeVariable, Rc), @@ -42,6 +81,10 @@ pub enum Type { /// bind to an integer without special checks to bind it to a non-type. Constant(u64), + /// The result of some type error. Remembering type errors as their own type variant lets + /// us avoid issuing repeat type errors for the same item. For example, a lambda with + /// an invalid type would otherwise issue a new error each time it is called + /// if not for this variant. Error, }