Skip to main content

runmat_runtime/builtins/strings/transform/
pad.rs

1//! MATLAB-compatible `pad` builtin with GPU-aware semantics for RunMat.
2
3#[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}