use std::collections::HashMap; use crate::runtime::data::{String as RStr, pattern_bound_names}; use crate::runtime::{eval_trampoline, Budget, Eval, Scope, Value}; use crate::{Error, Result}; use super::control::try_match; // ── Helpers ────────────────────────────────────────────────────────────────── /// Build a single-key call `Value`: `{proc: [args...]}`. pub(crate) fn make_call(proc: &str, args: Vec, budget: &Budget) -> Result { let arr = Value::array(args, budget)?; Value::call(proc, arr, budget) } fn capture_scope( body: &Value, bound: &std::collections::HashSet, scope: &Scope, budget: &Budget, ) -> Result { let pairs: Vec<(RStr, Value)> = body .free_vars() .into_iter() .filter(|n| !bound.contains(n)) .filter_map(|n| scope.get(&n).map(|v| (n, v))) .map(|(n, v)| { let key = RStr::new(&n, budget)?; Ok((key, v)) }) .collect::>()?; let inner = Value::object(pairs, budget)?; // Tag the scope object so eval_inner treats it as self-evaluating data, // preventing misinterpretation as a procedure call on round-trips. Value::call("runtime/scope", inner, budget) } /// Wrap a scope value in a `runtime/scope` tag if it isn't already tagged. /// /// The inner value must be a `Value::Object`. Anything else — e.g. a number /// or string — is a programming error caught here at function-definition time /// rather than at call time. fn ensure_scope_obj_tagged(v: Value, budget: &Budget) -> Result { if let Some(("runtime/scope", _)) = v.as_call() { return Ok(v); } match &v { Value::Object(_) => Value::call("runtime/scope", v, budget), _ => Err(Error::MalformedFnValue( "scope_obj must evaluate to an object".to_string(), )), } } // ── Core operative value constructor ────────────────────────────────────────── /// Build a `vau` value form from formals + body (+ optional env formal). /// /// This is the single constructor for all operatives. All recursion is handled /// externally via `letrec`/`defrec` + mu values — no auto-wrapping here. pub(crate) fn make_fn_value( args: &[Value], scope: Scope, budget: &Budget, eval_limit: &mut usize, ) -> Result { match args { // 2-element source form: [formals_pattern, body] // Formals are patterns (not expressions) — used as-is, not evaluated. [formals, body] => { let bound = pattern_bound_names(formals); let scope_obj = capture_scope(body, &bound, &scope, budget)?; make_call("vau", vec![formals.clone(), body.clone(), scope_obj], budget) } // 3-element: either value form [formals, body, scope_obj] // or env source form [formals, env_str, body] [formals, second, third] => { // Check if this is [formals, env_str, body] — 3-element source form with env formal if let Value::String(env_name) = second { // 3-element source with env formal: [formals, "env", body] let body = third; let mut bound = pattern_bound_names(formals); bound.insert(env_name.to_string()); let scope_obj = capture_scope(body, &bound, &scope, budget)?; make_call("vau", vec![formals.clone(), second.clone(), body.clone(), scope_obj], budget) } else { // 3-element value/explicit-scope form: [formals, body, scope_obj] let body = second; let scope_obj = third; let is_value_form = matches!(scope_obj.as_call(), Some(("runtime/scope", _))); if is_value_form { make_call("vau", vec![formals.clone(), body.clone(), scope_obj.clone()], budget) } else { let scope_evaled = eval_trampoline(budget, eval_limit, scope_obj.clone(), scope)?; let scope_tagged = ensure_scope_obj_tagged(scope_evaled, budget)?; make_call("vau", vec![formals.clone(), body.clone(), scope_tagged], budget) } } } // 4-element value form: [formals, env_str, body, scope_obj] [formals, env_name, body, scope_obj] => { let is_value_form = matches!(scope_obj.as_call(), Some(("runtime/scope", _))); if is_value_form { make_call("vau", vec![formals.clone(), env_name.clone(), body.clone(), scope_obj.clone()], budget) } else { let scope_evaled = eval_trampoline(budget, eval_limit, scope_obj.clone(), scope)?; let scope_tagged = ensure_scope_obj_tagged(scope_evaled, budget)?; make_call("vau", vec![formals.clone(), env_name.clone(), body.clone(), scope_tagged], budget) } } _ => Err(Error::arity("vau", "2 or 3", args.len())), } } // ── Public builtins ─────────────────────────────────────────────────────────── /// `vau` — the primitive operative constructor. /// /// Source forms: `{"vau": [formals, body]}` or `{"vau": [formals, "env", body]}` pub fn op_vau( args: &[Value], budget: &Budget, eval_limit: &mut usize, scope: Scope, ) -> Result { make_fn_value(args, scope, budget, eval_limit).map(Eval::Done) } // fn and macro are self-hosted in stdlib.jst. /// `wrap` — wraps an operative into an applicative (evaluates args before dispatch). /// /// Also handles value-form round-tripping: if called with an already-wrapped value, /// returns it unchanged (needed because wrap values serialize as `{"wrap": vau_val}` /// which goes through eval_inner → dispatch when re-evaluated). pub fn op_wrap( args: &[Value], budget: &Budget, eval_limit: &mut usize, scope: Scope, ) -> Result { match args { [arg] => { let val = eval_trampoline(budget, eval_limit, arg.clone(), scope)?; match val.as_call() { Some(("vau", _)) | Some(("mu", _)) => Value::call("wrap", val, budget).map(Eval::Done), _ => Err(Error::type_err("wrap", 0, &val, "operative (vau or mu value)")), } } _ => Err(Error::arity("wrap", "1", args.len())), } } /// `unwrap` — extracts the inner operative from a wrapped applicative. pub fn op_unwrap( args: &[Value], budget: &Budget, eval_limit: &mut usize, scope: Scope, ) -> Result { match args { [arg] => { let val = eval_trampoline(budget, eval_limit, arg.clone(), scope)?; if let Some(("wrap", inner)) = val.as_call() { Eval::done(inner.clone()) } else { Err(Error::type_err("unwrap", 0, &val, "applicative (wrap value)")) } } _ => Err(Error::arity("unwrap", "1", args.len())), } } // ── Scope extraction ───────────────────────────────────────────────────────── /// Extract a scope HashMap from a scope_obj value. /// /// scope_obj may be in one of these forms after eval + round-trips: /// - `{"runtime/scope": {...}}` — the standard tagged form /// - `{...}` — untagged form (multi-entry) pub(crate) fn extract_scope_map( sobj: Value, ) -> crate::Result> { fn from_val(val: &Value) -> crate::Result> { match val { Value::Object(obj) => Ok(obj.iter() .map(|(k, v)| (k.to_string(), v.clone())) .collect()), _ => { // A 1-entry scope object is recognized as a call form by as_call(). if let Some((k, v)) = val.as_call() { Ok([(k.to_string(), v.clone())].into_iter().collect()) } else { Err(crate::Error::MalformedFnValue( "scope must be an object".to_string(), )) } } } } // Check for tagged form: {"runtime/scope": inner} if let Some(("runtime/scope", inner)) = sobj.as_call() { return from_val(inner); } from_val(&sobj) } // ── call_operative ─────────────────────────────────────────────────────────── /// Invoke a `vau` operative value against a list of (unevaluated) call arguments. /// /// `fn_arg` — the argument part of the `vau` singleton object, i.e. the /// `Value::Array([formals_pat, body, scope_obj?])` or /// `Value::Array([formals_pat, env_str, body, scope_obj?])`. /// `caller_scope` — the scope at the call site (for env formal binding). /// /// Operatives NEVER evaluate args — that's the caller's responsibility /// (wrap does it for applicatives). pub(crate) fn call_operative( fn_arg: Value, call_args: &[Value], budget: &Budget, eval_limit: &mut usize, caller_scope: Scope, ) -> Result { let items = match fn_arg { Value::Array(arr) => arr, _ => { return Err(Error::MalformedFnValue( "operative value is not an array".to_string(), )) } }; // Parse the vau value structure: // [formals_pat, body] — no scope, no env // [formals_pat, body, scope_obj] — closed, no env // [formals_pat, env_str, body, scope_obj] — closed, with env let (formals_pat, env_formal, body, scope_opt): (Value, Option, Value, Option) = match items.as_ref() { [f, b] => (f.clone(), None, b.clone(), None), [f, b, s] => { // Disambiguate: if s is a runtime/scope tag, this is [formals, body, scope_obj]. // Otherwise check if b is a String (env formal): [formals, env_str, body]. if matches!(s.as_call(), Some(("runtime/scope", _))) { (f.clone(), None, b.clone(), Some(s.clone())) } else if let Value::String(env) = b { // 3-element without scope_obj: [formals, env, body] (unclosed) (f.clone(), Some(env.to_string()), s.clone(), None) } else { // Assume it's [formals, body, explicit_scope_expr] (shouldn't normally happen in value forms) (f.clone(), None, b.clone(), Some(s.clone())) } } [f, env, b, s] => { let env_name = match env { Value::String(s) => s.to_string(), _ => return Err(Error::MalformedFnValue( "env formal must be a string".to_string(), )), }; (f.clone(), Some(env_name), b.clone(), Some(s.clone())) } _ => { return Err(Error::MalformedFnValue( "operative value has unexpected structure".to_string(), )) } }; // Operatives do NOT evaluate args — pass through as-is. let evaled = call_args.to_vec(); // Match args against the formals pattern. // // If formals is an Array, match against the args-as-array (positional binding). // If formals is NOT an Array, expect exactly one argument and match directly // against it (single-arg destructuring). This mirrors the singleton-call // convention: {"foo": x} ↔ {"foo": [x]}. let match_target = if matches!(&formals_pat, Value::Array(_)) { Value::array(evaled, budget)? } else { if evaled.len() != 1 { return Err(Error::arity("vau", "1", evaled.len())); } evaled.into_iter().next().unwrap() }; let mut bindings: HashMap = HashMap::new(); if !try_match(&formals_pat, &match_target, &mut bindings, budget, eval_limit, caller_scope.clone())? { return Err(Error::arity("vau", "matching args", call_args.len())); } // Reconstruct execution scope. If the operative value carries an embedded // scope_obj, layer it on top of the global root so that builtins are // always reachable. Otherwise use the call-site scope. let exec_scope = if let Some(sobj) = scope_opt { let map = extract_scope_map(sobj)?; caller_scope.globals().local(map) } else { caller_scope.clone() }; // If env formal is present, bind caller's scope as a Value::Object. if let Some(env_name) = env_formal { let env_val = scope_to_value(&caller_scope, budget)?; bindings.insert(env_name, env_val); } Ok(Eval::TailEval { value: body, scope: exec_scope.local(bindings) }) } /// Convert a Scope to a Value::Object for env formal binding. /// /// Includes all bindings (including `runtime/*` names) for consistency /// with `get-scope`. Env formals expose the full scope. fn scope_to_value(scope: &Scope, budget: &Budget) -> Result { let all = scope.collect_all(); let pairs: Vec<(crate::runtime::data::String, Value)> = all .into_iter() .map(|(name, val)| { let key = crate::runtime::data::String::new(&name, budget)?; Ok((key, val)) }) .collect::>()?; Value::object(pairs, budget) }