diff --git a/Cargo.lock b/Cargo.lock index b175442dc1..30b2051ecf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -517,6 +517,7 @@ dependencies = [ "rstest", "salsa", "semver 1.0.0", + "smallvec", "strum", "vec1", "wasm-bindgen-test", diff --git a/crates/analyzer/Cargo.toml b/crates/analyzer/Cargo.toml index d66a863c7c..805bb8db03 100644 --- a/crates/analyzer/Cargo.toml +++ b/crates/analyzer/Cargo.toml @@ -19,6 +19,7 @@ salsa = "0.16.1" parking_lot_core = { version = "=0.8.0" } # used by salsa; version pinned for wasm compatibility indexmap = "1.6.2" if_chain = "1.0.1" +smallvec = { version = "1.6.1", features = ["union"] } [dev-dependencies] insta = "1.7.1" diff --git a/crates/analyzer/src/builtins.rs b/crates/analyzer/src/builtins.rs index 3d08b55f45..b92e911be3 100644 --- a/crates/analyzer/src/builtins.rs +++ b/crates/analyzer/src/builtins.rs @@ -1,6 +1,6 @@ use strum::{AsRefStr, EnumIter, EnumString}; -#[derive(Debug, PartialEq, EnumString)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, EnumString, AsRefStr)] #[strum(serialize_all = "snake_case")] pub enum ValueMethod { Clone, @@ -10,29 +10,36 @@ pub enum ValueMethod { #[derive(Clone, Copy, Debug, PartialEq, Eq, EnumString, AsRefStr, Hash, EnumIter)] #[strum(serialize_all = "snake_case")] -pub enum GlobalMethod { +pub enum GlobalFunction { Keccak256, SendValue, Balance, BalanceOf, } -#[derive(Debug, PartialEq, EnumString, AsRefStr)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, EnumString, AsRefStr)] #[strum(serialize_all = "snake_case")] pub enum ContractTypeMethod { Create, Create2, } +impl ContractTypeMethod { + pub fn arg_count(&self) -> usize { + match self { + ContractTypeMethod::Create => 1, + ContractTypeMethod::Create2 => 2, + } + } +} + #[derive(Copy, Clone, Debug, Eq, PartialEq, EnumString, EnumIter, AsRefStr)] #[strum(serialize_all = "lowercase")] -pub enum Object { +pub enum GlobalObject { Block, Chain, Msg, Tx, - #[strum(serialize = "self")] - Self_, } #[derive(Debug, PartialEq, EnumString)] @@ -67,6 +74,6 @@ pub enum TxField { #[derive(Debug, PartialEq, EnumString)] #[strum(serialize_all = "snake_case")] -pub enum SelfField { +pub enum ContractSelfField { Address, } diff --git a/crates/analyzer/src/context.rs b/crates/analyzer/src/context.rs index 471e472a29..9a646f133f 100644 --- a/crates/analyzer/src/context.rs +++ b/crates/analyzer/src/context.rs @@ -1,7 +1,7 @@ -use crate::builtins::GlobalMethod; +use crate::builtins::{ContractTypeMethod, GlobalFunction, ValueMethod}; use crate::errors::{self, CannotMove, TypeError}; -use crate::namespace::items::{DiagnosticSink, EventId, FunctionId, Item}; -use crate::namespace::types::{FixedSize, Type}; +use crate::namespace::items::{Class, ContractId, DiagnosticSink, EventId, FunctionId, Item}; +use crate::namespace::types::{FixedSize, SelfDecl, Type}; use crate::AnalyzerDb; use fe_common::diagnostics::Diagnostic; pub use fe_common::diagnostics::Label; @@ -96,6 +96,16 @@ pub trait AnalyzerContext { #[derive(Clone, Debug, PartialEq, Eq)] pub enum NamedThing { Item(Item), + SelfValue { + /// Function `self` parameter. + decl: Option, + + /// The function's parent, if any. If `None`, `self` has been + /// used in a module-level function. + class: Option, + span: Option, + }, + // SelfType // when/if we add a `Self` type keyword Variable { name: String, typ: Result, @@ -107,6 +117,7 @@ impl NamedThing { pub fn name_span(&self, db: &dyn AnalyzerDb) -> Option { match self { NamedThing::Item(item) => item.name_span(db), + NamedThing::SelfValue { span, .. } => *span, NamedThing::Variable { span, .. } => Some(*span), } } @@ -115,6 +126,7 @@ impl NamedThing { match self { NamedThing::Item(item) => item.is_builtin(), NamedThing::Variable { .. } => false, + NamedThing::SelfValue { .. } => false, } } @@ -122,6 +134,7 @@ impl NamedThing { match self { NamedThing::Item(item) => item.item_kind_display_name(), NamedThing::Variable { .. } => "variable", + NamedThing::SelfValue { .. } => "value", } } } @@ -210,16 +223,15 @@ impl ExpressionAttributes { /// Adds a move to value, if it is in storage or memory. pub fn into_loaded(mut self) -> Result { match self.typ { - Type::Base(_) => {} - Type::Contract(_) => {} - _ => return Err(CannotMove), - } + Type::Base(_) | Type::Contract(_) => { + if self.location != Location::Value { + self.move_location = Some(Location::Value); + } - if self.location != Location::Value { - self.move_location = Some(Location::Value); + Ok(self) + } + _ => Err(CannotMove), } - - Ok(self) } /// The final location of an expression after a possible move. @@ -233,21 +245,77 @@ impl ExpressionAttributes { impl fmt::Display for ExpressionAttributes { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!( - f, - "{}: {:?} => {:?}", - self.typ, self.location, self.move_location - ) + if let Some(move_to) = self.move_location { + write!(f, "{}: {:?} => {:?}", self.typ, self.location, move_to) + } else { + write!(f, "{}: {:?}", self.typ, self.location) + } } } /// The type of a function call. #[derive(Clone, Debug, PartialEq, Eq)] pub enum CallType { - BuiltinFunction(GlobalMethod), - TypeConstructor { typ: Type }, - SelfAttribute { func_name: String, self_span: Span }, + BuiltinFunction(GlobalFunction), + BuiltinValueMethod(ValueMethod), + + // create, create2 (will be methods of the context struct soon) + BuiltinAssociatedFunction { + contract: ContractId, + function: ContractTypeMethod, + }, + + // MyStruct.foo() (soon MyStruct::foo()) + AssociatedFunction { + class: Class, + function: FunctionId, + }, + ValueMethod { + is_self: bool, + class: Class, + method: FunctionId, + }, Pure(FunctionId), - ValueAttribute, - TypeAttribute { typ: Type, func_name: String }, + TypeConstructor(Type), +} + +impl CallType { + pub fn function(&self) -> Option { + use CallType::*; + match self { + BuiltinFunction(_) + | BuiltinValueMethod(_) + | TypeConstructor(_) + | BuiltinAssociatedFunction { .. } => None, + AssociatedFunction { function: id, .. } | ValueMethod { method: id, .. } | Pure(id) => { + Some(*id) + } + } + } + + pub fn function_name(&self, db: &dyn AnalyzerDb) -> String { + match self { + CallType::BuiltinFunction(f) => f.as_ref().to_string(), + CallType::BuiltinValueMethod(f) => f.as_ref().to_string(), + CallType::BuiltinAssociatedFunction { function, .. } => function.as_ref().to_string(), + + CallType::AssociatedFunction { function: id, .. } + | CallType::ValueMethod { method: id, .. } + | CallType::Pure(id) => id.name(db), + CallType::TypeConstructor(typ) => typ.to_string(), + } + } + + pub fn is_unsafe(&self, db: &dyn AnalyzerDb) -> bool { + // There are no built-in unsafe fns yet + self.function() + .map(|id| id.unsafe_span(db).is_some()) + .unwrap_or(false) + } +} + +impl fmt::Display for CallType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "{:?}", self) + } } diff --git a/crates/analyzer/src/db.rs b/crates/analyzer/src/db.rs index 3fb9a76360..80f1a908bf 100644 --- a/crates/analyzer/src/db.rs +++ b/crates/analyzer/src/db.rs @@ -73,10 +73,6 @@ pub trait AnalyzerDb { fn contract_function_map(&self, id: ContractId) -> Analysis>>; #[salsa::invoke(queries::contracts::contract_public_function_map)] fn contract_public_function_map(&self, id: ContractId) -> Rc>; - #[salsa::invoke(queries::contracts::contract_pure_function_map)] - fn contract_pure_function_map(&self, id: ContractId) -> Rc>; - #[salsa::invoke(queries::contracts::contract_self_function_map)] - fn contract_self_function_map(&self, id: ContractId) -> Rc>; #[salsa::invoke(queries::contracts::contract_init_function)] fn contract_init_function(&self, id: ContractId) -> Analysis>; @@ -114,6 +110,10 @@ pub trait AnalyzerDb { &self, field: StructFieldId, ) -> Analysis>; + #[salsa::invoke(queries::structs::struct_all_functions)] + fn struct_all_functions(&self, id: StructId) -> Rc>; + #[salsa::invoke(queries::structs::struct_function_map)] + fn struct_function_map(&self, id: StructId) -> Analysis>>; // Event #[salsa::invoke(queries::events::event_type)] diff --git a/crates/analyzer/src/db/queries/contracts.rs b/crates/analyzer/src/db/queries/contracts.rs index dae4ed8c54..780ba0623e 100644 --- a/crates/analyzer/src/db/queries/contracts.rs +++ b/crates/analyzer/src/db/queries/contracts.rs @@ -11,6 +11,7 @@ use indexmap::map::{Entry, IndexMap}; use std::rc::Rc; +/// A `Vec` of every function defined in the contract, including duplicates and the init function. pub fn contract_all_functions(db: &dyn AnalyzerDb, contract: ContractId) -> Rc> { let module = contract.module(db); let body = &contract.data(db).ast.kind.body; @@ -21,8 +22,8 @@ pub fn contract_all_functions(db: &dyn AnalyzerDb, contract: ContractId) -> Rc { Some(db.intern_function(Rc::new(items::Function { ast: node.clone(), - contract: Some(contract), module, + parent: Some(items::Class::Contract(contract)), }))) } }) @@ -37,7 +38,7 @@ pub fn contract_function_map( let mut scope = ItemScope::new(db, contract.module(db)); let mut map = IndexMap::::new(); - for func in contract.all_functions(db).iter() { + for func in db.contract_all_functions(contract).iter() { let def = &func.data(db).ast; let def_name = def.name(); if def_name == "__init__" { @@ -102,37 +103,11 @@ pub fn contract_public_function_map( ) } -pub fn contract_pure_function_map( - db: &dyn AnalyzerDb, - contract: ContractId, -) -> Rc> { - Rc::new( - contract - .functions(db) - .iter() - .filter_map(|(name, func)| func.is_pure(db).then(|| (name.clone(), *func))) - .collect(), - ) -} - -pub fn contract_self_function_map( - db: &dyn AnalyzerDb, - contract: ContractId, -) -> Rc> { - Rc::new( - contract - .functions(db) - .iter() - .filter_map(|(name, func)| (!func.is_pure(db)).then(|| (name.clone(), *func))) - .collect(), - ) -} - pub fn contract_init_function( db: &dyn AnalyzerDb, contract: ContractId, ) -> Analysis> { - let all_fns = contract.all_functions(db); + let all_fns = db.contract_all_functions(contract); let mut init_fns = all_fns.iter().filter_map(|func| { let def = &func.data(db).ast; (def.name() == "__init__").then(|| (func, def.span)) @@ -180,6 +155,7 @@ pub fn contract_init_function( } } +/// A `Vec` of all events defined within the contract, including those with duplicate names. pub fn contract_all_events(db: &dyn AnalyzerDb, contract: ContractId) -> Rc> { let body = &contract.data(db).ast.kind.body; Rc::new( @@ -227,6 +203,7 @@ pub fn contract_event_map( } } +/// All field ids, including those with duplicate names pub fn contract_all_fields(db: &dyn AnalyzerDb, contract: ContractId) -> Rc> { let fields = contract .data(db) diff --git a/crates/analyzer/src/db/queries/functions.rs b/crates/analyzer/src/db/queries/functions.rs index 1f32fd2376..1fa2cf7327 100644 --- a/crates/analyzer/src/db/queries/functions.rs +++ b/crates/analyzer/src/db/queries/functions.rs @@ -1,7 +1,7 @@ use crate::context::{AnalyzerContext, FunctionBody}; use crate::db::{Analysis, AnalyzerDb}; use crate::errors::TypeError; -use crate::namespace::items::FunctionId; +use crate::namespace::items::{Class, FunctionId}; use crate::namespace::scopes::{BlockScope, BlockScopeType, FunctionScope, ItemScope}; use crate::namespace::types::{self, FixedSize, SelfDecl}; use crate::traversal::functions::traverse_statements; @@ -24,10 +24,10 @@ pub fn function_signature( let def = &node.kind; let mut scope = ItemScope::new(db, function.module(db)); - let contract = function.contract(db); + let fn_parent = function.parent(db); if_chain! { - if contract.is_some(); + if let Some(Class::Contract(_)) = fn_parent; if let Some(pub_span) = function.pub_span(db); if let Some(unsafe_span) = function.unsafe_span(db); then { @@ -37,7 +37,7 @@ pub fn function_signature( } } - let mut self_decl = SelfDecl::None; + let mut self_decl = None; let mut names = HashMap::new(); let params = def .args @@ -45,14 +45,14 @@ pub fn function_signature( .enumerate() .filter_map(|(index, arg)| match &arg.kind { ast::FunctionArg::Zelf => { - if contract.is_none() { + if fn_parent.is_none() { scope.error( - "`self` can only be used in contract functions", + "`self` can only be used in contract or struct functions", arg.span, - "not allowed in functions defined outside of a contract", + "not allowed in functions defined outside of a contract or struct", ); } else { - self_decl = SelfDecl::Mutable; + self_decl = Some(SelfDecl::Mutable); if index != 0 { scope.error( "`self` is not the first parameter", diff --git a/crates/analyzer/src/db/queries/module.rs b/crates/analyzer/src/db/queries/module.rs index 1e2627360e..ec69642aec 100644 --- a/crates/analyzer/src/db/queries/module.rs +++ b/crates/analyzer/src/db/queries/module.rs @@ -32,10 +32,12 @@ fn std_prelude_items() -> IndexMap { types::GenericType::iter().map(|typ| (typ.name().to_string(), Item::GenericType(typ))), ); items.extend( - builtins::GlobalMethod::iter() + builtins::GlobalFunction::iter() .map(|fun| (fun.as_ref().to_string(), Item::BuiltinFunction(fun))), ); - items.extend(builtins::Object::iter().map(|obj| (obj.as_ref().to_string(), Item::Object(obj)))); + items.extend( + builtins::GlobalObject::iter().map(|obj| (obj.as_ref().to_string(), Item::Object(obj))), + ); items } @@ -78,8 +80,8 @@ pub fn module_all_items(db: &dyn AnalyzerDb, module: ModuleId) -> Rc> ast::ModuleStmt::Function(node) => { Some(Item::Function(db.intern_function(Rc::new(Function { ast: node.clone(), - contract: None, module, + parent: None, })))) } ast::ModuleStmt::Pragma(_) => None, diff --git a/crates/analyzer/src/db/queries/structs.rs b/crates/analyzer/src/db/queries/structs.rs index 29046071b9..b22ad3cec6 100644 --- a/crates/analyzer/src/db/queries/structs.rs +++ b/crates/analyzer/src/db/queries/structs.rs @@ -1,7 +1,8 @@ +use crate::builtins; use crate::context::AnalyzerContext; use crate::db::Analysis; use crate::errors::TypeError; -use crate::namespace::items::{StructField, StructFieldId, StructId}; +use crate::namespace::items::{self, Function, FunctionId, StructField, StructFieldId, StructId}; use crate::namespace::scopes::ItemScope; use crate::namespace::types; use crate::traversal::types::type_desc; @@ -9,12 +10,13 @@ use crate::AnalyzerDb; use fe_parser::ast; use indexmap::map::{Entry, IndexMap}; use std::rc::Rc; +use std::str::FromStr; pub fn struct_type(db: &dyn AnalyzerDb, struct_: StructId) -> Rc { Rc::new(types::Struct { name: struct_.name(db), id: struct_, - field_count: struct_.all_fields(db).len(), + field_count: struct_.fields(db).len(), }) } @@ -106,3 +108,78 @@ pub fn struct_field_type( diagnostics: Rc::new(scope.diagnostics), } } + +pub fn struct_all_functions(db: &dyn AnalyzerDb, struct_: StructId) -> Rc> { + let struct_data = struct_.data(db); + let fields = struct_data + .ast + .kind + .functions + .iter() + .map(|node| { + db.intern_function(Rc::new(Function { + ast: node.clone(), + module: struct_data.module, + parent: Some(items::Class::Struct(struct_)), + })) + }) + .collect(); + Rc::new(fields) +} + +pub fn struct_function_map( + db: &dyn AnalyzerDb, + struct_: StructId, +) -> Analysis>> { + let mut scope = ItemScope::new(db, struct_.module(db)); + let mut map = IndexMap::::new(); + + for func in db.struct_all_functions(struct_).iter() { + let def = &func.data(db).ast; + let def_name = def.name(); + if def_name == "__init__" { + continue; + } + + if let Some(named_item) = scope.resolve_name(def_name) { + scope.name_conflict_error( + "function", + def_name, + &named_item, + named_item.name_span(db), + def.kind.name.span, + ); + continue; + } + + if builtins::ValueMethod::from_str(def_name).is_ok() { + scope.error( + &format!( + "function name `{}` conflicts with built-in function", + def_name + ), + def.kind.name.span, + &format!("`{}` is a built-in function", def_name), + ); + continue; + } + + match map.entry(def_name.to_string()) { + Entry::Occupied(entry) => { + scope.duplicate_name_error( + &format!("duplicate function names in `struct {}`", struct_.name(db)), + entry.key(), + entry.get().data(db).ast.span, + def.span, + ); + } + Entry::Vacant(entry) => { + entry.insert(*func); + } + } + } + Analysis { + value: Rc::new(map), + diagnostics: Rc::new(scope.diagnostics), + } +} diff --git a/crates/analyzer/src/namespace/items.rs b/crates/analyzer/src/namespace/items.rs index 0f6d9f82e4..b4b5b49e2a 100644 --- a/crates/analyzer/src/namespace/items.rs +++ b/crates/analyzer/src/namespace/items.rs @@ -2,7 +2,7 @@ use crate::builtins; use crate::context; use crate::errors::{self, TypeError}; use crate::impl_intern_key; -use crate::namespace::types::{self, GenericType, SelfDecl}; +use crate::namespace::types::{self, GenericType}; use crate::traversal::pragma::check_pragma_version; use crate::AnalyzerDb; use fe_common::diagnostics::Diagnostic; @@ -17,7 +17,6 @@ use std::rc::Rc; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Item { // Module // TODO: modules don't have names yet - // Constant // TODO: when `const` is implemented Type(TypeDef), // GenericType probably shouldn't be a separate category. // Any of the items inside TypeDef (struct, alias, etc) @@ -30,12 +29,12 @@ pub enum Item { Constant(ModuleConstantId), // Needed until we can represent keccak256 as a FunctionId. // We can't represent keccak256's arg type yet. - BuiltinFunction(builtins::GlobalMethod), + BuiltinFunction(builtins::GlobalFunction), // This should go away soon. The globals (block, msg, etc) will be replaced // with a context struct that'll appear in the fn parameter list. // `self` should just be removed from here and handled as a special parameter. - Object(builtins::Object), + Object(builtins::GlobalObject), } impl Item { @@ -353,11 +352,6 @@ impl ContractId { db.contract_field_map(*self).value } - /// All field ids, including those with duplicate names - pub fn all_fields(&self, db: &dyn AnalyzerDb) -> Rc> { - db.contract_all_fields(*self) - } - pub fn field_type( &self, db: &dyn AnalyzerDb, @@ -369,7 +363,8 @@ impl ContractId { } pub fn resolve_name(&self, db: &dyn AnalyzerDb, name: &str) -> Option { - self.pure_function(db, name) + self.function(db, name) + .filter(|f| !f.takes_self(db)) .map(Item::Function) .or_else(|| self.event(db, name).map(Item::Event)) .or_else(|| self.module(db).resolve_name(db, name)) @@ -394,34 +389,9 @@ impl ContractId { db.contract_public_function_map(*self) } - /// Lookup a function by name. Matches on public and private functions, excludes init function. - pub fn public_function(&self, db: &dyn AnalyzerDb, name: &str) -> Option { - self.public_functions(db).get(name).copied() - } - - /// Functions that do not have a self parameter. - pub fn pure_functions(&self, db: &dyn AnalyzerDb) -> Rc> { - db.contract_pure_function_map(*self) - } - - /// Get a pure function by its name. - pub fn pure_function(&self, db: &dyn AnalyzerDb, name: &str) -> Option { - self.pure_functions(db).get(name).copied() - } - - /// Functions that have a self parameter. - pub fn self_functions(&self, db: &dyn AnalyzerDb) -> Rc> { - db.contract_self_function_map(*self) - } - /// Get a function that takes self by its name. pub fn self_function(&self, db: &dyn AnalyzerDb, name: &str) -> Option { - self.self_functions(db).get(name).copied() - } - - /// A `Vec` of every function defined in the contract, including duplicates and the init function. - pub fn all_functions(&self, db: &dyn AnalyzerDb) -> Rc> { - db.contract_all_functions(*self) + self.function(db, name).filter(|f| f.takes_self(db)) } /// Lookup an event by name. @@ -434,11 +404,6 @@ impl ContractId { db.contract_event_map(*self).value } - /// A `Vec` of all events defined within the contract, including those with duplicate names. - pub fn all_events(&self, db: &dyn AnalyzerDb) -> Rc> { - db.contract_all_events(*self) - } - pub fn sink_diagnostics(&self, db: &dyn AnalyzerDb, sink: &mut impl DiagnosticSink) { // fields db.contract_field_map(*self).sink_diagnostics(sink); @@ -488,8 +453,8 @@ impl ContractFieldId { #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct Function { pub ast: Node, - pub contract: Option, pub module: ModuleId, + pub parent: Option, } #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] @@ -508,15 +473,31 @@ impl FunctionId { pub fn name_span(&self, db: &dyn AnalyzerDb) -> Span { self.data(db).ast.kind.name.span } - pub fn contract(&self, db: &dyn AnalyzerDb) -> Option { - self.data(db).contract + + pub fn parent(&self, db: &dyn AnalyzerDb) -> Option { + self.data(db).parent } + pub fn module(&self, db: &dyn AnalyzerDb) -> ModuleId { self.data(db).module } - pub fn is_pure(&self, db: &dyn AnalyzerDb) -> bool { - self.signature(db).self_decl == SelfDecl::None + + 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) + .ast + .kind + .args + .iter() + .find_map(|arg| matches!(arg.kind, ast::FunctionArg::Zelf).then(|| arg.span)) + } else { + None + } } + pub fn is_public(&self, db: &dyn AnalyzerDb) -> bool { self.pub_span(db).is_some() } @@ -538,6 +519,44 @@ impl FunctionId { } } +/// A `Class` is an item that can have member functions. +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub enum Class { + Contract(ContractId), + Struct(StructId), +} +impl Class { + pub fn function(&self, db: &dyn AnalyzerDb, name: &str) -> Option { + match self { + Class::Contract(id) => id.function(db, name), + Class::Struct(id) => id.function(db, name), + } + } + pub fn self_function(&self, db: &dyn AnalyzerDb, name: &str) -> Option { + let fun = self.function(db, name)?; + fun.takes_self(db).then(|| fun) + } + + pub fn name(&self, db: &dyn AnalyzerDb) -> String { + match self { + Class::Contract(inner) => inner.name(db), + Class::Struct(inner) => inner.name(db), + } + } + pub fn kind(&self) -> &str { + match self { + Class::Contract(_) => "contract", + Class::Struct(_) => "struct", + } + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub enum MemberFunction { + BuiltIn(builtins::ValueMethod), + Function(FunctionId), +} + #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct Struct { pub ast: Node, @@ -570,19 +589,35 @@ impl StructId { pub fn field(&self, db: &dyn AnalyzerDb, name: &str) -> Option { self.fields(db).get(name).copied() } - /// All fields, including duplicates - pub fn all_fields(&self, db: &dyn AnalyzerDb) -> Rc> { - db.struct_all_fields(*self) + pub fn field_type( + &self, + db: &dyn AnalyzerDb, + name: &str, + ) -> Option> { + Some(self.field(db, name)?.typ(db)) } + pub fn fields(&self, db: &dyn AnalyzerDb) -> Rc> { db.struct_field_map(*self).value } - + pub fn functions(&self, db: &dyn AnalyzerDb) -> Rc> { + db.struct_function_map(*self).value + } + pub fn function(&self, db: &dyn AnalyzerDb, name: &str) -> Option { + self.functions(db).get(name).copied() + } + pub fn self_function(&self, db: &dyn AnalyzerDb, name: &str) -> Option { + self.function(db, name).filter(|f| f.takes_self(db)) + } pub fn sink_diagnostics(&self, db: &dyn AnalyzerDb, sink: &mut impl DiagnosticSink) { sink.push_all(db.struct_field_map(*self).diagnostics.iter()); db.struct_all_fields(*self) .iter() - .for_each(|field| field.sink_diagnostics(db, sink)); + .for_each(|id| id.sink_diagnostics(db, sink)); + + db.struct_all_functions(*self) + .iter() + .for_each(|id| id.sink_diagnostics(db, sink)); } } diff --git a/crates/analyzer/src/namespace/scopes.rs b/crates/analyzer/src/namespace/scopes.rs index 2e93d21c95..d5e1c88a89 100644 --- a/crates/analyzer/src/namespace/scopes.rs +++ b/crates/analyzer/src/namespace/scopes.rs @@ -2,7 +2,7 @@ use crate::context::{AnalyzerContext, CallType, ExpressionAttributes, FunctionBody, NamedThing}; use crate::errors::{AlreadyDefined, TypeError}; -use crate::namespace::items::{EventId, FunctionId, ModuleId}; +use crate::namespace::items::{Class, EventId, FunctionId, ModuleId}; use crate::namespace::types::FixedSize; use crate::AnalyzerDb; use fe_common::diagnostics::Diagnostic; @@ -142,10 +142,18 @@ impl<'a> AnalyzerContext for FunctionScope<'a> { } fn resolve_name(&self, name: &str) -> Option { + let sig = self.function.signature(self.db); + + if name == "self" { + return Some(NamedThing::SelfValue { + decl: sig.self_decl, + class: self.function.parent(self.db), + span: self.function.self_span(self.db), + }); + } + // Getting param names and spans should be simpler - self.function - .signature(self.db) - .params + sig.params .iter() .find_map(|param| { (param.name == name).then(|| { @@ -167,7 +175,7 @@ impl<'a> AnalyzerContext for FunctionScope<'a> { }) }) .or_else(|| { - if let Some(contract) = self.function.contract(self.db) { + if let Some(Class::Contract(contract)) = self.function.parent(self.db) { contract.resolve_name(self.db, name) } else { self.function.module(self.db).resolve_name(self.db, name) @@ -236,15 +244,6 @@ impl<'a, 'b> BlockScope<'a, 'b> { } } - pub fn contract_name(&self) -> Option { - Some( - self.root - .function - .contract(self.root.db)? - .name(self.root.db), - ) - } - /// Add a variable to the block scope. pub fn add_var( &mut self, @@ -252,36 +251,49 @@ impl<'a, 'b> BlockScope<'a, 'b> { typ: FixedSize, span: Span, ) -> Result<(), AlreadyDefined> { - if let Some(named_item) = self.resolve_name(name) { - if named_item.is_builtin() { + match self.resolve_name(name) { + Some(NamedThing::SelfValue { .. }) => { self.error( - &format!( - "variable name conflicts with built-in {}", - named_item.item_kind_display_name(), - ), - span, - &format!( - "`{}` is a built-in {}", - name, - named_item.item_kind_display_name() - ), - ); - return Err(AlreadyDefined); - } else { - // It's (currently) an error to shadow a variable in a nested scope - self.duplicate_name_error( - &format!("duplicate definition of variable `{}`", name), - name, - named_item - .name_span(self.db()) - .expect("missing name_span of non-builtin"), + "`self` can't be used as a variable name", span, + "expected a name, found keyword `self`", ); + Err(AlreadyDefined) + } + + Some(named_item) => { + if named_item.is_builtin() { + self.error( + &format!( + "variable name conflicts with built-in {}", + named_item.item_kind_display_name(), + ), + span, + &format!( + "`{}` is a built-in {}", + name, + named_item.item_kind_display_name() + ), + ); + return Err(AlreadyDefined); + } else { + // It's (currently) an error to shadow a variable in a nested scope + self.duplicate_name_error( + &format!("duplicate definition of variable `{}`", name), + name, + named_item + .name_span(self.db()) + .expect("missing name_span of non-builtin"), + span, + ); + } + Err(AlreadyDefined) + } + + None => { + self.variable_defs.insert(name.to_string(), (typ, span)); + Ok(()) } - Err(AlreadyDefined) - } else { - self.variable_defs.insert(name.to_string(), (typ, span)); - Ok(()) } } diff --git a/crates/analyzer/src/namespace/types.rs b/crates/analyzer/src/namespace/types.rs index 67534fd20b..ea8ca754d7 100644 --- a/crates/analyzer/src/namespace/types.rs +++ b/crates/analyzer/src/namespace/types.rs @@ -1,5 +1,6 @@ use crate::errors::{NotFixedSize, TypeError}; -use crate::namespace::items::{ContractId, StructId}; +use crate::namespace::items::{Class, ContractId, StructId}; +use crate::AnalyzerDb; use num_bigint::BigInt; use num_traits::ToPrimitive; @@ -38,7 +39,11 @@ pub enum Type { Map(Map), Tuple(Tuple), String(FeString), + /// An "external" contract. Effectively just a `newtype`d address. Contract(Contract), + /// The type of a contract while it's being executed. Ie. the type + /// of `self` within a contract function. + SelfContract(Contract), Struct(Struct), } @@ -102,12 +107,29 @@ pub struct Struct { pub id: StructId, pub field_count: usize, } +impl Struct { + pub fn from_id(id: StructId, db: &dyn AnalyzerDb) -> Self { + Self { + name: id.name(db), + id, + field_count: id.fields(db).len(), + } + } +} #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Contract { pub name: String, pub id: ContractId, } +impl Contract { + pub fn from_id(id: ContractId, db: &dyn AnalyzerDb) -> Self { + Self { + name: id.name(db), + id, + } + } +} #[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)] pub struct FeString { @@ -116,14 +138,13 @@ pub struct FeString { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct FunctionSignature { - pub self_decl: SelfDecl, + pub self_decl: Option, pub params: Vec, pub return_type: Result, } #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)] pub enum SelfDecl { - None, Mutable, } @@ -322,18 +343,6 @@ impl Type { *self == Type::Base(Base::Unit) } - pub fn is_fixed_size(&self) -> bool { - match self { - Type::Array(_) => true, - Type::Base(_) => true, - Type::Tuple(_) => true, - Type::String(_) => true, - Type::Struct(_) => true, - Type::Contract(_) => true, - Type::Map(_) => false, - } - } - pub fn int(int_type: Integer) -> Self { Type::Base(Base::Numeric(int_type)) } @@ -361,6 +370,7 @@ pub trait TypeDowncast { fn as_map(&self) -> Option<&Map>; fn as_int(&self) -> Option; fn as_primitive(&self) -> Option; + fn as_class(&self) -> Option; } impl TypeDowncast for Type { @@ -400,6 +410,14 @@ impl TypeDowncast for Type { _ => None, } } + fn as_class(&self) -> Option { + match self { + Type::Struct(inner) => Some(Class::Struct(inner.id)), + Type::Contract(inner) => Some(Class::Contract(inner.id)), + Type::SelfContract(inner) => Some(Class::Contract(inner.id)), + _ => None, + } + } } impl TypeDowncast for Option<&Type> { @@ -421,6 +439,9 @@ impl TypeDowncast for Option<&Type> { fn as_primitive(&self) -> Option { self.and_then(|t| t.as_primitive()) } + fn as_class(&self) -> Option { + self.and_then(|t| t.as_class()) + } } impl From for Type { @@ -512,6 +533,7 @@ impl TryFrom for FixedSize { Type::Struct(val) => Ok(FixedSize::Struct(val)), Type::Map(_) => Err(NotFixedSize), Type::Contract(contract) => Ok(FixedSize::Contract(contract)), + Type::SelfContract(_) => Err(NotFixedSize), } } } @@ -608,6 +630,7 @@ impl fmt::Display for Type { Type::Tuple(inner) => inner.fmt(f), Type::String(inner) => inner.fmt(f), Type::Contract(inner) => inner.fmt(f), + Type::SelfContract(inner) => inner.fmt(f), Type::Struct(inner) => inner.fmt(f), } } diff --git a/crates/analyzer/src/operations.rs b/crates/analyzer/src/operations.rs index 548633c40a..62764b3aa1 100644 --- a/crates/analyzer/src/operations.rs +++ b/crates/analyzer/src/operations.rs @@ -14,6 +14,7 @@ pub fn index(value: Type, index: Type) -> Result { Type::Tuple(_) => Err(IndexingError::NotSubscriptable), Type::String(_) => Err(IndexingError::NotSubscriptable), Type::Contract(_) => Err(IndexingError::NotSubscriptable), + Type::SelfContract(_) => Err(IndexingError::NotSubscriptable), Type::Struct(_) => Err(IndexingError::NotSubscriptable), } } diff --git a/crates/analyzer/src/traversal/expressions.rs b/crates/analyzer/src/traversal/expressions.rs index 10c55e2e82..17370422f7 100644 --- a/crates/analyzer/src/traversal/expressions.rs +++ b/crates/analyzer/src/traversal/expressions.rs @@ -1,13 +1,13 @@ use crate::builtins::{ - BlockField, ChainField, ContractTypeMethod, GlobalMethod, MsgField, Object, SelfField, TxField, - ValueMethod, + BlockField, ChainField, ContractSelfField, ContractTypeMethod, GlobalFunction, GlobalObject, + MsgField, TxField, ValueMethod, }; use crate::context::{AnalyzerContext, CallType, ExpressionAttributes, Location, NamedThing}; use crate::errors::{FatalError, IndexingError, NotFixedSize}; -use crate::namespace::items::{ContractId, FunctionId, Item}; +use crate::namespace::items::{Class, FunctionId, Item}; use crate::namespace::scopes::{BlockScope, BlockScopeType}; use crate::namespace::types::{ - Array, Base, Contract, FeString, Integer, SelfDecl, Struct, Tuple, Type, TypeDowncast, U256, + Array, Base, Contract, FeString, Integer, Struct, Tuple, Type, TypeDowncast, U256, }; use crate::operations; use crate::traversal::call_args::{validate_arg_count, validate_named_args, LabelPolicy}; @@ -19,7 +19,6 @@ use fe_common::Span; use fe_parser::ast as fe; use fe_parser::ast::UnaryOperator; use fe_parser::node::Node; -use if_chain::if_chain; use num_bigint::BigInt; use std::convert::TryInto; use std::ops::RangeInclusive; @@ -43,7 +42,11 @@ pub fn expr( fe::Expr::BinOperation { .. } => expr_bin_operation(scope, exp, expected_type.as_int()), fe::Expr::UnaryOperation { .. } => expr_unary_operation(scope, exp, expected_type), fe::Expr::CompOperation { .. } => expr_comp_operation(scope, exp), - fe::Expr::Call { .. } => expr_call(scope, exp), + fe::Expr::Call { + func, + generic_args, + args, + } => expr_call(scope, func, generic_args, args), fe::Expr::List { elts } => expr_list(scope, elts, expected_type.as_array()), fe::Expr::Tuple { .. } => expr_tuple(scope, exp, expected_type.as_tuple()), fe::Expr::Str(_) => expr_str(scope, exp), @@ -189,6 +192,15 @@ pub fn assignable_expr( attributes.move_location = Some(Location::Memory); } } + SelfContract(_) => { + // We can't tell from here how `self is being misused; it might be + // `x = self` or `f(self)` or `for x in self` or ... + return Err(FatalError::new(scope.error( + "invalid use of contract `self`", + exp.span, + "`self` can't be used here", + ))); + } Map(_) => { return Err(FatalError::new(scope.error( "`Map` type cannot reside in memory", @@ -259,6 +271,43 @@ fn expr_name( let location = Location::assign_location(&typ); Ok(ExpressionAttributes::new(typ.into(), location)) } + Some(NamedThing::SelfValue { decl, class, .. }) => { + if let Some(class) = class { + if decl.is_none() { + scope.fancy_error( + "`self` is not defined", + vec![Label::primary(exp.span, "undefined value")], + vec![ + "add `self` to the scope by including it in the function signature" + .to_string(), + format!( + "Example: `fn {}(self, foo: bool)`", + scope.root.function.name(scope.db()) + ), + ], + ); + } + match class { + Class::Struct(id) => Ok(ExpressionAttributes::new( + Type::Struct(Struct::from_id(id, scope.db())), + Location::Memory, + )), + Class::Contract(id) => Ok(ExpressionAttributes::new( + Type::SelfContract(Contract::from_id(id, scope.db())), + Location::Value, + )), + } + } else { + Err(FatalError::new(scope.fancy_error( + "`self` can only be used in contract or struct functions", + vec![Label::primary( + exp.span, + "not allowed in functions defined outside of a contract or struct", + )], + vec![], + ))) + } + } Some(NamedThing::Item(Item::Constant(id))) => { let typ = id .typ(scope.db())? @@ -428,157 +477,170 @@ fn expr_attribute( scope: &mut BlockScope, exp: &Node, ) -> Result { - if let fe::Expr::Attribute { value, attr } = &exp.kind { - let base_type = |typ| Ok(ExpressionAttributes::new(Type::Base(typ), Location::Value)); - - // If the value is a name, check if it is a builtin object and attribute. - if let fe::Expr::Name(name) = &value.kind { - match Object::from_str(name) { - Ok(Object::Self_) => return expr_attribute_self(scope, value, attr), - Ok(Object::Block) => { - return match BlockField::from_str(&attr.kind) { - Ok(BlockField::Coinbase) => base_type(Base::Address), - Ok(BlockField::Difficulty) => base_type(U256), - Ok(BlockField::Number) => base_type(U256), - Ok(BlockField::Timestamp) => base_type(U256), - Err(_) => { - Err(FatalError::new(scope.fancy_error( - "Not a block field", - vec![ - Label::primary( - attr.span, - "", - ), - ], - vec!["Note: Only `coinbase`, `difficulty`, `number` and `timestamp` can be accessed on `block`.".into()], - ))) - } - } - } - Ok(Object::Chain) => { - return match ChainField::from_str(&attr.kind) { - Ok(ChainField::Id) => base_type(U256), - Err(_) => { - Err(FatalError::new(scope.fancy_error( - "Not a chain field", - vec![Label::primary(attr.span, "")], - vec!["Note: Only `id` can be accessed on `chain`.".into()], - ))) - } - } - } - Ok(Object::Msg) => { - return match MsgField::from_str(&attr.kind) { - Ok(MsgField::Sender) => base_type(Base::Address), - Ok(MsgField::Sig) => base_type(U256), - Ok(MsgField::Value) => base_type(U256), - Err(_) => { - Err(FatalError::new(scope.fancy_error( - "Not a `msg` field", - vec![ - Label::primary( - attr.span, - "", - ), - ], - vec!["Note: Only `sender`, `sig` and `value` can be accessed on `msg`.".into()], - ))) - } + let (target, field) = match &exp.kind { + fe::Expr::Attribute { value, attr } => (value, attr), + _ => unreachable!(), + }; + let base_type = |typ| Ok(ExpressionAttributes::new(Type::Base(typ), Location::Value)); + + // We have to check if its a global object first, because the global + // objects are magical type-less things that don't work with normal expr checking. + // This will all go away when the `Context` struct is ready. + + if let fe::Expr::Name(name) = &target.kind { + match GlobalObject::from_str(name) { + Ok(GlobalObject::Block) => { + return match BlockField::from_str(&field.kind) { + Ok(BlockField::Coinbase) => base_type(Base::Address), + Ok(BlockField::Difficulty) => base_type(U256), + Ok(BlockField::Number) => base_type(U256), + Ok(BlockField::Timestamp) => base_type(U256), + Err(_) => { + Err(FatalError::new(scope.fancy_error( + "Not a block field", + vec![ + Label::primary( + field.span, + "", + ), + ], + vec!["Note: Only `coinbase`, `difficulty`, `number` and `timestamp` can be accessed on `block`.".into()], + ))) } } - Ok(Object::Tx) => { - return match TxField::from_str(&attr.kind) { - Ok(TxField::GasPrice) => base_type(U256), - Ok(TxField::Origin) => base_type(Base::Address), - Err(_) => { - Err(FatalError::new(scope.fancy_error( - "Not a `tx` field", - vec![Label::primary(attr.span, "")], - vec![ - "Note: Only `gas_price` and `origin` can be accessed on `tx`." - .into(), - ], - ))) - } + } + Ok(GlobalObject::Chain) => { + return match ChainField::from_str(&field.kind) { + Ok(ChainField::Id) => base_type(U256), + Err(_) => { + Err(FatalError::new(scope.fancy_error( + "Not a chain field", + vec![Label::primary(field.span, "")], + vec!["Note: Only `id` can be accessed on `chain`.".into()], + ))) } } - Err(_) => {} } - } - - // We attempt to analyze the value as an expression. If this is successful, we - // build a new set of attributes from the value attributes. - let expression_attributes = expr(scope, value, None)?; - return match expression_attributes { - // If the value is a struct, we return the type of the attribute. The location stays the - // same and can be memory or storage. - ExpressionAttributes { - typ: Type::Struct(struct_), - location, - .. - } => { - if let Some(field) = struct_.id.field(scope.db(), &attr.kind) { - Ok(ExpressionAttributes::new( - field.typ(scope.db())?.into(), - location, - )) - } else { - Err(FatalError::new(scope.fancy_error( - &format!( - "No field `{}` exists on struct `{}`", - &attr.kind, struct_.name - ), - vec![Label::primary(attr.span, "undefined field")], - vec![], - ))) + Ok(GlobalObject::Msg) => { + return match MsgField::from_str(&field.kind) { + Ok(MsgField::Sender) => base_type(Base::Address), + Ok(MsgField::Sig) => base_type(U256), + Ok(MsgField::Value) => base_type(U256), + Err(_) => { + Err(FatalError::new(scope.fancy_error( + "Not a `msg` field", + vec![ + Label::primary( + field.span, + "", + ), + ], + vec!["Note: Only `sender`, `sig` and `value` can be accessed on `msg`.".into()], + ))) + } } } - ExpressionAttributes { - typ: Type::Tuple(tuple), - location, - .. - } => { - let item_index = match tuple_item_index(&attr.kind) { - Some(index) => index, - None => { - return Err(FatalError::new(scope.fancy_error( - &format!("No field `{}` exists on this tuple", &attr.kind), + Ok(GlobalObject::Tx) => { + return match TxField::from_str(&field.kind) { + Ok(TxField::GasPrice) => base_type(U256), + Ok(TxField::Origin) => base_type(Base::Address), + Err(_) => { + Err(FatalError::new(scope.fancy_error( + "Not a `tx` field", + vec![Label::primary(field.span, "")], vec![ - Label::primary( - attr.span, - "undefined field", - ) + "Note: Only `gas_price` and `origin` can be accessed on `tx`." + .into(), ], - vec!["Note: Tuple values are accessed via `itemN` properties such as `item0` or `item1`".into()], - ))); + ))) } - }; - - if let Some(typ) = tuple.items.get(item_index) { - Ok(ExpressionAttributes::new(typ.to_owned().into(), location)) - } else { - Err(FatalError::new(scope.fancy_error( - &format!("No field `item{}` exists on this tuple", item_index), - vec![Label::primary(attr.span, "unknown field")], - vec![format!( - "Note: The highest possible field for this tuple is `item{}`", - tuple.items.len() - 1 - )], - ))) } } - _ => Err(FatalError::new(scope.fancy_error( - &format!( - "No field `{}` exists on type {}", - &attr.kind, expression_attributes.typ - ), - vec![Label::primary(attr.span, "unknown field")], - vec![], - ))), - }; + Err(_) => {} + } } - unreachable!() + let attrs = expr(scope, target, None)?; + return match attrs.typ { + Type::SelfContract(contract) => { + // Check built-in `.address` field first. (This will go away soon.) + if let Ok(ContractSelfField::Address) = ContractSelfField::from_str(&field.kind) { + return Ok(ExpressionAttributes::new( + Type::Base(Base::Address), + Location::Value, + )); + } + + match contract.id.field_type(scope.db(), &field.kind) { + Some((typ, nonce)) => Ok(ExpressionAttributes::new( + typ?, + Location::Storage { nonce: Some(nonce) }, + )), + None => Err(FatalError::new(scope.fancy_error( + &format!("No field `{}` exists on this contract", &field.kind), + vec![Label::primary(field.span, "undefined field")], + vec![], + ))), + } + } + // If the value is a struct, we return the type of the struct field. The location stays the + // same and can be memory or storage. + Type::Struct(struct_) => { + if let Some(field) = struct_.id.field(scope.db(), &field.kind) { + Ok(ExpressionAttributes::new( + field.typ(scope.db())?.into(), + attrs.location, + )) + } else { + Err(FatalError::new(scope.fancy_error( + &format!( + "No field `{}` exists on struct `{}`", + &field.kind, struct_.name + ), + vec![Label::primary(field.span, "undefined field")], + vec![], + ))) + } + } + Type::Tuple(tuple) => { + let item_index = match tuple_item_index(&field.kind) { + Some(index) => index, + None => { + return Err(FatalError::new(scope.fancy_error( + &format!("No field `{}` exists on this tuple", &field.kind), + vec![ + Label::primary( + field.span, + "undefined field", + ) + ], + vec!["Note: Tuple values are accessed via `itemN` properties such as `item0` or `item1`".into()], + ))); + } + }; + + if let Some(typ) = tuple.items.get(item_index) { + Ok(ExpressionAttributes::new( + typ.to_owned().into(), + attrs.location, + )) + } else { + Err(FatalError::new(scope.fancy_error( + &format!("No field `item{}` exists on this tuple", item_index), + vec![Label::primary(field.span, "unknown field")], + vec![format!( + "Note: The highest possible field for this tuple is `item{}`", + tuple.items.len() - 1 + )], + ))) + } + } + _ => Err(FatalError::new(scope.fancy_error( + &format!("No field `{}` exists on type {}", &field.kind, attrs.typ), + vec![Label::primary(field.span, "unknown field")], + vec![], + ))), + }; } /// Pull the item index from the attribute string (e.g. "item4" -> "4"). @@ -590,33 +652,6 @@ fn tuple_item_index(item: &str) -> Option { } } -fn expr_attribute_self( - scope: &mut BlockScope, - value: &Node, - attr: &Node, -) -> Result { - let contract = resolve_self(scope, value.span)?; - - if let Ok(SelfField::Address) = SelfField::from_str(&attr.kind) { - return Ok(ExpressionAttributes::new( - Type::Base(Base::Address), - Location::Value, - )); - } - - match contract.field_type(scope.db(), &attr.kind) { - Some((typ, nonce)) => Ok(ExpressionAttributes::new( - typ?, - Location::Storage { nonce: Some(nonce) }, - )), - None => Err(FatalError::new(scope.fancy_error( - &format!("No field `{}` exists on this contract", &attr.kind), - vec![Label::primary(attr.span, "undefined field")], - vec![], - ))), - } -} - fn expr_bin_operation( scope: &mut BlockScope, exp: &Node, @@ -714,45 +749,188 @@ fn expr_unary_operation( fn expr_call( scope: &mut BlockScope, - exp: &Node, + func: &Node, + generic_args: &Option>>, + args: &Node>>, ) -> Result { - if let fe::Expr::Call { - func, - generic_args, - args, - } = &exp.kind - { - return match expr_call_type(scope, func, generic_args.as_ref())? { - CallType::BuiltinFunction(builtin) => { - expr_call_builtin_function(scope, builtin, func.span, args) - } - CallType::TypeConstructor { typ } => { - expr_call_type_constructor(scope, func.span, typ, args) - } - CallType::SelfAttribute { - func_name, - self_span, - } => expr_call_self_attribute(scope, &func_name, func.span, self_span, args), - CallType::Pure(func_id) => expr_call_pure(scope, func.span, func_id, args), - CallType::ValueAttribute => expr_call_value_attribute(scope, func, args), - CallType::TypeAttribute { typ, func_name } => { - expr_call_type_attribute(scope, typ, &func_name, func.span, args) - } - }; + let (attributes, call_type) = match &func.kind { + fe::Expr::Name(name) => expr_call_name(scope, name, func.span, generic_args, args)?, + fe::Expr::Attribute { value, attr } => { + // TODO: err if there are generic args + expr_call_method(scope, value, attr, generic_args, args)? + } + _ => { + let expression = expr(scope, func, None)?; + return Err(FatalError::new(scope.fancy_error( + &format!("`{}` type is not callable", expression.typ), + vec![Label::primary( + func.span, + format!("this has type `{}`", expression.typ), + )], + vec![], + ))); + } + }; + + if !scope.inherits_type(BlockScopeType::Unsafe) && call_type.is_unsafe(scope.db()) { + let mut labels = vec![Label::primary(func.span, "call to unsafe function")]; + let fn_name = call_type.function_name(scope.db()); + if let Some(function) = call_type.function() { + let def_name_span = function.name_span(scope.db()); + let unsafe_span = function.unsafe_span(scope.db()); + labels.push(Label::secondary( + def_name_span + unsafe_span, + format!("`{}` is defined here as unsafe", &fn_name), + )) + } + scope.fancy_error(&format!("unsafe function `{}` can only be called in an unsafe function or block", + &fn_name), + labels, + vec!["Hint: put this call in an `unsafe` block if you're confident that it's safe to use here".into()], + ); } - unreachable!() + scope.root.add_call(func, call_type); + Ok(attributes) +} + +fn expr_call_name( + scope: &mut BlockScope, + name: &str, + name_span: Span, + generic_args: &Option>>, + args: &Node>>, +) -> Result<(ExpressionAttributes, CallType), FatalError> { + check_for_call_to_init_fn(scope, name, name_span)?; + + let named_item = scope.resolve_name(name).ok_or_else(|| { + // Check for call to a fn in the current class that takes self. + if let Some(function) = scope + .root + .function + .parent(scope.db()) + .and_then(|class| class.self_function(scope.db(), name)) + { + // TODO: this doesn't have to be fatal + FatalError::new(scope.fancy_error( + &format!("`{}` must be called via `self`", name), + vec![ + Label::primary( + function.name_span(scope.db()), + &format!("`{}` is defined here as a function that takes `self`", name), + ), + Label::primary( + name_span, + format!("`{}` is called here as a standalone function", name), + ), + ], + vec![format!( + "Suggestion: use `self.{}(...)` instead of `{}(...)`", + name, name + )], + )) + } else { + FatalError::new(scope.error( + &format!("`{}` is not defined", name), + name_span, + &format!("`{}` has not been defined in this scope", name), + )) + } + })?; + + match named_item { + NamedThing::Item(Item::BuiltinFunction(function)) => { + expr_call_builtin_function(scope, function, name_span, generic_args, args) + } + NamedThing::Item(Item::Function(function)) => { + expr_call_pure(scope, function, generic_args, args) + } + NamedThing::Item(Item::Type(id)) => { + if let Some(args) = generic_args { + scope.fancy_error( + &format!("`{}` type is not generic", name), + vec![Label::primary( + args.span, + "unexpected generic argument list", + )], + vec![], + ); + } + expr_call_type_constructor(scope, id.typ(scope.db())?, name_span, args) + } + NamedThing::Item(Item::GenericType(generic)) => { + let concrete_type = + apply_generic_type_args(scope, generic, name_span, generic_args.as_ref())?; + expr_call_type_constructor(scope, concrete_type, name_span, args) + } + + // Nothing else is callable (for now at least) + NamedThing::SelfValue { .. } => Err(FatalError::new(scope.error( + "`self` is not callable", + name_span, + "can't be used as a function", + ))), + NamedThing::Variable { typ, span, .. } => Err(FatalError::new(scope.fancy_error( + &format!("`{}` is not callable", name), + vec![ + Label::secondary(span, format!("`{}` has type `{}`", name, typ?)), + Label::primary(name_span, format!("`{}` can't be used as a function", name)), + ], + vec![], + ))), + NamedThing::Item(Item::Constant(id)) => Err(FatalError::new(scope.error( + &format!("`{}` is not callable", name), + name_span, + &format!( + "`{}` is a constant of type `{}`, and can't be used as a function", + name, + id.typ(scope.db())?, + ), + ))), + NamedThing::Item(Item::Object(_)) => Err(FatalError::new(scope.error( + &format!("`{}` is not callable", name), + name_span, + &format!( + "`{}` is a built-in object, and can't be used as a function", + name + ), + ))), + NamedThing::Item(Item::Event(_)) => Err(FatalError::new(scope.fancy_error( + &format!("`{}` is not callable", name), + vec![Label::primary( + name_span, + &format!( + "`{}` is an event, and can't be constructed in this context", + name + ), + )], + vec![format!("Hint: to emit an event, use `emit {}(..)`", name)], + ))), + } } fn expr_call_builtin_function( scope: &mut BlockScope, - function: GlobalMethod, + function: GlobalFunction, name_span: Span, + generic_args: &Option>>, args: &Node>>, -) -> Result { +) -> Result<(ExpressionAttributes, CallType), FatalError> { + if let Some(args) = generic_args { + scope.error( + &format!( + "`{}` function does not expect generic arguments", + function.as_ref() + ), + args.span, + "unexpected generic argument list", + ); + } + let argument_attributes = expr_call_args(scope, args)?; - match function { - GlobalMethod::Keccak256 => { + + let attrs = match function { + GlobalFunction::Keccak256 => { validate_arg_count(scope, function.as_ref(), name_span, args, 1, "argument"); expect_no_label_on_arg(scope, args, 0); @@ -778,14 +956,13 @@ fn expr_call_builtin_function( ); } }; - - Ok(ExpressionAttributes::new(Type::Base(U256), Location::Value)) + ExpressionAttributes::new(Type::Base(U256), Location::Value) } - GlobalMethod::Balance => { + GlobalFunction::Balance => { validate_arg_count(scope, function.as_ref(), name_span, args, 0, "argument"); - Ok(ExpressionAttributes::new(Type::Base(U256), Location::Value)) + ExpressionAttributes::new(Type::Base(U256), Location::Value) } - GlobalMethod::BalanceOf => { + GlobalFunction::BalanceOf => { validate_arg_count(scope, function.as_ref(), name_span, args, 1, "argument"); expect_no_label_on_arg(scope, args, 0); @@ -805,9 +982,9 @@ fn expr_call_builtin_function( ); } }; - Ok(ExpressionAttributes::new(Type::Base(U256), Location::Value)) + ExpressionAttributes::new(Type::Base(U256), Location::Value) } - GlobalMethod::SendValue => { + GlobalFunction::SendValue => { validate_arg_count(scope, function.as_ref(), name_span, args, 2, "argument"); // There's no label support for builtin functions today. That problem disappears as soon as they are written in Fe expect_no_label_on_arg(scope, args, 0); @@ -853,63 +1030,54 @@ fn expr_call_builtin_function( } } - Ok(ExpressionAttributes::new(Type::unit(), Location::Value)) + ExpressionAttributes::new(Type::unit(), Location::Value) } - } + }; + Ok((attrs, CallType::BuiltinFunction(function))) } -fn expr_call_struct_constructor( +fn expr_call_pure( scope: &mut BlockScope, - name_span: Span, - struct_: Struct, + function: FunctionId, + generic_args: &Option>>, args: &Node>>, -) -> Result { - let db = scope.root.db; - let fields = struct_ - .id - .fields(db) - .iter() - .map(|(name, field)| (name.clone(), field.typ(db))) - .collect::>(); - validate_named_args( - scope, - &struct_.name, - name_span, +) -> Result<(ExpressionAttributes, CallType), FatalError> { + let fn_name = function.name(scope.db()); + if let Some(args) = generic_args { + scope.fancy_error( + &format!("`{}` function is not generic", fn_name), + vec![Label::primary( + args.span, + "unexpected generic argument list", + )], + vec![], + ); + } + + let sig = function.signature(scope.db()); + validate_named_args( + scope, + &fn_name, + function.name_span(scope.db()), args, - &fields, - LabelPolicy::AllowUnlabledIfNameEqual, + &sig.params, + LabelPolicy::AllowAnyUnlabeled, )?; - Ok(ExpressionAttributes::new( - Type::Struct(struct_), - Location::Memory, + let return_type = sig.return_type.clone()?; + let return_location = Location::assign_location(&return_type); + Ok(( + ExpressionAttributes::new(return_type.into(), return_location), + CallType::Pure(function), )) } -fn expect_no_label_on_arg( - scope: &mut BlockScope, - args: &Node>>, - arg_index: usize, -) { - if let Some(label) = args - .kind - .get(arg_index) - .and_then(|arg| arg.kind.label.as_ref()) - { - scope.error( - "argument should not be labeled", - label.span, - "remove this label", - ); - } -} - fn expr_call_type_constructor( scope: &mut BlockScope, - name_span: Span, typ: Type, + name_span: Span, args: &Node>>, -) -> Result { +) -> Result<(ExpressionAttributes, CallType), FatalError> { match typ { Type::Struct(struct_type) => { return expr_call_struct_constructor(scope, name_span, struct_type, args) @@ -942,13 +1110,13 @@ fn expr_call_type_constructor( validate_arg_count(scope, &format!("{}", typ), name_span, args, 1, "argument"); expect_no_label_on_arg(scope, args, 0); - match &typ { + let expr_attrs = match &typ { Type::String(string_type) => { if let Some(arg) = args.kind.first() { assignable_expr(scope, &arg.kind.value, None)?; validate_str_literal_fits_type(scope, &arg.kind.value, string_type); } - Ok(ExpressionAttributes::new(typ, Location::Memory)) + ExpressionAttributes::new(typ.clone(), Location::Memory) } Type::Contract(_) => { if let Some(arg) = args.kind.first() { @@ -957,7 +1125,7 @@ fn expr_call_type_constructor( scope.type_error("type mismatch", arg.span, &Base::Address, &typ); } } - Ok(ExpressionAttributes::new(typ, Location::Value)) + ExpressionAttributes::new(typ.clone(), Location::Value) } Type::Base(Base::Numeric(integer)) => { if let Some(arg) = args.kind.first() { @@ -981,15 +1149,13 @@ fn expr_call_type_constructor( } } } - Ok(ExpressionAttributes::new(typ, Location::Value)) + ExpressionAttributes::new(typ.clone(), Location::Value) } Type::Base(Base::Address) => { if let Some(arg) = args.kind.first() { let arg_attr = assignable_expr(scope, &arg.kind.value, None)?; match arg_attr.typ { - Type::Contract(_) - | Type::Base(Base::Numeric(_)) - | Type::Base(Base::Address) => {} + Type::Contract(_) | Type::Base(Base::Numeric(_) | Base::Address) => {} _ => { scope.fancy_error( &format!("`{}` can not be used as a parameter to `address(..)`", arg_attr.typ), @@ -999,10 +1165,7 @@ fn expr_call_type_constructor( } } }; - Ok(ExpressionAttributes::new( - Type::Base(Base::Address), - Location::Value, - )) + ExpressionAttributes::new(Type::Base(Base::Address), Location::Value) } Type::Base(Base::Unit) => unreachable!(), // rejected in expr_call_type Type::Base(Base::Bool) => unreachable!(), // handled above @@ -1010,647 +1173,502 @@ fn expr_call_type_constructor( Type::Struct(_) => unreachable!(), // handled above Type::Map(_) => unreachable!(), // handled above Type::Array(_) => unreachable!(), // handled above - } + Type::SelfContract(_) => unreachable!(), // unnameable; contract names all become Type::Contract + }; + Ok((expr_attrs, CallType::TypeConstructor(typ))) } -fn expr_call_args( +fn expr_call_struct_constructor( scope: &mut BlockScope, + name_span: Span, + struct_: Struct, args: &Node>>, -) -> Result, FatalError> { - args.kind +) -> Result<(ExpressionAttributes, CallType), FatalError> { + let db = scope.root.db; + let fields = struct_ + .id + .fields(db) .iter() - .map(|arg| assignable_expr(scope, &arg.kind.value, None)) - .collect::, _>>() -} - -fn check_for_call_to_init_fn( - scope: &mut BlockScope, - name: &str, - span: Span, -) -> Result<(), FatalError> { - if name == "__init__" { - Err(FatalError::new(scope.fancy_error( - "`__init__()` is not directly callable", - vec![Label::primary(span, "")], - vec![ - "Note: `__init__` is the constructor function, and can't be called at runtime." - .into(), - ], - ))) - } else { - Ok(()) - } -} - -fn resolve_self(scope: &mut BlockScope, use_span: Span) -> Result { - let contract = scope.root.function.contract(scope.db()).ok_or_else(|| { - FatalError::new(scope.fancy_error( - "`self` can only be used in contract functions", - vec![Label::primary( - use_span, - "not allowed in functions defined outside of a contract", - )], - vec![], - )) - })?; + .map(|(name, field)| (name.clone(), field.typ(db))) + .collect::>(); + validate_named_args( + scope, + &struct_.name, + name_span, + args, + &fields, + LabelPolicy::AllowUnlabledIfNameEqual, + )?; - if scope.root.function.signature(scope.db()).self_decl == SelfDecl::None { - scope.fancy_error( - "`self` is not defined", - vec![Label::primary(use_span, "undefined value")], - vec![ - "add `self` to the scope by including it in the function signature".to_string(), - "Example: `fn foo(self, bar: bool)`".to_string(), - ], - ); - } - Ok(contract) + Ok(( + ExpressionAttributes::new(Type::Struct(struct_.clone()), Location::Memory), + CallType::TypeConstructor(Type::Struct(struct_)), + )) } -fn check_for_unsafe_call_outside_unsafe( +fn expr_call_method( scope: &mut BlockScope, - fn_name: &str, - call_name_span: Span, - function: FunctionId, -) { - if_chain! { - if !scope.inherits_type(BlockScopeType::Unsafe); - if let Some(unsafe_span) = function.unsafe_span(scope.db()); - then { - let def_name_span = function.name_span(scope.db()); - scope.fancy_error(&format!("unsafe function `{}` can only be called in an unsafe function or block", - fn_name), - vec![Label::primary(call_name_span, "call to unsafe function"), - Label::secondary(unsafe_span + def_name_span, format!("`{}` is defined here as unsafe", fn_name))], - vec!["Hint: put this call in an `unsafe` block if you're confident that it's safe to use here".into()], - ); + target: &Node, + field: &Node, + generic_args: &Option>>, + args: &Node>>, +) -> Result<(ExpressionAttributes, CallType), FatalError> { + // We need to check if the target is a type or a global object before calling `expr()`. + // When the type method call syntax is changed to `MyType::foo()` and the global objects + // are replaced by `Context`, we can remove this. + // All other `NamedThing`s will be handled correctly by `expr()`. + if let fe::Expr::Name(name) = &target.kind { + match scope.resolve_name(name) { + Some(NamedThing::Item(Item::Type(id))) => { + return expr_call_type_attribute( + scope, + id.typ(scope.db())?, + target.span, + field, + generic_args, + args, + ) + } + Some(NamedThing::Item(Item::Object(object))) => { + return Err(FatalError::new(scope.error( + &format!( + "no function `{}` exists on `{}`", + &field.kind, + object.as_ref(), + ), + target.span + field.span, + "undefined function", + ))); + } + _ => {} } } -} - -fn expr_call_self_attribute( - scope: &mut BlockScope, - func_name: &str, - name_span: Span, - self_span: Span, - args: &Node>>, -) -> Result { - check_for_call_to_init_fn(scope, func_name, name_span)?; - let contract = resolve_self(scope, self_span)?; - if let Some(func) = contract.self_function(scope.db(), func_name) { - check_for_unsafe_call_outside_unsafe(scope, func_name, name_span, func); + let target_attributes = expr(scope, target, None)?; - let sig = func.signature(scope.root.db); - validate_named_args( + // Check built-in methods. + if let Ok(method) = ValueMethod::from_str(&field.kind) { + return expr_call_builtin_value_method( scope, - func_name, - name_span, + target_attributes, + target, + method, + field, args, - &sig.params, - LabelPolicy::AllowAnyUnlabeled, - )?; - - let return_type = sig.return_type.clone()?; - let return_location = Location::assign_location(&return_type); - Ok(ExpressionAttributes::new( - return_type.into(), - return_location, - )) - } else { - let voucher = if contract.pure_function(scope.db(), func_name).is_none() { - scope.fancy_error( - &format!("no function named `{}` exists in this contract", func_name), - vec![Label::primary(name_span, "undefined function")], - vec![], - ) - } else { - scope.fancy_error( - &format!("`{}` must be called without `self`", func_name), - vec![Label::primary(name_span, "function does not take self")], - vec![format!( - "Suggestion: try `{}(...)` instead of `self.{}(...)`", - func_name, func_name - )], - ) - }; - Err(FatalError::new(voucher)) + ); } -} - -fn expr_call_pure( - scope: &mut BlockScope, - call_name_span: Span, - function: FunctionId, - args: &Node>>, -) -> Result { - assert!(function.is_pure(scope.db())); - - let fn_name = function.name(scope.db()); - check_for_unsafe_call_outside_unsafe(scope, &fn_name, call_name_span, function); - - let sig = function.signature(scope.db()); - validate_named_args( - scope, - &fn_name, - function.name_span(scope.db()), - args, - &sig.params, - LabelPolicy::AllowAnyUnlabeled, - )?; - - let return_type = sig.return_type.clone()?; - let return_location = Location::assign_location(&return_type); - Ok(ExpressionAttributes::new( - return_type.into(), - return_location, - )) -} -fn expr_call_value_attribute( - scope: &mut BlockScope, - func: &Node, - args: &Node>>, -) -> Result { - if let fe::Expr::Attribute { value, attr } = &func.kind { - let value_attributes = expr(scope, value, None)?; + // If the target is a "class" type (contract or struct), check for a member function + if let Some(class) = target_attributes.typ.as_class() { + if matches!(class, Class::Contract(_)) { + check_for_call_to_init_fn(scope, &field.kind, field.span)?; + } + if let Some(method) = class.function(scope.db(), &field.kind) { + let is_self = is_self_value(target); - if let Type::Contract(contract) = &value_attributes.typ { - // We must ensure the expression is loaded onto the stack. - let expression = value_attributes.clone().into_loaded().map_err(|_| { - // TODO: Add test code that triggers this - FatalError::new(scope.fancy_error( - "can't move value onto stack", - vec![Label::primary(value.span, "Value to be moved")], + if is_self && !method.takes_self(scope.db()) { + scope.fancy_error( + &format!("`{}` must be called without `self`", &field.kind), + vec![Label::primary(field.span, "function does not take self")], vec![format!( - "Note: Can't move `{}` types on the stack", - value_attributes.typ + "Suggestion: try `{}(...)` instead of `self.{}(...)`", + &field.kind, &field.kind )], - )) - })?; - - scope.root.update_expression(value, expression); - return expr_call_contract_attribute( - scope, - contract.to_owned(), - &attr.kind, - attr.span, - args, - ); - } - - // for now all of these functions expect 0 arguments - validate_arg_count(scope, &attr.kind, attr.span, args, 0, "argument"); - - return match ValueMethod::from_str(&attr.kind) { - Err(_) => { - return Err(FatalError::new(scope.fancy_error( + ); + } else if !is_self && !method.is_public(scope.db()) { + scope.fancy_error( &format!( - "No function `{}` exists on type `{}`", - &attr.kind, &value_attributes.typ + "The function `{}` on `{} {}` is private", + &field.kind, + class.kind(), + class.name(scope.db()) ), - vec![Label::primary(attr.span, "undefined function")], + vec![ + Label::primary(field.span, "this function is not `pub`"), + Label::secondary( + method.data(scope.db()).ast.span, + format!("`{}` is defined here", &field.kind), + ), + ], vec![], - ))); + ); } - Ok(ValueMethod::Clone) => { - match value_attributes.location { - Location::Storage { .. } => { - scope.fancy_error( - "`clone()` called on value in storage", - vec![ - Label::primary(value.span, "this value is in storage"), - Label::secondary(attr.span, "hint: try `to_mem()` here"), - ], - vec![], - ); - } - Location::Value => { - scope.fancy_error( - "`clone()` called on primitive type", - vec![ - Label::primary(value.span, "this value does not need to be cloned"), - Label::secondary(attr.span, "hint: remove `.clone()`"), - ], - vec![], + + match class { + Class::Contract(_) => { + if !is_self { + // External contract address must be loaded onto the stack. + scope.root.update_expression( + target, + target_attributes + .into_loaded() + .expect("should be able to move contract type to stack"), ); } - Location::Memory => {} } - Ok(value_attributes.into_cloned()) - } - Ok(ValueMethod::ToMem) => { - match value_attributes.location { - Location::Storage { .. } => {} - Location::Value => { - scope.fancy_error( - "`to_mem()` called on primitive type", - vec![ - Label::primary( - value.span, - "this value does not need to be copied to memory", - ), - Label::secondary(attr.span, "hint: remove `.to_mem()`"), - ], - vec![], - ); - } - Location::Memory => { + Class::Struct(_) => { + if matches!(target_attributes.final_location(), Location::Storage { .. }) { scope.fancy_error( - "`to_mem()` called on value in memory", + "struct functions can only be called on structs in memory", vec![ - Label::primary(value.span, "this value is already in memory"), + Label::primary(target.span, "this value is in storage"), Label::secondary( - attr.span, - "hint: to make a copy, use `.clone()` here", + field.span, + "hint: copy the struct to memory with `.to_mem()`", ), ], vec![], ); } } - Ok(value_attributes.into_cloned()) } - Ok(ValueMethod::AbiEncode) => match &value_attributes.typ { - Type::Struct(struct_) => { - if value_attributes.final_location() != Location::Memory { - scope.fancy_error( - "value must be copied to memory", - vec![Label::primary(value.span, "this value is in storage")], - vec!["Hint: values located in storage can be copied to memory using the `to_mem` function.".into(), - "Example: `self.my_array.to_mem().abi_encode()`".into(), - ], - ); - } - Ok(ExpressionAttributes::new( - Type::Array(Array { - inner: Base::Numeric(Integer::U8), - size: struct_.id.fields(scope.db()).len() * 32, - }), - Location::Memory, - )) + let sig = method.signature(scope.db()); + let return_type = sig.return_type.clone()?; + + validate_named_args( + scope, + &field.kind, + field.span, + args, + &sig.params, + LabelPolicy::AllowAnyUnlabeled, + )?; + + let location = Location::assign_location(&return_type); + return Ok(( + ExpressionAttributes::new(return_type.into(), location), + CallType::ValueMethod { + is_self, + class, + method, + }, + )); + } + } + Err(FatalError::new(scope.fancy_error( + &format!( + "No function `{}` exists on type `{}`", + &field.kind, &target_attributes.typ + ), + vec![Label::primary(field.span, "undefined function")], + vec![], + ))) +} + +fn expr_call_builtin_value_method( + scope: &mut BlockScope, + value_attrs: ExpressionAttributes, + value: &Node, + method: ValueMethod, + method_name: &Node, + args: &Node>>, +) -> Result<(ExpressionAttributes, CallType), FatalError> { + // for now all of these functions expect 0 arguments + validate_arg_count( + scope, + &method_name.kind, + method_name.span, + args, + 0, + "argument", + ); + + let calltype = CallType::BuiltinValueMethod(method); + match method { + ValueMethod::Clone => { + match value_attrs.location { + Location::Storage { .. } => { + scope.fancy_error( + "`clone()` called on value in storage", + vec![ + Label::primary(value.span, "this value is in storage"), + Label::secondary(method_name.span, "hint: try `to_mem()` here"), + ], + vec![], + ); + } + Location::Value => { + scope.fancy_error( + "`clone()` called on primitive type", + vec![ + Label::primary(value.span, "this value does not need to be cloned"), + Label::secondary(method_name.span, "hint: remove `.clone()`"), + ], + vec![], + ); + } + Location::Memory => {} + } + Ok((value_attrs.into_cloned(), calltype)) + } + ValueMethod::ToMem => { + match value_attrs.location { + Location::Storage { .. } => {} + Location::Value => { + scope.fancy_error( + "`to_mem()` called on primitive type", + vec![ + Label::primary( + value.span, + "this value does not need to be copied to memory", + ), + Label::secondary(method_name.span, "hint: remove `.to_mem()`"), + ], + vec![], + ); + } + Location::Memory => { + scope.fancy_error( + "`to_mem()` called on value in memory", + vec![ + Label::primary(value.span, "this value is already in memory"), + Label::secondary( + method_name.span, + "hint: to make a copy, use `.clone()` here", + ), + ], + vec![], + ); + } + } + Ok((value_attrs.into_cloned(), calltype)) + } + ValueMethod::AbiEncode => match &value_attrs.typ { + Type::Struct(struct_) => { + if value_attrs.final_location() != Location::Memory { + scope.fancy_error( + "value must be copied to memory", + vec![Label::primary(value.span, "this value is in storage")], + vec!["Hint: values located in storage can be copied to memory using the `to_mem` function.".into(), + "Example: `self.my_array.to_mem().abi_encode()`".into(), + ], + ); } - Type::Tuple(tuple) => { - if value_attributes.final_location() != Location::Memory { - scope.fancy_error( - "value must be copied to memory", - vec![Label::primary(value.span, "this value is in storage")], - vec!["Hint: values located in storage can be copied to memory using the `to_mem` function.".into(), - "Example: `self.my_array.to_mem().abi_encode()`".into(), - ], - ); - } - Ok(ExpressionAttributes::new( + Ok(( + ExpressionAttributes::new( Type::Array(Array { inner: Base::Numeric(Integer::U8), - size: tuple.items.len() * 32, + size: struct_.id.fields(scope.db()).len() * 32, }), Location::Memory, - )) - } - _ => { + ), + calltype, + )) + } + Type::Tuple(tuple) => { + if value_attrs.final_location() != Location::Memory { scope.fancy_error( - &format!( - "value of type `{}` does not support `abi_encode()`", - value_attributes.typ - ), - vec![Label::primary( - value.span, - "this value cannot be encoded using `abi_encode()`", - )], - vec![ - "Hint: struct and tuple values can be encoded.".into(), - "Example: `(42,).abi_encode()`".into(), + "value must be copied to memory", + vec![Label::primary(value.span, "this value is in storage")], + vec!["Hint: values located in storage can be copied to memory using the `to_mem` function.".into(), + "Example: `self.my_array.to_mem().abi_encode()`".into(), ], ); - - Ok(ExpressionAttributes::new(Type::unit(), Location::Value)) } - }, - }; - } - unreachable!() + Ok(( + ExpressionAttributes::new( + Type::Array(Array { + inner: Base::Numeric(Integer::U8), + size: tuple.items.len() * 32, + }), + Location::Memory, + ), + calltype, + )) + } + _ => Err(FatalError::new(scope.fancy_error( + &format!( + "value of type `{}` does not support `abi_encode()`", + value_attrs.typ + ), + vec![Label::primary( + value.span, + "this value cannot be encoded using `abi_encode()`", + )], + vec![ + "Hint: struct and tuple values can be encoded.".into(), + "Example: `(42,).abi_encode()`".into(), + ], + ))), + }, + } } fn expr_call_type_attribute( scope: &mut BlockScope, typ: Type, - func_name: &str, - name_span: Span, + target_span: Span, + field: &Node, + generic_args: &Option>>, args: &Node>>, -) -> Result { - let arg_attributes = expr_call_args(scope, args)?; - - let report_circular_dependency = |scope: &mut BlockScope, contract: &str, method: &str| { - scope.fancy_error( - &format!("`{contract}.{}(...)` called within `{contract}` creates an illegal circular dependency", method, contract=contract), - vec![Label::primary(name_span, "Contract creation")], - vec![format!("Note: Consider using a dedicated factory contract to create instances of `{}`", contract)]); - }; +) -> Result<(ExpressionAttributes, CallType), FatalError> { + if let Some(generic_args) = generic_args { + scope.error( + "unexpected generic argument list", + generic_args.span, + "unexpected", + ); + } - // TODO: we should check for SomeContract.__init__() here and suggest create/create2 + let arg_attributes = expr_call_args(scope, args)?; - match (typ.clone(), ContractTypeMethod::from_str(func_name)) { - (Type::Contract(contract), Ok(ContractTypeMethod::Create2)) => { - validate_arg_count(scope, func_name, name_span, args, 2, "argument"); + if let Some(class) = typ.as_class() { + let class_name = class.name(scope.db()); - if scope.contract_name().as_ref() == Some(&contract.name) { - report_circular_dependency( - scope, - &contract.name, - ContractTypeMethod::Create2.as_ref(), - ); + if let Class::Contract(contract) = class { + // Check for Foo.create/create2 (this will go away when the context object is ready) + if let Ok(function) = ContractTypeMethod::from_str(&field.kind) { + if scope.root.function.parent(scope.db()) == Some(Class::Contract(contract)) { + scope.fancy_error( + &format!("`{contract}.{}(...)` called within `{contract}` creates an illegal circular dependency", function.as_ref(), contract=&class_name), + vec![Label::primary(field.span, "Contract creation")], + vec![format!("Note: Consider using a dedicated factory contract to create instances of `{}`", &class_name)]); + } + let arg_count = function.arg_count(); + validate_arg_count(scope, &field.kind, field.span, args, arg_count, "argument"); + + for i in 0..arg_count { + if let Some(attrs) = arg_attributes.get(i) { + if !matches!(&attrs.typ, Type::Base(Base::Numeric(_))) { + scope.fancy_error( + &format!( + "incorrect type for argument to `{}.{}`", + &class_name, + function.as_ref() + ), + vec![Label::primary( + args.kind[i].span, + format!("this has type `{}`; expected a number", &attrs.typ), + )], + vec![], + ); + } + } + } + return Ok(( + ExpressionAttributes::new(typ, Location::Value), + CallType::BuiltinAssociatedFunction { contract, function }, + )); } + } - if !matches!( - (&arg_attributes[0].typ, &arg_attributes[1].typ), - (Type::Base(Base::Numeric(_)), Type::Base(Base::Numeric(_))) - ) { - scope.fancy_error( - "function `create2` expects numeric parameters", - vec![Label::primary(args.span, "invalid argument")], + if let Some(function) = class.function(scope.db(), &field.kind) { + if function.takes_self(scope.db()) { + return Err(FatalError::new(scope.fancy_error( + &format!( + "`{}` function `{}` must be called on an instance of `{}`", + &class_name, &field.kind, &class_name, + ), + vec![Label::primary( + target_span, + format!( + "expected a value of type `{}`, found type name", + &class_name + ), + )], vec![], - ); + ))); } - Ok(ExpressionAttributes::new( - Type::Contract(contract), - Location::Value, - )) - } - (Type::Contract(contract), Ok(ContractTypeMethod::Create)) => { - validate_arg_count(scope, func_name, name_span, args, 1, "argument"); - if scope.contract_name().as_ref() == Some(&contract.name) { - report_circular_dependency( - scope, - &contract.name, - ContractTypeMethod::Create.as_ref(), + if !function.is_public(scope.db()) + && scope.root.function.parent(scope.db()) != Some(class) + { + scope.fancy_error( + &format!( + "the function `{}.{}` is private", + &class_name, + &field.kind, + ), + vec![ + Label::primary(field.span, "this function is not `pub`"), + Label::secondary( + function.data(scope.db()).ast.span, + format!("`{}` is defined here", &field.kind), + ), + ], + vec![ + format!("`{}.{}` can only be called from other functions within `{}`", &class_name, &field.kind, &class_name), + format!("Hint: use `pub fn {fun}(..` to make `{cls}.{fun}` callable from outside of `{cls}`", fun=&field.kind, cls=&class_name), + ], ); } - if !matches!(&arg_attributes[0].typ, Type::Base(Base::Numeric(_))) { - scope.fancy_error( - "function `create` expects numeric parameter", - vec![Label::primary(args.span, "invalid argument")], - vec![], + if matches!(class, Class::Contract(_)) { + // TODO: `MathLibContract::square(x)` can't be called yet, because yulgen doesn't compile-in + // pure functions defined on external contracts. + // We should also discuss how/if this will work when the external contract is deployed + // and called via STATICCALL or whatever. + scope.not_yet_implemented( + &format!("calling contract-associated pure functions. Consider moving `{}` outside of `{}`", + &field.kind, class_name), + target_span + field.span, ); } - Ok(ExpressionAttributes::new( - Type::Contract(contract), - Location::Value, - )) + let ret_type = function.signature(scope.db()).return_type.clone()?; + let location = Location::assign_location(&ret_type); + return Ok(( + ExpressionAttributes::new(ret_type.into(), location), + CallType::AssociatedFunction { class, function }, + )); } - _ => Err(FatalError::new(scope.fancy_error( - &format!("No function `{}` exists on type `{}`", func_name, &typ), - vec![Label::primary(name_span, "undefined function")], - vec![], - ))), } + + Err(FatalError::new(scope.fancy_error( + &format!("No function `{}` exists on type `{}`", &field.kind, &typ), + vec![Label::primary(field.span, "undefined function")], + vec![], + ))) } -fn expr_call_contract_attribute( +fn expr_call_args( scope: &mut BlockScope, - contract: Contract, - func_name: &str, - name_span: Span, args: &Node>>, -) -> Result { - check_for_call_to_init_fn(scope, func_name, name_span)?; - - if let Some(function) = contract.id.public_function(scope.db(), func_name) { - let sig = function.signature(scope.db()); - let return_type = sig.return_type.clone()?; - - validate_named_args( - scope, - func_name, - name_span, - args, - &sig.params, - LabelPolicy::AllowAnyUnlabeled, - )?; - - let location = Location::assign_location(&return_type); - Ok(ExpressionAttributes::new(return_type.into(), location)) - } else if let Some(function) = contract.id.function(scope.db(), func_name) { - Err(FatalError::new(scope.fancy_error( - &format!( - "The function `{}` on `contract {}` is private", - func_name, contract.name - ), - vec![ - Label::primary(name_span, "this function is not `pub`"), - Label::secondary( - function.data(scope.db()).ast.span, - format!("`{}` is defined here", func_name), - ), - ], - vec![], - ))) - } else { - Err(FatalError::new(scope.fancy_error( - &format!( - "No function `{}` exists on contract `{}`", - func_name, contract.name - ), - vec![Label::primary(name_span, "undefined function")], - vec![], - ))) - } +) -> Result, FatalError> { + args.kind + .iter() + .map(|arg| assignable_expr(scope, &arg.kind.value, None)) + .collect::, _>>() } -fn expr_call_type( +fn expect_no_label_on_arg( scope: &mut BlockScope, - func: &Node, - generic_args: Option<&Node>>, -) -> Result { - let call_type = match &func.kind { - fe::Expr::Name(name) => expr_name_call_type(scope, name, func.span, generic_args), - fe::Expr::Attribute { .. } => { - // TODO: err if there are generic args - expr_attribute_call_type(scope, func) - } - _ => { - let expression = expr(scope, func, None)?; - Err(FatalError::new(scope.fancy_error( - &format!("`{}` type is not callable", expression.typ), - vec![Label::primary( - func.span, - format!("this has type `{}`", expression.typ), - )], - vec![], - ))) - } - }?; - - scope.root.add_call(func, call_type.clone()); - Ok(call_type) + args: &Node>>, + arg_index: usize, +) { + if let Some(label) = args + .kind + .get(arg_index) + .and_then(|arg| arg.kind.label.as_ref()) + { + scope.error( + "argument should not be labeled", + label.span, + "remove this label", + ); + } } -fn expr_name_call_type( +fn check_for_call_to_init_fn( scope: &mut BlockScope, name: &str, - name_span: Span, - generic_args: Option<&Node>>, -) -> Result { - check_for_call_to_init_fn(scope, name, name_span)?; - - let named_item = scope.resolve_name(name).ok_or_else(|| { - if let Some(function) = scope - .root - .function - .contract(scope.db()) - .and_then(|contract| contract.self_function(scope.db(), name)) - { - FatalError::new(scope.fancy_error( - &format!("`{}` must be called via `self`", name), - vec![ - Label::primary( - function.name_span(scope.db()), - &format!("`{}` is defined here as a function that takes `self`", name), - ), - Label::primary( - name_span, - format!("`{}` is called here as a standalone function", name), - ), - ], - vec![format!( - "Suggestion: use `self.{}(...)` instead of `{}(...)`", - name, name - )], - )) - } else { - FatalError::new(scope.error( - &format!("`{}` is not defined", name), - name_span, - &format!("`{}` has not been defined in this scope", name), - )) - } - })?; - - match named_item { - NamedThing::Variable { typ, span, .. } => Err(FatalError::new(scope.fancy_error( - &format!("`{}` is not callable", name), + span: Span, +) -> Result<(), FatalError> { + if name == "__init__" { + Err(FatalError::new(scope.fancy_error( + "`__init__()` is not directly callable", + vec![Label::primary(span, "")], vec![ - Label::secondary(span, format!("`{}` has type `{}`", name, typ?)), - Label::primary(name_span, format!("`{}` can't be used as a function", name)), + "Note: `__init__` is the constructor function, and can't be called at runtime." + .into(), ], - vec![], - ))), - NamedThing::Item(Item::Constant(id)) => Err(FatalError::new(scope.error( - &format!("`{}` is not callable", name), - name_span, - &format!( - "`{}` is a constant of type `{}`, and can't be used as a function", - name, - id.typ(scope.db())?, - ), - ))), - NamedThing::Item(Item::Object(_)) => Err(FatalError::new(scope.error( - &format!("`{}` is not callable", name), - name_span, - &format!( - "`{}` is a built-in object, and can't be used as a function", - name - ), - ))), - NamedThing::Item(Item::Event(_)) => Err(FatalError::new(scope.fancy_error( - &format!("`{}` is not callable", name), - vec![Label::primary( - name_span, - &format!( - "`{}` is an event, and can't be constructed in this context", - name - ), - )], - vec![format!("Hint: to emit an event, use `emit {}(..)`", name)], - ))), - NamedThing::Item(Item::BuiltinFunction(function)) => { - if let Some(args) = generic_args { - scope.error( - &format!("`{}` function does not expect generic arguments", name), - args.span, - "unexpected generic argument list", - ); - } - Ok(CallType::BuiltinFunction(function)) - } - - NamedThing::Item(Item::Function(function)) => { - if let Some(args) = generic_args { - scope.fancy_error( - &format!("`{}` function is not generic", name), - vec![Label::primary( - args.span, - "unexpected generic argument list", - )], - vec![], - ); - } - Ok(CallType::Pure(function)) - } - NamedThing::Item(Item::Type(id)) => { - if let Some(args) = generic_args { - scope.fancy_error( - &format!("`{}` type is not generic", name), - vec![Label::primary( - args.span, - "unexpected generic argument list", - )], - vec![], - ); - } - Ok(CallType::TypeConstructor { - typ: id.typ(scope.db())?, - }) - } - NamedThing::Item(Item::GenericType(generic)) => Ok(CallType::TypeConstructor { - typ: apply_generic_type_args(scope, generic, name_span, generic_args)?, - }), - } -} - -fn expr_attribute_call_type( - scope: &mut BlockScope, - exp: &Node, -) -> Result { - if let fe::Expr::Attribute { value, attr } = &exp.kind { - if let fe::Expr::Name(name) = &value.kind { - match scope.resolve_name(name) { - Some(NamedThing::Item(Item::Object(object))) => match object { - Object::Self_ => { - return Ok(CallType::SelfAttribute { - func_name: attr.kind.to_string(), - self_span: value.span, - }) - } - Object::Block | Object::Chain | Object::Msg | Object::Tx => { - return Err(FatalError::new(scope.fancy_error( - &format!("no function `{}` exists on builtin `{}`", &attr.kind, &name), - vec![Label::primary(exp.span, "undefined function")], - vec![], - ))); - } - }, - Some(NamedThing::Item(Item::Type(id))) => { - return Ok(CallType::TypeAttribute { - typ: id.typ(scope.db())?, - func_name: attr.kind.to_string(), - }) - } - // Everything else is handled in expr_call_value_attribute - _ => {} - }; - } - - return Ok(CallType::ValueAttribute); + ))) + } else { + Ok(()) } - - unreachable!() } fn validate_numeric_literal_fits_type( @@ -1809,3 +1827,11 @@ fn to_bigint(num: &str) -> BigInt { .parse::() .expect("the numeric literal contains a invalid digit") } + +fn is_self_value(expr: &Node) -> bool { + if let fe::Expr::Name(name) = &expr.kind { + name == "self" + } else { + false + } +} diff --git a/crates/analyzer/tests/analysis.rs b/crates/analyzer/tests/analysis.rs index 6033be753f..13a39149d5 100644 --- a/crates/analyzer/tests/analysis.rs +++ b/crates/analyzer/tests/analysis.rs @@ -1,6 +1,5 @@ -use fe_analyzer::context; use fe_analyzer::namespace::items::{self, Item, TypeDef}; -use fe_analyzer::namespace::types::{Event, FixedSize, FunctionSignature, Type}; +use fe_analyzer::namespace::types::{Event, FixedSize}; use fe_analyzer::{AnalyzerDb, Db}; use fe_common::diagnostics::{diagnostics_string, print_diagnostics, Diagnostic, Label, Severity}; use fe_common::files::FileStore; @@ -8,6 +7,7 @@ use fe_parser::node::NodeId; use fe_parser::node::Span; use indexmap::IndexMap; use insta::assert_snapshot; +use smallvec::SmallVec; use std::collections::hash_map::DefaultHasher; use std::collections::HashMap; use std::fmt::Debug; @@ -46,10 +46,10 @@ macro_rules! test_analysis { // for larger diffs. I recommend commenting out all tests but one. fe_common::assert_snapshot_wasm!( concat!("snapshots/analysis__", stringify!($name), ".snap"), - build_snapshot(files, module, &db) + build_snapshot(&files, module, &db) ); } else { - assert_snapshot!(build_snapshot(files, module, &db)); + assert_snapshot!(build_snapshot(&files, module, &db)); } } }; @@ -60,6 +60,7 @@ test_analysis! { guest_book, "demos/guest_book.fe"} test_analysis! { uniswap, "demos/uniswap.fe"} test_analysis! { address_bytes10_map, "features/address_bytes10_map.fe"} test_analysis! { assert, "features/assert.fe"} +test_analysis! { associated_fns, "features/associated_fns.fe"} test_analysis! { aug_assign, "features/aug_assign.fe"} test_analysis! { base_tuple, "features/base_tuple.fe"} test_analysis! { call_statement_with_args, "features/call_statement_with_args.fe"} @@ -145,6 +146,7 @@ test_analysis! { balances, "features/balances.fe"} test_analysis! { sized_vals_in_sto, "features/sized_vals_in_sto.fe"} test_analysis! { strings, "features/strings.fe"} test_analysis! { structs, "features/structs.fe"} +test_analysis! { struct_fns, "features/struct_fns.fe"} test_analysis! { ternary_expression, "features/ternary_expression.fe"} test_analysis! { two_contracts, "features/two_contracts.fe"} test_analysis! { u8_u8_map, "features/u8_u8_map.fe"} @@ -162,128 +164,164 @@ test_analysis! { data_copying_stress, "stress/data_copying_stress.fe"} test_analysis! { tuple_stress, "stress/tuple_stress.fe"} test_analysis! { type_aliases, "features/type_aliases.fe"} -fn build_snapshot(file_store: FileStore, module: items::ModuleId, db: &dyn AnalyzerDb) -> String { - // contract and struct types aren't worth printing - let type_aliases = module +fn build_snapshot(file_store: &FileStore, module: items::ModuleId, db: &dyn AnalyzerDb) -> String { + let diagnostics = module .all_items(db) .iter() - .filter_map(|def| match def { - Item::Type(TypeDef::Alias(alias)) => { - Some((alias.data(db).ast.span, alias.typ(db).unwrap())) - } - _ => None, - }) - .collect::>(); + .map(|item| match item { + Item::Type(TypeDef::Alias(alias)) => vec![build_display_diagnostic( + alias.data(db).ast.span, + &alias.typ(db).unwrap(), + )], + Item::Type(TypeDef::Struct(struct_)) => [ + label_in_non_overlapping_groups( + &struct_ + .fields(db) + .values() + .map(|field| (field.data(db).ast.span, field.typ(db).unwrap())) + .collect::>(), - let struct_fields: Vec<(Span, Type)> = module - .all_structs(db) - .iter() - .map(|struc| { - struc - .all_fields(db) - .iter() - .map(|field| (field.data(db).ast.span, field.typ(db).unwrap().into())) - .collect::>() - }) - .flatten() - .collect(); + ), + struct_ + .functions(db) + .values() + .map(|id| function_diagnostics(*id, db)) + .flatten() + .collect(), + ] + .concat(), + Item::Type(TypeDef::Contract(contract)) => [ + label_in_non_overlapping_groups( + &contract + .fields(db) + .values() + .map(|field| (field.data(db).ast.span, field.typ(db).unwrap())) + .collect::>(), + ), + contract + .events(db) + .values() + .map(|id| event_diagnostics(*id, db)) + .flatten() + .collect(), + contract + .functions(db) + .values() + .map(|id| function_diagnostics(*id, db)) + .flatten() + .collect(), + ] + .concat(), - let contract_ids = module.all_contracts(db); - let contract_fields: Vec<(Span, Type)> = contract_ids - .iter() - .map(|contract| { - contract - .all_fields(db) - .iter() - .map(|field| (field.data(db).ast.span, field.typ(db).unwrap())) - .collect::>() - }) - .flatten() - .collect(); - let event_fields = contract_ids - .iter() - .map(|contract| { - contract - .all_events(db) - .iter() - .map(|event| { - // Event field spans are a bit of a hassle right now - event - .data(db) - .ast - .kind - .fields - .iter() - .map(|node| node.span) - .zip( - event - .typ(db) - .fields - .iter() - .map(|field| field.typ.clone().unwrap()), - ) - .collect::>() - }) - .flatten() - .collect::>() + Item::Function(id) => function_diagnostics(*id, db), + Item::Constant(id) => vec![build_display_diagnostic(id.span(db), &id.typ(db).unwrap())], + + + // Events can't be defined at the module level yet. + Item::Event(_) + // Built-in stuff + | Item::Type(TypeDef::Primitive(_)) + | Item::GenericType(_) + | Item::BuiltinFunction(_) + | Item::Object(_) => vec![], }) .flatten() - .collect::>(); + .collect::>(); - let all_function_ids: Vec = contract_ids - .iter() - .map(|contract| contract.all_functions(db).as_ref().clone()) - .flatten() - .collect(); + diagnostics_string(&diagnostics, file_store) +} - let function_sigs = all_function_ids - .iter() - .map(|fun| (fun.data(db).ast.span, fun.signature(db))) - .collect::)>>(); +fn new_diagnostic(labels: Vec