Skip to main content

runmat_runtime/builtins/strings/transform/
upper.rs

1//! MATLAB-compatible `upper` builtin with GPU-aware semantics for RunMat.
2use runmat_builtins::{
3    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
4    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
5};
6use runmat_macros::runtime_builtin;
7use runmat_value::{CellArray, CharArray, StringArray, Value};
8
9use crate::builtins::common::map_control_flow_with_builtin;
10use crate::builtins::common::spec::{
11    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
12    ReductionNaN, ResidencyPolicy, ShapeRequirements,
13};
14use crate::builtins::strings::common::{char_row_to_string_slice, uppercase_preserving_missing};
15use crate::builtins::strings::type_resolvers::text_preserve_type;
16use crate::{build_runtime_error, gather_if_needed_async, make_cell, BuiltinResult, RuntimeError};
17use runmat_builtins::{BuiltinIntegerAuditDescriptor, BuiltinIntegerAuditKind};
18
19#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::transform::upper")]
20pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
21    name: "upper",
22    op_kind: GpuOpKind::Custom("string-transform"),
23    supported_precisions: &[],
24    broadcast: BroadcastSemantics::None,
25    provider_hooks: &[],
26    constant_strategy: ConstantStrategy::InlineLiteral,
27    residency: ResidencyPolicy::GatherImmediately,
28    nan_mode: ReductionNaN::Include,
29    two_pass_threshold: None,
30    workgroup_size: None,
31    accepts_nan_mode: false,
32    notes:
33        "Executes on the CPU; GPU-resident inputs are gathered to host memory before conversion.",
34};
35
36#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::transform::upper")]
37pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
38    name: "upper",
39    shape: ShapeRequirements::Any,
40    constant_strategy: ConstantStrategy::InlineLiteral,
41    elementwise: None,
42    reduction: None,
43    emits_nan: false,
44    notes: "String transformation builtin; not eligible for fusion and always gathers GPU inputs.",
45};
46
47const BUILTIN_NAME: &str = "upper";
48
49const UPPER_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
50    name: "out",
51    ty: BuiltinParamType::Any,
52    arity: BuiltinParamArity::Required,
53    default: None,
54    description: "Uppercased text preserving input container kind and shape.",
55}];
56
57const UPPER_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
58    name: "str",
59    ty: BuiltinParamType::Any,
60    arity: BuiltinParamArity::Required,
61    default: None,
62    description: "String/char/cell text input to transform.",
63}];
64
65const UPPER_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
66    label: "out = upper(str)",
67    inputs: &UPPER_INPUTS,
68    outputs: &UPPER_OUTPUT,
69}];
70
71const UPPER_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
72    code: "RM.UPPER.INVALID_INPUT",
73    identifier: Some("RunMat:upper:InvalidInput"),
74    when: "Input is not a string array, character array, or cell array of text scalars.",
75    message:
76        "upper: first argument must be a string array, character array, or cell array of character vectors",
77};
78
79const UPPER_ERROR_CELL_ELEMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
80    code: "RM.UPPER.CELL_ELEMENT",
81    identifier: Some("RunMat:upper:CellElement"),
82    when: "Cell array contains a non-text element or non-row char array element.",
83    message: "upper: cell array elements must be string scalars or character vectors",
84};
85
86const UPPER_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
87    code: "RM.UPPER.INTERNAL",
88    identifier: Some("RunMat:upper:InternalError"),
89    when: "Internal output container construction failed.",
90    message: "upper: internal error",
91};
92
93const UPPER_ERRORS: [BuiltinErrorDescriptor; 3] = [
94    UPPER_ERROR_INVALID_INPUT,
95    UPPER_ERROR_CELL_ELEMENT,
96    UPPER_ERROR_INTERNAL,
97];
98
99pub const UPPER_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
100    signatures: &UPPER_SIGNATURES,
101    output_mode: BuiltinOutputMode::Fixed,
102    completion_policy: BuiltinCompletionPolicy::Public,
103    errors: &UPPER_ERRORS,
104};
105
106pub const UPPER_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor = BuiltinIntegerAuditDescriptor {
107    kind: BuiltinIntegerAuditKind::NotApplicable,
108    canonical_builtin: None,
109    notes: "upper accepts string arrays, character arrays, or cell arrays of character vectors. Numeric and integer inputs reject without implicit text conversion or provider access.",
110};
111
112fn map_flow(err: RuntimeError) -> RuntimeError {
113    map_control_flow_with_builtin(err, BUILTIN_NAME)
114}
115
116fn upper_error_with_message(
117    message: impl Into<String>,
118    error: &'static BuiltinErrorDescriptor,
119) -> RuntimeError {
120    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
121    if let Some(identifier) = error.identifier {
122        builder = builder.with_identifier(identifier);
123    }
124    builder.build()
125}
126
127fn upper_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
128    upper_error_with_message(error.message, error)
129}
130
131#[runtime_builtin(
132    name = "upper",
133    category = "strings/transform",
134    summary = "Convert text inputs to uppercase character forms.",
135    keywords = "upper,uppercase,strings,character array,text",
136    accel = "sink",
137    type_resolver(text_preserve_type),
138    descriptor(crate::builtins::strings::transform::upper::UPPER_DESCRIPTOR),
139    integer_audit(crate::builtins::strings::transform::upper::UPPER_INTEGER_AUDIT),
140    builtin_path = "crate::builtins::strings::transform::upper"
141)]
142async fn upper_builtin(value: Value) -> BuiltinResult<Value> {
143    if crate::dispatcher::value_contains_gpu(&value) {
144        return Err(upper_error(&UPPER_ERROR_INVALID_INPUT));
145    }
146    let gathered = gather_if_needed_async(&value).await.map_err(map_flow)?;
147    match gathered {
148        Value::String(text) => Ok(Value::String(uppercase_preserving_missing(text))),
149        Value::StringArray(array) => upper_string_array(array),
150        Value::CharArray(array) => upper_char_array(array),
151        Value::Cell(cell) => upper_cell_array(cell),
152        _ => Err(upper_error(&UPPER_ERROR_INVALID_INPUT)),
153    }
154}
155
156fn upper_string_array(array: StringArray) -> BuiltinResult<Value> {
157    let StringArray { data, shape, .. } = array;
158    let uppered = data
159        .into_iter()
160        .map(uppercase_preserving_missing)
161        .collect::<Vec<_>>();
162    let upper_array = StringArray::new(uppered, shape).map_err(|e| {
163        upper_error_with_message(format!("{BUILTIN_NAME}: {e}"), &UPPER_ERROR_INTERNAL)
164    })?;
165    Ok(Value::StringArray(upper_array))
166}
167
168fn upper_char_array(array: CharArray) -> BuiltinResult<Value> {
169    let CharArray {
170        data,
171        shape,
172        rows,
173        cols,
174    } = array;
175    if rows == 0 || cols == 0 {
176        return Ok(Value::CharArray(CharArray {
177            data,
178            shape,
179            rows,
180            cols,
181        }));
182    }
183
184    let mut upper_rows = Vec::with_capacity(rows);
185    let mut target_cols = cols;
186    for row in 0..rows {
187        let text = char_row_to_string_slice(&data, cols, row).to_uppercase();
188        let len = text.chars().count();
189        target_cols = target_cols.max(len);
190        upper_rows.push(text);
191    }
192
193    let mut upper_data = Vec::with_capacity(rows * target_cols);
194    for row_text in upper_rows {
195        let mut chars: Vec<char> = row_text.chars().collect();
196        if chars.len() < target_cols {
197            chars.resize(target_cols, ' ');
198        }
199        upper_data.extend(chars.into_iter());
200    }
201
202    CharArray::new(upper_data, rows, target_cols)
203        .map(Value::CharArray)
204        .map_err(|e| {
205            upper_error_with_message(format!("{BUILTIN_NAME}: {e}"), &UPPER_ERROR_INTERNAL)
206        })
207}
208
209fn upper_cell_array(cell: CellArray) -> BuiltinResult<Value> {
210    let CellArray {
211        data, rows, cols, ..
212    } = cell;
213    let mut upper_values = Vec::with_capacity(rows * cols);
214    for row in 0..rows {
215        for col in 0..cols {
216            let idx = row * cols + col;
217            let upper = upper_cell_element(&data[idx])?;
218            upper_values.push(upper);
219        }
220    }
221    make_cell(upper_values, rows, cols).map_err(|e| {
222        upper_error_with_message(format!("{BUILTIN_NAME}: {e}"), &UPPER_ERROR_INTERNAL)
223    })
224}
225
226fn upper_cell_element(value: &Value) -> BuiltinResult<Value> {
227    match value {
228        Value::String(text) => Ok(Value::String(uppercase_preserving_missing(text.clone()))),
229        Value::StringArray(sa) if sa.data.len() == 1 => Ok(Value::String(
230            uppercase_preserving_missing(sa.data[0].clone()),
231        )),
232        Value::CharArray(ca) if ca.rows <= 1 => upper_char_array(ca.clone()),
233        Value::CharArray(_) => Err(upper_error(&UPPER_ERROR_CELL_ELEMENT)),
234        _ => Err(upper_error(&UPPER_ERROR_CELL_ELEMENT)),
235    }
236}
237
238#[cfg(test)]
239pub(crate) mod tests {
240    use super::*;
241    use runmat_builtins::{ResolveContext, Type};
242
243    fn run_upper(value: Value) -> BuiltinResult<Value> {
244        futures::executor::block_on(upper_builtin(value))
245    }
246
247    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
248    #[test]
249    fn upper_string_scalar_value() {
250        let result = run_upper(Value::String("RunMat".into())).expect("upper");
251        assert_eq!(result, Value::String("RUNMAT".into()));
252    }
253
254    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
255    #[test]
256    fn upper_string_array_preserves_shape() {
257        let array = StringArray::new(
258            vec![
259                "gpu".into(),
260                "accel".into(),
261                "<missing>".into(),
262                "MiXeD".into(),
263            ],
264            vec![2, 2],
265        )
266        .unwrap();
267        let result = run_upper(Value::StringArray(array)).expect("upper");
268        match result {
269            Value::StringArray(sa) => {
270                assert_eq!(sa.shape, vec![2, 2]);
271                assert_eq!(
272                    sa.data,
273                    vec![
274                        String::from("GPU"),
275                        String::from("ACCEL"),
276                        String::from("<missing>"),
277                        String::from("MIXED")
278                    ]
279                );
280            }
281            other => panic!("expected string array, got {other:?}"),
282        }
283    }
284
285    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
286    #[test]
287    fn upper_char_array_multiple_rows() {
288        let data: Vec<char> = vec!['c', 'a', 't', 'd', 'o', 'g'];
289        let array = CharArray::new(data, 2, 3).unwrap();
290        let result = run_upper(Value::CharArray(array)).expect("upper");
291        match result {
292            Value::CharArray(ca) => {
293                assert_eq!(ca.rows, 2);
294                assert_eq!(ca.cols, 3);
295                assert_eq!(ca.data, vec!['C', 'A', 'T', 'D', 'O', 'G']);
296            }
297            other => panic!("expected char array, got {other:?}"),
298        }
299    }
300
301    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
302    #[test]
303    fn upper_char_vector_handles_padding() {
304        let array = CharArray::new_row("hello ");
305        let result = run_upper(Value::CharArray(array)).expect("upper");
306        match result {
307            Value::CharArray(ca) => {
308                assert_eq!(ca.rows, 1);
309                assert_eq!(ca.cols, 6);
310                let expected: Vec<char> = "HELLO ".chars().collect();
311                assert_eq!(ca.data, expected);
312            }
313            other => panic!("expected char array, got {other:?}"),
314        }
315    }
316
317    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
318    #[test]
319    fn upper_char_array_unicode_expansion_extends_width() {
320        let data: Vec<char> = vec!['ß', 'a'];
321        let array = CharArray::new(data, 1, 2).unwrap();
322        let result = run_upper(Value::CharArray(array)).expect("upper");
323        match result {
324            Value::CharArray(ca) => {
325                assert_eq!(ca.rows, 1);
326                assert_eq!(ca.cols, 3);
327                let expected: Vec<char> = vec!['S', 'S', 'A'];
328                assert_eq!(ca.data, expected);
329            }
330            other => panic!("expected char array, got {other:?}"),
331        }
332    }
333
334    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
335    #[test]
336    fn upper_cell_array_mixed_content() {
337        let cell = CellArray::new(
338            vec![
339                Value::CharArray(CharArray::new_row("run")),
340                Value::String("Mat".into()),
341            ],
342            1,
343            2,
344        )
345        .unwrap();
346        let result = run_upper(Value::Cell(cell)).expect("upper");
347        match result {
348            Value::Cell(out) => {
349                let first = out.get(0, 0).unwrap();
350                let second = out.get(0, 1).unwrap();
351                assert_eq!(first, Value::CharArray(CharArray::new_row("RUN")));
352                assert_eq!(second, Value::String("MAT".into()));
353            }
354            other => panic!("expected cell array, got {other:?}"),
355        }
356    }
357
358    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
359    #[test]
360    fn upper_errors_on_invalid_input() {
361        let err = run_upper(Value::Num(1.0)).unwrap_err();
362        assert_eq!(err.to_string(), UPPER_ERROR_INVALID_INPUT.message);
363    }
364
365    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
366    #[test]
367    fn upper_cell_errors_on_invalid_element() {
368        let cell = CellArray::new(vec![Value::Num(1.0)], 1, 1).unwrap();
369        let err = run_upper(Value::Cell(cell)).unwrap_err();
370        assert_eq!(err.to_string(), UPPER_ERROR_CELL_ELEMENT.message);
371    }
372
373    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
374    #[test]
375    fn upper_preserves_missing_string() {
376        let result = run_upper(Value::String("<missing>".into())).expect("upper");
377        assert_eq!(result, Value::String("<missing>".into()));
378    }
379
380    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
381    #[test]
382    fn upper_cell_allows_empty_char_vector() {
383        let empty_char = CharArray::new(Vec::new(), 1, 0).unwrap();
384        let cell = CellArray::new(vec![Value::CharArray(empty_char.clone())], 1, 1).unwrap();
385        let result = run_upper(Value::Cell(cell)).expect("upper");
386        match result {
387            Value::Cell(out) => {
388                let element = out.get(0, 0).unwrap();
389                assert_eq!(element, Value::CharArray(empty_char));
390            }
391            other => panic!("expected cell array, got {other:?}"),
392        }
393    }
394
395    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
396    #[test]
397    #[cfg(feature = "wgpu")]
398    fn upper_gpu_tensor_input_gathers_then_errors() {
399        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
400            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
401        );
402        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
403        let data = [1.0f64, 2.0];
404        let shape = [2usize, 1usize];
405        let handle = provider
406            .upload(&runmat_accelerate_api::HostTensorView {
407                data: &data,
408                shape: &shape,
409            })
410            .expect("upload");
411        let err = run_upper(Value::GpuTensor(handle.clone())).unwrap_err();
412        assert_eq!(err.to_string(), UPPER_ERROR_INVALID_INPUT.message);
413        provider.free(&handle).ok();
414    }
415
416    #[test]
417    fn upper_type_preserves_text() {
418        assert_eq!(
419            text_preserve_type(&[Type::String], &ResolveContext::new(Vec::new())),
420            Type::String
421        );
422    }
423}