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}