plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-core/src/loader.rs
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use crate::ast::{Import, Item, Source};
use crate::parser::AstParser;
/// Resolves `entry`'s `import` statements (transitively, and tolerating
/// diamond imports and cycles) against `lib_path`, and merges every reachable
/// file's top-level items into a single `Source`. The result's `module` and
/// `imports` fields are `entry`'s own (imported files' `module`/`imports`
/// fields are not otherwise used once their `items` have been merged in) —
/// only `items` accumulates across files.
///
/// `import "std/option"` resolves to `<lib_path>/std/option.plum`.
pub fn loadAndMerge(entry: &Path, lib_path: &Path) -> Result<Source, String> {
let entry_canon = std::fs::canonicalize(entry)
.map_err(|e| format!("cannot read entry file '{}': {}", entry.display(), e))?;
let entry_source = parseFile(&entry_canon)?;
let mut visited: HashSet<PathBuf> = HashSet::new();
let mut items: Vec<Item> = Vec::new();
let mut names: HashMap<String, PathBuf> = HashMap::new();
visited.insert(entry_canon.clone());
mergeItems(&entry_canon, &entry_source, &mut items, &mut names)?;
for import in &entry_source.imports {
loadImport(import, lib_path, &mut visited, &mut items, &mut names)?;
}
Ok(Source {
module: entry_source.module,
imports: entry_source.imports,
items,
})
}
fn loadImport(
import: &Import,
lib_path: &Path,
visited: &mut HashSet<PathBuf>,
items: &mut Vec<Item>,
names: &mut HashMap<String, PathBuf>,
) -> Result<(), String> {
let target = lib_path.join(format!("{}.plum", import.path));
let target_canon = std::fs::canonicalize(&target)
.map_err(|_| format!("import '{}': no such file '{}'", import.path, target.display()))?;
if !visited.insert(target_canon.clone()) {
// Already loaded — a diamond import or a cycle. Either way, its items
// are already in `items`; nothing more to do.
return Ok(());
}
let source = parseFile(&target_canon)?;
mergeItems(&target_canon, &source, items, names)?;
for nested in &source.imports {
loadImport(nested, lib_path, visited, items, names)?;
}
Ok(())
}
fn parseFile(path: &Path) -> Result<Source, String> {
let text = std::fs::read_to_string(path)
.map_err(|e| format!("cannot read '{}': {}", path.display(), e))?;
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_plum::LANGUAGE.into())
.map_err(|e| format!("language error: {}", e))?;
let tree = parser
.parse(&text, None)
.ok_or_else(|| format!("parse failed for '{}'", path.display()))?;
let ap = AstParser::new(&text);
Ok(ap.parseSource(tree.root_node()))
}
fn mergeItems(
file: &Path,
source: &Source,
items: &mut Vec<Item>,
names: &mut HashMap<String, PathBuf>,
) -> Result<(), String> {
for item in &source.items {
let key = itemNameKey(item);
if let Some(existing_file) = names.get(&key) {
return Err(format!(
"duplicate declaration '{}': declared in both '{}' and '{}'",
key,
existing_file.display(),
file.display()
));
}
names.insert(key, file.to_path_buf());
items.push(item.clone());
}
Ok(())
}
/// A collision key that mirrors `plum-checker`'s own separation of
/// namespaces: methods are keyed by `(receiver, name)` (so `length<Cat>` and
/// `length<Box>` never collide, exactly like `plum_checker::MethodEnv`),
/// while classes/traits/enums/consts/free-functions are each their own
/// flat, kind-qualified namespace.
fn itemNameKey(item: &Item) -> String {
match item {
Item::Class(c) => format!("class::{}", c.name),
Item::Trait(t) => format!("trait::{}", t.name),
Item::Enum(e) => format!("enum::{}", e.name),
Item::Const(c) => format!("const::{}", c.name),
Item::Fn(f) => match &f.type_param {
Some(recv) => format!("method::{}::{}", recv, f.name),
None => format!("fn::{}", f.name),
},
}
}