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;
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 (source_file, repo_root) = git_context_for_ns(&ns_name, 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 def_form = find_def_form(&forms, name).ok_or_else(|| {
EvalError::Runtime(format!(
"Cannot find definition of `{name}` in `{ns_name}@{commit}`"
))
})?;
let val = eval_in_snapshot(def_form, &ns_name, commit, env)?;
env.globals
.cache_versioned(&ns_name, name, commit, val.clone());
Ok(val)
}
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,
}
}