Skip to main content

sui_eval/
convert.rs

1//! Bidirectional conversion between bytecode VM values and tree-walker values.
2//!
3//! The bytecode VM (`sui_bytecode`) uses `VMValue` / `StringKeyedValue`,
4//! while the tree-walker (`sui_eval`) uses `Value` / `NixAttrs`. This module
5//! bridges the two representations so that the VM can be wired as an
6//! alternative evaluation backend.
7
8use crate::value::{NixAttrs, SmolStr, Value, Rc};
9use sui_bytecode::value::{StringKeyedValue, VMValue};
10use sui_intern::Interner;
11
12/// Convert a `StringKeyedValue` (from the bytecode VM) to a tree-walker `Value`.
13///
14/// This is the primary conversion path: the VM evaluates an expression and
15/// returns a `StringKeyedValue` (fully resolved string keys), which we then
16/// convert to the `Value` type used by the rest of sui (build, orchestrate, etc.).
17#[must_use]
18pub fn string_keyed_to_eval(sk: &StringKeyedValue) -> Value {
19    match sk {
20        StringKeyedValue::Null => Value::Null,
21        StringKeyedValue::Bool(b) => Value::Bool(*b),
22        StringKeyedValue::Int(n) => Value::Int(*n),
23        StringKeyedValue::Float(f) => Value::Float(*f),
24        StringKeyedValue::String(s) => Value::string(s.clone()),
25        StringKeyedValue::Path(p) => Value::Path(Box::new(SmolStr::from(p.as_str()))),
26        StringKeyedValue::List(items) => {
27            Value::list(items.iter().map(string_keyed_to_eval).collect())
28        }
29        StringKeyedValue::Attrs(map) => {
30            let mut attrs = NixAttrs::with_capacity(map.len());
31            for (k, v) in map {
32                attrs.insert(k.clone(), string_keyed_to_eval(v));
33            }
34            Value::Attrs(Rc::new(attrs))
35        }
36        StringKeyedValue::Lambda => Value::Null, // bare lambdas cannot cross the boundary
37        StringKeyedValue::Callable(cb) => {
38            // Wrap the bridge callback as a tree-walker BuiltinFn.
39            let cb_clone = std::rc::Rc::clone(cb);
40            Value::Builtin(Box::new(crate::value::BuiltinFn {
41                name: "<bridge-fn>",
42                func: std::rc::Rc::new(move |args: &[Value]| {
43                    let arg = args.first().cloned().unwrap_or(Value::Null);
44                    let sk_arg = crate::eval_to_string_keyed(&arg);
45                    let sk_result = cb_clone(sk_arg)
46                        .map_err(|e| crate::value::EvalError::TypeError(e))?;
47                    Ok(string_keyed_to_eval(&sk_result))
48                }),
49            }))
50        }
51        StringKeyedValue::Thunk(cb) => {
52            // Wrap the StringKeyedValue thunk as a tree-walker native thunk.
53            let cb_clone = std::rc::Rc::clone(cb);
54            Value::Thunk(crate::value::Thunk::new_native(move || {
55                let sk_val = cb_clone()
56                    .map_err(|e| crate::value::EvalError::TypeError(e))?;
57                Ok(string_keyed_to_eval(&sk_val))
58            }))
59        }
60    }
61}
62
63/// Convert a `VMValue` (with interned keys) to a tree-walker `Value`.
64///
65/// Requires access to the interner to resolve `Symbol` keys back to strings.
66#[must_use]
67pub fn vm_to_eval(vm: &VMValue, interner: &Interner) -> Value {
68    let sk = vm.to_string_keyed(interner);
69    string_keyed_to_eval(&sk)
70}
71
72/// Convert a tree-walker `Value` to a `VMValue` for consumption by the VM.
73///
74/// Closures and builtins cannot cross the boundary (converted to Null).
75/// Thunks are wrapped lazily: already-evaluated thunks have their value
76/// extracted, while unevaluated thunks become `VMThunk(NativeCallback)`
77/// so they are only forced when the VM accesses the value.
78#[must_use]
79pub fn eval_to_vm(val: &Value, interner: &mut Interner) -> VMValue {
80    match val {
81        Value::Null => VMValue::Null,
82        Value::Bool(b) => VMValue::Bool(*b),
83        Value::Int(n) => VMValue::Int(*n),
84        Value::Float(f) => VMValue::Float(*f),
85        Value::String(s) => VMValue::String(s.chars.to_string()),
86        Value::Path(p) => VMValue::Path(p.to_string()),
87        Value::List(items) => {
88            VMValue::List(items.iter().map(|v| eval_to_vm(v, interner)).collect())
89        }
90        Value::Attrs(attrs) => {
91            let mut map = std::collections::BTreeMap::new();
92            for (k, v) in attrs.iter() {
93                let sym = interner.intern(&k);
94                map.insert(sym, eval_to_vm(v, interner));
95            }
96            VMValue::Attrs(map)
97        }
98        // Closures and builtins cross the boundary via bridge callbacks.
99        Value::Lambda(_) | Value::Builtin(_) => {
100            let sk = crate::eval_to_string_keyed(val);
101            sui_bytecode::builtins::string_keyed_to_vmvalue(&sk, interner)
102        }
103        Value::Thunk(t) => {
104            if t.is_evaluated() {
105                match t.force(&|e, env| crate::eval::eval_expr(e, env)) {
106                    Ok(v) => eval_to_vm(&v, interner),
107                    Err(_) => VMValue::Null,
108                }
109            } else {
110                // Wrap the tree-walker thunk in a NativeCallback VMThunk.
111                let thunk_clone = t.clone();
112                let cb: std::rc::Rc<dyn Fn() -> Result<StringKeyedValue, String>> =
113                    std::rc::Rc::new(move || {
114                        let forced = thunk_clone
115                            .force(&|e, env| crate::eval::eval_expr(e, env))
116                            .map_err(|e| e.to_string())?;
117                        Ok(crate::eval_to_string_keyed(&forced))
118                    });
119                VMValue::Thunk(sui_bytecode::VMThunk {
120                    state: std::rc::Rc::new(std::cell::Cell::new(Some(
121                        sui_bytecode::value::ThunkState::NativeCallback(cb),
122                    ))),
123                })
124            }
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn roundtrip_scalar_null() {
135        let sk = StringKeyedValue::Null;
136        let val = string_keyed_to_eval(&sk);
137        assert_eq!(val, Value::Null);
138    }
139
140    #[test]
141    fn roundtrip_scalar_bool() {
142        assert_eq!(string_keyed_to_eval(&StringKeyedValue::Bool(true)), Value::Bool(true));
143        assert_eq!(string_keyed_to_eval(&StringKeyedValue::Bool(false)), Value::Bool(false));
144    }
145
146    #[test]
147    fn roundtrip_scalar_int() {
148        assert_eq!(string_keyed_to_eval(&StringKeyedValue::Int(42)), Value::Int(42));
149    }
150
151    #[test]
152    fn roundtrip_scalar_float() {
153        assert_eq!(string_keyed_to_eval(&StringKeyedValue::Float(3.14)), Value::Float(3.14));
154    }
155
156    #[test]
157    fn roundtrip_string() {
158        let sk = StringKeyedValue::String("hello".to_string());
159        let val = string_keyed_to_eval(&sk);
160        assert_eq!(val, Value::string("hello"));
161    }
162
163    #[test]
164    fn roundtrip_path() {
165        let sk = StringKeyedValue::Path("/tmp/x".to_string());
166        let val = string_keyed_to_eval(&sk);
167        assert_eq!(val, Value::Path(Box::new(SmolStr::from("/tmp/x"))));
168    }
169
170    #[test]
171    fn roundtrip_list() {
172        let sk = StringKeyedValue::List(vec![
173            StringKeyedValue::Int(1),
174            StringKeyedValue::Int(2),
175        ]);
176        let val = string_keyed_to_eval(&sk);
177        assert_eq!(val, Value::list(vec![Value::Int(1), Value::Int(2)]));
178    }
179
180    #[test]
181    fn roundtrip_attrs() {
182        let mut map = std::collections::BTreeMap::new();
183        map.insert("a".to_string(), StringKeyedValue::Int(1));
184        map.insert("b".to_string(), StringKeyedValue::String("hi".to_string()));
185        let sk = StringKeyedValue::Attrs(map);
186        let val = string_keyed_to_eval(&sk);
187        match val {
188            Value::Attrs(attrs) => {
189                assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
190                assert_eq!(attrs.get("b"), Some(&Value::string("hi")));
191            }
192            _ => panic!("expected Attrs"),
193        }
194    }
195
196    #[test]
197    fn lambda_becomes_null() {
198        let sk = StringKeyedValue::Lambda;
199        let val = string_keyed_to_eval(&sk);
200        assert_eq!(val, Value::Null);
201    }
202
203    #[test]
204    fn vm_to_eval_with_interner() {
205        let mut interner = Interner::new();
206        let key = interner.intern("x");
207        let mut attrs = std::collections::BTreeMap::new();
208        attrs.insert(key, VMValue::Int(42));
209        let vm_val = VMValue::Attrs(attrs);
210        let eval_val = vm_to_eval(&vm_val, &interner);
211        match eval_val {
212            Value::Attrs(a) => assert_eq!(a.get("x"), Some(&Value::Int(42))),
213            _ => panic!("expected Attrs"),
214        }
215    }
216
217    #[test]
218    fn eval_to_vm_scalars() {
219        let mut interner = Interner::new();
220        assert_eq!(eval_to_vm(&Value::Null, &mut interner), VMValue::Null);
221        assert_eq!(eval_to_vm(&Value::Bool(true), &mut interner), VMValue::Bool(true));
222        assert_eq!(eval_to_vm(&Value::Int(7), &mut interner), VMValue::Int(7));
223        assert_eq!(eval_to_vm(&Value::Float(1.5), &mut interner), VMValue::Float(1.5));
224    }
225
226    #[test]
227    fn eval_to_vm_string() {
228        let mut interner = Interner::new();
229        let val = Value::string("test");
230        let vm = eval_to_vm(&val, &mut interner);
231        assert_eq!(vm, VMValue::String("test".to_string()));
232    }
233
234    #[test]
235    fn eval_to_vm_attrs() {
236        let mut interner = Interner::new();
237        let mut attrs = NixAttrs::new();
238        attrs.insert("key".to_string(), Value::Int(99));
239        let val = Value::Attrs(Rc::new(attrs));
240        let vm = eval_to_vm(&val, &mut interner);
241        let sk = vm.to_string_keyed(&interner);
242        match sk {
243            StringKeyedValue::Attrs(map) => {
244                assert_eq!(map.get("key"), Some(&StringKeyedValue::Int(99)));
245            }
246            _ => panic!("expected Attrs"),
247        }
248    }
249}