1use bock_interp::{BockString, BuiltinRegistry, RuntimeError, TypeTag, Value};
7use regex::Regex;
8
9pub fn register(registry: &mut BuiltinRegistry) {
11 registry.register(TypeTag::String, "add", string_add);
13
14 registry.register(TypeTag::String, "compare", string_compare);
16
17 registry.register(TypeTag::String, "hash_code", string_hash_code);
19
20 registry.register(TypeTag::String, "display", string_display);
22
23 registry.register(TypeTag::String, "contains", string_contains);
25 registry.register(TypeTag::String, "starts_with", string_starts_with);
26 registry.register(TypeTag::String, "ends_with", string_ends_with);
27 registry.register(TypeTag::String, "to_upper", string_to_upper);
28 registry.register(TypeTag::String, "to_lower", string_to_lower);
29 registry.register(TypeTag::String, "trim", string_trim);
30 registry.register(TypeTag::String, "split", string_split);
31 registry.register(TypeTag::String, "char_at", string_char_at);
32 registry.register(TypeTag::String, "substring", string_substring);
33 registry.register(TypeTag::String, "slice", string_substring);
34 registry.register(TypeTag::String, "replace", string_replace);
35 registry.register(TypeTag::String, "is_empty", string_is_empty);
36 registry.register(TypeTag::String, "len", string_len);
37 registry.register(TypeTag::String, "byte_len", string_byte_len);
38 registry.register(TypeTag::String, "chars", string_chars);
39 registry.register(TypeTag::String, "repeat", string_repeat);
40 registry.register(TypeTag::String, "index_of", string_index_of);
41 registry.register(TypeTag::String, "trim_start", string_trim_start);
42 registry.register(TypeTag::String, "trim_end", string_trim_end);
43 registry.register(TypeTag::String, "pad_start", string_pad_start);
44 registry.register(TypeTag::String, "pad_end", string_pad_end);
45 registry.register(TypeTag::String, "reverse", string_reverse);
46 registry.register(TypeTag::String, "bytes", string_bytes);
47 registry.register(TypeTag::String, "join", string_join);
48 registry.register(TypeTag::String, "format", string_format);
49
50 registry.register(TypeTag::String, "regex_match", string_regex_match);
52 registry.register(TypeTag::String, "regex_find", string_regex_find);
53 registry.register(TypeTag::String, "regex_replace", string_regex_replace);
54}
55
56fn expect_str<'a>(args: &'a [Value], pos: usize, method: &str) -> Result<&'a str, RuntimeError> {
59 match args.get(pos) {
60 Some(Value::String(s)) => Ok(s.as_str()),
61 Some(other) => Err(RuntimeError::TypeError(format!(
62 "String.{method} expects String, got {other}"
63 ))),
64 None => Err(RuntimeError::ArityMismatch {
65 expected: pos + 1,
66 got: args.len(),
67 }),
68 }
69}
70
71fn expect_int(args: &[Value], pos: usize, method: &str) -> Result<i64, RuntimeError> {
72 match args.get(pos) {
73 Some(Value::Int(v)) => Ok(*v),
74 Some(other) => Err(RuntimeError::TypeError(format!(
75 "String.{method} expects Int, got {other}"
76 ))),
77 None => Err(RuntimeError::ArityMismatch {
78 expected: pos + 1,
79 got: args.len(),
80 }),
81 }
82}
83
84fn string_add(args: &[Value]) -> Result<Value, RuntimeError> {
87 let a = expect_str(args, 0, "add")?;
88 let b = expect_str(args, 1, "add")?;
89 Ok(Value::String(BockString::new(format!("{a}{b}"))))
90}
91
92fn string_compare(args: &[Value]) -> Result<Value, RuntimeError> {
95 let a = expect_str(args, 0, "compare")?;
96 let b = expect_str(args, 1, "compare")?;
97 Ok(Value::ordering(a.cmp(b)))
98}
99
100fn string_hash_code(args: &[Value]) -> Result<Value, RuntimeError> {
103 use std::hash::{Hash, Hasher};
104 let a = expect_str(args, 0, "hash_code")?;
105 let mut hasher = std::collections::hash_map::DefaultHasher::new();
106 a.hash(&mut hasher);
107 Ok(Value::Int(hasher.finish() as i64))
108}
109
110fn string_display(args: &[Value]) -> Result<Value, RuntimeError> {
114 let a = expect_str(args, 0, "display")?;
115 Ok(Value::String(BockString::new(a)))
116}
117
118fn string_contains(args: &[Value]) -> Result<Value, RuntimeError> {
121 let haystack = expect_str(args, 0, "contains")?;
122 let needle = expect_str(args, 1, "contains")?;
123 Ok(Value::Bool(haystack.contains(needle)))
124}
125
126fn string_starts_with(args: &[Value]) -> Result<Value, RuntimeError> {
127 let s = expect_str(args, 0, "starts_with")?;
128 let prefix = expect_str(args, 1, "starts_with")?;
129 Ok(Value::Bool(s.starts_with(prefix)))
130}
131
132fn string_ends_with(args: &[Value]) -> Result<Value, RuntimeError> {
133 let s = expect_str(args, 0, "ends_with")?;
134 let suffix = expect_str(args, 1, "ends_with")?;
135 Ok(Value::Bool(s.ends_with(suffix)))
136}
137
138fn string_to_upper(args: &[Value]) -> Result<Value, RuntimeError> {
139 let s = expect_str(args, 0, "to_upper")?;
140 Ok(Value::String(BockString::new(s.to_uppercase())))
141}
142
143fn string_to_lower(args: &[Value]) -> Result<Value, RuntimeError> {
144 let s = expect_str(args, 0, "to_lower")?;
145 Ok(Value::String(BockString::new(s.to_lowercase())))
146}
147
148fn string_trim(args: &[Value]) -> Result<Value, RuntimeError> {
149 let s = expect_str(args, 0, "trim")?;
150 Ok(Value::String(BockString::new(s.trim())))
151}
152
153fn string_split(args: &[Value]) -> Result<Value, RuntimeError> {
154 let s = expect_str(args, 0, "split")?;
155 let sep = expect_str(args, 1, "split")?;
156 let parts: Vec<Value> = s
157 .split(sep)
158 .map(|p| Value::String(BockString::new(p)))
159 .collect();
160 Ok(Value::List(parts))
161}
162
163fn string_char_at(args: &[Value]) -> Result<Value, RuntimeError> {
164 let s = expect_str(args, 0, "char_at")?;
165 let idx = expect_int(args, 1, "char_at")?;
166 if idx < 0 {
167 return Ok(Value::Optional(None));
168 }
169 match s.chars().nth(idx as usize) {
170 Some(c) => Ok(Value::Optional(Some(Box::new(Value::Char(c))))),
171 None => Ok(Value::Optional(None)),
172 }
173}
174
175fn string_substring(args: &[Value]) -> Result<Value, RuntimeError> {
176 let s = expect_str(args, 0, "substring")?;
177 let start = expect_int(args, 1, "substring")?;
178 let end = expect_int(args, 2, "substring")?;
179 let chars: Vec<char> = s.chars().collect();
180 let len = chars.len() as i64;
181 let start = start.max(0) as usize;
182 let end = end.clamp(0, len) as usize;
183 if start >= end {
184 return Ok(Value::String(BockString::new("")));
185 }
186 let result: String = chars[start..end].iter().collect();
187 Ok(Value::String(BockString::new(result)))
188}
189
190fn string_replace(args: &[Value]) -> Result<Value, RuntimeError> {
191 let s = expect_str(args, 0, "replace")?;
192 let from = expect_str(args, 1, "replace")?;
193 let to = expect_str(args, 2, "replace")?;
194 Ok(Value::String(BockString::new(s.replace(from, to))))
195}
196
197fn string_is_empty(args: &[Value]) -> Result<Value, RuntimeError> {
198 let s = expect_str(args, 0, "is_empty")?;
199 Ok(Value::Bool(s.is_empty()))
200}
201
202fn string_len(args: &[Value]) -> Result<Value, RuntimeError> {
204 let s = expect_str(args, 0, "len")?;
205 Ok(Value::Int(s.chars().count() as i64))
206}
207
208fn string_byte_len(args: &[Value]) -> Result<Value, RuntimeError> {
209 let s = expect_str(args, 0, "byte_len")?;
210 Ok(Value::Int(s.len() as i64))
211}
212
213fn string_chars(args: &[Value]) -> Result<Value, RuntimeError> {
214 let s = expect_str(args, 0, "chars")?;
215 let chars: Vec<Value> = s.chars().map(Value::Char).collect();
216 Ok(Value::List(chars))
217}
218
219fn string_repeat(args: &[Value]) -> Result<Value, RuntimeError> {
220 let s = expect_str(args, 0, "repeat")?;
221 let n = expect_int(args, 1, "repeat")?;
222 if n < 0 {
223 return Err(RuntimeError::TypeError(
224 "String.repeat count must be non-negative".to_string(),
225 ));
226 }
227 Ok(Value::String(BockString::new(s.repeat(n as usize))))
228}
229
230fn string_index_of(args: &[Value]) -> Result<Value, RuntimeError> {
231 let haystack = expect_str(args, 0, "index_of")?;
232 let needle = expect_str(args, 1, "index_of")?;
233 match haystack.find(needle) {
235 Some(byte_idx) => {
236 let char_idx = haystack[..byte_idx].chars().count() as i64;
237 Ok(Value::Optional(Some(Box::new(Value::Int(char_idx)))))
238 }
239 None => Ok(Value::Optional(None)),
240 }
241}
242
243fn string_trim_start(args: &[Value]) -> Result<Value, RuntimeError> {
244 let s = expect_str(args, 0, "trim_start")?;
245 Ok(Value::String(BockString::new(s.trim_start())))
246}
247
248fn string_trim_end(args: &[Value]) -> Result<Value, RuntimeError> {
249 let s = expect_str(args, 0, "trim_end")?;
250 Ok(Value::String(BockString::new(s.trim_end())))
251}
252
253fn string_pad_start(args: &[Value]) -> Result<Value, RuntimeError> {
255 let s = expect_str(args, 0, "pad_start")?;
256 let target_len = expect_int(args, 1, "pad_start")? as usize;
257 let pad_char = expect_str(args, 2, "pad_start")?;
258 let char_len = s.chars().count();
259 if char_len >= target_len || pad_char.is_empty() {
260 return Ok(Value::String(BockString::new(s)));
261 }
262 let pad_chars: Vec<char> = pad_char.chars().collect();
263 let needed = target_len - char_len;
264 let mut prefix = String::with_capacity(needed);
265 for i in 0..needed {
266 prefix.push(pad_chars[i % pad_chars.len()]);
267 }
268 prefix.push_str(s);
269 Ok(Value::String(BockString::new(prefix)))
270}
271
272fn string_pad_end(args: &[Value]) -> Result<Value, RuntimeError> {
274 let s = expect_str(args, 0, "pad_end")?;
275 let target_len = expect_int(args, 1, "pad_end")? as usize;
276 let pad_char = expect_str(args, 2, "pad_end")?;
277 let char_len = s.chars().count();
278 if char_len >= target_len || pad_char.is_empty() {
279 return Ok(Value::String(BockString::new(s)));
280 }
281 let pad_chars: Vec<char> = pad_char.chars().collect();
282 let needed = target_len - char_len;
283 let mut result = String::from(s);
284 for i in 0..needed {
285 result.push(pad_chars[i % pad_chars.len()]);
286 }
287 Ok(Value::String(BockString::new(result)))
288}
289
290fn string_reverse(args: &[Value]) -> Result<Value, RuntimeError> {
291 let s = expect_str(args, 0, "reverse")?;
292 let reversed: String = s.chars().rev().collect();
293 Ok(Value::String(BockString::new(reversed)))
294}
295
296fn string_bytes(args: &[Value]) -> Result<Value, RuntimeError> {
298 let s = expect_str(args, 0, "bytes")?;
299 let bytes: Vec<Value> = s.bytes().map(|b| Value::Int(b as i64)).collect();
300 Ok(Value::List(bytes))
301}
302
303fn string_join(args: &[Value]) -> Result<Value, RuntimeError> {
305 let sep = expect_str(args, 0, "join")?;
306 let list = match args.get(1) {
307 Some(Value::List(items)) => items,
308 Some(other) => {
309 return Err(RuntimeError::TypeError(format!(
310 "String.join expects List, got {other}"
311 )))
312 }
313 None => {
314 return Err(RuntimeError::ArityMismatch {
315 expected: 2,
316 got: args.len(),
317 })
318 }
319 };
320 let parts: Result<Vec<&str>, RuntimeError> = list
321 .iter()
322 .map(|v| match v {
323 Value::String(s) => Ok(s.as_str()),
324 other => Err(RuntimeError::TypeError(format!(
325 "String.join list elements must be Strings, got {other}"
326 ))),
327 })
328 .collect();
329 Ok(Value::String(BockString::new(parts?.join(sep))))
330}
331
332fn string_format(args: &[Value]) -> Result<Value, RuntimeError> {
334 let template = expect_str(args, 0, "format")?;
335 let format_args = &args[1..];
336 let mut result = String::with_capacity(template.len());
337 let mut arg_idx = 0;
338 let mut chars = template.chars().peekable();
339 while let Some(c) = chars.next() {
340 if c == '{' {
341 if chars.peek() == Some(&'}') {
342 chars.next();
343 if arg_idx < format_args.len() {
344 result.push_str(&format_args[arg_idx].to_string());
345 arg_idx += 1;
346 } else {
347 result.push_str("{}");
348 }
349 } else {
350 result.push(c);
351 }
352 } else {
353 result.push(c);
354 }
355 }
356 Ok(Value::String(BockString::new(result)))
357}
358
359fn compile_regex(pattern: &str) -> Result<Regex, RuntimeError> {
362 Regex::new(pattern).map_err(|e| RuntimeError::TypeError(format!("invalid regex pattern: {e}")))
363}
364
365fn string_regex_match(args: &[Value]) -> Result<Value, RuntimeError> {
367 let s = expect_str(args, 0, "regex_match")?;
368 let pattern = expect_str(args, 1, "regex_match")?;
369 let re = compile_regex(pattern)?;
370 Ok(Value::Bool(re.is_match(s)))
371}
372
373fn string_regex_find(args: &[Value]) -> Result<Value, RuntimeError> {
376 let s = expect_str(args, 0, "regex_find")?;
377 let pattern = expect_str(args, 1, "regex_find")?;
378 let re = compile_regex(pattern)?;
379 let matches: Vec<Value> = re
380 .find_iter(s)
381 .map(|m| Value::String(BockString::new(m.as_str())))
382 .collect();
383 Ok(Value::List(matches))
384}
385
386fn string_regex_replace(args: &[Value]) -> Result<Value, RuntimeError> {
388 let s = expect_str(args, 0, "regex_replace")?;
389 let pattern = expect_str(args, 1, "regex_replace")?;
390 let replacement = expect_str(args, 2, "regex_replace")?;
391 let re = compile_regex(pattern)?;
392 let result = re.replace_all(s, replacement);
393 Ok(Value::String(BockString::new(result.into_owned())))
394}
395
396#[cfg(test)]
399mod tests {
400 use super::*;
401
402 fn reg() -> BuiltinRegistry {
403 let mut r = BuiltinRegistry::new();
404 register(&mut r);
405 r
406 }
407
408 fn s(v: &str) -> Value {
409 Value::String(BockString::new(v))
410 }
411
412 #[test]
413 fn add_concat() {
414 let r = reg();
415 let result = r.call(TypeTag::String, "add", &[s("hello"), s(" world")]);
416 assert_eq!(result.unwrap().unwrap(), s("hello world"));
417 }
418
419 #[test]
420 fn compare_less() {
421 let r = reg();
422 let result = r.call(TypeTag::String, "compare", &[s("a"), s("b")]);
423 assert_eq!(
424 result.unwrap().unwrap(),
425 Value::ordering(std::cmp::Ordering::Less)
426 );
427 }
428
429 #[test]
430 fn display_identity() {
431 let r = reg();
432 let result = r.call(TypeTag::String, "display", &[s("test")]);
433 assert_eq!(result.unwrap().unwrap(), s("test"));
434 }
435
436 #[test]
437 fn contains_true() {
438 let r = reg();
439 let result = r.call(TypeTag::String, "contains", &[s("hello world"), s("world")]);
440 assert_eq!(result.unwrap().unwrap(), Value::Bool(true));
441 }
442
443 #[test]
444 fn contains_false() {
445 let r = reg();
446 let result = r.call(TypeTag::String, "contains", &[s("hello"), s("xyz")]);
447 assert_eq!(result.unwrap().unwrap(), Value::Bool(false));
448 }
449
450 #[test]
451 fn starts_with_ok() {
452 let r = reg();
453 let result = r.call(TypeTag::String, "starts_with", &[s("hello"), s("hel")]);
454 assert_eq!(result.unwrap().unwrap(), Value::Bool(true));
455 }
456
457 #[test]
458 fn ends_with_ok() {
459 let r = reg();
460 let result = r.call(TypeTag::String, "ends_with", &[s("hello"), s("llo")]);
461 assert_eq!(result.unwrap().unwrap(), Value::Bool(true));
462 }
463
464 #[test]
465 fn to_upper_ok() {
466 let r = reg();
467 let result = r.call(TypeTag::String, "to_upper", &[s("hello")]);
468 assert_eq!(result.unwrap().unwrap(), s("HELLO"));
469 }
470
471 #[test]
472 fn to_lower_ok() {
473 let r = reg();
474 let result = r.call(TypeTag::String, "to_lower", &[s("HELLO")]);
475 assert_eq!(result.unwrap().unwrap(), s("hello"));
476 }
477
478 #[test]
479 fn trim_ok() {
480 let r = reg();
481 let result = r.call(TypeTag::String, "trim", &[s(" hello ")]);
482 assert_eq!(result.unwrap().unwrap(), s("hello"));
483 }
484
485 #[test]
486 fn split_ok() {
487 let r = reg();
488 let result = r.call(TypeTag::String, "split", &[s("a,b,c"), s(",")]);
489 assert_eq!(
490 result.unwrap().unwrap(),
491 Value::List(vec![s("a"), s("b"), s("c")])
492 );
493 }
494
495 #[test]
496 fn char_at_ok() {
497 let r = reg();
498 let result = r.call(TypeTag::String, "char_at", &[s("hello"), Value::Int(1)]);
499 assert_eq!(
500 result.unwrap().unwrap(),
501 Value::Optional(Some(Box::new(Value::Char('e'))))
502 );
503 }
504
505 #[test]
506 fn char_at_out_of_bounds() {
507 let r = reg();
508 let result = r.call(TypeTag::String, "char_at", &[s("hi"), Value::Int(5)]);
509 assert_eq!(result.unwrap().unwrap(), Value::Optional(None));
510 }
511
512 #[test]
513 fn substring_ok() {
514 let r = reg();
515 let result = r.call(
516 TypeTag::String,
517 "substring",
518 &[s("hello world"), Value::Int(0), Value::Int(5)],
519 );
520 assert_eq!(result.unwrap().unwrap(), s("hello"));
521 }
522
523 #[test]
524 fn slice_alias_ok() {
525 let r = reg();
526 let result = r.call(
527 TypeTag::String,
528 "slice",
529 &[s("hello world"), Value::Int(0), Value::Int(5)],
530 );
531 assert_eq!(result.unwrap().unwrap(), s("hello"));
532 }
533
534 #[test]
535 fn replace_ok() {
536 let r = reg();
537 let result = r.call(
538 TypeTag::String,
539 "replace",
540 &[s("hello world"), s("world"), s("bock")],
541 );
542 assert_eq!(result.unwrap().unwrap(), s("hello bock"));
543 }
544
545 #[test]
546 fn is_empty_true() {
547 let r = reg();
548 let result = r.call(TypeTag::String, "is_empty", &[s("")]);
549 assert_eq!(result.unwrap().unwrap(), Value::Bool(true));
550 }
551
552 #[test]
553 fn is_empty_false() {
554 let r = reg();
555 let result = r.call(TypeTag::String, "is_empty", &[s("x")]);
556 assert_eq!(result.unwrap().unwrap(), Value::Bool(false));
557 }
558
559 #[test]
562 fn len_ascii() {
563 let r = reg();
564 let result = r.call(TypeTag::String, "len", &[s("hello")]);
565 assert_eq!(result.unwrap().unwrap(), Value::Int(5));
566 }
567
568 #[test]
569 fn len_multibyte() {
570 let r = reg();
571 let result = r.call(TypeTag::String, "len", &[s("héllo")]);
573 assert_eq!(result.unwrap().unwrap(), Value::Int(5));
574 }
575
576 #[test]
577 fn len_emoji() {
578 let r = reg();
579 let result = r.call(TypeTag::String, "len", &[s("🎉")]);
581 assert_eq!(result.unwrap().unwrap(), Value::Int(1));
582 }
583
584 #[test]
587 fn byte_len_unicode() {
588 let r = reg();
589 let result = r.call(TypeTag::String, "byte_len", &[s("héllo")]);
591 assert_eq!(result.unwrap().unwrap(), Value::Int(6));
592 }
593
594 #[test]
595 fn byte_len_emoji() {
596 let r = reg();
597 let result = r.call(TypeTag::String, "byte_len", &[s("🎉")]);
599 assert_eq!(result.unwrap().unwrap(), Value::Int(4));
600 }
601
602 #[test]
603 fn chars_ok() {
604 let r = reg();
605 let result = r.call(TypeTag::String, "chars", &[s("hi")]);
606 assert_eq!(
607 result.unwrap().unwrap(),
608 Value::List(vec![Value::Char('h'), Value::Char('i')])
609 );
610 }
611
612 #[test]
613 fn repeat_ok() {
614 let r = reg();
615 let result = r.call(TypeTag::String, "repeat", &[s("ab"), Value::Int(3)]);
616 assert_eq!(result.unwrap().unwrap(), s("ababab"));
617 }
618
619 #[test]
620 fn index_of_found() {
621 let r = reg();
622 let result = r.call(TypeTag::String, "index_of", &[s("hello"), s("ll")]);
623 assert_eq!(
624 result.unwrap().unwrap(),
625 Value::Optional(Some(Box::new(Value::Int(2))))
626 );
627 }
628
629 #[test]
630 fn index_of_not_found() {
631 let r = reg();
632 let result = r.call(TypeTag::String, "index_of", &[s("hello"), s("xyz")]);
633 assert_eq!(result.unwrap().unwrap(), Value::Optional(None));
634 }
635
636 #[test]
637 fn hash_code_deterministic() {
638 let r = reg();
639 let h1 = r
640 .call(TypeTag::String, "hash_code", &[s("test")])
641 .unwrap()
642 .unwrap();
643 let h2 = r
644 .call(TypeTag::String, "hash_code", &[s("test")])
645 .unwrap()
646 .unwrap();
647 assert_eq!(h1, h2);
648 }
649
650 #[test]
653 fn trim_start_ok() {
654 let r = reg();
655 let result = r.call(TypeTag::String, "trim_start", &[s(" hello ")]);
656 assert_eq!(result.unwrap().unwrap(), s("hello "));
657 }
658
659 #[test]
660 fn trim_end_ok() {
661 let r = reg();
662 let result = r.call(TypeTag::String, "trim_end", &[s(" hello ")]);
663 assert_eq!(result.unwrap().unwrap(), s(" hello"));
664 }
665
666 #[test]
667 fn pad_start_ok() {
668 let r = reg();
669 let result = r.call(
670 TypeTag::String,
671 "pad_start",
672 &[s("hi"), Value::Int(5), s("0")],
673 );
674 assert_eq!(result.unwrap().unwrap(), s("000hi"));
675 }
676
677 #[test]
678 fn pad_start_no_op_when_long_enough() {
679 let r = reg();
680 let result = r.call(
681 TypeTag::String,
682 "pad_start",
683 &[s("hello"), Value::Int(3), s(" ")],
684 );
685 assert_eq!(result.unwrap().unwrap(), s("hello"));
686 }
687
688 #[test]
689 fn pad_end_ok() {
690 let r = reg();
691 let result = r.call(
692 TypeTag::String,
693 "pad_end",
694 &[s("hi"), Value::Int(5), s(".")],
695 );
696 assert_eq!(result.unwrap().unwrap(), s("hi..."));
697 }
698
699 #[test]
700 fn reverse_ok() {
701 let r = reg();
702 let result = r.call(TypeTag::String, "reverse", &[s("hello")]);
703 assert_eq!(result.unwrap().unwrap(), s("olleh"));
704 }
705
706 #[test]
707 fn reverse_unicode() {
708 let r = reg();
709 let result = r.call(TypeTag::String, "reverse", &[s("héllo")]);
710 assert_eq!(result.unwrap().unwrap(), s("olléh"));
711 }
712
713 #[test]
714 fn bytes_ok() {
715 let r = reg();
716 let result = r.call(TypeTag::String, "bytes", &[s("AB")]);
717 assert_eq!(
718 result.unwrap().unwrap(),
719 Value::List(vec![Value::Int(65), Value::Int(66)])
720 );
721 }
722
723 #[test]
724 fn join_ok() {
725 let r = reg();
726 let list = Value::List(vec![s("a"), s("b"), s("c")]);
727 let result = r.call(TypeTag::String, "join", &[s(", "), list]);
728 assert_eq!(result.unwrap().unwrap(), s("a, b, c"));
729 }
730
731 #[test]
732 fn join_empty_list() {
733 let r = reg();
734 let list = Value::List(vec![]);
735 let result = r.call(TypeTag::String, "join", &[s("-"), list]);
736 assert_eq!(result.unwrap().unwrap(), s(""));
737 }
738
739 #[test]
740 fn format_ok() {
741 let r = reg();
742 let result = r.call(
743 TypeTag::String,
744 "format",
745 &[
746 s("Hello, {}! You are {} years old."),
747 s("Alice"),
748 Value::Int(30),
749 ],
750 );
751 assert_eq!(
752 result.unwrap().unwrap(),
753 s("Hello, Alice! You are 30 years old.")
754 );
755 }
756
757 #[test]
758 fn format_no_placeholders() {
759 let r = reg();
760 let result = r.call(TypeTag::String, "format", &[s("no placeholders")]);
761 assert_eq!(result.unwrap().unwrap(), s("no placeholders"));
762 }
763
764 #[test]
767 fn regex_match_true() {
768 let r = reg();
769 let result = r.call(TypeTag::String, "regex_match", &[s("hello123"), s("\\d+")]);
770 assert_eq!(result.unwrap().unwrap(), Value::Bool(true));
771 }
772
773 #[test]
774 fn regex_match_false() {
775 let r = reg();
776 let result = r.call(TypeTag::String, "regex_match", &[s("hello"), s("\\d+")]);
777 assert_eq!(result.unwrap().unwrap(), Value::Bool(false));
778 }
779
780 #[test]
781 fn regex_find_all() {
782 let r = reg();
783 let result = r.call(
784 TypeTag::String,
785 "regex_find",
786 &[s("abc123def456"), s("\\d+")],
787 );
788 assert_eq!(
789 result.unwrap().unwrap(),
790 Value::List(vec![s("123"), s("456")])
791 );
792 }
793
794 #[test]
795 fn regex_find_no_matches() {
796 let r = reg();
797 let result = r.call(TypeTag::String, "regex_find", &[s("hello"), s("\\d+")]);
798 assert_eq!(result.unwrap().unwrap(), Value::List(vec![]));
799 }
800
801 #[test]
802 fn regex_replace_ok() {
803 let r = reg();
804 let result = r.call(
805 TypeTag::String,
806 "regex_replace",
807 &[s("hello 123 world 456"), s("\\d+"), s("NUM")],
808 );
809 assert_eq!(result.unwrap().unwrap(), s("hello NUM world NUM"));
810 }
811
812 #[test]
813 fn regex_invalid_pattern() {
814 let r = reg();
815 let result = r.call(TypeTag::String, "regex_match", &[s("test"), s("[invalid")]);
816 assert!(result.unwrap().is_err());
817 }
818}