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 :List tag and the split parts as fields
46            let variant = Value::Variant(Arc::new(VariantData::new(
47                global_string("List".to_string()),
48                fields,
49            )));
50
51            unsafe { push(stack, variant) }
52        }
53        _ => panic!("string_split: expected two strings on stack"),
54    }
55}
56
57/// Check if a string is empty
58///
59/// Stack effect: ( str -- bool )
60///
61/// # Safety
62/// Stack must have a String value on top
63#[unsafe(no_mangle)]
64pub unsafe extern "C" fn patch_seq_string_empty(stack: Stack) -> Stack {
65    assert!(!stack.is_null(), "string_empty: stack is empty");
66
67    let (stack, value) = unsafe { pop(stack) };
68
69    match value {
70        Value::String(s) => {
71            let is_empty = s.as_str().is_empty();
72            unsafe { push(stack, Value::Bool(is_empty)) }
73        }
74        _ => panic!("string_empty: expected String on stack"),
75    }
76}
77
78/// Check if a string contains a substring
79///
80/// Stack effect: ( str substring -- bool )
81///
82/// # Safety
83/// Stack must have two String values on top
84#[unsafe(no_mangle)]
85pub unsafe extern "C" fn patch_seq_string_contains(stack: Stack) -> Stack {
86    assert!(!stack.is_null(), "string_contains: stack is empty");
87
88    let (stack, substring_val) = unsafe { pop(stack) };
89    assert!(!stack.is_null(), "string_contains: need two strings");
90    let (stack, str_val) = unsafe { pop(stack) };
91
92    match (str_val, substring_val) {
93        (Value::String(s), Value::String(sub)) => {
94            let contains = s.as_str().contains(sub.as_str());
95            unsafe { push(stack, Value::Bool(contains)) }
96        }
97        _ => panic!("string_contains: expected two strings on stack"),
98    }
99}
100
101/// Check if a string starts with a prefix
102///
103/// Stack effect: ( str prefix -- bool )
104///
105/// # Safety
106/// Stack must have two String values on top
107#[unsafe(no_mangle)]
108pub unsafe extern "C" fn patch_seq_string_starts_with(stack: Stack) -> Stack {
109    assert!(!stack.is_null(), "string_starts_with: stack is empty");
110
111    let (stack, prefix_val) = unsafe { pop(stack) };
112    assert!(!stack.is_null(), "string_starts_with: need two strings");
113    let (stack, str_val) = unsafe { pop(stack) };
114
115    match (str_val, prefix_val) {
116        (Value::String(s), Value::String(prefix)) => {
117            let starts = s.as_str().starts_with(prefix.as_str());
118            unsafe { push(stack, Value::Bool(starts)) }
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            unsafe { push(stack, Value::Bool(equal)) }
425        }
426        _ => panic!("string_equal: expected two strings on stack"),
427    }
428}
429
430/// Compare two symbols for equality
431///
432/// Stack effect: ( Symbol Symbol -- Bool )
433///
434/// Optimization (Issue #166): If both symbols are interned (capacity=0),
435/// we use O(1) pointer comparison instead of O(n) string comparison.
436///
437/// # Safety
438/// Stack must have two Symbol values on top
439#[unsafe(no_mangle)]
440pub unsafe extern "C" fn patch_seq_symbol_equal(stack: Stack) -> Stack {
441    assert!(!stack.is_null(), "symbol_equal: stack is empty");
442
443    let (stack, sym2_val) = unsafe { pop(stack) };
444    assert!(!stack.is_null(), "symbol_equal: need two symbols");
445    let (stack, sym1_val) = unsafe { pop(stack) };
446
447    match (sym1_val, sym2_val) {
448        (Value::Symbol(s1), Value::Symbol(s2)) => {
449            // Fast path: both interned symbols -> O(1) pointer comparison
450            let equal = if s1.is_interned() && s2.is_interned() {
451                s1.as_ptr() == s2.as_ptr()
452            } else {
453                // Fallback: string comparison for runtime-created symbols
454                s1.as_str() == s2.as_str()
455            };
456            unsafe { push(stack, Value::Bool(equal)) }
457        }
458        _ => panic!("symbol_equal: expected two symbols on stack"),
459    }
460}
461
462/// Escape a string for JSON output
463///
464/// Stack effect: ( str -- str )
465///
466/// Escapes special characters according to JSON spec:
467/// - `"` → `\"`
468/// - `\` → `\\`
469/// - newline → `\n`
470/// - carriage return → `\r`
471/// - tab → `\t`
472/// - backspace → `\b`
473/// - form feed → `\f`
474/// - Control characters (0x00-0x1F) → `\uXXXX`
475///
476/// # Safety
477/// Stack must have a String value on top
478#[unsafe(no_mangle)]
479pub unsafe extern "C" fn patch_seq_json_escape(stack: Stack) -> Stack {
480    assert!(!stack.is_null(), "json_escape: stack is empty");
481
482    let (stack, value) = unsafe { pop(stack) };
483
484    match value {
485        Value::String(s) => {
486            let input = s.as_str();
487            let mut result = String::with_capacity(input.len() + 16);
488
489            for ch in input.chars() {
490                match ch {
491                    '"' => result.push_str("\\\""),
492                    '\\' => result.push_str("\\\\"),
493                    '\n' => result.push_str("\\n"),
494                    '\r' => result.push_str("\\r"),
495                    '\t' => result.push_str("\\t"),
496                    '\x08' => result.push_str("\\b"), // backspace
497                    '\x0C' => result.push_str("\\f"), // form feed
498                    // Control characters (0x00-0x1F except those handled above)
499                    // RFC 8259 uses uppercase hex in examples for Unicode escapes
500                    c if c.is_control() => {
501                        result.push_str(&format!("\\u{:04X}", c as u32));
502                    }
503                    c => result.push(c),
504                }
505            }
506
507            unsafe { push(stack, Value::String(global_string(result))) }
508        }
509        _ => panic!("json_escape: expected String on stack"),
510    }
511}
512
513/// Convert String to Int: ( String -- Int Int )
514/// Returns the parsed int and 1 on success, or 0 and 0 on failure.
515/// Accepts integers in range [-9223372036854775808, 9223372036854775807] (i64).
516/// Trims leading/trailing whitespace before parsing.
517/// Leading zeros are accepted (e.g., "007" parses to 7).
518///
519/// # Safety
520/// Stack must have a String value on top
521#[unsafe(no_mangle)]
522pub unsafe extern "C" fn patch_seq_string_to_int(stack: Stack) -> Stack {
523    assert!(!stack.is_null(), "string->int: stack is empty");
524    let (stack, val) = unsafe { pop(stack) };
525
526    match val {
527        Value::String(s) => match s.as_str().trim().parse::<i64>() {
528            Ok(i) => {
529                let stack = unsafe { push(stack, Value::Int(i)) };
530                unsafe { push(stack, Value::Bool(true)) }
531            }
532            Err(_) => {
533                let stack = unsafe { push(stack, Value::Int(0)) };
534                unsafe { push(stack, Value::Bool(false)) }
535            }
536        },
537        _ => panic!("string->int: expected String on stack"),
538    }
539}
540
541/// Remove trailing newline characters from a string
542///
543/// Stack effect: ( str -- str )
544///
545/// Removes trailing \n or \r\n (handles both Unix and Windows line endings).
546/// If the string doesn't end with a newline, returns it unchanged.
547///
548/// # Safety
549/// Stack must have a String value on top
550#[unsafe(no_mangle)]
551pub unsafe extern "C" fn patch_seq_string_chomp(stack: Stack) -> Stack {
552    assert!(!stack.is_null(), "string_chomp: stack is empty");
553
554    let (stack, str_val) = unsafe { pop(stack) };
555
556    match str_val {
557        Value::String(s) => {
558            let mut result = s.as_str().to_owned();
559            if result.ends_with('\n') {
560                result.pop();
561                if result.ends_with('\r') {
562                    result.pop();
563                }
564            }
565            unsafe { push(stack, Value::String(global_string(result))) }
566        }
567        _ => panic!("string_chomp: expected String on stack"),
568    }
569}
570
571// Public re-exports with short names for internal use
572pub use patch_seq_char_to_string as char_to_string;
573pub use patch_seq_json_escape as json_escape;
574pub use patch_seq_string_byte_length as string_byte_length;
575pub use patch_seq_string_char_at as string_char_at;
576pub use patch_seq_string_chomp as string_chomp;
577pub use patch_seq_string_concat as string_concat;
578pub use patch_seq_string_contains as string_contains;
579pub use patch_seq_string_empty as string_empty;
580pub use patch_seq_string_equal as string_equal;
581pub use patch_seq_string_find as string_find;
582pub use patch_seq_string_length as string_length;
583pub use patch_seq_string_split as string_split;
584pub use patch_seq_string_starts_with as string_starts_with;
585pub use patch_seq_string_substring as string_substring;
586pub use patch_seq_string_to_int as string_to_int;
587pub use patch_seq_string_to_lower as string_to_lower;
588pub use patch_seq_string_to_upper as string_to_upper;
589pub use patch_seq_string_trim as string_trim;
590
591// ============================================================================
592// FFI String Helpers
593// ============================================================================
594
595/// Convert a Seq String on the stack to a null-terminated C string.
596///
597/// The returned pointer must be freed by the caller using free().
598/// This peeks the string from the stack (caller pops after use).
599///
600/// Stack effect: ( String -- ) returns ptr to C string
601///
602/// # Memory Safety
603///
604/// The returned C string is a **completely independent copy** allocated via
605/// `malloc()`. It has no connection to Seq's memory management:
606///
607/// - The Seq String on the stack remains valid and unchanged
608/// - The returned pointer is owned by the C world and must be freed with `free()`
609/// - Even if the Seq String is garbage collected, the C string remains valid
610/// - Multiple calls with the same Seq String produce independent C strings
611///
612/// This design ensures FFI calls cannot cause use-after-free or double-free
613/// bugs between Seq and C code.
614///
615/// # Safety
616/// Stack must have a String value on top. The unused second argument
617/// exists for future extension (passing output buffer).
618#[unsafe(no_mangle)]
619pub unsafe extern "C" fn patch_seq_string_to_cstring(stack: Stack, _out: *mut u8) -> *mut u8 {
620    assert!(!stack.is_null(), "string_to_cstring: stack is empty");
621
622    use crate::stack::{DISC_STRING, peek_sv};
623
624    // Peek the string value (don't pop - caller will pop after we return)
625    let sv = unsafe { peek_sv(stack) };
626    if sv.slot0 != DISC_STRING {
627        panic!(
628            "string_to_cstring: expected String on stack, got discriminant {}",
629            sv.slot0
630        );
631    }
632
633    // Extract string data from StackValue slots
634    let str_ptr = sv.slot1 as *const u8;
635    let len = sv.slot2 as usize;
636
637    // Guard against overflow: len + 1 for null terminator
638    let alloc_size = len.checked_add(1).unwrap_or_else(|| {
639        panic!(
640            "string_to_cstring: string too large for C conversion (len={})",
641            len
642        )
643    });
644
645    // Allocate space for string + null terminator
646    let ptr = unsafe { libc::malloc(alloc_size) as *mut u8 };
647    if ptr.is_null() {
648        panic!("string_to_cstring: malloc failed");
649    }
650
651    // Copy string data
652    unsafe {
653        std::ptr::copy_nonoverlapping(str_ptr, ptr, len);
654        // Add null terminator
655        *ptr.add(len) = 0;
656    }
657
658    ptr
659}
660
661/// Convert a null-terminated C string to a Seq String and push onto stack.
662///
663/// The C string is NOT freed by this function.
664///
665/// Stack effect: ( -- String )
666///
667/// # Safety
668/// cstr must be a valid null-terminated C string.
669#[unsafe(no_mangle)]
670pub unsafe extern "C" fn patch_seq_cstring_to_string(stack: Stack, cstr: *const u8) -> Stack {
671    if cstr.is_null() {
672        // NULL string - push empty string
673        return unsafe { push(stack, Value::String(global_string(String::new()))) };
674    }
675
676    // Get string length
677    let len = unsafe { libc::strlen(cstr as *const libc::c_char) };
678
679    // Create Rust string from C string
680    let slice = unsafe { std::slice::from_raw_parts(cstr, len) };
681    let s = String::from_utf8_lossy(slice).into_owned();
682
683    unsafe { push(stack, Value::String(global_string(s))) }
684}
685
686#[cfg(test)]
687mod tests {
688    use super::*;
689
690    #[test]
691    fn test_string_split_simple() {
692        unsafe {
693            let stack = crate::stack::alloc_test_stack();
694            let stack = push(stack, Value::String(global_string("a b c".to_owned())));
695            let stack = push(stack, Value::String(global_string(" ".to_owned())));
696
697            let stack = string_split(stack);
698
699            // Should have a Variant with 3 fields: "a", "b", "c"
700            let (_stack, result) = pop(stack);
701            match result {
702                Value::Variant(v) => {
703                    assert_eq!(v.tag.as_str(), "List");
704                    assert_eq!(v.fields.len(), 3);
705                    assert_eq!(v.fields[0], Value::String(global_string("a".to_owned())));
706                    assert_eq!(v.fields[1], Value::String(global_string("b".to_owned())));
707                    assert_eq!(v.fields[2], Value::String(global_string("c".to_owned())));
708                }
709                _ => panic!("Expected Variant, got {:?}", result),
710            }
711        }
712    }
713
714    #[test]
715    fn test_string_split_empty() {
716        unsafe {
717            let stack = crate::stack::alloc_test_stack();
718            let stack = push(stack, Value::String(global_string("".to_owned())));
719            let stack = push(stack, Value::String(global_string(" ".to_owned())));
720
721            let stack = string_split(stack);
722
723            // Empty string splits to one empty part
724            let (_stack, result) = pop(stack);
725            match result {
726                Value::Variant(v) => {
727                    assert_eq!(v.tag.as_str(), "List");
728                    assert_eq!(v.fields.len(), 1);
729                    assert_eq!(v.fields[0], Value::String(global_string("".to_owned())));
730                }
731                _ => panic!("Expected Variant, got {:?}", result),
732            }
733        }
734    }
735
736    #[test]
737    fn test_string_empty_true() {
738        unsafe {
739            let stack = crate::stack::alloc_test_stack();
740            let stack = push(stack, Value::String(global_string("".to_owned())));
741
742            let stack = string_empty(stack);
743
744            let (_stack, result) = pop(stack);
745            assert_eq!(result, Value::Bool(true));
746        }
747    }
748
749    #[test]
750    fn test_string_empty_false() {
751        unsafe {
752            let stack = crate::stack::alloc_test_stack();
753            let stack = push(stack, Value::String(global_string("hello".to_owned())));
754
755            let stack = string_empty(stack);
756
757            let (_stack, result) = pop(stack);
758            assert_eq!(result, Value::Bool(false));
759        }
760    }
761
762    #[test]
763    fn test_string_contains_true() {
764        unsafe {
765            let stack = crate::stack::alloc_test_stack();
766            let stack = push(
767                stack,
768                Value::String(global_string("hello world".to_owned())),
769            );
770            let stack = push(stack, Value::String(global_string("world".to_owned())));
771
772            let stack = string_contains(stack);
773
774            let (_stack, result) = pop(stack);
775            assert_eq!(result, Value::Bool(true));
776        }
777    }
778
779    #[test]
780    fn test_string_contains_false() {
781        unsafe {
782            let stack = crate::stack::alloc_test_stack();
783            let stack = push(
784                stack,
785                Value::String(global_string("hello world".to_owned())),
786            );
787            let stack = push(stack, Value::String(global_string("foo".to_owned())));
788
789            let stack = string_contains(stack);
790
791            let (_stack, result) = pop(stack);
792            assert_eq!(result, Value::Bool(false));
793        }
794    }
795
796    #[test]
797    fn test_string_starts_with_true() {
798        unsafe {
799            let stack = crate::stack::alloc_test_stack();
800            let stack = push(
801                stack,
802                Value::String(global_string("hello world".to_owned())),
803            );
804            let stack = push(stack, Value::String(global_string("hello".to_owned())));
805
806            let stack = string_starts_with(stack);
807
808            let (_stack, result) = pop(stack);
809            assert_eq!(result, Value::Bool(true));
810        }
811    }
812
813    #[test]
814    fn test_string_starts_with_false() {
815        unsafe {
816            let stack = crate::stack::alloc_test_stack();
817            let stack = push(
818                stack,
819                Value::String(global_string("hello world".to_owned())),
820            );
821            let stack = push(stack, Value::String(global_string("world".to_owned())));
822
823            let stack = string_starts_with(stack);
824
825            let (_stack, result) = pop(stack);
826            assert_eq!(result, Value::Bool(false));
827        }
828    }
829
830    #[test]
831    fn test_http_request_line_parsing() {
832        // Real-world use case: Parse "GET /api/users HTTP/1.1"
833        unsafe {
834            let stack = crate::stack::alloc_test_stack();
835            let stack = push(
836                stack,
837                Value::String(global_string("GET /api/users HTTP/1.1".to_owned())),
838            );
839            let stack = push(stack, Value::String(global_string(" ".to_owned())));
840
841            let stack = string_split(stack);
842
843            // Should have a Variant with 3 fields: "GET", "/api/users", "HTTP/1.1"
844            let (_stack, result) = pop(stack);
845            match result {
846                Value::Variant(v) => {
847                    assert_eq!(v.tag.as_str(), "List");
848                    assert_eq!(v.fields.len(), 3);
849                    assert_eq!(v.fields[0], Value::String(global_string("GET".to_owned())));
850                    assert_eq!(
851                        v.fields[1],
852                        Value::String(global_string("/api/users".to_owned()))
853                    );
854                    assert_eq!(
855                        v.fields[2],
856                        Value::String(global_string("HTTP/1.1".to_owned()))
857                    );
858                }
859                _ => panic!("Expected Variant, got {:?}", result),
860            }
861        }
862    }
863
864    #[test]
865    fn test_path_routing() {
866        // Real-world use case: Check if path starts with "/api/"
867        unsafe {
868            let stack = crate::stack::alloc_test_stack();
869            let stack = push(stack, Value::String(global_string("/api/users".to_owned())));
870            let stack = push(stack, Value::String(global_string("/api/".to_owned())));
871
872            let stack = string_starts_with(stack);
873
874            let (_stack, result) = pop(stack);
875            assert_eq!(result, Value::Bool(true));
876        }
877    }
878
879    #[test]
880    fn test_string_concat() {
881        unsafe {
882            let stack = crate::stack::alloc_test_stack();
883            let stack = push(stack, Value::String(global_string("Hello, ".to_owned())));
884            let stack = push(stack, Value::String(global_string("World!".to_owned())));
885
886            let stack = string_concat(stack);
887
888            let (_stack, result) = pop(stack);
889            assert_eq!(
890                result,
891                Value::String(global_string("Hello, World!".to_owned()))
892            );
893        }
894    }
895
896    #[test]
897    fn test_string_length() {
898        unsafe {
899            let stack = crate::stack::alloc_test_stack();
900            let stack = push(stack, Value::String(global_string("Hello".to_owned())));
901
902            let stack = string_length(stack);
903
904            let (_stack, result) = pop(stack);
905            assert_eq!(result, Value::Int(5));
906        }
907    }
908
909    #[test]
910    fn test_string_length_empty() {
911        unsafe {
912            let stack = crate::stack::alloc_test_stack();
913            let stack = push(stack, Value::String(global_string("".to_owned())));
914
915            let stack = string_length(stack);
916
917            let (_stack, result) = pop(stack);
918            assert_eq!(result, Value::Int(0));
919        }
920    }
921
922    #[test]
923    fn test_string_trim() {
924        unsafe {
925            let stack = crate::stack::alloc_test_stack();
926            let stack = push(
927                stack,
928                Value::String(global_string("  Hello, World!  ".to_owned())),
929            );
930
931            let stack = string_trim(stack);
932
933            let (_stack, result) = pop(stack);
934            assert_eq!(
935                result,
936                Value::String(global_string("Hello, World!".to_owned()))
937            );
938        }
939    }
940
941    #[test]
942    fn test_string_to_upper() {
943        unsafe {
944            let stack = crate::stack::alloc_test_stack();
945            let stack = push(
946                stack,
947                Value::String(global_string("Hello, World!".to_owned())),
948            );
949
950            let stack = string_to_upper(stack);
951
952            let (_stack, result) = pop(stack);
953            assert_eq!(
954                result,
955                Value::String(global_string("HELLO, WORLD!".to_owned()))
956            );
957        }
958    }
959
960    #[test]
961    fn test_string_to_lower() {
962        unsafe {
963            let stack = crate::stack::alloc_test_stack();
964            let stack = push(
965                stack,
966                Value::String(global_string("Hello, World!".to_owned())),
967            );
968
969            let stack = string_to_lower(stack);
970
971            let (_stack, result) = pop(stack);
972            assert_eq!(
973                result,
974                Value::String(global_string("hello, world!".to_owned()))
975            );
976        }
977    }
978
979    #[test]
980    fn test_http_header_content_length() {
981        // Real-world use case: Build "Content-Length: 42" header
982        unsafe {
983            let stack = crate::stack::alloc_test_stack();
984            let stack = push(
985                stack,
986                Value::String(global_string("Content-Length: ".to_owned())),
987            );
988            let stack = push(stack, Value::String(global_string("42".to_owned())));
989
990            let stack = string_concat(stack);
991
992            let (_stack, result) = pop(stack);
993            assert_eq!(
994                result,
995                Value::String(global_string("Content-Length: 42".to_owned()))
996            );
997        }
998    }
999
1000    #[test]
1001    fn test_string_equal_true() {
1002        unsafe {
1003            let stack = crate::stack::alloc_test_stack();
1004            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1005            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1006
1007            let stack = string_equal(stack);
1008
1009            let (_stack, result) = pop(stack);
1010            assert_eq!(result, Value::Bool(true));
1011        }
1012    }
1013
1014    #[test]
1015    fn test_string_equal_false() {
1016        unsafe {
1017            let stack = crate::stack::alloc_test_stack();
1018            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1019            let stack = push(stack, Value::String(global_string("world".to_owned())));
1020
1021            let stack = string_equal(stack);
1022
1023            let (_stack, result) = pop(stack);
1024            assert_eq!(result, Value::Bool(false));
1025        }
1026    }
1027
1028    #[test]
1029    fn test_string_equal_empty_strings() {
1030        unsafe {
1031            let stack = crate::stack::alloc_test_stack();
1032            let stack = push(stack, Value::String(global_string("".to_owned())));
1033            let stack = push(stack, Value::String(global_string("".to_owned())));
1034
1035            let stack = string_equal(stack);
1036
1037            let (_stack, result) = pop(stack);
1038            assert_eq!(result, Value::Bool(true));
1039        }
1040    }
1041
1042    // UTF-8 String Primitives Tests
1043
1044    #[test]
1045    fn test_string_length_utf8() {
1046        // "héllo" has 5 characters but 6 bytes (é is 2 bytes in UTF-8)
1047        unsafe {
1048            let stack = crate::stack::alloc_test_stack();
1049            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
1050
1051            let stack = string_length(stack);
1052
1053            let (_stack, result) = pop(stack);
1054            assert_eq!(result, Value::Int(5)); // Characters, not bytes
1055        }
1056    }
1057
1058    #[test]
1059    fn test_string_length_emoji() {
1060        // Emoji is one code point but multiple bytes
1061        unsafe {
1062            let stack = crate::stack::alloc_test_stack();
1063            let stack = push(stack, Value::String(global_string("hi🎉".to_owned())));
1064
1065            let stack = string_length(stack);
1066
1067            let (_stack, result) = pop(stack);
1068            assert_eq!(result, Value::Int(3)); // 'h', 'i', and emoji
1069        }
1070    }
1071
1072    #[test]
1073    fn test_string_byte_length_ascii() {
1074        unsafe {
1075            let stack = crate::stack::alloc_test_stack();
1076            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1077
1078            let stack = string_byte_length(stack);
1079
1080            let (_stack, result) = pop(stack);
1081            assert_eq!(result, Value::Int(5)); // Same as char length for ASCII
1082        }
1083    }
1084
1085    #[test]
1086    fn test_string_byte_length_utf8() {
1087        // "héllo" has 5 characters but 6 bytes
1088        unsafe {
1089            let stack = crate::stack::alloc_test_stack();
1090            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
1091
1092            let stack = string_byte_length(stack);
1093
1094            let (_stack, result) = pop(stack);
1095            assert_eq!(result, Value::Int(6)); // Bytes, not characters
1096        }
1097    }
1098
1099    #[test]
1100    fn test_string_char_at_ascii() {
1101        unsafe {
1102            let stack = crate::stack::alloc_test_stack();
1103            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1104            let stack = push(stack, Value::Int(0));
1105
1106            let stack = string_char_at(stack);
1107
1108            let (_stack, result) = pop(stack);
1109            assert_eq!(result, Value::Int(104)); // 'h' = 104
1110        }
1111    }
1112
1113    #[test]
1114    fn test_string_char_at_utf8() {
1115        // Get the é character at index 1 in "héllo"
1116        unsafe {
1117            let stack = crate::stack::alloc_test_stack();
1118            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
1119            let stack = push(stack, Value::Int(1));
1120
1121            let stack = string_char_at(stack);
1122
1123            let (_stack, result) = pop(stack);
1124            assert_eq!(result, Value::Int(233)); // 'é' = U+00E9 = 233
1125        }
1126    }
1127
1128    #[test]
1129    fn test_string_char_at_out_of_bounds() {
1130        unsafe {
1131            let stack = crate::stack::alloc_test_stack();
1132            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1133            let stack = push(stack, Value::Int(10)); // Out of bounds
1134
1135            let stack = string_char_at(stack);
1136
1137            let (_stack, result) = pop(stack);
1138            assert_eq!(result, Value::Int(-1));
1139        }
1140    }
1141
1142    #[test]
1143    fn test_string_char_at_negative() {
1144        unsafe {
1145            let stack = crate::stack::alloc_test_stack();
1146            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1147            let stack = push(stack, Value::Int(-1));
1148
1149            let stack = string_char_at(stack);
1150
1151            let (_stack, result) = pop(stack);
1152            assert_eq!(result, Value::Int(-1));
1153        }
1154    }
1155
1156    #[test]
1157    fn test_string_substring_simple() {
1158        unsafe {
1159            let stack = crate::stack::alloc_test_stack();
1160            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1161            let stack = push(stack, Value::Int(1)); // start
1162            let stack = push(stack, Value::Int(3)); // len
1163
1164            let stack = string_substring(stack);
1165
1166            let (_stack, result) = pop(stack);
1167            assert_eq!(result, Value::String(global_string("ell".to_owned())));
1168        }
1169    }
1170
1171    #[test]
1172    fn test_string_substring_utf8() {
1173        // "héllo" - get "éll" (characters 1-3)
1174        unsafe {
1175            let stack = crate::stack::alloc_test_stack();
1176            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
1177            let stack = push(stack, Value::Int(1)); // start
1178            let stack = push(stack, Value::Int(3)); // len
1179
1180            let stack = string_substring(stack);
1181
1182            let (_stack, result) = pop(stack);
1183            assert_eq!(result, Value::String(global_string("éll".to_owned())));
1184        }
1185    }
1186
1187    #[test]
1188    fn test_string_substring_clamp() {
1189        // Request more than available - should clamp
1190        unsafe {
1191            let stack = crate::stack::alloc_test_stack();
1192            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1193            let stack = push(stack, Value::Int(2)); // start
1194            let stack = push(stack, Value::Int(100)); // len (way too long)
1195
1196            let stack = string_substring(stack);
1197
1198            let (_stack, result) = pop(stack);
1199            assert_eq!(result, Value::String(global_string("llo".to_owned())));
1200        }
1201    }
1202
1203    #[test]
1204    fn test_string_substring_beyond_end() {
1205        // Start beyond end - returns empty
1206        unsafe {
1207            let stack = crate::stack::alloc_test_stack();
1208            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1209            let stack = push(stack, Value::Int(10)); // start (beyond end)
1210            let stack = push(stack, Value::Int(3)); // len
1211
1212            let stack = string_substring(stack);
1213
1214            let (_stack, result) = pop(stack);
1215            assert_eq!(result, Value::String(global_string("".to_owned())));
1216        }
1217    }
1218
1219    #[test]
1220    fn test_char_to_string_ascii() {
1221        unsafe {
1222            let stack = crate::stack::alloc_test_stack();
1223            let stack = push(stack, Value::Int(65)); // 'A'
1224
1225            let stack = char_to_string(stack);
1226
1227            let (_stack, result) = pop(stack);
1228            assert_eq!(result, Value::String(global_string("A".to_owned())));
1229        }
1230    }
1231
1232    #[test]
1233    fn test_char_to_string_utf8() {
1234        unsafe {
1235            let stack = crate::stack::alloc_test_stack();
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        }
1243    }
1244
1245    #[test]
1246    fn test_char_to_string_newline() {
1247        unsafe {
1248            let stack = crate::stack::alloc_test_stack();
1249            let stack = push(stack, Value::Int(10)); // '\n'
1250
1251            let stack = char_to_string(stack);
1252
1253            let (_stack, result) = pop(stack);
1254            assert_eq!(result, Value::String(global_string("\n".to_owned())));
1255        }
1256    }
1257
1258    #[test]
1259    fn test_char_to_string_invalid() {
1260        unsafe {
1261            let stack = crate::stack::alloc_test_stack();
1262            let stack = push(stack, Value::Int(-1)); // Invalid
1263
1264            let stack = char_to_string(stack);
1265
1266            let (_stack, result) = pop(stack);
1267            assert_eq!(result, Value::String(global_string("".to_owned())));
1268        }
1269    }
1270
1271    #[test]
1272    fn test_string_find_found() {
1273        unsafe {
1274            let stack = crate::stack::alloc_test_stack();
1275            let stack = push(
1276                stack,
1277                Value::String(global_string("hello world".to_owned())),
1278            );
1279            let stack = push(stack, Value::String(global_string("world".to_owned())));
1280
1281            let stack = string_find(stack);
1282
1283            let (_stack, result) = pop(stack);
1284            assert_eq!(result, Value::Int(6)); // "world" starts at index 6
1285        }
1286    }
1287
1288    #[test]
1289    fn test_string_find_not_found() {
1290        unsafe {
1291            let stack = crate::stack::alloc_test_stack();
1292            let stack = push(
1293                stack,
1294                Value::String(global_string("hello world".to_owned())),
1295            );
1296            let stack = push(stack, Value::String(global_string("xyz".to_owned())));
1297
1298            let stack = string_find(stack);
1299
1300            let (_stack, result) = pop(stack);
1301            assert_eq!(result, Value::Int(-1));
1302        }
1303    }
1304
1305    #[test]
1306    fn test_string_find_first_match() {
1307        // Should return first occurrence
1308        unsafe {
1309            let stack = crate::stack::alloc_test_stack();
1310            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1311            let stack = push(stack, Value::String(global_string("l".to_owned())));
1312
1313            let stack = string_find(stack);
1314
1315            let (_stack, result) = pop(stack);
1316            assert_eq!(result, Value::Int(2)); // First 'l' is at index 2
1317        }
1318    }
1319
1320    #[test]
1321    fn test_string_find_utf8() {
1322        // Find in UTF-8 string - returns character index, not byte index
1323        unsafe {
1324            let stack = crate::stack::alloc_test_stack();
1325            let stack = push(
1326                stack,
1327                Value::String(global_string("héllo wörld".to_owned())),
1328            );
1329            let stack = push(stack, Value::String(global_string("wörld".to_owned())));
1330
1331            let stack = string_find(stack);
1332
1333            let (_stack, result) = pop(stack);
1334            assert_eq!(result, Value::Int(6)); // Character index, not byte index
1335        }
1336    }
1337
1338    // JSON Escape Tests
1339
1340    #[test]
1341    fn test_json_escape_quotes() {
1342        unsafe {
1343            let stack = crate::stack::alloc_test_stack();
1344            let stack = push(
1345                stack,
1346                Value::String(global_string("hello \"world\"".to_owned())),
1347            );
1348
1349            let stack = json_escape(stack);
1350
1351            let (_stack, result) = pop(stack);
1352            assert_eq!(
1353                result,
1354                Value::String(global_string("hello \\\"world\\\"".to_owned()))
1355            );
1356        }
1357    }
1358
1359    #[test]
1360    fn test_json_escape_backslash() {
1361        unsafe {
1362            let stack = crate::stack::alloc_test_stack();
1363            let stack = push(
1364                stack,
1365                Value::String(global_string("path\\to\\file".to_owned())),
1366            );
1367
1368            let stack = json_escape(stack);
1369
1370            let (_stack, result) = pop(stack);
1371            assert_eq!(
1372                result,
1373                Value::String(global_string("path\\\\to\\\\file".to_owned()))
1374            );
1375        }
1376    }
1377
1378    #[test]
1379    fn test_json_escape_newline_tab() {
1380        unsafe {
1381            let stack = crate::stack::alloc_test_stack();
1382            let stack = push(
1383                stack,
1384                Value::String(global_string("line1\nline2\ttabbed".to_owned())),
1385            );
1386
1387            let stack = json_escape(stack);
1388
1389            let (_stack, result) = pop(stack);
1390            assert_eq!(
1391                result,
1392                Value::String(global_string("line1\\nline2\\ttabbed".to_owned()))
1393            );
1394        }
1395    }
1396
1397    #[test]
1398    fn test_json_escape_carriage_return() {
1399        unsafe {
1400            let stack = crate::stack::alloc_test_stack();
1401            let stack = push(
1402                stack,
1403                Value::String(global_string("line1\r\nline2".to_owned())),
1404            );
1405
1406            let stack = json_escape(stack);
1407
1408            let (_stack, result) = pop(stack);
1409            assert_eq!(
1410                result,
1411                Value::String(global_string("line1\\r\\nline2".to_owned()))
1412            );
1413        }
1414    }
1415
1416    #[test]
1417    fn test_json_escape_control_chars() {
1418        unsafe {
1419            let stack = crate::stack::alloc_test_stack();
1420            // Test backspace (0x08) and form feed (0x0C)
1421            let stack = push(
1422                stack,
1423                Value::String(global_string("a\x08b\x0Cc".to_owned())),
1424            );
1425
1426            let stack = json_escape(stack);
1427
1428            let (_stack, result) = pop(stack);
1429            assert_eq!(result, Value::String(global_string("a\\bb\\fc".to_owned())));
1430        }
1431    }
1432
1433    #[test]
1434    fn test_json_escape_unicode_control() {
1435        unsafe {
1436            let stack = crate::stack::alloc_test_stack();
1437            // Test null character (0x00) - should be escaped as \u0000 (uppercase hex per RFC 8259)
1438            let stack = push(stack, Value::String(global_string("a\x00b".to_owned())));
1439
1440            let stack = json_escape(stack);
1441
1442            let (_stack, result) = pop(stack);
1443            assert_eq!(result, Value::String(global_string("a\\u0000b".to_owned())));
1444        }
1445    }
1446
1447    #[test]
1448    fn test_json_escape_mixed_special_chars() {
1449        // Test combination of multiple special characters
1450        unsafe {
1451            let stack = crate::stack::alloc_test_stack();
1452            let stack = push(
1453                stack,
1454                Value::String(global_string("Line 1\nLine \"2\"\ttab\r\n".to_owned())),
1455            );
1456
1457            let stack = json_escape(stack);
1458
1459            let (_stack, result) = pop(stack);
1460            assert_eq!(
1461                result,
1462                Value::String(global_string(
1463                    "Line 1\\nLine \\\"2\\\"\\ttab\\r\\n".to_owned()
1464                ))
1465            );
1466        }
1467    }
1468
1469    #[test]
1470    fn test_json_escape_no_change() {
1471        // Normal string without special chars should pass through unchanged
1472        unsafe {
1473            let stack = crate::stack::alloc_test_stack();
1474            let stack = push(
1475                stack,
1476                Value::String(global_string("Hello, World!".to_owned())),
1477            );
1478
1479            let stack = json_escape(stack);
1480
1481            let (_stack, result) = pop(stack);
1482            assert_eq!(
1483                result,
1484                Value::String(global_string("Hello, World!".to_owned()))
1485            );
1486        }
1487    }
1488
1489    #[test]
1490    fn test_json_escape_empty_string() {
1491        unsafe {
1492            let stack = crate::stack::alloc_test_stack();
1493            let stack = push(stack, Value::String(global_string("".to_owned())));
1494
1495            let stack = json_escape(stack);
1496
1497            let (_stack, result) = pop(stack);
1498            assert_eq!(result, Value::String(global_string("".to_owned())));
1499        }
1500    }
1501
1502    // string->int tests
1503
1504    #[test]
1505    fn test_string_to_int_success() {
1506        unsafe {
1507            let stack = crate::stack::alloc_test_stack();
1508            let stack = push(stack, Value::String(global_string("42".to_owned())));
1509
1510            let stack = string_to_int(stack);
1511
1512            let (stack, success) = pop(stack);
1513            let (_stack, value) = pop(stack);
1514            assert_eq!(success, Value::Bool(true));
1515            assert_eq!(value, Value::Int(42));
1516        }
1517    }
1518
1519    #[test]
1520    fn test_string_to_int_negative() {
1521        unsafe {
1522            let stack = crate::stack::alloc_test_stack();
1523            let stack = push(stack, Value::String(global_string("-99".to_owned())));
1524
1525            let stack = string_to_int(stack);
1526
1527            let (stack, success) = pop(stack);
1528            let (_stack, value) = pop(stack);
1529            assert_eq!(success, Value::Bool(true));
1530            assert_eq!(value, Value::Int(-99));
1531        }
1532    }
1533
1534    #[test]
1535    fn test_string_to_int_with_whitespace() {
1536        unsafe {
1537            let stack = crate::stack::alloc_test_stack();
1538            let stack = push(stack, Value::String(global_string("  123  ".to_owned())));
1539
1540            let stack = string_to_int(stack);
1541
1542            let (stack, success) = pop(stack);
1543            let (_stack, value) = pop(stack);
1544            assert_eq!(success, Value::Bool(true));
1545            assert_eq!(value, Value::Int(123));
1546        }
1547    }
1548
1549    #[test]
1550    fn test_string_to_int_failure() {
1551        unsafe {
1552            let stack = crate::stack::alloc_test_stack();
1553            let stack = push(
1554                stack,
1555                Value::String(global_string("not a number".to_owned())),
1556            );
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::Bool(false));
1563            assert_eq!(value, Value::Int(0));
1564        }
1565    }
1566
1567    #[test]
1568    fn test_string_to_int_empty() {
1569        unsafe {
1570            let stack = crate::stack::alloc_test_stack();
1571            let stack = push(stack, Value::String(global_string("".to_owned())));
1572
1573            let stack = string_to_int(stack);
1574
1575            let (stack, success) = pop(stack);
1576            let (_stack, value) = pop(stack);
1577            assert_eq!(success, Value::Bool(false));
1578            assert_eq!(value, Value::Int(0));
1579        }
1580    }
1581
1582    #[test]
1583    fn test_string_to_int_leading_zeros() {
1584        unsafe {
1585            let stack = crate::stack::alloc_test_stack();
1586            let stack = push(stack, Value::String(global_string("007".to_owned())));
1587
1588            let stack = string_to_int(stack);
1589
1590            let (stack, success) = pop(stack);
1591            let (_stack, value) = pop(stack);
1592            assert_eq!(success, Value::Bool(true));
1593            assert_eq!(value, Value::Int(7));
1594        }
1595    }
1596}