use std::path::Path;
use std::sync::Arc;
use cljrs_env::env::Env;
use cljrs_env::error::{EvalError, EvalResult};
use cljrs_reader::Form;
use cljrs_reader::form::FormKind;
use cljrs_value::{Symbol, Value};
pub fn resolve_versioned_symbol(sym: &Symbol, commit: &str, env: &mut Env) -> EvalResult {
let ns_name: Arc<str> = match &sym.namespace {
Some(ns_part) => env
.globals
.resolve_alias(&env.current_ns, ns_part)
.unwrap_or_else(|| Arc::clone(ns_part)),
None => Arc::clone(&env.current_ns),
};
let name = sym.name.as_ref();
if let Some(cached) = env.globals.get_cached_versioned(&ns_name, name, commit) {
return Ok(cached);
}
let git_ctx = git_context_for_ns(&ns_name, env);
let (source_file, repo_root) = match git_ctx {
Ok(ctx) => ctx,
Err(_) => return native_head_fallback(&ns_name, name, commit, env),
};
env.globals.check_commit_signature(&repo_root, commit)?;
let abs_file = Path::new(source_file.as_ref());
let repo_path = Path::new(repo_root.as_ref());
let rel_file = abs_file.strip_prefix(repo_path).map_err(|_| {
EvalError::Runtime(format!(
"Cannot compute relative path for {source_file} within {repo_root}"
))
})?;
let rel_file_str = rel_file.to_string_lossy();
let src = cljrs_vcs::get_file_at_commit(repo_path, &rel_file_str, commit)
.map_err(|e| EvalError::Runtime(format!("{e}")))?;
let file_label = format!("<{ns_name}@{commit}>");
let mut parser = cljrs_reader::Parser::new(src, file_label);
let forms = parser.parse_all().map_err(EvalError::Read)?;
let Some(def_form) = find_def_form(&forms, name) else {
return native_head_fallback(&ns_name, name, commit, env);
};
let val = eval_in_snapshot(def_form, &ns_name, commit, env)?;
env.globals
.cache_versioned(&ns_name, name, commit, val.clone());
Ok(val)
}
fn native_head_fallback(ns_name: &Arc<str>, name: &str, commit: &str, env: &mut Env) -> EvalResult {
match env.globals.lookup_in_ns(ns_name, name) {
Some(val) if matches!(val, Value::NativeFunction(_)) => {
env.globals
.cache_versioned(ns_name, name, commit, val.clone());
Ok(val)
}
Some(_) => Err(EvalError::Runtime(format!(
"Cannot find definition of `{name}` in `{ns_name}@{commit}`"
))),
None => Err(EvalError::UnboundSymbol(format!("{ns_name}/{name}"))),
}
}
fn git_context_for_ns(ns_name: &Arc<str>, env: &mut Env) -> EvalResult<(Arc<str>, Arc<str>)> {
if let Some(ctx) = env.globals.get_ns_git_context(ns_name) {
return Ok(ctx);
}
let spec = cljrs_env::env::RequireSpec {
ns: Arc::clone(ns_name),
version: None,
alias: None,
refer: cljrs_env::env::RequireRefer::None,
};
cljrs_env::loader::load_ns(Arc::clone(&env.globals), &spec, &env.current_ns)?;
env.globals.get_ns_git_context(ns_name).ok_or_else(|| {
EvalError::Runtime(format!(
"Namespace `{ns_name}` has no git context (built-in or not in a git repo); \
cannot resolve versioned symbols from it"
))
})
}
fn eval_in_snapshot(form: &Form, ns_name: &str, commit: &str, env: &mut Env) -> EvalResult {
let mut snap = Env::new_versioned(Arc::clone(&env.globals), ns_name, commit);
(env.globals.eval_fn)(form, &mut snap)
}
fn find_def_form<'a>(forms: &'a [Form], name: &str) -> Option<&'a Form> {
for form in forms {
if let FormKind::List(items) = &form.kind {
if items.len() < 2 {
continue;
}
let head = match &items[0].kind {
FormKind::Symbol(s) => s.as_str(),
_ => continue,
};
let is_def_like = matches!(
head,
"def" | "defn" | "defn-" | "def-" | "defmacro" | "defmulti"
);
if !is_def_like {
continue;
}
let name_form = &items[1];
let actual_name = def_form_name(name_form);
if actual_name.as_deref() == Some(name) {
return Some(form);
}
}
}
None
}
fn def_form_name(form: &Form) -> Option<String> {
match &form.kind {
FormKind::Symbol(s) => Some(s.clone()),
FormKind::Meta(_, inner) => def_form_name(inner),
_ => None,
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use cljrs_env::env::{Env, GlobalEnv};
use cljrs_gc::GcPtr;
use cljrs_value::{NativeFn, Value};
fn make_env(ns: &str) -> (Arc<GlobalEnv>, Env) {
let globals = crate::standard_env_minimal(None, None, None);
globals.get_or_create_ns(ns);
let env = Env::new(globals.clone(), ns);
(globals, env)
}
fn fake_commit() -> &'static str {
"abc1234def56"
}
fn const_native(tag: i64) -> NativeFn {
NativeFn {
name: Arc::from("test-fn"),
arity: cljrs_value::Arity::Fixed(0),
func: Arc::new(move |_args| Ok(Value::Long(tag))),
}
}
#[test]
fn head_fallback_for_native_function() {
let (globals, mut env) = make_env("mylib");
let commit = "deadbeef01234";
let nf = const_native(99);
globals.intern(
"mylib",
Arc::from("stable-fn"),
Value::NativeFunction(GcPtr::new(nf)),
);
let sym = cljrs_value::Symbol {
namespace: Some(Arc::from("mylib")),
name: Arc::from("stable-fn"),
version: Some(Arc::from(commit)),
};
let result = super::resolve_versioned_symbol(&sym, commit, &mut env)
.expect("HEAD fallback should succeed");
assert!(matches!(result, Value::NativeFunction(_)));
}
#[test]
fn missing_symbol_gives_unbound_error() {
let (_globals, mut env) = make_env("mylib");
let commit = fake_commit();
let sym = cljrs_value::Symbol {
namespace: Some(Arc::from("mylib")),
name: Arc::from("does-not-exist"),
version: Some(Arc::from(commit)),
};
let err = super::resolve_versioned_symbol(&sym, commit, &mut env)
.expect_err("should error for unknown symbol");
assert!(
matches!(err, cljrs_env::error::EvalError::UnboundSymbol(_)),
"expected UnboundSymbol, got {err:?}"
);
}
}