runmat-runtime 0.5.3

Core runtime for RunMat with builtins, BLAS/LAPACK integration, and execution APIs
Documentation
//! MATLAB-compatible scalar `sym` builtin for symbolic variables and constants.

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::{is_valid_identifier, symbolic_expr_to_value, text_scalar, value_to_symbolic_scalar};

const BUILTIN_NAME: &str = "sym";

const SYM_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "S",
    ty: BuiltinParamType::Any,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Scalar symbolic variable or constant.",
}];

const SYM_INPUTS: [BuiltinParamDescriptor; 2] = [
    BuiltinParamDescriptor {
        name: "value",
        ty: BuiltinParamType::Any,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Variable name, numeric scalar, or existing symbolic scalar.",
    },
    BuiltinParamDescriptor {
        name: "options",
        ty: BuiltinParamType::Any,
        arity: BuiltinParamArity::Variadic,
        default: None,
        description: "Reserved symbolic options.",
    },
];

const SYM_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
    label: "S = sym(value)",
    inputs: &SYM_INPUTS,
    outputs: &SYM_OUTPUT,
}];

const SYM_ERRORS: [BuiltinErrorDescriptor; 3] = [
    BuiltinErrorDescriptor {
        code: "RM.SYM.ARG_COUNT",
        identifier: Some("RunMat:sym:ArgCount"),
        when: "Options beyond the scalar input are supplied.",
        message: "sym: unsupported options",
    },
    BuiltinErrorDescriptor {
        code: "RM.SYM.INVALID_TEXT",
        identifier: Some("RunMat:sym:InvalidText"),
        when: "Text input is neither a valid identifier nor a numeric scalar literal.",
        message: "sym: invalid symbolic text",
    },
    BuiltinErrorDescriptor {
        code: "RM.SYM.INVALID_INPUT",
        identifier: Some("RunMat:sym:InvalidInput"),
        when: "Input cannot be represented as a scalar symbolic value.",
        message: "sym: expected a scalar text, numeric, logical, or symbolic input",
    },
];

pub const SYM_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
    signatures: &SYM_SIGNATURES,
    output_mode: BuiltinOutputMode::Fixed,
    completion_policy: BuiltinCompletionPolicy::Public,
    errors: &SYM_ERRORS,
};

#[runtime_builtin(
    name = "sym",
    category = "math/symbolic",
    summary = "Create a scalar symbolic variable or constant.",
    keywords = "sym,symbolic,variable,algebra",
    descriptor(crate::builtins::math::symbolic::sym::SYM_DESCRIPTOR),
    builtin_path = "crate::builtins::math::symbolic::sym"
)]
async fn sym_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
    if !rest.is_empty() {
        return Err(sym_error(&SYM_ERRORS[0]));
    }
    if let Some(expr) = value_to_symbolic_scalar(&value) {
        return Ok(symbolic_expr_to_value(expr));
    }
    if let Some(text) = text_scalar(&value) {
        let trimmed = text.trim();
        if is_valid_identifier(trimmed) {
            return Ok(Value::Symbolic(SymbolicExpr::variable(trimmed)));
        }
        if let Ok(value) = trimmed.parse::<f64>() {
            if value.is_finite() {
                return Ok(symbolic_expr_to_value(SymbolicExpr::constant(value)));
            }
        }
        return Err(sym_error(&SYM_ERRORS[1]));
    }
    Err(sym_error(&SYM_ERRORS[2]))
}

fn sym_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()
}

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

    #[test]
    fn creates_symbolic_variable_from_text() {
        let value =
            futures::executor::block_on(sym_builtin(Value::from("x"), Vec::new())).expect("sym");
        assert_eq!(value.to_string(), "x");
    }

    #[test]
    fn creates_symbolic_constant_from_numeric_text() {
        let value =
            futures::executor::block_on(sym_builtin(Value::from("3.5"), Vec::new())).expect("sym");
        match value {
            Value::Symbolic(expr) => assert_eq!(expr.constant_value(), Some(3.5)),
            other => panic!("expected symbolic constant, got {other:?}"),
        }
    }

    #[test]
    fn rejects_identifier_with_leading_underscore() {
        let err = futures::executor::block_on(sym_builtin(Value::from("_x"), Vec::new()))
            .expect_err("invalid identifier should error");

        assert_eq!(err.identifier.as_deref(), Some("RunMat:sym:InvalidText"));
    }
}