runmat-runtime 0.6.0

Core runtime for RunMat with builtins, BLAS/LAPACK integration, and execution APIs
Documentation
pub(crate) mod digits;
pub(crate) mod int;
pub(crate) mod limit;
pub(crate) mod piecewise;
pub(crate) mod sym;
pub(crate) mod syms;
pub(crate) mod vpa;

use runmat_builtins::{
    symbolic::{is_valid_symbolic_identifier, SymbolicFunction},
    SymbolicArray, SymbolicExpr, Tensor, Value,
};

use crate::builtins::common::broadcast::BroadcastPlan;
use crate::builtins::common::shape::is_scalar_shape;

#[derive(Debug, Clone, Copy)]
pub(crate) enum SymbolicBinaryOp {
    Add,
    Sub,
    Mul,
    Div,
    Pow,
    Eq,
}

pub(crate) fn symbolic_named_binary(lhs: &Value, rhs: &Value, name: &str) -> Option<Value> {
    let (lhs, rhs) = symbolic_binary_operands(lhs, rhs)?;
    Some(symbolic_expr_to_value(SymbolicExpr::function_call(
        name,
        vec![lhs, rhs],
    )))
}

pub(crate) fn symbolic_binary(lhs: &Value, rhs: &Value, op: SymbolicBinaryOp) -> Option<Value> {
    let (lhs, rhs) = symbolic_binary_operands(lhs, rhs)?;
    let expr = match op {
        SymbolicBinaryOp::Add => SymbolicExpr::add_expr(lhs, rhs),
        SymbolicBinaryOp::Sub => SymbolicExpr::sub_expr(lhs, rhs),
        SymbolicBinaryOp::Mul => SymbolicExpr::mul_expr(lhs, rhs),
        SymbolicBinaryOp::Div => SymbolicExpr::div_expr(lhs, rhs),
        SymbolicBinaryOp::Pow => SymbolicExpr::pow_expr(lhs, rhs),
        SymbolicBinaryOp::Eq => SymbolicExpr::equation(lhs, rhs),
    };
    Some(symbolic_expr_to_value(expr))
}

pub(crate) fn symbolic_binary_broadcast(
    lhs: &Value,
    rhs: &Value,
    op: SymbolicBinaryOp,
) -> Result<Option<Value>, String> {
    if !contains_symbolic_value(lhs) && !contains_symbolic_value(rhs) {
        return Ok(None);
    }

    let Some(lhs) = SymbolicOperand::from_value(lhs) else {
        return Ok(None);
    };
    let Some(rhs) = SymbolicOperand::from_value(rhs) else {
        return Ok(None);
    };

    let plan = BroadcastPlan::new(lhs.shape(), rhs.shape())?;
    let mut data = Vec::with_capacity(plan.len());
    for (_, lhs_idx, rhs_idx) in plan.iter() {
        let lhs_expr = lhs.expr_at(lhs_idx).clone();
        let rhs_expr = rhs.expr_at(rhs_idx).clone();
        data.push(match op {
            SymbolicBinaryOp::Add => SymbolicExpr::add_expr(lhs_expr, rhs_expr),
            SymbolicBinaryOp::Sub => SymbolicExpr::sub_expr(lhs_expr, rhs_expr),
            SymbolicBinaryOp::Mul => SymbolicExpr::mul_expr(lhs_expr, rhs_expr),
            SymbolicBinaryOp::Div => SymbolicExpr::div_expr(lhs_expr, rhs_expr),
            SymbolicBinaryOp::Pow => SymbolicExpr::pow_expr(lhs_expr, rhs_expr),
            SymbolicBinaryOp::Eq => SymbolicExpr::equation(lhs_expr, rhs_expr),
        });
    }

    if data.len() == 1 && is_scalar_shape(plan.output_shape()) {
        return Ok(Some(Value::Symbolic(data.remove(0))));
    }

    SymbolicArray::new(data, plan.output_shape().to_vec())
        .map(Value::SymbolicArray)
        .map(Some)
}

fn contains_symbolic_value(value: &Value) -> bool {
    matches!(value, Value::Symbolic(_) | Value::SymbolicArray(_))
}

struct SymbolicOperand {
    data: Vec<SymbolicExpr>,
    shape: Vec<usize>,
}

impl SymbolicOperand {
    fn scalar(expr: SymbolicExpr) -> Self {
        Self {
            data: vec![expr],
            shape: vec![1, 1],
        }
    }

    fn from_value(value: &Value) -> Option<Self> {
        match value {
            Value::Symbolic(expr) => Some(Self::scalar(expr.clone())),
            Value::SymbolicArray(array) => Some(Self {
                data: array.data.clone(),
                shape: normalize_symbolic_shape(&array.shape),
            }),
            Value::Num(value) => Some(Self::scalar(SymbolicExpr::constant(*value))),
            Value::Int(value) => Some(Self::scalar(SymbolicExpr::constant(value.to_f64()))),
            Value::Bool(value) => Some(Self::scalar(SymbolicExpr::constant(if *value {
                1.0
            } else {
                0.0
            }))),
            Value::Tensor(tensor) => Some(Self {
                data: tensor
                    .data
                    .iter()
                    .copied()
                    .map(SymbolicExpr::constant)
                    .collect(),
                shape: normalize_symbolic_shape(&tensor.shape),
            }),
            Value::LogicalArray(array) => Some(Self {
                data: array
                    .data
                    .iter()
                    .map(|value| SymbolicExpr::constant(if *value == 0 { 0.0 } else { 1.0 }))
                    .collect(),
                shape: normalize_symbolic_shape(&array.shape),
            }),
            _ => None,
        }
    }

    fn shape(&self) -> &[usize] {
        &self.shape
    }

    fn expr_at(&self, index: usize) -> &SymbolicExpr {
        &self.data[index]
    }
}

fn normalize_symbolic_shape(shape: &[usize]) -> Vec<usize> {
    if shape.len() == 1 && shape[0] != 1 {
        vec![1, shape[0]]
    } else if is_scalar_shape(shape) {
        vec![1, 1]
    } else {
        shape.to_vec()
    }
}

fn symbolic_binary_operands(lhs: &Value, rhs: &Value) -> Option<(SymbolicExpr, SymbolicExpr)> {
    if !matches!(lhs, Value::Symbolic(_)) && !matches!(rhs, Value::Symbolic(_)) {
        return None;
    }
    let lhs = value_to_symbolic_scalar(lhs)?;
    let rhs = value_to_symbolic_scalar(rhs)?;
    Some((lhs, rhs))
}

pub(crate) fn symbolic_function(value: &Value, function: SymbolicFunction) -> Option<Value> {
    let expr = match value {
        Value::Symbolic(expr) => expr.clone(),
        _ => return None,
    };
    Some(symbolic_expr_to_value(SymbolicExpr::function(
        function, expr,
    )))
}

pub(crate) fn value_to_symbolic_scalar(value: &Value) -> Option<SymbolicExpr> {
    match value {
        Value::Symbolic(expr) => Some(expr.clone()),
        Value::Num(value) => Some(SymbolicExpr::constant(*value)),
        Value::Int(value) => Some(SymbolicExpr::constant(value.to_f64())),
        Value::Bool(value) => Some(SymbolicExpr::constant(if *value { 1.0 } else { 0.0 })),
        Value::Tensor(tensor) if tensor.data.len() == 1 => {
            Some(SymbolicExpr::constant(tensor.data[0]))
        }
        _ => None,
    }
}

pub(crate) fn symbolic_expr_to_value(expr: SymbolicExpr) -> Value {
    Value::Symbolic(expr)
}

pub(crate) fn symbolic_variable_name_from_value(value: &Value) -> Option<String> {
    match value {
        Value::Symbolic(expr) => expr.variable_name().map(ToOwned::to_owned),
        _ => text_scalar(value).map(|text| text.trim().to_string()),
    }
    .filter(|name| is_valid_symbolic_identifier(name))
}

pub(crate) fn empty_return_value() -> Value {
    Value::Tensor(Tensor::zeros(vec![0, 0]))
}

pub(crate) fn text_scalar(value: &Value) -> Option<String> {
    match value {
        Value::String(text) => Some(text.clone()),
        Value::CharArray(chars) if chars.rows == 1 => Some(chars.data.iter().collect()),
        Value::StringArray(array) if array.data.len() == 1 => Some(array.data[0].clone()),
        _ => None,
    }
}

pub(crate) fn is_valid_identifier(name: &str) -> bool {
    is_valid_symbolic_identifier(name)
}