1use std::collections::HashSet;
4
5use regex::RegexBuilder;
6use runmat_builtins::{CellArray, CharArray, StringArray, Value};
7use runmat_macros::runtime_builtin;
8
9use crate::builtins::common::map_control_flow_with_builtin;
10use crate::builtins::common::spec::{
11 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
12 ReductionNaN, ResidencyPolicy, ShapeRequirements,
13};
14use crate::builtins::strings::common::{char_row_to_string_slice, is_missing_string};
15use crate::builtins::strings::type_resolvers::{string_array_type, unknown_type};
16use crate::{build_runtime_error, gather_if_needed_async, make_cell, BuiltinResult, RuntimeError};
17
18#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::transform::split")]
19pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
20 name: "split",
21 op_kind: GpuOpKind::Custom("string-transform"),
22 supported_precisions: &[],
23 broadcast: BroadcastSemantics::None,
24 provider_hooks: &[],
25 constant_strategy: ConstantStrategy::InlineLiteral,
26 residency: ResidencyPolicy::GatherImmediately,
27 nan_mode: ReductionNaN::Include,
28 two_pass_threshold: None,
29 workgroup_size: None,
30 accepts_nan_mode: false,
31 notes: "Executes on the CPU; GPU-resident inputs are gathered to host memory before splitting.",
32};
33
34#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::transform::split")]
35pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
36 name: "split",
37 shape: ShapeRequirements::Any,
38 constant_strategy: ConstantStrategy::InlineLiteral,
39 elementwise: None,
40 reduction: None,
41 emits_nan: false,
42 notes: "String transformation builtin; not eligible for fusion planning and always gathers GPU inputs.",
43};
44
45const BUILTIN_NAME: &str = "split";
46const ARG_TYPE_ERROR: &str =
47 "split: first argument must be a string scalar, string array, character array, or cell array of character vectors";
48const DELIMITER_TYPE_ERROR: &str =
49 "split: delimiter input must be a string scalar, string array, character array, or cell array of character vectors";
50const NAME_VALUE_PAIR_ERROR: &str = "split: name-value arguments must be supplied in pairs";
51const UNKNOWN_NAME_ERROR: &str =
52 "split: unrecognized name-value argument; supported names are 'CollapseDelimiters' and 'IncludeDelimiters'";
53const EMPTY_DELIMITER_ERROR: &str = "split: delimiters must contain at least one character";
54const CELL_ELEMENT_ERROR: &str =
55 "split: cell array elements must be string scalars or character vectors";
56const STRSPLIT_BUILTIN_NAME: &str = "strsplit";
57const STRSPLIT_ARG_TYPE_ERROR: &str =
58 "strsplit: first argument must be a string scalar or character vector";
59const STRSPLIT_DELIMITER_TYPE_ERROR: &str =
60 "strsplit: delimiter must be a character vector, string scalar, string array, or cell array of character vectors";
61const STRSPLIT_NAME_VALUE_PAIR_ERROR: &str =
62 "strsplit: name-value arguments must be supplied in pairs";
63const STRSPLIT_UNKNOWN_NAME_ERROR: &str =
64 "strsplit: unrecognized name-value argument; supported names are 'CollapseDelimiters' and 'DelimiterType'";
65const STRSPLIT_EMPTY_DELIMITER_ERROR: &str =
66 "strsplit: delimiters must contain at least one character";
67const STRSPLIT_DELIMITER_MODE_ERROR: &str =
68 "strsplit: value for 'DelimiterType' must be 'Simple' or 'RegularExpression'";
69
70fn runtime_error_for(message: impl Into<String>) -> RuntimeError {
71 build_runtime_error(message)
72 .with_builtin(BUILTIN_NAME)
73 .build()
74}
75
76fn map_flow(err: RuntimeError) -> RuntimeError {
77 map_control_flow_with_builtin(err, BUILTIN_NAME)
78}
79
80#[runtime_builtin(
81 name = "split",
82 category = "strings/transform",
83 summary = "Split strings, character arrays, and cell arrays into substrings using delimiters.",
84 keywords = "split,strsplit,delimiter,CollapseDelimiters,IncludeDelimiters",
85 accel = "sink",
86 type_resolver(string_array_type),
87 builtin_path = "crate::builtins::strings::transform::split"
88)]
89async fn split_builtin(text: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
90 let text = gather_if_needed_async(&text).await.map_err(map_flow)?;
91 let mut args: Vec<Value> = Vec::with_capacity(rest.len());
92 for arg in rest {
93 args.push(gather_if_needed_async(&arg).await.map_err(map_flow)?);
94 }
95
96 let options = SplitOptions::parse(&args)?;
97 let matrix = TextMatrix::from_value(text)?;
98 matrix.into_split_result(&options)
99}
100
101#[runtime_builtin(
102 name = "strsplit",
103 category = "strings/transform",
104 summary = "Split a string scalar or character vector into substrings using delimiters.",
105 keywords = "strsplit,split,delimiter,CollapseDelimiters,DelimiterType,matches",
106 accel = "sink",
107 type_resolver(unknown_type),
108 builtin_path = "crate::builtins::strings::transform::split"
109)]
110async fn strsplit_builtin(text: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
111 let text = gather_if_needed_async(&text)
112 .await
113 .map_err(|err| map_control_flow_with_builtin(err, STRSPLIT_BUILTIN_NAME))?;
114 let mut args = Vec::with_capacity(rest.len());
115 for arg in rest {
116 args.push(
117 gather_if_needed_async(&arg)
118 .await
119 .map_err(|err| map_control_flow_with_builtin(err, STRSPLIT_BUILTIN_NAME))?,
120 );
121 }
122
123 let (input_kind, subject) = extract_strsplit_subject(text)?;
124 let options = StrsplitOptions::parse(&args)?;
125 let (parts, matches) = strsplit_text(&subject, &options)?;
126 let parts_value = make_strsplit_output(parts, input_kind)?;
127
128 if let Some(out_count) = crate::output_count::current_output_count() {
129 if out_count == 0 {
130 return Ok(Value::OutputList(Vec::new()));
131 }
132 let matches_value = make_strsplit_output(matches, input_kind)?;
133 return Ok(crate::output_count::output_list_with_padding(
134 out_count,
135 vec![parts_value, matches_value],
136 ));
137 }
138
139 Ok(parts_value)
140}
141
142#[derive(Clone)]
143enum DelimiterSpec {
144 Whitespace,
145 Patterns(Vec<String>),
146}
147
148#[derive(Clone)]
149struct SplitOptions {
150 delimiters: DelimiterSpec,
151 collapse_delimiters: bool,
152 include_delimiters: bool,
153}
154
155impl SplitOptions {
156 fn parse(args: &[Value]) -> BuiltinResult<Self> {
157 let mut index = 0usize;
158 let mut delimiters = DelimiterSpec::Whitespace;
159
160 if index < args.len() && !is_name_key(&args[index]) {
161 let list = extract_delimiters(&args[index])?;
162 if list.is_empty() {
163 return Err(runtime_error_for(EMPTY_DELIMITER_ERROR));
164 }
165 let mut seen = HashSet::new();
166 let mut patterns: Vec<String> = Vec::new();
167 for pattern in list {
168 if pattern.is_empty() {
169 return Err(runtime_error_for(EMPTY_DELIMITER_ERROR));
170 }
171 if seen.insert(pattern.clone()) {
172 patterns.push(pattern);
173 }
174 }
175 patterns.sort_by_key(|pat| std::cmp::Reverse(pat.len()));
176 delimiters = DelimiterSpec::Patterns(patterns);
177 index += 1;
178 }
179
180 let mut collapse = match delimiters {
181 DelimiterSpec::Whitespace => true,
182 DelimiterSpec::Patterns(_) => false,
183 };
184 let mut include = false;
185
186 while index < args.len() {
187 let name = match name_key(&args[index]) {
188 Some(NameKey::CollapseDelimiters) => NameKey::CollapseDelimiters,
189 Some(NameKey::IncludeDelimiters) => NameKey::IncludeDelimiters,
190 None => return Err(runtime_error_for(UNKNOWN_NAME_ERROR)),
191 };
192 index += 1;
193 if index >= args.len() {
194 return Err(runtime_error_for(NAME_VALUE_PAIR_ERROR));
195 }
196 let value = &args[index];
197 index += 1;
198
199 match name {
200 NameKey::CollapseDelimiters => {
201 collapse = parse_bool(value, "CollapseDelimiters")?;
202 }
203 NameKey::IncludeDelimiters => {
204 include = parse_bool(value, "IncludeDelimiters")?;
205 }
206 }
207 }
208
209 Ok(Self {
210 delimiters,
211 collapse_delimiters: collapse,
212 include_delimiters: include,
213 })
214 }
215}
216
217struct TextMatrix {
218 data: Vec<String>,
219 rows: usize,
220 cols: usize,
221}
222
223impl TextMatrix {
224 fn from_value(value: Value) -> BuiltinResult<Self> {
225 match value {
226 Value::String(text) => Ok(Self {
227 data: vec![text],
228 rows: 1,
229 cols: 1,
230 }),
231 Value::StringArray(array) => Ok(Self {
232 data: array.data,
233 rows: array.rows,
234 cols: array.cols,
235 }),
236 Value::CharArray(array) => Self::from_char_array(array),
237 Value::Cell(cell) => Self::from_cell_array(cell),
238 _ => Err(runtime_error_for(ARG_TYPE_ERROR)),
239 }
240 }
241
242 fn from_char_array(array: CharArray) -> BuiltinResult<Self> {
243 let CharArray { data, rows, cols } = array;
244 if rows == 0 {
245 return Ok(Self {
246 data: Vec::new(),
247 rows: 0,
248 cols: 1,
249 });
250 }
251 let mut strings = Vec::with_capacity(rows);
252 for row in 0..rows {
253 strings.push(char_row_to_string_slice(&data, cols, row));
254 }
255 Ok(Self {
256 data: strings,
257 rows,
258 cols: 1,
259 })
260 }
261
262 fn from_cell_array(cell: CellArray) -> BuiltinResult<Self> {
263 let CellArray {
264 data, rows, cols, ..
265 } = cell;
266 let mut strings = Vec::with_capacity(data.len());
267 for col in 0..cols {
268 for row in 0..rows {
269 let idx = row * cols + col;
270 let value_ref: &Value = &data[idx];
271 strings.push(
272 cell_element_to_string(value_ref)
273 .ok_or_else(|| runtime_error_for(CELL_ELEMENT_ERROR))?,
274 );
275 }
276 }
277 Ok(Self {
278 data: strings,
279 rows,
280 cols,
281 })
282 }
283
284 fn into_split_result(self, options: &SplitOptions) -> BuiltinResult<Value> {
285 let TextMatrix { data, rows, cols } = self;
286
287 if data.is_empty() {
288 let block_cols = if cols == 0 { 0 } else { 1 };
289 let shape = if cols == 0 {
290 vec![rows, 0]
291 } else {
292 vec![rows, cols * block_cols]
293 };
294 let array = StringArray::new(Vec::new(), shape)
295 .map_err(|e| runtime_error_for(format!("{BUILTIN_NAME}: {e}")))?;
296 return Ok(Value::StringArray(array));
297 }
298
299 let mut per_element: Vec<Vec<String>> = Vec::with_capacity(data.len());
300 let mut max_tokens = 0usize;
301 for text in &data {
302 let tokens = split_text(text, options);
303 max_tokens = max_tokens.max(tokens.len());
304 per_element.push(tokens);
305 }
306 if max_tokens == 0 {
307 max_tokens = 1;
308 }
309 let block_cols = max_tokens;
310 let result_cols = block_cols * cols.max(1);
311 let total = rows * result_cols;
312 let missing = "<missing>".to_string();
313 let mut output = vec![missing.clone(); total];
314
315 for col in 0..cols.max(1) {
316 for row in 0..rows {
317 let element_index = if cols == 0 { row } else { row + col * rows };
318 if element_index >= per_element.len() {
319 continue;
320 }
321 let tokens = &per_element[element_index];
322 for t in 0..block_cols {
323 let out_col = if cols == 0 { t } else { col * block_cols + t };
324 let out_index = row + out_col * rows;
325 if out_index >= output.len() {
326 continue;
327 }
328 if t < tokens.len() {
329 output[out_index] = tokens[t].clone();
330 } else {
331 output[out_index] = missing.clone();
332 }
333 }
334 }
335 }
336
337 let shape = vec![rows, result_cols];
338 let array = StringArray::new(output, shape)
339 .map_err(|e| runtime_error_for(format!("{BUILTIN_NAME}: {e}")))?;
340 Ok(Value::StringArray(array))
341 }
342}
343
344fn split_text(text: &str, options: &SplitOptions) -> Vec<String> {
345 if is_missing_string(text) {
346 return vec![text.to_string()];
347 }
348 match &options.delimiters {
349 DelimiterSpec::Whitespace => split_whitespace(text, options),
350 DelimiterSpec::Patterns(patterns) => split_by_patterns(text, patterns, options),
351 }
352}
353
354fn split_whitespace(text: &str, options: &SplitOptions) -> Vec<String> {
355 if text.is_empty() {
356 return vec![String::new()];
357 }
358
359 let mut parts: Vec<String> = Vec::new();
360 let mut idx = 0usize;
361 let mut last = 0usize;
362 let len = text.len();
363
364 while idx < len {
365 let ch = text[idx..].chars().next().unwrap();
366 let width = ch.len_utf8();
367 if !ch.is_whitespace() {
368 idx += width;
369 continue;
370 }
371
372 let token = &text[last..idx];
373 if !token.is_empty() || !options.collapse_delimiters {
374 parts.push(token.to_string());
375 }
376
377 let run_end = advance_whitespace(text, idx);
378 if options.include_delimiters {
379 if options.collapse_delimiters {
380 parts.push(text[idx..run_end].to_string());
381 } else {
382 parts.push(text[idx..idx + width].to_string());
383 }
384 }
385
386 if options.collapse_delimiters {
387 idx = run_end;
388 last = run_end;
389 } else {
390 idx += width;
391 last = idx;
392 }
393 }
394
395 let tail = &text[last..];
396 if !tail.is_empty() || !options.collapse_delimiters {
397 parts.push(tail.to_string());
398 }
399 if parts.is_empty() {
400 parts.push(String::new());
401 }
402 parts
403}
404
405fn split_by_patterns(text: &str, patterns: &[String], options: &SplitOptions) -> Vec<String> {
406 if patterns.is_empty() {
407 return vec![text.to_string()];
408 }
409
410 let mut parts: Vec<String> = Vec::new();
411 let mut idx = 0usize;
412 let mut last = 0usize;
413 while idx < text.len() {
414 if let Some(pattern) = patterns
415 .iter()
416 .find(|candidate| text[idx..].starts_with(candidate.as_str()))
417 {
418 let token = &text[last..idx];
419 if !token.is_empty() || !options.collapse_delimiters {
420 parts.push(token.to_string());
421 }
422
423 let pat_len = pattern.len();
424 if options.collapse_delimiters {
425 let mut run_end = idx + pat_len;
426 while run_end < text.len() {
427 if let Some(next) = patterns
428 .iter()
429 .find(|candidate| text[run_end..].starts_with(candidate.as_str()))
430 {
431 let len = next.len();
432 if len == 0 {
433 break;
434 }
435 run_end += len;
436 } else {
437 break;
438 }
439 }
440 if options.include_delimiters {
441 parts.push(text[idx..run_end].to_string());
442 }
443 idx = run_end;
444 last = run_end;
445 } else {
446 if options.include_delimiters {
447 parts.push(text[idx..idx + pat_len].to_string());
448 }
449 idx += pat_len;
450 last = idx;
451 }
452
453 continue;
454 }
455 let ch = text[idx..].chars().next().unwrap();
456 idx += ch.len_utf8();
457 }
458 let tail = &text[last..];
459 if !tail.is_empty() || !options.collapse_delimiters {
460 parts.push(tail.to_string());
461 }
462 if parts.is_empty() {
463 parts.push(String::new());
464 }
465 parts
466}
467
468fn advance_whitespace(text: &str, mut start: usize) -> usize {
469 while start < text.len() {
470 let ch = text[start..].chars().next().unwrap();
471 if !ch.is_whitespace() {
472 break;
473 }
474 start += ch.len_utf8();
475 }
476 start
477}
478
479fn extract_delimiters(value: &Value) -> BuiltinResult<Vec<String>> {
480 match value {
481 Value::String(text) => Ok(vec![text.clone()]),
482 Value::StringArray(array) => Ok(array.data.clone()),
483 Value::CharArray(array) => {
484 if array.rows == 0 {
485 return Ok(Vec::new());
486 }
487 let mut entries = Vec::with_capacity(array.rows);
488 for row in 0..array.rows {
489 entries.push(char_row_to_string_slice(&array.data, array.cols, row));
490 }
491 Ok(entries)
492 }
493 Value::Cell(cell) => {
494 let mut entries = Vec::with_capacity(cell.data.len());
495 for element in &cell.data {
496 entries.push(
497 cell_element_to_string(element)
498 .ok_or_else(|| runtime_error_for(CELL_ELEMENT_ERROR))?,
499 );
500 }
501 Ok(entries)
502 }
503 _ => Err(runtime_error_for(DELIMITER_TYPE_ERROR)),
504 }
505}
506
507fn cell_element_to_string(value: &Value) -> Option<String> {
508 match value {
509 Value::String(text) => Some(text.clone()),
510 Value::StringArray(array) if array.data.len() == 1 => Some(array.data[0].clone()),
511 Value::CharArray(array) if array.rows <= 1 => {
512 if array.rows == 0 {
513 Some(String::new())
514 } else {
515 Some(char_row_to_string_slice(&array.data, array.cols, 0))
516 }
517 }
518 _ => None,
519 }
520}
521
522fn value_to_scalar_string(value: &Value) -> Option<String> {
523 match value {
524 Value::String(text) => Some(text.clone()),
525 Value::StringArray(array) if array.data.len() == 1 => Some(array.data[0].clone()),
526 Value::CharArray(array) if array.rows <= 1 => {
527 if array.rows == 0 {
528 Some(String::new())
529 } else {
530 Some(char_row_to_string_slice(&array.data, array.cols, 0))
531 }
532 }
533 Value::Cell(cell) if cell.data.len() == 1 => cell_element_to_string(&cell.data[0]),
534 _ => None,
535 }
536}
537
538fn parse_bool(value: &Value, name: &str) -> BuiltinResult<bool> {
539 parse_bool_for_builtin(value, name, BUILTIN_NAME)
540}
541
542fn parse_bool_for_builtin(
543 value: &Value,
544 name: &str,
545 builtin_name: &'static str,
546) -> BuiltinResult<bool> {
547 match value {
548 Value::Bool(b) => Ok(*b),
549 Value::Int(i) => Ok(i.to_i64() != 0),
550 Value::Num(n) => Ok(*n != 0.0),
551 Value::LogicalArray(array) => {
552 if array.data.len() == 1 {
553 Ok(array.data[0] != 0)
554 } else {
555 Err(runtime_error_for_builtin(
556 builtin_name,
557 format!(
558 "{builtin_name}: value for '{}' must be logical true or false",
559 name
560 ),
561 ))
562 }
563 }
564 Value::Tensor(tensor) => {
565 if tensor.data.len() == 1 {
566 Ok(tensor.data[0] != 0.0)
567 } else {
568 Err(runtime_error_for_builtin(
569 builtin_name,
570 format!(
571 "{builtin_name}: value for '{}' must be logical true or false",
572 name
573 ),
574 ))
575 }
576 }
577 _ => {
578 if let Some(text) = value_to_scalar_string(value) {
579 let lowered = text.trim().to_ascii_lowercase();
580 match lowered.as_str() {
581 "true" | "on" | "yes" => Ok(true),
582 "false" | "off" | "no" => Ok(false),
583 _ => Err(runtime_error_for_builtin(
584 builtin_name,
585 format!(
586 "{builtin_name}: value for '{}' must be logical true or false",
587 name
588 ),
589 )),
590 }
591 } else {
592 Err(runtime_error_for_builtin(
593 builtin_name,
594 format!(
595 "{builtin_name}: value for '{}' must be logical true or false",
596 name
597 ),
598 ))
599 }
600 }
601 }
602}
603
604fn runtime_error_for_builtin(
605 builtin_name: &'static str,
606 message: impl Into<String>,
607) -> RuntimeError {
608 build_runtime_error(message)
609 .with_builtin(builtin_name)
610 .build()
611}
612
613fn runtime_error_for_strsplit(message: impl Into<String>) -> RuntimeError {
614 runtime_error_for_builtin(STRSPLIT_BUILTIN_NAME, message)
615}
616
617fn extract_strsplit_subject(value: Value) -> BuiltinResult<(StrsplitInputKind, String)> {
618 match value {
619 Value::String(text) => Ok((StrsplitInputKind::String, text)),
620 Value::StringArray(array) if array.data.len() == 1 => {
621 Ok((StrsplitInputKind::String, array.data[0].clone()))
622 }
623 Value::CharArray(array) if array.rows <= 1 => {
624 if array.rows == 0 {
625 Ok((StrsplitInputKind::Char, String::new()))
626 } else {
627 Ok((
628 StrsplitInputKind::Char,
629 char_row_to_string_slice(&array.data, array.cols, 0),
630 ))
631 }
632 }
633 _ => Err(runtime_error_for_strsplit(STRSPLIT_ARG_TYPE_ERROR)),
634 }
635}
636
637fn strsplit_text(
638 text: &str,
639 options: &StrsplitOptions,
640) -> BuiltinResult<(Vec<String>, Vec<String>)> {
641 let regex = compile_strsplit_regex(options)?;
642 let mut parts = Vec::new();
643 let mut matches = Vec::new();
644 let mut last = 0usize;
645
646 for found in regex.find_iter(text) {
647 parts.push(text[last..found.start()].to_string());
648 matches.push(found.as_str().to_string());
649 last = found.end();
650 }
651
652 parts.push(text[last..].to_string());
653 Ok((parts, matches))
654}
655
656fn compile_strsplit_regex(options: &StrsplitOptions) -> BuiltinResult<regex::Regex> {
657 let pattern = match (&options.delimiters, options.delimiter_type) {
658 (None, _) => {
659 if options.collapse_delimiters {
660 "[\\x20\\x0C\\n\\r\\t\\x0B]+".to_string()
661 } else {
662 "[\\x20\\x0C\\n\\r\\t\\x0B]".to_string()
663 }
664 }
665 (Some(delimiters), StrsplitDelimiterType::Simple) => {
666 let alternation = delimiters
667 .iter()
668 .map(|pattern| regex::escape(pattern))
669 .collect::<Vec<_>>()
670 .join("|");
671 if options.collapse_delimiters {
672 format!("(?:{alternation})+")
673 } else {
674 format!("(?:{alternation})")
675 }
676 }
677 (Some(delimiters), StrsplitDelimiterType::RegularExpression) => {
678 let alternation = delimiters.join("|");
679 if options.collapse_delimiters {
680 format!("(?:{alternation})+")
681 } else {
682 format!("(?:{alternation})")
683 }
684 }
685 };
686
687 RegexBuilder::new(&pattern)
688 .build()
689 .map_err(|err| runtime_error_for_strsplit(format!("strsplit: {err}")))
690}
691
692fn make_strsplit_output(tokens: Vec<String>, kind: StrsplitInputKind) -> BuiltinResult<Value> {
693 match kind {
694 StrsplitInputKind::String => {
695 let len = tokens.len();
696 let array = StringArray::new(tokens, vec![1, len])
697 .map_err(|err| runtime_error_for_strsplit(format!("strsplit: {err}")))?;
698 Ok(Value::StringArray(array))
699 }
700 StrsplitInputKind::Char => {
701 let values: Vec<Value> = tokens.into_iter().map(Value::String).collect();
702 let len = values.len();
703 make_cell(values, 1, len)
704 .map_err(|err| runtime_error_for_strsplit(format!("strsplit: {err}")))
705 }
706 }
707}
708
709#[derive(PartialEq, Eq)]
710enum NameKey {
711 CollapseDelimiters,
712 IncludeDelimiters,
713}
714
715#[derive(Clone, Copy)]
716enum StrsplitInputKind {
717 Char,
718 String,
719}
720
721#[derive(Clone, Copy)]
722enum StrsplitDelimiterType {
723 Simple,
724 RegularExpression,
725}
726
727#[derive(Clone)]
728struct StrsplitOptions {
729 delimiters: Option<Vec<String>>,
730 collapse_delimiters: bool,
731 delimiter_type: StrsplitDelimiterType,
732}
733
734impl StrsplitOptions {
735 fn parse(args: &[Value]) -> BuiltinResult<Self> {
736 let mut index = 0usize;
737 let mut delimiters = None;
738
739 if index < args.len() && !is_strsplit_name_key(&args[index]) {
740 let list = extract_delimiters(&args[index])
741 .map_err(|_| runtime_error_for_strsplit(STRSPLIT_DELIMITER_TYPE_ERROR))?;
742 delimiters = Some(list);
743 index += 1;
744 }
745
746 let mut collapse_delimiters = true;
747 let mut delimiter_type = StrsplitDelimiterType::Simple;
748
749 while index < args.len() {
750 let name = match strsplit_name_key(&args[index]) {
751 Some(name) => name,
752 None => return Err(runtime_error_for_strsplit(STRSPLIT_UNKNOWN_NAME_ERROR)),
753 };
754 index += 1;
755 if index >= args.len() {
756 return Err(runtime_error_for_strsplit(STRSPLIT_NAME_VALUE_PAIR_ERROR));
757 }
758 let value = &args[index];
759 index += 1;
760
761 match name {
762 StrsplitNameKey::CollapseDelimiters => {
763 collapse_delimiters =
764 parse_bool_for_builtin(value, "CollapseDelimiters", STRSPLIT_BUILTIN_NAME)?;
765 }
766 StrsplitNameKey::DelimiterType => {
767 let text = value_to_scalar_string(value)
768 .ok_or_else(|| runtime_error_for_strsplit(STRSPLIT_DELIMITER_MODE_ERROR))?;
769 delimiter_type = match text.trim().to_ascii_lowercase().as_str() {
770 "simple" => StrsplitDelimiterType::Simple,
771 "regularexpression" => StrsplitDelimiterType::RegularExpression,
772 _ => return Err(runtime_error_for_strsplit(STRSPLIT_DELIMITER_MODE_ERROR)),
773 };
774 }
775 }
776 }
777
778 if let Some(patterns) = &delimiters {
779 if patterns.is_empty() {
780 return Err(runtime_error_for_strsplit(STRSPLIT_EMPTY_DELIMITER_ERROR));
781 }
782 if matches!(delimiter_type, StrsplitDelimiterType::Simple)
783 && patterns.iter().any(|pattern| pattern.is_empty())
784 {
785 return Err(runtime_error_for_strsplit(STRSPLIT_EMPTY_DELIMITER_ERROR));
786 }
787 }
788
789 Ok(Self {
790 delimiters,
791 collapse_delimiters,
792 delimiter_type,
793 })
794 }
795}
796
797#[derive(PartialEq, Eq)]
798enum StrsplitNameKey {
799 CollapseDelimiters,
800 DelimiterType,
801}
802
803fn is_name_key(value: &Value) -> bool {
804 name_key(value).is_some()
805}
806
807fn is_strsplit_name_key(value: &Value) -> bool {
808 strsplit_name_key(value).is_some()
809}
810
811fn name_key(value: &Value) -> Option<NameKey> {
812 value_to_scalar_string(value).and_then(|text| {
813 let lowered = text.trim().to_ascii_lowercase();
814 match lowered.as_str() {
815 "collapsedelimiters" => Some(NameKey::CollapseDelimiters),
816 "includedelimiters" => Some(NameKey::IncludeDelimiters),
817 _ => None,
818 }
819 })
820}
821
822fn strsplit_name_key(value: &Value) -> Option<StrsplitNameKey> {
823 value_to_scalar_string(value).and_then(|text| {
824 let lowered = text.trim().to_ascii_lowercase();
825 match lowered.as_str() {
826 "collapsedelimiters" => Some(StrsplitNameKey::CollapseDelimiters),
827 "delimitertype" => Some(StrsplitNameKey::DelimiterType),
828 _ => None,
829 }
830 })
831}
832
833#[cfg(test)]
834pub(crate) mod tests {
835 use super::*;
836 use runmat_builtins::{CellArray, LogicalArray, ResolveContext, Tensor, Type};
837
838 fn split_builtin(text: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
839 futures::executor::block_on(super::split_builtin(text, rest))
840 }
841
842 fn strsplit_builtin(text: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
843 futures::executor::block_on(super::strsplit_builtin(text, rest))
844 }
845
846 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
847 #[test]
848 fn split_string_whitespace_default() {
849 let input = Value::String("RunMat Accelerate Planner".to_string());
850 let result = split_builtin(input, Vec::new()).expect("split");
851 match result {
852 Value::StringArray(array) => {
853 assert_eq!(array.shape, vec![1, 3]);
854 assert_eq!(
855 array.data,
856 vec![
857 "RunMat".to_string(),
858 "Accelerate".to_string(),
859 "Planner".to_string()
860 ]
861 );
862 }
863 other => panic!("expected string array, got {other:?}"),
864 }
865 }
866
867 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
868 #[test]
869 fn split_string_custom_delimiter() {
870 let input = Value::String("alpha,beta,gamma".to_string());
871 let args = vec![Value::String(",".to_string())];
872 let result = split_builtin(input, args).expect("split");
873 match result {
874 Value::StringArray(array) => {
875 assert_eq!(array.shape, vec![1, 3]);
876 assert_eq!(
877 array.data,
878 vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()]
879 );
880 }
881 other => panic!("expected string array, got {other:?}"),
882 }
883 }
884
885 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
886 #[test]
887 fn split_include_delimiters_true() {
888 let input = Value::String("A+B-C".to_string());
889 let args = vec![
890 Value::StringArray(
891 StringArray::new(vec!["+".to_string(), "-".to_string()], vec![1, 2]).unwrap(),
892 ),
893 Value::String("IncludeDelimiters".to_string()),
894 Value::Bool(true),
895 ];
896 let result = split_builtin(input, args).expect("split");
897 match result {
898 Value::StringArray(array) => {
899 assert_eq!(array.shape, vec![1, 5]);
900 assert_eq!(
901 array.data,
902 vec![
903 "A".to_string(),
904 "+".to_string(),
905 "B".to_string(),
906 "-".to_string(),
907 "C".to_string()
908 ]
909 );
910 }
911 other => panic!("expected string array, got {other:?}"),
912 }
913 }
914
915 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
916 #[test]
917 fn split_include_delimiters_whitespace_collapse_default() {
918 let input = Value::String("A B".to_string());
919 let args = vec![
920 Value::String("IncludeDelimiters".to_string()),
921 Value::Bool(true),
922 ];
923 let result = split_builtin(input, args).expect("split");
924 match result {
925 Value::StringArray(array) => {
926 assert_eq!(array.shape, vec![1, 3]);
927 assert_eq!(
928 array.data,
929 vec!["A".to_string(), " ".to_string(), "B".to_string()]
930 );
931 }
932 other => panic!("expected string array, got {other:?}"),
933 }
934 }
935
936 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
937 #[test]
938 fn split_patterns_include_delimiters_collapse_true() {
939 let input = Value::String("a,,b".to_string());
940 let args = vec![
941 Value::String(",".to_string()),
942 Value::String("IncludeDelimiters".to_string()),
943 Value::Bool(true),
944 Value::String("CollapseDelimiters".to_string()),
945 Value::Bool(true),
946 ];
947 let result = split_builtin(input, args).expect("split");
948 match result {
949 Value::StringArray(array) => {
950 assert_eq!(array.shape, vec![1, 3]);
951 assert_eq!(
952 array.data,
953 vec!["a".to_string(), ",,".to_string(), "b".to_string()]
954 );
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 split_collapse_false_preserves_empty_segments() {
963 let input = Value::String("one,,three,".to_string());
964 let args = vec![
965 Value::String(",".to_string()),
966 Value::String("CollapseDelimiters".to_string()),
967 Value::Bool(false),
968 ];
969 let result = split_builtin(input, args).expect("split");
970 match result {
971 Value::StringArray(array) => {
972 assert_eq!(array.shape, vec![1, 4]);
973 assert_eq!(
974 array.data,
975 vec![
976 "one".to_string(),
977 "".to_string(),
978 "three".to_string(),
979 "".to_string()
980 ]
981 );
982 }
983 other => panic!("expected string array, got {other:?}"),
984 }
985 }
986
987 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
988 #[test]
989 fn split_character_array_rows() {
990 let mut row1: Vec<char> = "GPU Accelerate".chars().collect();
991 let mut row2: Vec<char> = "VM Engine".chars().collect();
992 let width = row1.len().max(row2.len());
993 row1.resize(width, ' ');
994 row2.resize(width, ' ');
995 let mut data = row1;
996 data.extend(row2);
997 let char_array = CharArray::new(data, 2, width).unwrap();
998 let input = Value::CharArray(char_array);
999 let result = split_builtin(input, Vec::new()).expect("split");
1000 match result {
1001 Value::StringArray(array) => {
1002 assert_eq!(array.shape, vec![2, 2]);
1003 assert_eq!(
1004 array.data,
1005 vec![
1006 "GPU".to_string(),
1007 "VM".to_string(),
1008 "Accelerate".to_string(),
1009 "Engine".to_string()
1010 ]
1011 );
1012 }
1013 other => panic!("expected string array, got {other:?}"),
1014 }
1015 }
1016
1017 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1018 #[test]
1019 fn split_string_array_multiple_columns() {
1020 let data = vec![
1021 "RunMat Core".to_string(),
1022 "VM Interpreter".to_string(),
1023 "Accelerate Engine".to_string(),
1024 "<missing>".to_string(),
1025 ];
1026 let array = StringArray::new(data, vec![2, 2]).unwrap();
1027 let input = Value::StringArray(array);
1028 let result = split_builtin(input, Vec::new()).expect("split");
1029 match result {
1030 Value::StringArray(array) => {
1031 assert_eq!(array.shape, vec![2, 4]);
1032 assert_eq!(
1033 array.data,
1034 vec![
1035 "RunMat".to_string(),
1036 "VM".to_string(),
1037 "Core".to_string(),
1038 "Interpreter".to_string(),
1039 "Accelerate".to_string(),
1040 "<missing>".to_string(),
1041 "Engine".to_string(),
1042 "<missing>".to_string()
1043 ]
1044 );
1045 }
1046 other => panic!("expected string array, got {other:?}"),
1047 }
1048 }
1049
1050 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1051 #[test]
1052 fn split_cell_array_outputs_string_array() {
1053 let values = vec![
1054 Value::String("RunMat Snapshot".to_string()),
1055 Value::String("Fusion Planner".to_string()),
1056 ];
1057 let cell = crate::make_cell(values, 2, 1).expect("cell");
1058 let result = split_builtin(cell, vec![Value::String(" ".to_string())]).expect("split");
1059 match result {
1060 Value::StringArray(array) => {
1061 assert_eq!(array.shape, vec![2, 2]);
1062 assert_eq!(
1063 array.data,
1064 vec![
1065 "RunMat".to_string(),
1066 "Fusion".to_string(),
1067 "Snapshot".to_string(),
1068 "Planner".to_string()
1069 ]
1070 );
1071 }
1072 other => panic!("expected string array, got {other:?}"),
1073 }
1074 }
1075
1076 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1077 #[test]
1078 fn split_cell_array_multiple_columns() {
1079 let values = vec![
1080 Value::String("alpha beta".to_string()),
1081 Value::String("gamma".to_string()),
1082 Value::String("delta epsilon".to_string()),
1083 Value::String("<missing>".to_string()),
1084 ];
1085 let cell = crate::make_cell(values, 2, 2).expect("cell");
1086 let result = split_builtin(cell, Vec::new()).expect("split");
1087 match result {
1088 Value::StringArray(array) => {
1089 assert_eq!(array.shape, vec![2, 4]);
1090 assert_eq!(
1091 array.data,
1092 vec![
1093 "alpha".to_string(),
1094 "delta".to_string(),
1095 "beta".to_string(),
1096 "epsilon".to_string(),
1097 "gamma".to_string(),
1098 "<missing>".to_string(),
1099 "<missing>".to_string(),
1100 "<missing>".to_string()
1101 ]
1102 );
1103 }
1104 other => panic!("expected string array, got {other:?}"),
1105 }
1106 }
1107
1108 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1109 #[test]
1110 fn split_missing_string_propagates() {
1111 let input = Value::String("<missing>".to_string());
1112 let result = split_builtin(input, Vec::new()).expect("split");
1113 match result {
1114 Value::StringArray(array) => {
1115 assert_eq!(array.shape, vec![1, 1]);
1116 assert_eq!(array.data, vec!["<missing>".to_string()]);
1117 }
1118 other => panic!("expected string array, got {other:?}"),
1119 }
1120 }
1121
1122 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1123 #[test]
1124 fn split_invalid_name_value_pair_errors() {
1125 let input = Value::String("abc".to_string());
1126 let args = vec![Value::String("CollapseDelimiters".to_string())];
1127 let err = split_builtin(input, args).unwrap_err();
1128 assert!(err.to_string().contains("name-value"));
1129 }
1130
1131 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1132 #[test]
1133 fn split_invalid_text_argument_errors() {
1134 let err = split_builtin(Value::Num(1.0), Vec::new()).unwrap_err();
1135 assert!(err.to_string().contains("first argument"));
1136 }
1137
1138 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1139 #[test]
1140 fn split_invalid_delimiter_type_errors() {
1141 let err =
1142 split_builtin(Value::String("abc".to_string()), vec![Value::Num(1.0)]).unwrap_err();
1143 assert!(err.to_string().contains("delimiter input"));
1144 }
1145
1146 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1147 #[test]
1148 fn split_empty_delimiter_errors() {
1149 let err = split_builtin(
1150 Value::String("abc".to_string()),
1151 vec![Value::String(String::new())],
1152 )
1153 .unwrap_err();
1154 assert!(err.to_string().contains("at least one character"));
1155 }
1156
1157 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1158 #[test]
1159 fn split_unknown_name_argument_errors() {
1160 let err = split_builtin(
1161 Value::String("abc".to_string()),
1162 vec![
1163 Value::String("UnknownOption".to_string()),
1164 Value::Bool(true),
1165 ],
1166 )
1167 .unwrap_err();
1168 assert!(err.to_string().contains("unrecognized"));
1169 }
1170
1171 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1172 #[test]
1173 fn split_collapse_delimiters_accepts_logical_array() {
1174 let logical = LogicalArray::new(vec![1u8], vec![1]).unwrap();
1175 let args = vec![
1176 Value::String(",".to_string()),
1177 Value::String("CollapseDelimiters".to_string()),
1178 Value::LogicalArray(logical),
1179 ];
1180 let result = split_builtin(Value::String("a,,b".to_string()), args).expect("split");
1181 match result {
1182 Value::StringArray(array) => {
1183 assert_eq!(array.shape, vec![1, 2]);
1184 assert_eq!(array.data, vec!["a".to_string(), "b".to_string()]);
1185 }
1186 other => panic!("expected string array, got {other:?}"),
1187 }
1188 }
1189
1190 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1191 #[test]
1192 fn split_include_delimiters_accepts_tensor_scalar() {
1193 let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1194 let args = vec![
1195 Value::String(",".to_string()),
1196 Value::String("IncludeDelimiters".to_string()),
1197 Value::Tensor(tensor),
1198 ];
1199 let result = split_builtin(Value::String("a,b".to_string()), args).expect("split");
1200 match result {
1201 Value::StringArray(array) => {
1202 assert_eq!(array.shape, vec![1, 3]);
1203 assert_eq!(
1204 array.data,
1205 vec!["a".to_string(), ",".to_string(), "b".to_string()]
1206 );
1207 }
1208 other => panic!("expected string array, got {other:?}"),
1209 }
1210 }
1211
1212 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1213 #[test]
1214 fn split_cell_array_mixed_inputs() {
1215 let handles: Vec<_> = vec![
1216 runmat_gc::gc_allocate(Value::String("alpha beta".to_string())).unwrap(),
1217 runmat_gc::gc_allocate(Value::CharArray(
1218 CharArray::new("gamma".chars().collect(), 1, 5).unwrap(),
1219 ))
1220 .unwrap(),
1221 ];
1222 let cell =
1223 Value::Cell(CellArray::new_handles(handles, 1, 2).expect("cell array construction"));
1224 let result = split_builtin(cell, Vec::new()).expect("split");
1225 match result {
1226 Value::StringArray(array) => {
1227 assert_eq!(array.shape, vec![1, 4]);
1228 assert_eq!(
1229 array.data,
1230 vec![
1231 "alpha".to_string(),
1232 "beta".to_string(),
1233 "gamma".to_string(),
1234 "<missing>".to_string()
1235 ]
1236 );
1237 }
1238 other => panic!("expected string array, got {other:?}"),
1239 }
1240 }
1241
1242 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1243 #[test]
1244 fn strsplit_string_scalar_returns_string_array() {
1245 let result =
1246 strsplit_builtin(Value::String("one two three".into()), Vec::new()).expect("strsplit");
1247 match result {
1248 Value::StringArray(array) => {
1249 assert_eq!(array.shape, vec![1, 3]);
1250 assert_eq!(
1251 array.data,
1252 vec!["one".to_string(), "two".to_string(), "three".to_string()]
1253 );
1254 }
1255 other => panic!("expected string array, got {other:?}"),
1256 }
1257 }
1258
1259 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1260 #[test]
1261 fn strsplit_char_vector_returns_cell() {
1262 let input = Value::CharArray(CharArray::new("a,b".chars().collect(), 1, 3).unwrap());
1263 let result = strsplit_builtin(input, vec![Value::String(",".into())]).expect("strsplit");
1264 match result {
1265 Value::Cell(cell) => {
1266 assert_eq!(cell.rows, 1);
1267 assert_eq!(cell.cols, 2);
1268 assert_eq!(
1269 unsafe { &*cell.data[0].as_raw() },
1270 &Value::String("a".into())
1271 );
1272 assert_eq!(
1273 unsafe { &*cell.data[1].as_raw() },
1274 &Value::String("b".into())
1275 );
1276 }
1277 other => panic!("expected cell output, got {other:?}"),
1278 }
1279 }
1280
1281 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1282 #[test]
1283 fn strsplit_multi_output_returns_matches() {
1284 let _guard = crate::output_count::push_output_count(Some(2));
1285 let result = strsplit_builtin(
1286 Value::String("a,,b,".into()),
1287 vec![Value::String(",".into())],
1288 )
1289 .expect("strsplit");
1290 match result {
1291 Value::OutputList(values) => {
1292 assert_eq!(values.len(), 2);
1293 match &values[0] {
1294 Value::StringArray(array) => {
1295 assert_eq!(
1296 array.data,
1297 vec!["a".to_string(), "b".to_string(), "".to_string()]
1298 );
1299 }
1300 other => panic!("expected first output string array, got {other:?}"),
1301 }
1302 match &values[1] {
1303 Value::StringArray(array) => {
1304 assert_eq!(array.data, vec![",,".to_string(), ",".to_string()]);
1305 }
1306 other => panic!("expected second output string array, got {other:?}"),
1307 }
1308 }
1309 other => panic!("expected output list, got {other:?}"),
1310 }
1311 }
1312
1313 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1314 #[test]
1315 fn strsplit_regular_expression_mode() {
1316 let _guard = crate::output_count::push_output_count(Some(2));
1317 let result = strsplit_builtin(
1318 Value::String("1.21m/s 1.985 m/s".into()),
1319 vec![
1320 Value::String("\\s*m/s\\s*".into()),
1321 Value::String("DelimiterType".into()),
1322 Value::String("RegularExpression".into()),
1323 ],
1324 )
1325 .expect("strsplit");
1326 match result {
1327 Value::OutputList(values) => {
1328 match &values[0] {
1329 Value::StringArray(array) => {
1330 assert_eq!(
1331 array.data,
1332 vec!["1.21".to_string(), "1.985".to_string(), "".to_string()]
1333 );
1334 }
1335 other => panic!("expected split output string array, got {other:?}"),
1336 }
1337 match &values[1] {
1338 Value::StringArray(array) => {
1339 assert_eq!(array.data, vec!["m/s ".to_string(), " m/s".to_string()]);
1340 }
1341 other => panic!("expected matches output string array, got {other:?}"),
1342 }
1343 }
1344 other => panic!("expected output list, got {other:?}"),
1345 }
1346 }
1347
1348 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1349 #[test]
1350 fn strsplit_collapse_false_preserves_empty_segments() {
1351 let result = strsplit_builtin(
1352 Value::String("a,,b".into()),
1353 vec![
1354 Value::String(",".into()),
1355 Value::String("CollapseDelimiters".into()),
1356 Value::Bool(false),
1357 ],
1358 )
1359 .expect("strsplit");
1360 match result {
1361 Value::StringArray(array) => {
1362 assert_eq!(
1363 array.data,
1364 vec!["a".to_string(), "".to_string(), "b".to_string()]
1365 );
1366 }
1367 other => panic!("expected string array, got {other:?}"),
1368 }
1369 }
1370
1371 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1372 #[test]
1373 fn strsplit_rejects_nonscalar_text_inputs() {
1374 let input = Value::StringArray(
1375 StringArray::new(vec!["a b".into(), "c d".into()], vec![2, 1]).unwrap(),
1376 );
1377 let err = strsplit_builtin(input, Vec::new()).unwrap_err();
1378 assert!(err.to_string().contains("first argument"));
1379 }
1380
1381 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1382 #[test]
1383 fn strsplit_invalid_delimiter_type_option_errors() {
1384 let err = strsplit_builtin(
1385 Value::String("a,b".into()),
1386 vec![
1387 Value::String(",".into()),
1388 Value::String("DelimiterType".into()),
1389 Value::String("BadMode".into()),
1390 ],
1391 )
1392 .unwrap_err();
1393 assert!(err.to_string().contains("DelimiterType"));
1394 }
1395
1396 #[test]
1397 fn split_type_is_string_array() {
1398 assert_eq!(
1399 string_array_type(&[Type::String], &ResolveContext::new(Vec::new())),
1400 Type::cell_of(Type::String)
1401 );
1402 }
1403}