use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::{hash_def, verify_module, Def, Module, WeftError, WeftHash};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Package {
pub name: String,
pub exports: BTreeMap<String, WeftHash>,
pub defs: BTreeMap<WeftHash, Def>,
}
impl Package {
pub fn build(
name: &str,
defs: Vec<Def>,
exports: Vec<(&str, usize)>,
) -> Result<Self, WeftError> {
let hashed: Vec<(WeftHash, Def)> = defs.into_iter().map(|d| (hash_def(&d), d)).collect();
let mut map = BTreeMap::new();
let mut ex = BTreeMap::new();
for (petname, idx) in exports {
let (h, _) = hashed.get(idx).ok_or(WeftError::UnknownEntry)?;
ex.insert(petname.to_string(), *h);
}
for (h, d) in hashed {
map.insert(h, d);
}
let pkg = Package {
name: name.to_string(),
exports: ex,
defs: map,
};
pkg.verify()?;
Ok(pkg)
}
pub fn verify(&self) -> Result<(), WeftError> {
for (h, d) in &self.defs {
if hash_def(d) != *h {
return Err(WeftError::Type(format!("def {h} does not hash to its key")));
}
}
for (name, h) in &self.exports {
if !self.defs.contains_key(h) {
return Err(WeftError::Type(format!(
"export '{name}' points outside the package"
)));
}
}
if let Some(first) = self
.exports
.values()
.next()
.or_else(|| self.defs.keys().next())
{
let m = Module {
defs: self.defs.clone(),
entry: *first,
};
verify_module(&m)?;
}
Ok(())
}
pub fn export(&self, petname: &str) -> Option<WeftHash> {
self.exports.get(petname).copied()
}
}
pub fn link(
packages: &[Package],
local_defs: Vec<Def>,
entry_index: usize,
) -> Result<Module, WeftError> {
let mut defs: BTreeMap<WeftHash, Def> = BTreeMap::new();
for p in packages {
p.verify()?;
for (h, d) in &p.defs {
defs.insert(*h, d.clone());
}
}
let mut entry = None;
for (i, d) in local_defs.into_iter().enumerate() {
let h = hash_def(&d);
if i == entry_index {
entry = Some(h);
}
defs.insert(h, d);
}
let entry = entry.ok_or(WeftError::UnknownEntry)?;
let module = Module { defs, entry };
let mut keep: BTreeSet<WeftHash> = BTreeSet::new();
let mut stack = vec![entry];
while let Some(h) = stack.pop() {
if !keep.insert(h) {
continue;
}
if let Some(d) = module.defs.get(&h) {
for c in crate::callees_of(&d.body) {
stack.push(c);
}
}
}
let module = Module {
defs: module
.defs
.into_iter()
.filter(|(h, _)| keep.contains(h))
.collect(),
entry,
};
verify_module(&module)?;
Ok(module)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{PrimOp, Term, Ty};
fn double_def() -> Def {
Def {
params: vec![Ty::Int],
ret: Ty::Int,
effects: BTreeSet::new(),
body: Term::Prim(PrimOp::Mul, vec![Term::Var(0), Term::Int(2)]),
pre: None,
post: None,
}
}
#[test]
fn packages_build_verify_and_link_by_hash() {
let pkg = Package::build("math-basics", vec![double_def()], vec![("double", 0)]).unwrap();
let double = pkg.export("double").expect("exported");
let entry = Def {
params: vec![],
ret: Ty::Int,
effects: BTreeSet::new(),
body: Term::Call(double, vec![Term::Int(21)]),
pre: None,
post: None,
};
let module = link(&[pkg], vec![entry], 0).expect("links + verifies");
let out = crate::eval_call(&module, module.entry, vec![], 10_000).unwrap();
assert_eq!(out.value, crate::Value::Int(42));
}
#[test]
fn linking_is_a_set_union_no_diamond_problem() {
let a = Package::build("a", vec![double_def()], vec![("dbl", 0)]).unwrap();
let b = Package::build("b", vec![double_def()], vec![("twice", 0)]).unwrap();
assert_eq!(
a.export("dbl"),
b.export("twice"),
"same bytes, same identity"
);
let entry = Def {
params: vec![],
ret: Ty::Int,
effects: BTreeSet::new(),
body: Term::Call(a.export("dbl").unwrap(), vec![Term::Int(3)]),
pre: None,
post: None,
};
let m = link(&[a, b], vec![entry], 0).unwrap();
assert_eq!(m.defs.len(), 2, "one shared def + the entry");
}
#[test]
fn tampered_defs_are_refused() {
let mut pkg = Package::build("m", vec![double_def()], vec![("double", 0)]).unwrap();
let key = *pkg.defs.keys().next().unwrap();
pkg.defs.get_mut(&key).unwrap().body = Term::Int(666);
assert!(pkg.verify().is_err(), "hash mismatch must be fatal");
}
}