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;
16use std::sync::Arc;
17
18/// Split a string on a delimiter
19///
20/// Stack effect: ( str delim -- Variant )
21///
22/// Returns a Variant containing the split parts as fields.
23///
24/// # Safety
25/// Stack must have two String values on top
26#[unsafe(no_mangle)]
27pub unsafe extern "C" fn patch_seq_string_split(stack: Stack) -> Stack {
28    use crate::value::VariantData;
29
30    assert!(!stack.is_null(), "string_split: stack is empty");
31
32    let (stack, delim_val) = unsafe { pop(stack) };
33    assert!(!stack.is_null(), "string_split: need two strings");
34    let (stack, str_val) = unsafe { pop(stack) };
35
36    match (str_val, delim_val) {
37        (Value::String(s), Value::String(d)) => {
38            // Split and collect into Value::String instances
39            let fields: Vec<Value> = s
40                .as_str()
41                .split(d.as_str())
42                .map(|part| Value::String(global_string(part.to_owned())))
43                .collect();
44
45            // Create a Variant with tag 0 and the split parts as fields
46            let variant = Value::Variant(Arc::new(VariantData::new(0, fields)));
47
48            unsafe { push(stack, variant) }
49        }
50        _ => panic!("string_split: expected two strings on stack"),
51    }
52}
53
54/// Check if a string is empty
55///
56/// Stack effect: ( str -- bool )
57///
58/// # Safety
59/// Stack must have a String value on top
60#[unsafe(no_mangle)]
61pub unsafe extern "C" fn patch_seq_string_empty(stack: Stack) -> Stack {
62    assert!(!stack.is_null(), "string_empty: stack is empty");
63
64    let (stack, value) = unsafe { pop(stack) };
65
66    match value {
67        Value::String(s) => {
68            let is_empty = s.as_str().is_empty();
69            let result = if is_empty { 1 } else { 0 };
70            unsafe { push(stack, Value::Int(result)) }
71        }
72        _ => panic!("string_empty: expected String on stack"),
73    }
74}
75
76/// Check if a string contains a substring
77///
78/// Stack effect: ( str substring -- bool )
79///
80/// # Safety
81/// Stack must have two String values on top
82#[unsafe(no_mangle)]
83pub unsafe extern "C" fn patch_seq_string_contains(stack: Stack) -> Stack {
84    assert!(!stack.is_null(), "string_contains: stack is empty");
85
86    let (stack, substring_val) = unsafe { pop(stack) };
87    assert!(!stack.is_null(), "string_contains: need two strings");
88    let (stack, str_val) = unsafe { pop(stack) };
89
90    match (str_val, substring_val) {
91        (Value::String(s), Value::String(sub)) => {
92            let contains = s.as_str().contains(sub.as_str());
93            let result = if contains { 1 } else { 0 };
94            unsafe { push(stack, Value::Int(result)) }
95        }
96        _ => panic!("string_contains: expected two strings on stack"),
97    }
98}
99
100/// Check if a string starts with a prefix
101///
102/// Stack effect: ( str prefix -- bool )
103///
104/// # Safety
105/// Stack must have two String values on top
106#[unsafe(no_mangle)]
107pub unsafe extern "C" fn patch_seq_string_starts_with(stack: Stack) -> Stack {
108    assert!(!stack.is_null(), "string_starts_with: stack is empty");
109
110    let (stack, prefix_val) = unsafe { pop(stack) };
111    assert!(!stack.is_null(), "string_starts_with: need two strings");
112    let (stack, str_val) = unsafe { pop(stack) };
113
114    match (str_val, prefix_val) {
115        (Value::String(s), Value::String(prefix)) => {
116            let starts = s.as_str().starts_with(prefix.as_str());
117            let result = if starts { 1 } else { 0 };
118            unsafe { push(stack, Value::Int(result)) }
119        }
120        _ => panic!("string_starts_with: expected two strings on stack"),
121    }
122}
123
124/// Concatenate two strings
125///
126/// Stack effect: ( str1 str2 -- result )
127///
128/// # Safety
129/// Stack must have two String values on top
130#[unsafe(no_mangle)]
131pub unsafe extern "C" fn patch_seq_string_concat(stack: Stack) -> Stack {
132    assert!(!stack.is_null(), "string_concat: stack is empty");
133
134    let (stack, str2_val) = unsafe { pop(stack) };
135    assert!(!stack.is_null(), "string_concat: need two strings");
136    let (stack, str1_val) = unsafe { pop(stack) };
137
138    match (str1_val, str2_val) {
139        (Value::String(s1), Value::String(s2)) => {
140            let result = format!("{}{}", s1.as_str(), s2.as_str());
141            unsafe { push(stack, Value::String(global_string(result))) }
142        }
143        _ => panic!("string_concat: expected two strings on stack"),
144    }
145}
146
147/// Get the length of a string in Unicode characters (code points)
148///
149/// Stack effect: ( str -- int )
150///
151/// Note: This returns character count, not byte count.
152/// For UTF-8 byte length (e.g., HTTP Content-Length), use `string-byte-length`.
153///
154/// # Safety
155/// Stack must have a String value on top
156#[unsafe(no_mangle)]
157pub unsafe extern "C" fn patch_seq_string_length(stack: Stack) -> Stack {
158    assert!(!stack.is_null(), "string_length: stack is empty");
159
160    let (stack, str_val) = unsafe { pop(stack) };
161
162    match str_val {
163        Value::String(s) => {
164            let len = s.as_str().chars().count() as i64;
165            unsafe { push(stack, Value::Int(len)) }
166        }
167        _ => panic!("string_length: expected String on stack"),
168    }
169}
170
171/// Get the byte length of a string (UTF-8 encoded)
172///
173/// Stack effect: ( str -- int )
174///
175/// Use this for HTTP Content-Length headers and buffer allocation.
176///
177/// # Safety
178/// Stack must have a String value on top
179#[unsafe(no_mangle)]
180pub unsafe extern "C" fn patch_seq_string_byte_length(stack: Stack) -> Stack {
181    assert!(!stack.is_null(), "string_byte_length: stack is empty");
182
183    let (stack, str_val) = unsafe { pop(stack) };
184
185    match str_val {
186        Value::String(s) => {
187            let len = s.as_str().len() as i64;
188            unsafe { push(stack, Value::Int(len)) }
189        }
190        _ => panic!("string_byte_length: expected String on stack"),
191    }
192}
193
194/// Get the Unicode code point at a character index
195///
196/// Stack effect: ( str int -- int )
197///
198/// Returns the code point value at the given character index.
199/// Returns -1 if index is out of bounds.
200///
201/// # Safety
202/// Stack must have a String and Int on top
203#[unsafe(no_mangle)]
204pub unsafe extern "C" fn patch_seq_string_char_at(stack: Stack) -> Stack {
205    assert!(!stack.is_null(), "string_char_at: stack is empty");
206
207    let (stack, index_val) = unsafe { pop(stack) };
208    assert!(!stack.is_null(), "string_char_at: need string and index");
209    let (stack, str_val) = unsafe { pop(stack) };
210
211    match (str_val, index_val) {
212        (Value::String(s), Value::Int(index)) => {
213            let result = if index < 0 {
214                -1
215            } else {
216                s.as_str()
217                    .chars()
218                    .nth(index as usize)
219                    .map(|c| c as i64)
220                    .unwrap_or(-1)
221            };
222            unsafe { push(stack, Value::Int(result)) }
223        }
224        _ => panic!("string_char_at: expected String and Int on stack"),
225    }
226}
227
228/// Extract a substring using character indices
229///
230/// Stack effect: ( str start len -- str )
231///
232/// Arguments:
233/// - str: The source string
234/// - start: Starting character index
235/// - len: Number of characters to extract
236///
237/// Edge cases:
238/// - Start beyond end: returns empty string
239/// - Length extends past end: clamps to available
240///
241/// # Safety
242/// Stack must have String, Int, Int on top
243#[unsafe(no_mangle)]
244pub unsafe extern "C" fn patch_seq_string_substring(stack: Stack) -> Stack {
245    assert!(!stack.is_null(), "string_substring: stack is empty");
246
247    let (stack, len_val) = unsafe { pop(stack) };
248    assert!(
249        !stack.is_null(),
250        "string_substring: need string, start, len"
251    );
252    let (stack, start_val) = unsafe { pop(stack) };
253    assert!(
254        !stack.is_null(),
255        "string_substring: need string, start, len"
256    );
257    let (stack, str_val) = unsafe { pop(stack) };
258
259    match (str_val, start_val, len_val) {
260        (Value::String(s), Value::Int(start), Value::Int(len)) => {
261            let result = if start < 0 || len < 0 {
262                String::new()
263            } else {
264                s.as_str()
265                    .chars()
266                    .skip(start as usize)
267                    .take(len as usize)
268                    .collect()
269            };
270            unsafe { push(stack, Value::String(global_string(result))) }
271        }
272        _ => panic!("string_substring: expected String, Int, Int on stack"),
273    }
274}
275
276/// Convert a Unicode code point to a single-character string
277///
278/// Stack effect: ( int -- str )
279///
280/// Creates a string containing the single character represented by the code point.
281/// Panics if the code point is invalid.
282///
283/// # Safety
284/// Stack must have an Int on top
285#[unsafe(no_mangle)]
286pub unsafe extern "C" fn patch_seq_char_to_string(stack: Stack) -> Stack {
287    assert!(!stack.is_null(), "char_to_string: stack is empty");
288
289    let (stack, code_point_val) = unsafe { pop(stack) };
290
291    match code_point_val {
292        Value::Int(code_point) => {
293            let result = if !(0..=0x10FFFF).contains(&code_point) {
294                // Invalid code point - return empty string
295                String::new()
296            } else {
297                match char::from_u32(code_point as u32) {
298                    Some(c) => c.to_string(),
299                    None => String::new(), // Invalid code point (e.g., surrogate)
300                }
301            };
302            unsafe { push(stack, Value::String(global_string(result))) }
303        }
304        _ => panic!("char_to_string: expected Int on stack"),
305    }
306}
307
308/// Find the first occurrence of a substring
309///
310/// Stack effect: ( str needle -- int )
311///
312/// Returns the character index of the first occurrence of needle in str.
313/// Returns -1 if not found.
314///
315/// # Safety
316/// Stack must have two Strings on top
317#[unsafe(no_mangle)]
318pub unsafe extern "C" fn patch_seq_string_find(stack: Stack) -> Stack {
319    assert!(!stack.is_null(), "string_find: stack is empty");
320
321    let (stack, needle_val) = unsafe { pop(stack) };
322    assert!(!stack.is_null(), "string_find: need string and needle");
323    let (stack, str_val) = unsafe { pop(stack) };
324
325    match (str_val, needle_val) {
326        (Value::String(haystack), Value::String(needle)) => {
327            let haystack_str = haystack.as_str();
328            let needle_str = needle.as_str();
329
330            // Find byte position then convert to character position
331            let result = match haystack_str.find(needle_str) {
332                Some(byte_pos) => {
333                    // Count characters up to byte_pos
334                    haystack_str[..byte_pos].chars().count() as i64
335                }
336                None => -1,
337            };
338            unsafe { push(stack, Value::Int(result)) }
339        }
340        _ => panic!("string_find: expected two Strings on stack"),
341    }
342}
343
344/// Trim whitespace from both ends of a string
345///
346/// Stack effect: ( str -- trimmed )
347///
348/// # Safety
349/// Stack must have a String value on top
350#[unsafe(no_mangle)]
351pub unsafe extern "C" fn patch_seq_string_trim(stack: Stack) -> Stack {
352    assert!(!stack.is_null(), "string_trim: stack is empty");
353
354    let (stack, str_val) = unsafe { pop(stack) };
355
356    match str_val {
357        Value::String(s) => {
358            let trimmed = s.as_str().trim();
359            unsafe { push(stack, Value::String(global_string(trimmed.to_owned()))) }
360        }
361        _ => panic!("string_trim: expected String on stack"),
362    }
363}
364
365/// Convert a string to uppercase
366///
367/// Stack effect: ( str -- upper )
368///
369/// # Safety
370/// Stack must have a String value on top
371#[unsafe(no_mangle)]
372pub unsafe extern "C" fn patch_seq_string_to_upper(stack: Stack) -> Stack {
373    assert!(!stack.is_null(), "string_to_upper: stack is empty");
374
375    let (stack, str_val) = unsafe { pop(stack) };
376
377    match str_val {
378        Value::String(s) => {
379            let upper = s.as_str().to_uppercase();
380            unsafe { push(stack, Value::String(global_string(upper))) }
381        }
382        _ => panic!("string_to_upper: expected String on stack"),
383    }
384}
385
386/// Convert a string to lowercase
387///
388/// Stack effect: ( str -- lower )
389///
390/// # Safety
391/// Stack must have a String value on top
392#[unsafe(no_mangle)]
393pub unsafe extern "C" fn patch_seq_string_to_lower(stack: Stack) -> Stack {
394    assert!(!stack.is_null(), "string_to_lower: stack is empty");
395
396    let (stack, str_val) = unsafe { pop(stack) };
397
398    match str_val {
399        Value::String(s) => {
400            let lower = s.as_str().to_lowercase();
401            unsafe { push(stack, Value::String(global_string(lower))) }
402        }
403        _ => panic!("string_to_lower: expected String on stack"),
404    }
405}
406
407/// Check if two strings are equal
408///
409/// Stack effect: ( str1 str2 -- bool )
410///
411/// # Safety
412/// Stack must have two String values on top
413#[unsafe(no_mangle)]
414pub unsafe extern "C" fn patch_seq_string_equal(stack: Stack) -> Stack {
415    assert!(!stack.is_null(), "string_equal: stack is empty");
416
417    let (stack, str2_val) = unsafe { pop(stack) };
418    assert!(!stack.is_null(), "string_equal: need two strings");
419    let (stack, str1_val) = unsafe { pop(stack) };
420
421    match (str1_val, str2_val) {
422        (Value::String(s1), Value::String(s2)) => {
423            let equal = s1.as_str() == s2.as_str();
424            let result = if equal { 1 } else { 0 };
425            unsafe { push(stack, Value::Int(result)) }
426        }
427        _ => panic!("string_equal: expected two strings on stack"),
428    }
429}
430
431/// Escape a string for JSON output
432///
433/// Stack effect: ( str -- str )
434///
435/// Escapes special characters according to JSON spec:
436/// - `"` → `\"`
437/// - `\` → `\\`
438/// - newline → `\n`
439/// - carriage return → `\r`
440/// - tab → `\t`
441/// - backspace → `\b`
442/// - form feed → `\f`
443/// - Control characters (0x00-0x1F) → `\uXXXX`
444///
445/// # Safety
446/// Stack must have a String value on top
447#[unsafe(no_mangle)]
448pub unsafe extern "C" fn patch_seq_json_escape(stack: Stack) -> Stack {
449    assert!(!stack.is_null(), "json_escape: stack is empty");
450
451    let (stack, value) = unsafe { pop(stack) };
452
453    match value {
454        Value::String(s) => {
455            let input = s.as_str();
456            let mut result = String::with_capacity(input.len() + 16);
457
458            for ch in input.chars() {
459                match ch {
460                    '"' => result.push_str("\\\""),
461                    '\\' => result.push_str("\\\\"),
462                    '\n' => result.push_str("\\n"),
463                    '\r' => result.push_str("\\r"),
464                    '\t' => result.push_str("\\t"),
465                    '\x08' => result.push_str("\\b"), // backspace
466                    '\x0C' => result.push_str("\\f"), // form feed
467                    // Control characters (0x00-0x1F except those handled above)
468                    // RFC 8259 uses uppercase hex in examples for Unicode escapes
469                    c if c.is_control() => {
470                        result.push_str(&format!("\\u{:04X}", c as u32));
471                    }
472                    c => result.push(c),
473                }
474            }
475
476            unsafe { push(stack, Value::String(global_string(result))) }
477        }
478        _ => panic!("json_escape: expected String on stack"),
479    }
480}
481
482/// Convert String to Int: ( String -- Int Int )
483/// Returns the parsed int and 1 on success, or 0 and 0 on failure.
484/// Accepts integers in range [-9223372036854775808, 9223372036854775807] (i64).
485/// Trims leading/trailing whitespace before parsing.
486/// Leading zeros are accepted (e.g., "007" parses to 7).
487///
488/// # Safety
489/// Stack must have a String value on top
490#[unsafe(no_mangle)]
491pub unsafe extern "C" fn patch_seq_string_to_int(stack: Stack) -> Stack {
492    assert!(!stack.is_null(), "string->int: stack is empty");
493    let (stack, val) = unsafe { pop(stack) };
494
495    match val {
496        Value::String(s) => match s.as_str().trim().parse::<i64>() {
497            Ok(i) => {
498                let stack = unsafe { push(stack, Value::Int(i)) };
499                unsafe { push(stack, Value::Int(1)) }
500            }
501            Err(_) => {
502                let stack = unsafe { push(stack, Value::Int(0)) };
503                unsafe { push(stack, Value::Int(0)) }
504            }
505        },
506        _ => panic!("string->int: expected String on stack"),
507    }
508}
509
510/// Remove trailing newline characters from a string
511///
512/// Stack effect: ( str -- str )
513///
514/// Removes trailing \n or \r\n (handles both Unix and Windows line endings).
515/// If the string doesn't end with a newline, returns it unchanged.
516///
517/// # Safety
518/// Stack must have a String value on top
519#[unsafe(no_mangle)]
520pub unsafe extern "C" fn patch_seq_string_chomp(stack: Stack) -> Stack {
521    assert!(!stack.is_null(), "string_chomp: stack is empty");
522
523    let (stack, str_val) = unsafe { pop(stack) };
524
525    match str_val {
526        Value::String(s) => {
527            let mut result = s.as_str().to_owned();
528            if result.ends_with('\n') {
529                result.pop();
530                if result.ends_with('\r') {
531                    result.pop();
532                }
533            }
534            unsafe { push(stack, Value::String(global_string(result))) }
535        }
536        _ => panic!("string_chomp: expected String on stack"),
537    }
538}
539
540// Public re-exports with short names for internal use
541pub use patch_seq_char_to_string as char_to_string;
542pub use patch_seq_json_escape as json_escape;
543pub use patch_seq_string_byte_length as string_byte_length;
544pub use patch_seq_string_char_at as string_char_at;
545pub use patch_seq_string_chomp as string_chomp;
546pub use patch_seq_string_concat as string_concat;
547pub use patch_seq_string_contains as string_contains;
548pub use patch_seq_string_empty as string_empty;
549pub use patch_seq_string_equal as string_equal;
550pub use patch_seq_string_find as string_find;
551pub use patch_seq_string_length as string_length;
552pub use patch_seq_string_split as string_split;
553pub use patch_seq_string_starts_with as string_starts_with;
554pub use patch_seq_string_substring as string_substring;
555pub use patch_seq_string_to_int as string_to_int;
556pub use patch_seq_string_to_lower as string_to_lower;
557pub use patch_seq_string_to_upper as string_to_upper;
558pub use patch_seq_string_trim as string_trim;
559
560// ============================================================================
561// FFI String Helpers
562// ============================================================================
563
564/// Convert a Seq String on the stack to a null-terminated C string.
565///
566/// The returned pointer must be freed by the caller using free().
567/// This peeks the string from the stack (caller pops after use).
568///
569/// Stack effect: ( String -- ) returns ptr to C string
570///
571/// # Memory Safety
572///
573/// The returned C string is a **completely independent copy** allocated via
574/// `malloc()`. It has no connection to Seq's memory management:
575///
576/// - The Seq String on the stack remains valid and unchanged
577/// - The returned pointer is owned by the C world and must be freed with `free()`
578/// - Even if the Seq String is garbage collected, the C string remains valid
579/// - Multiple calls with the same Seq String produce independent C strings
580///
581/// This design ensures FFI calls cannot cause use-after-free or double-free
582/// bugs between Seq and C code.
583///
584/// # Safety
585/// Stack must have a String value on top. The unused second argument
586/// exists for future extension (passing output buffer).
587#[unsafe(no_mangle)]
588pub unsafe extern "C" fn patch_seq_string_to_cstring(stack: Stack, _out: *mut u8) -> *mut u8 {
589    assert!(!stack.is_null(), "string_to_cstring: stack is empty");
590
591    // Peek the string value (don't pop - caller will pop after we return)
592    let node = unsafe { &*stack };
593    match &node.value {
594        Value::String(s) => {
595            let bytes = s.as_str().as_bytes();
596            let len = bytes.len();
597
598            // Guard against overflow: len + 1 for null terminator
599            let alloc_size = len.checked_add(1).unwrap_or_else(|| {
600                panic!(
601                    "string_to_cstring: string too large for C conversion (len={})",
602                    len
603                )
604            });
605
606            // Allocate space for string + null terminator
607            let ptr = unsafe { libc::malloc(alloc_size) as *mut u8 };
608            if ptr.is_null() {
609                panic!("string_to_cstring: malloc failed");
610            }
611
612            // Copy string data
613            unsafe {
614                std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, len);
615                // Add null terminator
616                *ptr.add(len) = 0;
617            }
618
619            ptr
620        }
621        _ => panic!("string_to_cstring: expected String on stack"),
622    }
623}
624
625/// Convert a null-terminated C string to a Seq String and push onto stack.
626///
627/// The C string is NOT freed by this function.
628///
629/// Stack effect: ( -- String )
630///
631/// # Safety
632/// cstr must be a valid null-terminated C string.
633#[unsafe(no_mangle)]
634pub unsafe extern "C" fn patch_seq_cstring_to_string(stack: Stack, cstr: *const u8) -> Stack {
635    if cstr.is_null() {
636        // NULL string - push empty string
637        return unsafe { push(stack, Value::String(global_string(String::new()))) };
638    }
639
640    // Get string length
641    let len = unsafe { libc::strlen(cstr as *const libc::c_char) };
642
643    // Create Rust string from C string
644    let slice = unsafe { std::slice::from_raw_parts(cstr, len) };
645    let s = String::from_utf8_lossy(slice).into_owned();
646
647    unsafe { push(stack, Value::String(global_string(s))) }
648}
649
650#[cfg(test)]
651mod tests {
652    use super::*;
653
654    #[test]
655    fn test_string_split_simple() {
656        unsafe {
657            let stack = std::ptr::null_mut();
658            let stack = push(stack, Value::String(global_string("a b c".to_owned())));
659            let stack = push(stack, Value::String(global_string(" ".to_owned())));
660
661            let stack = string_split(stack);
662
663            // Should have a Variant with 3 fields: "a", "b", "c"
664            let (stack, result) = pop(stack);
665            match result {
666                Value::Variant(v) => {
667                    assert_eq!(v.tag, 0);
668                    assert_eq!(v.fields.len(), 3);
669                    assert_eq!(v.fields[0], Value::String(global_string("a".to_owned())));
670                    assert_eq!(v.fields[1], Value::String(global_string("b".to_owned())));
671                    assert_eq!(v.fields[2], Value::String(global_string("c".to_owned())));
672                }
673                _ => panic!("Expected Variant, got {:?}", result),
674            }
675
676            assert!(stack.is_null());
677        }
678    }
679
680    #[test]
681    fn test_string_split_empty() {
682        unsafe {
683            let stack = std::ptr::null_mut();
684            let stack = push(stack, Value::String(global_string("".to_owned())));
685            let stack = push(stack, Value::String(global_string(" ".to_owned())));
686
687            let stack = string_split(stack);
688
689            // Empty string splits to one empty part
690            let (stack, result) = pop(stack);
691            match result {
692                Value::Variant(v) => {
693                    assert_eq!(v.tag, 0);
694                    assert_eq!(v.fields.len(), 1);
695                    assert_eq!(v.fields[0], Value::String(global_string("".to_owned())));
696                }
697                _ => panic!("Expected Variant, got {:?}", result),
698            }
699
700            assert!(stack.is_null());
701        }
702    }
703
704    #[test]
705    fn test_string_empty_true() {
706        unsafe {
707            let stack = std::ptr::null_mut();
708            let stack = push(stack, Value::String(global_string("".to_owned())));
709
710            let stack = string_empty(stack);
711
712            let (stack, result) = pop(stack);
713            assert_eq!(result, Value::Int(1));
714            assert!(stack.is_null());
715        }
716    }
717
718    #[test]
719    fn test_string_empty_false() {
720        unsafe {
721            let stack = std::ptr::null_mut();
722            let stack = push(stack, Value::String(global_string("hello".to_owned())));
723
724            let stack = string_empty(stack);
725
726            let (stack, result) = pop(stack);
727            assert_eq!(result, Value::Int(0));
728            assert!(stack.is_null());
729        }
730    }
731
732    #[test]
733    fn test_string_contains_true() {
734        unsafe {
735            let stack = std::ptr::null_mut();
736            let stack = push(
737                stack,
738                Value::String(global_string("hello world".to_owned())),
739            );
740            let stack = push(stack, Value::String(global_string("world".to_owned())));
741
742            let stack = string_contains(stack);
743
744            let (stack, result) = pop(stack);
745            assert_eq!(result, Value::Int(1));
746            assert!(stack.is_null());
747        }
748    }
749
750    #[test]
751    fn test_string_contains_false() {
752        unsafe {
753            let stack = std::ptr::null_mut();
754            let stack = push(
755                stack,
756                Value::String(global_string("hello world".to_owned())),
757            );
758            let stack = push(stack, Value::String(global_string("foo".to_owned())));
759
760            let stack = string_contains(stack);
761
762            let (stack, result) = pop(stack);
763            assert_eq!(result, Value::Int(0));
764            assert!(stack.is_null());
765        }
766    }
767
768    #[test]
769    fn test_string_starts_with_true() {
770        unsafe {
771            let stack = std::ptr::null_mut();
772            let stack = push(
773                stack,
774                Value::String(global_string("hello world".to_owned())),
775            );
776            let stack = push(stack, Value::String(global_string("hello".to_owned())));
777
778            let stack = string_starts_with(stack);
779
780            let (stack, result) = pop(stack);
781            assert_eq!(result, Value::Int(1));
782            assert!(stack.is_null());
783        }
784    }
785
786    #[test]
787    fn test_string_starts_with_false() {
788        unsafe {
789            let stack = std::ptr::null_mut();
790            let stack = push(
791                stack,
792                Value::String(global_string("hello world".to_owned())),
793            );
794            let stack = push(stack, Value::String(global_string("world".to_owned())));
795
796            let stack = string_starts_with(stack);
797
798            let (stack, result) = pop(stack);
799            assert_eq!(result, Value::Int(0));
800            assert!(stack.is_null());
801        }
802    }
803
804    #[test]
805    fn test_http_request_line_parsing() {
806        // Real-world use case: Parse "GET /api/users HTTP/1.1"
807        unsafe {
808            let stack = std::ptr::null_mut();
809            let stack = push(
810                stack,
811                Value::String(global_string("GET /api/users HTTP/1.1".to_owned())),
812            );
813            let stack = push(stack, Value::String(global_string(" ".to_owned())));
814
815            let stack = string_split(stack);
816
817            // Should have a Variant with 3 fields: "GET", "/api/users", "HTTP/1.1"
818            let (stack, result) = pop(stack);
819            match result {
820                Value::Variant(v) => {
821                    assert_eq!(v.tag, 0);
822                    assert_eq!(v.fields.len(), 3);
823                    assert_eq!(v.fields[0], Value::String(global_string("GET".to_owned())));
824                    assert_eq!(
825                        v.fields[1],
826                        Value::String(global_string("/api/users".to_owned()))
827                    );
828                    assert_eq!(
829                        v.fields[2],
830                        Value::String(global_string("HTTP/1.1".to_owned()))
831                    );
832                }
833                _ => panic!("Expected Variant, got {:?}", result),
834            }
835
836            assert!(stack.is_null());
837        }
838    }
839
840    #[test]
841    fn test_path_routing() {
842        // Real-world use case: Check if path starts with "/api/"
843        unsafe {
844            let stack = std::ptr::null_mut();
845            let stack = push(stack, Value::String(global_string("/api/users".to_owned())));
846            let stack = push(stack, Value::String(global_string("/api/".to_owned())));
847
848            let stack = string_starts_with(stack);
849
850            let (stack, result) = pop(stack);
851            assert_eq!(result, Value::Int(1));
852            assert!(stack.is_null());
853        }
854    }
855
856    #[test]
857    fn test_string_concat() {
858        unsafe {
859            let stack = std::ptr::null_mut();
860            let stack = push(stack, Value::String(global_string("Hello, ".to_owned())));
861            let stack = push(stack, Value::String(global_string("World!".to_owned())));
862
863            let stack = string_concat(stack);
864
865            let (stack, result) = pop(stack);
866            assert_eq!(
867                result,
868                Value::String(global_string("Hello, World!".to_owned()))
869            );
870            assert!(stack.is_null());
871        }
872    }
873
874    #[test]
875    fn test_string_length() {
876        unsafe {
877            let stack = std::ptr::null_mut();
878            let stack = push(stack, Value::String(global_string("Hello".to_owned())));
879
880            let stack = string_length(stack);
881
882            let (stack, result) = pop(stack);
883            assert_eq!(result, Value::Int(5));
884            assert!(stack.is_null());
885        }
886    }
887
888    #[test]
889    fn test_string_length_empty() {
890        unsafe {
891            let stack = std::ptr::null_mut();
892            let stack = push(stack, Value::String(global_string("".to_owned())));
893
894            let stack = string_length(stack);
895
896            let (stack, result) = pop(stack);
897            assert_eq!(result, Value::Int(0));
898            assert!(stack.is_null());
899        }
900    }
901
902    #[test]
903    fn test_string_trim() {
904        unsafe {
905            let stack = std::ptr::null_mut();
906            let stack = push(
907                stack,
908                Value::String(global_string("  Hello, World!  ".to_owned())),
909            );
910
911            let stack = string_trim(stack);
912
913            let (stack, result) = pop(stack);
914            assert_eq!(
915                result,
916                Value::String(global_string("Hello, World!".to_owned()))
917            );
918            assert!(stack.is_null());
919        }
920    }
921
922    #[test]
923    fn test_string_to_upper() {
924        unsafe {
925            let stack = std::ptr::null_mut();
926            let stack = push(
927                stack,
928                Value::String(global_string("Hello, World!".to_owned())),
929            );
930
931            let stack = string_to_upper(stack);
932
933            let (stack, result) = pop(stack);
934            assert_eq!(
935                result,
936                Value::String(global_string("HELLO, WORLD!".to_owned()))
937            );
938            assert!(stack.is_null());
939        }
940    }
941
942    #[test]
943    fn test_string_to_lower() {
944        unsafe {
945            let stack = std::ptr::null_mut();
946            let stack = push(
947                stack,
948                Value::String(global_string("Hello, World!".to_owned())),
949            );
950
951            let stack = string_to_lower(stack);
952
953            let (stack, result) = pop(stack);
954            assert_eq!(
955                result,
956                Value::String(global_string("hello, world!".to_owned()))
957            );
958            assert!(stack.is_null());
959        }
960    }
961
962    #[test]
963    fn test_http_header_content_length() {
964        // Real-world use case: Build "Content-Length: 42" header
965        unsafe {
966            let stack = std::ptr::null_mut();
967            let stack = push(
968                stack,
969                Value::String(global_string("Content-Length: ".to_owned())),
970            );
971            let stack = push(stack, Value::String(global_string("42".to_owned())));
972
973            let stack = string_concat(stack);
974
975            let (stack, result) = pop(stack);
976            assert_eq!(
977                result,
978                Value::String(global_string("Content-Length: 42".to_owned()))
979            );
980            assert!(stack.is_null());
981        }
982    }
983
984    #[test]
985    fn test_string_equal_true() {
986        unsafe {
987            let stack = std::ptr::null_mut();
988            let stack = push(stack, Value::String(global_string("hello".to_owned())));
989            let stack = push(stack, Value::String(global_string("hello".to_owned())));
990
991            let stack = string_equal(stack);
992
993            let (stack, result) = pop(stack);
994            assert_eq!(result, Value::Int(1));
995            assert!(stack.is_null());
996        }
997    }
998
999    #[test]
1000    fn test_string_equal_false() {
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::String(global_string("world".to_owned())));
1005
1006            let stack = string_equal(stack);
1007
1008            let (stack, result) = pop(stack);
1009            assert_eq!(result, Value::Int(0));
1010            assert!(stack.is_null());
1011        }
1012    }
1013
1014    #[test]
1015    fn test_string_equal_empty_strings() {
1016        unsafe {
1017            let stack = std::ptr::null_mut();
1018            let stack = push(stack, Value::String(global_string("".to_owned())));
1019            let stack = push(stack, Value::String(global_string("".to_owned())));
1020
1021            let stack = string_equal(stack);
1022
1023            let (stack, result) = pop(stack);
1024            assert_eq!(result, Value::Int(1));
1025            assert!(stack.is_null());
1026        }
1027    }
1028
1029    // UTF-8 String Primitives Tests
1030
1031    #[test]
1032    fn test_string_length_utf8() {
1033        // "héllo" has 5 characters but 6 bytes (é is 2 bytes in UTF-8)
1034        unsafe {
1035            let stack = std::ptr::null_mut();
1036            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
1037
1038            let stack = string_length(stack);
1039
1040            let (stack, result) = pop(stack);
1041            assert_eq!(result, Value::Int(5)); // Characters, not bytes
1042            assert!(stack.is_null());
1043        }
1044    }
1045
1046    #[test]
1047    fn test_string_length_emoji() {
1048        // Emoji is one code point but multiple bytes
1049        unsafe {
1050            let stack = std::ptr::null_mut();
1051            let stack = push(stack, Value::String(global_string("hi🎉".to_owned())));
1052
1053            let stack = string_length(stack);
1054
1055            let (stack, result) = pop(stack);
1056            assert_eq!(result, Value::Int(3)); // 'h', 'i', and emoji
1057            assert!(stack.is_null());
1058        }
1059    }
1060
1061    #[test]
1062    fn test_string_byte_length_ascii() {
1063        unsafe {
1064            let stack = std::ptr::null_mut();
1065            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1066
1067            let stack = string_byte_length(stack);
1068
1069            let (stack, result) = pop(stack);
1070            assert_eq!(result, Value::Int(5)); // Same as char length for ASCII
1071            assert!(stack.is_null());
1072        }
1073    }
1074
1075    #[test]
1076    fn test_string_byte_length_utf8() {
1077        // "héllo" has 5 characters but 6 bytes
1078        unsafe {
1079            let stack = std::ptr::null_mut();
1080            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
1081
1082            let stack = string_byte_length(stack);
1083
1084            let (stack, result) = pop(stack);
1085            assert_eq!(result, Value::Int(6)); // Bytes, not characters
1086            assert!(stack.is_null());
1087        }
1088    }
1089
1090    #[test]
1091    fn test_string_char_at_ascii() {
1092        unsafe {
1093            let stack = std::ptr::null_mut();
1094            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1095            let stack = push(stack, Value::Int(0));
1096
1097            let stack = string_char_at(stack);
1098
1099            let (stack, result) = pop(stack);
1100            assert_eq!(result, Value::Int(104)); // 'h' = 104
1101            assert!(stack.is_null());
1102        }
1103    }
1104
1105    #[test]
1106    fn test_string_char_at_utf8() {
1107        // Get the é character at index 1 in "héllo"
1108        unsafe {
1109            let stack = std::ptr::null_mut();
1110            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
1111            let stack = push(stack, Value::Int(1));
1112
1113            let stack = string_char_at(stack);
1114
1115            let (stack, result) = pop(stack);
1116            assert_eq!(result, Value::Int(233)); // 'é' = U+00E9 = 233
1117            assert!(stack.is_null());
1118        }
1119    }
1120
1121    #[test]
1122    fn test_string_char_at_out_of_bounds() {
1123        unsafe {
1124            let stack = std::ptr::null_mut();
1125            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1126            let stack = push(stack, Value::Int(10)); // Out of bounds
1127
1128            let stack = string_char_at(stack);
1129
1130            let (stack, result) = pop(stack);
1131            assert_eq!(result, Value::Int(-1));
1132            assert!(stack.is_null());
1133        }
1134    }
1135
1136    #[test]
1137    fn test_string_char_at_negative() {
1138        unsafe {
1139            let stack = std::ptr::null_mut();
1140            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1141            let stack = push(stack, Value::Int(-1));
1142
1143            let stack = string_char_at(stack);
1144
1145            let (stack, result) = pop(stack);
1146            assert_eq!(result, Value::Int(-1));
1147            assert!(stack.is_null());
1148        }
1149    }
1150
1151    #[test]
1152    fn test_string_substring_simple() {
1153        unsafe {
1154            let stack = std::ptr::null_mut();
1155            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1156            let stack = push(stack, Value::Int(1)); // start
1157            let stack = push(stack, Value::Int(3)); // len
1158
1159            let stack = string_substring(stack);
1160
1161            let (stack, result) = pop(stack);
1162            assert_eq!(result, Value::String(global_string("ell".to_owned())));
1163            assert!(stack.is_null());
1164        }
1165    }
1166
1167    #[test]
1168    fn test_string_substring_utf8() {
1169        // "héllo" - get "éll" (characters 1-3)
1170        unsafe {
1171            let stack = std::ptr::null_mut();
1172            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
1173            let stack = push(stack, Value::Int(1)); // start
1174            let stack = push(stack, Value::Int(3)); // len
1175
1176            let stack = string_substring(stack);
1177
1178            let (stack, result) = pop(stack);
1179            assert_eq!(result, Value::String(global_string("éll".to_owned())));
1180            assert!(stack.is_null());
1181        }
1182    }
1183
1184    #[test]
1185    fn test_string_substring_clamp() {
1186        // Request more than available - should clamp
1187        unsafe {
1188            let stack = std::ptr::null_mut();
1189            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1190            let stack = push(stack, Value::Int(2)); // start
1191            let stack = push(stack, Value::Int(100)); // len (way too long)
1192
1193            let stack = string_substring(stack);
1194
1195            let (stack, result) = pop(stack);
1196            assert_eq!(result, Value::String(global_string("llo".to_owned())));
1197            assert!(stack.is_null());
1198        }
1199    }
1200
1201    #[test]
1202    fn test_string_substring_beyond_end() {
1203        // Start beyond end - returns empty
1204        unsafe {
1205            let stack = std::ptr::null_mut();
1206            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1207            let stack = push(stack, Value::Int(10)); // start (beyond end)
1208            let stack = push(stack, Value::Int(3)); // len
1209
1210            let stack = string_substring(stack);
1211
1212            let (stack, result) = pop(stack);
1213            assert_eq!(result, Value::String(global_string("".to_owned())));
1214            assert!(stack.is_null());
1215        }
1216    }
1217
1218    #[test]
1219    fn test_char_to_string_ascii() {
1220        unsafe {
1221            let stack = std::ptr::null_mut();
1222            let stack = push(stack, Value::Int(65)); // 'A'
1223
1224            let stack = char_to_string(stack);
1225
1226            let (stack, result) = pop(stack);
1227            assert_eq!(result, Value::String(global_string("A".to_owned())));
1228            assert!(stack.is_null());
1229        }
1230    }
1231
1232    #[test]
1233    fn test_char_to_string_utf8() {
1234        unsafe {
1235            let stack = std::ptr::null_mut();
1236            let stack = push(stack, Value::Int(233)); // 'é' = U+00E9
1237
1238            let stack = char_to_string(stack);
1239
1240            let (stack, result) = pop(stack);
1241            assert_eq!(result, Value::String(global_string("é".to_owned())));
1242            assert!(stack.is_null());
1243        }
1244    }
1245
1246    #[test]
1247    fn test_char_to_string_newline() {
1248        unsafe {
1249            let stack = std::ptr::null_mut();
1250            let stack = push(stack, Value::Int(10)); // '\n'
1251
1252            let stack = char_to_string(stack);
1253
1254            let (stack, result) = pop(stack);
1255            assert_eq!(result, Value::String(global_string("\n".to_owned())));
1256            assert!(stack.is_null());
1257        }
1258    }
1259
1260    #[test]
1261    fn test_char_to_string_invalid() {
1262        unsafe {
1263            let stack = std::ptr::null_mut();
1264            let stack = push(stack, Value::Int(-1)); // Invalid
1265
1266            let stack = char_to_string(stack);
1267
1268            let (stack, result) = pop(stack);
1269            assert_eq!(result, Value::String(global_string("".to_owned())));
1270            assert!(stack.is_null());
1271        }
1272    }
1273
1274    #[test]
1275    fn test_string_find_found() {
1276        unsafe {
1277            let stack = std::ptr::null_mut();
1278            let stack = push(
1279                stack,
1280                Value::String(global_string("hello world".to_owned())),
1281            );
1282            let stack = push(stack, Value::String(global_string("world".to_owned())));
1283
1284            let stack = string_find(stack);
1285
1286            let (stack, result) = pop(stack);
1287            assert_eq!(result, Value::Int(6)); // "world" starts at index 6
1288            assert!(stack.is_null());
1289        }
1290    }
1291
1292    #[test]
1293    fn test_string_find_not_found() {
1294        unsafe {
1295            let stack = std::ptr::null_mut();
1296            let stack = push(
1297                stack,
1298                Value::String(global_string("hello world".to_owned())),
1299            );
1300            let stack = push(stack, Value::String(global_string("xyz".to_owned())));
1301
1302            let stack = string_find(stack);
1303
1304            let (stack, result) = pop(stack);
1305            assert_eq!(result, Value::Int(-1));
1306            assert!(stack.is_null());
1307        }
1308    }
1309
1310    #[test]
1311    fn test_string_find_first_match() {
1312        // Should return first occurrence
1313        unsafe {
1314            let stack = std::ptr::null_mut();
1315            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1316            let stack = push(stack, Value::String(global_string("l".to_owned())));
1317
1318            let stack = string_find(stack);
1319
1320            let (stack, result) = pop(stack);
1321            assert_eq!(result, Value::Int(2)); // First 'l' is at index 2
1322            assert!(stack.is_null());
1323        }
1324    }
1325
1326    #[test]
1327    fn test_string_find_utf8() {
1328        // Find in UTF-8 string - returns character index, not byte index
1329        unsafe {
1330            let stack = std::ptr::null_mut();
1331            let stack = push(
1332                stack,
1333                Value::String(global_string("héllo wörld".to_owned())),
1334            );
1335            let stack = push(stack, Value::String(global_string("wörld".to_owned())));
1336
1337            let stack = string_find(stack);
1338
1339            let (stack, result) = pop(stack);
1340            assert_eq!(result, Value::Int(6)); // Character index, not byte index
1341            assert!(stack.is_null());
1342        }
1343    }
1344
1345    // JSON Escape Tests
1346
1347    #[test]
1348    fn test_json_escape_quotes() {
1349        unsafe {
1350            let stack = std::ptr::null_mut();
1351            let stack = push(
1352                stack,
1353                Value::String(global_string("hello \"world\"".to_owned())),
1354            );
1355
1356            let stack = json_escape(stack);
1357
1358            let (stack, result) = pop(stack);
1359            assert_eq!(
1360                result,
1361                Value::String(global_string("hello \\\"world\\\"".to_owned()))
1362            );
1363            assert!(stack.is_null());
1364        }
1365    }
1366
1367    #[test]
1368    fn test_json_escape_backslash() {
1369        unsafe {
1370            let stack = std::ptr::null_mut();
1371            let stack = push(
1372                stack,
1373                Value::String(global_string("path\\to\\file".to_owned())),
1374            );
1375
1376            let stack = json_escape(stack);
1377
1378            let (stack, result) = pop(stack);
1379            assert_eq!(
1380                result,
1381                Value::String(global_string("path\\\\to\\\\file".to_owned()))
1382            );
1383            assert!(stack.is_null());
1384        }
1385    }
1386
1387    #[test]
1388    fn test_json_escape_newline_tab() {
1389        unsafe {
1390            let stack = std::ptr::null_mut();
1391            let stack = push(
1392                stack,
1393                Value::String(global_string("line1\nline2\ttabbed".to_owned())),
1394            );
1395
1396            let stack = json_escape(stack);
1397
1398            let (stack, result) = pop(stack);
1399            assert_eq!(
1400                result,
1401                Value::String(global_string("line1\\nline2\\ttabbed".to_owned()))
1402            );
1403            assert!(stack.is_null());
1404        }
1405    }
1406
1407    #[test]
1408    fn test_json_escape_carriage_return() {
1409        unsafe {
1410            let stack = std::ptr::null_mut();
1411            let stack = push(
1412                stack,
1413                Value::String(global_string("line1\r\nline2".to_owned())),
1414            );
1415
1416            let stack = json_escape(stack);
1417
1418            let (stack, result) = pop(stack);
1419            assert_eq!(
1420                result,
1421                Value::String(global_string("line1\\r\\nline2".to_owned()))
1422            );
1423            assert!(stack.is_null());
1424        }
1425    }
1426
1427    #[test]
1428    fn test_json_escape_control_chars() {
1429        unsafe {
1430            let stack = std::ptr::null_mut();
1431            // Test backspace (0x08) and form feed (0x0C)
1432            let stack = push(
1433                stack,
1434                Value::String(global_string("a\x08b\x0Cc".to_owned())),
1435            );
1436
1437            let stack = json_escape(stack);
1438
1439            let (stack, result) = pop(stack);
1440            assert_eq!(result, Value::String(global_string("a\\bb\\fc".to_owned())));
1441            assert!(stack.is_null());
1442        }
1443    }
1444
1445    #[test]
1446    fn test_json_escape_unicode_control() {
1447        unsafe {
1448            let stack = std::ptr::null_mut();
1449            // Test null character (0x00) - should be escaped as \u0000 (uppercase hex per RFC 8259)
1450            let stack = push(stack, Value::String(global_string("a\x00b".to_owned())));
1451
1452            let stack = json_escape(stack);
1453
1454            let (stack, result) = pop(stack);
1455            assert_eq!(result, Value::String(global_string("a\\u0000b".to_owned())));
1456            assert!(stack.is_null());
1457        }
1458    }
1459
1460    #[test]
1461    fn test_json_escape_mixed_special_chars() {
1462        // Test combination of multiple special characters
1463        unsafe {
1464            let stack = std::ptr::null_mut();
1465            let stack = push(
1466                stack,
1467                Value::String(global_string("Line 1\nLine \"2\"\ttab\r\n".to_owned())),
1468            );
1469
1470            let stack = json_escape(stack);
1471
1472            let (stack, result) = pop(stack);
1473            assert_eq!(
1474                result,
1475                Value::String(global_string(
1476                    "Line 1\\nLine \\\"2\\\"\\ttab\\r\\n".to_owned()
1477                ))
1478            );
1479            assert!(stack.is_null());
1480        }
1481    }
1482
1483    #[test]
1484    fn test_json_escape_no_change() {
1485        // Normal string without special chars should pass through unchanged
1486        unsafe {
1487            let stack = std::ptr::null_mut();
1488            let stack = push(
1489                stack,
1490                Value::String(global_string("Hello, World!".to_owned())),
1491            );
1492
1493            let stack = json_escape(stack);
1494
1495            let (stack, result) = pop(stack);
1496            assert_eq!(
1497                result,
1498                Value::String(global_string("Hello, World!".to_owned()))
1499            );
1500            assert!(stack.is_null());
1501        }
1502    }
1503
1504    #[test]
1505    fn test_json_escape_empty_string() {
1506        unsafe {
1507            let stack = std::ptr::null_mut();
1508            let stack = push(stack, Value::String(global_string("".to_owned())));
1509
1510            let stack = json_escape(stack);
1511
1512            let (stack, result) = pop(stack);
1513            assert_eq!(result, Value::String(global_string("".to_owned())));
1514            assert!(stack.is_null());
1515        }
1516    }
1517
1518    // string->int tests
1519
1520    #[test]
1521    fn test_string_to_int_success() {
1522        unsafe {
1523            let stack = std::ptr::null_mut();
1524            let stack = push(stack, Value::String(global_string("42".to_owned())));
1525
1526            let stack = string_to_int(stack);
1527
1528            let (stack, success) = pop(stack);
1529            let (stack, value) = pop(stack);
1530            assert_eq!(success, Value::Int(1));
1531            assert_eq!(value, Value::Int(42));
1532            assert!(stack.is_null());
1533        }
1534    }
1535
1536    #[test]
1537    fn test_string_to_int_negative() {
1538        unsafe {
1539            let stack = std::ptr::null_mut();
1540            let stack = push(stack, Value::String(global_string("-99".to_owned())));
1541
1542            let stack = string_to_int(stack);
1543
1544            let (stack, success) = pop(stack);
1545            let (stack, value) = pop(stack);
1546            assert_eq!(success, Value::Int(1));
1547            assert_eq!(value, Value::Int(-99));
1548            assert!(stack.is_null());
1549        }
1550    }
1551
1552    #[test]
1553    fn test_string_to_int_with_whitespace() {
1554        unsafe {
1555            let stack = std::ptr::null_mut();
1556            let stack = push(stack, Value::String(global_string("  123  ".to_owned())));
1557
1558            let stack = string_to_int(stack);
1559
1560            let (stack, success) = pop(stack);
1561            let (stack, value) = pop(stack);
1562            assert_eq!(success, Value::Int(1));
1563            assert_eq!(value, Value::Int(123));
1564            assert!(stack.is_null());
1565        }
1566    }
1567
1568    #[test]
1569    fn test_string_to_int_failure() {
1570        unsafe {
1571            let stack = std::ptr::null_mut();
1572            let stack = push(
1573                stack,
1574                Value::String(global_string("not a number".to_owned())),
1575            );
1576
1577            let stack = string_to_int(stack);
1578
1579            let (stack, success) = pop(stack);
1580            let (stack, value) = pop(stack);
1581            assert_eq!(success, Value::Int(0));
1582            assert_eq!(value, Value::Int(0));
1583            assert!(stack.is_null());
1584        }
1585    }
1586
1587    #[test]
1588    fn test_string_to_int_empty() {
1589        unsafe {
1590            let stack = std::ptr::null_mut();
1591            let stack = push(stack, Value::String(global_string("".to_owned())));
1592
1593            let stack = string_to_int(stack);
1594
1595            let (stack, success) = pop(stack);
1596            let (stack, value) = pop(stack);
1597            assert_eq!(success, Value::Int(0));
1598            assert_eq!(value, Value::Int(0));
1599            assert!(stack.is_null());
1600        }
1601    }
1602
1603    #[test]
1604    fn test_string_to_int_leading_zeros() {
1605        unsafe {
1606            let stack = std::ptr::null_mut();
1607            let stack = push(stack, Value::String(global_string("007".to_owned())));
1608
1609            let stack = string_to_int(stack);
1610
1611            let (stack, success) = pop(stack);
1612            let (stack, value) = pop(stack);
1613            assert_eq!(success, Value::Int(1));
1614            assert_eq!(value, Value::Int(7));
1615            assert!(stack.is_null());
1616        }
1617    }
1618}