use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, LazyLock, Mutex};
use cirru_edn::EdnTag;
use crate::builtins::meta::type_of;
use crate::calcit::{Calcit, CalcitErr, CalcitErrKind, CalcitImport, CalcitList, CalcitScope};
use crate::{call_stack::CallStackList, runner};
pub(crate) type ValueAndListeners = (Calcit, HashMap<EdnTag, Calcit>);
type RefListeners = HashMap<Arc<str>, Arc<Mutex<ValueAndListeners>>>;
static REFS_DICT: LazyLock<Mutex<RefListeners>> = LazyLock::new(|| Mutex::new(HashMap::new()));
fn modify_ref(locked_pair: Arc<Mutex<ValueAndListeners>>, v: Calcit, call_stack: &CallStackList) -> Result<(), CalcitErr> {
let (listeners, prev) = {
let mut pair = locked_pair.lock().expect("read ref");
let prev = pair.0.to_owned();
if prev == v {
return Ok(());
}
let listeners = pair.1.to_owned();
v.clone_into(&mut pair.0);
(listeners, prev)
};
for f in listeners.values() {
match f {
Calcit::Fn { info, .. } => {
runner::run_fn(&[v.to_owned(), prev.to_owned()], info, call_stack)?;
}
a => {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Type,
format!("modify-ref expected a function to trigger after `reset!`, but received: {a}"),
call_stack,
a.get_location(),
));
}
}
}
Ok(())
}
pub fn defatom(expr: &CalcitList, scope: &CalcitScope, file_ns: &str, call_stack: &CallStackList) -> Result<Calcit, CalcitErr> {
match (expr.first(), expr.get(1)) {
(Some(Calcit::Symbol { sym, info, .. }), Some(code)) => {
let mut path: String = (*info.at_ns).to_owned();
path.push('/');
path.push_str(sym);
let path_info: Arc<str> = path.into();
let defined = {
let dict = REFS_DICT.lock().expect("read refs");
dict.get(&path_info).map(ToOwned::to_owned)
};
match defined {
Some(v) => Ok(Calcit::Ref(path_info, v.to_owned())),
None => {
let v = runner::evaluate_expr(code, scope, file_ns, call_stack)?;
let pair_value = Arc::new(Mutex::new((v, HashMap::new())));
let mut dict = REFS_DICT.lock().expect("read refs");
dict.insert(path_info.to_owned(), pair_value.to_owned());
Ok(Calcit::Ref(path_info, pair_value))
}
}
}
(Some(Calcit::Import(CalcitImport { def, ns, .. })), Some(code)) => {
let mut path: String = ns.to_string();
path.push('/');
path.push_str(def);
let path_info: Arc<str> = path.into();
let defined = {
let dict = REFS_DICT.lock().expect("read refs");
dict.get(&path_info).map(ToOwned::to_owned)
};
match defined {
Some(v) => Ok(Calcit::Ref(path_info, v.to_owned())),
None => {
let v = runner::evaluate_expr(code, scope, file_ns, call_stack)?;
let pair_value = Arc::new(Mutex::new((v, HashMap::new())));
let mut dict = REFS_DICT.lock().expect("read refs");
dict.insert(path_info.to_owned(), pair_value.to_owned());
Ok(Calcit::Ref(path_info, pair_value))
}
}
}
(Some(a), Some(b)) => Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Type,
format!("defatom expected a symbol and an expression, but received: {a} , {b}"),
call_stack,
a.get_location().or_else(|| b.get_location()),
)),
_ => Err(CalcitErr::use_msg_stack(
CalcitErrKind::Arity,
"defatom expected 2 nodes, but received none",
call_stack,
)),
}
}
static ATOM_ID_GEN: AtomicUsize = AtomicUsize::new(0);
pub fn atom(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
match xs.first() {
Some(value) => Ok(quick_build_atom(value.to_owned())),
_ => {
let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::Atom).unwrap_or_default();
crate::calcit::CalcitErr::err_str_with_hint(
crate::calcit::CalcitErrKind::Arity,
"atom requires 1 argument (initial value), but received none".to_string(),
hint,
)
}
}
}
pub fn quick_build_atom(v: Calcit) -> Calcit {
let atom_idx = ATOM_ID_GEN.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let path: String = format!("atom-{atom_idx}");
let path_info: Arc<str> = path.into();
let pair_value = Arc::new(Mutex::new((v, HashMap::new())));
Calcit::Ref(path_info, pair_value)
}
pub fn atom_deref(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
match xs.first() {
Some(Calcit::Ref(_path, locked_pair)) => {
let pair = (**locked_pair).lock().expect("read pair from block");
Ok(pair.0.to_owned())
}
Some(a) => {
let msg = format!(
"&atom:deref requires a ref (atom), but received: {}",
type_of(&[a.to_owned()])?.lisp_str()
);
let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::AtomDeref).unwrap_or_default();
crate::calcit::CalcitErr::err_str_with_hint(crate::calcit::CalcitErrKind::Type, msg, hint)
}
_ => {
let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::AtomDeref).unwrap_or_default();
crate::calcit::CalcitErr::err_str_with_hint(
crate::calcit::CalcitErrKind::Arity,
"&atom:deref requires 1 argument (a ref), but received none".to_string(),
hint,
)
}
}
}
pub fn reset_bang(expr: &CalcitList, scope: &CalcitScope, file_ns: &str, call_stack: &CallStackList) -> Result<Calcit, CalcitErr> {
if expr.len() < 2 {
return CalcitErr::err_nodes(CalcitErrKind::Arity, "reset! expected 2 arguments, but received:", &expr.to_vec());
}
let target = runner::evaluate_expr(&expr[0], scope, file_ns, call_stack)?;
let new_value = runner::evaluate_expr(&expr[1], scope, file_ns, call_stack)?;
match (target, &new_value) {
(Calcit::Ref(_path, locked_pair), v) => {
modify_ref(locked_pair, v.to_owned(), call_stack)?;
Ok(Calcit::Nil)
}
(Calcit::Thunk(thunk), _) => match &expr[0] {
Calcit::Symbol { .. } | Calcit::Import(CalcitImport { .. }) => {
let ret = thunk.evaluated(scope, call_stack)?;
match (ret, &new_value) {
(Calcit::Ref(_path, locked_pair), v) => {
modify_ref(locked_pair, v.to_owned(), call_stack)?;
Ok(Calcit::Nil)
}
(a, _) => Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Type,
format!("reset! expected a ref, but received: {a}"),
call_stack,
a.get_location(),
)),
}
}
_ => CalcitErr::err_str(
CalcitErrKind::Type,
format!("reset! expected a symbol, but received: {:?}", expr[0]),
),
},
(a, b) => Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Type,
format!("reset! expected a ref and a value, but received: {a} {b}"),
call_stack,
a.get_location().or_else(|| b.get_location()),
)),
}
}
pub fn add_watch(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
match (xs.first(), xs.get(1), xs.get(2)) {
(Some(Calcit::Ref(_path, locked_pair)), Some(Calcit::Tag(k)), Some(f @ Calcit::Fn { .. })) => {
let mut pair = locked_pair.lock().expect("trying to modify locked pair");
match pair.1.get(k) {
Some(_) => CalcitErr::err_str(
CalcitErrKind::Unexpected,
format!("add-watch failed: listener with key `{k}` already existed"),
),
None => {
pair.1.insert(k.to_owned(), f.to_owned());
Ok(Calcit::Nil)
}
}
}
(Some(Calcit::Ref(..)), Some(Calcit::Tag(_)), Some(a)) => {
let msg = format!(
"add-watch requires a function as 3rd argument, but received: {}",
type_of(&[a.to_owned()])?.lisp_str()
);
let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::AddWatch).unwrap_or_default();
crate::calcit::CalcitErr::err_str_with_hint(crate::calcit::CalcitErrKind::Type, msg, hint)
}
(Some(Calcit::Ref(..)), Some(a), Some(_)) => {
let msg = format!(
"add-watch requires a tag as 2nd argument (watch key), but received: {}",
type_of(&[a.to_owned()])?.lisp_str()
);
let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::AddWatch).unwrap_or_default();
crate::calcit::CalcitErr::err_str_with_hint(crate::calcit::CalcitErrKind::Type, msg, hint)
}
(Some(a), _, _) => {
let msg = format!(
"add-watch requires a ref (atom) as 1st argument, but received: {}",
type_of(&[a.to_owned()])?.lisp_str()
);
let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::AddWatch).unwrap_or_default();
crate::calcit::CalcitErr::err_str_with_hint(crate::calcit::CalcitErrKind::Type, msg, hint)
}
(a, b, c) => {
let msg = format!(
"add-watch requires 3 arguments (ref, tag-key, function), but received: {}",
if a.is_none() {
0
} else if b.is_none() {
1
} else if c.is_none() {
2
} else {
3
}
);
let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::AddWatch).unwrap_or_default();
crate::calcit::CalcitErr::err_str_with_hint(crate::calcit::CalcitErrKind::Arity, msg, hint)
}
}
}
pub fn remove_watch(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
match (xs.first(), xs.get(1)) {
(Some(Calcit::Ref(_path, locked_pair)), Some(Calcit::Tag(k))) => {
let mut pair = locked_pair.lock().expect("trying to modify locked pair");
match pair.1.get(k) {
None => CalcitErr::err_str(
CalcitErrKind::Unexpected,
format!("remove-watch failed: listener with key `{k}` not found"),
),
Some(_) => {
pair.1.remove(k);
Ok(Calcit::Nil)
}
}
}
(Some(a), Some(b)) => {
let msg = format!(
"remove-watch requires a ref and a tag, but received: {} and {}",
type_of(&[a.to_owned()])?.lisp_str(),
type_of(&[b.to_owned()])?.lisp_str()
);
let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::RemoveWatch).unwrap_or_default();
crate::calcit::CalcitErr::err_str_with_hint(crate::calcit::CalcitErrKind::Type, msg, hint)
}
(a, b) => {
let msg = format!(
"remove-watch requires 2 arguments (ref and tag-key), but received: {} arguments",
if a.is_none() {
0
} else if b.is_none() {
1
} else {
2
}
);
let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::RemoveWatch).unwrap_or_default();
crate::calcit::CalcitErr::err_str_with_hint(crate::calcit::CalcitErrKind::Arity, msg, hint)
}
}
}