plum
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 | } |