plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


plum-core/src/loader.rs
8a7f6ad 1
use std::collections::{HashMap, HashSet};
8a7f6ad 2
use std::path::{Path, PathBuf};
8a7f6ad 3
8a7f6ad 4
use crate::ast::{Import, Item, Source};
8a7f6ad 5
use crate::parser::AstParser;
8a7f6ad 6
8a7f6ad 7
/// Resolves `entry`'s `import` statements (transitively, and tolerating
8a7f6ad 8
/// diamond imports and cycles) against `lib_path`, and merges every reachable
8a7f6ad 9
/// file's top-level items into a single `Source`. The result's `module` and
8a7f6ad 10
/// `imports` fields are `entry`'s own (imported files' `module`/`imports`
8a7f6ad 11
/// fields are not otherwise used once their `items` have been merged in) —
8a7f6ad 12
/// only `items` accumulates across files.
8a7f6ad 13
///
8a7f6ad 14
/// `import "std/option"` resolves to `<lib_path>/std/option.plum`.
3d6f280 15
pub fn loadAndMerge(entry: &Path, lib_path: &Path) -> Result<Source, String> {
8a7f6ad 16
    let entry_canon = std::fs::canonicalize(entry)
8a7f6ad 17
        .map_err(|e| format!("cannot read entry file '{}': {}", entry.display(), e))?;
3d6f280 18
    let entry_source = parseFile(&entry_canon)?;
8a7f6ad 19
8a7f6ad 20
    let mut visited: HashSet<PathBuf> = HashSet::new();
8a7f6ad 21
    let mut items: Vec<Item> = Vec::new();
8a7f6ad 22
    let mut names: HashMap<String, PathBuf> = HashMap::new();
8a7f6ad 23
8a7f6ad 24
    visited.insert(entry_canon.clone());
3d6f280 25
    mergeItems(&entry_canon, &entry_source, &mut items, &mut names)?;
8a7f6ad 26
    for import in &entry_source.imports {
3d6f280 27
        loadImport(import, lib_path, &mut visited, &mut items, &mut names)?;
8a7f6ad 28
    }
8a7f6ad 29
8a7f6ad 30
    Ok(Source {
8a7f6ad 31
        module: entry_source.module,
8a7f6ad 32
        imports: entry_source.imports,
8a7f6ad 33
        items,
8a7f6ad 34
    })
8a7f6ad 35
}
8a7f6ad 36
3d6f280 37
fn loadImport(
8a7f6ad 38
    import: &Import,
8a7f6ad 39
    lib_path: &Path,
8a7f6ad 40
    visited: &mut HashSet<PathBuf>,
8a7f6ad 41
    items: &mut Vec<Item>,
8a7f6ad 42
    names: &mut HashMap<String, PathBuf>,
8a7f6ad 43
) -> Result<(), String> {
8a7f6ad 44
    let target = lib_path.join(format!("{}.plum", import.path));
8a7f6ad 45
    let target_canon = std::fs::canonicalize(&target)
8a7f6ad 46
        .map_err(|_| format!("import '{}': no such file '{}'", import.path, target.display()))?;
8a7f6ad 47
8a7f6ad 48
    if !visited.insert(target_canon.clone()) {
8a7f6ad 49
        // Already loaded — a diamond import or a cycle. Either way, its items
8a7f6ad 50
        // are already in `items`; nothing more to do.
8a7f6ad 51
        return Ok(());
8a7f6ad 52
    }
8a7f6ad 53
3d6f280 54
    let source = parseFile(&target_canon)?;
3d6f280 55
    mergeItems(&target_canon, &source, items, names)?;
8a7f6ad 56
    for nested in &source.imports {
3d6f280 57
        loadImport(nested, lib_path, visited, items, names)?;
8a7f6ad 58
    }
8a7f6ad 59
    Ok(())
8a7f6ad 60
}
8a7f6ad 61
3d6f280 62
fn parseFile(path: &Path) -> Result<Source, String> {
8a7f6ad 63
    let text = std::fs::read_to_string(path)
8a7f6ad 64
        .map_err(|e| format!("cannot read '{}': {}", path.display(), e))?;
8a7f6ad 65
    let mut parser = tree_sitter::Parser::new();
8a7f6ad 66
    parser
8a7f6ad 67
        .set_language(&tree_sitter_plum::LANGUAGE.into())
8a7f6ad 68
        .map_err(|e| format!("language error: {}", e))?;
8a7f6ad 69
    let tree = parser
8a7f6ad 70
        .parse(&text, None)
8a7f6ad 71
        .ok_or_else(|| format!("parse failed for '{}'", path.display()))?;
8a7f6ad 72
    let ap = AstParser::new(&text);
3d6f280 73
    Ok(ap.parseSource(tree.root_node()))
8a7f6ad 74
}
8a7f6ad 75
3d6f280 76
fn mergeItems(
8a7f6ad 77
    file: &Path,
8a7f6ad 78
    source: &Source,
8a7f6ad 79
    items: &mut Vec<Item>,
8a7f6ad 80
    names: &mut HashMap<String, PathBuf>,
8a7f6ad 81
) -> Result<(), String> {
8a7f6ad 82
    for item in &source.items {
3d6f280 83
        let key = itemNameKey(item);
8a7f6ad 84
        if let Some(existing_file) = names.get(&key) {
8a7f6ad 85
            return Err(format!(
8a7f6ad 86
                "duplicate declaration '{}': declared in both '{}' and '{}'",
8a7f6ad 87
                key,
8a7f6ad 88
                existing_file.display(),
8a7f6ad 89
                file.display()
8a7f6ad 90
            ));
8a7f6ad 91
        }
8a7f6ad 92
        names.insert(key, file.to_path_buf());
8a7f6ad 93
        items.push(item.clone());
8a7f6ad 94
    }
8a7f6ad 95
    Ok(())
8a7f6ad 96
}
8a7f6ad 97
8a7f6ad 98
/// A collision key that mirrors `plum-checker`'s own separation of
8a7f6ad 99
/// namespaces: methods are keyed by `(receiver, name)` (so `length<Cat>` and
8a7f6ad 100
/// `length<Box>` never collide, exactly like `plum_checker::MethodEnv`),
8a7f6ad 101
/// while classes/traits/enums/consts/free-functions are each their own
8a7f6ad 102
/// flat, kind-qualified namespace.
3d6f280 103
fn itemNameKey(item: &Item) -> String {
8a7f6ad 104
    match item {
8a7f6ad 105
        Item::Class(c) => format!("class::{}", c.name),
8a7f6ad 106
        Item::Trait(t) => format!("trait::{}", t.name),
8a7f6ad 107
        Item::Enum(e) => format!("enum::{}", e.name),
8a7f6ad 108
        Item::Const(c) => format!("const::{}", c.name),
8a7f6ad 109
        Item::Fn(f) => match &f.type_param {
8a7f6ad 110
            Some(recv) => format!("method::{}::{}", recv, f.name),
8a7f6ad 111
            None => format!("fn::{}", f.name),
8a7f6ad 112
        },
8a7f6ad 113
    }
8a7f6ad 114
}