Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: require trait method calls (foo.bar()) to have the trait in scope (imported) #6895

Merged
merged 24 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1df848d
fix: track trait IDs for trait impls on structs
asterite Dec 19, 2024
33e7b97
feat: require trait methods in paths to be in scope
asterite Dec 19, 2024
ed171c3
Show trait full path in error message
asterite Dec 19, 2024
9294758
Fix frontend tests
asterite Dec 19, 2024
6eaf88f
Fix bug introduced in fully_qualified_module_path
asterite Dec 19, 2024
28fdbe2
Add a couple more comments
asterite Dec 19, 2024
f661567
Fix: multiple trait methods were not registered for structs
asterite Dec 20, 2024
c274629
Reuse `Elaborator::lookup_method` in interpreter
asterite Dec 20, 2024
c980099
Require traits to be in scope to call methods
asterite Dec 20, 2024
4797814
Merge branch 'master' into ab/require-trait-to-be-in-scope
asterite Dec 20, 2024
8b6b175
Merge branch 'ab/require-trait-to-be-in-scope' into ab/require-trait-…
asterite Dec 20, 2024
3cbe8d0
Move test somewhere else
asterite Dec 20, 2024
07383b8
Merge branch 'master' into ab/require-trait-to-be-in-scope-for-methods
asterite Jan 3, 2025
c94448c
Remove duplicate method
asterite Jan 3, 2025
fdb2311
Merge branch 'master' into ab/require-trait-to-be-in-scope-for-methods
asterite Jan 5, 2025
91abe70
Merge branch 'master' into ab/require-trait-to-be-in-scope-for-methods
asterite Jan 6, 2025
44dbb3b
cargo fmt
asterite Jan 6, 2025
bddf052
Merge branch 'master' into ab/require-trait-to-be-in-scope-for-methods
asterite Jan 6, 2025
95366c0
Merge branch 'master' into ab/require-trait-to-be-in-scope-for-methods
asterite Jan 6, 2025
e2fe92d
Merge branch 'master' into ab/require-trait-to-be-in-scope-for-methods
TomAFrench Jan 6, 2025
3088b1d
Merge branch 'master' into ab/require-trait-to-be-in-scope-for-methods
asterite Jan 7, 2025
3248790
Revert "chore: also print test output to stdout in CI (#6930)"
asterite Jan 7, 2025
f3813b4
Merge branch 'master' into ab/require-trait-to-be-in-scope-for-methods
asterite Jan 7, 2025
d004015
Reapply "chore: also print test output to stdout in CI (#6930)"
asterite Jan 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test-js-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ jobs:
- name: Run nargo test
working-directory: ./test-repo/${{ matrix.project.path }}
run: |
nargo test --silence-warnings --pedantic-solving --skip-brillig-constraints-check --format json ${{ matrix.project.nargo_args }} | tee ${{ github.workspace }}/noir-repo/.github/critical_libraries_status/${{ matrix.project.repo }}/${{ matrix.project.path }}.actual.jsonl
nargo test --silence-warnings --skip-brillig-constraints-check --format json ${{ matrix.project.nargo_args }} | tee ${{ github.workspace }}/noir-repo/.github/critical_libraries_status/${{ matrix.project.repo }}/${{ matrix.project.path }}.actual.jsonl
env:
NARGO_IGNORE_TEST_FAILURES_FROM_FOREIGN_CALLS: true

Expand Down
2 changes: 1 addition & 1 deletion compiler/noirc_frontend/src/elaborator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1451,7 +1451,7 @@ impl<'context> Elaborator<'context> {
let method_name = self.interner.function_name(method_id).to_owned();

if let Some(first_fn) =
self.interner.add_method(self_type, method_name.clone(), *method_id, false)
self.interner.add_method(self_type, method_name.clone(), *method_id, None)
{
let error = ResolverError::DuplicateDefinition {
name: method_name,
Expand Down
7 changes: 1 addition & 6 deletions compiler/noirc_frontend/src/elaborator/path_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ use iter_extended::vecmap;
use noirc_errors::{Location, Span};

use crate::ast::{Ident, Path, PathKind, UnresolvedType};
use crate::hir::def_map::{fully_qualified_module_path, ModuleData, ModuleDefId, ModuleId, PerNs};
use crate::hir::def_map::{ModuleData, ModuleDefId, ModuleId, PerNs};
use crate::hir::resolution::import::{resolve_path_kind, PathResolutionError};

use crate::hir::resolution::errors::ResolverError;
use crate::hir::resolution::visibility::item_in_module_is_visible;

use crate::hir_def::traits::Trait;
use crate::locations::ReferencesTracker;
use crate::node_interner::{FuncId, GlobalId, StructId, TraitId, TypeAliasId};
use crate::{Shared, Type, TypeAlias};
Expand Down Expand Up @@ -448,10 +447,6 @@ impl<'context> Elaborator<'context> {
let per_ns = PerNs { types: None, values: Some(*item) };
StructMethodLookupResult::FoundTraitMethod(per_ns, trait_id)
}

fn fully_qualified_trait_path(&self, trait_: &Trait) -> String {
fully_qualified_module_path(self.def_maps, self.crate_graph, &trait_.crate_id, trait_.id.0)
}
}

fn merge_intermediate_path_resolution_item_with_module_def_id(
Expand Down
155 changes: 128 additions & 27 deletions compiler/noirc_frontend/src/elaborator/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::{
},
hir::{
def_collector::dc_crate::CompilationError,
def_map::{fully_qualified_module_path, ModuleDefId},
resolution::{errors::ResolverError, import::PathResolutionError},
type_check::{
generics::{Generic, TraitGenerics},
Expand All @@ -32,7 +33,8 @@ use crate::{
TraitImplKind, TraitMethodId,
},
token::SecondaryAttribute,
Generics, Kind, ResolvedGeneric, Type, TypeBinding, TypeBindings, UnificationError,
Generics, Kind, ResolvedGeneric, Shared, StructType, Type, TypeBinding, TypeBindings,
UnificationError,
};

use super::{lints, path_resolution::PathResolutionItem, Elaborator, UnsafeBlockStatus};
Expand Down Expand Up @@ -1309,7 +1311,7 @@ impl<'context> Elaborator<'context> {
}
}

pub(super) fn lookup_method(
pub(crate) fn lookup_method(
&mut self,
object_type: &Type,
method_name: &str,
Expand All @@ -1318,31 +1320,7 @@ impl<'context> Elaborator<'context> {
) -> Option<HirMethodReference> {
match object_type.follow_bindings() {
Type::Struct(typ, _args) => {
let id = typ.borrow().id;
match self.interner.lookup_method(object_type, id, method_name, false, has_self_arg)
{
Some(method_id) => Some(HirMethodReference::FuncId(method_id)),
None => {
let has_field_with_function_type =
typ.borrow().get_fields_as_written().into_iter().any(|field| {
field.name.0.contents == method_name && field.typ.is_function()
});
if has_field_with_function_type {
self.push_err(TypeCheckError::CannotInvokeStructFieldFunctionType {
method_name: method_name.to_string(),
object_type: object_type.clone(),
span,
});
} else {
self.push_err(TypeCheckError::UnresolvedMethodCall {
method_name: method_name.to_string(),
object_type: object_type.clone(),
span,
});
}
None
}
}
self.lookup_struct_method(object_type, method_name, span, has_self_arg, typ)
}
// TODO: We should allow method calls on `impl Trait`s eventually.
// For now it is fine since they are only allowed on return types.
Expand Down Expand Up @@ -1388,6 +1366,125 @@ impl<'context> Elaborator<'context> {
}
}

fn lookup_struct_method(
&mut self,
object_type: &Type,
method_name: &str,
span: Span,
has_self_arg: bool,
typ: Shared<StructType>,
) -> Option<HirMethodReference> {
let id = typ.borrow().id;

// First search in the struct methods
if let Some(method_id) =
self.interner.lookup_direct_method(object_type, id, method_name, has_self_arg)
{
return Some(HirMethodReference::FuncId(method_id));
}

// Next lookup all matching trait methods.
let trait_methods =
self.interner.lookup_trait_methods(object_type, id, method_name, has_self_arg);

if trait_methods.is_empty() {
// If we couldn't find any trait methods, search in
// impls for all types `T`, e.g. `impl<T> Foo for T`
if let Some(func_id) =
self.interner.lookup_generic_method(object_type, method_name, has_self_arg)
{
return Some(HirMethodReference::FuncId(func_id));
}

// Otherwise it's an error
let has_field_with_function_type = typ
.borrow()
.get_fields_as_written()
.into_iter()
.any(|field| field.name.0.contents == method_name && field.typ.is_function());
if has_field_with_function_type {
self.push_err(TypeCheckError::CannotInvokeStructFieldFunctionType {
method_name: method_name.to_string(),
object_type: object_type.clone(),
span,
});
} else {
self.push_err(TypeCheckError::UnresolvedMethodCall {
method_name: method_name.to_string(),
object_type: object_type.clone(),
span,
});
}
return None;
}

// We found some trait methods... but is only one of them currently in scope?
let module_id = self.module_id();
let module_data = self.get_module(module_id);

let trait_methods_in_scope: Vec<_> = trait_methods
.iter()
.filter_map(|(func_id, trait_id)| {
let trait_ = self.interner.get_trait(*trait_id);
let trait_name = &trait_.name;
let Some(map) = module_data.scope().types().get(trait_name) else {
return None;
};
let Some(imported_item) = map.get(&None) else {
return None;
};
if imported_item.0 == ModuleDefId::TraitId(*trait_id) {
Some((*func_id, *trait_id, trait_name))
} else {
None
}
})
.collect();

for (_, _, trait_name) in &trait_methods_in_scope {
self.usage_tracker.mark_as_used(module_id, trait_name);
}

if trait_methods_in_scope.is_empty() {
if trait_methods.len() == 1 {
// This is the backwards-compatible case where there's a single trait method but it's not in scope
let (func_id, trait_id) = trait_methods[0];
let trait_ = self.interner.get_trait(trait_id);
let trait_name = self.fully_qualified_trait_path(trait_);
self.push_err(PathResolutionError::TraitMethodNotInScope {
ident: Ident::new(method_name.into(), span),
trait_name,
});
return Some(HirMethodReference::FuncId(func_id));
} else {
let traits = vecmap(trait_methods, |(_, trait_id)| {
let trait_ = self.interner.get_trait(trait_id);
self.fully_qualified_trait_path(trait_)
});
self.push_err(PathResolutionError::UnresolvedWithPossibleTraitsToImport {
ident: Ident::new(method_name.into(), span),
traits,
});
return None;
}
}

if trait_methods_in_scope.len() > 1 {
let traits = vecmap(trait_methods, |(_, trait_id)| {
let trait_ = self.interner.get_trait(trait_id);
self.fully_qualified_trait_path(trait_)
});
self.push_err(PathResolutionError::MultipleTraitsInScope {
ident: Ident::new(method_name.into(), span),
traits,
});
return None;
}

let func_id = trait_methods_in_scope[0].0;
Some(HirMethodReference::FuncId(func_id))
}

fn lookup_method_in_trait_constraints(
&mut self,
object_type: &Type,
Expand Down Expand Up @@ -1823,6 +1920,10 @@ impl<'context> Elaborator<'context> {
..*parent_trait_bound
}
}

pub(crate) fn fully_qualified_trait_path(&self, trait_: &Trait) -> String {
fully_qualified_module_path(self.def_maps, self.crate_graph, &trait_.crate_id, trait_.id.0)
}
}

pub(crate) fn bind_ordered_generics(
Expand Down
15 changes: 4 additions & 11 deletions compiler/noirc_frontend/src/hir/comptime/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1379,17 +1379,10 @@ impl<'local, 'interner> Interpreter<'local, 'interner> {
let typ = object.get_type().follow_bindings();
let method_name = &call.method.0.contents;

// TODO: Traits
let method = match &typ {
Type::Struct(struct_def, _) => self.elaborator.interner.lookup_method(
&typ,
struct_def.borrow().id,
method_name,
false,
true,
),
_ => self.elaborator.interner.lookup_primitive_method(&typ, method_name, true),
};
let method = self
.elaborator
.lookup_method(&typ, method_name, location.span, true)
.and_then(|method| method.func_id(self.elaborator.interner));

if let Some(method) = method {
self.call_function(method, arguments, TypeBindings::new(), location)
Expand Down
Loading
Loading