runmat-runtime 0.6.0

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

use runmat_builtins::{
    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
    CellArray, CharArray, StringArray, StructValue, Value,
};
use runmat_macros::runtime_builtin;

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

const BUILTIN_NAME: &str = "cell2struct";

const OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "S",
    ty: BuiltinParamType::Any,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Struct or struct array result.",
}];

const INPUTS: [BuiltinParamDescriptor; 3] = [
    BuiltinParamDescriptor {
        name: "C",
        ty: BuiltinParamType::Any,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Cell array containing field values.",
    },
    BuiltinParamDescriptor {
        name: "fields",
        ty: BuiltinParamType::Any,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Field names as char, string, or cellstr.",
    },
    BuiltinParamDescriptor {
        name: "dim",
        ty: BuiltinParamType::IntegerScalar,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Dimension whose entries correspond to field names.",
    },
];

const SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
    label: "S = cell2struct(C, fields, dim)",
    inputs: &INPUTS,
    outputs: &OUTPUT,
}];

const ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.CELL2STRUCT.INVALID_INPUT",
    identifier: Some("RunMat:cell2struct:InvalidInput"),
    when: "Arguments are not a cell array, field-name list, and valid dimension.",
    message: "cell2struct: invalid input",
};

const ERROR_SHAPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.CELL2STRUCT.SHAPE",
    identifier: Some("RunMat:cell2struct:ShapeMismatch"),
    when: "The number of field names does not match the selected cell dimension.",
    message: "cell2struct: field count does not match selected dimension",
};

const ERRORS: [BuiltinErrorDescriptor; 2] = [ERROR_INVALID_INPUT, ERROR_SHAPE];

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

#[runtime_builtin(
    name = "cell2struct",
    category = "cells/core",
    summary = "Convert a cell array into a scalar struct or struct array.",
    keywords = "cell2struct,cell,struct,conversion",
    accel = "gather",
    descriptor(crate::builtins::cells::core::cell2struct::CELL2STRUCT_DESCRIPTOR),
    builtin_path = "crate::builtins::cells::core::cell2struct"
)]
fn cell2struct_builtin(cells: Value, fields: Value, dim: Value) -> BuiltinResult<Value> {
    let Value::Cell(cells) = cells else {
        return Err(error(
            &ERROR_INVALID_INPUT,
            "cell2struct: C must be a cell array",
        ));
    };
    let fields = field_names(&fields)?;
    let dim = parse_dim(&dim)?;
    build_structs(cells, fields, dim)
}

fn build_structs(cells: CellArray, fields: Vec<String>, dim: usize) -> BuiltinResult<Value> {
    if fields.is_empty() {
        return Err(error(
            &ERROR_INVALID_INPUT,
            "cell2struct: fields must not be empty",
        ));
    }
    let rank = cells.shape.len().max(dim);
    let mut shape = cells.shape.clone();
    shape.resize(rank, 1);
    let field_dim = dim - 1;
    if shape[field_dim] != fields.len() {
        return Err(error(
            &ERROR_SHAPE,
            format!(
                "cell2struct: selected dimension has extent {}, but {} field names were supplied",
                shape[field_dim],
                fields.len()
            ),
        ));
    }

    let mut out_shape = shape.clone();
    out_shape[field_dim] = 1;
    if out_shape.iter().product::<usize>() == 1 {
        let mut st = StructValue::new();
        for (field_idx, field) in fields.iter().enumerate() {
            let mut coords = vec![0usize; rank];
            coords[field_dim] = field_idx;
            st.insert(
                field.clone(),
                cells.data[linear_col_major(&coords, &shape)].clone(),
            );
        }
        return Ok(Value::Struct(st));
    }

    let count = out_shape.iter().product::<usize>();
    let mut structs = Vec::with_capacity(count);
    for out_linear in 0..count {
        let out_coords = coords_col_major(out_linear, &out_shape);
        let mut st = StructValue::new();
        for (field_idx, field) in fields.iter().enumerate() {
            let mut coords = out_coords.clone();
            coords[field_dim] = field_idx;
            st.insert(
                field.clone(),
                cells.data[linear_col_major(&coords, &shape)].clone(),
            );
        }
        structs.push(Value::Struct(st));
    }
    if structs.len() == 1 {
        return Ok(structs.pop().expect("one struct"));
    }
    CellArray::new_with_shape(structs, out_shape)
        .map(Value::Cell)
        .map_err(|err| error(&ERROR_INVALID_INPUT, format!("cell2struct: {err}")))
}

fn field_names(value: &Value) -> BuiltinResult<Vec<String>> {
    match value {
        Value::String(text) => Ok(vec![text.clone()]),
        Value::StringArray(array) => Ok(array.data.clone()),
        Value::CharArray(chars) if chars.rows == 1 => Ok(vec![chars.data.iter().collect()]),
        Value::CharArray(chars) => {
            let mut names = Vec::with_capacity(chars.rows);
            for row in 0..chars.rows {
                let start = row * chars.cols;
                let text = chars.data[start..start + chars.cols]
                    .iter()
                    .collect::<String>()
                    .trim_end()
                    .to_string();
                names.push(text);
            }
            Ok(names)
        }
        Value::Cell(cell) => cell.data.iter().map(field_name_scalar).collect(),
        _ => Err(error(
            &ERROR_INVALID_INPUT,
            "cell2struct: fields must be text or cellstr",
        )),
    }
    .and_then(|names| {
        if names.iter().any(|name| name.is_empty()) {
            Err(error(
                &ERROR_INVALID_INPUT,
                "cell2struct: field names must not be empty",
            ))
        } else {
            Ok(names)
        }
    })
}

fn field_name_scalar(value: &Value) -> BuiltinResult<String> {
    match value {
        Value::String(text) => Ok(text.clone()),
        Value::StringArray(StringArray { data, .. }) if data.len() == 1 => Ok(data[0].clone()),
        Value::CharArray(CharArray { rows: 1, data, .. }) => Ok(data.iter().collect()),
        _ => Err(error(
            &ERROR_INVALID_INPUT,
            "cell2struct: cell field names must be text scalars",
        )),
    }
}

fn parse_dim(value: &Value) -> BuiltinResult<usize> {
    let raw = match value {
        Value::Num(value) if value.is_finite() => *value,
        Value::Int(value) => value.to_f64(),
        _ => {
            return Err(error(
                &ERROR_INVALID_INPUT,
                "cell2struct: dim must be a positive integer",
            ))
        }
    };
    if raw < 1.0 || raw.fract() != 0.0 {
        return Err(error(
            &ERROR_INVALID_INPUT,
            "cell2struct: dim must be a positive integer",
        ));
    }
    Ok(raw as usize)
}

fn coords_col_major(mut linear: usize, shape: &[usize]) -> Vec<usize> {
    let mut coords = Vec::with_capacity(shape.len());
    for &extent in shape {
        coords.push(if extent == 0 { 0 } else { linear % extent });
        if extent != 0 {
            linear /= extent;
        }
    }
    coords
}

fn linear_col_major(coords: &[usize], shape: &[usize]) -> usize {
    let mut linear = 0usize;
    let mut stride = 1usize;
    for (&coord, &extent) in coords.iter().zip(shape.iter()) {
        linear += coord * stride;
        stride *= extent;
    }
    linear
}

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

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

    #[test]
    fn scalar_struct_from_row_cell() {
        let cells = CellArray::new(vec![Value::Num(1.0), Value::from("Ada")], 1, 2).unwrap();
        let fields = CellArray::new(vec![Value::from("id"), Value::from("name")], 1, 2).unwrap();
        let out =
            cell2struct_builtin(Value::Cell(cells), Value::Cell(fields), Value::Num(2.0)).unwrap();
        let Value::Struct(st) = out else {
            panic!("expected scalar struct");
        };
        assert_eq!(st.fields.get("id"), Some(&Value::Num(1.0)));
        assert_eq!(st.fields.get("name"), Some(&Value::from("Ada")));
    }

    #[test]
    fn struct_array_from_field_dimension() {
        let cells = CellArray::new(
            vec![
                Value::Num(1.0),
                Value::from("Ada"),
                Value::Num(2.0),
                Value::from("Grace"),
            ],
            2,
            2,
        )
        .unwrap();
        let fields = CellArray::new(vec![Value::from("id"), Value::from("name")], 2, 1).unwrap();
        let out =
            cell2struct_builtin(Value::Cell(cells), Value::Cell(fields), Value::Num(1.0)).unwrap();
        let Value::Cell(out) = out else {
            panic!("expected struct array cell");
        };
        assert_eq!(out.shape, vec![1, 2]);
        assert!(
            matches!(&out.data[0], Value::Struct(st) if st.fields.get("id") == Some(&Value::Num(1.0)))
        );
        assert!(
            matches!(&out.data[1], Value::Struct(st) if st.fields.get("name") == Some(&Value::from("Grace")))
        );
    }
}