Skip to main content

runmat_runtime/builtins/strings/transform/
pad.rs

1//! MATLAB-compatible `pad` builtin with GPU-aware semantics for RunMat.
2
3use runmat_builtins::{CellArray, CharArray, StringArray, Value};
4use runmat_macros::runtime_builtin;
5
6use crate::builtins::common::map_control_flow_with_builtin;
7use crate::builtins::common::spec::{
8    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
9    ReductionNaN, ResidencyPolicy, ShapeRequirements,
10};
11use crate::builtins::strings::common::{char_row_to_string_slice, is_missing_string};
12use crate::builtins::strings::type_resolvers::text_preserve_type;
13use crate::{build_runtime_error, gather_if_needed_async, make_cell, BuiltinResult, RuntimeError};
14
15#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::transform::pad")]
16pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
17    name: "pad",
18    op_kind: GpuOpKind::Custom("string-transform"),
19    supported_precisions: &[],
20    broadcast: BroadcastSemantics::None,
21    provider_hooks: &[],
22    constant_strategy: ConstantStrategy::InlineLiteral,
23    residency: ResidencyPolicy::GatherImmediately,
24    nan_mode: ReductionNaN::Include,
25    two_pass_threshold: None,
26    workgroup_size: None,
27    accepts_nan_mode: false,
28    notes: "Executes on the CPU; GPU-resident inputs are gathered before padding to preserve MATLAB semantics.",
29};
30
31#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::transform::pad")]
32pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
33    name: "pad",
34    shape: ShapeRequirements::Any,
35    constant_strategy: ConstantStrategy::InlineLiteral,
36    elementwise: None,
37    reduction: None,
38    emits_nan: false,
39    notes: "String transformation builtin; always gathers inputs and is not eligible for fusion.",
40};
41
42const BUILTIN_NAME: &str = "pad";
43const ARG_TYPE_ERROR: &str =
44    "pad: first argument must be a string array, character array, or cell array of character vectors";
45const LENGTH_ERROR: &str = "pad: target length must be a non-negative integer scalar";
46const DIRECTION_ERROR: &str = "pad: direction must be 'left', 'right', or 'both'";
47const PAD_CHAR_ERROR: &str =
48    "pad: padding character must be a string scalar or character vector containing one character";
49const CELL_ELEMENT_ERROR: &str =
50    "pad: cell array elements must be string scalars or character vectors";
51const ARGUMENT_CONFIG_ERROR: &str = "pad: unable to interpret input arguments";
52
53fn runtime_error_for(message: impl Into<String>) -> RuntimeError {
54    build_runtime_error(message)
55        .with_builtin(BUILTIN_NAME)
56        .build()
57}
58
59fn map_flow(err: RuntimeError) -> RuntimeError {
60    map_control_flow_with_builtin(err, BUILTIN_NAME)
61}
62
63#[derive(Clone, Copy, Eq, PartialEq)]
64enum PadDirection {
65    Left,
66    Right,
67    Both,
68}
69
70#[derive(Clone, Copy)]
71enum PadTarget {
72    Auto,
73    Length(usize),
74}
75
76#[derive(Clone, Copy)]
77struct PadOptions {
78    target: PadTarget,
79    direction: PadDirection,
80    pad_char: char,
81}
82
83impl Default for PadOptions {
84    fn default() -> Self {
85        Self {
86            target: PadTarget::Auto,
87            direction: PadDirection::Right,
88            pad_char: ' ',
89        }
90    }
91}
92
93impl PadOptions {
94    fn base_target(&self, auto_target: usize) -> usize {
95        match self.target {
96            PadTarget::Auto => auto_target,
97            PadTarget::Length(len) => len,
98        }
99    }
100}
101
102#[runtime_builtin(
103    name = "pad",
104    category = "strings/transform",
105    summary = "Pad strings, character arrays, and cell arrays to a target length.",
106    keywords = "pad,align,strings,character array",
107    accel = "sink",
108    type_resolver(text_preserve_type),
109    builtin_path = "crate::builtins::strings::transform::pad"
110)]
111async fn pad_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
112    let options = parse_arguments(&rest)?;
113    let gathered = gather_if_needed_async(&value).await.map_err(map_flow)?;
114    match gathered {
115        Value::String(text) => pad_string(text, options),
116        Value::StringArray(array) => pad_string_array(array, options),
117        Value::CharArray(array) => pad_char_array(array, options),
118        Value::Cell(cell) => pad_cell_array(cell, options).await,
119        _ => Err(runtime_error_for(ARG_TYPE_ERROR)),
120    }
121}
122
123fn pad_string(text: String, options: PadOptions) -> BuiltinResult<Value> {
124    if is_missing_string(&text) {
125        return Ok(Value::String(text));
126    }
127    let char_count = string_length(&text);
128    let base_target = options.base_target(char_count);
129    let target_len = element_target_length(&options, base_target, char_count);
130    let padded = apply_padding_owned(text, char_count, target_len, &options);
131    Ok(Value::String(padded))
132}
133
134fn pad_string_array(array: StringArray, options: PadOptions) -> BuiltinResult<Value> {
135    let StringArray { data, shape, .. } = array;
136    let mut auto_len: usize = 0;
137    if matches!(options.target, PadTarget::Auto) {
138        for text in &data {
139            if !is_missing_string(text) {
140                auto_len = auto_len.max(string_length(text));
141            }
142        }
143    }
144    let base_target = options.base_target(auto_len);
145    let mut padded: Vec<String> = Vec::with_capacity(data.len());
146    for text in data.into_iter() {
147        if is_missing_string(&text) {
148            padded.push(text);
149            continue;
150        }
151        let char_count = string_length(&text);
152        let target_len = element_target_length(&options, base_target, char_count);
153        let new_text = apply_padding_owned(text, char_count, target_len, &options);
154        padded.push(new_text);
155    }
156    let result = StringArray::new(padded, shape)
157        .map_err(|e| runtime_error_for(format!("{BUILTIN_NAME}: {e}")))?;
158    Ok(Value::StringArray(result))
159}
160
161fn pad_char_array(array: CharArray, options: PadOptions) -> BuiltinResult<Value> {
162    let CharArray { data, rows, cols } = array;
163    if rows == 0 {
164        return Ok(Value::CharArray(CharArray { data, rows, cols }));
165    }
166
167    let mut rows_text: Vec<String> = Vec::with_capacity(rows);
168    let mut auto_len = 0usize;
169    for row in 0..rows {
170        let text = char_row_to_string_slice(&data, cols, row);
171        auto_len = auto_len.max(string_length(&text));
172        rows_text.push(text);
173    }
174
175    let base_target = options.base_target(auto_len);
176    let mut padded_rows: Vec<String> = Vec::with_capacity(rows);
177    let mut final_cols: usize = 0;
178    for row_text in rows_text.into_iter() {
179        let char_count = string_length(&row_text);
180        let target_len = element_target_length(&options, base_target, char_count);
181        let padded = apply_padding_owned(row_text, char_count, target_len, &options);
182        final_cols = final_cols.max(string_length(&padded));
183        padded_rows.push(padded);
184    }
185
186    let mut new_data: Vec<char> = Vec::with_capacity(rows * final_cols);
187    for row_text in padded_rows.into_iter() {
188        let mut chars: Vec<char> = row_text.chars().collect();
189        if chars.len() < final_cols {
190            chars.resize(final_cols, ' ');
191        }
192        new_data.extend(chars.into_iter());
193    }
194
195    CharArray::new(new_data, rows, final_cols)
196        .map(Value::CharArray)
197        .map_err(|e| runtime_error_for(format!("{BUILTIN_NAME}: {e}")))
198}
199
200async fn pad_cell_array(cell: CellArray, options: PadOptions) -> BuiltinResult<Value> {
201    let rows = cell.rows;
202    let cols = cell.cols;
203    let total = rows * cols;
204    let mut items: Vec<CellItem> = Vec::with_capacity(total);
205    let mut auto_len = 0usize;
206
207    for idx in 0..total {
208        let value = &cell.data[idx];
209        let gathered = gather_if_needed_async(value).await.map_err(map_flow)?;
210        let item = match gathered {
211            Value::String(text) => {
212                let is_missing = is_missing_string(&text);
213                let len = if is_missing { 0 } else { string_length(&text) };
214                if !is_missing {
215                    auto_len = auto_len.max(len);
216                }
217                CellItem {
218                    kind: CellKind::String,
219                    text,
220                    char_count: len,
221                    is_missing,
222                }
223            }
224            Value::StringArray(sa) if sa.data.len() == 1 => {
225                let text = sa.data.into_iter().next().unwrap_or_default();
226                let is_missing = is_missing_string(&text);
227                let len = if is_missing { 0 } else { string_length(&text) };
228                if !is_missing {
229                    auto_len = auto_len.max(len);
230                }
231                CellItem {
232                    kind: CellKind::String,
233                    text,
234                    char_count: len,
235                    is_missing,
236                }
237            }
238            Value::CharArray(ca) if ca.rows <= 1 => {
239                let text = if ca.rows == 0 {
240                    String::new()
241                } else {
242                    char_row_to_string_slice(&ca.data, ca.cols, 0)
243                };
244                let len = string_length(&text);
245                auto_len = auto_len.max(len);
246                CellItem {
247                    kind: CellKind::Char { rows: ca.rows },
248                    text,
249                    char_count: len,
250                    is_missing: false,
251                }
252            }
253            Value::CharArray(_) => return Err(runtime_error_for(CELL_ELEMENT_ERROR)),
254            _ => return Err(runtime_error_for(CELL_ELEMENT_ERROR)),
255        };
256        items.push(item);
257    }
258
259    let base_target = options.base_target(auto_len);
260    let mut results: Vec<Value> = Vec::with_capacity(total);
261    for item in items.into_iter() {
262        if item.is_missing {
263            results.push(Value::String(item.text));
264            continue;
265        }
266        let target_len = element_target_length(&options, base_target, item.char_count);
267        let padded = apply_padding_owned(item.text, item.char_count, target_len, &options);
268        match item.kind {
269            CellKind::String => results.push(Value::String(padded)),
270            CellKind::Char { rows } => {
271                let chars: Vec<char> = padded.chars().collect();
272                let cols = chars.len();
273                let array = CharArray::new(chars, rows, cols)
274                    .map_err(|e| runtime_error_for(format!("{BUILTIN_NAME}: {e}")))?;
275                results.push(Value::CharArray(array));
276            }
277        }
278    }
279
280    make_cell(results, rows, cols).map_err(|e| runtime_error_for(format!("{BUILTIN_NAME}: {e}")))
281}
282
283#[derive(Clone)]
284struct CellItem {
285    kind: CellKind,
286    text: String,
287    char_count: usize,
288    is_missing: bool,
289}
290
291#[derive(Clone)]
292enum CellKind {
293    String,
294    Char { rows: usize },
295}
296
297fn parse_arguments(args: &[Value]) -> BuiltinResult<PadOptions> {
298    let mut options = PadOptions::default();
299    match args.len() {
300        0 => Ok(options),
301        1 => {
302            if let Some(length) = parse_length(&args[0])? {
303                options.target = PadTarget::Length(length);
304                return Ok(options);
305            }
306            if let Some(direction) = try_parse_direction(&args[0], false)? {
307                options.direction = direction;
308                return Ok(options);
309            }
310            let pad_char = parse_pad_char(&args[0])?;
311            options.pad_char = pad_char;
312            Ok(options)
313        }
314        2 => {
315            if let Some(length) = parse_length(&args[0])? {
316                options.target = PadTarget::Length(length);
317                if let Some(direction) = try_parse_direction(&args[1], false)? {
318                    options.direction = direction;
319                } else {
320                    match parse_pad_char(&args[1]) {
321                        Ok(pad_char) => options.pad_char = pad_char,
322                        Err(_) => return Err(runtime_error_for(DIRECTION_ERROR)),
323                    }
324                }
325                Ok(options)
326            } else if let Some(direction) = try_parse_direction(&args[0], false)? {
327                options.direction = direction;
328                let pad_char = parse_pad_char(&args[1])?;
329                options.pad_char = pad_char;
330                Ok(options)
331            } else {
332                Err(runtime_error_for(ARGUMENT_CONFIG_ERROR))
333            }
334        }
335        3 => {
336            let length = parse_length(&args[0])?.ok_or_else(|| runtime_error_for(LENGTH_ERROR))?;
337            let direction = try_parse_direction(&args[1], true)?
338                .ok_or_else(|| runtime_error_for(DIRECTION_ERROR))?;
339            let pad_char = parse_pad_char(&args[2])?;
340            options.target = PadTarget::Length(length);
341            options.direction = direction;
342            options.pad_char = pad_char;
343            Ok(options)
344        }
345        _ => Err(runtime_error_for("pad: too many input arguments")),
346    }
347}
348
349fn parse_length(value: &Value) -> BuiltinResult<Option<usize>> {
350    match value {
351        Value::Num(n) => {
352            if !n.is_finite() || *n < 0.0 {
353                return Err(runtime_error_for(LENGTH_ERROR));
354            }
355            if (n.fract()).abs() > f64::EPSILON {
356                return Err(runtime_error_for(LENGTH_ERROR));
357            }
358            Ok(Some(*n as usize))
359        }
360        Value::Int(i) => {
361            let val = i.to_i64();
362            if val < 0 {
363                return Err(runtime_error_for(LENGTH_ERROR));
364            }
365            Ok(Some(val as usize))
366        }
367        _ => Ok(None),
368    }
369}
370
371fn try_parse_direction(value: &Value, strict: bool) -> BuiltinResult<Option<PadDirection>> {
372    let Some(text) = value_to_single_string(value) else {
373        return if strict {
374            Err(runtime_error_for(DIRECTION_ERROR))
375        } else {
376            Ok(None)
377        };
378    };
379    let lowered = text.trim().to_ascii_lowercase();
380    if lowered.is_empty() {
381        return if strict {
382            Err(runtime_error_for(DIRECTION_ERROR))
383        } else {
384            Ok(None)
385        };
386    }
387    let direction = match lowered.as_str() {
388        "left" => PadDirection::Left,
389        "right" => PadDirection::Right,
390        "both" => PadDirection::Both,
391        _ => {
392            return if strict {
393                Err(runtime_error_for(DIRECTION_ERROR))
394            } else {
395                Ok(None)
396            };
397        }
398    };
399    Ok(Some(direction))
400}
401
402fn parse_pad_char(value: &Value) -> BuiltinResult<char> {
403    let text = value_to_single_string(value).ok_or_else(|| runtime_error_for(PAD_CHAR_ERROR))?;
404    let mut chars = text.chars();
405    let Some(first) = chars.next() else {
406        return Err(runtime_error_for(PAD_CHAR_ERROR));
407    };
408    if chars.next().is_some() {
409        return Err(runtime_error_for(PAD_CHAR_ERROR));
410    }
411    Ok(first)
412}
413
414fn value_to_single_string(value: &Value) -> Option<String> {
415    match value {
416        Value::String(text) => Some(text.clone()),
417        Value::StringArray(sa) => {
418            if sa.data.len() == 1 {
419                Some(sa.data[0].clone())
420            } else {
421                None
422            }
423        }
424        Value::CharArray(ca) if ca.rows <= 1 => {
425            if ca.rows == 0 {
426                Some(String::new())
427            } else {
428                Some(char_row_to_string_slice(&ca.data, ca.cols, 0))
429            }
430        }
431        _ => None,
432    }
433}
434
435fn string_length(text: &str) -> usize {
436    text.chars().count()
437}
438
439fn element_target_length(options: &PadOptions, base_target: usize, current_len: usize) -> usize {
440    match options.target {
441        PadTarget::Auto => base_target.max(current_len),
442        PadTarget::Length(_) => base_target.max(current_len),
443    }
444}
445
446fn apply_padding_owned(
447    text: String,
448    current_len: usize,
449    target_len: usize,
450    options: &PadOptions,
451) -> String {
452    if current_len >= target_len {
453        return text;
454    }
455    let delta = target_len - current_len;
456    let (left_pad, right_pad) = match options.direction {
457        PadDirection::Left => (delta, 0),
458        PadDirection::Right => (0, delta),
459        PadDirection::Both => {
460            let left = delta / 2;
461            (left, delta - left)
462        }
463    };
464    let mut result = String::with_capacity(text.len() + delta * options.pad_char.len_utf8());
465    for _ in 0..left_pad {
466        result.push(options.pad_char);
467    }
468    result.push_str(&text);
469    for _ in 0..right_pad {
470        result.push(options.pad_char);
471    }
472    result
473}
474
475#[cfg(test)]
476pub(crate) mod tests {
477    use super::*;
478    #[cfg(feature = "wgpu")]
479    use crate::builtins::common::test_support;
480    use runmat_builtins::{ResolveContext, Type};
481
482    fn pad_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
483        futures::executor::block_on(super::pad_builtin(value, rest))
484    }
485
486    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
487    #[test]
488    fn pad_string_length_right() {
489        let result = pad_builtin(Value::String("GPU".into()), vec![Value::Num(5.0)]).expect("pad");
490        assert_eq!(result, Value::String("GPU  ".into()));
491    }
492
493    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
494    #[test]
495    fn pad_string_left_with_custom_char() {
496        let result = pad_builtin(
497            Value::String("42".into()),
498            vec![
499                Value::Num(4.0),
500                Value::String("left".into()),
501                Value::String("0".into()),
502            ],
503        )
504        .expect("pad");
505        assert_eq!(result, Value::String("0042".into()));
506    }
507
508    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
509    #[test]
510    fn pad_string_both_with_odd_count() {
511        let result = pad_builtin(
512            Value::String("core".into()),
513            vec![
514                Value::Num(9.0),
515                Value::String("both".into()),
516                Value::String("*".into()),
517            ],
518        )
519        .expect("pad");
520        assert_eq!(result, Value::String("**core***".into()));
521    }
522
523    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
524    #[test]
525    fn pad_string_array_auto_uses_longest_element() {
526        let strings =
527            StringArray::new(vec!["GPU".into(), "Accelerate".into()], vec![2, 1]).unwrap();
528        let result = pad_builtin(Value::StringArray(strings), Vec::new()).expect("pad");
529        match result {
530            Value::StringArray(sa) => {
531                assert_eq!(sa.data[0], "GPU       ");
532                assert_eq!(sa.data[1], "Accelerate");
533            }
534            other => panic!("expected string array, got {other:?}"),
535        }
536    }
537
538    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
539    #[test]
540    fn pad_string_array_pad_character_only() {
541        let strings = StringArray::new(vec!["A".into(), "Run".into()], vec![2, 1]).unwrap();
542        let result =
543            pad_builtin(Value::StringArray(strings), vec![Value::String("*".into())]).expect("pad");
544        match result {
545            Value::StringArray(sa) => {
546                assert_eq!(sa.data[0], "A**");
547                assert_eq!(sa.data[1], "Run");
548            }
549            other => panic!("expected string array, got {other:?}"),
550        }
551    }
552
553    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
554    #[test]
555    fn pad_string_array_length_with_pad_character() {
556        let strings = StringArray::new(vec!["7".into(), "512".into()], vec![2, 1]).unwrap();
557        let result = pad_builtin(
558            Value::StringArray(strings),
559            vec![Value::Num(4.0), Value::String("0".into())],
560        )
561        .expect("pad");
562        match result {
563            Value::StringArray(sa) => {
564                assert_eq!(sa.data[0], "7000");
565                assert_eq!(sa.data[1], "5120");
566            }
567            other => panic!("expected string array, got {other:?}"),
568        }
569    }
570
571    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
572    #[test]
573    fn pad_string_array_direction_only() {
574        let strings =
575            StringArray::new(vec!["Mary".into(), "Elizabeth".into()], vec![2, 1]).unwrap();
576        let result = pad_builtin(
577            Value::StringArray(strings),
578            vec![Value::String("left".into())],
579        )
580        .expect("pad");
581        match result {
582            Value::StringArray(sa) => {
583                assert_eq!(sa.data[0], "     Mary");
584                assert_eq!(sa.data[1], "Elizabeth");
585            }
586            other => panic!("expected string array, got {other:?}"),
587        }
588    }
589
590    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
591    #[test]
592    fn pad_single_string_pad_character_only_leaves_length() {
593        let result =
594            pad_builtin(Value::String("GPU".into()), vec![Value::String("-".into())]).expect("pad");
595        assert_eq!(result, Value::String("GPU".into()));
596    }
597
598    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
599    #[test]
600    fn pad_char_array_resizes_columns() {
601        let chars: Vec<char> = "GPUrun".chars().collect();
602        let array = CharArray::new(chars, 2, 3).unwrap();
603        let result = pad_builtin(Value::CharArray(array), vec![Value::Num(5.0)]).expect("pad");
604        match result {
605            Value::CharArray(ca) => {
606                assert_eq!(ca.rows, 2);
607                assert_eq!(ca.cols, 5);
608                let expected: Vec<char> = "GPU  run  ".chars().collect();
609                assert_eq!(ca.data, expected);
610            }
611            other => panic!("expected char array, got {other:?}"),
612        }
613    }
614
615    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
616    #[test]
617    fn pad_cell_array_mixed_content() {
618        let cell = CellArray::new(
619            vec![
620                Value::String("solver".into()),
621                Value::CharArray(CharArray::new_row("jit")),
622                Value::String("planner".into()),
623            ],
624            1,
625            3,
626        )
627        .unwrap();
628        let result = pad_builtin(
629            Value::Cell(cell),
630            vec![Value::String("right".into()), Value::String(".".into())],
631        )
632        .expect("pad");
633        match result {
634            Value::Cell(out) => {
635                assert_eq!(out.rows, 1);
636                assert_eq!(out.cols, 3);
637                assert_eq!(out.get(0, 0).unwrap(), Value::String("solver.".into()));
638                assert_eq!(
639                    out.get(0, 1).unwrap(),
640                    Value::CharArray(CharArray::new_row("jit...."))
641                );
642                assert_eq!(out.get(0, 2).unwrap(), Value::String("planner".into()));
643            }
644            other => panic!("expected cell array, got {other:?}"),
645        }
646    }
647
648    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
649    #[test]
650    fn pad_preserves_missing_string() {
651        let result =
652            pad_builtin(Value::String("<missing>".into()), vec![Value::Num(8.0)]).expect("pad");
653        assert_eq!(result, Value::String("<missing>".into()));
654    }
655
656    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
657    #[test]
658    fn pad_errors_on_invalid_input_type() {
659        let err = pad_builtin(Value::Num(1.0), Vec::new()).unwrap_err();
660        assert_eq!(err.to_string(), ARG_TYPE_ERROR);
661    }
662
663    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
664    #[test]
665    fn pad_errors_on_negative_length() {
666        let err = pad_builtin(Value::String("data".into()), vec![Value::Num(-1.0)]).unwrap_err();
667        assert_eq!(err.to_string(), LENGTH_ERROR);
668    }
669
670    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
671    #[test]
672    fn pad_errors_on_invalid_direction() {
673        let err = pad_builtin(
674            Value::String("data".into()),
675            vec![Value::Num(6.0), Value::String("around".into())],
676        )
677        .unwrap_err();
678        assert_eq!(err.to_string(), DIRECTION_ERROR);
679    }
680
681    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
682    #[test]
683    fn pad_errors_on_invalid_pad_character() {
684        let err = pad_builtin(
685            Value::String("data".into()),
686            vec![Value::String("left".into()), Value::String("##".into())],
687        )
688        .unwrap_err();
689        assert_eq!(err.to_string(), PAD_CHAR_ERROR);
690    }
691
692    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
693    #[test]
694    #[cfg(feature = "wgpu")]
695    fn pad_works_with_wgpu_provider_active() {
696        test_support::with_test_provider(|_| {
697            let result =
698                pad_builtin(Value::String("GPU".into()), vec![Value::Num(6.0)]).expect("pad");
699            assert_eq!(result, Value::String("GPU   ".into()));
700        });
701    }
702
703    #[test]
704    fn pad_type_preserves_text() {
705        assert_eq!(
706            text_preserve_type(&[Type::String], &ResolveContext::new(Vec::new())),
707            Type::String
708        );
709    }
710}