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. A [`Ctx`] holds one for its lifetime,
18//! together with its resolver, so loads through [`Ctx::load`] share a cache. The
19//! free functions [`load_module`] and [`load_module_source`] take both explicitly.
20
21use alloc::string::String;
22use alloc::vec::Vec;
23use core::cell::Ref;
24
25use crate::eval::{instantiate, Ctx};
26use crate::module::{ModuleData, ModuleName};
27use crate::{Error, Value};
28
29/// A module's canonical name and source.
30///
31/// The name defines the module's identity; all specs resolving to one module must
32/// return the same canonical name.
33pub struct Resolved {
34 /// Unique name for the module.
35 pub name: String,
36 /// Module source.
37 pub source: String,
38}
39
40/// Host-provided module loader. Maps an import spec to its canonical name and source.
41///
42/// Because `elly-core` is `no_std`, file I/O is handled by the host via this trait.
43pub trait ModuleResolver {
44 fn resolve(&self, spec: &str) -> Option<Resolved>;
45}
46
47/// Map from canonical name to instantiated module.
48///
49/// Deduplicates the import graph within a single [`load_module`] and acts as the
50/// cache a [`Ctx`] keeps across loads. Since a name fixes the frame and imports,
51/// cached instances remain valid unless dropped.
52#[derive(Debug, Default)]
53pub struct Instances {
54 entries: Vec<(ModuleName, Value)>,
55}
56
57impl Instances {
58 /// Create an empty instance map.
59 pub fn new() -> Instances {
60 Instances {
61 entries: Vec::new(),
62 }
63 }
64
65 /// Get the instance for `name`.
66 pub fn get(&self, name: &str) -> Option<&Value> {
67 self.entries
68 .iter()
69 .find(|(n, _)| n.as_str() == name)
70 .map(|(_, v)| v)
71 }
72
73 /// Insert or replace the instance for `name`.
74 pub fn insert(&mut self, name: ModuleName, value: Value) {
75 match self.entries.iter_mut().find(|(n, _)| *n == name) {
76 Some(entry) => entry.1 = value,
77 None => self.entries.push((name, value)),
78 }
79 }
80
81 /// Number of modules in the map.
82 pub fn len(&self) -> usize {
83 self.entries.len()
84 }
85
86 /// Whether the map is empty.
87 pub fn is_empty(&self) -> bool {
88 self.entries.is_empty()
89 }
90
91 /// Canonical names in insertion order.
92 pub fn names(&self) -> impl Iterator<Item = &ModuleName> {
93 self.entries.iter().map(|(n, _)| n)
94 }
95}
96
97/// Compile-time module loading errors.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum LoadError {
100 /// Spec could not be resolved.
101 NotFound { spec: String },
102 /// Import cycle detected; `path` is the closing chain of names.
103 Cycle { path: Vec<ModuleName> },
104 /// Module failed to compile.
105 Compile { name: ModuleName, error: Error },
106}
107
108impl Ctx {
109 /// Loads a module and its import graph through this context's resolver,
110 /// returning the instance for `spec`. A module already in the cache is
111 /// reused, so two loads that reach one module share its instance.
112 ///
113 /// Without a resolver, every spec is [`LoadError::NotFound`].
114 ///
115 /// Loading is not re-entrant: nothing evaluates while a graph is
116 /// instantiated, so no host callback can run a nested load.
117 pub fn load(&self, spec: &str) -> Result<Value, LoadError> {
118 let mut seen = self.instances.borrow_mut();
119 load_module(spec, self.resolver(), self, &mut seen)
120 }
121
122 /// Loads a module from `src` under `name`, resolving its imports through this
123 /// context's resolver. The caller supplied the text, so it replaces whatever
124 /// `name` held in the cache — the explicit reload.
125 pub fn load_source(&self, name: &str, src: &str) -> Result<Value, LoadError> {
126 let mut seen = self.instances.borrow_mut();
127 load_module_source(name, src, self.resolver(), self, &mut seen)
128 }
129
130 /// The modules loaded so far, by canonical name.
131 pub fn instances(&self) -> Ref<'_, Instances> {
132 self.instances.borrow()
133 }
134
135 fn resolver(&self) -> &dyn ModuleResolver {
136 match &self.resolver {
137 Some(resolver) => resolver.as_ref(),
138 None => &NoResolver,
139 }
140 }
141}
142
143/// The resolver of a [`Ctx`] given none: nothing resolves.
144struct NoResolver;
145
146impl ModuleResolver for NoResolver {
147 fn resolve(&self, _spec: &str) -> Option<Resolved> {
148 None
149 }
150}
151
152/// Loads a module and its import graph: resolves, compiles, and instantiates
153/// modules leaves-first, returning the instance for `spec`.
154///
155/// `seen` deduplicates instances and may be used as a cache. `ctx` handles
156/// item ID allocation.
157pub fn load_module(
158 spec: &str,
159 resolver: &dyn ModuleResolver,
160 ctx: &Ctx,
161 seen: &mut Instances,
162) -> Result<Value, LoadError> {
163 let mut path = Vec::new();
164 load_spec(spec, resolver, ctx, seen, &mut path)
165}
166
167/// Loads a module from provided source under `name`.
168///
169/// Ignores and replaces any existing instance of `name` in `seen`, allowing
170/// explicit reloads.
171pub fn load_module_source(
172 name: &str,
173 src: &str,
174 resolver: &dyn ModuleResolver,
175 ctx: &Ctx,
176 seen: &mut Instances,
177) -> Result<Value, LoadError> {
178 let mut path = Vec::new();
179 instantiate_source(ModuleName::from(name), src, resolver, ctx, seen, &mut path)
180}
181
182/// Resolves a spec to an instance, using `seen` for deduplication.
183/// Detects cycles via `path`.
184fn load_spec(
185 spec: &str,
186 resolver: &dyn ModuleResolver,
187 ctx: &Ctx,
188 seen: &mut Instances,
189 path: &mut Vec<ModuleName>,
190) -> Result<Value, LoadError> {
191 let resolved = resolver.resolve(spec).ok_or_else(|| LoadError::NotFound {
192 spec: String::from(spec),
193 })?;
194 let name = ModuleName::from(resolved.name.as_str());
195 if let Some(instance) = seen.get(name.as_str()) {
196 return Ok(instance.clone());
197 }
198 if path.contains(&name) {
199 let mut cycle = path.clone();
200 cycle.push(name);
201 return Err(LoadError::Cycle { path: cycle });
202 }
203 instantiate_source(name, &resolved.source, resolver, ctx, seen, path)
204}
205
206/// Compiles source as `name`, loads imports, and instantiates the module.
207///
208/// Traversal is post-order; imports are loaded before the module is instantiated,
209/// ensuring frame slots contain finished modules.
210fn instantiate_source(
211 name: ModuleName,
212 src: &str,
213 resolver: &dyn ModuleResolver,
214 ctx: &Ctx,
215 seen: &mut Instances,
216 path: &mut Vec<ModuleName>,
217) -> Result<Value, LoadError> {
218 let data: alloc::rc::Rc<ModuleData> = crate::compile_module_named(src, &[], Some(name.clone()))
219 .map_err(|error| LoadError::Compile {
220 name: name.clone(),
221 error,
222 })?;
223 path.push(name.clone());
224 let mut frame: Vec<Value> = Vec::with_capacity(data.imports().len());
225 for spec in data.imports() {
226 frame.push(load_spec(spec.as_str(), resolver, ctx, seen, path)?);
227 }
228 path.pop();
229 // The module was compiled against no host frame, so its slots are exactly its
230 // imports and the arity check cannot fail.
231 let instance =
232 instantiate(data, &frame, ctx).expect("a module's frame is exactly its own imports");
233 seen.insert(name, instance.clone());
234 Ok(instance)
235}