Skip to main content

elly_core/
load.rs

1//! Resolves and instantiates a module's import graph.
2//!
3//! Imports are top-level declarations; the reachable module set is fixed by source.
4//! The [`ModuleResolver`] provides a module's canonical name and source, which is then
5//! compiled and instantiated into the importing module's frame. Instantiated modules
6//! contain unevaluated bodies (see [`crate::instantiate`]).
7//!
8//! ## Loading Process
9//!
10//! Traversal is post-order depth-first; a module is instantiated only after its imports.
11//! This ensures frames contain only finished values and enforces acyclicity.
12//! Cycles are detected during traversal and reported as [`LoadError::Cycle`].
13//!
14//! Modules are deduplicated by canonical name. All specs resolving to the same name
15//! share a single instance, ensuring consistent identity across the import graph.
16//!
17//! [`Instances`] maps names to instances. Host-level caching is achieved by persisting
18//! this map across multiple compilation sessions.
19
20use alloc::string::String;
21use alloc::vec::Vec;
22
23use crate::eval::{instantiate, Ctx};
24use crate::module::{ModuleData, ModuleName};
25use crate::{Error, Value};
26
27/// A module's canonical name and source.
28///
29/// The name defines the module's identity; all specs resolving to one module must
30/// return the same canonical name.
31pub struct Resolved {
32    /// Unique name for the module.
33    pub name: String,
34    /// Module source.
35    pub source: String,
36}
37
38/// Host-provided module loader. Maps an import spec to its canonical name and source.
39///
40/// Because `elly-core` is `no_std`, file I/O is handled by the host via this trait.
41pub trait ModuleResolver {
42    fn resolve(&self, spec: &str) -> Option<Resolved>;
43}
44
45/// Map from canonical name to instantiated module.
46///
47/// Deduplicates the import graph within a single [`load_module`] and can act as a
48/// host-level cache across calls. Since a name fixes the frame and imports,
49/// cached instances remain valid unless dropped.
50#[derive(Debug, Default)]
51pub struct Instances {
52    entries: Vec<(ModuleName, Value)>,
53}
54
55impl Instances {
56    /// Create an empty instance map.
57    pub fn new() -> Instances {
58        Instances {
59            entries: Vec::new(),
60        }
61    }
62
63    /// Get the instance for `name`.
64    pub fn get(&self, name: &str) -> Option<&Value> {
65        self.entries
66            .iter()
67            .find(|(n, _)| n.as_str() == name)
68            .map(|(_, v)| v)
69    }
70
71    /// Insert or replace the instance for `name`.
72    pub fn insert(&mut self, name: ModuleName, value: Value) {
73        match self.entries.iter_mut().find(|(n, _)| *n == name) {
74            Some(entry) => entry.1 = value,
75            None => self.entries.push((name, value)),
76        }
77    }
78
79    /// Number of modules in the map.
80    pub fn len(&self) -> usize {
81        self.entries.len()
82    }
83
84    /// Whether the map is empty.
85    pub fn is_empty(&self) -> bool {
86        self.entries.is_empty()
87    }
88
89    /// Canonical names in insertion order.
90    pub fn names(&self) -> impl Iterator<Item = &ModuleName> {
91        self.entries.iter().map(|(n, _)| n)
92    }
93}
94
95/// Compile-time module loading errors.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum LoadError {
98    /// Spec could not be resolved.
99    NotFound { spec: String },
100    /// Import cycle detected; `path` is the closing chain of names.
101    Cycle { path: Vec<ModuleName> },
102    /// Module failed to compile.
103    Compile { name: ModuleName, error: Error },
104}
105
106/// Loads a module and its import graph: resolves, compiles, and instantiates
107/// modules leaves-first, returning the instance for `spec`.
108///
109/// `seen` deduplicates instances and may be used as a cache. `ctx` handles
110/// item ID allocation.
111pub fn load_module(
112    spec: &str,
113    resolver: &dyn ModuleResolver,
114    ctx: &Ctx,
115    seen: &mut Instances,
116) -> Result<Value, LoadError> {
117    let mut path = Vec::new();
118    load_spec(spec, resolver, ctx, seen, &mut path)
119}
120
121/// Loads a module from provided source under `name`.
122///
123/// Ignores and replaces any existing instance of `name` in `seen`, allowing
124/// explicit reloads.
125pub fn load_module_source(
126    name: &str,
127    src: &str,
128    resolver: &dyn ModuleResolver,
129    ctx: &Ctx,
130    seen: &mut Instances,
131) -> Result<Value, LoadError> {
132    let mut path = Vec::new();
133    instantiate_source(ModuleName::from(name), src, resolver, ctx, seen, &mut path)
134}
135
136/// Resolves a spec to an instance, using `seen` for deduplication.
137/// Detects cycles via `path`.
138fn load_spec(
139    spec: &str,
140    resolver: &dyn ModuleResolver,
141    ctx: &Ctx,
142    seen: &mut Instances,
143    path: &mut Vec<ModuleName>,
144) -> Result<Value, LoadError> {
145    let resolved = resolver.resolve(spec).ok_or_else(|| LoadError::NotFound {
146        spec: String::from(spec),
147    })?;
148    let name = ModuleName::from(resolved.name.as_str());
149    if let Some(instance) = seen.get(name.as_str()) {
150        return Ok(instance.clone());
151    }
152    if path.contains(&name) {
153        let mut cycle = path.clone();
154        cycle.push(name);
155        return Err(LoadError::Cycle { path: cycle });
156    }
157    instantiate_source(name, &resolved.source, resolver, ctx, seen, path)
158}
159
160/// Compiles source as `name`, loads imports, and instantiates the module.
161///
162/// Traversal is post-order; imports are loaded before the module is instantiated,
163/// ensuring frame slots contain finished modules.
164fn instantiate_source(
165    name: ModuleName,
166    src: &str,
167    resolver: &dyn ModuleResolver,
168    ctx: &Ctx,
169    seen: &mut Instances,
170    path: &mut Vec<ModuleName>,
171) -> Result<Value, LoadError> {
172    let data: alloc::rc::Rc<ModuleData> = crate::compile_module_named(src, &[], Some(name.clone()))
173        .map_err(|error| LoadError::Compile {
174            name: name.clone(),
175            error,
176        })?;
177    path.push(name.clone());
178    let mut frame: Vec<Value> = Vec::with_capacity(data.imports().len());
179    for spec in data.imports() {
180        frame.push(load_spec(spec.as_str(), resolver, ctx, seen, path)?);
181    }
182    path.pop();
183    // The module was compiled against no host frame, so its slots are exactly its
184    // imports and the arity check cannot fail.
185    let instance =
186        instantiate(data, &frame, ctx).expect("a module's frame is exactly its own imports");
187    seen.insert(name, instance.clone());
188    Ok(instance)
189}