Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions compiler/noirc_frontend/src/node_interner/dependency.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
use std::borrow::Cow;

use noirc_errors::Location;
use petgraph::{
algo::tarjan_scc,
graph::{DiGraph, NodeIndex as PetGraphIndex},
};

use crate::{
Type,
hir::{def_collector::dc_crate::CompilationError, resolution::errors::ResolverError},
node_interner::{FuncId, GlobalId, TraitId, TypeAliasId, TypeId},
};

use super::NodeInterner;

/// A dependency in the dependency graph may be a type or a definition.
/// Types can depend on definitions too. E.g. `Foo` depends on `COUNT` in:
///
/// ```struct
/// global COUNT = 3;
///
/// struct Foo {
/// array: [Field; COUNT],
/// }
/// ```
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum DependencyId {
Struct(TypeId),
Global(GlobalId),
Function(FuncId),
Alias(TypeAliasId),
Trait(TraitId),
Variable(Location),
}

impl NodeInterner {
/// Gets the dependency graph from the node interner.
pub fn dependency_graph(&self) -> &DiGraph<DependencyId, ()> {
&self.dependency_graph
}

/// Register that `dependent` depends on `dependency`.
/// This is usually because `dependent` refers to `dependency` in one of its struct fields.
pub fn add_type_dependency(&mut self, dependent: DependencyId, dependency: TypeId) {
self.add_dependency(dependent, DependencyId::Struct(dependency));
}

pub fn add_global_dependency(&mut self, dependent: DependencyId, dependency: GlobalId) {
self.add_dependency(dependent, DependencyId::Global(dependency));
}

pub fn add_function_dependency(&mut self, dependent: DependencyId, dependency: FuncId) {
self.add_dependency(dependent, DependencyId::Function(dependency));
}

pub fn add_type_alias_dependency(&mut self, dependent: DependencyId, dependency: TypeAliasId) {
self.add_dependency(dependent, DependencyId::Alias(dependency));
}

pub fn add_trait_dependency(&mut self, dependent: DependencyId, dependency: TraitId) {
self.add_dependency(dependent, DependencyId::Trait(dependency));
}

pub fn add_dependency(&mut self, dependent: DependencyId, dependency: DependencyId) {
let dependent_index = self.get_or_insert_dependency(dependent);
let dependency_index = self.get_or_insert_dependency(dependency);
self.dependency_graph.update_edge(dependent_index, dependency_index, ());
}

pub fn get_or_insert_dependency(&mut self, id: DependencyId) -> PetGraphIndex {
if let Some(index) = self.dependency_graph_indices.get(&id) {
return *index;
}

let index = self.dependency_graph.add_node(id);
self.dependency_graph_indices.insert(id, index);
index
}

pub(crate) fn check_for_dependency_cycles(&self) -> Vec<CompilationError> {
let strongly_connected_components = tarjan_scc(&self.dependency_graph);
let mut errors = Vec::new();

let mut push_error = |item: String, scc: &[_], i, location: Location| {
let cycle = self.get_cycle_error_string(scc, i);
let error = ResolverError::DependencyCycle { item, cycle, location };
errors.push(error.into());
};

for scc in strongly_connected_components {
if scc.len() > 1 {
// If a SCC contains a type, type alias, or global, it must be the only element in the SCC
for (i, index) in scc.iter().enumerate() {
match self.dependency_graph[*index] {
DependencyId::Struct(struct_id) => {
let struct_type = self.get_type(struct_id);
let struct_type = struct_type.borrow();
push_error(struct_type.name.to_string(), &scc, i, struct_type.location);
break;
}
DependencyId::Global(global_id) => {
let global = self.get_global(global_id);
let name = global.ident.to_string();
push_error(name, &scc, i, global.location);
break;
}
DependencyId::Alias(alias_id) => {
let alias = self.get_type_alias(alias_id);
// If type aliases form a cycle, we have to manually break the cycle
// here to prevent infinite recursion in the type checker.
alias.borrow_mut().typ = Type::Error;

// push_error will borrow the alias so we have to drop the mutable borrow
let alias = alias.borrow();
push_error(alias.name.to_string(), &scc, i, alias.location);
break;
}
DependencyId::Trait(trait_id) => {
let the_trait = self.get_trait(trait_id);
push_error(the_trait.name.to_string(), &scc, i, the_trait.location);
break;
}
// Mutually recursive functions are allowed
DependencyId::Function(_) => (),
// Local variables should never be in a dependency cycle, scoping rules
// prevents referring to them before they're defined
DependencyId::Variable(loc) => unreachable!(
"Variable used at location {loc:?} caught in a dependency cycle"
),
}
}
}
}

errors
}

/// Build up a string starting from the given item containing each item in the dependency
/// cycle. The final result will resemble `foo -> bar -> baz -> foo`, always going back to the
/// element at the given start index.
fn get_cycle_error_string(&self, scc: &[PetGraphIndex], start_index: usize) -> String {
let index_to_string = |index: PetGraphIndex| match self.dependency_graph[index] {
DependencyId::Struct(id) => Cow::Owned(self.get_type(id).borrow().name.to_string()),
DependencyId::Function(id) => Cow::Borrowed(self.function_name(&id)),
DependencyId::Alias(id) => {
Cow::Owned(self.get_type_alias(id).borrow().name.to_string())
}
DependencyId::Global(id) => Cow::Borrowed(self.get_global(id).ident.as_str()),
DependencyId::Trait(id) => Cow::Owned(self.get_trait(id).name.to_string()),
DependencyId::Variable(loc) => {
unreachable!("Variable used at location {loc:?} caught in a dependency cycle")
}
};

let mut cycle = index_to_string(scc[start_index]).to_string();

// Reversing the dependencies here matches the order users would expect for the error message
for i in (0..scc.len()).rev() {
cycle += " -> ";
cycle += &index_to_string(scc[(start_index + i) % scc.len()]);
}

cycle
}
}
185 changes: 185 additions & 0 deletions compiler/noirc_frontend/src/node_interner/function.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
use noirc_errors::Location;

use crate::{
Type,
ast::{FunctionDefinition, ItemVisibility},
hir::def_map::ModuleId,
hir_def::{
expr::{HirExpression, HirIdent},
function::{FuncMeta, HirFunction},
},
node_interner::{
DefinitionId, DefinitionKind, ExprId, FuncId, FunctionModifiers, Node, ReferenceId, TraitId,
},
token::Attributes,
};

use super::NodeInterner;

impl NodeInterner {
/// Intern an empty function.
pub fn push_empty_fn(&mut self) -> FuncId {
self.push_fn(HirFunction::empty())
}
/// Updates the underlying interned Function.
///
/// This method is used as we eagerly intern empty functions to
/// generate function identifiers and then we update at a later point in
/// time.
pub fn update_fn(&mut self, func_id: FuncId, hir_func: HirFunction) {
let def =
self.nodes.get_mut(func_id.0).expect("ice: all function ids should have definitions");

let func = match def {
Node::Function(func) => func,
_ => panic!("ice: all function ids should correspond to a function in the interner"),
};
*func = hir_func;
}

pub fn find_function(&self, function_name: &str) -> Option<FuncId> {
self.func_meta
.iter()
.find(|(func_id, _func_meta)| self.function_name(func_id) == function_name)
.map(|(func_id, _meta)| *func_id)
}

///Interns a function's metadata.
///
/// Note that the FuncId has been created already.
/// See ModCollector for it's usage.
pub fn push_fn_meta(&mut self, func_data: FuncMeta, func_id: FuncId) {
self.func_meta.insert(func_id, func_data);
}

/// Push a function with the default modifiers and [`ModuleId`] for testing
#[cfg(test)]
pub fn push_test_function_definition(&mut self, name: String) -> FuncId {
let id = self.push_fn(HirFunction::empty());
let mut modifiers = FunctionModifiers::new();
modifiers.name = name;
let module = ModuleId::dummy_id();
let location = Location::dummy();
self.push_function_definition(id, modifiers, module, location);
id
}

pub fn push_function(
&mut self,
id: FuncId,
function: &FunctionDefinition,
module: ModuleId,
location: Location,
) -> DefinitionId {
let name_location = Location::new(function.name.span(), location.file);
let modifiers = FunctionModifiers {
name: function.name.to_string(),
visibility: function.visibility,
attributes: function.attributes.clone(),
is_unconstrained: function.is_unconstrained,
generic_count: function.generics.len(),
is_comptime: function.is_comptime,
name_location,
};
let definition_id = self.push_function_definition(id, modifiers, module, location);
self.add_definition_location(ReferenceId::Function(id), name_location);
definition_id
}

pub fn push_function_definition(
&mut self,
func: FuncId,
modifiers: FunctionModifiers,
module: ModuleId,
location: Location,
) -> DefinitionId {
let name = modifiers.name.clone();
let comptime = modifiers.is_comptime;
self.function_modifiers.insert(func, modifiers);
self.function_modules.insert(func, module);
self.push_definition(name, false, comptime, DefinitionKind::Function(func), location)
}

pub fn set_function_trait(&mut self, func: FuncId, self_type: Type, trait_id: TraitId) {
self.func_id_to_trait.insert(func, (self_type, trait_id));
}

pub fn get_function_trait(&self, func: &FuncId) -> Option<(Type, TraitId)> {
self.func_id_to_trait.get(func).cloned()
}

/// Returns the visibility of the given function.
///
/// The underlying function_visibilities map is populated during def collection,
/// so this function can be called anytime afterward.
pub fn function_visibility(&self, func: FuncId) -> ItemVisibility {
self.function_modifiers[&func].visibility
}

/// Returns the module this function was defined within
pub fn function_module(&self, func: FuncId) -> ModuleId {
self.function_modules[&func]
}

/// Returns the [`FuncId`] corresponding to the function referred to by `expr_id`
pub fn lookup_function_from_expr(&self, expr: &ExprId) -> Option<FuncId> {
if let HirExpression::Ident(HirIdent { id, .. }, _) = self.expression(expr) {
match self.try_definition(id).map(|def| &def.kind) {
Some(DefinitionKind::Function(func_id)) => Some(*func_id),
Some(DefinitionKind::Local(Some(expr_id))) => {
self.lookup_function_from_expr(expr_id)
}
_ => None,
}
} else {
None
}
}

/// Returns the interned HIR function corresponding to `func_id`
//
// Cloning HIR structures is cheap, so we return owned structures
pub fn function(&self, func_id: &FuncId) -> HirFunction {
let def = self.nodes.get(func_id.0).expect("ice: all function ids should have definitions");

match def {
Node::Function(func) => func.clone(),
_ => panic!("ice: all function ids should correspond to a function in the interner"),
}
}

/// Returns the interned meta data corresponding to `func_id`
pub fn function_meta(&self, func_id: &FuncId) -> &FuncMeta {
self.func_meta.get(func_id).expect("ice: all function ids should have metadata")
}

pub fn function_meta_mut(&mut self, func_id: &FuncId) -> &mut FuncMeta {
self.func_meta.get_mut(func_id).expect("ice: all function ids should have metadata")
}

pub fn try_function_meta(&self, func_id: &FuncId) -> Option<&FuncMeta> {
self.func_meta.get(func_id)
}

pub fn function_ident(&self, func_id: &FuncId) -> crate::ast::Ident {
let name = self.function_name(func_id).to_owned();
let location = self.function_meta(func_id).name.location;
crate::ast::Ident::new(name, location)
}

pub fn function_name(&self, func_id: &FuncId) -> &str {
&self.function_modifiers[func_id].name
}

pub fn function_modifiers(&self, func_id: &FuncId) -> &FunctionModifiers {
&self.function_modifiers[func_id]
}

pub fn function_modifiers_mut(&mut self, func_id: &FuncId) -> &mut FunctionModifiers {
self.function_modifiers.get_mut(func_id).expect("func_id should always have modifiers")
}

pub fn function_attributes(&self, func_id: &FuncId) -> &Attributes {
&self.function_modifiers[func_id].attributes
}
}
Loading
Loading