Skip to main content

sui_bytecode/
bridge.rs

1//! Builtin bridge: allows the bytecode VM to call tree-walker builtins.
2//!
3//! The VM has native implementations for ~103 builtins, but nixpkgs needs
4//! ~119+. Instead of reimplementing every builtin in the VM's value system,
5//! this module provides a callback mechanism that delegates to the
6//! tree-walker's builtins for any builtin the VM doesn't handle natively.
7//!
8//! # Architecture
9//!
10//! Follows the same pattern as `set_flake_resolver` in `vm.rs`:
11//! a thread-local callback with an RAII guard for cleanup.
12//!
13//! ```text
14//! sui-eval (BytecodeEvaluator)
15//!   │
16//!   ├─ set_builtin_bridge(callback)  ← installs bridge before VM eval
17//!   │
18//!   └─ VM execution
19//!       │
20//!       └─ CallBuiltin for "getEnv" etc.
21//!           │
22//!           └─ BuiltinRegistry stub → call_builtin_bridge("getEnv", args)
23//!               │
24//!               └─ callback converts VMValue↔Value, calls tree-walker builtin
25//! ```
26
27use std::cell::RefCell;
28
29use crate::value::StringKeyedValue;
30
31/// Callback type for bridging tree-walker builtins into the VM.
32///
33/// Takes: builtin name, args as `StringKeyedValue` (interner-free).
34/// Returns: `StringKeyedValue` result or error string.
35///
36/// We use `StringKeyedValue` instead of `VMValue` because:
37/// - It doesn't require an interner for key resolution
38/// - `sui-eval` already has `string_keyed_to_eval` / `eval_to_string_keyed`
39///   conversion functions
40/// - The bridge callback runs in `sui-eval` context where the tree-walker
41///   value types are available
42pub type BuiltinBridgeFn = Box<dyn Fn(&str, Vec<StringKeyedValue>) -> Result<StringKeyedValue, String>>;
43
44thread_local! {
45    static BUILTIN_BRIDGE: RefCell<Option<BuiltinBridgeFn>> = const { RefCell::new(None) };
46}
47
48/// Install a builtin bridge callback for the current thread.
49///
50/// Returns an RAII guard that restores the previous bridge on drop.
51/// This ensures the bridge is always properly cleaned up even when
52/// evaluation errors occur.
53pub fn set_builtin_bridge(bridge: BuiltinBridgeFn) -> BuiltinBridgeGuard {
54    let prev = BUILTIN_BRIDGE.with(|b| b.borrow_mut().replace(bridge));
55    BuiltinBridgeGuard { _prev: prev }
56}
57
58/// RAII guard that restores the previous builtin bridge on drop.
59pub struct BuiltinBridgeGuard {
60    _prev: Option<BuiltinBridgeFn>,
61}
62
63impl Drop for BuiltinBridgeGuard {
64    fn drop(&mut self) {
65        let prev = self._prev.take();
66        BUILTIN_BRIDGE.with(|b| *b.borrow_mut() = prev);
67    }
68}
69
70/// Call the builtin bridge for a named builtin.
71///
72/// Returns:
73/// - `Ok(Some(result))` if the bridge handled the builtin
74/// - `Ok(None)` if no bridge is installed (caller should error)
75/// - `Err(msg)` if the bridge returned an error
76pub fn call_builtin_bridge(
77    name: &str,
78    args: Vec<StringKeyedValue>,
79) -> Result<Option<StringKeyedValue>, String> {
80    BUILTIN_BRIDGE.with(|b| {
81        let borrow = b.borrow();
82        if let Some(ref bridge) = *borrow {
83            bridge(name, args).map(Some)
84        } else {
85            Ok(None) // No bridge set
86        }
87    })
88}
89
90// ── Path materializer ──────────────────────────────────────────────────
91//
92// WHY THIS EXISTS (measured 2026-08-17): a fetched flake input is NAMED by a
93// `/nix/store/<narhash>-source` path that sui never writes to disk — the bytes
94// live in the fetcher cache (`~/.cache/sui/inputs/…`). sui-eval's tree-walker
95// redirects every filesystem READ through `crate::path::materialize`, keeping
96// the store-path STRING byte-correct while the read lands on the real tree.
97//
98// The VM had NO such redirect: its `pathExists`/`readFile`/`readFileType` and
99// `import` called `std::fs` on the raw store name, so a flake input's files
100// read as ABSENT. That is silent — `pathExists` answers `false`, no error is
101// raised, and the VM's per-file fallback to the tree-walker structurally
102// cannot fire on a wrong-but-successful answer.
103//
104// `sui-bytecode` CANNOT depend on `sui-eval` (see this crate's Cargo.toml: the
105// dev-dep is path-only to break a publish cycle), so the redirect arrives the
106// same way the builtin bridge does — a thread-local callback installed by
107// sui-eval, with an RAII guard. Bridgeless (`tests/parity.rs`, the benches),
108// `materialize` is the IDENTITY and the VM behaves exactly as before.
109
110/// Callback type for redirecting a filesystem path before it is read.
111///
112/// Takes the path the evaluator *names*; returns the path the read should
113/// actually land on. Must be the identity for any path it does not own.
114pub type PathMaterializerFn = Box<dyn Fn(&str) -> String>;
115
116thread_local! {
117    static PATH_MATERIALIZER: RefCell<Option<PathMaterializerFn>> = const { RefCell::new(None) };
118}
119
120/// Install a path materializer for the current thread.
121///
122/// Returns an RAII guard that restores the previous materializer on drop.
123pub fn set_path_materializer(materializer: PathMaterializerFn) -> PathMaterializerGuard {
124    let prev = PATH_MATERIALIZER.with(|m| m.borrow_mut().replace(materializer));
125    PathMaterializerGuard { _prev: prev }
126}
127
128/// RAII guard that restores the previous path materializer on drop.
129pub struct PathMaterializerGuard {
130    _prev: Option<PathMaterializerFn>,
131}
132
133impl Drop for PathMaterializerGuard {
134    fn drop(&mut self) {
135        let prev = self._prev.take();
136        PATH_MATERIALIZER.with(|m| *m.borrow_mut() = prev);
137    }
138}
139
140/// Redirect a path through the installed materializer.
141///
142/// The IDENTITY when no materializer is installed — the VM must keep working
143/// standalone, so this is never allowed to fail or to change a path it does
144/// not own. This is a FILESYSTEM-READ-ONLY redirect: the result is used to
145/// touch disk, never to build a value the evaluator observes.
146#[must_use]
147pub fn materialize(path: &str) -> String {
148    PATH_MATERIALIZER.with(|m| {
149        let borrow = m.borrow();
150        match *borrow {
151            Some(ref f) => f(path),
152            None => path.to_string(),
153        }
154    })
155}
156
157/// `materialize` for a `&Path`, returning an owned `PathBuf`.
158#[must_use]
159pub fn materialize_path(path: &std::path::Path) -> std::path::PathBuf {
160    std::path::PathBuf::from(materialize(&path.to_string_lossy()))
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn no_materializer_is_the_identity() {
169        // The bridgeless VM (parity.rs, the benches) must see paths unchanged.
170        assert_eq!(materialize("/nix/store/abc-source/flake.nix"), "/nix/store/abc-source/flake.nix");
171        assert_eq!(materialize("relative/path.nix"), "relative/path.nix");
172    }
173
174    #[test]
175    fn materializer_redirects_and_guard_restores() {
176        {
177            let _guard = set_path_materializer(Box::new(|p: &str| {
178                p.replace("/nix/store/abc-source", "/cache/abc")
179            }));
180            assert_eq!(materialize("/nix/store/abc-source/flake.nix"), "/cache/abc/flake.nix");
181            // A path the materializer does not own passes through untouched.
182            assert_eq!(materialize("/etc/nix/nix.conf"), "/etc/nix/nix.conf");
183        }
184        // Guard dropped — back to the identity.
185        assert_eq!(materialize("/nix/store/abc-source/flake.nix"), "/nix/store/abc-source/flake.nix");
186    }
187
188    #[test]
189    fn materializer_guard_restores_previous() {
190        let _outer = set_path_materializer(Box::new(|_: &str| "/outer".to_string()));
191        {
192            let _inner = set_path_materializer(Box::new(|_: &str| "/inner".to_string()));
193            assert_eq!(materialize("/x"), "/inner");
194        }
195        assert_eq!(materialize("/x"), "/outer");
196    }
197
198    #[test]
199    fn materialize_path_roundtrips_through_pathbuf() {
200        let _guard = set_path_materializer(Box::new(|p: &str| p.replace("/store", "/real")));
201        assert_eq!(
202            materialize_path(std::path::Path::new("/store/f.nix")),
203            std::path::PathBuf::from("/real/f.nix")
204        );
205    }
206
207    #[test]
208    fn no_bridge_returns_none() {
209        let result = call_builtin_bridge("getEnv", vec![StringKeyedValue::String("HOME".into())]);
210        assert!(matches!(result, Ok(None)));
211    }
212
213    #[test]
214    fn bridge_handles_call() {
215        let _guard = set_builtin_bridge(Box::new(|name, args| {
216            assert_eq!(name, "getEnv");
217            match &args[0] {
218                StringKeyedValue::String(s) => {
219                    Ok(StringKeyedValue::String(format!("mocked:{s}")))
220                }
221                _ => Err("expected string".into()),
222            }
223        }));
224
225        let result = call_builtin_bridge(
226            "getEnv",
227            vec![StringKeyedValue::String("HOME".into())],
228        );
229        assert_eq!(
230            result.unwrap().unwrap(),
231            StringKeyedValue::String("mocked:HOME".into())
232        );
233    }
234
235    #[test]
236    fn bridge_error_propagates() {
237        let _guard = set_builtin_bridge(Box::new(|_, _| {
238            Err("bridge error".into())
239        }));
240
241        let result = call_builtin_bridge("anything", vec![]);
242        assert_eq!(result.unwrap_err(), "bridge error");
243    }
244
245    #[test]
246    fn guard_clears_bridge_on_drop() {
247        {
248            let _guard = set_builtin_bridge(Box::new(|_, _| {
249                Ok(StringKeyedValue::Null)
250            }));
251            assert!(matches!(
252                call_builtin_bridge("x", vec![]),
253                Ok(Some(StringKeyedValue::Null))
254            ));
255        }
256        // After guard drops, bridge should be cleared
257        assert!(matches!(call_builtin_bridge("x", vec![]), Ok(None)));
258    }
259
260    // -- set_builtin_bridge installs callback --------------------------
261
262    #[test]
263    fn set_builtin_bridge_installs_callback() {
264        let _guard = set_builtin_bridge(Box::new(|name, _| {
265            Ok(StringKeyedValue::String(format!("handled:{name}")))
266        }));
267        let result = call_builtin_bridge("myBuiltin", vec![]);
268        assert_eq!(
269            result.unwrap().unwrap(),
270            StringKeyedValue::String("handled:myBuiltin".into())
271        );
272    }
273
274    // -- RAII guard clears callback on drop -----------------------------
275
276    #[test]
277    fn raii_guard_restores_previous_bridge() {
278        // Install first bridge.
279        let _outer = set_builtin_bridge(Box::new(|_, _| {
280            Ok(StringKeyedValue::String("outer".into()))
281        }));
282        {
283            // Install inner bridge (replaces outer temporarily).
284            let _inner = set_builtin_bridge(Box::new(|_, _| {
285                Ok(StringKeyedValue::String("inner".into()))
286            }));
287            let result = call_builtin_bridge("x", vec![]);
288            assert_eq!(
289                result.unwrap().unwrap(),
290                StringKeyedValue::String("inner".into())
291            );
292        }
293        // Inner guard dropped — outer bridge should be restored.
294        let result = call_builtin_bridge("x", vec![]);
295        assert_eq!(
296            result.unwrap().unwrap(),
297            StringKeyedValue::String("outer".into())
298        );
299    }
300
301    // -- call_builtin_bridge returns None when no bridge ----------------
302
303    #[test]
304    fn call_builtin_bridge_returns_none_when_no_bridge() {
305        // Ensure no bridge is installed (clean state after guard drop).
306        {
307            let _guard = set_builtin_bridge(Box::new(|_, _| Ok(StringKeyedValue::Null)));
308        }
309        let result = call_builtin_bridge("nonexistent", vec![]);
310        assert!(matches!(result, Ok(None)));
311    }
312
313    // -- call_builtin_bridge returns Some when bridge set ---------------
314
315    #[test]
316    fn call_builtin_bridge_returns_some_when_bridge_set() {
317        let _guard = set_builtin_bridge(Box::new(|_, _| {
318            Ok(StringKeyedValue::Int(42))
319        }));
320        let result = call_builtin_bridge("anything", vec![]);
321        assert!(result.is_ok());
322        assert!(result.unwrap().is_some());
323    }
324
325    // -- bridge with simple string argument and return -----------------
326
327    #[test]
328    fn bridge_with_string_argument_and_return() {
329        let _guard = set_builtin_bridge(Box::new(|name, args| {
330            assert_eq!(name, "echo");
331            match &args[0] {
332                StringKeyedValue::String(s) => {
333                    Ok(StringKeyedValue::String(format!("echo:{s}")))
334                }
335                _ => Err("expected string arg".into()),
336            }
337        }));
338        let result = call_builtin_bridge(
339            "echo",
340            vec![StringKeyedValue::String("hello".into())],
341        );
342        assert_eq!(
343            result.unwrap().unwrap(),
344            StringKeyedValue::String("echo:hello".into())
345        );
346    }
347
348    // -- bridge with attrset argument ----------------------------------
349
350    #[test]
351    fn bridge_with_attrset_argument() {
352        let _guard = set_builtin_bridge(Box::new(|name, args| {
353            assert_eq!(name, "inspect");
354            match &args[0] {
355                StringKeyedValue::Attrs(map) => {
356                    let keys: Vec<&String> = map.keys().collect();
357                    Ok(StringKeyedValue::Int(keys.len() as i64))
358                }
359                _ => Err("expected attrset".into()),
360            }
361        }));
362
363        let mut attrs = std::collections::BTreeMap::new();
364        attrs.insert("a".to_string(), StringKeyedValue::Int(1));
365        attrs.insert("b".to_string(), StringKeyedValue::Int(2));
366        attrs.insert("c".to_string(), StringKeyedValue::Int(3));
367
368        let result = call_builtin_bridge(
369            "inspect",
370            vec![StringKeyedValue::Attrs(attrs)],
371        );
372        assert_eq!(result.unwrap().unwrap(), StringKeyedValue::Int(3));
373    }
374}