Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 11 additions & 4 deletions compiler/noirc_frontend/src/elaborator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1164,10 +1164,17 @@ impl<'context> Elaborator<'context> {
has_inline_attribute: bool,
location: Location,
) {
if (is_entry_point && !typ.is_valid_for_program_input())
|| (has_inline_attribute && !typ.is_valid_non_inlined_function_input())
{
self.push_err(TypeCheckError::InvalidTypeForEntryPoint { location });
if is_entry_point {
if let Some(invalid_type) = typ.program_input_validity() {
self.push_err(TypeCheckError::InvalidTypeForEntryPoint { invalid_type, location });
return;
}
}

if has_inline_attribute {
if let Some(invalid_type) = typ.non_inlined_function_input_validity() {
self.push_err(TypeCheckError::InvalidTypeForEntryPoint { invalid_type, location });
}
}
}

Expand Down
87 changes: 82 additions & 5 deletions compiler/noirc_frontend/src/hir/type_check/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::hir_def::types::{BinaryTypeOperator, Kind, Type};
use crate::node_interner::NodeInterner;
use crate::shared::Signedness;
use crate::signed_field::SignedField;
use crate::validity::InvalidType;

/// Rust also only shows 3 maximum, even for short patterns.
pub const MAX_MISSING_CASES: usize = 3;
Expand Down Expand Up @@ -208,7 +209,7 @@ pub enum TypeCheckError {
#[error("Expected a constant, but found `{typ}`")]
NonConstantEvaluated { typ: Type, location: Location },
#[error("Only sized types may be used in the entry point to a program")]
InvalidTypeForEntryPoint { location: Location },
InvalidTypeForEntryPoint { invalid_type: InvalidType, location: Location },
#[error("Mismatched number of parameters in trait implementation")]
MismatchTraitImplNumParameters {
actual_num_parameters: usize,
Expand Down Expand Up @@ -332,7 +333,7 @@ impl TypeCheckError {
| TypeCheckError::Unsafe { location }
| TypeCheckError::UnsafeFn { location }
| TypeCheckError::NonConstantEvaluated { location, .. }
| TypeCheckError::InvalidTypeForEntryPoint { location }
| TypeCheckError::InvalidTypeForEntryPoint { location, .. }
| TypeCheckError::MismatchTraitImplNumParameters { location, .. }
| TypeCheckError::StringIndexAssign { location }
| TypeCheckError::MacroReturningNonExpr { location, .. }
Expand Down Expand Up @@ -627,9 +628,19 @@ impl<'a> From<&'a TypeCheckError> for Diagnostic {
let msg = format!("Constraint for `{typ}: {trait_name}` is not needed, another matching impl is already in scope");
Diagnostic::simple_warning(msg, "Unnecessary trait constraint in where clause".into(), *location)
}
TypeCheckError::InvalidTypeForEntryPoint { location } => Diagnostic::simple_error(
"Only sized types may be used in the entry point to a program".to_string(),
"Slices, references, or any type containing them may not be used in main, contract functions, test functions, fuzz functions or foldable functions".to_string(), *location),
TypeCheckError::InvalidTypeForEntryPoint { invalid_type, location } => {
let primary_message = "Invalid type found in the entry point to a program".to_string();
let mut diagnostic = Diagnostic::simple_error(primary_message, String::new(), *location);
diagnostic.secondaries.clear();

if matches!(invalid_type, InvalidType::StructField {..} | InvalidType::Alias {..}) {
diagnostic.add_secondary("This type has an invalid entry point type inside it".to_string(), *location);
}

diagnostic.add_note("Note: slices, references, empty arrays, empty strings, or any type containing them may not be used in main, contract functions, test functions, fuzz functions or foldable functions.".to_string());
add_invalid_type_to_diagnostic(invalid_type, *location, &mut diagnostic);
diagnostic
},
TypeCheckError::MismatchTraitImplNumParameters {
expected_num_parameters,
actual_num_parameters,
Expand Down Expand Up @@ -816,3 +827,69 @@ impl NoMatchingImplFoundError {
Some(Self { constraints, location })
}
}

fn add_invalid_type_to_diagnostic(
invalid_type: &InvalidType,
location: Location,
diagnostic: &mut Diagnostic,
) {
match invalid_type {
InvalidType::Primitive(typ) => match typ {
// Use a slightly better message for common types that might be used as entry point types
Type::Unit => {
diagnostic.add_secondary(format!("Unit is not a valid entry point type"), location);
}
Type::Reference(..) => {
diagnostic.add_secondary(
format!("Reference is not a valid entry point type. Found: {typ}"),
location,
);
}
Type::Slice(..) => {
diagnostic.add_secondary(
format!("Slice is not a valid entry point type. Found: {typ}"),
location,
);
}
_ => {
diagnostic.add_secondary(format!("Invalid entry point type: {typ}"), location);
}
},
InvalidType::Enum(typ) => {
diagnostic.add_secondary(
format!("Enum is not yet allowed as an entry point type. Found: {typ}"),
location,
);
}
InvalidType::EmptyArray(typ) => {
diagnostic.add_secondary(
format!("Empty array is not a valid entry point type. Found: {typ}"),
location,
);
}
InvalidType::EmptyString(typ) => {
diagnostic.add_secondary(
format!("Empty string is not a valid entry point type. Found: {typ}"),
location,
);
}
InvalidType::StructField { struct_name, field_name, invalid_type } => {
diagnostic.add_secondary(
format!("Struct {struct_name} has an invalid entry point type"),
struct_name.location(),
);
diagnostic.add_secondary(
format!("Field {field_name} has an invalid entry point type"),
field_name.location(),
);
add_invalid_type_to_diagnostic(invalid_type, field_name.location(), diagnostic);
}
InvalidType::Alias { alias_name, invalid_type } => {
diagnostic.add_secondary(
format!("Alias {alias_name} has an invalid entry point type"),
alias_name.location(),
);
add_invalid_type_to_diagnostic(invalid_type, alias_name.location(), diagnostic);
}
}
}
188 changes: 1 addition & 187 deletions compiler/noirc_frontend/src/hir_def/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use super::traits::NamedType;

mod arithmetic;
mod unification;
pub(crate) mod validity;

pub use unification::UnificationError;

Expand Down Expand Up @@ -1352,193 +1353,6 @@ impl Type {
matches!(self.follow_bindings_shallow().as_ref(), Type::Reference(_, _))
}

/// True if this type can be used as a parameter to `main` or a contract function.
/// This is only false for unsized types like slices or slices that do not make sense
/// as a program input such as named generics or mutable references.
///
/// This function should match the same check done in `create_value_from_type` in acir_gen.
/// If this function does not catch a case where a type should be valid, it will later lead to a
/// panic in that function instead of a user-facing compiler error message.
pub(crate) fn is_valid_for_program_input(&self) -> bool {
match self {
// Type::Error is allowed as usual since it indicates an error was already issued and
// we don't need to issue further errors about this likely unresolved type
// TypeVariable and Generic are allowed here too as they can only result from
// generics being declared on the function itself, but we produce a different error in that case.
Type::FieldElement
| Type::Integer(_, _)
| Type::Bool
| Type::Constant(_, _)
| Type::TypeVariable(_)
| Type::NamedGeneric(_)
| Type::Error => true,

Type::Unit
| Type::FmtString(_, _)
| Type::Function(_, _, _, _)
| Type::Reference(..)
| Type::Forall(_, _)
| Type::Quoted(_)
| Type::Slice(_)
| Type::TraitAsType(..) => false,

Type::CheckedCast { to, .. } => to.is_valid_for_program_input(),

Type::Alias(alias, generics) => {
let alias = alias.borrow();
alias.get_type(generics).is_valid_for_program_input()
}

Type::Array(length, element) => {
self.array_or_string_len_is_not_zero()
&& length.is_valid_for_program_input()
&& element.is_valid_for_program_input()
}
Type::String(length) => {
self.array_or_string_len_is_not_zero() && length.is_valid_for_program_input()
}
Type::Tuple(elements) => elements.iter().all(|elem| elem.is_valid_for_program_input()),
Type::DataType(definition, generics) => {
if let Some(fields) = definition.borrow().get_fields(generics) {
fields.into_iter().all(|(_, field, _)| field.is_valid_for_program_input())
} else {
// Arbitrarily disallow enums from program input, though we may support them later
false
}
}

Type::InfixExpr(lhs, _, rhs, _) => {
lhs.is_valid_for_program_input() && rhs.is_valid_for_program_input()
}
}
}

/// Empty arrays and strings (which are arrays under the hood) are disallowed
/// as input to program entry points.
///
/// The point of inputs to entry points is to process input data.
/// Thus, passing empty arrays is pointless and adds extra complexity to the compiler
/// for handling them.
fn array_or_string_len_is_not_zero(&self) -> bool {
match self {
Type::Array(length, _) | Type::String(length) => {
let length = length.evaluate_to_u32(Location::dummy()).unwrap_or(0);
length != 0
}
_ => panic!("ICE: Expected an array or string type"),
}
}

/// True if this type can be used as a parameter to an ACIR function that is not `main` or a contract function.
/// This encapsulates functions for which we may not want to inline during compilation.
///
/// The inputs allowed for a function entry point differ from those allowed as input to a program as there are
/// certain types which through compilation we know what their size should be.
/// This includes types such as numeric generics.
pub(crate) fn is_valid_non_inlined_function_input(&self) -> bool {
match self {
// Type::Error is allowed as usual since it indicates an error was already issued and
// we don't need to issue further errors about this likely unresolved type
Type::FieldElement
| Type::Integer(_, _)
| Type::Bool
| Type::Unit
| Type::Constant(_, _)
| Type::TypeVariable(_)
| Type::NamedGeneric(_)
| Type::InfixExpr(..)
| Type::Error => true,

Type::FmtString(_, _)
// To enable this we would need to determine the size of the closure outputs at compile-time.
// This is possible as long as the output size is not dependent upon a witness condition.
| Type::Function(_, _, _, _)
| Type::Slice(_)
| Type::Reference(..)
| Type::Forall(_, _)
// TODO: probably can allow code as it is all compile time
| Type::Quoted(_)
| Type::TraitAsType(..) => false,

Type::CheckedCast { to, .. } => to.is_valid_non_inlined_function_input(),

Type::Alias(alias, generics) => {
let alias = alias.borrow();
alias.get_type(generics).is_valid_non_inlined_function_input()
}

Type::Array(length, element) => {
length.is_valid_non_inlined_function_input() && element.is_valid_non_inlined_function_input()
}
Type::String(length) => length.is_valid_non_inlined_function_input(),
Type::Tuple(elements) => elements.iter().all(|elem| elem.is_valid_non_inlined_function_input()),
Type::DataType(definition, generics) => {
if let Some(fields) = definition.borrow().get_fields(generics) {
fields.into_iter()
.all(|(_, field, _)| field.is_valid_non_inlined_function_input())
} else {
false
}
}
}
}

/// Returns true if a value of this type can safely pass between constrained and
/// unconstrained functions (and vice-versa).
pub(crate) fn is_valid_for_unconstrained_boundary(&self) -> bool {
match self {
Type::FieldElement
| Type::Integer(_, _)
| Type::Bool
| Type::Unit
| Type::Constant(_, _)
| Type::Slice(_)
| Type::Function(_, _, _, _)
| Type::FmtString(_, _)
| Type::InfixExpr(..)
| Type::Error => true,

Type::TypeVariable(type_var) | Type::NamedGeneric(NamedGeneric { type_var, .. }) => {
if let TypeBinding::Bound(typ) = &*type_var.borrow() {
typ.is_valid_for_unconstrained_boundary()
} else {
true
}
}

Type::CheckedCast { to, .. } => to.is_valid_for_unconstrained_boundary(),

// Quoted objects only exist at compile-time where the only execution
// environment is the interpreter. In this environment, they are valid.
Type::Quoted(_) => true,

Type::Reference(..) | Type::Forall(_, _) | Type::TraitAsType(..) => false,

Type::Alias(alias, generics) => {
let alias = alias.borrow();
alias.get_type(generics).is_valid_for_unconstrained_boundary()
}

Type::Array(length, element) => {
length.is_valid_for_unconstrained_boundary()
&& element.is_valid_for_unconstrained_boundary()
}
Type::String(length) => length.is_valid_for_unconstrained_boundary(),
Type::Tuple(elements) => {
elements.iter().all(|elem| elem.is_valid_for_unconstrained_boundary())
}
Type::DataType(definition, generics) => {
if let Some(fields) = definition.borrow().get_fields(generics) {
fields
.into_iter()
.all(|(_, field, _)| field.is_valid_for_unconstrained_boundary())
} else {
false
}
}
}
}

/// Returns the number of `Forall`-quantified type variables on this type.
/// Returns 0 if this is not a Type::Forall
pub fn generic_count(&self) -> usize {
Expand Down
Loading
Loading