use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf;
use std::sync::Arc;
use dashmap::DashMap;
use dashmap::mapref::entry::Entry;
use crate::atom::Atom;
use crate::constant_pool::ConstantPool;
use crate::error::ExecError;
use crate::loader::{Instruction, LambdaEntry, LineInfo, Literal};
use crate::native::NativeEntry;
#[derive(Copy, Clone, Debug)]
pub enum ResolvedImportTarget {
Code {
module: Atom,
label: u32,
},
Native(NativeEntry),
Denied {
capability: crate::native::Capability,
},
Deferred {
module: Atom,
function: Atom,
arity: u8,
},
Unresolved {
module: Atom,
function: Atom,
arity: u8,
},
}
#[derive(Copy, Clone, Debug)]
pub struct ResolvedImport {
pub module: Atom,
pub function: Atom,
pub arity: u8,
pub target: ResolvedImportTarget,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ModuleOrigin {
Filesystem(PathBuf),
Embedded,
Preloaded,
}
impl ModuleOrigin {
#[must_use]
pub const fn source_atom_name(&self) -> &'static str {
match self {
Self::Filesystem(_) => "filesystem",
Self::Embedded => "embedded",
Self::Preloaded => "preloaded",
}
}
}
#[derive(Clone, Debug)]
pub struct Module {
pub name: Atom,
pub generation: u64,
pub origin: ModuleOrigin,
pub exports: HashMap<(Atom, u8), u32>,
pub label_index: HashMap<u32, usize>,
pub code: Vec<Instruction>,
pub function_table: Vec<(usize, Atom, u8)>,
pub line_table: Vec<(usize, usize)>,
pub literals: Vec<Literal>,
pub constant_pool: ConstantPool,
pub resolved_imports: Vec<ResolvedImport>,
pub lambdas: Vec<LambdaEntry>,
pub string_table: Vec<u8>,
pub line_info: Vec<LineInfo>,
}
impl Module {
#[must_use]
pub const fn generation(&self) -> u64 {
self.generation
}
#[must_use]
pub fn find_lambda_by_id(&self, unique_id: u64) -> Option<&LambdaEntry> {
self.lambdas
.iter()
.find(|lambda| lambda.unique_id == unique_id)
}
pub fn label_ip(&self, label: u32) -> Result<usize, ExecError> {
self.label_index
.get(&label)
.copied()
.ok_or(ExecError::InvalidLabel { label })
}
pub fn export_ip(&self, function: Atom, arity: u8) -> Result<usize, ExecError> {
let label = self
.exports
.get(&(function, arity))
.copied()
.ok_or(ExecError::Undef {
module: self.name,
function,
arity,
})?;
self.label_ip(label)
}
#[must_use]
pub fn function_at_ip(&self, ip: usize) -> Option<(Atom, u8)> {
let index = self
.function_table
.binary_search_by_key(&ip, |(entry_ip, _, _)| *entry_ip)
.map_or_else(|insertion| insertion.checked_sub(1), Some)?;
let (_, function, arity) = self.function_table.get(index).copied()?;
Some((function, arity))
}
#[must_use]
pub fn line_at_ip(&self, ip: usize) -> Option<u32> {
let index = self
.line_table
.binary_search_by_key(&ip, |(entry_ip, _)| *entry_ip)
.map_or_else(|insertion| insertion.checked_sub(1), Some)?;
let (_, line_info_index) = self.line_table.get(index).copied()?;
self.line_info.get(line_info_index).map(|info| info.line)
}
}
#[derive(Clone, Debug)]
pub struct CodePointer {
pub module: Arc<Module>,
pub label: u32,
pub generation: u64,
}
impl PartialEq for CodePointer {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.module, &other.module)
&& self.label == other.label
&& self.generation == other.generation
}
}
impl Eq for CodePointer {}
#[derive(Clone, Debug)]
pub struct ModuleVersions {
pub current: Arc<Module>,
pub old: Option<Arc<Module>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PurgeError {
StillReferenced { module: Atom, ref_count: usize },
NoOldVersion { module: Atom },
}
impl fmt::Display for PurgeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::StillReferenced { module, ref_count } => write!(
formatter,
"old module version {:?} is still referenced ({ref_count} references)",
module
),
Self::NoOldVersion { module } => {
write!(formatter, "module {:?} has no old version to purge", module)
}
}
}
}
impl std::error::Error for PurgeError {}
#[derive(Debug, Default)]
pub struct ModuleRegistry {
modules: DashMap<Atom, ModuleVersions>,
}
impl ModuleRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&self, module: Module) -> Arc<Module> {
self.insert_version(module)
}
pub fn insert_arc(&self, module: Arc<Module>) -> Arc<Module> {
self.insert_version((*module).clone())
}
fn insert_version(&self, mut module: Module) -> Arc<Module> {
let name = module.name;
match self.modules.entry(name) {
Entry::Occupied(mut entry) => {
let previous_current = Arc::clone(&entry.get().current);
module.generation = previous_current.generation().saturating_add(1);
let module = Arc::new(module);
*entry.get_mut() = ModuleVersions {
current: Arc::clone(&module),
old: Some(previous_current),
};
module
}
Entry::Vacant(entry) => {
module.generation = 1;
let module = Arc::new(module);
entry.insert(ModuleVersions {
current: Arc::clone(&module),
old: None,
});
module
}
}
}
#[must_use]
pub fn lookup(&self, name: Atom) -> Option<Arc<Module>> {
self.modules
.get(&name)
.map(|entry| Arc::clone(&entry.value().current))
}
#[must_use]
pub fn origin(&self, name: Atom) -> Option<ModuleOrigin> {
self.lookup(name).map(|module| module.origin.clone())
}
#[must_use]
pub fn all_loaded(&self) -> Vec<(Atom, ModuleOrigin)> {
let mut modules: Vec<_> = self
.modules
.iter()
.map(|entry| (*entry.key(), entry.value().current.origin.clone()))
.collect();
modules.sort_by_key(|(name, _)| name.index());
modules
}
#[must_use]
pub fn lookup_old(&self, name: Atom) -> Option<Arc<Module>> {
self.modules
.get(&name)
.and_then(|entry| entry.value().old.as_ref().map(Arc::clone))
}
#[must_use]
pub fn module_version_count(&self, name: Atom) -> usize {
self.modules
.get(&name)
.map_or(0, |entry| 1 + usize::from(entry.value().old.is_some()))
}
pub fn purge_old(&self, name: Atom) -> Result<(), PurgeError> {
let mut entry = self
.modules
.get_mut(&name)
.ok_or(PurgeError::NoOldVersion { module: name })?;
let old = entry
.old
.as_ref()
.ok_or(PurgeError::NoOldVersion { module: name })?;
let ref_count = Arc::strong_count(old);
if ref_count != 1 {
return Err(PurgeError::StillReferenced {
module: name,
ref_count,
});
}
entry.old = None;
Ok(())
}
pub fn lookup_mfa(
&self,
module: Atom,
function: Atom,
arity: u8,
) -> Result<CodePointer, ExecError> {
let loaded = self.lookup(module).ok_or(ExecError::Undef {
module,
function,
arity,
})?;
let label = loaded
.exports
.get(&(function, arity))
.copied()
.ok_or(ExecError::Undef {
module,
function,
arity,
})?;
Ok(CodePointer {
generation: loaded.generation(),
module: loaded,
label,
})
}
#[must_use]
pub fn has_old_code(&self, name: Atom) -> bool {
self.lookup_old(name).is_some()
}
pub fn delete_module(&self, name: Atom) -> bool {
self.modules.remove(&name).is_some()
}
pub(crate) fn force_remove_old(&self, name: Atom) -> Result<(), PurgeError> {
let mut entry = self
.modules
.get_mut(&name)
.ok_or(PurgeError::NoOldVersion { module: name })?;
entry
.old
.take()
.ok_or(PurgeError::NoOldVersion { module: name })?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::{Module, ModuleOrigin, ModuleRegistry, PurgeError};
use crate::atom::AtomTable;
use crate::error::ExecError;
use crate::loader::{LambdaEntry, LineInfo};
fn label_index(code: &[crate::loader::Instruction]) -> HashMap<u32, usize> {
code.iter()
.enumerate()
.filter_map(|(ip, instruction)| match instruction {
crate::loader::Instruction::Label { label } => Some((*label, ip)),
_ => None,
})
.collect()
}
fn empty_module(name: crate::atom::Atom) -> Module {
Module {
name,
generation: 0,
origin: ModuleOrigin::Preloaded,
exports: HashMap::new(),
label_index: HashMap::new(),
code: Vec::new(),
literals: Vec::new(),
constant_pool: crate::constant_pool::ConstantPool::default(),
resolved_imports: Vec::new(),
lambdas: Vec::new(),
string_table: Vec::new(),
function_table: Vec::new(),
line_table: Vec::new(),
line_info: Vec::new(),
}
}
#[test]
fn function_at_ip_resolves_last_preceding_func_info() {
let mut module = empty_module(crate::atom::Atom::MODULE);
module.function_table = vec![
(1, crate::atom::Atom::OK, 0),
(5, crate::atom::Atom::BADARG, 1),
(9, crate::atom::Atom::FLUSH, 2),
];
assert_eq!(module.function_at_ip(0), None);
assert_eq!(module.function_at_ip(1), Some((crate::atom::Atom::OK, 0)));
assert_eq!(module.function_at_ip(4), Some((crate::atom::Atom::OK, 0)));
assert_eq!(
module.function_at_ip(5),
Some((crate::atom::Atom::BADARG, 1))
);
assert_eq!(
module.function_at_ip(12),
Some((crate::atom::Atom::FLUSH, 2))
);
}
#[test]
fn line_at_ip_resolves_last_preceding_line_marker() {
let mut module = empty_module(crate::atom::Atom::MODULE);
module.line_info = vec![
LineInfo { file: 0, line: 10 },
LineInfo { file: 0, line: 20 },
];
module.line_table = vec![(2, 0), (6, 1), (10, 99)];
assert_eq!(module.line_at_ip(1), None);
assert_eq!(module.line_at_ip(2), Some(10));
assert_eq!(module.line_at_ip(5), Some(10));
assert_eq!(module.line_at_ip(6), Some(20));
assert_eq!(module.line_at_ip(10), None);
}
#[test]
fn registry_stores_and_replaces_modules_by_name() {
let atoms = AtomTable::new();
let module_name = atoms.intern("sample");
let registry = ModuleRegistry::new();
let first = registry.insert(empty_module(module_name));
let mut replacement = empty_module(module_name);
replacement.code.push(crate::loader::Instruction::Return);
let second = registry.insert(replacement);
assert!(std::sync::Arc::ptr_eq(
®istry.lookup(module_name).expect("module loaded"),
&second
));
assert!(!std::sync::Arc::ptr_eq(&first, &second));
}
#[test]
fn registry_retains_only_current_and_previous_old_versions() {
let atoms = AtomTable::new();
let module_name = atoms.intern("sample");
let registry = ModuleRegistry::new();
let v1 = registry.insert(empty_module(module_name));
assert_eq!(registry.module_version_count(module_name), 1);
assert!(registry.lookup_old(module_name).is_none());
assert!(std::sync::Arc::ptr_eq(
®istry.lookup(module_name).expect("v1 current"),
&v1
));
let mut second = empty_module(module_name);
second.code.push(crate::loader::Instruction::Return);
let v2 = registry.insert(second);
assert_eq!(registry.module_version_count(module_name), 2);
assert!(std::sync::Arc::ptr_eq(
®istry.lookup(module_name).expect("v2 current"),
&v2
));
assert!(std::sync::Arc::ptr_eq(
®istry.lookup_old(module_name).expect("v1 old"),
&v1
));
let mut third = empty_module(module_name);
third.code.push(crate::loader::Instruction::Return);
third.code.push(crate::loader::Instruction::Return);
let v3 = registry.insert(third);
assert_eq!(registry.module_version_count(module_name), 2);
assert!(std::sync::Arc::ptr_eq(
®istry.lookup(module_name).expect("v3 current"),
&v3
));
assert!(std::sync::Arc::ptr_eq(
®istry.lookup_old(module_name).expect("v2 old"),
&v2
));
assert_eq!(v1.generation(), 1);
assert_eq!(v2.generation(), 2);
assert_eq!(v3.generation(), 3);
}
#[test]
fn generations_are_tracked_per_module_name() {
let atoms = AtomTable::new();
let first_name = atoms.intern("first");
let second_name = atoms.intern("second");
let registry = ModuleRegistry::new();
let first_v1 = registry.insert(empty_module(first_name));
let second_v1 = registry.insert(empty_module(second_name));
let first_v2 = registry.insert(empty_module(first_name));
assert_eq!(first_v1.generation(), 1);
assert_eq!(second_v1.generation(), 1);
assert_eq!(first_v2.generation(), 2);
}
#[test]
fn purge_old_requires_no_external_references() {
let atoms = AtomTable::new();
let module_name = atoms.intern("sample");
let registry = ModuleRegistry::new();
registry.insert(empty_module(module_name));
registry.insert(empty_module(module_name));
let old_ref = registry.lookup_old(module_name).expect("old version");
assert!(matches!(
registry.purge_old(module_name),
Err(PurgeError::StillReferenced { module, ref_count })
if module == module_name && ref_count >= 2
));
drop(old_ref);
assert_eq!(registry.purge_old(module_name), Ok(()));
assert!(registry.lookup_old(module_name).is_none());
assert_eq!(registry.module_version_count(module_name), 1);
assert_eq!(
registry.purge_old(module_name),
Err(PurgeError::NoOldVersion {
module: module_name
})
);
}
#[test]
fn registry_lookup_unloaded_module_returns_none() {
let atoms = AtomTable::new();
let registry = ModuleRegistry::new();
assert!(registry.lookup(atoms.intern("missing")).is_none());
}
#[test]
fn lookup_mfa_returns_code_pointer_for_export() {
let atoms = AtomTable::new();
let module_name = atoms.intern("sample");
let function = atoms.intern("main");
let registry = ModuleRegistry::new();
let mut module = empty_module(module_name);
module.exports.insert((function, 0), 7);
registry.insert(module);
let pointer = registry
.lookup_mfa(module_name, function, 0)
.expect("exported function");
assert_eq!(pointer.label, 7);
assert_eq!(pointer.module.name, module_name);
assert_eq!(pointer.generation, 1);
}
#[test]
fn module_resolves_labels_from_index() {
let atoms = AtomTable::new();
let mut module = empty_module(atoms.intern("sample"));
module.code = vec![
crate::loader::Instruction::Return,
crate::loader::Instruction::Label { label: 10 },
crate::loader::Instruction::Return,
crate::loader::Instruction::Label { label: 20 },
];
module.label_index = label_index(&module.code);
assert_eq!(module.label_ip(10), Ok(1));
assert_eq!(module.label_ip(20), Ok(3));
assert_eq!(
module.label_ip(30),
Err(ExecError::InvalidLabel { label: 30 })
);
}
#[test]
fn module_resolves_exports_to_instruction_indices() {
let atoms = AtomTable::new();
let function = atoms.intern("main");
let mut module = empty_module(atoms.intern("sample"));
module.code = vec![
crate::loader::Instruction::Return,
crate::loader::Instruction::Label { label: 10 },
crate::loader::Instruction::Return,
crate::loader::Instruction::Label { label: 20 },
];
module.label_index = label_index(&module.code);
module.exports.insert((function, 0), 20);
assert_eq!(module.export_ip(function, 0), Ok(3));
}
#[test]
fn module_reports_undef_for_missing_export() {
let atoms = AtomTable::new();
let module_name = atoms.intern("sample");
let function = atoms.intern("missing");
let module = empty_module(module_name);
assert!(matches!(
module.export_ip(function, 0),
Err(ExecError::Undef {
module,
function: undef_function,
arity: 0,
}) if module == module_name && undef_function == function
));
}
#[test]
fn module_reports_invalid_label_for_export_missing_from_index() {
let atoms = AtomTable::new();
let function = atoms.intern("main");
let mut module = empty_module(atoms.intern("sample"));
module.exports.insert((function, 0), 99);
assert_eq!(
module.export_ip(function, 0),
Err(ExecError::InvalidLabel { label: 99 })
);
}
#[test]
fn find_lambda_by_id_resolves_reordered_lambda_tables() {
let atoms = AtomTable::new();
let module_name = atoms.intern("sample");
let first_fun = atoms.intern("first@anon");
let second_fun = atoms.intern("second@anon");
let first_id = crate::loader::lambda_unique_id(&atoms, module_name, first_fun, 1, 2)
.expect("first id");
let second_id = crate::loader::lambda_unique_id(&atoms, module_name, second_fun, 0, 0)
.expect("second id");
let mut v1 = empty_module(module_name);
v1.lambdas = vec![
LambdaEntry {
function: first_fun,
arity: 1,
label: 10,
num_free: 2,
unique_id: first_id,
},
LambdaEntry {
function: second_fun,
arity: 0,
label: 20,
num_free: 0,
unique_id: second_id,
},
];
let mut v2 = empty_module(module_name);
v2.lambdas = vec![
LambdaEntry {
function: second_fun,
arity: 0,
label: 200,
num_free: 0,
unique_id: second_id,
},
LambdaEntry {
function: first_fun,
arity: 1,
label: 100,
num_free: 2,
unique_id: first_id,
},
];
assert_eq!(
v1.find_lambda_by_id(first_id).map(|lambda| lambda.label),
Some(10)
);
assert_eq!(
v2.find_lambda_by_id(first_id).map(|lambda| lambda.label),
Some(100)
);
assert_eq!(
v1.find_lambda_by_id(second_id).map(|lambda| lambda.label),
Some(20)
);
assert_eq!(
v2.find_lambda_by_id(second_id).map(|lambda| lambda.label),
Some(200)
);
}
#[test]
fn lookup_mfa_reports_undef_for_missing_targets() {
let atoms = AtomTable::new();
let module_name = atoms.intern("sample");
let function = atoms.intern("main");
let other = atoms.intern("other");
let registry = ModuleRegistry::new();
registry.insert(empty_module(module_name));
assert!(matches!(
registry.lookup_mfa(other, function, 0),
Err(ExecError::Undef {
module,
function: undef_function,
arity: 0,
}) if module == other && undef_function == function
));
assert!(matches!(
registry.lookup_mfa(module_name, function, 0),
Err(ExecError::Undef {
module,
function: undef_function,
arity: 0,
}) if module == module_name && undef_function == function
));
assert!(matches!(
registry.lookup_mfa(module_name, function, 1),
Err(ExecError::Undef {
module,
function: undef_function,
arity: 1,
}) if module == module_name && undef_function == function
));
}
}