runmat-runtime 0.6.0

Core runtime for RunMat with builtins, BLAS/LAPACK integration, and execution APIs
Documentation
//! Minimal symbolic `piecewise` builtin.

use runmat_builtins::{
    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
    SymbolicExpr, Value,
};
use runmat_macros::runtime_builtin;

use crate::{build_runtime_error, BuiltinResult, RuntimeError};

use super::{symbolic_expr_to_value, value_to_symbolic_scalar};

const BUILTIN_NAME: &str = "piecewise";

const PIECEWISE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "f",
    ty: BuiltinParamType::Any,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Symbolic piecewise expression.",
}];

const PIECEWISE_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "arg",
    ty: BuiltinParamType::Any,
    arity: BuiltinParamArity::Variadic,
    default: None,
    description: "Condition/value pairs and optional default value.",
}];

const PIECEWISE_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
    label: "f = piecewise(cond1, value1, ...)",
    inputs: &PIECEWISE_INPUTS,
    outputs: &PIECEWISE_OUTPUT,
}];

const PIECEWISE_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.PIECEWISE.INVALID_INPUT",
    identifier: Some("RunMat:piecewise:InvalidInput"),
    when: "Arguments cannot be represented by the scalar symbolic core.",
    message: "piecewise: expected symbolic or scalar numeric arguments",
};

const PIECEWISE_ERRORS: [BuiltinErrorDescriptor; 1] = [PIECEWISE_ERROR_INVALID_INPUT];

pub const PIECEWISE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
    signatures: &PIECEWISE_SIGNATURES,
    output_mode: BuiltinOutputMode::Fixed,
    completion_policy: BuiltinCompletionPolicy::Public,
    errors: &PIECEWISE_ERRORS,
};

fn piecewise_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
    let mut builder = build_runtime_error(error.message).with_builtin(BUILTIN_NAME);
    if let Some(identifier) = error.identifier {
        builder = builder.with_identifier(identifier);
    }
    builder.build()
}

#[runtime_builtin(
    name = "piecewise",
    category = "math/symbolic",
    summary = "Construct a scalar symbolic piecewise expression.",
    keywords = "piecewise,symbolic,condition",
    descriptor(crate::builtins::math::symbolic::piecewise::PIECEWISE_DESCRIPTOR),
    builtin_path = "crate::builtins::math::symbolic::piecewise"
)]
async fn piecewise_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
    if args.len() < 2 {
        return Err(piecewise_error(&PIECEWISE_ERROR_INVALID_INPUT));
    }

    let mut symbolic_args = Vec::with_capacity(args.len());
    for arg in &args {
        let Some(expr) = value_to_symbolic_scalar(arg) else {
            return Err(piecewise_error(&PIECEWISE_ERROR_INVALID_INPUT));
        };
        symbolic_args.push(expr);
    }

    Ok(symbolic_expr_to_value(SymbolicExpr::function_call(
        BUILTIN_NAME,
        symbolic_args,
    )))
}

#[cfg(test)]
mod tests {
    use super::*;
    use futures::executor::block_on;

    #[test]
    fn builds_symbolic_piecewise_expression() {
        let result = block_on(piecewise_builtin(vec![
            Value::Symbolic(SymbolicExpr::function_call(
                "lt",
                vec![SymbolicExpr::variable("x"), SymbolicExpr::constant(0.0)],
            )),
            Value::Num(-1.0),
            Value::Symbolic(SymbolicExpr::function_call(
                "gt",
                vec![SymbolicExpr::variable("x"), SymbolicExpr::constant(0.0)],
            )),
            Value::Num(1.0),
        ]))
        .expect("piecewise");

        assert_eq!(result.to_string(), "piecewise(lt(x, 0), -1, gt(x, 0), 1)");
    }

    #[test]
    fn rejects_too_few_arguments() {
        let err = block_on(piecewise_builtin(vec![Value::Symbolic(
            SymbolicExpr::variable("x"),
        )]))
        .expect_err("piecewise arity should fail");

        assert_eq!(
            err.identifier.as_deref(),
            Some("RunMat:piecewise:InvalidInput")
        );
    }

    #[test]
    fn rejects_nonconvertible_arguments() {
        let err = block_on(piecewise_builtin(vec![
            Value::Symbolic(SymbolicExpr::variable("x")),
            Value::String("unsupported".to_string()),
        ]))
        .expect_err("piecewise unsupported argument should fail");

        assert_eq!(
            err.identifier.as_deref(),
            Some("RunMat:piecewise:InvalidInput")
        );
    }
}