array-mumu 0.2.0-rc.5

Array tools plugin for the Mumu ecosystem
Documentation
use mumu::{
    parser::types::FunctionValue::RustClosure,
    parser::types::Value,
    Interpreter,
};
use std::sync::{Arc, Mutex};

/// `array:range` bridging, partial placeholder usage:
/// - 0 arguments => partial => (start=?, end=?)
/// - 1 argument => if `_`, remain partial; else that sets start
/// - 2 arguments => if none `_`, do_range_final => produce [start..end)
///                  else partial
///
/// The final call returns an IntArray of consecutive integers from `start` (inclusive)
/// up to but excluding `end`, matching Ramda's `range(start, end)`.
pub fn array_range_bridge(
    _interp: &mut Interpreter,
    mut args: Vec<Value>
) -> Result<Value, String> {
    match args.len() {
        // No arguments => partial => (start=?, end=?)
        0 => Ok(make_two_arg_partial(None, None)),

        // One argument => if `_`, remain partial, else that's `start`
        1 => {
            let first = args.remove(0);
            match first {
                Value::Placeholder => {
                    Ok(make_two_arg_partial(None, None))
                }
                // We'll interpret everything else as a numeric `start`
                val => {
                    Ok(make_two_arg_partial(Some(val), None))
                }
            }
        }

        // Two arguments => maybe final if no placeholders, else partial
        2 => {
            let start_val = args.remove(0);
            let end_val = args.remove(0);

            let s_pl = matches!(start_val, Value::Placeholder);
            let e_pl = matches!(end_val, Value::Placeholder);

            if !s_pl && !e_pl {
                // both real => do_range_final
                do_range_final(start_val, end_val)
            } else {
                // partial
                Ok(make_two_arg_partial(
                    if s_pl { None } else { Some(start_val) },
                    if e_pl { None } else { Some(end_val) },
                ))
            }
        }

        n => Err(format!("array:range => expected up to 2 arguments, got {}", n)),
    }
}

/// If both start & end are known numeric => produce [start..end).
fn do_range_final(start_val: Value, end_val: Value) -> Result<Value, String> {
    let s = match coerce_int(&start_val)? {
        Some(i) => i,
        None => return Ok(Value::IntArray(vec![])),  // or do empty?
    };
    let e = match coerce_int(&end_val)? {
        Some(j) => j,
        None => return Ok(Value::IntArray(vec![])),
    };

    if s >= e {
        // if start >= end => empty array
        return Ok(Value::IntArray(vec![]));
    }

    let length = (e - s) as usize;
    let mut out = Vec::with_capacity(length);
    for i in s..e {
        out.push(i);
    }
    Ok(Value::IntArray(out))
}

/// Coerce a Value into an i32, or return an error if it's not numeric or placeholder:
fn coerce_int(val: &Value) -> Result<Option<i32>, String> {
    match val {
        Value::Placeholder => {
            // In practice, we never call do_range_final if the argument is `_`.
            // But just to be safe, we can return an error or empty. We'll return error:
            Err("array:range => found placeholder in do_range_final".to_string())
        }
        Value::Int(i) => Ok(Some(*i)),
        Value::IntArray(arr) if arr.len() == 1 => Ok(Some(arr[0])),
        // If you prefer we skip them => we can return None => empty. But let's error:
        Value::Float(_) | Value::Long(_) => Err(format!("array:range => requires i32, got {:?}", val)),
        _ => Err(format!("array:range => not an int or single-element IntArray, got {:?}", val)),
    }
}

/// Return a partial closure capturing whichever of (start, end) we have so far.
/// We do not mutate the environment => the closure is `Fn`.
fn make_two_arg_partial(
    start_opt: Option<Value>,
    end_opt: Option<Value>
) -> Value {
    Value::Function(Box::new(RustClosure(
        "array:range-partial".to_string(),
        Arc::new(Mutex::new(move |_interp: &mut Interpreter, new_args: Vec<Value>| {
            let mut s_current = start_opt.clone();
            let mut e_current = end_opt.clone();

            for arg in new_args {
                // fill start first
                if s_current.is_none() {
                    if matches!(arg, Value::Placeholder) {
                        // remain None
                    } else {
                        s_current = Some(arg);
                    }
                    continue;
                }
                // fill end next
                if e_current.is_none() {
                    if matches!(arg, Value::Placeholder) {
                        // remain None
                    } else {
                        e_current = Some(arg);
                    }
                    continue;
                }
                // If we get here => we already have both => extra => error
                return Err("array:range => partial => too many arguments".to_string());
            }

            // if both known => do_range_final
            if s_current.is_some() && e_current.is_some() {
                do_range_final(
                    s_current.as_ref().unwrap().clone(),
                    e_current.as_ref().unwrap().clone()
                )
            } else {
                // still partial
                Ok(make_two_arg_partial(s_current, e_current))
            }
        })),
        0,
    )))
}