Skip to content

Commit

Permalink
Implement trait associated functions
Browse files Browse the repository at this point in the history
  • Loading branch information
cburgdorf committed Nov 21, 2022
1 parent 4850e6c commit 49c2972
Show file tree
Hide file tree
Showing 31 changed files with 534 additions and 36 deletions.
13 changes: 11 additions & 2 deletions crates/analyzer/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,13 @@ impl<T> Analysis<T> {

pub trait AnalyzerContext {
fn resolve_name(&self, name: &str, span: Span) -> Result<Option<NamedThing>, IncompleteItem>;
/// Resolves the given path and registers all errors
fn resolve_path(&self, path: &ast::Path, span: Span) -> Result<NamedThing, FatalError>;
fn maybe_resolve_path(&self, path: &ast::Path) -> Option<NamedThing>;
/// Resolves the given path only if it is visible. Does not register any errors
fn resolve_visible_path(&self, path: &ast::Path) -> Option<NamedThing>;
/// Resolves the given path. Does not register any errors
fn resolve_any_path(&self, path: &ast::Path) -> Option<NamedThing>;

fn add_diagnostic(&self, diag: Diagnostic);
fn db(&self) -> &dyn AnalyzerDb;

Expand Down Expand Up @@ -318,7 +323,11 @@ impl AnalyzerContext for TempContext {
panic!("TempContext can't resolve paths")
}

fn maybe_resolve_path(&self, _path: &ast::Path) -> Option<NamedThing> {
fn resolve_visible_path(&self, _path: &ast::Path) -> Option<NamedThing> {
panic!("TempContext can't resolve paths")
}

fn resolve_any_path(&self, _path: &ast::Path) -> Option<NamedThing> {
panic!("TempContext can't resolve paths")
}

Expand Down
52 changes: 38 additions & 14 deletions crates/analyzer/src/namespace/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,7 @@ impl TypeDef {
}
TypeDef::Enum(val) => val.pure_functions_as_items(db),
TypeDef::Contract(val) => val.pure_functions_as_items(db),
_ => todo!("cannot access items in types yet"),
_ => Rc::new(indexmap! {}),
}
}

Expand Down Expand Up @@ -1811,6 +1811,40 @@ impl ImplId {
vec![],
));
}

if impl_fn.takes_self(db) != trait_fn.takes_self(db) {
let ((selfy_thing, selfy_span), (non_selfy_thing, non_selfy_span)) =
if impl_fn.takes_self(db) {
(
("impl", impl_fn.name_span(db)),
("trait", trait_fn.name_span(db)),
)
} else {
(
("trait", trait_fn.name_span(db)),
("impl", impl_fn.name_span(db)),
)
};
sink.push(&errors::fancy_error(
format!(
"method `{}` has a `self` declaration in the {}, but not in the `{}`",
impl_fn.name(db),
selfy_thing,
non_selfy_thing
),
vec![
Label::primary(
selfy_span,
format!("`self` declared on the `{}`", selfy_thing),
),
Label::primary(
non_selfy_span,
format!("no `self` declared on the `{}`", non_selfy_thing),
),
],
vec![],
));
}
} else {
sink.push(&errors::fancy_error(
format!(
Expand Down Expand Up @@ -1913,19 +1947,9 @@ impl TraitId {
}

pub fn sink_diagnostics(&self, db: &dyn AnalyzerDb, sink: &mut impl DiagnosticSink) {
db.trait_all_functions(*self).iter().for_each(|id| {
if !id.takes_self(db) {
sink.push(&errors::fancy_error(
"associated functions aren't yet supported in traits",
vec![Label::primary(
id.data(db).ast.span,
"function doesn't take `self`",
)],
vec!["Hint: add a `self` param to this function".into()],
));
}
id.sink_diagnostics(db, sink)
});
db.trait_all_functions(*self)
.iter()
.for_each(|id| id.sink_diagnostics(db, sink));
}
}

Expand Down
37 changes: 32 additions & 5 deletions crates/analyzer/src/namespace/scopes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ impl<'a> AnalyzerContext for ItemScope<'a> {
}
}

fn maybe_resolve_path(&self, path: &ast::Path) -> Option<NamedThing> {
fn resolve_visible_path(&self, path: &ast::Path) -> Option<NamedThing> {
let resolved = self.module.resolve_path_internal(self.db(), path);

if resolved.diagnostics.len() > 0 {
Expand All @@ -156,6 +156,16 @@ impl<'a> AnalyzerContext for ItemScope<'a> {
}
}

fn resolve_any_path(&self, path: &ast::Path) -> Option<NamedThing> {
let resolved = self.module.resolve_path_internal(self.db(), path);

if resolved.diagnostics.len() > 0 {
return None;
}

resolved.value
}

fn add_diagnostic(&self, diag: Diagnostic) {
self.diagnostics.borrow_mut().push(diag)
}
Expand Down Expand Up @@ -381,7 +391,7 @@ impl<'a> AnalyzerContext for FunctionScope<'a> {
}
}

fn maybe_resolve_path(&self, path: &ast::Path) -> Option<NamedThing> {
fn resolve_visible_path(&self, path: &ast::Path) -> Option<NamedThing> {
let resolved = self
.function
.module(self.db())
Expand All @@ -399,6 +409,19 @@ impl<'a> AnalyzerContext for FunctionScope<'a> {
}
}

fn resolve_any_path(&self, path: &ast::Path) -> Option<NamedThing> {
let resolved = self
.function
.module(self.db())
.resolve_path_internal(self.db(), path);

if resolved.diagnostics.len() > 0 {
return None;
}

resolved.value
}

fn get_context_type(&self) -> Option<TypeId> {
if let Ok(Some(NamedThing::Item(Item::Type(TypeDef::Struct(id))))) =
self.resolve_name("Context", Span::dummy())
Expand Down Expand Up @@ -523,8 +546,12 @@ impl AnalyzerContext for BlockScope<'_, '_> {
self.root.resolve_path(path, span)
}

fn maybe_resolve_path(&self, path: &ast::Path) -> Option<NamedThing> {
self.root.maybe_resolve_path(path)
fn resolve_visible_path(&self, path: &ast::Path) -> Option<NamedThing> {
self.root.resolve_visible_path(path)
}

fn resolve_any_path(&self, path: &ast::Path) -> Option<NamedThing> {
self.root.resolve_any_path(path)
}

fn add_diagnostic(&self, diag: Diagnostic) {
Expand Down Expand Up @@ -627,7 +654,7 @@ impl<T> OptionExt for Option<T> {

/// Check an item visibility and sink diagnostics if an item is invisible from
/// the scope.
fn check_visibility(context: &dyn AnalyzerContext, named_thing: &NamedThing, span: Span) {
pub fn check_visibility(context: &dyn AnalyzerContext, named_thing: &NamedThing, span: Span) {
if let NamedThing::Item(item) = named_thing {
let item_module = item
.module(context.db())
Expand Down
36 changes: 36 additions & 0 deletions crates/analyzer/src/namespace/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ use std::rc::Rc;
use std::str::FromStr;
use strum::{AsRefStr, EnumIter, EnumString};

use super::items::FunctionId;
use super::items::Item;

pub fn u256_min() -> BigInt {
BigInt::from(0)
}
Expand Down Expand Up @@ -53,6 +56,9 @@ pub enum Type {
Enum(EnumId),
Generic(Generic),
}

type TraitFunctionLookup = (Vec<(FunctionId, ImplId)>, Vec<(FunctionId, ImplId)>);

#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub struct TypeId(pub(crate) u32);
impl_intern_key!(TypeId);
Expand Down Expand Up @@ -131,6 +137,36 @@ impl TypeId {
db.all_impls(*self)
}

/// Looks up all possible candidates of the given function name that are implemented via traits.
pub fn trait_function_candidates(
&self,
context: &mut dyn AnalyzerContext,
fn_name: &str,
) -> TraitFunctionLookup {
let candidates = self
.get_all_impls(context.db())
.iter()
.cloned()
.filter_map(|_impl| {
_impl
.function(context.db(), fn_name)
.map(|fun| (fun, _impl))
})
.collect::<Vec<_>>();

let in_scope_candidates = candidates
.iter()
.cloned()
.filter(|(_, _impl)| {
context
.module()
.is_in_scope(context.db(), Item::Trait(_impl.trait_id(context.db())))
})
.collect::<Vec<_>>();

(candidates, in_scope_candidates)
}

pub fn function_sig(&self, db: &dyn AnalyzerDb, name: &str) -> Option<FunctionSigId> {
match self.typ(db) {
Type::Contract(id) => id.function(db, name).map(|fun| fun.sig(db)),
Expand Down
137 changes: 134 additions & 3 deletions crates/analyzer/src/traversal/expressions.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::display::Displayable;
use crate::errors::{FatalError, IndexingError};
use crate::namespace::items::{FunctionId, FunctionSigId, ImplId, Item, StructId, TypeDef};
use crate::namespace::scopes::BlockScopeType;
use crate::namespace::scopes::{check_visibility, BlockScopeType};
use crate::namespace::types::{Array, Base, FeString, Integer, Tuple, Type, TypeDowncast, TypeId};
use crate::operations;
use crate::traversal::call_args::{validate_arg_count, validate_named_args};
Expand Down Expand Up @@ -995,8 +995,139 @@ fn expr_call_path<T: std::fmt::Display>(
generic_args: &Option<Node<Vec<fe::GenericArg>>>,
args: &Node<Vec<Node<fe::CallArg>>>,
) -> Result<(ExpressionAttributes, CallType), FatalError> {
let named_thing = context.resolve_path(path, func.span)?;
expr_call_named_thing(context, named_thing, func, generic_args, args)
match context.resolve_any_path(path) {
Some(named_thing) => {
check_visibility(context, &named_thing, func.span);
validate_has_no_conflicting_trait_in_scope(context, &named_thing, path, func)?;
expr_call_named_thing(context, named_thing, func, generic_args, args)
}
// If we we can't resolve a call to a path e.g. `foo::Bar::do_thing()` there is a chance that `do_thing`
// does not exist on type `foo::Bar` but is implemented as a trait associated function for `foo::Bar`.
None => expr_call_trait_associated_function(context, path, func, generic_args, args),
}
}

fn validate_has_no_conflicting_trait_in_scope<T: std::fmt::Display>(
context: &mut dyn AnalyzerContext,
named_thing: &NamedThing,
path: &fe::Path,
func: &Node<T>,
) -> Result<(), FatalError> {
let fn_name = &path.segments.last().unwrap().kind;
let parent_path = path.remove_last();
let parent_thing = context.resolve_path(&parent_path, func.span)?;

if let NamedThing::Item(Item::Type(val)) = parent_thing {
let type_id = val.type_id(context.db())?;
let (_, in_scope_candidates) = type_id.trait_function_candidates(context, fn_name);

if !in_scope_candidates.is_empty() {
let labels = vec![Label::primary(
named_thing.name_span(context.db()).unwrap(),
format!(
"candidate 1 is defined here on the {}",
parent_thing.item_kind_display_name(),
),
)]
.into_iter()
.chain(
in_scope_candidates
.iter()
.enumerate()
.map(|(idx, (fun, _impl))| {
Label::primary(
fun.name_span(context.db()),
format!(
"candidate #{} is defined here on trait `{}`",
idx + 2,
_impl.trait_id(context.db()).name(context.db())
),
)
}),
)
.collect::<Vec<_>>();

return Err(FatalError::new(context.fancy_error(
"multiple applicable items in scope",
labels,
vec![
"Hint: Rename one of the methods or make sure only one of them is in scope"
.into(),
],
)));
}
}

Ok(())
}

fn expr_call_trait_associated_function<T: std::fmt::Display>(
context: &mut dyn AnalyzerContext,
path: &fe::Path,
func: &Node<T>,
generic_args: &Option<Node<Vec<fe::GenericArg>>>,
args: &Node<Vec<Node<fe::CallArg>>>,
) -> Result<(ExpressionAttributes, CallType), FatalError> {
let fn_name = &path.segments.last().unwrap().kind;
let parent_path = path.remove_last();
let parent_thing = context.resolve_path(&parent_path, func.span)?;

if let NamedThing::Item(Item::Type(val)) = parent_thing {
let type_id = val.type_id(context.db())?;

let (candidates, in_scope_candidates) = type_id.trait_function_candidates(context, fn_name);

if in_scope_candidates.len() > 1 {
context.fancy_error(
"multiple applicable items in scope",
in_scope_candidates
.iter()
.enumerate()
.map(|(idx, (fun, _impl))| {
Label::primary(
fun.name_span(context.db()),
format!(
"candidate #{} is defined here on trait `{}`",
idx + 1,
_impl.trait_id(context.db()).name(context.db())
),
)
})
.collect(),
vec![
"Hint: Rename one of the methods or make sure only one of them is in scope"
.into(),
],
);
// We arbitrarily carry on with the first candidate since the error doesn't need to be fatal
let (fun, _) = in_scope_candidates[0];
return expr_call_pure(context, fun, func.span, generic_args, args);
} else if in_scope_candidates.is_empty() && !candidates.is_empty() {
context.fancy_error(
"Applicable items exist but are not in scope",
candidates.iter().enumerate().map(|(idx, (fun, _impl ))| {
Label::primary(fun.name_span(context.db()), format!(
"candidate #{} is defined here on trait `{}`",
idx + 1,
_impl.trait_id(context.db()).name(context.db())
))
}).collect(),
vec!["Hint: Bring one of these candidates in scope via `use module_name::trait_name`".into()],
);
// We arbitrarily carry on with an applicable candidate since the error doesn't need to be fatal
let (fun, _) = candidates[0];
return expr_call_pure(context, fun, func.span, generic_args, args);
} else if in_scope_candidates.len() == 1 {
let (fun, _) = in_scope_candidates[0];
return expr_call_pure(context, fun, func.span, generic_args, args);
}
}

Err(FatalError::new(context.error(
"unresolved path item",
func.span,
"not found",
)))
}

fn expr_call_named_thing<T: std::fmt::Display>(
Expand Down
2 changes: 1 addition & 1 deletion crates/analyzer/src/traversal/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ fn match_pattern(
tuple_pattern(scope, elts, &expected_elts, pat.span, None)
}

Pattern::Path(path) => match scope.maybe_resolve_path(&path.kind) {
Pattern::Path(path) => match scope.resolve_visible_path(&path.kind) {
Some(NamedThing::EnumVariant(variant)) => {
let db = scope.db();
let parent_type = variant.parent(db).as_type(db);
Expand Down
Loading

0 comments on commit 49c2972

Please sign in to comment.