Skip to main content

aion/runtime/
module.rs

1//! Runtime module registration helpers.
2
3use std::collections::HashMap;
4
5use beamr::atom::Atom;
6use beamr::loader::decode::compact::Operand;
7use beamr::loader::{Instruction, Literal, lambda_unique_id, prepare_module};
8use beamr::module::ResolvedImportTarget;
9
10use crate::{EngineError, RuntimeHandle};
11
12/// Atom-level rename map from original module names to deployed names.
13///
14/// Built by [`RuntimeHandle::package_rename_map`] from a package's module
15/// list and consumed by [`RuntimeHandle::register_module_with_renames`].
16pub type ModuleRenameMap = HashMap<Atom, Atom>;
17
18impl RuntimeHandle {
19    /// Register transformed BEAM bytes under their already-deployed module name.
20    ///
21    /// Only rewrites self-references. Use [`Self::register_module_with_renames`]
22    /// for package loads where cross-module imports need the full rename map.
23    ///
24    /// # Errors
25    ///
26    /// Returns [`EngineError::Runtime`] when beamr cannot prepare the module bytes
27    /// or when the deployed name still has retained old code in the registry.
28    pub fn register_module(
29        &self,
30        deployed_name: &str,
31        beam_bytes: &[u8],
32    ) -> Result<(), EngineError> {
33        let mut self_only = HashMap::new();
34        let deployed_atom = self.atom_table.intern(deployed_name);
35        let (module, _) = prepare_module(
36            beam_bytes,
37            &self.atom_table,
38            &self.module_registry,
39            self.native_registry.as_ref(),
40        )
41        .map_err(runtime_error_from_display)?;
42        self_only.insert(module.name, deployed_atom);
43        self.register_prepared_module(module, deployed_name, &self_only)
44    }
45
46    /// Register BEAM bytes with a full package rename map for cross-module
47    /// import rewriting.
48    ///
49    /// # Errors
50    ///
51    /// Returns [`EngineError::Runtime`] when beamr cannot prepare the module bytes
52    /// or when the deployed name still has retained old code in the registry.
53    pub fn register_module_with_renames(
54        &self,
55        deployed_name: &str,
56        beam_bytes: &[u8],
57        rename_map: &ModuleRenameMap,
58    ) -> Result<(), EngineError> {
59        let (module, unresolved) = prepare_module(
60            beam_bytes,
61            &self.atom_table,
62            &self.module_registry,
63            self.native_registry.as_ref(),
64        )
65        .map_err(runtime_error_from_display)?;
66        if !unresolved.is_empty() {
67            tracing::warn!(
68                module = deployed_name,
69                count = unresolved.imports().len(),
70                "unresolved BEAM imports after BIF + module registration"
71            );
72        }
73        self.register_prepared_module(module, deployed_name, rename_map)
74    }
75
76    /// Build the atom-level rename map for every module in a package.
77    #[must_use]
78    pub fn package_rename_map(
79        &self,
80        original_names: &[&str],
81        deployed_names: &[&str],
82    ) -> ModuleRenameMap {
83        original_names
84            .iter()
85            .zip(deployed_names.iter())
86            .map(|(original, deployed)| {
87                (
88                    self.atom_table.intern(original),
89                    self.atom_table.intern(deployed),
90                )
91            })
92            .collect()
93    }
94
95    fn register_prepared_module(
96        &self,
97        mut module: beamr::module::Module,
98        deployed_name: &str,
99        rename_map: &ModuleRenameMap,
100    ) -> Result<(), EngineError> {
101        let deployed_atom = self.atom_table.intern(deployed_name);
102        if self.module_registry.lookup_old(deployed_atom).is_some() {
103            return Err(runtime_error(format!(
104                "cannot register deployed module `{deployed_name}` while old code is still retained"
105            )));
106        }
107        rename_module_references(&mut module, deployed_atom, rename_map, &self.atom_table)?;
108        self.module_registry.insert(module);
109        Ok(())
110    }
111
112    /// Return true when a module has been registered in the embedded module registry.
113    #[must_use]
114    pub fn has_registered_module(&self, deployed_name: &str) -> bool {
115        let module = self.atom_table.intern(deployed_name);
116        self.module_registry.lookup(module).is_some()
117    }
118
119    /// Return true when a registered module exports `function` at any arity.
120    ///
121    /// Workflow entry points are spawned at arity 0 or 1 depending on input;
122    /// the load-time route commit only needs to know the name is exported at
123    /// all — a wrong-arity spawn still fails typed at dispatch.
124    #[must_use]
125    pub fn module_exports_function(&self, deployed_name: &str, function: &str) -> bool {
126        let module_atom = self.atom_table.intern(deployed_name);
127        let Some(module) = self.module_registry.lookup(module_atom) else {
128            return false;
129        };
130        let function_atom = self.atom_table.intern(function);
131        module
132            .exports
133            .keys()
134            .any(|(name, _arity)| *name == function_atom)
135    }
136
137    /// Remove a module registered during a failed staged package load.
138    pub(crate) fn unregister_module(&self, deployed_name: &str) -> Result<(), EngineError> {
139        let module = self.atom_table.intern(deployed_name);
140        if self.module_registry.delete_module(module) {
141            Ok(())
142        } else {
143            Err(runtime_error(format!(
144                "module `{deployed_name}` was not registered"
145            )))
146        }
147    }
148}
149
150fn rename_module_references(
151    module: &mut beamr::module::Module,
152    deployed_atom: Atom,
153    rename_map: &ModuleRenameMap,
154    atom_table: &beamr::atom::AtomTable,
155) -> Result<(), EngineError> {
156    if module.name == deployed_atom && rename_map.len() <= 1 {
157        return Ok(());
158    }
159
160    module.name = deployed_atom;
161    for import in &mut module.resolved_imports {
162        if let Some(&new_atom) = rename_map.get(&import.module) {
163            import.module = new_atom;
164        }
165        rewrite_resolved_import_target(&mut import.target, rename_map);
166    }
167    for instruction in &mut module.code {
168        rewrite_instruction_module_operand(instruction, rename_map);
169    }
170    for literal in &mut module.literals {
171        rewrite_literal_atom(literal, rename_map);
172    }
173    // `prepare_module` materialises the constant pool before this rename pass
174    // runs, so the pool still holds terms (e.g. export funs) referencing the
175    // original module names. Rebuild it from the rewritten descriptors.
176    module.constant_pool =
177        beamr::constant_pool::materialise_literals(&module.literals, Some(atom_table))
178            .map_err(runtime_error_from_display)?;
179    for lambda in &mut module.lambdas {
180        lambda.unique_id = lambda_unique_id(
181            atom_table,
182            deployed_atom,
183            lambda.function,
184            lambda.arity,
185            lambda.num_free,
186        )
187        .map_err(runtime_error_from_display)?;
188    }
189
190    Ok(())
191}
192
193fn rewrite_resolved_import_target(target: &mut ResolvedImportTarget, rename_map: &ModuleRenameMap) {
194    match target {
195        ResolvedImportTarget::Code { module, .. }
196        | ResolvedImportTarget::Deferred { module, .. }
197        | ResolvedImportTarget::Unresolved { module, .. } => {
198            if let Some(&new_atom) = rename_map.get(module) {
199                *module = new_atom;
200            }
201        }
202        ResolvedImportTarget::Native(_) | ResolvedImportTarget::Denied { .. } => {}
203    }
204}
205
206fn rewrite_instruction_module_operand(instruction: &mut Instruction, rename_map: &ModuleRenameMap) {
207    if let Instruction::FuncInfo { module, .. } = instruction {
208        rewrite_operand_atom(module, rename_map);
209    }
210}
211
212fn rewrite_operand_atom(operand: &mut Operand, rename_map: &ModuleRenameMap) {
213    match operand {
214        Operand::Atom(Some(atom)) => {
215            if let Some(&new_atom) = rename_map.get(atom) {
216                *atom = new_atom;
217            }
218        }
219        Operand::List(items) => {
220            for item in items {
221                rewrite_operand_atom(item, rename_map);
222            }
223        }
224        Operand::TypedRegister { register, .. } => {
225            rewrite_operand_atom(register, rename_map);
226        }
227        Operand::Literal(_)
228        | Operand::Integer(_)
229        | Operand::Unsigned(_)
230        | Operand::Atom(_)
231        | Operand::X(_)
232        | Operand::Y(_)
233        | Operand::Label(_)
234        | Operand::Character(_)
235        | Operand::FloatRegister(_)
236        | Operand::Allocation(_) => {}
237    }
238}
239
240fn rewrite_literal_atom(literal: &mut Literal, rename_map: &ModuleRenameMap) {
241    match literal {
242        Literal::Atom(atom) => {
243            if let Some(&new_atom) = rename_map.get(atom) {
244                *atom = new_atom;
245            }
246        }
247        Literal::Tuple(items) => {
248            for item in items {
249                rewrite_literal_atom(item, rename_map);
250            }
251        }
252        Literal::List(items, tail) => {
253            for item in items {
254                rewrite_literal_atom(item, rename_map);
255            }
256            rewrite_literal_atom(tail, rename_map);
257        }
258        Literal::Map(entries) => {
259            for (key, value) in entries {
260                rewrite_literal_atom(key, rename_map);
261                rewrite_literal_atom(value, rename_map);
262            }
263        }
264        Literal::Integer(_)
265        | Literal::Float(_)
266        | Literal::BigInteger(_)
267        | Literal::Binary(_)
268        | Literal::Nil
269        | Literal::String(_) => {}
270        Literal::ExportFun {
271            module, function, ..
272        } => {
273            if let Some(&new_atom) = rename_map.get(module) {
274                *module = new_atom;
275            }
276            if let Some(&new_atom) = rename_map.get(function) {
277                *function = new_atom;
278            }
279        }
280    }
281}
282
283fn runtime_error(reason: String) -> EngineError {
284    EngineError::Runtime { reason }
285}
286
287fn runtime_error_from_display(reason: impl std::fmt::Display) -> EngineError {
288    runtime_error(reason.to_string())
289}