runmat-vm 0.4.4

RunMat virtual machine and bytecode interpreter
Documentation
use crate::interpreter::errors::mex;
use runmat_builtins::{self, Access, Closure, StructValue, Value};
use runmat_runtime::RuntimeError;

pub async fn load_member(
    base: Value,
    field: String,
    allow_init: bool,
) -> Result<Value, RuntimeError> {
    match base {
        Value::Object(obj) => {
            if let Some((p, _owner)) = runmat_builtins::lookup_property(&obj.class_name, &field) {
                if p.is_static {
                    return Err(format!(
                        "Property '{}' is static; use classref('{}').{}",
                        field, obj.class_name, field
                    )
                    .into());
                }
                if p.get_access == Access::Private {
                    return Err(format!("Property '{}' is private", field).into());
                }
                if p.is_dependent {
                    let getter = format!("get.{field}");
                    if let Ok(v) =
                        runmat_runtime::call_builtin_async(&getter, &[Value::Object(obj.clone())])
                            .await
                    {
                        return Ok(v);
                    }
                }
            }
            if let Some(v) = obj.properties.get(&field) {
                Ok(v.clone())
            } else if let Some((p2, _)) = runmat_builtins::lookup_property(&obj.class_name, &field)
            {
                if p2.is_dependent {
                    let backing = format!("{field}_backing");
                    if let Some(vb) = obj.properties.get(&backing) {
                        return Ok(vb.clone());
                    }
                }
                Err(format!(
                    "Undefined property '{}' for class {}",
                    field, obj.class_name
                )
                .into())
            } else if let Some(cls) = runmat_builtins::get_class(&obj.class_name) {
                if cls.methods.contains_key("subsref") {
                    let args = vec![
                        Value::Object(obj),
                        Value::String("subsref".to_string()),
                        Value::String(".".to_string()),
                        Value::String(field),
                    ];
                    runmat_runtime::call_builtin_async("call_method", &args).await
                } else {
                    Err(format!(
                        "Undefined property '{}' for class {}",
                        field, obj.class_name
                    )
                    .into())
                }
            } else {
                Err(format!("Unknown class {}", obj.class_name).into())
            }
        }
        Value::HandleObject(handle) => {
            let args = vec![
                Value::HandleObject(handle),
                Value::String("subsref".to_string()),
                Value::String(".".to_string()),
                Value::String(field),
            ];
            runmat_runtime::call_builtin_async("call_method", &args).await
        }
        Value::ClassRef(cls) => load_static_member(&cls, &field),
        Value::Struct(st) => {
            if let Some(v) = st.fields.get(&field) {
                Ok(v.clone())
            } else if allow_init {
                Ok(Value::Struct(StructValue::new()))
            } else {
                Err(format!("Undefined field '{}'", field).into())
            }
        }
        Value::Cell(ca) => {
            let mut out: Vec<Value> = Vec::with_capacity(ca.data.len());
            for v in &ca.data {
                match &**v {
                    Value::Struct(st) => {
                        if let Some(fv) = st.fields.get(&field) {
                            out.push(fv.clone());
                        } else {
                            out.push(Value::Num(0.0));
                        }
                    }
                    other => out.push(other.clone()),
                }
            }
            let new_cell = runmat_builtins::CellArray::new(out, ca.rows, ca.cols)
                .map_err(|e| format!("cell field gather: {e}"))?;
            Ok(Value::Cell(new_cell))
        }
        Value::MException(mexn) => {
            let value = match field.as_str() {
                "identifier" => Value::String(mexn.identifier.clone()),
                "message" => Value::String(mexn.message.clone()),
                "stack" => {
                    let values: Vec<Value> = mexn
                        .stack
                        .iter()
                        .map(|s| Value::String(s.clone()))
                        .collect();
                    let rows = values.len();
                    let cell = runmat_builtins::CellArray::new(values, rows, 1)
                        .map_err(|e| format!("MException.stack: {e}"))?;
                    Value::Cell(cell)
                }
                other => return Err(format!("Reference to non-existent field '{}'.", other).into()),
            };
            Ok(value)
        }
        _ => Err(mex("LoadMember", "LoadMember on non-object")),
    }
}

pub async fn load_member_dynamic(
    base: Value,
    name: String,
    allow_init: bool,
) -> Result<Value, RuntimeError> {
    load_member(base, name, allow_init).await
}

pub fn load_static_member(cls: &str, field: &str) -> Result<Value, RuntimeError> {
    if let Some((p, owner)) = runmat_builtins::lookup_property(cls, field) {
        if !p.is_static {
            return Err(format!("Property '{}' is not static", field).into());
        }
        if p.get_access == Access::Private {
            return Err(format!("Property '{}' is private", field).into());
        }
        if let Some(v) = runmat_builtins::get_static_property_value(&owner, field) {
            Ok(v)
        } else if let Some(v) = &p.default_value {
            Ok(v.clone())
        } else {
            Ok(Value::Num(0.0))
        }
    } else if let Some((m, _owner)) = runmat_builtins::lookup_method(cls, field) {
        if !m.is_static {
            return Err(format!("Method '{}' is not static", field).into());
        }
        Ok(Value::Closure(Closure {
            function_name: m.function_name,
            captures: vec![],
        }))
    } else {
        let qualified = format!("{cls}.{field}");
        if runmat_builtins::builtin_functions()
            .iter()
            .any(|b| b.name == qualified)
        {
            Ok(Value::Closure(Closure {
                function_name: qualified,
                captures: vec![],
            }))
        } else {
            Err(format!("Unknown property '{}' on class {}", field, cls).into())
        }
    }
}

pub async fn store_member<OnWrite>(
    base: Value,
    field: String,
    rhs: Value,
    allow_init: bool,
    mut on_write: OnWrite,
) -> Result<Value, RuntimeError>
where
    OnWrite: FnMut(&Value, &Value),
{
    match base {
        Value::Object(mut obj) => {
            if let Some((p, _owner)) = runmat_builtins::lookup_property(&obj.class_name, &field) {
                if p.is_static {
                    return Err(format!(
                        "Property '{}' is static; use classref('{}').{}",
                        field, obj.class_name, field
                    )
                    .into());
                }
                if p.set_access == Access::Private {
                    return Err(format!("Property '{}' is private", field).into());
                }
                if p.is_dependent {
                    let setter = format!("set.{field}");
                    if let Ok(v) = runmat_runtime::call_builtin_async(
                        &setter,
                        &[Value::Object(obj.clone()), rhs.clone()],
                    )
                    .await
                    {
                        return Ok(v);
                    }
                }
                if let Some(oldv) = obj.properties.get(&field) {
                    on_write(oldv, &rhs);
                }
                obj.properties.insert(field, rhs);
                Ok(Value::Object(obj))
            } else if let Some(cls) = runmat_builtins::get_class(&obj.class_name) {
                if cls.methods.contains_key("subsasgn") {
                    let args = vec![
                        Value::Object(obj),
                        Value::String("subsasgn".to_string()),
                        Value::String(".".to_string()),
                        Value::String(field),
                        rhs,
                    ];
                    runmat_runtime::call_builtin_async("call_method", &args).await
                } else {
                    Err(format!("Undefined property '{}' for class {}", field, cls.name).into())
                }
            } else {
                Err(format!("Unknown class {}", obj.class_name).into())
            }
        }
        Value::ClassRef(cls) => {
            if let Some((p, owner)) = runmat_builtins::lookup_property(&cls, &field) {
                if !p.is_static {
                    return Err(format!("Property '{}' is not static", field).into());
                }
                if p.set_access == Access::Private {
                    return Err(format!("Property '{}' is private", field).into());
                }
                runmat_builtins::set_static_property_value_in_owner(&owner, &field, rhs)?;
                Ok(Value::ClassRef(cls))
            } else {
                Err(format!("Unknown property '{}' on class {}", field, cls).into())
            }
        }
        Value::HandleObject(handle) => {
            let args = vec![
                Value::HandleObject(handle),
                Value::String("subsasgn".to_string()),
                Value::String(".".to_string()),
                Value::String(field),
                rhs,
            ];
            runmat_runtime::call_builtin_async("call_method", &args).await
        }
        Value::Struct(mut st) => {
            if let Some(oldv) = st.fields.get(&field) {
                on_write(oldv, &rhs);
            }
            st.fields.insert(field, rhs);
            Ok(Value::Struct(st))
        }
        Value::Cell(mut ca) => {
            let rhs_cell = if let Value::Cell(rc) = &rhs {
                Some(rc)
            } else {
                None
            };
            if let Some(rc) = rhs_cell {
                if rc.rows != ca.rows || rc.cols != ca.cols {
                    return Err("Field assignment: cell rhs shape mismatch"
                        .to_string()
                        .into());
                }
            }
            for i in 0..ca.data.len() {
                let rv = if let Some(rc) = rhs_cell {
                    (*rc.data[i]).clone()
                } else {
                    rhs.clone()
                };
                match &mut *ca.data[i] {
                    Value::Struct(st) => {
                        if let Some(oldv) = st.fields.get(&field) {
                            on_write(oldv, &rv);
                        }
                        st.fields.insert(field.clone(), rv);
                    }
                    other => {
                        let mut st = StructValue::new();
                        st.fields.insert(field.clone(), rv);
                        *other = Value::Struct(st);
                    }
                }
            }
            Ok(Value::Cell(ca))
        }
        Value::Num(0.0) if allow_init => {
            let mut st = StructValue::new();
            st.fields.insert(field, rhs);
            Ok(Value::Struct(st))
        }
        _ => Err(mex("StoreMember", "StoreMember on non-object")),
    }
}

pub async fn store_member_dynamic<OnWrite>(
    base: Value,
    name: String,
    rhs: Value,
    allow_init: bool,
    on_write: OnWrite,
) -> Result<Value, RuntimeError>
where
    OnWrite: FnMut(&Value, &Value),
{
    store_member(base, name, rhs, allow_init, on_write).await
}