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    // ── Equatable trait ──────────────────────────────────────────────────
18    registry.register(TypeTag::String, "equals", string_equals);
19
20    // ── Hashable trait ───────────────────────────────────────────────────
21    registry.register(TypeTag::String, "hash_code", string_hash_code);
22
23    // ── Displayable trait ────────────────────────────────────────────────
24    registry.register(TypeTag::String, "display", string_display);
25
26    // ── Type-specific methods ────────────────────────────────────────────
27    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    // ── Regex methods ─────────────────────────────────────────────────────
54    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
59// ─── Helpers ──────────────────────────────────────────────────────────────────
60
61fn 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
87// ─── Add (concatenation) ─────────────────────────────────────────────────────
88
89fn 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
95// ─── Comparable ───────────────────────────────────────────────────────────────
96
97fn 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
103// ─── Equatable ────────────────────────────────────────────────────────────────
104
105fn 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
111// ─── Hashable ─────────────────────────────────────────────────────────────────
112
113fn 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
121// ─── Displayable ──────────────────────────────────────────────────────────────
122
123/// For strings, display returns the string itself (identity).
124fn string_display(args: &[Value]) -> Result<Value, RuntimeError> {
125    let a = expect_str(args, 0, "display")?;
126    Ok(Value::String(BockString::new(a)))
127}
128
129// ─── Type-specific methods ────────────────────────────────────────────────────
130
131fn 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
213/// Returns the number of Unicode scalar values (characters) in the string.
214fn 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    // Return character index, not byte index
245    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
264/// `"hi".pad_start(5, " ")` → `"   hi"`
265fn 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
283/// `"hi".pad_end(5, " ")` → `"hi   "`
284fn 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
307/// Returns a list of `Value::Int` byte values.
308fn 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
314/// Static-style join: `separator.join(list_of_strings)`.
315fn 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
343/// Simple positional format: `"Hello, {}!".format("world")` → `"Hello, world!"`.
344fn 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
370// ─── Regex methods ────────────────────────────────────────────────────────────
371
372fn compile_regex(pattern: &str) -> Result<Regex, RuntimeError> {
373    Regex::new(pattern).map_err(|e| RuntimeError::TypeError(format!("invalid regex pattern: {e}")))
374}
375
376/// `"hello123".regex_match("\\d+")` → `true`
377fn 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
384/// `"hello123world".regex_find("\\d+")` → `Optional(Some("123"))`
385/// Returns a list of all matches.
386fn 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
397/// `"hello123world".regex_replace("\\d+", "NUM")` → `"helloNUMworld"`
398fn 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// ─── Tests ────────────────────────────────────────────────────────────────────
408
409#[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    // ── len (character count) ─────────────────────────────────────────
575
576    #[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        // "héllo" has 5 chars but 6 bytes (é is 2 bytes in UTF-8)
587        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        // 🎉 is 4 bytes but 1 character
595        let result = r.call(TypeTag::String, "len", &[s("🎉")]);
596        assert_eq!(result.unwrap().unwrap(), Value::Int(1));
597    }
598
599    // ── byte_len ────────────────────────────────────────────────────────
600
601    #[test]
602    fn byte_len_unicode() {
603        let r = reg();
604        // "héllo" has 5 chars but 6 bytes (é is 2 bytes in UTF-8)
605        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        // 🎉 is 4 bytes in UTF-8
613        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    // ── New string methods (P6.6) ─────────────────────────────────────────
666
667    #[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    // ── Regex tests ───────────────────────────────────────────────────────
780
781    #[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}