array-mumu 0.2.0-rc.5

Array tools plugin for the Mumu ecosystem
Documentation
// src/apply.rs
//
// Local compatibility layer to apply function values using ONLY the public API.
// Also provides a local copy of `do_map` so callers don't depend on core internals.
// Extended to support partial application and "_" placeholder completion for array:apply.

use mumu::parser::ast::Expr;
use mumu::parser::interpreter::Interpreter;
use mumu::parser::types::{FunctionValue, IteratorKind, Value};

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};

static GENSYM: AtomicU64 = AtomicU64::new(1);

fn gensym(prefix: &str) -> String {
    let n = GENSYM.fetch_add(1, Ordering::Relaxed);
    format!("__mu_apply_{}_{}", prefix, n)
}

/// Helper: does this value represent a placeholder ("_")?
fn is_placeholder(v: &Value) -> bool {
    matches!(v, Value::Placeholder)
        || matches!(v, Value::SingleString(s) if s == "_")
        || matches!(v, Value::StrArray(ss) if ss.len() == 1 && ss[0] == "_")
}

/// Helper: does this value look like an argument array (or iterator)?
fn looks_like_array(v: &Value) -> bool {
    matches!(
        v,
        Value::MixedArray(_)
            | Value::IntArray(_)
            | Value::FloatArray(_)
            | Value::BoolArray(_)
            | Value::StrArray(_)
            | Value::Iterator(_)
    )
}

/// Helper: does this value look like a function?
fn looks_like_function(v: &Value) -> bool {
    matches!(v, Value::Function(_))
}

/// Apply a FunctionValue to N arguments using only public API.
///
/// This binds the function and each argument into uniquely named
/// temporaries and then evaluates an AST call expression.
pub fn apply_n_ary_function_value(
    interp: &mut Interpreter,
    func: Box<FunctionValue>,
    args: Vec<Value>,
) -> Result<Value, String> {
    // 1) Bind function into a fresh variable
    let fn_sym = gensym("f");
    interp.set_variable(&fn_sym, Value::Function(func));

    // 2) Bind arguments into fresh variables and build identifier nodes
    let mut arg_idents = Vec::with_capacity(args.len());
    for (i, arg) in args.into_iter().enumerate() {
        let sym = gensym(&format!("a{}", i));
        interp.set_variable(&sym, arg);
        arg_idents.push(Expr::Ident { name: sym, line: 0, col: 0 });
    }

    // 3) Build:  fn_sym(arg0, arg1, ...)
    let call = Expr::FunctionCall {
        callee: Box::new(Expr::Ident { name: fn_sym, line: 0, col: 0 }),
        args: arg_idents,
        line: 0,
        col: 0,
    };

    // 4) Evaluate
    interp.eval_expression(&call)
}

/// Apply a FunctionValue to a single argument.
pub fn apply_one_function_value(
    interp: &mut Interpreter,
    func: Box<FunctionValue>,
    arg: Value,
) -> Result<Value, String> {
    apply_n_ary_function_value(interp, func, vec![arg])
}

/// Local copy of core's `do_map` built on the public `apply_*` helpers above.
/// This keeps `array:map` working without importing core internals.
pub fn do_map(
    interp: &mut Interpreter,
    map_fn: &FunctionValue,
    data_val: Value,
) -> Result<Value, String> {
    match data_val {
        Value::IntArray(xs) => {
            let mut out = Vec::with_capacity(xs.len());
            for x in xs {
                match apply_one_function_value(interp, Box::new(map_fn.clone()), Value::Int(x))? {
                    Value::Int(i2) => out.push(i2),
                    other => {
                        return Err(format!(
                            "array:map => user function returned non-int: {:?}",
                            other
                        ))
                    }
                }
            }
            Ok(Value::IntArray(out))
        }
        Value::FloatArray(ff) => {
            let mut out = Vec::with_capacity(ff.len());
            for x in ff {
                match apply_one_function_value(interp, Box::new(map_fn.clone()), Value::Float(x))? {
                    Value::Float(y) => out.push(y),
                    other => {
                        return Err(format!(
                            "array:map => user function returned non-float: {:?}",
                            other
                        ))
                    }
                }
            }
            Ok(Value::FloatArray(out))
        }
        Value::StrArray(ss) => {
            let mut results = Vec::with_capacity(ss.len());
            let mut out_kind: Option<&'static str> = None;
            for s in ss {
                let ret = apply_one_function_value(
                    interp,
                    Box::new(map_fn.clone()),
                    Value::SingleString(s),
                )?;
                match &ret {
                    Value::Int(_) => {
                        if out_kind.get_or_insert("int") != &"int" {
                            return Err("array:map => inconsistent return types".into());
                        }
                    }
                    Value::SingleString(_) => {
                        if out_kind.get_or_insert("string") != &"string" {
                            return Err("array:map => inconsistent return types".into());
                        }
                    }
                    _ => return Err("array:map => unsupported return type from user function".into()),
                }
                results.push(ret);
            }
            match out_kind {
                None => Ok(Value::StrArray(vec![])),
                Some("int") => Ok(Value::IntArray(
                    results
                        .into_iter()
                        .map(|v| if let Value::Int(n) = v { n } else { unreachable!() })
                        .collect(),
                )),
                Some("string") => Ok(Value::StrArray(
                    results
                        .into_iter()
                        .map(|v| if let Value::SingleString(s) = v { s } else { unreachable!() })
                        .collect(),
                )),
                _ => Err("array:map => unrecognized type".into()),
            }
        }
        Value::Iterator(h) => match &h.kind {
            IteratorKind::Core(state_arc) => {
                let mut out = Vec::new();
                let mut guard = state_arc
                    .lock()
                    .map_err(|_| "IteratorState lock error".to_string())?;
                while !guard.done && guard.current < guard.end {
                    let next_val = guard.current;
                    guard.current += 1;
                    if guard.current >= guard.end {
                        guard.done = true;
                    }
                    drop(guard);

                    match apply_one_function_value(
                        interp,
                        Box::new(map_fn.clone()),
                        Value::Int(next_val),
                    )? {
                        Value::Int(i2) => out.push(i2),
                        other => {
                            return Err(format!(
                                "array:map => user function returned non-int: {:?}",
                                other
                            ))
                        }
                    }
                    guard = state_arc
                        .lock()
                        .map_err(|_| "IteratorState lock error".to_string())?;
                }
                Ok(Value::IntArray(out))
            }
            IteratorKind::Plugin(plugin_arc) => {
                let mut out = Vec::new();
                let mut plugin = plugin_arc
                    .lock()
                    .map_err(|_| "Plugin Iterator lock error".to_string())?;
                loop {
                    match plugin.next_value() {
                        Ok(val) => match apply_one_function_value(
                            interp,
                            Box::new(map_fn.clone()),
                            val,
                        )? {
                            Value::Int(i2) => out.push(i2),
                            _ => {
                                return Err(
                                    "array:map => user function returned non-int for plugin iterator"
                                        .into()
                                )
                            }
                        },
                        Err(e) if e == "NO_MORE_DATA" => break,
                        Err(e) => return Err(e),
                    }
                }
                Ok(Value::IntArray(out))
            }
        },
        other => Err(format!("array:map => unsupported input: {:?}", other)),
    }
}

/// Flatten any "args-array" or iterator into a Vec<Value> suitable for apply.
/// If the input is not an array-like value, it is treated as a single argument.
fn flatten_args(arg_values: Value) -> Result<Vec<Value>, String> {
    match arg_values {
        Value::MixedArray(xs) => Ok(xs),
        Value::IntArray(xs) => Ok(xs.into_iter().map(Value::Int).collect()),
        Value::FloatArray(xs) => Ok(xs.into_iter().map(Value::Float).collect()),
        Value::BoolArray(xs) => Ok(xs.into_iter().map(Value::Bool).collect()),
        Value::StrArray(xs) => Ok(xs.into_iter().map(Value::SingleString).collect()),
        Value::Iterator(h) => match &h.kind {
            IteratorKind::Core(state_arc) => {
                let mut out = Vec::new();
                let mut guard = state_arc
                    .lock()
                    .map_err(|_| "IteratorState lock error".to_string())?;
                while !guard.done && guard.current < guard.end {
                    let next_val = guard.current;
                    guard.current += 1;
                    if guard.current >= guard.end {
                        guard.done = true;
                    }
                    out.push(Value::Int(next_val));
                }
                Ok(out)
            }
            IteratorKind::Plugin(plugin_arc) => {
                let mut out = Vec::new();
                let mut plugin = plugin_arc
                    .lock()
                    .map_err(|_| "Plugin Iterator lock error".to_string())?;
                loop {
                    match plugin.next_value() {
                        Ok(val) => out.push(val),
                        Err(e) if e == "NO_MORE_DATA" => break,
                        Err(e) => return Err(e),
                    }
                }
                Ok(out)
            }
        },
        Value::Placeholder => Err("array:apply => missing args-array".to_string()),
        other => Ok(vec![other]),
    }
}

/// Call helper for array:apply once both pieces are present.
fn do_apply_call(interp: &mut Interpreter, func: Value, arg_values: Value) -> Result<Value, String> {
    let func_box = match func {
        Value::Function(fb) => fb,
        other => return Err(format!("array:apply => first argument must be a function, got {:?}", other)),
    };

    let call_args = flatten_args(arg_values)?;
    apply_n_ary_function_value(interp, func_box, call_args)
}

/// Register a *named* partial that completes array:apply.
/// We avoid returning raw Rust closures as function values across the FFI boundary.
fn make_named_partial_apply(
    interp: &mut Interpreter,
    f_opt: Option<Value>,
    args_opt: Option<Value>,
) -> Result<Value, String> {
    let name = gensym("apply_partial");

    // Capture current partial state.
    let c_f = f_opt.clone();
    let c_args = args_opt.clone();

    // The closure fills missing slots left-to-right, skipping explicit placeholders.
    let closure = move |interp: &mut Interpreter, new_args: Vec<Value>| {
        let mut f = c_f.clone();
        let mut arr = c_args.clone();

        for arg in new_args {
            if is_placeholder(&arg) {
                continue;
            }
            if f.is_none() && looks_like_function(&arg) {
                f = Some(arg);
                continue;
            }
            if arr.is_none() {
                arr = Some(arg);
                continue;
            }
            // If we already had both, extra args are an error.
            return Err("array:apply partial => too many arguments".to_string());
        }

        if let (Some(ff), Some(av)) = (f.clone(), arr.clone()) {
            return do_apply_call(interp, ff, av);
        }

        // Still partial — return another named partial with updated captures.
        make_named_partial_apply(interp, f, arr)
    };

    // Register and return a named dynamic function.
    // Note: we intentionally do not use a raw Rust closure FunctionValue variant to
    // avoid potential segfaults when crossing the plugin boundary.
    interp.register_dynamic_function(&name, Arc::new(Mutex::new(closure)));

    Ok(Value::Function(Box::new(FunctionValue::Named(name))))
}

/// Bridge for `array:apply(fn, args_array)`
///
/// Also supports partial application and "_" placeholder completion:
///   - array:apply(fn)                 => returns a function expecting args-array
///   - array:apply(_, args)            => returns a function expecting fn
///   - array:apply(fn, _)              => returns a function expecting args-array
///   - array:apply(_) or array:apply() => returns a function expecting (fn, args)
///   - myAdder = array:apply((a,b,c)=>a*b*c); myAdder([2,3,4]) => 24
pub fn array_apply_bridge(interp: &mut Interpreter, mut args: Vec<Value>) -> Result<Value, String> {
    match args.len() {
        0 => {
            // Nothing provided — fully partial.
            make_named_partial_apply(interp, None, None)
        }
        1 => {
            let a = args.remove(0);
            if is_placeholder(&a) {
                // Placeholder — fully partial.
                make_named_partial_apply(interp, None, None)
            } else if looks_like_function(&a) {
                // Function provided — wait for args-array.
                make_named_partial_apply(interp, Some(a), None)
            } else {
                // Treat as args-array or single-arg vector — wait for function.
                make_named_partial_apply(interp, None, Some(a))
            }
        }
        2 => {
            let a = args.remove(0);
            let b = args.remove(0);

            // Handle placeholders explicitly for predictable partial behavior.
            let a_ph = is_placeholder(&a);
            let b_ph = is_placeholder(&b);

            match (a_ph, b_ph) {
                (true, true) => return make_named_partial_apply(interp, None, None),
                (true, false) => return make_named_partial_apply(interp, None, Some(b)),
                (false, true) => return make_named_partial_apply(interp, Some(a), None),
                (false, false) => { /* fall through to resolution below */ }
            }

            // No placeholders; resolve which is the function and which are the arguments.
            if looks_like_function(&a) {
                return do_apply_call(interp, a, b);
            }
            if looks_like_function(&b) {
                return do_apply_call(interp, b, a);
            }

            // Neither looks like a function — keep it partial waiting for the function.
            // Pack what we have as "args" (the first value becomes the pending args payload).
            make_named_partial_apply(interp, None, Some(a))
        }
        n => Err(format!(
            "array:apply => expected at most 2 arguments (fn?, args-array?), got {}",
            n
        )),
    }
}