array-mumu 0.2.0-rc.5

Array tools plugin for the Mumu ecosystem
Documentation
// array/src/reduce.rs

use mumu::parser::interpreter::Interpreter;
use mumu::parser::types::{FunctionValue, IteratorKind, Value};
use crate::apply::apply_n_ary_function_value;

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

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

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

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] == "_")
}

fn looks_like_array(v: &Value) -> bool {
    matches!(
        v,
        Value::IntArray(_)
            | Value::FloatArray(_)
            | Value::BoolArray(_)
            | Value::StrArray(_)
            | Value::MixedArray(_)
            | Value::Iterator(_)
    )
}

fn looks_like_function(v: &Value) -> bool {
    matches!(v, Value::Function(_))
}

/// Register a *named* partial function into the interpreter and return it.
/// We avoid returning `RustClosure` for partials to keep execution fully inside
/// the interpreter's dynamic-function machinery (reduces cross-FFI edge cases).
fn make_named_partial(
    interp: &mut Interpreter,
    f_opt: Option<Value>,
    init_opt: Option<Value>,
    data_opt: Option<Value>,
) -> Result<Value, String> {
    let name = gensym("partial");

    let closure = move |interp: &mut Interpreter, new_args: Vec<Value>| {
        let mut f = f_opt.clone();
        let mut init = init_opt.clone();
        let mut data = data_opt.clone();

        // Fill missing slots left-to-right, skipping placeholders.
        for arg in new_args {
            if f.is_none() {
                if !is_placeholder(&arg) {
                    f = Some(arg);
                }
                continue;
            }
            if init.is_none() {
                // If an "array-looking" thing arrives where init is missing, treat it as data.
                if looks_like_array(&arg) {
                    if data.is_none() {
                        data = Some(arg);
                    } else {
                        // Already had data; store as init.
                        init = Some(arg);
                    }
                } else if !is_placeholder(&arg) {
                    init = Some(arg);
                }
                continue;
            }
            if data.is_none() {
                if !is_placeholder(&arg) {
                    data = Some(arg);
                }
                continue;
            }
            return Err("array:reduce partial => too many arguments".to_string());
        }

        if let (Some(fv), Some(dv)) = (f.clone(), data.clone()) {
            // We can run the reduction now (init may be None).
            return do_reduce(interp, fv, init.clone(), dv);
        }

        // Still partial — register a new named partial and return that.
        make_named_partial(interp, f, init, data)
    };

    // Register the dynamic function under `name`
    let f: mumu::parser::interpreter::DynamicFn = Arc::new(Mutex::new(closure));
    interp.register_dynamic_function(&name, f);

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

pub fn array_reduce_bridge(interp: &mut Interpreter, mut args: Vec<Value>) -> Result<Value, String> {
    match args.len() {
        0 => make_named_partial(interp, None, None, None),

        1 => {
            let a = args.remove(0);
            if is_placeholder(&a) {
                make_named_partial(interp, None, None, None)
            } else if looks_like_function(&a) {
                make_named_partial(interp, Some(a), None, None)
            } else if looks_like_array(&a) {
                make_named_partial(interp, None, None, Some(a))
            } else {
                // treat as init only
                make_named_partial(interp, None, Some(a), None)
            }
        }

        2 => {
            let a = args.remove(0);
            let b = args.remove(0);

            // Placeholders first
            if is_placeholder(&a) && is_placeholder(&b) {
                return make_named_partial(interp, None, None, None);
            }
            if is_placeholder(&a) {
                if looks_like_array(&b) {
                    return make_named_partial(interp, None, None, Some(b));
                } else {
                    return make_named_partial(interp, None, Some(b), None);
                }
            }
            if is_placeholder(&b) {
                if looks_like_function(&a) {
                    return make_named_partial(interp, Some(a), None, None);
                } else if looks_like_array(&a) {
                    return make_named_partial(interp, None, None, Some(a));
                } else {
                    return make_named_partial(interp, None, Some(a), None);
                }
            }

            // No placeholders in 2-arg form:
            if looks_like_function(&a) && looks_like_array(&b) {
                // reduce(fn, array) — use first element as initial acc
                return do_reduce(interp, a, None, b);
            }
            if looks_like_function(&a) && !looks_like_array(&b) {
                // reduce(fn, init) — return partial waiting for the array
                return make_named_partial(interp, Some(a), Some(b), None);
            }
            if !looks_like_function(&a) && looks_like_array(&b) {
                // reduce(init, array) — return partial waiting for the function
                return make_named_partial(interp, None, Some(a), Some(b));
            }

            // Otherwise keep it partial with both captured as best-effort
            make_named_partial(interp, Some(a), Some(b), None)
        }

        3 => {
            let f = args.remove(0);
            let i = args.remove(0);
            let d = args.remove(0);

            if is_placeholder(&f) || is_placeholder(&i) || is_placeholder(&d) {
                return make_named_partial(
                    interp,
                    if is_placeholder(&f) { None } else { Some(f) },
                    if is_placeholder(&i) { None } else { Some(i) },
                    if is_placeholder(&d) { None } else { Some(d) },
                );
            }

            do_reduce(interp, f, Some(i), d)
        }

        n => Err(format!("array:reduce expects up to 3 arguments (fn, init?, array?), got {}", n)),
    }
}

fn do_reduce(
    interp: &mut Interpreter,
    func_val: Value,
    init_opt: Option<Value>,
    data_val: Value,
) -> Result<Value, String> {
    let func_box = match func_val {
        Value::Function(fb) => fb,
        other => return Err(format!("array:reduce => first argument must be a function, got {:?}", other)),
    };

    match data_val {
        Value::IntArray(xs) => {
            let iter = xs.into_iter().map(Value::Int);
            reduce_values(interp, func_box, init_opt, iter)
        }
        Value::FloatArray(xs) => {
            let iter = xs.into_iter().map(Value::Float);
            reduce_values(interp, func_box, init_opt, iter)
        }
        Value::BoolArray(xs) => {
            let iter = xs.into_iter().map(Value::Bool);
            reduce_values(interp, func_box, init_opt, iter)
        }
        Value::StrArray(xs) => {
            let iter = xs.into_iter().map(Value::SingleString);
            reduce_values(interp, func_box, init_opt, iter)
        }
        Value::MixedArray(xs) => {
            reduce_values(interp, func_box, init_opt, xs.into_iter())
        }
        Value::Iterator(h) => {
            match &h.kind {
                IteratorKind::Core(state_arc) => {
                    // Core iterator yields ints [current..end)
                    let mut guard = state_arc.lock().map_err(|_| "IteratorState lock error".to_string())?;
                    let mut items: Vec<Value> = Vec::new();
                    while !guard.done && guard.current < guard.end {
                        let next_val = guard.current;
                        guard.current += 1;
                        if guard.current >= guard.end {
                            guard.done = true;
                        }
                        items.push(Value::Int(next_val));
                    }
                    drop(guard);
                    reduce_values(interp, func_box, init_opt, items.into_iter())
                }
                IteratorKind::Plugin(plugin_arc) => {
                    let mut plugin = plugin_arc.lock().map_err(|_| "Plugin Iterator lock error".to_string())?;
                    let mut items: Vec<Value> = Vec::new();
                    loop {
                        match plugin.next_value() {
                            Ok(v) => items.push(v),
                            Err(e) if e == "NO_MORE_DATA" => break,
                            Err(e) => return Err(format!("array:reduce => plugin iterator error: {}", e)),
                        }
                    }
                    reduce_values(interp, func_box, init_opt, items.into_iter())
                }
            }
        }
        other => Err(format!(
            "array:reduce => third argument must be an array or iterator, got {:?}",
            other
        )),
    }
}

fn reduce_values<I>(
    interp: &mut Interpreter,
    func_box: Box<FunctionValue>,
    init: Option<Value>,
    iter: I,
) -> Result<Value, String>
where
    I: IntoIterator<Item = Value>,
{
    let mut it = iter.into_iter();

    let mut acc = match init {
        Some(v) => v,
        None => it
            .next()
            .ok_or_else(|| "array:reduce => empty input with no initial value".to_string())?,
    };

    for x in it {
        // Call user reducer as reducer(acc, x)
        acc = apply_n_ary_function_value(interp, func_box.clone(), vec![acc, x])?;
    }

    Ok(acc)
}