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:assoc` bridging function with placeholder-based partial usage.
/// We want to accept up to 3 arguments: (key, value, object).
///
/// Usage examples:
///
/// ```af
/// keyed = [name:"Alice"]
/// bananaSetter = array:assoc(_, "banana", keyed)
/// sput( bananaSetter("fruit") )  # => [name:"Alice", fruit:"banana"]
/// ```
///
/// The logic is very similar to `array:map` or `array:reduce`, returning
/// partial functions until all three arguments are known (and not `_`).
pub fn array_assoc_bridge(
    _interp: &mut Interpreter,
    mut args: Vec<Value>
) -> Result<Value, String> {
    match args.len() {
        0 => Ok(make_three_arg_partial(None, None, None)),

        1 => {
            let a = args.remove(0);
            match a {
                // _ => key=? still missing
                Value::Placeholder => Ok(make_three_arg_partial(None, None, None)),

                // single string => key=someString
                Value::SingleString(_) => Ok(make_three_arg_partial(Some(a), None, None)),

                other => Err(format!(
                    "array:assoc => first param must be string/_ => got {:?}",
                    other
                )),
            }
        }

        2 => {
            let a1 = args.remove(0);
            let a2 = args.remove(0);

            // The first must be `_` or single string
            let a1_is_pl = matches!(a1, Value::Placeholder);
            if !a1_is_pl && !matches!(a1, Value::SingleString(_)) {
                return Err(format!(
                    "array:assoc => first param must be string/_ => got {:?}",
                    a1
                ));
            }

            let a2_is_pl = matches!(a2, Value::Placeholder);

            // If no placeholders => partial anyway (object is missing)
            Ok(make_three_arg_partial(
                if a1_is_pl { None } else { Some(a1) },
                if a2_is_pl { None } else { Some(a2) },
                None,
            ))
        }

        3 => {
            let a1 = args.remove(0);
            let a2 = args.remove(0);
            let a3 = args.remove(0);

            let a1_pl = matches!(a1, Value::Placeholder);
            let a2_pl = matches!(a2, Value::Placeholder);
            let a3_pl = matches!(a3, Value::Placeholder);

            // If none are placeholders => do_assoc_final
            if !a1_pl && !a2_pl && !a3_pl {
                do_assoc_final(a1, a2, a3)
            } else {
                // partial usage
                Ok(make_three_arg_partial(
                    if a1_pl { None } else { Some(a1) },
                    if a2_pl { None } else { Some(a2) },
                    if a3_pl { None } else { Some(a3) },
                ))
            }
        }

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

/// When all three arguments (key, value, object) are known, we finalize the keyed array insertion.
fn do_assoc_final(
    key_val: Value,
    val_val: Value,
    obj_val: Value
) -> Result<Value, String> {
    // Key must be a single string (not placeholder, not array, etc.)
    let key_str = match key_val {
        Value::SingleString(s) => s,
        other => {
            return Err(format!(
                "array:assoc => first param must be string, got {:?}",
                other
            ));
        }
    };

    // object must be KeyedArray
    match obj_val {
        Value::KeyedArray(m) => {
            let mut new_map = m.clone();
            new_map.insert(key_str, val_val);
            Ok(Value::KeyedArray(new_map))
        }
        other => Err(format!(
            "array:assoc => final object must be a KeyedArray, got {:?}",
            other
        )),
    }
}

/// Returns a partial function capturing whichever of (key, value, object) we have so far.
/// Inside the closure, we do not mutate the captured variables. Instead, we copy them into
/// local mutable variables. This ensures the closure implements `Fn` rather than `FnMut`.
fn make_three_arg_partial(
    key_opt: Option<Value>,
    val_opt: Option<Value>,
    obj_opt: Option<Value>
) -> Value {
    // We'll make local clones so we don't mutate the captured environment variables.
    let k_clone = key_opt.clone();
    let v_clone = val_opt.clone();
    let o_clone = obj_opt.clone();

    Value::Function(Box::new(RustClosure(
        "array:assoc-partial".to_string(),
        Arc::new(Mutex::new(move |_interp: &mut Interpreter, new_args: Vec<Value>| {
            // Create local mut variables from our clones
            let mut k_current = k_clone.clone();
            let mut v_current = v_clone.clone();
            let mut o_current = o_clone.clone();

            // We iterate new_args to fill whichever slots remain missing.
            for arg in new_args {
                // fill key first
                if k_current.is_none() {
                    match arg {
                        Value::Placeholder => {}
                        Value::SingleString(_) => {
                            k_current = Some(arg);
                        }
                        other => {
                            return Err(format!(
                                "array:assoc => first param must be string/_ => got {:?}",
                                other
                            ));
                        }
                    }
                    continue;
                }
                // fill value next
                if v_current.is_none() {
                    if matches!(arg, Value::Placeholder) {
                        // remain None
                    } else {
                        v_current = Some(arg);
                    }
                    continue;
                }
                // fill object last
                if o_current.is_none() {
                    if matches!(arg, Value::Placeholder) {
                        // remain None
                    } else {
                        o_current = Some(arg);
                    }
                    continue;
                }
                // If we get here => we already have all 3 => extra => error
                return Err("array:assoc => partial => too many arguments".to_string());
            }

            // if all 3 are known => do_assoc_final
            if k_current.is_some() && v_current.is_some() && o_current.is_some() {
                do_assoc_final(
                    k_current.as_ref().unwrap().clone(),
                    v_current.as_ref().unwrap().clone(),
                    o_current.as_ref().unwrap().clone(),
                )
            } else {
                // still partial => return a new partial function
                Ok(make_three_arg_partial(k_current, v_current, o_current))
            }
        })),
        0,
    )))
}