seq_runtime/
string_ops.rs

1//! String operations for Seq
2//!
3//! These functions are exported with C ABI for LLVM codegen to call.
4//!
5//! # Design Decision: split Return Value
6//!
7//! `split` uses Option A (push parts + count):
8//! - "a b c" " " split → "a" "b" "c" 3
9//!
10//! This is the simplest approach, requiring no new types.
11//! The count allows the caller to know how many parts were pushed.
12
13use crate::seqstring::global_string;
14use crate::stack::{Stack, pop, push};
15use crate::value::Value;
16
17/// Split a string on a delimiter
18///
19/// Stack effect: ( str delim -- Variant )
20///
21/// Returns a Variant containing the split parts as fields.
22///
23/// # Safety
24/// Stack must have two String values on top
25#[unsafe(no_mangle)]
26pub unsafe extern "C" fn patch_seq_string_split(stack: Stack) -> Stack {
27    use crate::value::VariantData;
28
29    assert!(!stack.is_null(), "string_split: stack is empty");
30
31    let (stack, delim_val) = unsafe { pop(stack) };
32    assert!(!stack.is_null(), "string_split: need two strings");
33    let (stack, str_val) = unsafe { pop(stack) };
34
35    match (str_val, delim_val) {
36        (Value::String(s), Value::String(d)) => {
37            // Split and collect into Value::String instances
38            let fields: Vec<Value> = s
39                .as_str()
40                .split(d.as_str())
41                .map(|part| Value::String(global_string(part.to_owned())))
42                .collect();
43
44            // Create a Variant with tag 0 and the split parts as fields
45            let variant = Value::Variant(Box::new(VariantData::new(0, fields)));
46
47            unsafe { push(stack, variant) }
48        }
49        _ => panic!("string_split: expected two strings on stack"),
50    }
51}
52
53/// Check if a string is empty
54///
55/// Stack effect: ( str -- bool )
56///
57/// # Safety
58/// Stack must have a String value on top
59#[unsafe(no_mangle)]
60pub unsafe extern "C" fn patch_seq_string_empty(stack: Stack) -> Stack {
61    assert!(!stack.is_null(), "string_empty: stack is empty");
62
63    let (stack, value) = unsafe { pop(stack) };
64
65    match value {
66        Value::String(s) => {
67            let is_empty = s.as_str().is_empty();
68            let result = if is_empty { 1 } else { 0 };
69            unsafe { push(stack, Value::Int(result)) }
70        }
71        _ => panic!("string_empty: expected String on stack"),
72    }
73}
74
75/// Check if a string contains a substring
76///
77/// Stack effect: ( str substring -- bool )
78///
79/// # Safety
80/// Stack must have two String values on top
81#[unsafe(no_mangle)]
82pub unsafe extern "C" fn patch_seq_string_contains(stack: Stack) -> Stack {
83    assert!(!stack.is_null(), "string_contains: stack is empty");
84
85    let (stack, substring_val) = unsafe { pop(stack) };
86    assert!(!stack.is_null(), "string_contains: need two strings");
87    let (stack, str_val) = unsafe { pop(stack) };
88
89    match (str_val, substring_val) {
90        (Value::String(s), Value::String(sub)) => {
91            let contains = s.as_str().contains(sub.as_str());
92            let result = if contains { 1 } else { 0 };
93            unsafe { push(stack, Value::Int(result)) }
94        }
95        _ => panic!("string_contains: expected two strings on stack"),
96    }
97}
98
99/// Check if a string starts with a prefix
100///
101/// Stack effect: ( str prefix -- bool )
102///
103/// # Safety
104/// Stack must have two String values on top
105#[unsafe(no_mangle)]
106pub unsafe extern "C" fn patch_seq_string_starts_with(stack: Stack) -> Stack {
107    assert!(!stack.is_null(), "string_starts_with: stack is empty");
108
109    let (stack, prefix_val) = unsafe { pop(stack) };
110    assert!(!stack.is_null(), "string_starts_with: need two strings");
111    let (stack, str_val) = unsafe { pop(stack) };
112
113    match (str_val, prefix_val) {
114        (Value::String(s), Value::String(prefix)) => {
115            let starts = s.as_str().starts_with(prefix.as_str());
116            let result = if starts { 1 } else { 0 };
117            unsafe { push(stack, Value::Int(result)) }
118        }
119        _ => panic!("string_starts_with: expected two strings on stack"),
120    }
121}
122
123/// Concatenate two strings
124///
125/// Stack effect: ( str1 str2 -- result )
126///
127/// # Safety
128/// Stack must have two String values on top
129#[unsafe(no_mangle)]
130pub unsafe extern "C" fn patch_seq_string_concat(stack: Stack) -> Stack {
131    assert!(!stack.is_null(), "string_concat: stack is empty");
132
133    let (stack, str2_val) = unsafe { pop(stack) };
134    assert!(!stack.is_null(), "string_concat: need two strings");
135    let (stack, str1_val) = unsafe { pop(stack) };
136
137    match (str1_val, str2_val) {
138        (Value::String(s1), Value::String(s2)) => {
139            let result = format!("{}{}", s1.as_str(), s2.as_str());
140            unsafe { push(stack, Value::String(global_string(result))) }
141        }
142        _ => panic!("string_concat: expected two strings on stack"),
143    }
144}
145
146/// Get the length of a string in Unicode characters (code points)
147///
148/// Stack effect: ( str -- int )
149///
150/// Note: This returns character count, not byte count.
151/// For UTF-8 byte length (e.g., HTTP Content-Length), use `string-byte-length`.
152///
153/// # Safety
154/// Stack must have a String value on top
155#[unsafe(no_mangle)]
156pub unsafe extern "C" fn patch_seq_string_length(stack: Stack) -> Stack {
157    assert!(!stack.is_null(), "string_length: stack is empty");
158
159    let (stack, str_val) = unsafe { pop(stack) };
160
161    match str_val {
162        Value::String(s) => {
163            let len = s.as_str().chars().count() as i64;
164            unsafe { push(stack, Value::Int(len)) }
165        }
166        _ => panic!("string_length: expected String on stack"),
167    }
168}
169
170/// Get the byte length of a string (UTF-8 encoded)
171///
172/// Stack effect: ( str -- int )
173///
174/// Use this for HTTP Content-Length headers and buffer allocation.
175///
176/// # Safety
177/// Stack must have a String value on top
178#[unsafe(no_mangle)]
179pub unsafe extern "C" fn patch_seq_string_byte_length(stack: Stack) -> Stack {
180    assert!(!stack.is_null(), "string_byte_length: stack is empty");
181
182    let (stack, str_val) = unsafe { pop(stack) };
183
184    match str_val {
185        Value::String(s) => {
186            let len = s.as_str().len() as i64;
187            unsafe { push(stack, Value::Int(len)) }
188        }
189        _ => panic!("string_byte_length: expected String on stack"),
190    }
191}
192
193/// Get the Unicode code point at a character index
194///
195/// Stack effect: ( str int -- int )
196///
197/// Returns the code point value at the given character index.
198/// Returns -1 if index is out of bounds.
199///
200/// # Safety
201/// Stack must have a String and Int on top
202#[unsafe(no_mangle)]
203pub unsafe extern "C" fn patch_seq_string_char_at(stack: Stack) -> Stack {
204    assert!(!stack.is_null(), "string_char_at: stack is empty");
205
206    let (stack, index_val) = unsafe { pop(stack) };
207    assert!(!stack.is_null(), "string_char_at: need string and index");
208    let (stack, str_val) = unsafe { pop(stack) };
209
210    match (str_val, index_val) {
211        (Value::String(s), Value::Int(index)) => {
212            let result = if index < 0 {
213                -1
214            } else {
215                s.as_str()
216                    .chars()
217                    .nth(index as usize)
218                    .map(|c| c as i64)
219                    .unwrap_or(-1)
220            };
221            unsafe { push(stack, Value::Int(result)) }
222        }
223        _ => panic!("string_char_at: expected String and Int on stack"),
224    }
225}
226
227/// Extract a substring using character indices
228///
229/// Stack effect: ( str start len -- str )
230///
231/// Arguments:
232/// - str: The source string
233/// - start: Starting character index
234/// - len: Number of characters to extract
235///
236/// Edge cases:
237/// - Start beyond end: returns empty string
238/// - Length extends past end: clamps to available
239///
240/// # Safety
241/// Stack must have String, Int, Int on top
242#[unsafe(no_mangle)]
243pub unsafe extern "C" fn patch_seq_string_substring(stack: Stack) -> Stack {
244    assert!(!stack.is_null(), "string_substring: stack is empty");
245
246    let (stack, len_val) = unsafe { pop(stack) };
247    assert!(
248        !stack.is_null(),
249        "string_substring: need string, start, len"
250    );
251    let (stack, start_val) = unsafe { pop(stack) };
252    assert!(
253        !stack.is_null(),
254        "string_substring: need string, start, len"
255    );
256    let (stack, str_val) = unsafe { pop(stack) };
257
258    match (str_val, start_val, len_val) {
259        (Value::String(s), Value::Int(start), Value::Int(len)) => {
260            let result = if start < 0 || len < 0 {
261                String::new()
262            } else {
263                s.as_str()
264                    .chars()
265                    .skip(start as usize)
266                    .take(len as usize)
267                    .collect()
268            };
269            unsafe { push(stack, Value::String(global_string(result))) }
270        }
271        _ => panic!("string_substring: expected String, Int, Int on stack"),
272    }
273}
274
275/// Convert a Unicode code point to a single-character string
276///
277/// Stack effect: ( int -- str )
278///
279/// Creates a string containing the single character represented by the code point.
280/// Panics if the code point is invalid.
281///
282/// # Safety
283/// Stack must have an Int on top
284#[unsafe(no_mangle)]
285pub unsafe extern "C" fn patch_seq_char_to_string(stack: Stack) -> Stack {
286    assert!(!stack.is_null(), "char_to_string: stack is empty");
287
288    let (stack, code_point_val) = unsafe { pop(stack) };
289
290    match code_point_val {
291        Value::Int(code_point) => {
292            let result = if !(0..=0x10FFFF).contains(&code_point) {
293                // Invalid code point - return empty string
294                String::new()
295            } else {
296                match char::from_u32(code_point as u32) {
297                    Some(c) => c.to_string(),
298                    None => String::new(), // Invalid code point (e.g., surrogate)
299                }
300            };
301            unsafe { push(stack, Value::String(global_string(result))) }
302        }
303        _ => panic!("char_to_string: expected Int on stack"),
304    }
305}
306
307/// Find the first occurrence of a substring
308///
309/// Stack effect: ( str needle -- int )
310///
311/// Returns the character index of the first occurrence of needle in str.
312/// Returns -1 if not found.
313///
314/// # Safety
315/// Stack must have two Strings on top
316#[unsafe(no_mangle)]
317pub unsafe extern "C" fn patch_seq_string_find(stack: Stack) -> Stack {
318    assert!(!stack.is_null(), "string_find: stack is empty");
319
320    let (stack, needle_val) = unsafe { pop(stack) };
321    assert!(!stack.is_null(), "string_find: need string and needle");
322    let (stack, str_val) = unsafe { pop(stack) };
323
324    match (str_val, needle_val) {
325        (Value::String(haystack), Value::String(needle)) => {
326            let haystack_str = haystack.as_str();
327            let needle_str = needle.as_str();
328
329            // Find byte position then convert to character position
330            let result = match haystack_str.find(needle_str) {
331                Some(byte_pos) => {
332                    // Count characters up to byte_pos
333                    haystack_str[..byte_pos].chars().count() as i64
334                }
335                None => -1,
336            };
337            unsafe { push(stack, Value::Int(result)) }
338        }
339        _ => panic!("string_find: expected two Strings on stack"),
340    }
341}
342
343/// Trim whitespace from both ends of a string
344///
345/// Stack effect: ( str -- trimmed )
346///
347/// # Safety
348/// Stack must have a String value on top
349#[unsafe(no_mangle)]
350pub unsafe extern "C" fn patch_seq_string_trim(stack: Stack) -> Stack {
351    assert!(!stack.is_null(), "string_trim: stack is empty");
352
353    let (stack, str_val) = unsafe { pop(stack) };
354
355    match str_val {
356        Value::String(s) => {
357            let trimmed = s.as_str().trim();
358            unsafe { push(stack, Value::String(global_string(trimmed.to_owned()))) }
359        }
360        _ => panic!("string_trim: expected String on stack"),
361    }
362}
363
364/// Convert a string to uppercase
365///
366/// Stack effect: ( str -- upper )
367///
368/// # Safety
369/// Stack must have a String value on top
370#[unsafe(no_mangle)]
371pub unsafe extern "C" fn patch_seq_string_to_upper(stack: Stack) -> Stack {
372    assert!(!stack.is_null(), "string_to_upper: stack is empty");
373
374    let (stack, str_val) = unsafe { pop(stack) };
375
376    match str_val {
377        Value::String(s) => {
378            let upper = s.as_str().to_uppercase();
379            unsafe { push(stack, Value::String(global_string(upper))) }
380        }
381        _ => panic!("string_to_upper: expected String on stack"),
382    }
383}
384
385/// Convert a string to lowercase
386///
387/// Stack effect: ( str -- lower )
388///
389/// # Safety
390/// Stack must have a String value on top
391#[unsafe(no_mangle)]
392pub unsafe extern "C" fn patch_seq_string_to_lower(stack: Stack) -> Stack {
393    assert!(!stack.is_null(), "string_to_lower: stack is empty");
394
395    let (stack, str_val) = unsafe { pop(stack) };
396
397    match str_val {
398        Value::String(s) => {
399            let lower = s.as_str().to_lowercase();
400            unsafe { push(stack, Value::String(global_string(lower))) }
401        }
402        _ => panic!("string_to_lower: expected String on stack"),
403    }
404}
405
406/// Check if two strings are equal
407///
408/// Stack effect: ( str1 str2 -- bool )
409///
410/// # Safety
411/// Stack must have two String values on top
412#[unsafe(no_mangle)]
413pub unsafe extern "C" fn patch_seq_string_equal(stack: Stack) -> Stack {
414    assert!(!stack.is_null(), "string_equal: stack is empty");
415
416    let (stack, str2_val) = unsafe { pop(stack) };
417    assert!(!stack.is_null(), "string_equal: need two strings");
418    let (stack, str1_val) = unsafe { pop(stack) };
419
420    match (str1_val, str2_val) {
421        (Value::String(s1), Value::String(s2)) => {
422            let equal = s1.as_str() == s2.as_str();
423            let result = if equal { 1 } else { 0 };
424            unsafe { push(stack, Value::Int(result)) }
425        }
426        _ => panic!("string_equal: expected two strings on stack"),
427    }
428}
429
430// Public re-exports with short names for internal use
431pub use patch_seq_char_to_string as char_to_string;
432pub use patch_seq_string_byte_length as string_byte_length;
433pub use patch_seq_string_char_at as string_char_at;
434pub use patch_seq_string_concat as string_concat;
435pub use patch_seq_string_contains as string_contains;
436pub use patch_seq_string_empty as string_empty;
437pub use patch_seq_string_equal as string_equal;
438pub use patch_seq_string_find as string_find;
439pub use patch_seq_string_length as string_length;
440pub use patch_seq_string_split as string_split;
441pub use patch_seq_string_starts_with as string_starts_with;
442pub use patch_seq_string_substring as string_substring;
443pub use patch_seq_string_to_lower as string_to_lower;
444pub use patch_seq_string_to_upper as string_to_upper;
445pub use patch_seq_string_trim as string_trim;
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    #[test]
452    fn test_string_split_simple() {
453        unsafe {
454            let stack = std::ptr::null_mut();
455            let stack = push(stack, Value::String(global_string("a b c".to_owned())));
456            let stack = push(stack, Value::String(global_string(" ".to_owned())));
457
458            let stack = string_split(stack);
459
460            // Should have a Variant with 3 fields: "a", "b", "c"
461            let (stack, result) = pop(stack);
462            match result {
463                Value::Variant(v) => {
464                    assert_eq!(v.tag, 0);
465                    assert_eq!(v.fields.len(), 3);
466                    assert_eq!(v.fields[0], Value::String(global_string("a".to_owned())));
467                    assert_eq!(v.fields[1], Value::String(global_string("b".to_owned())));
468                    assert_eq!(v.fields[2], Value::String(global_string("c".to_owned())));
469                }
470                _ => panic!("Expected Variant, got {:?}", result),
471            }
472
473            assert!(stack.is_null());
474        }
475    }
476
477    #[test]
478    fn test_string_split_empty() {
479        unsafe {
480            let stack = std::ptr::null_mut();
481            let stack = push(stack, Value::String(global_string("".to_owned())));
482            let stack = push(stack, Value::String(global_string(" ".to_owned())));
483
484            let stack = string_split(stack);
485
486            // Empty string splits to one empty part
487            let (stack, result) = pop(stack);
488            match result {
489                Value::Variant(v) => {
490                    assert_eq!(v.tag, 0);
491                    assert_eq!(v.fields.len(), 1);
492                    assert_eq!(v.fields[0], Value::String(global_string("".to_owned())));
493                }
494                _ => panic!("Expected Variant, got {:?}", result),
495            }
496
497            assert!(stack.is_null());
498        }
499    }
500
501    #[test]
502    fn test_string_empty_true() {
503        unsafe {
504            let stack = std::ptr::null_mut();
505            let stack = push(stack, Value::String(global_string("".to_owned())));
506
507            let stack = string_empty(stack);
508
509            let (stack, result) = pop(stack);
510            assert_eq!(result, Value::Int(1));
511            assert!(stack.is_null());
512        }
513    }
514
515    #[test]
516    fn test_string_empty_false() {
517        unsafe {
518            let stack = std::ptr::null_mut();
519            let stack = push(stack, Value::String(global_string("hello".to_owned())));
520
521            let stack = string_empty(stack);
522
523            let (stack, result) = pop(stack);
524            assert_eq!(result, Value::Int(0));
525            assert!(stack.is_null());
526        }
527    }
528
529    #[test]
530    fn test_string_contains_true() {
531        unsafe {
532            let stack = std::ptr::null_mut();
533            let stack = push(
534                stack,
535                Value::String(global_string("hello world".to_owned())),
536            );
537            let stack = push(stack, Value::String(global_string("world".to_owned())));
538
539            let stack = string_contains(stack);
540
541            let (stack, result) = pop(stack);
542            assert_eq!(result, Value::Int(1));
543            assert!(stack.is_null());
544        }
545    }
546
547    #[test]
548    fn test_string_contains_false() {
549        unsafe {
550            let stack = std::ptr::null_mut();
551            let stack = push(
552                stack,
553                Value::String(global_string("hello world".to_owned())),
554            );
555            let stack = push(stack, Value::String(global_string("foo".to_owned())));
556
557            let stack = string_contains(stack);
558
559            let (stack, result) = pop(stack);
560            assert_eq!(result, Value::Int(0));
561            assert!(stack.is_null());
562        }
563    }
564
565    #[test]
566    fn test_string_starts_with_true() {
567        unsafe {
568            let stack = std::ptr::null_mut();
569            let stack = push(
570                stack,
571                Value::String(global_string("hello world".to_owned())),
572            );
573            let stack = push(stack, Value::String(global_string("hello".to_owned())));
574
575            let stack = string_starts_with(stack);
576
577            let (stack, result) = pop(stack);
578            assert_eq!(result, Value::Int(1));
579            assert!(stack.is_null());
580        }
581    }
582
583    #[test]
584    fn test_string_starts_with_false() {
585        unsafe {
586            let stack = std::ptr::null_mut();
587            let stack = push(
588                stack,
589                Value::String(global_string("hello world".to_owned())),
590            );
591            let stack = push(stack, Value::String(global_string("world".to_owned())));
592
593            let stack = string_starts_with(stack);
594
595            let (stack, result) = pop(stack);
596            assert_eq!(result, Value::Int(0));
597            assert!(stack.is_null());
598        }
599    }
600
601    #[test]
602    fn test_http_request_line_parsing() {
603        // Real-world use case: Parse "GET /api/users HTTP/1.1"
604        unsafe {
605            let stack = std::ptr::null_mut();
606            let stack = push(
607                stack,
608                Value::String(global_string("GET /api/users HTTP/1.1".to_owned())),
609            );
610            let stack = push(stack, Value::String(global_string(" ".to_owned())));
611
612            let stack = string_split(stack);
613
614            // Should have a Variant with 3 fields: "GET", "/api/users", "HTTP/1.1"
615            let (stack, result) = pop(stack);
616            match result {
617                Value::Variant(v) => {
618                    assert_eq!(v.tag, 0);
619                    assert_eq!(v.fields.len(), 3);
620                    assert_eq!(v.fields[0], Value::String(global_string("GET".to_owned())));
621                    assert_eq!(
622                        v.fields[1],
623                        Value::String(global_string("/api/users".to_owned()))
624                    );
625                    assert_eq!(
626                        v.fields[2],
627                        Value::String(global_string("HTTP/1.1".to_owned()))
628                    );
629                }
630                _ => panic!("Expected Variant, got {:?}", result),
631            }
632
633            assert!(stack.is_null());
634        }
635    }
636
637    #[test]
638    fn test_path_routing() {
639        // Real-world use case: Check if path starts with "/api/"
640        unsafe {
641            let stack = std::ptr::null_mut();
642            let stack = push(stack, Value::String(global_string("/api/users".to_owned())));
643            let stack = push(stack, Value::String(global_string("/api/".to_owned())));
644
645            let stack = string_starts_with(stack);
646
647            let (stack, result) = pop(stack);
648            assert_eq!(result, Value::Int(1));
649            assert!(stack.is_null());
650        }
651    }
652
653    #[test]
654    fn test_string_concat() {
655        unsafe {
656            let stack = std::ptr::null_mut();
657            let stack = push(stack, Value::String(global_string("Hello, ".to_owned())));
658            let stack = push(stack, Value::String(global_string("World!".to_owned())));
659
660            let stack = string_concat(stack);
661
662            let (stack, result) = pop(stack);
663            assert_eq!(
664                result,
665                Value::String(global_string("Hello, World!".to_owned()))
666            );
667            assert!(stack.is_null());
668        }
669    }
670
671    #[test]
672    fn test_string_length() {
673        unsafe {
674            let stack = std::ptr::null_mut();
675            let stack = push(stack, Value::String(global_string("Hello".to_owned())));
676
677            let stack = string_length(stack);
678
679            let (stack, result) = pop(stack);
680            assert_eq!(result, Value::Int(5));
681            assert!(stack.is_null());
682        }
683    }
684
685    #[test]
686    fn test_string_length_empty() {
687        unsafe {
688            let stack = std::ptr::null_mut();
689            let stack = push(stack, Value::String(global_string("".to_owned())));
690
691            let stack = string_length(stack);
692
693            let (stack, result) = pop(stack);
694            assert_eq!(result, Value::Int(0));
695            assert!(stack.is_null());
696        }
697    }
698
699    #[test]
700    fn test_string_trim() {
701        unsafe {
702            let stack = std::ptr::null_mut();
703            let stack = push(
704                stack,
705                Value::String(global_string("  Hello, World!  ".to_owned())),
706            );
707
708            let stack = string_trim(stack);
709
710            let (stack, result) = pop(stack);
711            assert_eq!(
712                result,
713                Value::String(global_string("Hello, World!".to_owned()))
714            );
715            assert!(stack.is_null());
716        }
717    }
718
719    #[test]
720    fn test_string_to_upper() {
721        unsafe {
722            let stack = std::ptr::null_mut();
723            let stack = push(
724                stack,
725                Value::String(global_string("Hello, World!".to_owned())),
726            );
727
728            let stack = string_to_upper(stack);
729
730            let (stack, result) = pop(stack);
731            assert_eq!(
732                result,
733                Value::String(global_string("HELLO, WORLD!".to_owned()))
734            );
735            assert!(stack.is_null());
736        }
737    }
738
739    #[test]
740    fn test_string_to_lower() {
741        unsafe {
742            let stack = std::ptr::null_mut();
743            let stack = push(
744                stack,
745                Value::String(global_string("Hello, World!".to_owned())),
746            );
747
748            let stack = string_to_lower(stack);
749
750            let (stack, result) = pop(stack);
751            assert_eq!(
752                result,
753                Value::String(global_string("hello, world!".to_owned()))
754            );
755            assert!(stack.is_null());
756        }
757    }
758
759    #[test]
760    fn test_http_header_content_length() {
761        // Real-world use case: Build "Content-Length: 42" header
762        unsafe {
763            let stack = std::ptr::null_mut();
764            let stack = push(
765                stack,
766                Value::String(global_string("Content-Length: ".to_owned())),
767            );
768            let stack = push(stack, Value::String(global_string("42".to_owned())));
769
770            let stack = string_concat(stack);
771
772            let (stack, result) = pop(stack);
773            assert_eq!(
774                result,
775                Value::String(global_string("Content-Length: 42".to_owned()))
776            );
777            assert!(stack.is_null());
778        }
779    }
780
781    #[test]
782    fn test_string_equal_true() {
783        unsafe {
784            let stack = std::ptr::null_mut();
785            let stack = push(stack, Value::String(global_string("hello".to_owned())));
786            let stack = push(stack, Value::String(global_string("hello".to_owned())));
787
788            let stack = string_equal(stack);
789
790            let (stack, result) = pop(stack);
791            assert_eq!(result, Value::Int(1));
792            assert!(stack.is_null());
793        }
794    }
795
796    #[test]
797    fn test_string_equal_false() {
798        unsafe {
799            let stack = std::ptr::null_mut();
800            let stack = push(stack, Value::String(global_string("hello".to_owned())));
801            let stack = push(stack, Value::String(global_string("world".to_owned())));
802
803            let stack = string_equal(stack);
804
805            let (stack, result) = pop(stack);
806            assert_eq!(result, Value::Int(0));
807            assert!(stack.is_null());
808        }
809    }
810
811    #[test]
812    fn test_string_equal_empty_strings() {
813        unsafe {
814            let stack = std::ptr::null_mut();
815            let stack = push(stack, Value::String(global_string("".to_owned())));
816            let stack = push(stack, Value::String(global_string("".to_owned())));
817
818            let stack = string_equal(stack);
819
820            let (stack, result) = pop(stack);
821            assert_eq!(result, Value::Int(1));
822            assert!(stack.is_null());
823        }
824    }
825
826    // UTF-8 String Primitives Tests
827
828    #[test]
829    fn test_string_length_utf8() {
830        // "héllo" has 5 characters but 6 bytes (é is 2 bytes in UTF-8)
831        unsafe {
832            let stack = std::ptr::null_mut();
833            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
834
835            let stack = string_length(stack);
836
837            let (stack, result) = pop(stack);
838            assert_eq!(result, Value::Int(5)); // Characters, not bytes
839            assert!(stack.is_null());
840        }
841    }
842
843    #[test]
844    fn test_string_length_emoji() {
845        // Emoji is one code point but multiple bytes
846        unsafe {
847            let stack = std::ptr::null_mut();
848            let stack = push(stack, Value::String(global_string("hi🎉".to_owned())));
849
850            let stack = string_length(stack);
851
852            let (stack, result) = pop(stack);
853            assert_eq!(result, Value::Int(3)); // 'h', 'i', and emoji
854            assert!(stack.is_null());
855        }
856    }
857
858    #[test]
859    fn test_string_byte_length_ascii() {
860        unsafe {
861            let stack = std::ptr::null_mut();
862            let stack = push(stack, Value::String(global_string("hello".to_owned())));
863
864            let stack = string_byte_length(stack);
865
866            let (stack, result) = pop(stack);
867            assert_eq!(result, Value::Int(5)); // Same as char length for ASCII
868            assert!(stack.is_null());
869        }
870    }
871
872    #[test]
873    fn test_string_byte_length_utf8() {
874        // "héllo" has 5 characters but 6 bytes
875        unsafe {
876            let stack = std::ptr::null_mut();
877            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
878
879            let stack = string_byte_length(stack);
880
881            let (stack, result) = pop(stack);
882            assert_eq!(result, Value::Int(6)); // Bytes, not characters
883            assert!(stack.is_null());
884        }
885    }
886
887    #[test]
888    fn test_string_char_at_ascii() {
889        unsafe {
890            let stack = std::ptr::null_mut();
891            let stack = push(stack, Value::String(global_string("hello".to_owned())));
892            let stack = push(stack, Value::Int(0));
893
894            let stack = string_char_at(stack);
895
896            let (stack, result) = pop(stack);
897            assert_eq!(result, Value::Int(104)); // 'h' = 104
898            assert!(stack.is_null());
899        }
900    }
901
902    #[test]
903    fn test_string_char_at_utf8() {
904        // Get the é character at index 1 in "héllo"
905        unsafe {
906            let stack = std::ptr::null_mut();
907            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
908            let stack = push(stack, Value::Int(1));
909
910            let stack = string_char_at(stack);
911
912            let (stack, result) = pop(stack);
913            assert_eq!(result, Value::Int(233)); // 'é' = U+00E9 = 233
914            assert!(stack.is_null());
915        }
916    }
917
918    #[test]
919    fn test_string_char_at_out_of_bounds() {
920        unsafe {
921            let stack = std::ptr::null_mut();
922            let stack = push(stack, Value::String(global_string("hello".to_owned())));
923            let stack = push(stack, Value::Int(10)); // Out of bounds
924
925            let stack = string_char_at(stack);
926
927            let (stack, result) = pop(stack);
928            assert_eq!(result, Value::Int(-1));
929            assert!(stack.is_null());
930        }
931    }
932
933    #[test]
934    fn test_string_char_at_negative() {
935        unsafe {
936            let stack = std::ptr::null_mut();
937            let stack = push(stack, Value::String(global_string("hello".to_owned())));
938            let stack = push(stack, Value::Int(-1));
939
940            let stack = string_char_at(stack);
941
942            let (stack, result) = pop(stack);
943            assert_eq!(result, Value::Int(-1));
944            assert!(stack.is_null());
945        }
946    }
947
948    #[test]
949    fn test_string_substring_simple() {
950        unsafe {
951            let stack = std::ptr::null_mut();
952            let stack = push(stack, Value::String(global_string("hello".to_owned())));
953            let stack = push(stack, Value::Int(1)); // start
954            let stack = push(stack, Value::Int(3)); // len
955
956            let stack = string_substring(stack);
957
958            let (stack, result) = pop(stack);
959            assert_eq!(result, Value::String(global_string("ell".to_owned())));
960            assert!(stack.is_null());
961        }
962    }
963
964    #[test]
965    fn test_string_substring_utf8() {
966        // "héllo" - get "éll" (characters 1-3)
967        unsafe {
968            let stack = std::ptr::null_mut();
969            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
970            let stack = push(stack, Value::Int(1)); // start
971            let stack = push(stack, Value::Int(3)); // len
972
973            let stack = string_substring(stack);
974
975            let (stack, result) = pop(stack);
976            assert_eq!(result, Value::String(global_string("éll".to_owned())));
977            assert!(stack.is_null());
978        }
979    }
980
981    #[test]
982    fn test_string_substring_clamp() {
983        // Request more than available - should clamp
984        unsafe {
985            let stack = std::ptr::null_mut();
986            let stack = push(stack, Value::String(global_string("hello".to_owned())));
987            let stack = push(stack, Value::Int(2)); // start
988            let stack = push(stack, Value::Int(100)); // len (way too long)
989
990            let stack = string_substring(stack);
991
992            let (stack, result) = pop(stack);
993            assert_eq!(result, Value::String(global_string("llo".to_owned())));
994            assert!(stack.is_null());
995        }
996    }
997
998    #[test]
999    fn test_string_substring_beyond_end() {
1000        // Start beyond end - returns empty
1001        unsafe {
1002            let stack = std::ptr::null_mut();
1003            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1004            let stack = push(stack, Value::Int(10)); // start (beyond end)
1005            let stack = push(stack, Value::Int(3)); // len
1006
1007            let stack = string_substring(stack);
1008
1009            let (stack, result) = pop(stack);
1010            assert_eq!(result, Value::String(global_string("".to_owned())));
1011            assert!(stack.is_null());
1012        }
1013    }
1014
1015    #[test]
1016    fn test_char_to_string_ascii() {
1017        unsafe {
1018            let stack = std::ptr::null_mut();
1019            let stack = push(stack, Value::Int(65)); // 'A'
1020
1021            let stack = char_to_string(stack);
1022
1023            let (stack, result) = pop(stack);
1024            assert_eq!(result, Value::String(global_string("A".to_owned())));
1025            assert!(stack.is_null());
1026        }
1027    }
1028
1029    #[test]
1030    fn test_char_to_string_utf8() {
1031        unsafe {
1032            let stack = std::ptr::null_mut();
1033            let stack = push(stack, Value::Int(233)); // 'é' = U+00E9
1034
1035            let stack = char_to_string(stack);
1036
1037            let (stack, result) = pop(stack);
1038            assert_eq!(result, Value::String(global_string("é".to_owned())));
1039            assert!(stack.is_null());
1040        }
1041    }
1042
1043    #[test]
1044    fn test_char_to_string_newline() {
1045        unsafe {
1046            let stack = std::ptr::null_mut();
1047            let stack = push(stack, Value::Int(10)); // '\n'
1048
1049            let stack = char_to_string(stack);
1050
1051            let (stack, result) = pop(stack);
1052            assert_eq!(result, Value::String(global_string("\n".to_owned())));
1053            assert!(stack.is_null());
1054        }
1055    }
1056
1057    #[test]
1058    fn test_char_to_string_invalid() {
1059        unsafe {
1060            let stack = std::ptr::null_mut();
1061            let stack = push(stack, Value::Int(-1)); // Invalid
1062
1063            let stack = char_to_string(stack);
1064
1065            let (stack, result) = pop(stack);
1066            assert_eq!(result, Value::String(global_string("".to_owned())));
1067            assert!(stack.is_null());
1068        }
1069    }
1070
1071    #[test]
1072    fn test_string_find_found() {
1073        unsafe {
1074            let stack = std::ptr::null_mut();
1075            let stack = push(
1076                stack,
1077                Value::String(global_string("hello world".to_owned())),
1078            );
1079            let stack = push(stack, Value::String(global_string("world".to_owned())));
1080
1081            let stack = string_find(stack);
1082
1083            let (stack, result) = pop(stack);
1084            assert_eq!(result, Value::Int(6)); // "world" starts at index 6
1085            assert!(stack.is_null());
1086        }
1087    }
1088
1089    #[test]
1090    fn test_string_find_not_found() {
1091        unsafe {
1092            let stack = std::ptr::null_mut();
1093            let stack = push(
1094                stack,
1095                Value::String(global_string("hello world".to_owned())),
1096            );
1097            let stack = push(stack, Value::String(global_string("xyz".to_owned())));
1098
1099            let stack = string_find(stack);
1100
1101            let (stack, result) = pop(stack);
1102            assert_eq!(result, Value::Int(-1));
1103            assert!(stack.is_null());
1104        }
1105    }
1106
1107    #[test]
1108    fn test_string_find_first_match() {
1109        // Should return first occurrence
1110        unsafe {
1111            let stack = std::ptr::null_mut();
1112            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1113            let stack = push(stack, Value::String(global_string("l".to_owned())));
1114
1115            let stack = string_find(stack);
1116
1117            let (stack, result) = pop(stack);
1118            assert_eq!(result, Value::Int(2)); // First 'l' is at index 2
1119            assert!(stack.is_null());
1120        }
1121    }
1122
1123    #[test]
1124    fn test_string_find_utf8() {
1125        // Find in UTF-8 string - returns character index, not byte index
1126        unsafe {
1127            let stack = std::ptr::null_mut();
1128            let stack = push(
1129                stack,
1130                Value::String(global_string("héllo wörld".to_owned())),
1131            );
1132            let stack = push(stack, Value::String(global_string("wörld".to_owned())));
1133
1134            let stack = string_find(stack);
1135
1136            let (stack, result) = pop(stack);
1137            assert_eq!(result, Value::Int(6)); // Character index, not byte index
1138            assert!(stack.is_null());
1139        }
1140    }
1141}