1use 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}