Skip to content

Commit

Permalink
Implement order-independent module evaluation (#5300)
Browse files Browse the repository at this point in the history
## Description

This PR implements a new module evaluation system to allow
order-independent mod statements when importing modules.

It supports these sort of mod patterns to compile:

`a.sw`:
```rust
library;

use ::b::*;

pub fn a() { b::b(); }

```
`b.sw`:
```rust
library;

pub fn b() {}

```
`lib.sw`:
```rust
library;

mod a;
mod b;

fn main() -> u32 {
  b::b();
  0
}
```

To make this work, we now analyze the submodule structure first,
creating a dependency graph that we later use to inform the module
evaluation order.

We also now check for cyclic dependencies using the same graph, and
report them when they are found.

Closes #409.

## Checklist

- [x] I have linked to any relevant issues.
- [x] I have commented my code, particularly in hard-to-understand
areas.
- [x] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [x] I have added tests that prove my fix is effective or that my
feature works.
- [x] I have added (or requested a maintainer to add) the necessary
`Breaking*` or `New Feature` labels where relevant.
- [x] I have done my best to ensure that my PR adheres to [the Fuel Labs
Code Review
Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md).
- [x] I have requested a review from the relevant team or maintainers.
  • Loading branch information
tritao authored Dec 12, 2023
1 parent 88e92f5 commit 9e27e8f
Show file tree
Hide file tree
Showing 27 changed files with 386 additions and 6 deletions.
251 changes: 246 additions & 5 deletions sway-core/src/semantic_analysis/module.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,215 @@
use sway_error::handler::{ErrorEmitted, Handler};
use std::{collections::HashMap, fmt::Display, fs};

use graph_cycles::Cycles;
use sway_error::{
error::CompileError,
handler::{ErrorEmitted, Handler},
};

use crate::{
engine_threading::DebugWithEngines,
language::{parsed::*, ty, ModName},
semantic_analysis::*,
Engines,
};

#[derive(Clone, Debug)]
pub struct ModuleDepGraphEdge();

impl Display for ModuleDepGraphEdge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "")
}
}

pub type ModuleDepGraphNodeId = petgraph::graph::NodeIndex;

#[derive(Clone, Debug)]
pub enum ModuleDepGraphNode {
Module {},
Submodule { name: ModName },
}

impl DebugWithEngines for ModuleDepGraphNode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>, _engines: &Engines) -> std::fmt::Result {
let text = match self {
ModuleDepGraphNode::Module { .. } => {
format!("{:?}", "Root module")
}
ModuleDepGraphNode::Submodule { name: mod_name } => {
format!("{:?}", mod_name.as_str())
}
};
f.write_str(&text)
}
}

// Represents an ordered graph between declaration id indexes.
pub type ModuleDepNodeGraph = petgraph::graph::DiGraph<ModuleDepGraphNode, ModuleDepGraphEdge>;

pub struct ModuleDepGraph {
dep_graph: ModuleDepNodeGraph,
root: ModuleDepGraphNodeId,
node_name_map: HashMap<String, ModuleDepGraphNodeId>,
}

pub type ModuleEvaluationOrder = Vec<ModName>;

impl ModuleDepGraph {
pub(crate) fn new() -> Self {
Self {
dep_graph: Default::default(),
root: Default::default(),
node_name_map: Default::default(),
}
}

pub fn add_node(&mut self, node: ModuleDepGraphNode) -> ModuleDepGraphNodeId {
let node_id = self.dep_graph.add_node(node.clone());
match node {
ModuleDepGraphNode::Module {} => {}
ModuleDepGraphNode::Submodule { name: mod_name } => {
self.node_name_map.insert(mod_name.to_string(), node_id);
}
};
node_id
}

pub fn add_root_node(&mut self) -> ModuleDepGraphNodeId {
self.root = self.add_node(super::module::ModuleDepGraphNode::Module {});
self.root
}

fn get_node_id_for_module(
&self,
mod_name: &sway_types::BaseIdent,
) -> Option<ModuleDepGraphNodeId> {
self.node_name_map.get(&mod_name.to_string()).copied()
}

/// Prints out GraphViz DOT format for the dependency graph.
#[allow(dead_code)]
pub(crate) fn visualize(&self, engines: &Engines, print_graph: Option<String>) {
if let Some(graph_path) = print_graph {
use petgraph::dot::{Config, Dot};
let string_graph = self.dep_graph.filter_map(
|_idx, node| Some(format!("{:?}", engines.help_out(node))),
|_idx, edge| Some(format!("{}", edge)),
);

let output = format!(
"{:?}",
Dot::with_attr_getters(
&string_graph,
&[Config::NodeNoLabel, Config::EdgeNoLabel],
&|_, er| format!("label = {:?}", er.weight()),
&|_, nr| {
let _node = &self.dep_graph[nr.0];
let shape = "";
let url = "".to_string();
format!("{shape} label = {:?} {url}", nr.1)
},
)
);

if graph_path.is_empty() {
tracing::info!("{output}");
} else {
let result = fs::write(graph_path.clone(), output);
if let Some(error) = result.err() {
tracing::error!(
"There was an issue while outputing module dep analysis graph to path {graph_path:?}\n{error}"
);
}
}
}
}

/// Computes the ordered list by dependency, which will be used for evaluating the modules
/// in the correct order. We run a topological sort and cycle finding algorithm to check
/// for unsupported cyclic dependency cases.
pub(crate) fn compute_order(
&self,
handler: &Handler,
) -> Result<ModuleEvaluationOrder, ErrorEmitted> {
// Check for dependency cycles in the graph by running the Johnson's algorithm.
let cycles = self.dep_graph.cycles();
if !cycles.is_empty() {
let mut modules = Vec::new();
for cycle in cycles.first().unwrap() {
let node = self.dep_graph.node_weight(*cycle).unwrap();
match node {
ModuleDepGraphNode::Module {} => unreachable!(),
ModuleDepGraphNode::Submodule { name } => modules.push(name.clone()),
};
}
return Err(handler.emit_err(CompileError::ModuleDepGraphCyclicReference { modules }));
}

// Do a topological sort to compute an ordered list of nodes.
let sorted = match petgraph::algo::toposort(&self.dep_graph, None) {
Ok(value) => value,
// If we were not able to toposort, this means there is likely a cycle in the module dependency graph,
// which we already handled above, so lets just return an empty evaluation order instead of panic'ing.
// module dependencies, which we have already reported.
Err(_) => return Err(handler.emit_err(CompileError::ModuleDepGraphEvaluationError {})),
};

let sorted = sorted
.into_iter()
.filter_map(|node_index| {
let node = self.dep_graph.node_weight(node_index);
match node {
Some(node) => match node {
ModuleDepGraphNode::Module {} => None, // root module
ModuleDepGraphNode::Submodule { name: mod_name } => Some(mod_name.clone()),
},
None => None,
}
})
.rev()
.collect::<Vec<_>>();

Ok(sorted)
}
}

impl ty::TyModule {
/// Analyzes the given parsed module to produce a dependency graph.
pub fn analyze(
handler: &Handler,
parsed: &ParseModule,
) -> Result<ModuleDepGraph, ErrorEmitted> {
let mut dep_graph = ModuleDepGraph::new();
dep_graph.add_root_node();

let ParseModule { submodules, .. } = parsed;

// Create graph nodes for each submodule.
submodules.iter().for_each(|(name, _submodule)| {
let sub_mod_node =
dep_graph.add_node(ModuleDepGraphNode::Submodule { name: name.clone() });
dep_graph
.dep_graph
.add_edge(dep_graph.root, sub_mod_node, ModuleDepGraphEdge {});
});

// Analyze submodules first in order of declaration.
submodules.iter().for_each(|(name, submodule)| {
let _ = ty::TySubmodule::analyze(handler, &mut dep_graph, name.clone(), submodule);
});

Ok(dep_graph)
}

/// Type-check the given parsed module to produce a typed module.
///
/// Recursively type-checks submodules first.
pub fn type_check(
handler: &Handler,
mut ctx: TypeCheckContext,
parsed: &ParseModule,
module_eval_order: ModuleEvaluationOrder,
) -> Result<Self, ErrorEmitted> {
let ParseModule {
submodules,
Expand All @@ -23,10 +220,14 @@ impl ty::TyModule {
..
} = parsed;

// Type-check submodules first in order of declaration.
let submodules_res = submodules
// Type-check submodules first in order of evaluation previously computed by the dependency graph.
let submodules_res = module_eval_order
.iter()
.map(|(name, submodule)| {
.map(|eval_mod_name| {
let (name, submodule) = submodules
.iter()
.find(|(submod_name, _submodule)| eval_mod_name == submod_name)
.unwrap();
Ok((
name.clone(),
ty::TySubmodule::type_check(handler, ctx.by_ref(), name.clone(), submodule)?,
Expand Down Expand Up @@ -70,6 +271,43 @@ impl ty::TyModule {
}

impl ty::TySubmodule {
pub fn analyze(
_handler: &Handler,
module_dep_graph: &mut ModuleDepGraph,
mod_name: ModName,
submodule: &ParseSubmodule,
) -> Result<(), ErrorEmitted> {
let ParseSubmodule { module, .. } = submodule;
let sub_mod_node = module_dep_graph.get_node_id_for_module(&mod_name).unwrap();
for node in module.tree.root_nodes.iter() {
match &node.content {
AstNodeContent::UseStatement(use_stmt) => {
if let Some(use_mod_ident) = use_stmt.call_path.first() {
if let Some(mod_name_node) =
module_dep_graph.get_node_id_for_module(use_mod_ident)
{
// Prevent adding edge loops between the same node as that will throw off
// the cyclic dependency analysis.
if sub_mod_node != mod_name_node {
module_dep_graph.dep_graph.add_edge(
sub_mod_node,
mod_name_node,
ModuleDepGraphEdge {},
);
}
}
}
}
AstNodeContent::Declaration(_) => {}
AstNodeContent::Expression(_) => {}
AstNodeContent::ImplicitReturnExpression(_) => {}
AstNodeContent::IncludeStatement(_) => {}
AstNodeContent::Error(_, _) => {}
}
}
Ok(())
}

pub fn type_check(
handler: &Handler,
parent_ctx: TypeCheckContext,
Expand All @@ -81,8 +319,11 @@ impl ty::TySubmodule {
mod_name_span,
visibility,
} = submodule;
let modules_dep_graph = ty::TyModule::analyze(handler, module)?;
let module_eval_order = modules_dep_graph.compute_order(handler)?;
parent_ctx.enter_submodule(mod_name, *visibility, module.span.clone(), |submod_ctx| {
let module_res = ty::TyModule::type_check(handler, submod_ctx, module);
let module_res =
ty::TyModule::type_check(handler, submod_ctx, module, module_eval_order);
module_res.map(|module| ty::TySubmodule {
module,
mod_name_span: mod_name_span.clone(),
Expand Down
7 changes: 6 additions & 1 deletion sway-core/src/semantic_analysis/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ impl TyProgram {
let ctx =
TypeCheckContext::from_root(&mut namespace, engines).with_kind(parsed.kind.clone());
let ParseProgram { root, kind } = parsed;
ty::TyModule::type_check(handler, ctx, root).and_then(|root| {

// Analyze the dependency order for the submodules.
let modules_dep_graph = ty::TyModule::analyze(handler, root)?;
let module_eval_order = modules_dep_graph.compute_order(handler)?;

ty::TyModule::type_check(handler, ctx, root, module_eval_order).and_then(|root| {
let res = Self::validate_root(handler, engines, &root, kind.clone(), package_name);
res.map(|(kind, declarations, configurables)| Self {
kind,
Expand Down
12 changes: 12 additions & 0 deletions sway-error/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ impl fmt::Display for InterfaceName {
// this type.
#[derive(Error, Debug, Clone, PartialEq, Eq, Hash)]
pub enum CompileError {
#[error(
"There was an error while evaluating the evaluation order for the module dependency graph."
)]
ModuleDepGraphEvaluationError {},
#[error("A cyclic reference was found between the modules: {}.",
modules.iter().map(|ident| ident.as_str().to_string())
.collect::<Vec<_>>()
.join(", "))]
ModuleDepGraphCyclicReference { modules: Vec<BaseIdent> },

#[error("Variable \"{var_name}\" does not exist in this scope.")]
UnknownVariable { var_name: Ident, span: Span },
#[error("Identifier \"{name}\" was used as a variable, but it is actually a {what_it_is}.")]
Expand Down Expand Up @@ -756,6 +766,8 @@ impl Spanned for CompileError {
fn span(&self) -> Span {
use CompileError::*;
match self {
ModuleDepGraphEvaluationError { .. } => Span::dummy(),
ModuleDepGraphCyclicReference { .. } => Span::dummy(),
UnknownVariable { span, .. } => span.clone(),
NotAVariable { span, .. } => span.clone(),
Unimplemented(_, span) => span.clone(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[[package]]
name = "module_cyclic_reference"
source = "member"
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
entry = "lib.sw"
license = "Apache-2.0"
name = "module_cyclic_reference"
implicit-std = false
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
library;

use ::b::*;

pub fn a() { b(); }
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
library;

use ::a::*;

pub fn b() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
library;

mod a;
mod b;

fn main() -> u32 {
a::a();
0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
category = "fail"

# check: $()A cyclic reference was found between the modules: a, b.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[[package]]
name = "module_dep"
source = "member"
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
entry = "lib.sw"
license = "Apache-2.0"
name = "module_dep"
implicit-std = false
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
library;

use ::b::*;

pub fn a() { b(); }
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
library;

pub fn b() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
library;

mod a;
mod b;

fn main() -> u32 {
1
}
Loading

0 comments on commit 9e27e8f

Please sign in to comment.