diff --git a/crates/analyzer/src/context.rs b/crates/analyzer/src/context.rs index fa6e3c5ef7..cf7dcab96d 100644 --- a/crates/analyzer/src/context.rs +++ b/crates/analyzer/src/context.rs @@ -508,7 +508,7 @@ impl CallType { // check that this is the `Context` struct defined in `std` // this should be deleted once associated functions are supported and we can // define unsafe constructors in Fe - struct_.name == "Context" && struct_.id.module(db).ingot(db).name(db) == "std" + struct_.name == "Context" && struct_.id.module(db).is_in_std(db) } else { self.function().map(|id| id.is_unsafe(db)).unwrap_or(false) } diff --git a/crates/analyzer/src/db.rs b/crates/analyzer/src/db.rs index bbcc476a90..67a1057a15 100644 --- a/crates/analyzer/src/db.rs +++ b/crates/analyzer/src/db.rs @@ -2,7 +2,7 @@ use crate::context::{Analysis, Constant, FunctionBody}; use crate::errors::{ConstEvalError, TypeError}; use crate::namespace::items::{ self, ContractFieldId, ContractId, DepGraphWrapper, EventId, FunctionId, IngotId, Item, - ModuleConstantId, ModuleId, StructFieldId, StructId, TypeAliasId, + ModuleConstantId, ModuleId, StructFieldId, StructId, TraitId, TypeAliasId, }; use crate::namespace::types; use fe_common::db::{SourceDb, SourceDbStorage, Upcast, UpcastMut}; @@ -24,6 +24,8 @@ pub trait AnalyzerDb: SourceDb + Upcast + UpcastMut #[salsa::interned] fn intern_struct(&self, data: Rc) -> StructId; #[salsa::interned] + fn intern_trait(&self, data: Rc) -> TraitId; + #[salsa::interned] fn intern_struct_field(&self, data: Rc) -> StructFieldId; #[salsa::interned] fn intern_type_alias(&self, data: Rc) -> TypeAliasId; @@ -154,6 +156,10 @@ pub trait AnalyzerDb: SourceDb + Upcast + UpcastMut #[salsa::invoke(queries::structs::struct_dependency_graph)] fn struct_dependency_graph(&self, id: StructId) -> Analysis; + // Trait + #[salsa::invoke(queries::traits::trait_type)] + fn trait_type(&self, id: TraitId) -> Rc; + // Event #[salsa::invoke(queries::events::event_type)] fn event_type(&self, event: EventId) -> Analysis>; diff --git a/crates/analyzer/src/db/queries.rs b/crates/analyzer/src/db/queries.rs index f6c5db8ad6..59519f493f 100644 --- a/crates/analyzer/src/db/queries.rs +++ b/crates/analyzer/src/db/queries.rs @@ -4,4 +4,5 @@ pub mod functions; pub mod ingots; pub mod module; pub mod structs; +pub mod traits; pub mod types; diff --git a/crates/analyzer/src/db/queries/functions.rs b/crates/analyzer/src/db/queries/functions.rs index 0facf832a4..493a57b586 100644 --- a/crates/analyzer/src/db/queries/functions.rs +++ b/crates/analyzer/src/db/queries/functions.rs @@ -1,13 +1,15 @@ use crate::context::{AnalyzerContext, CallType, FunctionBody}; use crate::db::{Analysis, AnalyzerDb}; use crate::errors::TypeError; -use crate::namespace::items::{DepGraph, DepGraphWrapper, DepLocality, FunctionId, Item, TypeDef}; +use crate::namespace::items::{ + Class, DepGraph, DepGraphWrapper, DepLocality, FunctionId, Item, TypeDef, +}; use crate::namespace::scopes::{BlockScope, BlockScopeType, FunctionScope, ItemScope}; -use crate::namespace::types::{self, Contract, CtxDecl, SelfDecl, Struct, Type}; +use crate::namespace::types::{self, Contract, CtxDecl, Generic, SelfDecl, Struct, Type}; use crate::traversal::functions::traverse_statements; use crate::traversal::types::type_desc; use fe_common::diagnostics::Label; -use fe_parser::ast; +use fe_parser::ast::{self, GenericParameter}; use fe_parser::node::Node; use if_chain::if_chain; use std::collections::HashMap; @@ -30,6 +32,17 @@ pub fn function_signature( let mut names = HashMap::new(); let mut labels = HashMap::new(); + if let (Some(Class::Contract(_)), true) = (fn_parent, function.is_generic(db)) { + scope.fancy_error( + "generic function parameters aren't yet supported in contract functions", + vec![Label::primary( + function.data(db).ast.kind.generic_params.span, + "This can not appear here", + )], + vec!["Hint: Struct functions can have generic parameters".into()], + ); + } + let params = def .args .iter() @@ -55,7 +68,7 @@ pub fn function_signature( None } ast::FunctionArg::Regular(reg) => { - let typ = type_desc(&mut scope, ®.typ).and_then(|typ| match typ { + let typ = resolve_function_param_type(db, function, &mut scope, ®.typ).and_then(|typ| match typ { typ if typ.has_fixed_size() => Ok(typ), _ => Err(TypeError::new(scope.error( "function parameter types must have fixed size", @@ -192,6 +205,40 @@ pub fn function_signature( } } +pub fn resolve_function_param_type( + db: &dyn AnalyzerDb, + function: FunctionId, + context: &mut dyn AnalyzerContext, + desc: &Node, +) -> Result { + // First check if the param type is a local generic. This may not hold when generics can appear + // on contract, struct or module level but it's good enough to get the party started. + if let ast::TypeDesc::Base { base } = &desc.kind { + if let Some(val) = function.generic_param(db, base) { + let bounds = match val { + GenericParameter::Unbounded(_) => vec![], + GenericParameter::Bounded { bound, .. } => match type_desc(context, &bound)? { + Type::Trait(trait_ty) => vec![trait_ty.id], + _ => { + context.error( + "Only traits can be used as generic bounds", + bound.span, + "Not a trait", + ); + vec![] + } + }, + }; + + return Ok(Type::Generic(Generic { + name: base.clone(), + bounds, + })); + } + } + type_desc(context, desc) +} + /// Gather context information for a function body and check for type errors. pub fn function_body(db: &dyn AnalyzerDb, function: FunctionId) -> Analysis> { let def = &function.data(db).ast.kind; diff --git a/crates/analyzer/src/db/queries/module.rs b/crates/analyzer/src/db/queries/module.rs index 4dfc65d69e..acca1dcce0 100644 --- a/crates/analyzer/src/db/queries/module.rs +++ b/crates/analyzer/src/db/queries/module.rs @@ -3,7 +3,7 @@ use crate::db::AnalyzerDb; use crate::errors::{self, ConstEvalError, TypeError}; use crate::namespace::items::{ Contract, ContractId, Event, Function, Item, ModuleConstant, ModuleConstantId, ModuleId, - ModuleSource, Struct, StructId, TypeAlias, TypeDef, + ModuleSource, Struct, StructId, Trait, TypeAlias, TypeDef, }; use crate::namespace::scopes::ItemScope; use crate::namespace::types::{self, Type}; @@ -59,7 +59,6 @@ pub fn module_is_incomplete(db: &dyn AnalyzerDb, module: ModuleId) -> bool { pub fn module_all_items(db: &dyn AnalyzerDb, module: ModuleId) -> Rc<[Item]> { let body = &module.ast(db).body; - body.iter() .filter_map(|stmt| match stmt { ast::ModuleStmt::TypeAlias(node) => Some(Item::Type(TypeDef::Alias( @@ -95,7 +94,12 @@ pub fn module_all_items(db: &dyn AnalyzerDb, module: ModuleId) -> Rc<[Item]> { })))) } ast::ModuleStmt::Pragma(_) => None, - ast::ModuleStmt::Trait(_) => None, + ast::ModuleStmt::Trait(node) => Some(Item::Type(TypeDef::Trait(db.intern_trait( + Rc::new(Trait { + ast: node.clone(), + module, + }), + )))), ast::ModuleStmt::Use(_) => None, ast::ModuleStmt::Event(node) => Some(Item::Event(db.intern_event(Rc::new(Event { ast: node.clone(), diff --git a/crates/analyzer/src/db/queries/traits.rs b/crates/analyzer/src/db/queries/traits.rs new file mode 100644 index 0000000000..e0a4cbd2f5 --- /dev/null +++ b/crates/analyzer/src/db/queries/traits.rs @@ -0,0 +1,11 @@ +use crate::namespace::items::TraitId; +use crate::namespace::types; +use crate::AnalyzerDb; +use std::rc::Rc; + +pub fn trait_type(db: &dyn AnalyzerDb, trait_: TraitId) -> Rc { + Rc::new(types::Trait { + name: trait_.name(db), + id: trait_, + }) +} diff --git a/crates/analyzer/src/namespace/items.rs b/crates/analyzer/src/namespace/items.rs index 8cb18eb6d1..e42c25399a 100644 --- a/crates/analyzer/src/namespace/items.rs +++ b/crates/analyzer/src/namespace/items.rs @@ -9,6 +9,7 @@ use crate::{builtins, errors::ConstEvalError}; use fe_common::diagnostics::Diagnostic; use fe_common::files::{common_prefix, Utf8Path}; use fe_common::{impl_intern_key, FileKind, SourceFileId}; +use fe_parser::ast::GenericParameter; use fe_parser::node::{Node, Span}; use fe_parser::{ast, node::NodeId}; use indexmap::{indexmap, IndexMap}; @@ -469,6 +470,10 @@ impl ModuleId { db.module_is_incomplete(*self) } + pub fn is_in_std(&self, db: &dyn AnalyzerDb) -> bool { + self.ingot(db).name(db) == "std" + } + /// Includes duplicate names pub fn all_items(&self, db: &dyn AnalyzerDb) -> Rc<[Item]> { db.module_all_items(*self) @@ -747,6 +752,7 @@ impl ModuleConstantId { pub enum TypeDef { Alias(TypeAliasId), Struct(StructId), + Trait(TraitId), Contract(ContractId), Primitive(types::Base), } @@ -778,6 +784,7 @@ impl TypeDef { match self { TypeDef::Alias(id) => id.name(db), TypeDef::Struct(id) => id.name(db), + TypeDef::Trait(id) => id.name(db), TypeDef::Contract(id) => id.name(db), TypeDef::Primitive(typ) => typ.name(), } @@ -787,6 +794,7 @@ impl TypeDef { match self { TypeDef::Alias(id) => Some(id.name_span(db)), TypeDef::Struct(id) => Some(id.name_span(db)), + TypeDef::Trait(id) => Some(id.name_span(db)), TypeDef::Contract(id) => Some(id.name_span(db)), TypeDef::Primitive(_) => None, } @@ -800,6 +808,10 @@ impl TypeDef { name: id.name(db), field_count: id.fields(db).len(), // for the EvmSized trait })), + TypeDef::Trait(id) => Ok(types::Type::Trait(types::Trait { + id: *id, + name: id.name(db), + })), TypeDef::Contract(id) => Ok(types::Type::Contract(types::Contract { id: *id, name: id.name(db), @@ -812,6 +824,7 @@ impl TypeDef { match self { Self::Alias(id) => id.is_public(db), Self::Struct(id) => id.is_public(db), + Self::Trait(id) => id.is_public(db), Self::Contract(id) => id.is_public(db), Self::Primitive(_) => true, } @@ -821,6 +834,7 @@ impl TypeDef { match self { TypeDef::Alias(id) => Some(id.parent(db)), TypeDef::Struct(id) => Some(id.parent(db)), + TypeDef::Trait(id) => Some(id.parent(db)), TypeDef::Contract(id) => Some(id.parent(db)), TypeDef::Primitive(_) => None, } @@ -830,6 +844,7 @@ impl TypeDef { match self { TypeDef::Alias(id) => id.sink_diagnostics(db, sink), TypeDef::Struct(id) => id.sink_diagnostics(db, sink), + TypeDef::Trait(id) => id.sink_diagnostics(db, sink), TypeDef::Contract(id) => id.sink_diagnostics(db, sink), TypeDef::Primitive(_) => {} } @@ -1097,6 +1112,7 @@ impl FunctionId { pub fn takes_self(&self, db: &dyn AnalyzerDb) -> bool { self.signature(db).self_decl.is_some() } + pub fn self_span(&self, db: &dyn AnalyzerDb) -> Option { if self.takes_self(db) { self.data(db) @@ -1110,6 +1126,25 @@ impl FunctionId { } } + pub fn is_generic(&self, db: &dyn AnalyzerDb) -> bool { + !self.data(db).ast.kind.generic_params.kind.is_empty() + } + + pub fn generic_params(&self, db: &dyn AnalyzerDb) -> Vec { + self.data(db).ast.kind.generic_params.kind.clone() + } + + pub fn generic_param(&self, db: &dyn AnalyzerDb, param_name: &str) -> Option { + self.generic_params(db) + .iter() + .find(|param| match param { + GenericParameter::Unbounded(name) if name.kind == param_name => true, + GenericParameter::Bounded { name, .. } if name.kind == param_name => true, + _ => false, + }) + .cloned() + } + pub fn is_public(&self, db: &dyn AnalyzerDb) -> bool { self.pub_span(db).is_some() } @@ -1324,6 +1359,50 @@ impl StructFieldId { } } +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct Trait { + pub ast: Node, + pub module: ModuleId, +} + +#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)] +pub struct TraitId(pub(crate) u32); +impl_intern_key!(TraitId); +impl TraitId { + pub fn data(&self, db: &dyn AnalyzerDb) -> Rc { + db.lookup_intern_trait(*self) + } + pub fn span(&self, db: &dyn AnalyzerDb) -> Span { + self.data(db).ast.span + } + pub fn name(&self, db: &dyn AnalyzerDb) -> SmolStr { + self.data(db).ast.name().into() + } + pub fn name_span(&self, db: &dyn AnalyzerDb) -> Span { + self.data(db).ast.kind.name.span + } + + pub fn is_public(&self, db: &dyn AnalyzerDb) -> bool { + self.data(db).ast.kind.pub_qual.is_some() + } + + pub fn module(&self, db: &dyn AnalyzerDb) -> ModuleId { + self.data(db).module + } + + pub fn typ(&self, db: &dyn AnalyzerDb) -> Rc { + db.trait_type(*self) + } + pub fn is_in_std(&self, db: &dyn AnalyzerDb) -> bool { + self.module(db).is_in_std(db) + } + + pub fn parent(&self, db: &dyn AnalyzerDb) -> Item { + Item::Module(self.data(db).module) + } + pub fn sink_diagnostics(&self, _db: &dyn AnalyzerDb, _sink: &mut impl DiagnosticSink) {} +} + #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct Event { pub ast: Node, diff --git a/crates/analyzer/src/namespace/types.rs b/crates/analyzer/src/namespace/types.rs index cee6eba1d0..be544db837 100644 --- a/crates/analyzer/src/namespace/types.rs +++ b/crates/analyzer/src/namespace/types.rs @@ -1,5 +1,6 @@ use crate::context::AnalyzerContext; use crate::errors::TypeError; +use crate::namespace::items::TraitId; use crate::namespace::items::{Class, ContractId, StructId}; use crate::AnalyzerDb; @@ -34,6 +35,10 @@ pub trait SafeNames { fn lower_snake(&self) -> String; } +pub trait SatisfiesBound { + fn satisfies_bound(&self, db: &dyn AnalyzerDb, bound: &TraitId) -> bool; +} + #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Type { Base(Base), @@ -47,6 +52,16 @@ pub enum Type { /// of `self` within a contract function. SelfContract(Contract), Struct(Struct), + Trait(Trait), + Generic(Generic), +} + +impl SatisfiesBound for Type { + fn satisfies_bound(&self, db: &dyn AnalyzerDb, bound: &TraitId) -> bool { + // This obviously needs to be more sophisticated. This is just a quick way to + // implement some std marker traits + self.is_integer() && bound.name(db) == "IsNumeric" && bound.is_in_std(db) + } } #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -125,6 +140,26 @@ impl Struct { } } +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct Trait { + pub name: SmolStr, + pub id: TraitId, +} +impl Trait { + pub fn from_id(id: TraitId, db: &dyn AnalyzerDb) -> Self { + Self { + name: id.name(db), + id, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Generic { + pub name: SmolStr, + pub bounds: Vec, +} + #[derive(Clone, PartialEq, Eq, Hash)] pub struct Contract { pub name: SmolStr, @@ -388,6 +423,8 @@ impl Type { Type::Tuple(inner) => inner.to_string().into(), Type::String(inner) => inner.to_string().into(), Type::Struct(inner) => inner.name.clone(), + Type::Trait(inner) => inner.name.clone(), + Type::Generic(inner) => inner.name.clone(), Type::Contract(inner) | Type::SelfContract(inner) => inner.name.clone(), } } @@ -456,8 +493,9 @@ impl Type { | Type::Tuple(_) | Type::String(_) | Type::Struct(_) + | Type::Generic(_) | Type::Contract(_) => true, - Type::Map(_) | Type::SelfContract(_) => false, + Type::Map(_) | Type::SelfContract(_) | Type::Trait(_) => false, } } } @@ -637,6 +675,8 @@ impl fmt::Display for Type { Type::Contract(inner) => inner.fmt(f), Type::SelfContract(inner) => inner.fmt(f), Type::Struct(inner) => inner.fmt(f), + Type::Trait(inner) => inner.fmt(f), + Type::Generic(inner) => inner.fmt(f), } } } @@ -730,6 +770,23 @@ impl fmt::Debug for Struct { } } +impl fmt::Display for Trait { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name) + } +} +impl fmt::Debug for Trait { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Struct").field("name", &self.name).finish() + } +} + +impl fmt::Display for Generic { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name) + } +} + impl FromStr for Base { type Err = strum::ParseError; diff --git a/crates/analyzer/src/operations.rs b/crates/analyzer/src/operations.rs index 21814d90c9..e1ab87c00f 100644 --- a/crates/analyzer/src/operations.rs +++ b/crates/analyzer/src/operations.rs @@ -15,6 +15,8 @@ pub fn index(value: Type, index: Type) -> Result { | Type::String(_) | Type::Contract(_) | Type::SelfContract(_) + | Type::Trait(_) + | Type::Generic(_) | Type::Struct(_) => Err(IndexingError::NotSubscriptable), } } diff --git a/crates/analyzer/src/traversal/call_args.rs b/crates/analyzer/src/traversal/call_args.rs index 9238a0a045..d5134a96ad 100644 --- a/crates/analyzer/src/traversal/call_args.rs +++ b/crates/analyzer/src/traversal/call_args.rs @@ -1,6 +1,6 @@ use crate::context::{AnalyzerContext, DiagnosticVoucher}; use crate::errors::{FatalError, TypeError}; -use crate::namespace::types::{EventField, FunctionParam, Type}; +use crate::namespace::types::{EventField, FunctionParam, Generic, SatisfiesBound, Type}; use crate::traversal::expressions::assignable_expr; use fe_common::{diagnostics::Label, utils::humanize::pluralize_conditionally}; use fe_common::{Span, Spanned}; @@ -151,7 +151,7 @@ pub fn validate_named_args( let param_type = param.typ()?; let val_attrs = assignable_expr(context, &arg.kind.value, Some(¶m_type.clone()))?; - if param_type != val_attrs.typ { + if !validate_arg_type(context, arg, &val_attrs.typ, ¶m_type) { let msg = if let Some(label) = param.label() { format!("incorrect type for `{}` argument `{}`", name, label) } else { @@ -165,3 +165,35 @@ pub fn validate_named_args( } Ok(()) } + +fn validate_arg_type( + context: &mut dyn AnalyzerContext, + arg: &Node, + arg_type: &Type, + param_type: &Type, +) -> bool { + if let Type::Generic(Generic { bounds, .. }) = param_type { + for bound in bounds { + if !arg_type.satisfies_bound(context.db(), bound) { + context.error( + &format!( + "the trait bound `{}: {}` is not satisfied", + arg_type, + bound.name(context.db()) + ), + arg.span, + &format!( + "the trait `{}` is not implemented for `{}`", + bound.name(context.db()), + arg_type + ), + ); + + return false; + } + } + true + } else { + arg_type == param_type + } +} diff --git a/crates/analyzer/src/traversal/expressions.rs b/crates/analyzer/src/traversal/expressions.rs index 30e061c07f..d82575be02 100644 --- a/crates/analyzer/src/traversal/expressions.rs +++ b/crates/analyzer/src/traversal/expressions.rs @@ -179,7 +179,7 @@ pub fn assignable_expr( attributes.move_location = Some(Location::Value); } } - Array(_) | Tuple(_) | String(_) | Struct(_) => { + Array(_) | Tuple(_) | String(_) | Struct(_) | Trait(_) | Generic(_) => { if attributes.final_location() != Location::Memory { context.fancy_error( "value must be copied to memory", @@ -1223,6 +1223,8 @@ fn expr_call_type_constructor( Type::Struct(_) => unreachable!(), // handled above Type::Map(_) => unreachable!(), // handled above Type::Array(_) => unreachable!(), // handled above + Type::Trait(_) => unreachable!(), // TODO + Type::Generic(_) => unreachable!(), // TODO Type::SelfContract(_) => unreachable!(), /* unnameable; contract names all become * Type::Contract */ }; diff --git a/crates/analyzer/tests/analysis.rs b/crates/analyzer/tests/analysis.rs index 42df1684aa..955ecfd153 100644 --- a/crates/analyzer/tests/analysis.rs +++ b/crates/analyzer/tests/analysis.rs @@ -353,7 +353,9 @@ fn build_snapshot(db: &dyn AnalyzerDb, module: items::ModuleId) -> String { | Item::BuiltinFunction(_) | Item::Intrinsic(_) | Item::Ingot(_) - | Item::Module(_) => vec![], + | Item::Module(_) + // FIXME + | Item::Type(TypeDef::Trait(_)) => vec![], }) .collect::>(); diff --git a/crates/analyzer/tests/errors.rs b/crates/analyzer/tests/errors.rs index 4b36fe3bf8..4d55411d4a 100644 --- a/crates/analyzer/tests/errors.rs +++ b/crates/analyzer/tests/errors.rs @@ -216,6 +216,7 @@ test_file! { bad_string } test_file! { bad_tuple_attr1 } test_file! { bad_tuple_attr2 } test_file! { bad_tuple_attr3 } +test_file! { call_generic_function_with_unsatisfied_bound} test_file! { call_builtin_object } test_file! { call_create_with_wrong_type } test_file! { call_create2_with_wrong_type } @@ -247,6 +248,7 @@ test_file! { duplicate_var_in_for_loop } test_file! { emit_bad_args } test_file! { external_call_type_error } test_file! { external_call_wrong_number_of_params } +test_file! { contract_function_with_generic_params } test_file! { indexed_event } test_file! { invalid_compiler_version } test_file! { invalid_block_field } diff --git a/crates/analyzer/tests/snapshots/errors__call_generic_function_with_unsatisfied_bound.snap b/crates/analyzer/tests/snapshots/errors__call_generic_function_with_unsatisfied_bound.snap new file mode 100644 index 0000000000..7e91beb201 --- /dev/null +++ b/crates/analyzer/tests/snapshots/errors__call_generic_function_with_unsatisfied_bound.snap @@ -0,0 +1,18 @@ +--- +source: crates/analyzer/tests/errors.rs +expression: "error_string(&path, test_files::fixture(path))" + +--- +error: the trait bound `bool: IsNumeric` is not satisfied + ┌─ compile_errors/call_generic_function_with_unsatisfied_bound.fe:14:13 + │ +14 │ foo.bar(true) + │ ^^^^ the trait `IsNumeric` is not implemented for `bool` + +error: incorrect type for `bar` argument at position 0 + ┌─ compile_errors/call_generic_function_with_unsatisfied_bound.fe:14:13 + │ +14 │ foo.bar(true) + │ ^^^^ this has type `bool`; expected type `T` + + diff --git a/crates/analyzer/tests/snapshots/errors__contract_function_with_generic_params.snap b/crates/analyzer/tests/snapshots/errors__contract_function_with_generic_params.snap new file mode 100644 index 0000000000..810727eaa3 --- /dev/null +++ b/crates/analyzer/tests/snapshots/errors__contract_function_with_generic_params.snap @@ -0,0 +1,14 @@ +--- +source: crates/analyzer/tests/errors.rs +expression: "error_string(&path, test_files::fixture(path))" + +--- +error: generic function parameters aren't yet supported in contract functions + ┌─ compile_errors/contract_function_with_generic_params.fe:4:15 + │ +4 │ pub fn bar(val: T) {} + │ ^^^^^^^^^^^^^^ This can not appear here + │ + = Hint: Struct functions can have generic parameters + + diff --git a/crates/library/std/src/traits.fe b/crates/library/std/src/traits.fe new file mode 100644 index 0000000000..db492d7ba3 --- /dev/null +++ b/crates/library/std/src/traits.fe @@ -0,0 +1,2 @@ +# Marker trait that all numeric types implement +pub trait IsNumeric {} \ No newline at end of file diff --git a/crates/mir/src/db.rs b/crates/mir/src/db.rs index e2c2525a7f..11cc30612b 100644 --- a/crates/mir/src/db.rs +++ b/crates/mir/src/db.rs @@ -51,6 +51,12 @@ pub trait MirDb: AnalyzerDb + Upcast + UpcastMut &self, analyzer_func: analyzer_items::FunctionId, ) -> ir::FunctionId; + #[salsa::invoke(queries::function::mir_lowered_monomorphized_func_signature)] + fn mir_lowered_monomorphized_func_signature( + &self, + analyzer_func: analyzer_items::FunctionId, + concrete_args: Vec, + ) -> ir::FunctionId; #[salsa::invoke(queries::function::mir_lowered_func_body)] fn mir_lowered_func_body(&self, func: ir::FunctionId) -> Rc; } diff --git a/crates/mir/src/db/queries/function.rs b/crates/mir/src/db/queries/function.rs index d863f36d18..7c32cb8470 100644 --- a/crates/mir/src/db/queries/function.rs +++ b/crates/mir/src/db/queries/function.rs @@ -1,12 +1,13 @@ use std::rc::Rc; use fe_analyzer::namespace::items as analyzer_items; +use fe_analyzer::namespace::types as analyzer_types; use smol_str::SmolStr; use crate::{ db::MirDb, ir::{self, function::Linkage, FunctionSignature, TypeId}, - lower::function::{lower_func_body, lower_func_signature}, + lower::function::{lower_func_body, lower_func_signature, lower_monomorphized_func_signature}, }; pub fn mir_lowered_func_signature( @@ -16,6 +17,14 @@ pub fn mir_lowered_func_signature( lower_func_signature(db, analyzer_func) } +pub fn mir_lowered_monomorphized_func_signature( + db: &dyn MirDb, + analyzer_func: analyzer_items::FunctionId, + concrete_args: Vec, +) -> ir::FunctionId { + lower_monomorphized_func_signature(db, analyzer_func, &concrete_args) +} + pub fn mir_lowered_func_body(db: &dyn MirDb, func: ir::FunctionId) -> Rc { lower_func_body(db, func) } @@ -60,11 +69,20 @@ impl ir::FunctionId { let analyzer_func = self.analyzer_func(db); let func_name = analyzer_func.name(db.upcast()); + // Construct a suffix for any monomorphized generic function parameters + let type_suffix = self + .signature(db) + .resolved_generics + .values() + .fold(String::new(), |acc, param| { + format!("{}_{}", acc, param.name()) + }); + if let Some(class) = analyzer_func.class(db.upcast()) { let class_name = class.name(db.upcast()); - format!("{}::{}", class_name, func_name).into() + format!("{}::{}{}", class_name, func_name, type_suffix).into() } else { - func_name + format!("{}{}", func_name, type_suffix).into() } } diff --git a/crates/mir/src/db/queries/types.rs b/crates/mir/src/db/queries/types.rs index 5a97972c07..82dc89982a 100644 --- a/crates/mir/src/db/queries/types.rs +++ b/crates/mir/src/db/queries/types.rs @@ -27,6 +27,10 @@ impl TypeId { db.lookup_mir_intern_type(self) } + pub fn analyzer_ty(self, db: &dyn MirDb) -> Option { + self.data(db).analyzer_ty.clone() + } + pub fn projection_ty(self, db: &dyn MirDb, access: &Value) -> TypeId { let ty = self.deref(db); match &ty.data(db).as_ref().kind { diff --git a/crates/mir/src/ir/function.rs b/crates/mir/src/ir/function.rs index 1943b8345f..7fefda79bb 100644 --- a/crates/mir/src/ir/function.rs +++ b/crates/mir/src/ir/function.rs @@ -1,9 +1,11 @@ use fe_analyzer::namespace::items as analyzer_items; +use fe_analyzer::namespace::types as analyzer_types; use fe_common::impl_intern_key; use fxhash::FxHashMap; use id_arena::Arena; use num_bigint::BigInt; use smol_str::SmolStr; +use std::collections::BTreeMap; use crate::db::MirDb; @@ -20,6 +22,7 @@ use super::{ #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct FunctionSignature { pub params: Vec, + pub resolved_generics: BTreeMap, pub return_type: Option, pub module_id: analyzer_items::ModuleId, pub analyzer_func_id: analyzer_items::FunctionId, diff --git a/crates/mir/src/lower/function.rs b/crates/mir/src/lower/function.rs index 2c0fd7a0fb..7562523816 100644 --- a/crates/mir/src/lower/function.rs +++ b/crates/mir/src/lower/function.rs @@ -1,9 +1,12 @@ -use std::rc::Rc; +use std::{collections::BTreeMap, rc::Rc}; use fe_analyzer::{ builtins::{ContractTypeMethod, GlobalFunction, ValueMethod}, context::CallType as AnalyzerCallType, - namespace::{items as analyzer_items, types as analyzer_types}, + namespace::{ + items as analyzer_items, + types::{self as analyzer_types, Type}, + }, }; use fe_common::numeric::Literal; use fe_parser::{ast, node::Node}; @@ -28,20 +31,52 @@ use crate::{ type ScopeId = Id; pub fn lower_func_signature(db: &dyn MirDb, func: analyzer_items::FunctionId) -> FunctionId { + lower_monomorphized_func_signature(db, func, &[]) +} +pub fn lower_monomorphized_func_signature( + db: &dyn MirDb, + func: analyzer_items::FunctionId, + concrete_args: &[Type], +) -> FunctionId { // TODO: Remove this when an analyzer's function signature contains `self` type. let mut params = vec![]; + let mut concrete_args: &[Type] = concrete_args; let has_self = func.takes_self(db.upcast()); if has_self { let self_ty = func.self_typ(db.upcast()).unwrap(); let source = self_arg_source(db, func); params.push(make_param(db, "self", self_ty, source)); + // similarly, when in the future analyzer params contain `self` we won't need to adjust the concrete args anymore + if !concrete_args.is_empty() { + concrete_args = &concrete_args[1..] + } } - let analyzer_signature = func.signature(db.upcast()); - params.extend(analyzer_signature.params.iter().map(|param| { + let mut resolved_generics: BTreeMap = BTreeMap::new(); + + for (index, param) in analyzer_signature.params.iter().enumerate() { let source = arg_source(db, func, ¶m.name); - make_param(db, param.name.clone(), param.typ.clone().unwrap(), source) - })); + let provided_type = concrete_args + .get(index) + .cloned() + .unwrap_or_else(|| param.typ.clone().unwrap()); + + params.push(make_param( + db, + param.clone().name, + provided_type.clone(), + source, + )); + + if let analyzer_types::FunctionParam { + typ: Ok(Type::Generic(generic)), + .. + } = param + { + resolved_generics.insert(generic.clone(), provided_type); + } + } + let return_type = db.mir_lowered_type(analyzer_signature.return_type.clone().unwrap()); let linkage = if func.is_public(db.upcast()) { @@ -56,6 +91,7 @@ pub fn lower_func_signature(db: &dyn MirDb, func: analyzer_items::FunctionId) -> let sig = FunctionSignature { params, + resolved_generics, return_type: Some(return_type), module_id: func.module(db.upcast()), analyzer_func_id: func, @@ -79,6 +115,7 @@ struct BodyLowerHelper<'db, 'a> { db: &'db dyn MirDb, builder: BodyBuilder, ast: &'a Node, + func: FunctionId, analyzer_body: &'a fe_analyzer::context::FunctionBody, scopes: Arena, current_scope: ScopeId, @@ -98,17 +135,33 @@ impl<'db, 'a> BodyLowerHelper<'db, 'a> { // constants. let root = Scope::root(db, func, &mut builder); let current_scope = scopes.alloc(root); - Self { db, builder, ast, + func, analyzer_body, scopes, current_scope, } } + fn lower_analyzer_type(&self, analyzer_ty: analyzer_types::Type) -> TypeId { + // If the analyzer type is generic we first need to resolve it to its concrete type before lowering to a MIR type + if let analyzer_types::Type::Generic(generic) = analyzer_ty { + let resolved_type = self + .func + .signature(self.db) + .resolved_generics + .get(&generic) + .expect("expected generic to be resolved") + .clone(); + return self.db.mir_lowered_type(resolved_type); + } + + self.db.mir_lowered_type(analyzer_ty) + } + fn lower(mut self) -> FunctionBody { for stmt in &self.ast.kind.body { self.lower_stmt(stmt) @@ -141,9 +194,7 @@ impl<'db, 'a> BodyLowerHelper<'db, 'a> { } ast::FuncStmt::ConstantDecl { name, value, .. } => { - let ty = self - .db - .mir_lowered_type(self.analyzer_body.var_types[&name.id].clone()); + let ty = self.lower_analyzer_type(self.analyzer_body.var_types[&name.id].clone()); let value = self.analyzer_body.expressions[&value.id] .const_value @@ -310,9 +361,7 @@ impl<'db, 'a> BodyLowerHelper<'db, 'a> { ) { match &var.kind { ast::VarDeclTarget::Name(name) => { - let ty = self - .db - .mir_lowered_type(self.analyzer_body.var_types[&var.id].clone()); + let ty = self.lower_analyzer_type(self.analyzer_body.var_types[&var.id].clone()); let local = Local::user_local(name.clone(), ty, var.into()); let value = self.builder.declare(local); @@ -353,9 +402,7 @@ impl<'db, 'a> BodyLowerHelper<'db, 'a> { ) { match &var.kind { ast::VarDeclTarget::Name(name) => { - let ty = self - .db - .mir_lowered_type(self.analyzer_body.var_types[&var.id].clone()); + let ty = self.lower_analyzer_type(self.analyzer_body.var_types[&var.id].clone()); let local = Local::user_local(name.clone(), ty, var.into()); let lhs = self.builder.declare(local); @@ -453,7 +500,7 @@ impl<'db, 'a> BodyLowerHelper<'db, 'a> { let entry_bb = self.builder.make_block(); let exit_bb = self.builder.make_block(); let iter_elem_ty = self.analyzer_body.var_types[&loop_variable.id].clone(); - let iter_elem_ty = self.db.mir_lowered_type(iter_elem_ty); + let iter_elem_ty = self.lower_analyzer_type(iter_elem_ty); self.builder.jump(preheader_bb, SourceInfo::dummy()); @@ -701,7 +748,7 @@ impl<'db, 'a> BodyLowerHelper<'db, 'a> { fn expr_ty(&self, expr: &Node) -> TypeId { let analyzer_ty = self.analyzer_body.expressions[&expr.id].typ.clone(); - self.db.mir_lowered_type(analyzer_ty) + self.lower_analyzer_type(analyzer_ty) } fn lower_bool_op( @@ -848,7 +895,22 @@ impl<'db, 'a> BodyLowerHelper<'db, 'a> { let mut method_args = vec![self.lower_method_receiver(func)]; method_args.append(&mut args); - let func_id = self.db.mir_lowered_func_signature(*method); + let func_id = if method.is_generic(self.db.upcast()) { + let concrete_args = method_args + .iter() + .map(|val| { + self.builder + .value_ty(*val) + .analyzer_ty(self.db) + .expect("invalid parameter") + }) + .collect::>(); + self.db + .mir_lowered_monomorphized_func_signature(*method, concrete_args) + } else { + self.db.mir_lowered_func_signature(*method) + }; + self.builder .call(func_id, method_args, CallType::Internal, source) } diff --git a/crates/mir/src/lower/types.rs b/crates/mir/src/lower/types.rs index b53a89600e..a49e8dca0f 100644 --- a/crates/mir/src/lower/types.rs +++ b/crates/mir/src/lower/types.rs @@ -18,6 +18,12 @@ pub fn lower_type(db: &dyn MirDb, analyzer_ty: &analyzer_types::Type) -> TypeId analyzer_types::Type::Contract(_) => TypeKind::Address, analyzer_types::Type::SelfContract(contract) => lower_contract(db, contract), analyzer_types::Type::Struct(struct_) => lower_struct(db, struct_), + analyzer_types::Type::Trait(_) => { + panic!("traits should only appear in generic bounds for now") + } + analyzer_types::Type::Generic(_) => { + panic!("should be lowered in `lower_types_in_functions`") + } }; intern_type(db, ty_kind, Some(analyzer_ty)) diff --git a/crates/parser/src/ast.rs b/crates/parser/src/ast.rs index 9ecc4d62ab..5433c6559e 100644 --- a/crates/parser/src/ast.rs +++ b/crates/parser/src/ast.rs @@ -135,7 +135,7 @@ pub enum GenericParameter { Unbounded(Node), Bounded { name: Node, - bound: Node, + bound: Node, }, } @@ -377,6 +377,12 @@ impl Node { } } +impl Node { + pub fn name(&self) -> &str { + &self.kind.name.kind + } +} + impl Node { pub fn name(&self) -> &str { &self.kind.name.kind diff --git a/crates/parser/src/grammar.rs b/crates/parser/src/grammar.rs index 0b8c906573..00bfb7447f 100644 --- a/crates/parser/src/grammar.rs +++ b/crates/parser/src/grammar.rs @@ -2,5 +2,4 @@ pub mod contracts; pub mod expressions; pub mod functions; pub mod module; -pub mod traits; pub mod types; diff --git a/crates/parser/src/grammar/functions.rs b/crates/parser/src/grammar/functions.rs index 0ab78b9028..e0c872f270 100644 --- a/crates/parser/src/grammar/functions.rs +++ b/crates/parser/src/grammar/functions.rs @@ -3,7 +3,7 @@ use super::types::parse_type_desc; use crate::ast::{ BinOperator, Expr, FuncStmt, Function, FunctionArg, GenericParameter, RegularFunctionArg, - VarDeclTarget, + TypeDesc, VarDeclTarget, }; use crate::node::{Node, Span}; use crate::{Label, ParseFailed, ParseResult, Parser, TokenKind}; @@ -115,7 +115,12 @@ pub fn parse_generic_param(par: &mut Parser) -> ParseResult { let bound = par.assert(Name); Ok(GenericParameter::Bounded { name: Node::new(name.text.into(), name.span), - bound: Node::new(bound.text.into(), bound.span), + bound: Node::new( + TypeDesc::Base { + base: bound.text.into(), + }, + bound.span, + ), }) } None => Ok(GenericParameter::Unbounded(Node::new( diff --git a/crates/parser/src/grammar/module.rs b/crates/parser/src/grammar/module.rs index cc039177cb..af7f2c95fd 100644 --- a/crates/parser/src/grammar/module.rs +++ b/crates/parser/src/grammar/module.rs @@ -1,9 +1,9 @@ use super::contracts::parse_contract_def; use super::expressions::parse_expr; use super::functions::parse_fn_def; -use super::traits::parse_trait_def; use super::types::{ - parse_event_def, parse_path_tail, parse_struct_def, parse_type_alias, parse_type_desc, + parse_event_def, parse_path_tail, parse_struct_def, parse_trait_def, parse_type_alias, + parse_type_desc, }; use crate::ast::{ConstantDecl, Module, ModuleStmt, Pragma, Use, UseTree}; use crate::node::{Node, Span}; @@ -56,6 +56,7 @@ pub fn parse_module_stmt(par: &mut Parser) -> ParseResult { ModuleStmt::Function(parse_fn_def(par, Some(pub_span))?) } TokenKind::Struct => ModuleStmt::Struct(parse_struct_def(par, Some(pub_span))?), + TokenKind::Trait => ModuleStmt::Trait(parse_trait_def(par, Some(pub_span))?), TokenKind::Type => ModuleStmt::TypeAlias(parse_type_alias(par, Some(pub_span))?), TokenKind::Const => ModuleStmt::Constant(parse_constant(par, Some(pub_span))?), TokenKind::Contract => { diff --git a/crates/parser/src/grammar/traits.rs b/crates/parser/src/grammar/traits.rs deleted file mode 100644 index 147367aceb..0000000000 --- a/crates/parser/src/grammar/traits.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::ast::Trait; -use crate::grammar::functions::parse_single_word_stmt; -use crate::node::{Node, Span}; -use crate::{ParseFailed, ParseResult, Parser, TokenKind}; - -/// Parse a trait definition. -/// # Panics -/// Panics if the next token isn't `trait`. -pub fn parse_trait_def(par: &mut Parser, trait_pub_qual: Option) -> ParseResult> { - let trait_tok = par.assert(TokenKind::Trait); - - // trait Event: - // pass - // - - let trait_name = par.expect_with_notes( - TokenKind::Name, - "failed to parse trait definition", - |_| vec!["Note: `trait` must be followed by a name, which must start with a letter and contain only letters, numbers, or underscores".into()], - )?; - - let header_span = trait_tok.span + trait_name.span; - par.enter_block(header_span, "trait definition")?; - - loop { - match par.peek() { - Some(TokenKind::Pass) => { - parse_single_word_stmt(par)?; - } - Some(TokenKind::Dedent) => { - par.next()?; - break; - } - None => break, - Some(_) => { - let tok = par.next()?; - par.unexpected_token_error( - tok.span, - "failed to parse trait definition body", - vec![], - ); - return Err(ParseFailed); - } - }; - } - - let span = header_span + trait_pub_qual; - Ok(Node::new( - Trait { - name: Node::new(trait_name.text.into(), trait_name.span), - pub_qual: trait_pub_qual, - }, - span, - )) -} diff --git a/crates/parser/src/grammar/types.rs b/crates/parser/src/grammar/types.rs index 3353df8245..5c5df16b31 100644 --- a/crates/parser/src/grammar/types.rs +++ b/crates/parser/src/grammar/types.rs @@ -1,4 +1,4 @@ -use crate::ast::{self, EventField, Field, GenericArg, Path, TypeAlias, TypeDesc}; +use crate::ast::{self, EventField, Field, GenericArg, Path, Trait, TypeAlias, TypeDesc}; use crate::grammar::expressions::parse_expr; use crate::grammar::functions::parse_fn_def; use crate::node::{Node, Span}; @@ -64,6 +64,50 @@ pub fn parse_struct_def( )) } +/// Parse a trait definition. +/// # Panics +/// Panics if the next token isn't `trait`. +pub fn parse_trait_def(par: &mut Parser, trait_pub_qual: Option) -> ParseResult> { + let trait_tok = par.assert(TokenKind::Trait); + + // trait Event {} + + let trait_name = par.expect_with_notes( + TokenKind::Name, + "failed to parse trait definition", + |_| vec!["Note: `trait` must be followed by a name, which must start with a letter and contain only letters, numbers, or underscores".into()], + )?; + + let header_span = trait_tok.span + trait_name.span; + par.enter_block(header_span, "trait definition")?; + + loop { + match par.peek_or_err()? { + TokenKind::Fn => { + // TODO: parse trait functions + } + TokenKind::BraceClose => { + par.next()?; + break; + } + _ => { + let tok = par.next()?; + par.unexpected_token_error(&tok, "failed to parse trait definition body", vec![]); + return Err(ParseFailed); + } + }; + } + + let span = header_span + trait_pub_qual; + Ok(Node::new( + Trait { + name: Node::new(trait_name.text.into(), trait_name.span), + pub_qual: trait_pub_qual, + }, + span, + )) +} + /// Parse a type alias definition, e.g. `type MyMap = Map`. /// # Panics /// Panics if the next token isn't `type`. diff --git a/crates/parser/tests/cases/snapshots/cases__errors__module_bad_stmt.snap b/crates/parser/tests/cases/snapshots/cases__errors__module_bad_stmt.snap index cd41c36260..a0ae7cec6e 100644 --- a/crates/parser/tests/cases/snapshots/cases__errors__module_bad_stmt.snap +++ b/crates/parser/tests/cases/snapshots/cases__errors__module_bad_stmt.snap @@ -9,6 +9,6 @@ error: failed to parse module 1 │ if x { y } │ ^^ unexpected token │ - = Note: expected import, contract, struct, trait, type, const or event + = Note: expected import, contract, struct, type, const or event diff --git a/crates/parser/tests/cases/snapshots/cases__parse_ast__fn_def_generic.snap b/crates/parser/tests/cases/snapshots/cases__parse_ast__fn_def_generic.snap index ae957dd09f..43a5edaf1f 100644 --- a/crates/parser/tests/cases/snapshots/cases__parse_ast__fn_def_generic.snap +++ b/crates/parser/tests/cases/snapshots/cases__parse_ast__fn_def_generic.snap @@ -1,6 +1,6 @@ --- source: crates/parser/tests/cases/parse_ast.rs -expression: "ast_string(stringify!(fn_def_generic), try_parse_module,\n \"fn foo(this: T, that: R, _ val: u64) -> bool { false }\")" +expression: "ast_string(stringify!(fn_def_generic), try_parse_module,\n \"fn foo(this: T, that: R, _ val: u64) -> bool { false }\")" --- Node( @@ -35,7 +35,9 @@ Node( ), ), bound: Node( - kind: "Event", + kind: Base( + base: "Event", + ), span: Span( start: 13, end: 18, diff --git a/crates/test-files/fixtures/compile_errors/call_generic_function_with_unsatisfied_bound.fe b/crates/test-files/fixtures/compile_errors/call_generic_function_with_unsatisfied_bound.fe new file mode 100644 index 0000000000..5cca0e114e --- /dev/null +++ b/crates/test-files/fixtures/compile_errors/call_generic_function_with_unsatisfied_bound.fe @@ -0,0 +1,16 @@ +use std::traits::IsNumeric + +struct Foo { + + pub fn bar(self, _ x: T) -> bool { + return true + } +} + +contract Meh { + + pub fn call_bar(self) { + let foo: Foo = Foo(); + foo.bar(true) + } +} diff --git a/crates/test-files/fixtures/compile_errors/contract_function_with_generic_params.fe b/crates/test-files/fixtures/compile_errors/contract_function_with_generic_params.fe new file mode 100644 index 0000000000..758cc31b64 --- /dev/null +++ b/crates/test-files/fixtures/compile_errors/contract_function_with_generic_params.fe @@ -0,0 +1,5 @@ +use std::traits::IsNumeric + +contract Foo { + pub fn bar(val: T) {} +} diff --git a/foo.fe b/foo.fe index 59c3226881..299c8b69f5 100644 --- a/foo.fe +++ b/foo.fe @@ -1,7 +1,27 @@ -trait Foo: - pass +use std::traits::IsNumeric -contract Meh: +struct Foo { - pub fn bar(x: T) -> bool: - return true \ No newline at end of file + pub fn bar(self, x: T, y: u256) -> bool { + return true + } + + pub fn call_me_maybe(self) { + self.bar(x: 1, y: 1) + self.bar(x: i8(-1), y:1) + self.bar(x: true, y:1) + } +} + + +contract Meh { + + # We should disallow generic parameters in contract functions for now. + # But should generally be possible for private functions where we know all call sites. + + pub fn call_me_maybe(self) { + let foo: Foo = Foo(); + foo.bar(x: 1, y: 1) + foo.bar(x: i8(-1), y:1) + } +}