Skip to main content

cljrs_runtime/interp/
versioned.rs

1//! Versioned symbol resolution — tree-walker entry point.
2//!
3//! The implementation lives in `crate::env::versioned` so that every execution
4//! tier (tree-walker, IR interpreter, JIT/AOT runtime bridges) shares one
5//! resolver.  Resolving `ns/name@commit` loads the whole versioned namespace
6//! `"ns@commit"` (from embedded source or git history) and then performs a
7//! plain lookup in it; native (Rust-backed) functions with no Clojure source
8//! fall back to the HEAD implementation.
9
10use crate::env::env::Env;
11use crate::env::error::EvalResult;
12use cljrs_value::Symbol;
13
14/// Resolve `sym` (which may or may not carry a version) at `commit` within
15/// `env`.
16pub fn resolve_versioned_symbol(sym: &Symbol, commit: &str, env: &mut Env) -> EvalResult {
17    crate::env::versioned::resolve_versioned_value(
18        &env.globals,
19        &env.current_ns,
20        sym.namespace.as_deref(),
21        &sym.name,
22        commit,
23    )
24}
25
26// ── Tests ─────────────────────────────────────────────────────────────────────
27
28#[cfg(test)]
29mod tests {
30    use std::sync::Arc;
31
32    use crate::env::env::{Env, GlobalEnv};
33    use cljrs_gc::GcPtr;
34    use cljrs_value::{NativeFn, Value};
35
36    fn make_env(ns: &str) -> (Arc<GlobalEnv>, Env) {
37        let globals = crate::Runtime::builder()
38            .execution_mode(crate::ExecutionMode::TreeWalk)
39            .build()
40            .expect("runtime")
41            .into_globals();
42        globals.get_or_create_ns(ns);
43        let env = Env::new(globals.clone(), ns);
44        (globals, env)
45    }
46
47    fn fake_commit() -> &'static str {
48        "abc1234def56"
49    }
50
51    /// Build a trivial NativeFn that returns a fixed Long value.
52    fn const_native(tag: i64) -> NativeFn {
53        NativeFn {
54            name: Arc::from("test-fn"),
55            arity: cljrs_value::Arity::Fixed(0),
56            func: Arc::new(move |_args| Ok(Value::Long(tag))),
57        }
58    }
59
60    // ── HEAD fallback ─────────────────────────────────────────────────────────
61
62    /// When a versioned symbol is requested for a native function, the HEAD
63    /// implementation is returned (no Clojure source exists at the commit).
64    #[test]
65    fn head_fallback_for_native_function() {
66        let (globals, mut env) = make_env("mylib");
67        let commit = "deadbeef01234";
68
69        // Register the function at HEAD.  No Clojure source exists for mylib
70        // at any commit, so the git-context path will fail.
71        let nf = const_native(99);
72        globals.intern(
73            "mylib",
74            Arc::from("stable-fn"),
75            Value::NativeFunction(GcPtr::new(nf)),
76        );
77
78        let sym = cljrs_value::Symbol {
79            namespace: Some(Arc::from("mylib")),
80            name: Arc::from("stable-fn"),
81            version: Some(Arc::from(commit)),
82        };
83        let result = super::resolve_versioned_symbol(&sym, commit, &mut env)
84            .expect("HEAD fallback should succeed");
85
86        assert!(matches!(result, Value::NativeFunction(_)));
87    }
88
89    /// When the symbol doesn't exist anywhere, we get UnboundSymbol — not the
90    /// confusing "Cannot find definition" message that the bare git-source
91    /// path would produce.
92    #[test]
93    fn missing_symbol_gives_unbound_error() {
94        let (_globals, mut env) = make_env("mylib");
95        let commit = fake_commit();
96
97        let sym = cljrs_value::Symbol {
98            namespace: Some(Arc::from("mylib")),
99            name: Arc::from("does-not-exist"),
100            version: Some(Arc::from(commit)),
101        };
102        let err = super::resolve_versioned_symbol(&sym, commit, &mut env)
103            .expect_err("should error for unknown symbol");
104
105        assert!(
106            matches!(err, crate::env::error::EvalError::UnboundSymbol(_)),
107            "expected UnboundSymbol, got {err:?}"
108        );
109    }
110
111    // ── Native provenance (verified HEAD binding) ─────────────────────────────
112
113    fn versioned_sym(ns: &str, name: &str, commit: &str) -> cljrs_value::Symbol {
114        cljrs_value::Symbol {
115            namespace: Some(Arc::from(ns)),
116            name: Arc::from(name),
117            version: Some(Arc::from(commit)),
118        }
119    }
120
121    /// Matching provenance (either side may be abbreviated) resolves silently
122    /// — no entry lands in the warned set.
123    #[test]
124    fn matching_provenance_is_silent() {
125        let (globals, mut env) = make_env("provlib");
126        let full_commit = "deadbeef0123456789abcdef0123456789abcdef";
127
128        globals.intern(
129            "provlib",
130            Arc::from("f"),
131            Value::NativeFunction(GcPtr::new(const_native(1))),
132        );
133        globals.set_native_provenance("provlib", full_commit);
134
135        // Pin with an abbreviated prefix of the recorded commit.
136        let sym = versioned_sym("provlib", "f", "deadbeef012");
137        super::resolve_versioned_symbol(&sym, "deadbeef012", &mut env).expect("should resolve");
138
139        assert!(
140            globals.provenance_warned.lock().unwrap().is_empty(),
141            "matching provenance must not warn"
142        );
143    }
144
145    /// Mismatching provenance still resolves (HEAD binding) but records a
146    /// once-per-pin warning.
147    #[test]
148    fn mismatched_provenance_warns_once() {
149        let (globals, mut env) = make_env("provlib2");
150        globals.intern(
151            "provlib2",
152            Arc::from("f"),
153            Value::NativeFunction(GcPtr::new(const_native(2))),
154        );
155        globals.set_native_provenance("provlib2", "1111111111111111");
156
157        let commit = "2222222222222222";
158        let sym = versioned_sym("provlib2", "f", commit);
159        let val = super::resolve_versioned_symbol(&sym, commit, &mut env)
160            .expect("mismatch still resolves to HEAD by default");
161        assert!(matches!(val, Value::NativeFunction(_)));
162
163        let warned = globals.provenance_warned.lock().unwrap();
164        assert_eq!(warned.len(), 1, "exactly one warning per pin");
165        assert!(warned.contains(&Arc::<str>::from("provlib2@2222222222222222")));
166    }
167
168    /// Under --enforce-native-versions a provenance mismatch is an error.
169    #[test]
170    fn enforce_native_versions_makes_mismatch_an_error() {
171        let (globals, mut env) = make_env("provlib3");
172        globals.intern(
173            "provlib3",
174            Arc::from("f"),
175            Value::NativeFunction(GcPtr::new(const_native(3))),
176        );
177        globals.set_native_provenance("provlib3", "1111111111111111");
178        globals.set_enforce_native_versions(true);
179
180        let commit = "2222222222222222";
181        let sym = versioned_sym("provlib3", "f", commit);
182        let err = super::resolve_versioned_symbol(&sym, commit, &mut env)
183            .expect_err("strict mode must reject a provenance mismatch");
184        let msg = format!("{err:?}");
185        assert!(
186            msg.contains("provlib3") && msg.contains("1111111111111111"),
187            "error should describe the mismatch: {msg}"
188        );
189    }
190}