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