1#[cfg(test)]
4use runmat_accelerate_api::{HostIntegerDataView, HostIntegerTensorView};
5use runmat_builtins::{
6 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
7 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
8};
9use runmat_builtins::{
10 BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor, BuiltinIntegerComputationDomain,
11 BuiltinIntegerInputAvailability, BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule,
12 BuiltinIntegerOverflowRule, BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule,
13};
14use runmat_macros::runtime_builtin;
15use runmat_value::{CellArray, CharArray, IntValue, StringArray, Value};
16
17use crate::builtins::common::map_control_flow_with_builtin;
18use crate::builtins::common::spec::{
19 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
20 ReductionNaN, ResidencyPolicy, ShapeRequirements,
21};
22use crate::builtins::common::tensor;
23use crate::builtins::strings::common::{char_row_to_string_slice, is_missing_string};
24use crate::builtins::strings::type_resolvers::text_preserve_type;
25use crate::{build_runtime_error, gather_if_needed_async, make_cell, BuiltinResult, RuntimeError};
26
27#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::transform::pad")]
28pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
29 name: "pad",
30 op_kind: GpuOpKind::Custom("string-transform"),
31 supported_precisions: &[],
32 broadcast: BroadcastSemantics::None,
33 provider_hooks: &[],
34 constant_strategy: ConstantStrategy::InlineLiteral,
35 residency: ResidencyPolicy::GatherImmediately,
36 nan_mode: ReductionNaN::Include,
37 two_pass_threshold: None,
38 workgroup_size: None,
39 accepts_nan_mode: false,
40 notes: "Executes on the CPU; GPU-resident inputs are gathered before padding to preserve MATLAB semantics.",
41};
42
43#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::transform::pad")]
44pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
45 name: "pad",
46 shape: ShapeRequirements::Any,
47 constant_strategy: ConstantStrategy::InlineLiteral,
48 elementwise: None,
49 reduction: None,
50 emits_nan: false,
51 notes: "String transformation builtin; always gathers inputs and is not eligible for fusion.",
52};
53
54const BUILTIN_NAME: &str = "pad";
55const PAD_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
56 name: "out",
57 ty: BuiltinParamType::Any,
58 arity: BuiltinParamArity::Required,
59 default: None,
60 description: "Padded text preserving input container kind and shape.",
61}];
62
63const PAD_INPUTS_BASE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
64 name: "str",
65 ty: BuiltinParamType::Any,
66 arity: BuiltinParamArity::Required,
67 default: None,
68 description: "Input text (string/char/cell).",
69}];
70
71const PAD_INPUTS_LENGTH: [BuiltinParamDescriptor; 2] = [
72 BuiltinParamDescriptor {
73 name: "str",
74 ty: BuiltinParamType::Any,
75 arity: BuiltinParamArity::Required,
76 default: None,
77 description: "Input text (string/char/cell).",
78 },
79 BuiltinParamDescriptor {
80 name: "len",
81 ty: BuiltinParamType::IntegerScalar,
82 arity: BuiltinParamArity::Required,
83 default: None,
84 description: "Target length (non-negative integer).",
85 },
86];
87
88const PAD_INPUTS_DIRECTION: [BuiltinParamDescriptor; 2] = [
89 BuiltinParamDescriptor {
90 name: "str",
91 ty: BuiltinParamType::Any,
92 arity: BuiltinParamArity::Required,
93 default: None,
94 description: "Input text (string/char/cell).",
95 },
96 BuiltinParamDescriptor {
97 name: "direction",
98 ty: BuiltinParamType::StringScalar,
99 arity: BuiltinParamArity::Required,
100 default: Some("\"right\""),
101 description: "Padding direction (`\"left\"|\"right\"|\"both\"`).",
102 },
103];
104
105const PAD_INPUTS_PADCHAR: [BuiltinParamDescriptor; 2] = [
106 BuiltinParamDescriptor {
107 name: "str",
108 ty: BuiltinParamType::Any,
109 arity: BuiltinParamArity::Required,
110 default: None,
111 description: "Input text (string/char/cell).",
112 },
113 BuiltinParamDescriptor {
114 name: "padCharacter",
115 ty: BuiltinParamType::StringScalar,
116 arity: BuiltinParamArity::Required,
117 default: Some("\" \""),
118 description: "Single-character padding value.",
119 },
120];
121
122const PAD_INPUTS_LENGTH_DIRECTION: [BuiltinParamDescriptor; 3] = [
123 BuiltinParamDescriptor {
124 name: "str",
125 ty: BuiltinParamType::Any,
126 arity: BuiltinParamArity::Required,
127 default: None,
128 description: "Input text (string/char/cell).",
129 },
130 BuiltinParamDescriptor {
131 name: "len",
132 ty: BuiltinParamType::IntegerScalar,
133 arity: BuiltinParamArity::Required,
134 default: None,
135 description: "Target length (non-negative integer).",
136 },
137 BuiltinParamDescriptor {
138 name: "direction",
139 ty: BuiltinParamType::StringScalar,
140 arity: BuiltinParamArity::Required,
141 default: Some("\"right\""),
142 description: "Padding direction (`\"left\"|\"right\"|\"both\"`).",
143 },
144];
145
146const PAD_INPUTS_LENGTH_PADCHAR: [BuiltinParamDescriptor; 3] = [
147 BuiltinParamDescriptor {
148 name: "str",
149 ty: BuiltinParamType::Any,
150 arity: BuiltinParamArity::Required,
151 default: None,
152 description: "Input text (string/char/cell).",
153 },
154 BuiltinParamDescriptor {
155 name: "len",
156 ty: BuiltinParamType::IntegerScalar,
157 arity: BuiltinParamArity::Required,
158 default: None,
159 description: "Target length (non-negative integer).",
160 },
161 BuiltinParamDescriptor {
162 name: "padCharacter",
163 ty: BuiltinParamType::StringScalar,
164 arity: BuiltinParamArity::Required,
165 default: Some("\" \""),
166 description: "Single-character padding value.",
167 },
168];
169
170const PAD_INPUTS_DIRECTION_PADCHAR: [BuiltinParamDescriptor; 3] = [
171 BuiltinParamDescriptor {
172 name: "str",
173 ty: BuiltinParamType::Any,
174 arity: BuiltinParamArity::Required,
175 default: None,
176 description: "Input text (string/char/cell).",
177 },
178 BuiltinParamDescriptor {
179 name: "direction",
180 ty: BuiltinParamType::StringScalar,
181 arity: BuiltinParamArity::Required,
182 default: Some("\"right\""),
183 description: "Padding direction (`\"left\"|\"right\"|\"both\"`).",
184 },
185 BuiltinParamDescriptor {
186 name: "padCharacter",
187 ty: BuiltinParamType::StringScalar,
188 arity: BuiltinParamArity::Required,
189 default: Some("\" \""),
190 description: "Single-character padding value.",
191 },
192];
193
194const PAD_INPUTS_LENGTH_DIRECTION_PADCHAR: [BuiltinParamDescriptor; 4] = [
195 BuiltinParamDescriptor {
196 name: "str",
197 ty: BuiltinParamType::Any,
198 arity: BuiltinParamArity::Required,
199 default: None,
200 description: "Input text (string/char/cell).",
201 },
202 BuiltinParamDescriptor {
203 name: "len",
204 ty: BuiltinParamType::IntegerScalar,
205 arity: BuiltinParamArity::Required,
206 default: None,
207 description: "Target length (non-negative integer).",
208 },
209 BuiltinParamDescriptor {
210 name: "direction",
211 ty: BuiltinParamType::StringScalar,
212 arity: BuiltinParamArity::Required,
213 default: Some("\"right\""),
214 description: "Padding direction (`\"left\"|\"right\"|\"both\"`).",
215 },
216 BuiltinParamDescriptor {
217 name: "padCharacter",
218 ty: BuiltinParamType::StringScalar,
219 arity: BuiltinParamArity::Required,
220 default: Some("\" \""),
221 description: "Single-character padding value.",
222 },
223];
224
225const PAD_SIGNATURES: [BuiltinSignatureDescriptor; 8] = [
226 BuiltinSignatureDescriptor {
227 label: "out = pad(str)",
228 inputs: &PAD_INPUTS_BASE,
229 outputs: &PAD_OUTPUT,
230 },
231 BuiltinSignatureDescriptor {
232 label: "out = pad(str, len)",
233 inputs: &PAD_INPUTS_LENGTH,
234 outputs: &PAD_OUTPUT,
235 },
236 BuiltinSignatureDescriptor {
237 label: "out = pad(str, direction)",
238 inputs: &PAD_INPUTS_DIRECTION,
239 outputs: &PAD_OUTPUT,
240 },
241 BuiltinSignatureDescriptor {
242 label: "out = pad(str, padCharacter)",
243 inputs: &PAD_INPUTS_PADCHAR,
244 outputs: &PAD_OUTPUT,
245 },
246 BuiltinSignatureDescriptor {
247 label: "out = pad(str, len, direction)",
248 inputs: &PAD_INPUTS_LENGTH_DIRECTION,
249 outputs: &PAD_OUTPUT,
250 },
251 BuiltinSignatureDescriptor {
252 label: "out = pad(str, len, padCharacter)",
253 inputs: &PAD_INPUTS_LENGTH_PADCHAR,
254 outputs: &PAD_OUTPUT,
255 },
256 BuiltinSignatureDescriptor {
257 label: "out = pad(str, direction, padCharacter)",
258 inputs: &PAD_INPUTS_DIRECTION_PADCHAR,
259 outputs: &PAD_OUTPUT,
260 },
261 BuiltinSignatureDescriptor {
262 label: "out = pad(str, len, direction, padCharacter)",
263 inputs: &PAD_INPUTS_LENGTH_DIRECTION_PADCHAR,
264 outputs: &PAD_OUTPUT,
265 },
266];
267
268const PAD_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
269 code: "RM.PAD.INVALID_INPUT",
270 identifier: Some("RunMat:pad:InvalidInput"),
271 when: "First argument is not a string array, char array, or cell array of text scalars.",
272 message:
273 "pad: first argument must be a string array, character array, or cell array of character vectors",
274};
275
276const PAD_ERROR_LENGTH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
277 code: "RM.PAD.LENGTH",
278 identifier: Some("RunMat:pad:Length"),
279 when: "Length argument is not a non-negative integer scalar.",
280 message: "pad: target length must be a non-negative integer scalar",
281};
282
283const PAD_ERROR_DIRECTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
284 code: "RM.PAD.DIRECTION",
285 identifier: Some("RunMat:pad:Direction"),
286 when: "Direction argument is not one of left/right/both.",
287 message: "pad: direction must be 'left', 'right', or 'both'",
288};
289
290const PAD_ERROR_PAD_CHAR: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
291 code: "RM.PAD.PAD_CHAR",
292 identifier: Some("RunMat:pad:PadChar"),
293 when: "Padding character is not a single-character string/char scalar.",
294 message:
295 "pad: padding character must be a string scalar or character vector containing one character",
296};
297
298const PAD_ERROR_CELL_ELEMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
299 code: "RM.PAD.CELL_ELEMENT",
300 identifier: Some("RunMat:pad:CellElement"),
301 when: "Cell arrays contain non-text elements or non-row char arrays.",
302 message: "pad: cell array elements must be string scalars or character vectors",
303};
304
305const PAD_ERROR_ARGUMENT_CONFIG: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
306 code: "RM.PAD.ARGUMENT_CONFIG",
307 identifier: Some("RunMat:pad:ArgumentConfig"),
308 when: "Second/third arguments cannot be interpreted as valid pad argument combinations.",
309 message: "pad: unable to interpret input arguments",
310};
311
312const PAD_ERROR_ARG_COUNT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
313 code: "RM.PAD.ARG_COUNT",
314 identifier: Some("RunMat:pad:ArgCount"),
315 when: "More than four total arguments are supplied.",
316 message: "pad: too many input arguments",
317};
318
319const PAD_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
320 code: "RM.PAD.INTERNAL",
321 identifier: Some("RunMat:pad:InternalError"),
322 when: "Internal output container construction failed.",
323 message: "pad: internal error",
324};
325
326const PAD_ERRORS: [BuiltinErrorDescriptor; 8] = [
327 PAD_ERROR_INVALID_INPUT,
328 PAD_ERROR_LENGTH,
329 PAD_ERROR_DIRECTION,
330 PAD_ERROR_PAD_CHAR,
331 PAD_ERROR_CELL_ELEMENT,
332 PAD_ERROR_ARGUMENT_CONFIG,
333 PAD_ERROR_ARG_COUNT,
334 PAD_ERROR_INTERNAL,
335];
336
337const MAX_PAD_TARGET_LENGTH: usize = isize::MAX as usize;
338
339pub const PAD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
340 signatures: &PAD_SIGNATURES,
341 output_mode: BuiltinOutputMode::Fixed,
342 completion_policy: BuiltinCompletionPolicy::Public,
343 errors: &PAD_ERRORS,
344};
345
346const PAD_LENGTH_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 1] =
347 [BuiltinIntegerInputCapability {
348 name: "numberOfCharacters",
349 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
350 availability: BuiltinIntegerInputAvailability::Documented,
351 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
352 notes: "The compatibility target explicitly documents single, double, and every built-in integer class for the nonnegative scalar output length.",
353 }];
354
355pub const PAD_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
356 [BuiltinIntegerCapabilityDescriptor {
357 form: "newStr = pad(str, integer_numberOfCharacters, side?, padCharacter?)",
358 inputs: &PAD_LENGTH_INTEGER_INPUTS,
359 computation_domain: BuiltinIntegerComputationDomain::Structural,
360 output_class: BuiltinIntegerOutputClassRule::NotApplicable,
361 overflow: BuiltinIntegerOverflowRule::Error,
362 backend: BuiltinIntegerBackendRule::HostOnly,
363 overload: BuiltinIntegerOverloadKind::StructuralParameter,
364 notes: "The exact integer controls only target text length; output preserves the input text container. Explicit interactive GPU length input is unsupported, while automatic residency may gather transparently.",
365 }];
366
367fn map_flow(err: RuntimeError) -> RuntimeError {
368 map_control_flow_with_builtin(err, BUILTIN_NAME)
369}
370
371fn pad_error_with_message(
372 message: impl Into<String>,
373 error: &'static BuiltinErrorDescriptor,
374) -> RuntimeError {
375 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
376 if let Some(identifier) = error.identifier {
377 builder = builder.with_identifier(identifier);
378 }
379 builder.build()
380}
381
382fn pad_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
383 pad_error_with_message(error.message, error)
384}
385
386#[derive(Clone, Copy, Eq, PartialEq)]
387enum PadDirection {
388 Left,
389 Right,
390 Both,
391}
392
393#[derive(Clone, Copy)]
394enum PadTarget {
395 Auto,
396 Length(usize),
397}
398
399#[derive(Clone, Copy)]
400struct PadOptions {
401 target: PadTarget,
402 direction: PadDirection,
403 pad_char: char,
404}
405
406impl Default for PadOptions {
407 fn default() -> Self {
408 Self {
409 target: PadTarget::Auto,
410 direction: PadDirection::Right,
411 pad_char: ' ',
412 }
413 }
414}
415
416impl PadOptions {
417 fn base_target(&self, auto_target: usize) -> usize {
418 match self.target {
419 PadTarget::Auto => auto_target,
420 PadTarget::Length(len) => len,
421 }
422 }
423}
424
425#[runtime_builtin(
426 name = "pad",
427 category = "strings/transform",
428 summary = "Pad text values to target lengths with configurable direction and fill characters.",
429 keywords = "pad,align,strings,character array",
430 accel = "sink",
431 type_resolver(text_preserve_type),
432 descriptor(crate::builtins::strings::transform::pad::PAD_DESCRIPTOR),
433 integer_capabilities(crate::builtins::strings::transform::pad::PAD_INTEGER_CAPABILITIES),
434 builtin_path = "crate::builtins::strings::transform::pad"
435)]
436async fn pad_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
437 if crate::dispatcher::value_contains_gpu(&value) {
438 return Err(pad_error(&PAD_ERROR_INVALID_INPUT));
439 }
440 if rest.iter().any(
441 |arg| matches!(arg, Value::GpuTensor(handle) if runmat_accelerate_api::handle_is_explicit(handle)),
442 ) {
443 return Err(pad_error(&PAD_ERROR_LENGTH));
444 }
445 let mut gathered_rest = Vec::with_capacity(rest.len());
446 for arg in rest {
447 gathered_rest.push(gather_if_needed_async(&arg).await.map_err(map_flow)?);
448 }
449 let options = parse_arguments(&gathered_rest)?;
450 let gathered = gather_if_needed_async(&value).await.map_err(map_flow)?;
451 match gathered {
452 Value::String(text) => pad_string(text, options),
453 Value::StringArray(array) => pad_string_array(array, options),
454 Value::CharArray(array) => pad_char_array(array, options),
455 Value::Cell(cell) => pad_cell_array(cell, options).await,
456 _ => Err(pad_error(&PAD_ERROR_INVALID_INPUT)),
457 }
458}
459
460fn pad_string(text: String, options: PadOptions) -> BuiltinResult<Value> {
461 if is_missing_string(&text) {
462 return Ok(Value::String(text));
463 }
464 let char_count = string_length(&text);
465 let base_target = options.base_target(char_count);
466 let target_len = element_target_length(&options, base_target, char_count);
467 let padded = apply_padding_owned(text, char_count, target_len, &options)?;
468 Ok(Value::String(padded))
469}
470
471fn pad_string_array(array: StringArray, options: PadOptions) -> BuiltinResult<Value> {
472 let StringArray { data, shape, .. } = array;
473 let mut auto_len: usize = 0;
474 if matches!(options.target, PadTarget::Auto) {
475 for text in &data {
476 if !is_missing_string(text) {
477 auto_len = auto_len.max(string_length(text));
478 }
479 }
480 }
481 let base_target = options.base_target(auto_len);
482 let mut padded: Vec<String> = Vec::with_capacity(data.len());
483 for text in data.into_iter() {
484 if is_missing_string(&text) {
485 padded.push(text);
486 continue;
487 }
488 let char_count = string_length(&text);
489 let target_len = element_target_length(&options, base_target, char_count);
490 let new_text = apply_padding_owned(text, char_count, target_len, &options)?;
491 padded.push(new_text);
492 }
493 let result = StringArray::new(padded, shape)
494 .map_err(|e| pad_error_with_message(format!("{BUILTIN_NAME}: {e}"), &PAD_ERROR_INTERNAL))?;
495 Ok(Value::StringArray(result))
496}
497
498fn pad_char_array(array: CharArray, options: PadOptions) -> BuiltinResult<Value> {
499 let CharArray {
500 data,
501 shape,
502 rows,
503 cols,
504 } = array;
505 if rows == 0 {
506 return Ok(Value::CharArray(CharArray {
507 data,
508 shape,
509 rows,
510 cols,
511 }));
512 }
513
514 let mut rows_text: Vec<String> = Vec::with_capacity(rows);
515 let mut auto_len = 0usize;
516 for row in 0..rows {
517 let text = char_row_to_string_slice(&data, cols, row);
518 auto_len = auto_len.max(string_length(&text));
519 rows_text.push(text);
520 }
521
522 let base_target = options.base_target(auto_len);
523 let mut padded_rows: Vec<String> = Vec::with_capacity(rows);
524 let mut final_cols: usize = 0;
525 for row_text in rows_text.into_iter() {
526 let char_count = string_length(&row_text);
527 let target_len = element_target_length(&options, base_target, char_count);
528 let padded = apply_padding_owned(row_text, char_count, target_len, &options)?;
529 final_cols = final_cols.max(string_length(&padded));
530 padded_rows.push(padded);
531 }
532
533 let total_chars = rows
534 .checked_mul(final_cols)
535 .ok_or_else(|| pad_error(&PAD_ERROR_LENGTH))?;
536 let mut new_data: Vec<char> = Vec::new();
537 new_data
538 .try_reserve_exact(total_chars)
539 .map_err(|_| pad_error(&PAD_ERROR_LENGTH))?;
540 for row_text in padded_rows.into_iter() {
541 let mut chars: Vec<char> = row_text.chars().collect();
542 if chars.len() < final_cols {
543 chars.resize(final_cols, ' ');
544 }
545 new_data.extend(chars.into_iter());
546 }
547
548 CharArray::new(new_data, rows, final_cols)
549 .map(Value::CharArray)
550 .map_err(|e| pad_error_with_message(format!("{BUILTIN_NAME}: {e}"), &PAD_ERROR_INTERNAL))
551}
552
553async fn pad_cell_array(cell: CellArray, options: PadOptions) -> BuiltinResult<Value> {
554 let rows = cell.rows;
555 let cols = cell.cols;
556 let total = rows * cols;
557 let mut items: Vec<CellItem> = Vec::with_capacity(total);
558 let mut auto_len = 0usize;
559
560 for idx in 0..total {
561 let value = &cell.data[idx];
562 let gathered = gather_if_needed_async(value).await.map_err(map_flow)?;
563 let item = match gathered {
564 Value::String(text) => {
565 let is_missing = is_missing_string(&text);
566 let len = if is_missing { 0 } else { string_length(&text) };
567 if !is_missing {
568 auto_len = auto_len.max(len);
569 }
570 CellItem {
571 kind: CellKind::String,
572 text,
573 char_count: len,
574 is_missing,
575 }
576 }
577 Value::StringArray(sa) if sa.data.len() == 1 => {
578 let text = sa.data.into_iter().next().unwrap_or_default();
579 let is_missing = is_missing_string(&text);
580 let len = if is_missing { 0 } else { string_length(&text) };
581 if !is_missing {
582 auto_len = auto_len.max(len);
583 }
584 CellItem {
585 kind: CellKind::String,
586 text,
587 char_count: len,
588 is_missing,
589 }
590 }
591 Value::CharArray(ca) if ca.rows <= 1 => {
592 let text = if ca.rows == 0 {
593 String::new()
594 } else {
595 char_row_to_string_slice(&ca.data, ca.cols, 0)
596 };
597 let len = string_length(&text);
598 auto_len = auto_len.max(len);
599 CellItem {
600 kind: CellKind::Char { rows: ca.rows },
601 text,
602 char_count: len,
603 is_missing: false,
604 }
605 }
606 Value::CharArray(_) => return Err(pad_error(&PAD_ERROR_CELL_ELEMENT)),
607 _ => return Err(pad_error(&PAD_ERROR_CELL_ELEMENT)),
608 };
609 items.push(item);
610 }
611
612 let base_target = options.base_target(auto_len);
613 let mut results: Vec<Value> = Vec::with_capacity(total);
614 for item in items.into_iter() {
615 if item.is_missing {
616 results.push(Value::String(item.text));
617 continue;
618 }
619 let target_len = element_target_length(&options, base_target, item.char_count);
620 let padded = apply_padding_owned(item.text, item.char_count, target_len, &options)?;
621 match item.kind {
622 CellKind::String => results.push(Value::String(padded)),
623 CellKind::Char { rows } => {
624 let chars: Vec<char> = padded.chars().collect();
625 let cols = chars.len();
626 let array = CharArray::new(chars, rows, cols).map_err(|e| {
627 pad_error_with_message(format!("{BUILTIN_NAME}: {e}"), &PAD_ERROR_INTERNAL)
628 })?;
629 results.push(Value::CharArray(array));
630 }
631 }
632 }
633
634 make_cell(results, rows, cols)
635 .map_err(|e| pad_error_with_message(format!("{BUILTIN_NAME}: {e}"), &PAD_ERROR_INTERNAL))
636}
637
638#[derive(Clone)]
639struct CellItem {
640 kind: CellKind,
641 text: String,
642 char_count: usize,
643 is_missing: bool,
644}
645
646#[derive(Clone)]
647enum CellKind {
648 String,
649 Char { rows: usize },
650}
651
652fn parse_arguments(args: &[Value]) -> BuiltinResult<PadOptions> {
653 let mut options = PadOptions::default();
654 match args.len() {
655 0 => Ok(options),
656 1 => {
657 if let Some(length) = parse_length(&args[0])? {
658 options.target = PadTarget::Length(length);
659 return Ok(options);
660 }
661 if let Some(direction) = try_parse_direction(&args[0], false)? {
662 options.direction = direction;
663 return Ok(options);
664 }
665 let pad_char = parse_pad_char(&args[0])?;
666 options.pad_char = pad_char;
667 Ok(options)
668 }
669 2 => {
670 if let Some(length) = parse_length(&args[0])? {
671 options.target = PadTarget::Length(length);
672 if let Some(direction) = try_parse_direction(&args[1], false)? {
673 options.direction = direction;
674 } else {
675 match parse_pad_char(&args[1]) {
676 Ok(pad_char) => options.pad_char = pad_char,
677 Err(_) => return Err(pad_error(&PAD_ERROR_DIRECTION)),
678 }
679 }
680 Ok(options)
681 } else if let Some(direction) = try_parse_direction(&args[0], false)? {
682 options.direction = direction;
683 let pad_char = parse_pad_char(&args[1])?;
684 options.pad_char = pad_char;
685 Ok(options)
686 } else {
687 Err(pad_error(&PAD_ERROR_ARGUMENT_CONFIG))
688 }
689 }
690 3 => {
691 let length = parse_length(&args[0])?.ok_or_else(|| pad_error(&PAD_ERROR_LENGTH))?;
692 let direction = try_parse_direction(&args[1], true)?
693 .ok_or_else(|| pad_error(&PAD_ERROR_DIRECTION))?;
694 let pad_char = parse_pad_char(&args[2])?;
695 options.target = PadTarget::Length(length);
696 options.direction = direction;
697 options.pad_char = pad_char;
698 Ok(options)
699 }
700 _ => Err(pad_error(&PAD_ERROR_ARG_COUNT)),
701 }
702}
703
704fn parse_length(value: &Value) -> BuiltinResult<Option<usize>> {
705 match value {
706 Value::Num(n) => parse_numeric_length(*n).map(Some),
707 Value::Int(i) => i
708 .try_to_usize()
709 .filter(|length| *length <= MAX_PAD_TARGET_LENGTH)
710 .map(Some)
711 .ok_or_else(|| pad_error(&PAD_ERROR_LENGTH)),
712 Value::Tensor(t) if tensor::is_scalar_tensor(t) => {
713 if let Some(value) = t.integer_storage().and_then(|storage| storage.value_at(0)) {
714 return parse_integer_length(&value).map(Some);
715 }
716 parse_numeric_length(tensor::tensor_value_f64(t, 0)).map(Some)
717 }
718 Value::Tensor(_) => Err(pad_error(&PAD_ERROR_LENGTH)),
719 _ => Ok(None),
720 }
721}
722
723fn parse_integer_length(value: &IntValue) -> BuiltinResult<usize> {
724 value
725 .try_to_usize()
726 .filter(|length| *length <= MAX_PAD_TARGET_LENGTH)
727 .ok_or_else(|| pad_error(&PAD_ERROR_LENGTH))
728}
729
730fn parse_numeric_length(value: f64) -> BuiltinResult<usize> {
731 if !value.is_finite() || value < 0.0 {
732 return Err(pad_error(&PAD_ERROR_LENGTH));
733 }
734 if (value.fract()).abs() > f64::EPSILON {
735 return Err(pad_error(&PAD_ERROR_LENGTH));
736 }
737 if value >= MAX_PAD_TARGET_LENGTH as f64 {
738 return Err(pad_error(&PAD_ERROR_LENGTH));
739 }
740 Ok(value as usize)
741}
742
743fn try_parse_direction(value: &Value, strict: bool) -> BuiltinResult<Option<PadDirection>> {
744 let Some(text) = value_to_single_string(value) else {
745 return if strict {
746 Err(pad_error(&PAD_ERROR_DIRECTION))
747 } else {
748 Ok(None)
749 };
750 };
751 let lowered = text.trim().to_ascii_lowercase();
752 if lowered.is_empty() {
753 return if strict {
754 Err(pad_error(&PAD_ERROR_DIRECTION))
755 } else {
756 Ok(None)
757 };
758 }
759 let direction = match lowered.as_str() {
760 "left" => PadDirection::Left,
761 "right" => PadDirection::Right,
762 "both" => PadDirection::Both,
763 _ => {
764 return if strict {
765 Err(pad_error(&PAD_ERROR_DIRECTION))
766 } else {
767 Ok(None)
768 };
769 }
770 };
771 Ok(Some(direction))
772}
773
774fn parse_pad_char(value: &Value) -> BuiltinResult<char> {
775 let text = value_to_single_string(value).ok_or_else(|| pad_error(&PAD_ERROR_PAD_CHAR))?;
776 let mut chars = text.chars();
777 let Some(first) = chars.next() else {
778 return Err(pad_error(&PAD_ERROR_PAD_CHAR));
779 };
780 if chars.next().is_some() {
781 return Err(pad_error(&PAD_ERROR_PAD_CHAR));
782 }
783 Ok(first)
784}
785
786fn value_to_single_string(value: &Value) -> Option<String> {
787 match value {
788 Value::String(text) => Some(text.clone()),
789 Value::StringArray(sa) => {
790 if sa.data.len() == 1 {
791 Some(sa.data[0].clone())
792 } else {
793 None
794 }
795 }
796 Value::CharArray(ca) if ca.rows <= 1 => {
797 if ca.rows == 0 {
798 Some(String::new())
799 } else {
800 Some(char_row_to_string_slice(&ca.data, ca.cols, 0))
801 }
802 }
803 _ => None,
804 }
805}
806
807fn string_length(text: &str) -> usize {
808 text.chars().count()
809}
810
811fn element_target_length(options: &PadOptions, base_target: usize, current_len: usize) -> usize {
812 match options.target {
813 PadTarget::Auto => base_target.max(current_len),
814 PadTarget::Length(_) => base_target.max(current_len),
815 }
816}
817
818fn apply_padding_owned(
819 text: String,
820 current_len: usize,
821 target_len: usize,
822 options: &PadOptions,
823) -> BuiltinResult<String> {
824 if current_len >= target_len {
825 return Ok(text);
826 }
827 let delta = target_len - current_len;
828 let (left_pad, right_pad) = match options.direction {
829 PadDirection::Left => (delta, 0),
830 PadDirection::Right => (0, delta),
831 PadDirection::Both => {
832 let left = delta / 2;
833 (left, delta - left)
834 }
835 };
836 let pad_bytes = delta
837 .checked_mul(options.pad_char.len_utf8())
838 .ok_or_else(|| pad_error(&PAD_ERROR_LENGTH))?;
839 let capacity = text
840 .len()
841 .checked_add(pad_bytes)
842 .ok_or_else(|| pad_error(&PAD_ERROR_LENGTH))?;
843 let mut result = String::new();
844 result
845 .try_reserve_exact(capacity)
846 .map_err(|_| pad_error(&PAD_ERROR_LENGTH))?;
847 for _ in 0..left_pad {
848 result.push(options.pad_char);
849 }
850 result.push_str(&text);
851 for _ in 0..right_pad {
852 result.push(options.pad_char);
853 }
854 Ok(result)
855}
856
857#[cfg(test)]
858pub(crate) mod tests {
859 use super::*;
860 use crate::builtins::common::test_support;
861 use runmat_builtins::{ResolveContext, Type};
862 use runmat_value::{IntValue, IntegerStorage, Tensor};
863
864 fn pad_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
865 futures::executor::block_on(super::pad_builtin(value, rest))
866 }
867
868 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
869 #[test]
870 fn pad_string_length_right() {
871 let result = pad_builtin(Value::String("GPU".into()), vec![Value::Num(5.0)]).expect("pad");
872 assert_eq!(result, Value::String("GPU ".into()));
873 }
874
875 #[test]
876 fn pad_length_reads_typed_integer_tensor_exactly() {
877 let length = Tensor::new_integer(IntegerStorage::U64(vec![5]), vec![1, 1]).expect("length");
878
879 let result =
880 pad_builtin(Value::String("GPU".into()), vec![Value::Tensor(length)]).expect("pad");
881 assert_eq!(result, Value::String("GPU ".into()));
882 }
883
884 #[test]
885 fn pad_gathers_automatic_integer_length_but_rejects_explicit_length() {
886 test_support::with_test_provider(|provider| {
887 let values = [5_u64];
888 let handle = provider
889 .upload_integer(&HostIntegerTensorView {
890 data: HostIntegerDataView::U64(&values),
891 shape: &[1, 1],
892 })
893 .expect("automatic integer length");
894 let handle =
895 handle.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
896 let result = pad_builtin(
897 Value::String("GPU".into()),
898 vec![Value::GpuTensor(handle.clone())],
899 )
900 .expect("automatic residency is transparent");
901 assert_eq!(result, Value::String("GPU ".into()));
902
903 let handle =
904 handle.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
905 let error = pad_builtin(
906 Value::String("GPU".into()),
907 vec![Value::GpuTensor(handle.clone())],
908 )
909 .expect_err("explicit gpuArray length is unsupported");
910 assert_eq!(error.identifier(), PAD_ERROR_LENGTH.identifier);
911 provider.free(&handle).expect("free length");
912 });
913 }
914
915 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
916 #[test]
917 fn pad_string_left_with_custom_char() {
918 let result = pad_builtin(
919 Value::String("42".into()),
920 vec![
921 Value::Num(4.0),
922 Value::String("left".into()),
923 Value::String("0".into()),
924 ],
925 )
926 .expect("pad");
927 assert_eq!(result, Value::String("0042".into()));
928 }
929
930 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
931 #[test]
932 fn pad_string_both_with_odd_count() {
933 let result = pad_builtin(
934 Value::String("core".into()),
935 vec![
936 Value::Num(9.0),
937 Value::String("both".into()),
938 Value::String("*".into()),
939 ],
940 )
941 .expect("pad");
942 assert_eq!(result, Value::String("**core***".into()));
943 }
944
945 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
946 #[test]
947 fn pad_string_array_auto_uses_longest_element() {
948 let strings =
949 StringArray::new(vec!["GPU".into(), "Accelerate".into()], vec![2, 1]).unwrap();
950 let result = pad_builtin(Value::StringArray(strings), Vec::new()).expect("pad");
951 match result {
952 Value::StringArray(sa) => {
953 assert_eq!(sa.data[0], "GPU ");
954 assert_eq!(sa.data[1], "Accelerate");
955 }
956 other => panic!("expected string array, got {other:?}"),
957 }
958 }
959
960 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
961 #[test]
962 fn pad_string_array_pad_character_only() {
963 let strings = StringArray::new(vec!["A".into(), "Run".into()], vec![2, 1]).unwrap();
964 let result =
965 pad_builtin(Value::StringArray(strings), vec![Value::String("*".into())]).expect("pad");
966 match result {
967 Value::StringArray(sa) => {
968 assert_eq!(sa.data[0], "A**");
969 assert_eq!(sa.data[1], "Run");
970 }
971 other => panic!("expected string array, got {other:?}"),
972 }
973 }
974
975 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
976 #[test]
977 fn pad_string_array_length_with_pad_character() {
978 let strings = StringArray::new(vec!["7".into(), "512".into()], vec![2, 1]).unwrap();
979 let result = pad_builtin(
980 Value::StringArray(strings),
981 vec![Value::Num(4.0), Value::String("0".into())],
982 )
983 .expect("pad");
984 match result {
985 Value::StringArray(sa) => {
986 assert_eq!(sa.data[0], "7000");
987 assert_eq!(sa.data[1], "5120");
988 }
989 other => panic!("expected string array, got {other:?}"),
990 }
991 }
992
993 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
994 #[test]
995 fn pad_string_array_direction_only() {
996 let strings =
997 StringArray::new(vec!["Mary".into(), "Elizabeth".into()], vec![2, 1]).unwrap();
998 let result = pad_builtin(
999 Value::StringArray(strings),
1000 vec![Value::String("left".into())],
1001 )
1002 .expect("pad");
1003 match result {
1004 Value::StringArray(sa) => {
1005 assert_eq!(sa.data[0], " Mary");
1006 assert_eq!(sa.data[1], "Elizabeth");
1007 }
1008 other => panic!("expected string array, got {other:?}"),
1009 }
1010 }
1011
1012 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1013 #[test]
1014 fn pad_single_string_pad_character_only_leaves_length() {
1015 let result =
1016 pad_builtin(Value::String("GPU".into()), vec![Value::String("-".into())]).expect("pad");
1017 assert_eq!(result, Value::String("GPU".into()));
1018 }
1019
1020 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1021 #[test]
1022 fn pad_char_array_resizes_columns() {
1023 let chars: Vec<char> = "GPUrun".chars().collect();
1024 let array = CharArray::new(chars, 2, 3).unwrap();
1025 let result = pad_builtin(Value::CharArray(array), vec![Value::Num(5.0)]).expect("pad");
1026 match result {
1027 Value::CharArray(ca) => {
1028 assert_eq!(ca.rows, 2);
1029 assert_eq!(ca.cols, 5);
1030 let expected: Vec<char> = "GPU run ".chars().collect();
1031 assert_eq!(ca.data, expected);
1032 }
1033 other => panic!("expected char array, got {other:?}"),
1034 }
1035 }
1036
1037 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1038 #[test]
1039 fn pad_cell_array_mixed_content() {
1040 let cell = CellArray::new(
1041 vec![
1042 Value::String("solver".into()),
1043 Value::CharArray(CharArray::new_row("jit")),
1044 Value::String("planner".into()),
1045 ],
1046 1,
1047 3,
1048 )
1049 .unwrap();
1050 let result = pad_builtin(
1051 Value::Cell(cell),
1052 vec![Value::String("right".into()), Value::String(".".into())],
1053 )
1054 .expect("pad");
1055 match result {
1056 Value::Cell(out) => {
1057 assert_eq!(out.rows, 1);
1058 assert_eq!(out.cols, 3);
1059 assert_eq!(out.get(0, 0).unwrap(), Value::String("solver.".into()));
1060 assert_eq!(
1061 out.get(0, 1).unwrap(),
1062 Value::CharArray(CharArray::new_row("jit...."))
1063 );
1064 assert_eq!(out.get(0, 2).unwrap(), Value::String("planner".into()));
1065 }
1066 other => panic!("expected cell array, got {other:?}"),
1067 }
1068 }
1069
1070 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1071 #[test]
1072 fn pad_preserves_missing_string() {
1073 let result =
1074 pad_builtin(Value::String("<missing>".into()), vec![Value::Num(8.0)]).expect("pad");
1075 assert_eq!(result, Value::String("<missing>".into()));
1076 }
1077
1078 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1079 #[test]
1080 fn pad_errors_on_invalid_input_type() {
1081 let err = pad_builtin(Value::Num(1.0), Vec::new()).unwrap_err();
1082 assert_eq!(err.to_string(), PAD_ERROR_INVALID_INPUT.message);
1083 }
1084
1085 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1086 #[test]
1087 fn pad_errors_on_negative_length() {
1088 let err = pad_builtin(Value::String("data".into()), vec![Value::Num(-1.0)]).unwrap_err();
1089 assert_eq!(err.to_string(), PAD_ERROR_LENGTH.message);
1090 }
1091
1092 #[test]
1093 fn pad_length_rejects_negative_typed_integer_tensor() {
1094 let length =
1095 Tensor::new_integer(IntegerStorage::I64(vec![-1]), vec![1, 1]).expect("length");
1096
1097 let err =
1098 pad_builtin(Value::String("data".into()), vec![Value::Tensor(length)]).unwrap_err();
1099 assert_eq!(err.to_string(), PAD_ERROR_LENGTH.message);
1100 }
1101
1102 #[test]
1103 fn pad_length_rejects_oversized_exact_integer_and_double_lengths() {
1104 let length =
1105 Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX]), vec![1, 1]).expect("length");
1106 let err =
1107 pad_builtin(Value::String("data".into()), vec![Value::Tensor(length)]).unwrap_err();
1108 assert_eq!(err.to_string(), PAD_ERROR_LENGTH.message);
1109
1110 let err = pad_builtin(
1111 Value::String("data".into()),
1112 vec![Value::Int(IntValue::U64(u64::MAX))],
1113 )
1114 .unwrap_err();
1115 assert_eq!(err.to_string(), PAD_ERROR_LENGTH.message);
1116
1117 let err = pad_builtin(
1118 Value::String("data".into()),
1119 vec![Value::Num(MAX_PAD_TARGET_LENGTH as f64)],
1120 )
1121 .unwrap_err();
1122 assert_eq!(err.to_string(), PAD_ERROR_LENGTH.message);
1123 }
1124
1125 #[test]
1126 fn pad_length_rejects_nonscalar_typed_integer_tensor() {
1127 let length =
1128 Tensor::new_integer(IntegerStorage::I32(vec![2, 3]), vec![1, 2]).expect("length");
1129
1130 for args in [
1131 vec![Value::Tensor(length.clone())],
1132 vec![Value::Tensor(length.clone()), Value::String("left".into())],
1133 vec![
1134 Value::Tensor(length.clone()),
1135 Value::String("left".into()),
1136 Value::String("*".into()),
1137 ],
1138 ] {
1139 let err = pad_builtin(Value::String("data".into()), args).unwrap_err();
1140 assert_eq!(err.to_string(), PAD_ERROR_LENGTH.message);
1141 }
1142 }
1143
1144 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1145 #[test]
1146 fn pad_errors_on_invalid_direction() {
1147 let err = pad_builtin(
1148 Value::String("data".into()),
1149 vec![Value::Num(6.0), Value::String("around".into())],
1150 )
1151 .unwrap_err();
1152 assert_eq!(err.to_string(), PAD_ERROR_DIRECTION.message);
1153 }
1154
1155 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1156 #[test]
1157 fn pad_errors_on_invalid_pad_character() {
1158 let err = pad_builtin(
1159 Value::String("data".into()),
1160 vec![Value::String("left".into()), Value::String("##".into())],
1161 )
1162 .unwrap_err();
1163 assert_eq!(err.to_string(), PAD_ERROR_PAD_CHAR.message);
1164 }
1165
1166 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1167 #[test]
1168 #[cfg(feature = "wgpu")]
1169 fn pad_works_with_wgpu_provider_active() {
1170 test_support::with_test_provider(|_| {
1171 let result =
1172 pad_builtin(Value::String("GPU".into()), vec![Value::Num(6.0)]).expect("pad");
1173 assert_eq!(result, Value::String("GPU ".into()));
1174 });
1175 }
1176
1177 #[test]
1178 fn pad_type_preserves_text() {
1179 assert_eq!(
1180 text_preserve_type(&[Type::String], &ResolveContext::new(Vec::new())),
1181 Type::String
1182 );
1183 }
1184}