Skip to main content

panproto_vcs/
expr.rs

1//! Content-addressed storage for expressions.
2//!
3//! Expressions are serialized via `MessagePack` and stored as blob objects
4//! in the VCS, following the same pattern used for schemas and protocols.
5
6use panproto_expr::Expr;
7
8use crate::error::VcsError;
9use crate::hash::ObjectId;
10use crate::object::Object;
11use crate::store::Store;
12
13/// Serialize an expression and store it as a content-addressed object.
14///
15/// Returns the `ObjectId` (blake3 hash) of the stored expression. If an
16/// identical expression already exists in the store, this is a no-op that
17/// returns the existing ID.
18///
19/// # Errors
20///
21/// Returns an error if serialization or storage fails.
22pub fn store_expr(store: &mut dyn Store, expr: &Expr) -> Result<ObjectId, VcsError> {
23    let object = Object::Expr(Box::new(expr.clone()));
24    store.put(&object)
25}
26
27/// Load an expression from the store by its content-addressed ID.
28///
29/// # Errors
30///
31/// Returns [`VcsError::ObjectNotFound`] if no object exists with the
32/// given ID, or [`VcsError::WrongObjectType`] if the object is not an
33/// expression.
34pub fn load_expr(store: &dyn Store, id: &ObjectId) -> Result<Expr, VcsError> {
35    match store.get(id)? {
36        Object::Expr(expr) => Ok(*expr),
37        other => Err(VcsError::WrongObjectType {
38            expected: "expr",
39            found: other.type_name(),
40        }),
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use panproto_expr::{BuiltinOp, Literal};
48
49    use crate::MemStore;
50
51    #[test]
52    fn store_load_round_trip() -> Result<(), VcsError> {
53        let mut store = MemStore::new();
54        let expr = Expr::let_in(
55            "x",
56            Expr::Lit(Literal::Int(42)),
57            Expr::builtin(
58                BuiltinOp::Add,
59                vec![Expr::var("x"), Expr::Lit(Literal::Int(1))],
60            ),
61        );
62
63        let id = store_expr(&mut store, &expr)?;
64        let loaded = load_expr(&store, &id)?;
65        assert_eq!(loaded, expr);
66        Ok(())
67    }
68
69    #[test]
70    fn store_idempotent() -> Result<(), VcsError> {
71        let mut store = MemStore::new();
72        let expr = Expr::Lit(Literal::Str("hello".into()));
73
74        let id1 = store_expr(&mut store, &expr)?;
75        let id2 = store_expr(&mut store, &expr)?;
76        assert_eq!(id1, id2);
77        Ok(())
78    }
79
80    #[test]
81    fn load_wrong_type_returns_error() -> Result<(), VcsError> {
82        use std::collections::HashMap;
83
84        let mut store = MemStore::new();
85        let schema = panproto_schema::Schema {
86            protocol: "test".into(),
87            vertices: HashMap::new(),
88            edges: HashMap::new(),
89            hyper_edges: HashMap::new(),
90            constraints: HashMap::new(),
91            required: HashMap::new(),
92            nsids: HashMap::new(),
93            entries: Vec::new(),
94            variants: HashMap::new(),
95            orderings: HashMap::new(),
96            recursion_points: HashMap::new(),
97            spans: HashMap::new(),
98            usage_modes: HashMap::new(),
99            nominal: HashMap::new(),
100            coercions: HashMap::new(),
101            mergers: HashMap::new(),
102            defaults: HashMap::new(),
103            policies: HashMap::new(),
104            outgoing: HashMap::new(),
105            incoming: HashMap::new(),
106            between: HashMap::new(),
107        };
108        let id = crate::tree::store_schema_as_tree(&mut store, schema)?;
109        let result = load_expr(&store, &id);
110        assert!(matches!(
111            result,
112            Err(VcsError::WrongObjectType {
113                expected: "expr",
114                ..
115            })
116        ));
117        Ok(())
118    }
119
120    #[test]
121    fn load_nonexistent_returns_error() {
122        let store = MemStore::new();
123        let result = load_expr(&store, &ObjectId::ZERO);
124        assert!(matches!(result, Err(VcsError::ObjectNotFound { .. })));
125    }
126
127    #[test]
128    fn different_exprs_get_different_ids() -> Result<(), VcsError> {
129        let mut store = MemStore::new();
130        let e1 = Expr::Lit(Literal::Int(1));
131        let e2 = Expr::Lit(Literal::Int(2));
132
133        let id1 = store_expr(&mut store, &e1)?;
134        let id2 = store_expr(&mut store, &e2)?;
135        assert_ne!(id1, id2);
136        Ok(())
137    }
138
139    #[test]
140    fn complex_expr_round_trip() -> Result<(), VcsError> {
141        let mut store = MemStore::new();
142        let expr = Expr::lam(
143            "record",
144            Expr::builtin(
145                BuiltinOp::Concat,
146                vec![
147                    Expr::field(Expr::var("record"), "first_name"),
148                    Expr::Lit(Literal::Str(" ".into())),
149                ],
150            ),
151        );
152
153        let id = store_expr(&mut store, &expr)?;
154        let loaded = load_expr(&store, &id)?;
155        assert_eq!(loaded, expr);
156        Ok(())
157    }
158}