runmat-runtime 0.6.0

Core runtime for RunMat with builtins, BLAS/LAPACK integration, and execution APIs
Documentation
//! MATLAB-compatible `uint32` builtin with GPU-aware semantics for RunMat.

use runmat_accelerate_api::GpuTensorHandle;
use runmat_builtins::{
    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
    CharArray, IntValue, IntegerStorage, SymbolicArray, Tensor, Value,
};
use runmat_macros::runtime_builtin;

use crate::builtins::common::{
    gpu_helpers,
    spec::{
        BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
        ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
    },
    tensor,
};
use crate::builtins::math::type_resolvers::numeric_unary_type;
use crate::{build_runtime_error, BuiltinResult, RuntimeError};

const BUILTIN_NAME: &str = "uint32";
const UINT32_MAX_F64: f64 = u32::MAX as f64;

const OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "Y",
    ty: BuiltinParamType::NumericArray,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "uint32-converted output value.",
}];

const INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "X",
    ty: BuiltinParamType::Any,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Input scalar/array value to convert.",
}];

const SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
    label: "Y = uint32(X)",
    inputs: &INPUTS,
    outputs: &OUTPUT,
}];

const ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.UINT32.INVALID_ARGUMENT",
    identifier: Some("RunMat:uint32:InvalidArgument"),
    when: "Optional arguments are malformed or unsupported.",
    message: "uint32: invalid argument",
};

const ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.UINT32.INVALID_INPUT",
    identifier: Some("RunMat:uint32:InvalidInput"),
    when: "Input value cannot be converted to uint32.",
    message: "uint32: invalid input",
};

const ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.UINT32.INTERNAL",
    identifier: Some("RunMat:uint32:Internal"),
    when: "Internal conversion, gather, or provider upload failed.",
    message: "uint32: internal error",
};

const ERRORS: [BuiltinErrorDescriptor; 3] =
    [ERROR_INVALID_ARGUMENT, ERROR_INVALID_INPUT, ERROR_INTERNAL];

pub const UINT32_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
    signatures: &SIGNATURES,
    output_mode: BuiltinOutputMode::Fixed,
    completion_policy: BuiltinCompletionPolicy::Public,
    errors: &ERRORS,
};

#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::elementwise::uint32")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
    name: "uint32",
    op_kind: GpuOpKind::Elementwise,
    supported_precisions: &[ScalarType::F32, ScalarType::F64],
    broadcast: BroadcastSemantics::Matlab,
    provider_hooks: &[],
    constant_strategy: ConstantStrategy::InlineLiteral,
    residency: ResidencyPolicy::GatherImmediately,
    nan_mode: ReductionNaN::Include,
    two_pass_threshold: None,
    workgroup_size: None,
    accepts_nan_mode: false,
    notes: "No provider-native uint32 storage hook yet; gpuArray inputs gather to host and return a host uint32 result.",
};

#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::elementwise::uint32")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
    name: "uint32",
    shape: ShapeRequirements::BroadcastCompatible,
    constant_strategy: ConstantStrategy::InlineLiteral,
    elementwise: None,
    reduction: None,
    emits_nan: false,
    notes:
        "Runs outside fusion today because integer storage remains host-represented in f64 buffers.",
};

#[runtime_builtin(
    name = "uint32",
    category = "math/elementwise",
    summary = "Convert scalars, arrays, and gpuArray values to uint32 using MATLAB saturating rounding.",
    keywords = "uint32,cast,integer,conversion,gpuArray",
    accel = "unary",
    type_resolver(numeric_unary_type),
    descriptor(crate::builtins::math::elementwise::uint32::UINT32_DESCRIPTOR),
    builtin_path = "crate::builtins::math::elementwise::uint32"
)]
async fn uint32_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
    if !rest.is_empty() {
        return Err(error_with_detail(
            &ERROR_INVALID_ARGUMENT,
            "too many input arguments",
        ));
    }
    match value {
        Value::Num(n) => Ok(Value::Int(IntValue::U32(cast_scalar_to_uint32(n)))),
        Value::Int(i) => Ok(Value::Int(IntValue::U32(cast_scalar_to_uint32(
            int_to_lossless_f64(&i),
        )))),
        Value::Bool(flag) => Ok(Value::Int(IntValue::U32(if flag { 1 } else { 0 }))),
        Value::Tensor(tensor) => Ok(value_from_tensor(
            tensor_to_host(tensor).map_err(|e| error_with_detail(&ERROR_INTERNAL, e))?,
        )),
        Value::SparseTensor(_) => Err(conversion_error("sparse")),
        Value::LogicalArray(array) => {
            let tensor = tensor::logical_to_tensor(&array)
                .map_err(|e| error_with_detail(&ERROR_INTERNAL, e))?;
            Ok(value_from_tensor(
                tensor_to_host(tensor).map_err(|e| error_with_detail(&ERROR_INTERNAL, e))?,
            ))
        }
        Value::CharArray(chars) => from_char_array(chars),
        Value::GpuTensor(handle) => from_gpu(handle).await,
        Value::Complex(_, _) | Value::ComplexTensor(_) => Err(conversion_error("complex")),
        Value::String(_) | Value::StringArray(_) => Err(conversion_error("string")),
        Value::Symbolic(expr) => expr
            .numeric_constant_value()
            .map(|value| Value::Int(IntValue::U32(cast_scalar_to_uint32(value))))
            .ok_or_else(|| conversion_error("sym")),
        Value::SymbolicArray(array) => uint32_from_symbolic_array(array),
        Value::Cell(_) => Err(conversion_error("cell")),
        Value::Struct(_) => Err(conversion_error("struct")),
        Value::Object(obj) => Err(conversion_error(&obj.class_name)),
        Value::HandleObject(handle) => Err(conversion_error(&handle.class_name)),
        Value::Listener(_) => Err(conversion_error("event.listener")),
        Value::FunctionHandle(_)
        | Value::ExternalFunctionHandle(_)
        | Value::MethodFunctionHandle(_)
        | Value::BoundFunctionHandle { .. }
        | Value::Closure(_) => Err(conversion_error("function_handle")),
        Value::ClassRef(_) => Err(conversion_error("meta.class")),
        Value::MException(_) => Err(conversion_error("MException")),
        Value::OutputList(_) => Err(conversion_error("OutputList")),
    }
}

fn uint32_from_symbolic_array(array: SymbolicArray) -> BuiltinResult<Value> {
    let values = array
        .data
        .into_iter()
        .map(|expression| {
            expression
                .numeric_constant_value()
                .map(cast_scalar_to_uint32)
                .ok_or_else(|| conversion_error("sym"))
        })
        .collect::<BuiltinResult<Vec<_>>>()?;
    Tensor::new_integer(IntegerStorage::U32(values), array.shape)
        .map(Value::Tensor)
        .map_err(|error| error_with_detail(&ERROR_INTERNAL, error))
}

fn from_char_array(chars: CharArray) -> BuiltinResult<Value> {
    let data: Vec<u32> = chars
        .data
        .iter()
        .map(|&ch| cast_scalar_to_uint32(ch as u32 as f64))
        .collect();
    let tensor = Tensor::new_integer(IntegerStorage::U32(data), vec![chars.rows, chars.cols])
        .map_err(|e| error_with_detail(&ERROR_INTERNAL, e))?;
    Ok(value_from_tensor(tensor))
}

async fn from_gpu(handle: GpuTensorHandle) -> BuiltinResult<Value> {
    let converted = tensor_to_host(
        gpu_helpers::gather_tensor_async(&handle)
            .await
            .map_err(|e| error_with_detail(&ERROR_INTERNAL, e))?,
    )
    .map_err(|e| error_with_detail(&ERROR_INTERNAL, e))?;
    Ok(value_from_tensor(converted))
}

fn tensor_to_host(tensor: Tensor) -> Result<Tensor, String> {
    let data = tensor.data.into_iter().map(cast_scalar_to_uint32).collect();
    Tensor::new_integer(IntegerStorage::U32(data), tensor.shape)
}

fn value_from_tensor(tensor: Tensor) -> Value {
    if tensor.data.len() == 1 {
        Value::Int(IntValue::U32(cast_scalar_to_uint32(tensor.data[0])))
    } else {
        Value::Tensor(tensor)
    }
}

fn int_to_lossless_f64(value: &IntValue) -> f64 {
    match value {
        IntValue::U64(value) if *value > UINT32_MAX_F64 as u64 => UINT32_MAX_F64,
        IntValue::U64(value) => *value as f64,
        _ => value.to_f64(),
    }
}

fn cast_scalar_to_uint32(value: f64) -> u32 {
    if value.is_nan() {
        return 0;
    }
    if value.is_infinite() {
        return if value.is_sign_negative() {
            0
        } else {
            u32::MAX
        };
    }
    value.round().clamp(0.0, UINT32_MAX_F64) as u32
}

fn conversion_error(type_name: &str) -> RuntimeError {
    error_with_detail(
        &ERROR_INVALID_INPUT,
        format!("conversion to uint32 from {type_name} is not possible"),
    )
}

fn error_with_detail(
    error: &'static BuiltinErrorDescriptor,
    detail: impl std::fmt::Display,
) -> RuntimeError {
    let message = format!("{}: {}", error.message, detail);
    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
    if let Some(identifier) = error.identifier {
        builder = builder.with_identifier(identifier);
    }
    builder.build()
}

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

    fn call(value: Value) -> BuiltinResult<Value> {
        block_on(uint32_builtin(value, Vec::new()))
    }

    #[test]
    fn uint32_scalar_saturates_and_rounds() {
        assert_eq!(
            call(Value::Num(3.5)).expect("uint32"),
            Value::Int(IntValue::U32(4))
        );
        assert_eq!(
            call(Value::Num(-1.0)).expect("uint32"),
            Value::Int(IntValue::U32(0))
        );
        assert_eq!(
            call(Value::Num(f64::INFINITY)).expect("uint32"),
            Value::Int(IntValue::U32(u32::MAX))
        );
        assert_eq!(
            call(Value::Num(f64::NAN)).expect("uint32"),
            Value::Int(IntValue::U32(0))
        );
    }

    #[test]
    fn uint32_tensor_preserves_shape_and_class() {
        let tensor =
            Tensor::new(vec![-2.0, 2.49, 2.5, (u32::MAX as f64) + 99.0], vec![2, 2]).unwrap();
        let result = call(Value::Tensor(tensor)).expect("uint32");
        match result {
            Value::Tensor(out) => {
                assert_eq!(out.shape, vec![2, 2]);
                assert_eq!(out.data, vec![0.0, 2.0, 3.0, u32::MAX as f64]);
                assert_eq!(
                    out.integer_storage(),
                    Some(&IntegerStorage::U32(vec![0, 2, 3, u32::MAX]))
                );
            }
            other => panic!("expected tensor, got {other:?}"),
        }
    }

    #[test]
    fn uint32_rejects_complex_input() {
        let err = call(Value::Complex(1.0, 0.0)).expect_err("complex should fail");
        assert_eq!(err.identifier(), ERROR_INVALID_INPUT.identifier);
    }
}