use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::sync::Arc;
use crate::driver::{DependencyGraph, MAIN_MODULE, MAIN_STR};
use crate::error::{Error, ErrorCollector, RichError, Span};
use crate::impl_eq_hash;
use crate::parse::{self, AliasedSymbolName, Function, TypeAlias, Visibility};
use crate::str::{AliasName, FunctionName, SymbolName};
#[derive(Clone, Debug)]
pub struct Program {
items: Arc<[parse::Item]>,
aliases: SymbolTable<AliasName>,
functions: SymbolTable<FunctionName>,
span: Span,
}
impl Program {
pub fn from_parse(
parsed: &parse::Program,
content: Arc<str>,
handler: &mut ErrorCollector,
) -> Option<Self> {
let module_count = 1;
let mut items: Vec<parse::Item> = Vec::new();
let mut aliases = NamespaceTracker::<AliasName>::new(module_count);
let mut functions = NamespaceTracker::<FunctionName>::new(module_count);
for item in parsed.items() {
if let parse::Item::Use(use_decl) = item {
handler.push(
RichError::new(Error::UnknownLibrary(use_decl.str_path()), *use_decl.span())
.with_content(content.clone()),
);
continue;
}
let mut new_elem = item.clone();
match &mut new_elem {
parse::Item::TypeAlias(type_alias) => {
if let Err(err) = register_type_alias(type_alias, &mut aliases, MAIN_MODULE) {
handler.push(err.with_content(content.clone()));
continue;
}
}
parse::Item::Function(function) => {
if let Err(err) = register_function(function, &mut functions, MAIN_MODULE) {
handler.push(err.with_content(content.clone()));
continue;
}
}
parse::Item::Module | parse::Item::Use(_) => continue,
}
items.push(new_elem);
}
(!handler.has_errors()).then(|| Program {
items: items.into(),
aliases: aliases.into_symbol_table(),
functions: functions.into_symbol_table(),
span: *parsed.as_ref(),
})
}
pub fn items(&self) -> &[parse::Item] {
&self.items
}
pub fn aliases(&self) -> &SymbolTable<AliasName> {
&self.aliases
}
pub fn functions(&self) -> &SymbolTable<FunctionName> {
&self.functions
}
pub fn span(&self) -> &Span {
&self.span
}
}
impl_eq_hash!(Program; items, aliases, functions);
#[derive(Clone, Debug)]
pub struct SymbolTable<T> {
local_scopes: Arc<[BTreeSet<T>]>,
imports: ImportRegistry<T>,
}
impl<T> SymbolTable<T> {
pub fn local_scopes(&self) -> &[BTreeSet<T>] {
&self.local_scopes
}
pub fn imports(&self) -> &ImportRegistry<T> {
&self.imports
}
}
impl_eq_hash!(SymbolTable<T>; local_scopes, imports);
pub type FileScoped<T> = (T, usize);
pub type ImportMap<T> = BTreeMap<FileScoped<T>, FileScoped<T>>;
#[derive(Clone, Debug)]
pub struct ImportRegistry<T> {
direct_targets: ImportMap<T>,
resolved_roots: ImportMap<T>,
}
impl<T> ImportRegistry<T> {
pub fn direct_targets(&self) -> &ImportMap<T> {
&self.direct_targets
}
pub fn resolved_roots(&self) -> &ImportMap<T> {
&self.resolved_roots
}
}
impl_eq_hash!(ImportRegistry<T>; direct_targets, resolved_roots);
impl DependencyGraph {
pub fn linearize_and_build(
&self,
handler: &mut ErrorCollector,
) -> Result<Option<Program>, String> {
match self.linearize() {
Ok(order) => Ok(self.build_program(&order, handler)),
Err(err) => Err(err.to_string()),
}
}
fn build_program(&self, order: &[usize], handler: &mut ErrorCollector) -> Option<Program> {
let mut items: Vec<parse::Item> = Vec::new();
let mut aliases = NamespaceTracker::<AliasName>::new(self.modules.len());
let mut functions = NamespaceTracker::<FunctionName>::new(self.modules.len());
for &source_id in order {
let module = &self.modules[source_id];
let source = &module.source;
for elem in module.parsed_program.items() {
if let parse::Item::Use(use_decl) = elem {
let resolve_path =
match self.dependency_map.resolve_path(source.name(), use_decl) {
Ok(path) => path,
Err(err) => {
handler.push(err.with_source(source.clone()));
continue;
}
};
let ind = self.lookup[&resolve_path];
let use_decl_items = match use_decl.items() {
parse::UseItems::Single(elem) => std::slice::from_ref(elem),
parse::UseItems::List(elems) => elems.as_slice(),
};
for aliased_item in use_decl_items {
let alias_err = Self::process_use_item(
&mut aliases,
source_id,
ind,
aliased_item,
use_decl,
);
let function_err = Self::process_use_item(
&mut functions,
source_id,
ind,
aliased_item,
use_decl,
);
if let Err(err) =
Self::resolve_processing_use_items_error(alias_err, function_err)
{
handler.push(err.with_source(source.clone()));
}
}
continue;
}
let mut new_elem = elem.clone();
match &mut new_elem {
parse::Item::TypeAlias(type_alias) => {
if let Err(err) = register_type_alias(type_alias, &mut aliases, source_id) {
handler.push(err.with_source(source.clone()));
continue;
}
}
parse::Item::Function(function) => {
if let Err(err) = register_function(function, &mut functions, source_id) {
handler.push(err.with_source(source.clone()));
continue;
}
}
parse::Item::Module | parse::Item::Use(_) => continue,
}
items.push(new_elem);
}
}
(!handler.has_errors()).then(|| Program {
items: items.into(),
aliases: aliases.into_symbol_table(),
functions: functions.into_symbol_table(),
span: *self.modules[0].parsed_program.as_ref(),
})
}
fn resolve_processing_use_items_error(
alias: Result<(), RichError>,
function: Result<(), RichError>,
) -> Result<(), RichError> {
match (alias, function) {
(Ok(()), _) | (_, Ok(())) => Ok(()),
(Err(err_alias), Err(err_func)) => {
let alias_is_missing = matches!(err_alias.error(), Error::UnresolvedItem(_));
let func_is_missing = matches!(err_func.error(), Error::UnresolvedItem(_));
if !alias_is_missing || func_is_missing {
Err(err_alias)
} else {
Err(err_func)
}
}
}
}
fn process_use_item<T>(
namespace: &mut NamespaceTracker<T>,
source_id: usize,
ind: usize,
(name, alias): &AliasedSymbolName,
use_decl: &parse::UseDecl,
) -> Result<(), RichError>
where
T: From<SymbolName> + std::fmt::Display + Clone + Eq + std::hash::Hash + std::cmp::Ord,
{
let span = *use_decl.span();
let target_name: T = name.clone().into();
let orig_id = (target_name.clone(), ind);
let visibility: &Visibility = namespace.resolutions[ind]
.get(&target_name)
.ok_or_else(|| RichError::new(Error::UnresolvedItem(name.to_string()), span))?;
if matches!(visibility, parse::Visibility::Private) {
return Err(RichError::new(Error::PrivateItem(name.to_string()), span));
}
let local_symbol = alias.as_ref().unwrap_or(name);
let local_name: T = if let Some(alias_sym) = alias {
let t_alias: T = alias_sym.clone().into();
if t_alias.to_string() == MAIN_STR {
return Err(RichError::new(Error::MainCannotBeAlias, span));
}
t_alias
} else {
name.clone().into()
};
let local_id = (local_name.clone(), source_id);
if namespace.registry.direct_targets.contains_key(&local_id) {
return Err(RichError::new(
Error::DuplicateAlias(local_symbol.to_string()),
span,
));
}
if namespace.memo.contains(&local_id) {
return Err(RichError::new(
Error::RedefinedItem(local_symbol.to_string()),
span,
));
}
namespace.memo.insert(local_id.clone());
namespace
.registry
.direct_targets
.insert(local_id.clone(), orig_id.clone());
let true_root = namespace
.registry
.resolved_roots
.get(&orig_id)
.cloned()
.unwrap_or_else(|| orig_id.clone());
namespace
.registry
.resolved_roots
.insert(local_id, true_root);
namespace.resolutions[source_id].insert(local_name, use_decl.visibility().clone());
Ok(())
}
}
fn register_type_alias(
item: &mut TypeAlias,
tracker: &mut NamespaceTracker<AliasName>,
source_id: usize,
) -> Result<(), RichError> {
item.set_file_id(source_id);
let name = item.name();
let local_id = (name.clone(), source_id);
if tracker.memo.contains(&local_id) {
return Err(RichError::new(
Error::RedefinedAlias(name.clone()),
*item.span(),
));
}
tracker.memo.insert(local_id);
tracker.resolutions[source_id].insert(name.clone(), item.visibility().clone());
Ok(())
}
fn register_function(
item: &mut Function,
tracker: &mut NamespaceTracker<FunctionName>,
source_id: usize,
) -> Result<(), RichError> {
item.set_file_id(source_id);
let name = item.name();
let local_id = (name.clone(), source_id);
if name.as_inner() == MAIN_STR && matches!(item.visibility(), Visibility::Public) {
return Err(RichError::new(Error::MainCannotBePublic, *item.span()));
}
if tracker.memo.contains(&local_id) {
return Err(RichError::new(
Error::FunctionRedefined(name.clone()),
*item.span(),
));
}
tracker.memo.insert(local_id);
tracker.resolutions[source_id].insert(name.clone(), item.visibility().clone());
Ok(())
}
#[derive(Clone, Debug)]
struct NamespaceTracker<T> {
resolutions: Vec<HashMap<T, Visibility>>,
registry: ImportRegistry<T>,
memo: BTreeSet<FileScoped<T>>,
}
impl<T: Ord + Clone + Default + std::hash::Hash> NamespaceTracker<T> {
pub fn new(module_count: usize) -> Self {
Self {
resolutions: vec![HashMap::new(); module_count],
registry: ImportRegistry::<T>::default(),
memo: BTreeSet::new(),
}
}
pub fn into_symbol_table(self) -> SymbolTable<T> {
SymbolTable {
local_scopes: self
.resolutions
.into_iter()
.map(|map| map.into_keys().collect::<BTreeSet<_>>())
.collect::<Vec<_>>()
.into(),
imports: self.registry,
}
}
}
impl<T> Default for ImportRegistry<T> {
fn default() -> Self {
Self {
direct_targets: BTreeMap::new(),
resolved_roots: BTreeMap::new(),
}
}
}
impl<T> Default for SymbolTable<T> {
fn default() -> Self {
Self {
local_scopes: Arc::from([]),
imports: ImportRegistry::<T>::default(),
}
}
}
impl AsRef<Span> for Program {
fn as_ref(&self) -> &Span {
&self.span
}
}
#[cfg(test)]
mod resolve_order_tests {
use crate::driver::tests::setup_graph;
use super::*;
#[test]
fn test_local_definitions_visibility() {
let (graph, ids, _dir) = setup_graph(vec![(
"main.simf",
"fn private_fn() {} pub fn public_fn() {}",
)]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
let Some(program) = program_option else {
panic!("{}", error_handler);
};
let root_id = ids["main"];
let resolutions = &program.functions.local_scopes[root_id];
resolutions
.get(&FunctionName::from_str_unchecked("private_fn"))
.expect("private_fn missing");
resolutions
.get(&FunctionName::from_str_unchecked("public_fn"))
.expect("public_fn missing");
}
#[test]
fn test_pub_use_propagation() {
let (graph, ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub fn foo() {}"),
("libs/lib/B.simf", "pub use lib::A::foo;"),
("main.simf", "use lib::B::foo;"),
]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
let Some(program) = program_option else {
panic!("{}", error_handler);
};
let id_b = ids["B"];
let id_root = ids["main"];
program.functions.local_scopes[id_b]
.get(&FunctionName::from_str_unchecked("foo"))
.expect("foo missing in B");
program.functions.local_scopes[id_root]
.get(&FunctionName::from_str_unchecked("foo"))
.expect("foo missing in Root");
}
#[test]
fn test_private_import_encapsulation_error() {
let (graph, _ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub fn foo() {}"),
("libs/lib/B.simf", "use lib::A::foo;"), ("main.simf", "use lib::B::foo;"), ]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
assert!(
program_option.is_none(),
"Build should fail and return None when importing a private binding"
);
assert!(error_handler
.to_string()
.contains(&"Item `foo` is private".to_string()));
}
#[test]
fn test_separated_type_aliases_and_functions() {
let (graph, ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub type bar = u32; pub fn bar() {}"),
("main.simf", "use lib::A::bar;"),
]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
let Some(program) = program_option else {
panic!("{}", error_handler);
};
let root_id = ids["main"];
program.functions.local_scopes[root_id]
.get(&FunctionName::from_str_unchecked("bar"))
.expect("Function bar missing in main");
program.aliases.local_scopes[root_id]
.get(&AliasName::from_str_unchecked("bar"))
.expect("Type alias missing in main");
}
#[test]
fn test_private_alias_error_does_not_mask_duplicate_function_import() {
let (graph, _ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub fn foo() {}"),
("libs/lib/B.simf", "pub fn foo() {} type foo = u32;"),
("main.simf", "use lib::A::foo; use lib::B::foo;"),
]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
let _errors = error_handler.to_string();
assert!(
program_option.is_none(),
"build should fail when a second import reuses the function name `foo`"
);
}
#[test]
fn test_public_main_is_forbidden() {
let (graph, _ids, _dir) = setup_graph(vec![("main.simf", "pub fn main() {}")]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
assert!(
program_option.is_none(),
"Compiler should return None when `main` is declared public"
);
let error_msg = error_handler.to_string();
assert!(
error_msg.contains("main") && error_msg.contains("public"),
"Error message should mention that `main` cannot be public. Got: {}",
error_msg
);
}
#[test]
fn test_aliasing_to_main_is_forbidden() {
let (graph, _ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub type bar = u32;"),
("main.simf", "use lib::A::bar as main;"),
]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
assert!(
program_option.is_none(),
"Compiler should return None when a user tries to alias an import to `main`"
);
let error_msg = error_handler.to_string();
assert!(
error_msg.contains("main") && error_msg.contains("alias"),
"Error message should clearly state that `main` cannot be used as an alias. Got: {}",
error_msg
);
}
}
#[cfg(test)]
mod alias_tests {
use super::*;
use crate::driver::tests::setup_graph;
#[test]
fn test_renaming_with_use() {
let (graph, ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub fn foo() {}"),
("main.simf", "use lib::A::foo as bar;"),
]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
let Some(program) = program_option else {
panic!("{}", error_handler);
};
let id_root = ids["main"];
let scope = &program.functions.local_scopes[id_root];
assert!(
scope
.get(&FunctionName::from_str_unchecked("foo"))
.is_none(),
"Original name 'foo' should not be in scope"
);
assert!(
scope
.get(&FunctionName::from_str_unchecked("bar"))
.is_some(),
"Alias 'bar' should be in scope"
);
}
#[test]
fn test_multiple_aliases_in_list() {
let (graph, ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub fn foo() {} pub fn baz() {}"),
("main.simf", "use lib::A::{foo as bar, baz as qux};"),
]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
let Some(program) = program_option else {
panic!("{}", error_handler);
};
let id_root = ids["main"];
let scope = &program.functions.local_scopes[id_root];
assert!(scope
.get(&FunctionName::from_str_unchecked("foo"))
.is_none());
assert!(scope
.get(&FunctionName::from_str_unchecked("baz"))
.is_none());
assert!(scope
.get(&FunctionName::from_str_unchecked("bar"))
.is_some());
assert!(scope
.get(&FunctionName::from_str_unchecked("qux"))
.is_some());
}
#[test]
fn test_alias_private_item_fails() {
let (graph, _ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "fn secret() {}"), ("main.simf", "use lib::A::secret as my_secret;"),
]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
assert!(
program_option.is_none(),
"Compiler should emit an error and return None when aliasing a private item"
);
assert!(
error_handler
.to_string()
.contains("Item `secret` is private"),
"Error should mention the private item restriction"
);
}
#[test]
fn test_deep_reexport_with_aliases() {
let (graph, ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub fn original() {}"),
("libs/lib/B.simf", "pub use lib::A::original as middle;"),
("main.simf", "use lib::B::middle as final_name;"),
]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
let Some(program) = program_option else {
panic!("{}", error_handler);
};
let id_b = ids["B"];
let id_root = ids["main"];
let main_scope = &program.functions.local_scopes[id_root];
assert!(main_scope
.get(&FunctionName::from_str_unchecked("original"))
.is_none());
assert!(main_scope
.get(&FunctionName::from_str_unchecked("middle"))
.is_none());
assert!(
main_scope
.get(&FunctionName::from_str_unchecked("final_name"))
.is_some(),
"Main must see the final alias"
);
let b_scope = &program.functions.local_scopes[id_b];
assert!(
b_scope
.get(&FunctionName::from_str_unchecked("middle"))
.is_some(),
"File B must contain its own public alias"
);
}
#[test]
fn test_deep_reexport_private_link_fails() {
let (graph, _ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub fn target() {}"),
("libs/lib/B.simf", "use lib::A::target as hidden_alias;"),
("main.simf", "use lib::B::hidden_alias;"),
]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
assert!(
program_option.is_none(),
"Compiler must return None when trying to import a private alias from an intermediate module"
);
assert!(
error_handler
.to_string()
.contains("Item `hidden_alias` is private"),
"Error should correctly identify the private intermediate alias"
);
}
#[test]
fn test_alias_cycle_detection() {
let (graph, _ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub use lib::B::pong as ping;"),
("libs/lib/B.simf", "pub use lib::A::ping as pong;"),
("main.simf", "use lib::A::ping;"),
]);
let mut error_handler = ErrorCollector::new();
let result = graph.linearize_and_build(&mut error_handler);
match result {
Err(e) => {
println!("{e}");
assert!(
e.contains("Cycle") || e.contains("Circular"),
"DFS Linearizer must catch infinite alias cycles"
);
}
Ok(None) => {
assert!(
error_handler.has_errors(),
"If linearization passes, the builder must catch the cycle"
);
}
Ok(Some(_)) => {
panic!("Expected compilation to fail due to a dependency cycle, but it succeeded!")
}
}
}
#[test]
fn test_plain_import_and_alias_to_same_name_is_rejected() {
let (graph, _ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub fn foo() {}"),
("libs/lib/B.simf", "pub fn foo() {}"),
("main.simf", "use lib::A::foo; use lib::B::foo as foo;"),
]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
assert!(
program_option.is_none(),
"build should fail when two imports bind the same local name"
);
assert!(
error_handler
.to_string()
.contains("The alias `foo` was defined multiple times"),
"expected a duplicate-alias diagnostic"
);
}
#[test]
fn test_failed_alias_import_does_not_poison_following_imports() {
let (graph, _ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub fn nope() {}"),
("libs/lib/B.simf", "pub fn bar() {}"),
(
"main.simf",
"use lib::A::missing as foo; use lib::B::bar as foo;",
),
]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
let errors = error_handler.to_string();
assert!(
program_option.is_none(),
"build should fail on the unresolved import"
);
assert!(errors.contains("Item `missing` could not be found"));
assert!(
!errors.contains("The alias `foo` was defined multiple times"),
"a failed import must not reserve the alias name"
);
}
#[test]
fn test_alias_cannot_reuse_local_definition_name() {
let (graph, _ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub fn bar() {}"),
("main.simf", "pub fn foo() {} use lib::A::bar as foo;"),
]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
dbg!(&error_handler.to_string());
assert!(
program_option.is_none(),
"build should fail when an alias reuses a local definition name"
);
assert!(
error_handler
.to_string()
.contains("Item `foo` was defined multiple times"),
"expected a redefined-item diagnostic"
);
}
#[test]
fn test_local_function_cannot_reuse_alias_name() {
let (graph, _ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub fn bar() {}"),
("main.simf", "use lib::A::bar as foo; pub fn foo() {}"),
]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
assert!(
program_option.is_none(),
"build should fail when a local definition reuses an alias name"
);
assert!(
error_handler
.to_string()
.contains("Function `foo` was defined multiple times"),
"expected a redefined-item diagnostic"
);
}
#[test]
fn test_local_type_alias_cannot_reuse_alias_name() {
let (graph, _ids, _dir) = setup_graph(vec![
("libs/lib/A.simf", "pub type bar = u32;"),
("main.simf", "use lib::A::bar as foo; type foo = u64;"),
]);
let mut error_handler = ErrorCollector::new();
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
assert!(
program_option.is_none(),
"build should fail when a local definition reuses an alias name"
);
assert!(
error_handler
.to_string()
.contains("Type alias `foo` was defined multiple times"),
"expected a redefined-item diagnostic"
);
}
}