use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub(super) use crate::ast::{Script, Stmt};
pub(super) use crate::diagnostics::Diagnostic;
pub(super) use crate::project::DependencyMap;
pub(super) use crate::stdlib::{self, Module};
pub(super) use crate::token::Span;
mod diag;
use diag::*;
#[cfg(test)]
mod tests;
pub struct Program {
pub files: Vec<ProgramFile>,
pub init_order: Vec<u32>,
}
pub struct ProgramFile {
pub file_id: u32,
pub is_entry: bool,
pub name: String,
pub path: String,
pub source: String,
pub script: Script,
pub stdlib_imports: Vec<(String, &'static Module)>,
pub user_imports: Vec<(String, u32)>,
}
pub fn load_program(entry_path: &str, entry_source: &str) -> Result<Program, Diagnostic> {
load_program_with_deps(entry_path, entry_source, DependencyMap::new())
}
pub fn load_program_with_deps(
entry_path: &str,
entry_source: &str,
deps: DependencyMap,
) -> Result<Program, Diagnostic> {
let entry_script = crate::parser::parse(entry_path, entry_source)?;
let mut loader = Loader {
modules: Vec::new(),
by_path: HashMap::new(),
init_order: Vec::new(),
active: Vec::new(),
next_id: 1,
entry_key: canonical_key(Path::new(entry_path)),
deps,
};
let (stdlib_imports, user_imports) =
loader.resolve_imports(entry_path, entry_source, &entry_script)?;
let entry = ProgramFile {
file_id: 0,
is_entry: true,
name: file_stem(entry_path),
path: entry_path.to_string(),
source: entry_source.to_string(),
script: entry_script,
stdlib_imports,
user_imports,
};
let init_order = loader.modules.iter().map(|m| m.file_id).collect();
let mut slots: Vec<Option<ProgramFile>> = (0..=loader.modules.len()).map(|_| None).collect();
slots[0] = Some(entry);
for module in loader.modules {
let id = module.file_id as usize;
slots[id] = Some(module);
}
let files = slots
.into_iter()
.map(|s| s.expect("compiler bug: unfilled program file slot"))
.collect();
Ok(Program { files, init_order })
}
pub fn single_file_program(
path: &str,
source: &str,
script: Script,
) -> Result<Program, Diagnostic> {
let mut stdlib_imports = Vec::new();
for stmt in &script.stmts {
if let Stmt::Import {
module,
path: import_path,
span,
} = stmt
{
match (import_path, stdlib::module(module)) {
(None, Some(m)) => stdlib_imports.push((module.clone(), m)),
_ => return Err(unknown_stdlib_module(path, source, module, *span)),
}
}
}
let entry = ProgramFile {
file_id: 0,
is_entry: true,
name: file_stem(path),
path: path.to_string(),
source: source.to_string(),
script,
stdlib_imports,
user_imports: Vec::new(),
};
Ok(Program {
files: vec![entry],
init_order: Vec::new(),
})
}
type ResolvedImports = (Vec<(String, &'static Module)>, Vec<(String, u32)>);
struct Loader {
modules: Vec<ProgramFile>,
by_path: HashMap<PathBuf, u32>,
init_order: Vec<u32>,
active: Vec<ActiveModule>,
next_id: u32,
entry_key: PathBuf,
deps: DependencyMap,
}
struct ActiveModule {
name: String,
key: PathBuf,
}
impl Loader {
fn resolve_imports(
&mut self,
importer_path: &str,
importer_source: &str,
script: &Script,
) -> Result<ResolvedImports, Diagnostic> {
let dir = Path::new(importer_path).parent();
let mut stdlib_imports = Vec::new();
let mut user_imports = Vec::new();
for stmt in &script.stmts {
let Stmt::Import { module, path, span } = stmt else {
continue;
};
match path {
None => {
if let Some(entry) = stdlib::module(module) {
if module_file_exists(dir, module) {
return Err(shadow_diag(importer_path, importer_source, module, *span));
}
stdlib_imports.push((module.clone(), entry));
continue;
}
}
Some(_) if stdlib::module(module).is_some() => {
return Err(shadow_diag(importer_path, importer_source, module, *span));
}
Some(_) => {}
}
let target = match path {
Some(raw) => path_import_path(dir, raw),
None => match self.dep_entry(importer_path, module) {
Some(entry) => {
if module_file_exists(dir, module) {
return Err(dep_conflict_diag(
importer_path,
importer_source,
module,
*span,
));
}
entry
}
None => module_path(dir, module),
},
};
let target_id = self.resolve_user_module(
importer_path,
importer_source,
module,
path.as_deref(),
&target,
*span,
)?;
user_imports.push((module.clone(), target_id));
}
Ok((stdlib_imports, user_imports))
}
fn dep_entry(&self, importer_path: &str, alias: &str) -> Option<PathBuf> {
let pkg = self.owning_package(importer_path)?;
self.deps.get(&pkg)?.get(alias).cloned()
}
fn owning_package(&self, importer_path: &str) -> Option<PathBuf> {
let canon = std::fs::canonicalize(importer_path).ok()?;
let mut dir = canon.parent();
while let Some(candidate) = dir {
if self.deps.contains_key(candidate) {
return Some(candidate.to_path_buf());
}
dir = candidate.parent();
}
None
}
fn resolve_user_module(
&mut self,
importer_path: &str,
importer_source: &str,
name: &str,
raw_path: Option<&str>,
target: &Path,
span: Span,
) -> Result<u32, Diagnostic> {
let key = match std::fs::canonicalize(target) {
Ok(key) => key,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(match raw_path {
Some(raw) => {
missing_path_module_diag(importer_path, importer_source, raw, target, span)
}
None => missing_module_diag(importer_path, importer_source, name, span),
});
}
Err(err) => {
return Err(read_error_diag(
importer_path,
importer_source,
name,
target,
&err,
span,
))
}
};
if key == self.entry_key {
return Err(entry_import_diag(importer_path, importer_source, span));
}
if self.active.iter().any(|m| m.key == key) {
let chain: Vec<String> = self.active.iter().map(|m| m.name.clone()).collect();
return Err(cycle_diag(
importer_path,
importer_source,
&chain,
name,
span,
));
}
if let Some(id) = self.by_path.get(&key) {
return Ok(*id);
}
self.load_module(name, target, key, importer_path, importer_source, span)
}
fn load_module(
&mut self,
name: &str,
target: &Path,
key: PathBuf,
importer_path: &str,
importer_source: &str,
span: Span,
) -> Result<u32, Diagnostic> {
let source = match std::fs::read_to_string(target) {
Ok(source) => source,
Err(err) => {
return Err(read_error_diag(
importer_path,
importer_source,
name,
target,
&err,
span,
))
}
};
let path_str = target.to_string_lossy().into_owned();
let script = crate::parser::parse(&path_str, &source)?;
let file_id = self.next_id;
self.next_id += 1;
self.by_path.insert(key.clone(), file_id);
self.active.push(ActiveModule {
name: name.to_string(),
key,
});
let (stdlib_imports, user_imports) = self.resolve_imports(&path_str, &source, &script)?;
self.active.pop();
self.modules.push(ProgramFile {
file_id,
is_entry: false,
name: name.to_string(),
path: path_str,
source,
script,
stdlib_imports,
user_imports,
});
self.init_order.push(file_id);
Ok(file_id)
}
}
fn module_path(dir: Option<&Path>, name: &str) -> PathBuf {
let file = format!("{name}.doge");
match dir {
Some(dir) => dir.join(file),
None => PathBuf::from(file),
}
}
fn path_import_path(dir: Option<&Path>, raw: &str) -> PathBuf {
match dir {
Some(dir) => dir.join(raw),
None => PathBuf::from(raw),
}
}
fn canonical_key(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
fn module_file_exists(dir: Option<&Path>, name: &str) -> bool {
module_path(dir, name).is_file()
}
fn file_stem(path: &str) -> String {
Path::new(path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(path)
.to_string()
}