zuzu-rust 0.2.0

Rust implementation of ZuzuScript
Documentation
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;

use super::super::{
    FieldSpec, MethodValue, ObjectValue, Runtime, TraitValue, UserClassValue, Value,
};
use crate::error::{Result, ZuzuRustError};

pub(super) fn exports() -> HashMap<String, Value> {
    HashMap::from([(
        "BigNum".to_owned(),
        Value::builtin_class("BigNum".to_owned()),
    )])
}

pub(super) fn call_class_method(
    runtime: &Runtime,
    class_name: &str,
    name: &str,
    args: &[Value],
) -> Option<Result<Value>> {
    if class_name != "BigNum" {
        return None;
    }
    Some(match name {
        "from_dec" => make_from_dec(runtime, args),
        "from_hex" => make_from_hex(runtime, args),
        _ => return None,
    })
}

pub(super) fn call_object_method(
    runtime: &Runtime,
    class_name: &str,
    builtin_value: &Value,
    name: &str,
    args: &[Value],
) -> Option<Result<Value>> {
    if class_name != "BigNum" {
        return None;
    }
    let Value::Dict(fields) = builtin_value else {
        return Some(Err(ZuzuRustError::runtime("BigNum internal state missing")));
    };
    let value = fields
        .get("value")
        .and_then(|v| v.to_number().ok())
        .unwrap_or(0.0);
    let text = fields
        .get("text")
        .map(render_string)
        .unwrap_or_else(|| value.to_string());
    let is_int = fields.get("is_int").map(Value::is_truthy).unwrap_or(false);
    Some(match name {
        "is_int" => Ok(Value::Boolean(is_int)),
        "bcmp" => compare_to(runtime, value, args),
        "beq" => compare_bool(runtime, value, args, |cmp| cmp == 0),
        "bne" => compare_bool(runtime, value, args, |cmp| cmp != 0),
        "blt" => compare_bool(runtime, value, args, |cmp| cmp < 0),
        "ble" => compare_bool(runtime, value, args, |cmp| cmp <= 0),
        "bgt" => compare_bool(runtime, value, args, |cmp| cmp > 0),
        "bge" => compare_bool(runtime, value, args, |cmp| cmp >= 0),
        "babs" => Ok(make_bignum(
            value.abs(),
            trim_decimal(&value.abs().to_string()),
            is_int,
        )),
        "bneg" => Ok(make_bignum(
            -value,
            trim_decimal(&(-value).to_string()),
            is_int,
        )),
        "binv" => Ok(make_bignum_auto(1.0 / value)),
        "bsin" => Ok(make_bignum(
            value.sin(),
            trim_decimal(&value.sin().to_string()),
            false,
        )),
        "bcos" => Ok(make_bignum_auto(value.cos())),
        "btan" => Ok(make_bignum(
            value.tan(),
            trim_decimal(&value.tan().to_string()),
            false,
        )),
        "bsqrt" => Ok(make_bignum_auto(value.sqrt())),
        "bround" => Ok(make_bignum(
            value.round(),
            trim_decimal(&value.round().to_string()),
            true,
        )),
        "bfloor" => Ok(make_bignum(
            value.floor(),
            trim_decimal(&value.floor().to_string()),
            true,
        )),
        "bceil" => Ok(make_bignum(
            value.ceil(),
            trim_decimal(&value.ceil().to_string()),
            true,
        )),
        "badd" => binary_num(
            runtime,
            value,
            args,
            |left, right| left + right,
            Some(false),
        ),
        "bsub" => binary_num(
            runtime,
            value,
            args,
            |left, right| left - right,
            Some(false),
        ),
        "bmul" => binary_num(
            runtime,
            value,
            args,
            |left, right| left * right,
            Some(false),
        ),
        "bdiv" => binary_num(runtime, value, args, |left, right| left / right, None),
        "bmod" => binary_num(runtime, value, args, |left, right| left % right, None),
        "bpow" => binary_num(runtime, value, args, |left, right| left.powf(right), None),
        "to_hex" => Ok(Value::String(format!("0x{:x}", value.trunc() as i64))),
        "to_dec" | "to_String" => Ok(if is_int {
            Value::Number(value.trunc())
        } else {
            Value::String(trim_decimal(&text))
        }),
        "to_Number" => Ok(Value::Number(value)),
        _ => return None,
    })
}

pub(super) fn has_builtin_object_method(class_name: &str, name: &str) -> bool {
    class_name == "BigNum"
        && matches!(
            name,
            "is_int"
                | "bcmp"
                | "beq"
                | "bne"
                | "blt"
                | "ble"
                | "bgt"
                | "bge"
                | "babs"
                | "bneg"
                | "binv"
                | "bsin"
                | "bcos"
                | "btan"
                | "bsqrt"
                | "bround"
                | "bfloor"
                | "bceil"
                | "badd"
                | "bsub"
                | "bmul"
                | "bdiv"
                | "bmod"
                | "bpow"
                | "to_hex"
                | "to_dec"
                | "to_String"
                | "to_Number"
        )
}

fn class() -> Rc<UserClassValue> {
    Rc::new(UserClassValue {
        name: "BigNum".to_owned(),
        base: None,
        traits: Vec::<Rc<TraitValue>>::new(),
        fields: vec![
            FieldSpec {
                name: "value".to_owned(),
                declared_type: Some("Number".to_owned()),
                mutable: true,
                accessors: Vec::new(),
                default_value: None,
                is_weak_storage: false,
            },
            FieldSpec {
                name: "text".to_owned(),
                declared_type: Some("String".to_owned()),
                mutable: true,
                accessors: Vec::new(),
                default_value: None,
                is_weak_storage: false,
            },
            FieldSpec {
                name: "is_int".to_owned(),
                declared_type: Some("Boolean".to_owned()),
                mutable: true,
                accessors: Vec::new(),
                default_value: None,
                is_weak_storage: false,
            },
        ],
        methods: HashMap::<String, Rc<MethodValue>>::new(),
        static_methods: HashMap::<String, Rc<MethodValue>>::new(),
        nested_classes: HashMap::new(),
        source_decl: None,
        closure_env: None,
    })
}

fn make_from_dec(runtime: &Runtime, args: &[Value]) -> Result<Value> {
    let text = args
        .first()
        .map(|value| runtime.render_value(value))
        .transpose()?
        .unwrap_or_else(|| "0".to_owned());
    let trimmed = text.trim();
    let parsed = trimmed.parse::<f64>().unwrap_or(0.0);
    Ok(make_bignum(
        parsed,
        if trimmed.is_empty() {
            "0".to_owned()
        } else {
            trimmed.to_owned()
        },
        !trimmed.contains(['.', 'e', 'E']),
    ))
}

fn make_from_hex(runtime: &Runtime, args: &[Value]) -> Result<Value> {
    let text = args
        .first()
        .map(|value| runtime.render_value(value))
        .transpose()?
        .unwrap_or_else(|| "0".to_owned());
    let trimmed = text
        .trim()
        .trim_start_matches("0x")
        .trim_start_matches("0X");
    let parsed =
        i64::from_str_radix(if trimmed.is_empty() { "0" } else { trimmed }, 16).unwrap_or(0) as f64;
    Ok(make_bignum(parsed, trim_decimal(&parsed.to_string()), true))
}

fn make_bignum(value: f64, text: String, is_int: bool) -> Value {
    let fields = HashMap::from([
        ("value".to_owned(), Value::Number(value)),
        ("text".to_owned(), Value::String(text.clone())),
        ("is_int".to_owned(), Value::Boolean(is_int)),
    ]);
    Value::Object(Rc::new(RefCell::new(ObjectValue {
        class: class(),
        fields: fields.clone(),
        weak_fields: std::collections::HashSet::new(),
        builtin_value: Some(Value::Dict(fields)),
    })))
}

fn binary_num(
    _runtime: &Runtime,
    left: f64,
    args: &[Value],
    op: impl FnOnce(f64, f64) -> f64,
    is_int: Option<bool>,
) -> Result<Value> {
    let right = args.first().map(coerce_other).transpose()?.unwrap_or(0.0);
    let value = op(left, right);
    Ok(match is_int {
        Some(is_int) => make_bignum(value, trim_decimal(&value.to_string()), is_int),
        None => make_bignum_auto(value),
    })
}

fn compare_to(_runtime: &Runtime, left: f64, args: &[Value]) -> Result<Value> {
    let right = args.first().map(coerce_other).transpose()?.unwrap_or(0.0);
    Ok(Value::Number(if left < right {
        -1.0
    } else if left > right {
        1.0
    } else {
        0.0
    }))
}

fn compare_bool(
    _runtime: &Runtime,
    left: f64,
    args: &[Value],
    test: impl FnOnce(i32) -> bool,
) -> Result<Value> {
    let right = args.first().map(coerce_other).transpose()?.unwrap_or(0.0);
    let cmp = if left < right {
        -1
    } else if left > right {
        1
    } else {
        0
    };
    Ok(Value::Boolean(test(cmp)))
}

fn coerce_other(value: &Value) -> Result<f64> {
    match value {
        Value::Object(object) if object.borrow().class.name == "BigNum" => Ok(object
            .borrow()
            .fields
            .get("value")
            .and_then(|v| v.to_number().ok())
            .unwrap_or(0.0)),
        other => other.to_number(),
    }
}

fn render_string(value: &Value) -> String {
    match value {
        Value::String(value) => value.clone(),
        Value::Number(value) => value.to_string(),
        other => other.render(),
    }
}

fn trim_decimal(text: &str) -> String {
    if text.contains('.') {
        text.trim_end_matches('0').trim_end_matches('.').to_owned()
    } else {
        text.to_owned()
    }
}

fn make_bignum_auto(value: f64) -> Value {
    make_bignum(
        value,
        trim_decimal(&value.to_string()),
        value.fract() == 0.0,
    )
}