Skip to main content

weft_lang/
pack.rs

1//! # weft-pack — packages for a content-addressed language
2//!
3//! npm distributes *names that move*; weft-pack distributes **hashes that
4//! can't**. A definition's name IS the hash of its canonical bytes, so:
5//!
6//! - a package can never be *changed under you* (no left-pad: unpublishing a
7//!   hash breaks nothing for anyone who has the bytes, and the bytes verify),
8//! - two packages can never *conflict* (defs are keyed by hash; the same def
9//!   pulled in twice is literally one entry),
10//! - names are **petnames**: a package's `exports` maps human names to
11//!   hashes purely for authoring convenience — the linked module never
12//!   contains a name, only hashes,
13//! - and there is no trust problem a registry must solve: **verification is
14//!   local**. Whoever serves you the bytes, the verifier decides.
15//!
16//! A registry is therefore just a static file host — a directory, a CDN, or
17//! a Thread host's `.well-known/weft/` — serving `<name>.weftpack.json`.
18//! The `weftpack` CLI (crates/weft-pack) hashes, verifies, links, and fetches.
19
20use std::collections::{BTreeMap, BTreeSet};
21
22use serde::{Deserialize, Serialize};
23
24use crate::{hash_def, verify_module, Def, Module, WeftError, WeftHash};
25
26/// A distributable set of definitions plus the petnames it exports.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Package {
29    /// Human-facing package name (informational — identity lives in hashes).
30    pub name: String,
31    /// Petname → definition hash. What `use`-ing this package gives you.
32    pub exports: BTreeMap<String, WeftHash>,
33    /// Every definition the exports (transitively) need, keyed by hash.
34    pub defs: BTreeMap<WeftHash, Def>,
35}
36
37impl Package {
38    /// Build a package from defs + `(petname, def_index)` export pairs.
39    pub fn build(
40        name: &str,
41        defs: Vec<Def>,
42        exports: Vec<(&str, usize)>,
43    ) -> Result<Self, WeftError> {
44        let hashed: Vec<(WeftHash, Def)> = defs.into_iter().map(|d| (hash_def(&d), d)).collect();
45        let mut map = BTreeMap::new();
46        let mut ex = BTreeMap::new();
47        for (petname, idx) in exports {
48            let (h, _) = hashed.get(idx).ok_or(WeftError::UnknownEntry)?;
49            ex.insert(petname.to_string(), *h);
50        }
51        for (h, d) in hashed {
52            map.insert(h, d);
53        }
54        let pkg = Package {
55            name: name.to_string(),
56            exports: ex,
57            defs: map,
58        };
59        pkg.verify()?;
60        Ok(pkg)
61    }
62
63    /// A package must be **self-verifying**: every def's hash matches its
64    /// bytes, every export points at a def, and the whole set verifies as a
65    /// module (types, effects, fuel — the full trust boundary, locally).
66    pub fn verify(&self) -> Result<(), WeftError> {
67        for (h, d) in &self.defs {
68            if hash_def(d) != *h {
69                return Err(WeftError::Type(format!("def {h} does not hash to its key")));
70            }
71        }
72        for (name, h) in &self.exports {
73            if !self.defs.contains_key(h) {
74                return Err(WeftError::Type(format!(
75                    "export '{name}' points outside the package"
76                )));
77            }
78        }
79        // Verify the def set as a module (entry choice is arbitrary).
80        if let Some(first) = self
81            .exports
82            .values()
83            .next()
84            .or_else(|| self.defs.keys().next())
85        {
86            let m = Module {
87                defs: self.defs.clone(),
88                entry: *first,
89            };
90            verify_module(&m)?;
91        }
92        Ok(())
93    }
94
95    /// The export's hash, by petname.
96    pub fn export(&self, petname: &str) -> Option<WeftHash> {
97        self.exports.get(petname).copied()
98    }
99}
100
101/// Link packages + local defs into one verified [`Module`]. Because defs are
102/// content-addressed, linking is a **set union** — no resolution order, no
103/// conflicts, no diamond problem: the same hash from two packages is one def.
104pub fn link(
105    packages: &[Package],
106    local_defs: Vec<Def>,
107    entry_index: usize,
108) -> Result<Module, WeftError> {
109    let mut defs: BTreeMap<WeftHash, Def> = BTreeMap::new();
110    for p in packages {
111        p.verify()?;
112        for (h, d) in &p.defs {
113            defs.insert(*h, d.clone());
114        }
115    }
116    let mut entry = None;
117    for (i, d) in local_defs.into_iter().enumerate() {
118        let h = hash_def(&d);
119        if i == entry_index {
120            entry = Some(h);
121        }
122        defs.insert(h, d);
123    }
124    let entry = entry.ok_or(WeftError::UnknownEntry)?;
125    let module = Module { defs, entry };
126    // Trim to the entry's transitive closure — ship only what's reachable.
127    let mut keep: BTreeSet<WeftHash> = BTreeSet::new();
128    let mut stack = vec![entry];
129    while let Some(h) = stack.pop() {
130        if !keep.insert(h) {
131            continue;
132        }
133        if let Some(d) = module.defs.get(&h) {
134            for c in crate::callees_of(&d.body) {
135                stack.push(c);
136            }
137        }
138    }
139    let module = Module {
140        defs: module
141            .defs
142            .into_iter()
143            .filter(|(h, _)| keep.contains(h))
144            .collect(),
145        entry,
146    };
147    verify_module(&module)?;
148    Ok(module)
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::{PrimOp, Term, Ty};
155
156    fn double_def() -> Def {
157        Def {
158            params: vec![Ty::Int],
159            ret: Ty::Int,
160            effects: BTreeSet::new(),
161            body: Term::Prim(PrimOp::Mul, vec![Term::Var(0), Term::Int(2)]),
162            pre: None,
163            post: None,
164        }
165    }
166
167    #[test]
168    fn packages_build_verify_and_link_by_hash() {
169        let pkg = Package::build("math-basics", vec![double_def()], vec![("double", 0)]).unwrap();
170        let double = pkg.export("double").expect("exported");
171        // A consumer module calls the package def BY HASH — no names on the wire.
172        let entry = Def {
173            params: vec![],
174            ret: Ty::Int,
175            effects: BTreeSet::new(),
176            body: Term::Call(double, vec![Term::Int(21)]),
177            pre: None,
178            post: None,
179        };
180        let module = link(&[pkg], vec![entry], 0).expect("links + verifies");
181        let out = crate::eval_call(&module, module.entry, vec![], 10_000).unwrap();
182        assert_eq!(out.value, crate::Value::Int(42));
183    }
184
185    #[test]
186    fn linking_is_a_set_union_no_diamond_problem() {
187        // Two packages exporting the SAME def (same hash) — linking dedups.
188        let a = Package::build("a", vec![double_def()], vec![("dbl", 0)]).unwrap();
189        let b = Package::build("b", vec![double_def()], vec![("twice", 0)]).unwrap();
190        assert_eq!(
191            a.export("dbl"),
192            b.export("twice"),
193            "same bytes, same identity"
194        );
195        let entry = Def {
196            params: vec![],
197            ret: Ty::Int,
198            effects: BTreeSet::new(),
199            body: Term::Call(a.export("dbl").unwrap(), vec![Term::Int(3)]),
200            pre: None,
201            post: None,
202        };
203        let m = link(&[a, b], vec![entry], 0).unwrap();
204        assert_eq!(m.defs.len(), 2, "one shared def + the entry");
205    }
206
207    #[test]
208    fn tampered_defs_are_refused() {
209        let mut pkg = Package::build("m", vec![double_def()], vec![("double", 0)]).unwrap();
210        // Flip the body of the stored def without re-keying it.
211        let key = *pkg.defs.keys().next().unwrap();
212        pkg.defs.get_mut(&key).unwrap().body = Term::Int(666);
213        assert!(pkg.verify().is_err(), "hash mismatch must be fatal");
214    }
215}