Skip to main content

bock_core/primitives/
string.rs

1//! String primitive type methods and trait implementations.
2//!
3//! Full method suite: split, join, trim, pad, replace, contains, starts_with,
4//! ends_with, format, repeat, reverse, chars, bytes, and regex operations.
5
6use bock_interp::{BockString, BuiltinRegistry, RuntimeError, TypeTag, Value};
7use regex::Regex;
8
9/// Register all String methods and trait implementations.
10pub fn register(registry: &mut BuiltinRegistry) {
11    // ── Add trait (concatenation) ────────────────────────────────────────
12    registry.register(TypeTag::String, "add", string_add);
13
14    // ── Comparable trait ─────────────────────────────────────────────────
15    registry.register(TypeTag::String, "compare", string_compare);
16
17    // ── Hashable trait ───────────────────────────────────────────────────
18    registry.register(TypeTag::String, "hash_code", string_hash_code);
19
20    // ── Displayable trait ────────────────────────────────────────────────
21    registry.register(TypeTag::String, "display", string_display);
22
23    // ── Type-specific methods ────────────────────────────────────────────
24    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    // ── Regex methods ─────────────────────────────────────────────────────
51    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
56// ─── Helpers ──────────────────────────────────────────────────────────────────
57
58fn 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
84// ─── Add (concatenation) ─────────────────────────────────────────────────────
85
86fn 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
92// ─── Comparable ───────────────────────────────────────────────────────────────
93
94fn 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
100// ─── Hashable ─────────────────────────────────────────────────────────────────
101
102fn 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
110// ─── Displayable ──────────────────────────────────────────────────────────────
111
112/// For strings, display returns the string itself (identity).
113fn string_display(args: &[Value]) -> Result<Value, RuntimeError> {
114    let a = expect_str(args, 0, "display")?;
115    Ok(Value::String(BockString::new(a)))
116}
117
118// ─── Type-specific methods ────────────────────────────────────────────────────
119
120fn 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
202/// Returns the number of Unicode scalar values (characters) in the string.
203fn 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    // Return character index, not byte index
234    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
253/// `"hi".pad_start(5, " ")` → `"   hi"`
254fn 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
272/// `"hi".pad_end(5, " ")` → `"hi   "`
273fn 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
296/// Returns a list of `Value::Int` byte values.
297fn 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
303/// Static-style join: `separator.join(list_of_strings)`.
304fn 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
332/// Simple positional format: `"Hello, {}!".format("world")` → `"Hello, world!"`.
333fn 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
359// ─── Regex methods ────────────────────────────────────────────────────────────
360
361fn compile_regex(pattern: &str) -> Result<Regex, RuntimeError> {
362    Regex::new(pattern).map_err(|e| RuntimeError::TypeError(format!("invalid regex pattern: {e}")))
363}
364
365/// `"hello123".regex_match("\\d+")` → `true`
366fn 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
373/// `"hello123world".regex_find("\\d+")` → `Optional(Some("123"))`
374/// Returns a list of all matches.
375fn 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
386/// `"hello123world".regex_replace("\\d+", "NUM")` → `"helloNUMworld"`
387fn 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// ─── Tests ────────────────────────────────────────────────────────────────────
397
398#[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    // ── len (character count) ─────────────────────────────────────────
560
561    #[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        // "héllo" has 5 chars but 6 bytes (é is 2 bytes in UTF-8)
572        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        // 🎉 is 4 bytes but 1 character
580        let result = r.call(TypeTag::String, "len", &[s("🎉")]);
581        assert_eq!(result.unwrap().unwrap(), Value::Int(1));
582    }
583
584    // ── byte_len ────────────────────────────────────────────────────────
585
586    #[test]
587    fn byte_len_unicode() {
588        let r = reg();
589        // "héllo" has 5 chars but 6 bytes (é is 2 bytes in UTF-8)
590        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        // 🎉 is 4 bytes in UTF-8
598        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    // ── New string methods (P6.6) ─────────────────────────────────────────
651
652    #[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    // ── Regex tests ───────────────────────────────────────────────────────
765
766    #[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}