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