zuzu-rust 0.6.0

Rust implementation of ZuzuScript
Documentation
use crate::{Result, ZuzuRustError};

use super::super::{Runtime, Value};
use super::common::{
    collection_get, collection_product, collection_sum, expect_function, make_iterator,
    optional_count, require_arity, require_arity_range, signed_index, stored_arg, unique_values,
    CollectionTarget,
};

fn random_index(max: usize) -> Result<usize> {
    if max == 0 {
        return Ok(0);
    }
    let mut bytes = [0_u8; 8];
    getrandom::getrandom(&mut bytes)
        .map_err(|err| ZuzuRustError::runtime(format!("random failed: {err}")))?;
    Ok((u64::from_le_bytes(bytes) % max as u64) as usize)
}

fn shuffled_values(values: &[Value]) -> Result<Vec<Value>> {
    let mut out = values.to_vec();
    for idx in (1..out.len()).rev() {
        let swap_idx = random_index(idx + 1)?;
        out.swap(idx, swap_idx);
    }
    Ok(out)
}

impl Runtime {
    pub(in crate::runtime) fn call_array_method(
        &self,
        values: &mut Vec<Value>,
        name: &str,
        args: &[Value],
    ) -> Result<Value> {
        match name {
            "append" | "push" | "add" => {
                values.extend(args.iter().cloned().map(Value::into_shared_if_composite));
                Ok(Value::Array(values.clone()))
            }
            "push_weak" => {
                values.extend(args.iter().map(|value| stored_arg(value, true)));
                Ok(Value::Array(values.clone()))
            }
            "copy" => {
                require_arity(name, args, 0)?;
                Ok(Value::Array(values.clone()))
            }
            "pop" => {
                require_arity(name, args, 0)?;
                Ok(values.pop().unwrap_or(Value::Null))
            }
            "prepend" | "unshift" => {
                let mut out = args
                    .iter()
                    .cloned()
                    .map(Value::into_shared_if_composite)
                    .collect::<Vec<_>>();
                out.extend(values.iter().cloned());
                *values = out;
                Ok(Value::Array(values.clone()))
            }
            "unshift_weak" => {
                let mut out = args
                    .iter()
                    .map(|value| stored_arg(value, true))
                    .collect::<Vec<_>>();
                out.extend(values.iter().cloned());
                *values = out;
                Ok(Value::Array(values.clone()))
            }
            "shift" => {
                require_arity(name, args, 0)?;
                if values.is_empty() {
                    Ok(Value::Null)
                } else {
                    Ok(values.remove(0))
                }
            }
            "length" | "count" => {
                require_arity(name, args, 0)?;
                Ok(Value::Number(values.len() as f64))
            }
            "empty" | "is_empty" => {
                require_arity(name, args, 0)?;
                Ok(Value::Boolean(values.is_empty()))
            }
            "get" => collection_get(values, args),
            "set" => {
                require_arity(name, args, 2)?;
                let index = signed_index(values.len(), &args[0]);
                if index < 0 {
                    return Err(ZuzuRustError::runtime("Array index is out of range"));
                }
                let index = index as usize;
                if index >= values.len() {
                    values.resize(index, Value::Null);
                    values.push(args[1].clone().into_shared_if_composite());
                } else {
                    values[index] = args[1].clone().into_shared_if_composite();
                }
                Ok(Value::Array(values.clone()))
            }
            "set_weak" => {
                require_arity(name, args, 2)?;
                let index = signed_index(values.len(), &args[0]);
                if index < 0 {
                    return Err(ZuzuRustError::runtime("Array index is out of range"));
                }
                let index = index as usize;
                let value = stored_arg(&args[1], true);
                if index >= values.len() {
                    values.resize(index, Value::Null);
                    values.push(value);
                } else {
                    values[index] = value;
                }
                Ok(Value::Array(values.clone()))
            }
            "clear" => {
                require_arity(name, args, 0)?;
                values.clear();
                Ok(Value::Array(values.clone()))
            }
            "to_Array" => {
                require_arity(name, args, 0)?;
                Ok(Value::Array(values.clone()))
            }
            "to_Set" => Ok(Value::Set(unique_values(values))),
            "to_Bag" => Ok(Value::Bag(values.clone())),
            "to_Iterator" => {
                require_arity(name, args, 0)?;
                Ok(make_iterator(values.clone()))
            }
            "sort" => {
                require_arity(name, args, 1)?;
                let cmp = expect_function(&args[0], "Collection sort expects a function callback")?;
                let mut out = values.clone();
                out.sort_by(|left, right| {
                    self.compare_via_callback(&cmp, left.clone(), right.clone())
                        .unwrap_or(std::cmp::Ordering::Equal)
                });
                Ok(Value::Array(out))
            }
            "sortstr" => {
                require_arity(name, args, 0)?;
                let mut out = values.clone();
                out.sort_by_key(|value| value.render());
                Ok(Value::Array(out))
            }
            "sortnum" => {
                require_arity(name, args, 0)?;
                let mut out = values.clone();
                out.sort_by(|left, right| {
                    let lhs = left.to_number().unwrap_or(0.0);
                    let rhs = right.to_number().unwrap_or(0.0);
                    lhs.partial_cmp(&rhs).unwrap_or(std::cmp::Ordering::Equal)
                });
                for value in &mut out {
                    if let Ok(number) = value.to_number() {
                        *value = Value::Number(number);
                    }
                }
                Ok(Value::Array(out))
            }
            "reverse" => {
                require_arity(name, args, 0)?;
                let mut out = values.clone();
                out.reverse();
                Ok(Value::Array(out))
            }
            "head" => {
                let n = optional_count(args, 1)?;
                Ok(Value::Array(values.iter().take(n).cloned().collect()))
            }
            "tail" => {
                let n = optional_count(args, 1)?;
                if n == 0 {
                    return Ok(Value::Array(Vec::new()));
                }
                let start = values.len().saturating_sub(n);
                Ok(Value::Array(values[start..].to_vec()))
            }
            "slice" => {
                if args.is_empty() || args.len() > 2 {
                    return Err(ZuzuRustError::runtime(
                        "slice() expects one or two arguments",
                    ));
                }
                let len = values.len() as isize;
                let mut start = args[0].to_number().unwrap_or(0.0) as isize;
                if start < 0 {
                    start += len;
                }
                start = start.clamp(0, len);
                let end = if args.len() == 2 {
                    let mut end = args[1].to_number().unwrap_or(len as f64) as isize;
                    if end < 0 {
                        end += len;
                    }
                    end.clamp(0, len)
                } else {
                    len
                };
                let from = start.min(end) as usize;
                let to = start.max(end) as usize;
                Ok(Value::Array(values[from..to].to_vec()))
            }
            "join" => {
                require_arity_range(name, args, 1, 2)?;
                let separator = self.value_to_operator_string(&args[0])?;
                let mut fallback_string = None;
                let mut out = Vec::new();
                for value in values.iter() {
                    match self.value_to_operator_string(value) {
                        Ok(text) => out.push(text),
                        Err(_) if args.len() == 2 => {
                            if let Value::Function(function) = &args[1] {
                                let replacement = self.await_if_task(self.call_function(
                                    function,
                                    vec![value.clone()],
                                    Vec::new(),
                                )?)?;
                                out.push(self.value_to_operator_string(&replacement)?);
                            } else {
                                if fallback_string.is_none() {
                                    fallback_string =
                                        Some(self.value_to_operator_string(&args[1])?);
                                }
                                out.push(fallback_string.clone().unwrap_or_default());
                            }
                        }
                        Err(err) => return Err(err),
                    }
                }
                Ok(Value::String(out.join(&separator)))
            }
            "sum" => {
                require_arity(name, args, 0)?;
                Ok(Value::Number(collection_sum(values)?))
            }
            "product" => {
                require_arity(name, args, 0)?;
                Ok(Value::Number(collection_product(values)?))
            }
            "shuffle" => {
                require_arity(name, args, 0)?;
                Ok(Value::Array(shuffled_values(values)?))
            }
            "sample" => {
                let n = optional_count(args, 1)?;
                Ok(Value::Array(
                    shuffled_values(values)?.into_iter().take(n).collect(),
                ))
            }
            "contains" => {
                require_arity(name, args, 1)?;
                Ok(Value::Boolean(
                    values.iter().any(|value| value.strict_eq(&args[0])),
                ))
            }
            "map" => self.map_values(values, args, "map", CollectionTarget::Array),
            "grep" => self.filter_values(values, args, "grep", CollectionTarget::Array),
            "any" => self.any_values(values, args),
            "all" => self.all_values(values, args),
            "first" => self.first_value(values, args),
            "remove" => {
                require_arity(name, args, 1)?;
                let pred =
                    expect_function(&args[0], "Collection method expects a function callback")?;
                values.retain(|value| {
                    !self
                        .predicate_callback(&pred, value.clone())
                        .unwrap_or(false)
                });
                Ok(Value::Array(values.clone()))
            }
            "first_index" => {
                require_arity(name, args, 1)?;
                let pred =
                    expect_function(&args[0], "Collection method expects a function callback")?;
                for (index, value) in values.iter().enumerate() {
                    if self.predicate_callback(&pred, value.clone())? {
                        return Ok(Value::Number(index as f64));
                    }
                }
                Ok(Value::Number(-1.0))
            }
            "for_each_value" => {
                require_arity(name, args, 1)?;
                let func =
                    expect_function(&args[0], "Collection method expects a function callback")?;
                for value in values.iter().cloned() {
                    let _ = self.call_function(&func, vec![value], Vec::new())?;
                }
                Ok(Value::Array(values.clone()))
            }
            "reduce" => self.reduce_values(values, args, false),
            "reductions" => self.reduce_values(values, args, true),
            other => Err(ZuzuRustError::thrown(format!(
                "unsupported Array method '{}'",
                other
            ))),
        }
    }
}