Skip to main content

runmat_runtime/builtins/strings/transform/
split.rs

1//! MATLAB-compatible `split` and `strsplit` builtins with GPU-aware semantics for RunMat.
2
3use 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}