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    use crate::stack::{DISC_STRING, peek_sv};
592
593    // Peek the string value (don't pop - caller will pop after we return)
594    let sv = unsafe { peek_sv(stack) };
595    if sv.slot0 != DISC_STRING {
596        panic!(
597            "string_to_cstring: expected String on stack, got discriminant {}",
598            sv.slot0
599        );
600    }
601
602    // Extract string data from StackValue slots
603    let str_ptr = sv.slot1 as *const u8;
604    let len = sv.slot2 as usize;
605
606    // Guard against overflow: len + 1 for null terminator
607    let alloc_size = len.checked_add(1).unwrap_or_else(|| {
608        panic!(
609            "string_to_cstring: string too large for C conversion (len={})",
610            len
611        )
612    });
613
614    // Allocate space for string + null terminator
615    let ptr = unsafe { libc::malloc(alloc_size) as *mut u8 };
616    if ptr.is_null() {
617        panic!("string_to_cstring: malloc failed");
618    }
619
620    // Copy string data
621    unsafe {
622        std::ptr::copy_nonoverlapping(str_ptr, ptr, len);
623        // Add null terminator
624        *ptr.add(len) = 0;
625    }
626
627    ptr
628}
629
630/// Convert a null-terminated C string to a Seq String and push onto stack.
631///
632/// The C string is NOT freed by this function.
633///
634/// Stack effect: ( -- String )
635///
636/// # Safety
637/// cstr must be a valid null-terminated C string.
638#[unsafe(no_mangle)]
639pub unsafe extern "C" fn patch_seq_cstring_to_string(stack: Stack, cstr: *const u8) -> Stack {
640    if cstr.is_null() {
641        // NULL string - push empty string
642        return unsafe { push(stack, Value::String(global_string(String::new()))) };
643    }
644
645    // Get string length
646    let len = unsafe { libc::strlen(cstr as *const libc::c_char) };
647
648    // Create Rust string from C string
649    let slice = unsafe { std::slice::from_raw_parts(cstr, len) };
650    let s = String::from_utf8_lossy(slice).into_owned();
651
652    unsafe { push(stack, Value::String(global_string(s))) }
653}
654
655#[cfg(test)]
656mod tests {
657    use super::*;
658
659    #[test]
660    fn test_string_split_simple() {
661        unsafe {
662            let stack = crate::stack::alloc_test_stack();
663            let stack = push(stack, Value::String(global_string("a b c".to_owned())));
664            let stack = push(stack, Value::String(global_string(" ".to_owned())));
665
666            let stack = string_split(stack);
667
668            // Should have a Variant with 3 fields: "a", "b", "c"
669            let (_stack, result) = pop(stack);
670            match result {
671                Value::Variant(v) => {
672                    assert_eq!(v.tag, 0);
673                    assert_eq!(v.fields.len(), 3);
674                    assert_eq!(v.fields[0], Value::String(global_string("a".to_owned())));
675                    assert_eq!(v.fields[1], Value::String(global_string("b".to_owned())));
676                    assert_eq!(v.fields[2], Value::String(global_string("c".to_owned())));
677                }
678                _ => panic!("Expected Variant, got {:?}", result),
679            }
680        }
681    }
682
683    #[test]
684    fn test_string_split_empty() {
685        unsafe {
686            let stack = crate::stack::alloc_test_stack();
687            let stack = push(stack, Value::String(global_string("".to_owned())));
688            let stack = push(stack, Value::String(global_string(" ".to_owned())));
689
690            let stack = string_split(stack);
691
692            // Empty string splits to one empty part
693            let (_stack, result) = pop(stack);
694            match result {
695                Value::Variant(v) => {
696                    assert_eq!(v.tag, 0);
697                    assert_eq!(v.fields.len(), 1);
698                    assert_eq!(v.fields[0], Value::String(global_string("".to_owned())));
699                }
700                _ => panic!("Expected Variant, got {:?}", result),
701            }
702        }
703    }
704
705    #[test]
706    fn test_string_empty_true() {
707        unsafe {
708            let stack = crate::stack::alloc_test_stack();
709            let stack = push(stack, Value::String(global_string("".to_owned())));
710
711            let stack = string_empty(stack);
712
713            let (_stack, result) = pop(stack);
714            assert_eq!(result, Value::Int(1));
715        }
716    }
717
718    #[test]
719    fn test_string_empty_false() {
720        unsafe {
721            let stack = crate::stack::alloc_test_stack();
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        }
729    }
730
731    #[test]
732    fn test_string_contains_true() {
733        unsafe {
734            let stack = crate::stack::alloc_test_stack();
735            let stack = push(
736                stack,
737                Value::String(global_string("hello world".to_owned())),
738            );
739            let stack = push(stack, Value::String(global_string("world".to_owned())));
740
741            let stack = string_contains(stack);
742
743            let (_stack, result) = pop(stack);
744            assert_eq!(result, Value::Int(1));
745        }
746    }
747
748    #[test]
749    fn test_string_contains_false() {
750        unsafe {
751            let stack = crate::stack::alloc_test_stack();
752            let stack = push(
753                stack,
754                Value::String(global_string("hello world".to_owned())),
755            );
756            let stack = push(stack, Value::String(global_string("foo".to_owned())));
757
758            let stack = string_contains(stack);
759
760            let (_stack, result) = pop(stack);
761            assert_eq!(result, Value::Int(0));
762        }
763    }
764
765    #[test]
766    fn test_string_starts_with_true() {
767        unsafe {
768            let stack = crate::stack::alloc_test_stack();
769            let stack = push(
770                stack,
771                Value::String(global_string("hello world".to_owned())),
772            );
773            let stack = push(stack, Value::String(global_string("hello".to_owned())));
774
775            let stack = string_starts_with(stack);
776
777            let (_stack, result) = pop(stack);
778            assert_eq!(result, Value::Int(1));
779        }
780    }
781
782    #[test]
783    fn test_string_starts_with_false() {
784        unsafe {
785            let stack = crate::stack::alloc_test_stack();
786            let stack = push(
787                stack,
788                Value::String(global_string("hello world".to_owned())),
789            );
790            let stack = push(stack, Value::String(global_string("world".to_owned())));
791
792            let stack = string_starts_with(stack);
793
794            let (_stack, result) = pop(stack);
795            assert_eq!(result, Value::Int(0));
796        }
797    }
798
799    #[test]
800    fn test_http_request_line_parsing() {
801        // Real-world use case: Parse "GET /api/users HTTP/1.1"
802        unsafe {
803            let stack = crate::stack::alloc_test_stack();
804            let stack = push(
805                stack,
806                Value::String(global_string("GET /api/users HTTP/1.1".to_owned())),
807            );
808            let stack = push(stack, Value::String(global_string(" ".to_owned())));
809
810            let stack = string_split(stack);
811
812            // Should have a Variant with 3 fields: "GET", "/api/users", "HTTP/1.1"
813            let (_stack, result) = pop(stack);
814            match result {
815                Value::Variant(v) => {
816                    assert_eq!(v.tag, 0);
817                    assert_eq!(v.fields.len(), 3);
818                    assert_eq!(v.fields[0], Value::String(global_string("GET".to_owned())));
819                    assert_eq!(
820                        v.fields[1],
821                        Value::String(global_string("/api/users".to_owned()))
822                    );
823                    assert_eq!(
824                        v.fields[2],
825                        Value::String(global_string("HTTP/1.1".to_owned()))
826                    );
827                }
828                _ => panic!("Expected Variant, got {:?}", result),
829            }
830        }
831    }
832
833    #[test]
834    fn test_path_routing() {
835        // Real-world use case: Check if path starts with "/api/"
836        unsafe {
837            let stack = crate::stack::alloc_test_stack();
838            let stack = push(stack, Value::String(global_string("/api/users".to_owned())));
839            let stack = push(stack, Value::String(global_string("/api/".to_owned())));
840
841            let stack = string_starts_with(stack);
842
843            let (_stack, result) = pop(stack);
844            assert_eq!(result, Value::Int(1));
845        }
846    }
847
848    #[test]
849    fn test_string_concat() {
850        unsafe {
851            let stack = crate::stack::alloc_test_stack();
852            let stack = push(stack, Value::String(global_string("Hello, ".to_owned())));
853            let stack = push(stack, Value::String(global_string("World!".to_owned())));
854
855            let stack = string_concat(stack);
856
857            let (_stack, result) = pop(stack);
858            assert_eq!(
859                result,
860                Value::String(global_string("Hello, World!".to_owned()))
861            );
862        }
863    }
864
865    #[test]
866    fn test_string_length() {
867        unsafe {
868            let stack = crate::stack::alloc_test_stack();
869            let stack = push(stack, Value::String(global_string("Hello".to_owned())));
870
871            let stack = string_length(stack);
872
873            let (_stack, result) = pop(stack);
874            assert_eq!(result, Value::Int(5));
875        }
876    }
877
878    #[test]
879    fn test_string_length_empty() {
880        unsafe {
881            let stack = crate::stack::alloc_test_stack();
882            let stack = push(stack, Value::String(global_string("".to_owned())));
883
884            let stack = string_length(stack);
885
886            let (_stack, result) = pop(stack);
887            assert_eq!(result, Value::Int(0));
888        }
889    }
890
891    #[test]
892    fn test_string_trim() {
893        unsafe {
894            let stack = crate::stack::alloc_test_stack();
895            let stack = push(
896                stack,
897                Value::String(global_string("  Hello, World!  ".to_owned())),
898            );
899
900            let stack = string_trim(stack);
901
902            let (_stack, result) = pop(stack);
903            assert_eq!(
904                result,
905                Value::String(global_string("Hello, World!".to_owned()))
906            );
907        }
908    }
909
910    #[test]
911    fn test_string_to_upper() {
912        unsafe {
913            let stack = crate::stack::alloc_test_stack();
914            let stack = push(
915                stack,
916                Value::String(global_string("Hello, World!".to_owned())),
917            );
918
919            let stack = string_to_upper(stack);
920
921            let (_stack, result) = pop(stack);
922            assert_eq!(
923                result,
924                Value::String(global_string("HELLO, WORLD!".to_owned()))
925            );
926        }
927    }
928
929    #[test]
930    fn test_string_to_lower() {
931        unsafe {
932            let stack = crate::stack::alloc_test_stack();
933            let stack = push(
934                stack,
935                Value::String(global_string("Hello, World!".to_owned())),
936            );
937
938            let stack = string_to_lower(stack);
939
940            let (_stack, result) = pop(stack);
941            assert_eq!(
942                result,
943                Value::String(global_string("hello, world!".to_owned()))
944            );
945        }
946    }
947
948    #[test]
949    fn test_http_header_content_length() {
950        // Real-world use case: Build "Content-Length: 42" header
951        unsafe {
952            let stack = crate::stack::alloc_test_stack();
953            let stack = push(
954                stack,
955                Value::String(global_string("Content-Length: ".to_owned())),
956            );
957            let stack = push(stack, Value::String(global_string("42".to_owned())));
958
959            let stack = string_concat(stack);
960
961            let (_stack, result) = pop(stack);
962            assert_eq!(
963                result,
964                Value::String(global_string("Content-Length: 42".to_owned()))
965            );
966        }
967    }
968
969    #[test]
970    fn test_string_equal_true() {
971        unsafe {
972            let stack = crate::stack::alloc_test_stack();
973            let stack = push(stack, Value::String(global_string("hello".to_owned())));
974            let stack = push(stack, Value::String(global_string("hello".to_owned())));
975
976            let stack = string_equal(stack);
977
978            let (_stack, result) = pop(stack);
979            assert_eq!(result, Value::Int(1));
980        }
981    }
982
983    #[test]
984    fn test_string_equal_false() {
985        unsafe {
986            let stack = crate::stack::alloc_test_stack();
987            let stack = push(stack, Value::String(global_string("hello".to_owned())));
988            let stack = push(stack, Value::String(global_string("world".to_owned())));
989
990            let stack = string_equal(stack);
991
992            let (_stack, result) = pop(stack);
993            assert_eq!(result, Value::Int(0));
994        }
995    }
996
997    #[test]
998    fn test_string_equal_empty_strings() {
999        unsafe {
1000            let stack = crate::stack::alloc_test_stack();
1001            let stack = push(stack, Value::String(global_string("".to_owned())));
1002            let stack = push(stack, Value::String(global_string("".to_owned())));
1003
1004            let stack = string_equal(stack);
1005
1006            let (_stack, result) = pop(stack);
1007            assert_eq!(result, Value::Int(1));
1008        }
1009    }
1010
1011    // UTF-8 String Primitives Tests
1012
1013    #[test]
1014    fn test_string_length_utf8() {
1015        // "héllo" has 5 characters but 6 bytes (é is 2 bytes in UTF-8)
1016        unsafe {
1017            let stack = crate::stack::alloc_test_stack();
1018            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
1019
1020            let stack = string_length(stack);
1021
1022            let (_stack, result) = pop(stack);
1023            assert_eq!(result, Value::Int(5)); // Characters, not bytes
1024        }
1025    }
1026
1027    #[test]
1028    fn test_string_length_emoji() {
1029        // Emoji is one code point but multiple bytes
1030        unsafe {
1031            let stack = crate::stack::alloc_test_stack();
1032            let stack = push(stack, Value::String(global_string("hi🎉".to_owned())));
1033
1034            let stack = string_length(stack);
1035
1036            let (_stack, result) = pop(stack);
1037            assert_eq!(result, Value::Int(3)); // 'h', 'i', and emoji
1038        }
1039    }
1040
1041    #[test]
1042    fn test_string_byte_length_ascii() {
1043        unsafe {
1044            let stack = crate::stack::alloc_test_stack();
1045            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1046
1047            let stack = string_byte_length(stack);
1048
1049            let (_stack, result) = pop(stack);
1050            assert_eq!(result, Value::Int(5)); // Same as char length for ASCII
1051        }
1052    }
1053
1054    #[test]
1055    fn test_string_byte_length_utf8() {
1056        // "héllo" has 5 characters but 6 bytes
1057        unsafe {
1058            let stack = crate::stack::alloc_test_stack();
1059            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
1060
1061            let stack = string_byte_length(stack);
1062
1063            let (_stack, result) = pop(stack);
1064            assert_eq!(result, Value::Int(6)); // Bytes, not characters
1065        }
1066    }
1067
1068    #[test]
1069    fn test_string_char_at_ascii() {
1070        unsafe {
1071            let stack = crate::stack::alloc_test_stack();
1072            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1073            let stack = push(stack, Value::Int(0));
1074
1075            let stack = string_char_at(stack);
1076
1077            let (_stack, result) = pop(stack);
1078            assert_eq!(result, Value::Int(104)); // 'h' = 104
1079        }
1080    }
1081
1082    #[test]
1083    fn test_string_char_at_utf8() {
1084        // Get the é character at index 1 in "héllo"
1085        unsafe {
1086            let stack = crate::stack::alloc_test_stack();
1087            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
1088            let stack = push(stack, Value::Int(1));
1089
1090            let stack = string_char_at(stack);
1091
1092            let (_stack, result) = pop(stack);
1093            assert_eq!(result, Value::Int(233)); // 'é' = U+00E9 = 233
1094        }
1095    }
1096
1097    #[test]
1098    fn test_string_char_at_out_of_bounds() {
1099        unsafe {
1100            let stack = crate::stack::alloc_test_stack();
1101            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1102            let stack = push(stack, Value::Int(10)); // Out of bounds
1103
1104            let stack = string_char_at(stack);
1105
1106            let (_stack, result) = pop(stack);
1107            assert_eq!(result, Value::Int(-1));
1108        }
1109    }
1110
1111    #[test]
1112    fn test_string_char_at_negative() {
1113        unsafe {
1114            let stack = crate::stack::alloc_test_stack();
1115            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1116            let stack = push(stack, Value::Int(-1));
1117
1118            let stack = string_char_at(stack);
1119
1120            let (_stack, result) = pop(stack);
1121            assert_eq!(result, Value::Int(-1));
1122        }
1123    }
1124
1125    #[test]
1126    fn test_string_substring_simple() {
1127        unsafe {
1128            let stack = crate::stack::alloc_test_stack();
1129            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1130            let stack = push(stack, Value::Int(1)); // start
1131            let stack = push(stack, Value::Int(3)); // len
1132
1133            let stack = string_substring(stack);
1134
1135            let (_stack, result) = pop(stack);
1136            assert_eq!(result, Value::String(global_string("ell".to_owned())));
1137        }
1138    }
1139
1140    #[test]
1141    fn test_string_substring_utf8() {
1142        // "héllo" - get "éll" (characters 1-3)
1143        unsafe {
1144            let stack = crate::stack::alloc_test_stack();
1145            let stack = push(stack, Value::String(global_string("héllo".to_owned())));
1146            let stack = push(stack, Value::Int(1)); // start
1147            let stack = push(stack, Value::Int(3)); // len
1148
1149            let stack = string_substring(stack);
1150
1151            let (_stack, result) = pop(stack);
1152            assert_eq!(result, Value::String(global_string("éll".to_owned())));
1153        }
1154    }
1155
1156    #[test]
1157    fn test_string_substring_clamp() {
1158        // Request more than available - should clamp
1159        unsafe {
1160            let stack = crate::stack::alloc_test_stack();
1161            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1162            let stack = push(stack, Value::Int(2)); // start
1163            let stack = push(stack, Value::Int(100)); // len (way too long)
1164
1165            let stack = string_substring(stack);
1166
1167            let (_stack, result) = pop(stack);
1168            assert_eq!(result, Value::String(global_string("llo".to_owned())));
1169        }
1170    }
1171
1172    #[test]
1173    fn test_string_substring_beyond_end() {
1174        // Start beyond end - returns empty
1175        unsafe {
1176            let stack = crate::stack::alloc_test_stack();
1177            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1178            let stack = push(stack, Value::Int(10)); // start (beyond end)
1179            let stack = push(stack, Value::Int(3)); // len
1180
1181            let stack = string_substring(stack);
1182
1183            let (_stack, result) = pop(stack);
1184            assert_eq!(result, Value::String(global_string("".to_owned())));
1185        }
1186    }
1187
1188    #[test]
1189    fn test_char_to_string_ascii() {
1190        unsafe {
1191            let stack = crate::stack::alloc_test_stack();
1192            let stack = push(stack, Value::Int(65)); // 'A'
1193
1194            let stack = char_to_string(stack);
1195
1196            let (_stack, result) = pop(stack);
1197            assert_eq!(result, Value::String(global_string("A".to_owned())));
1198        }
1199    }
1200
1201    #[test]
1202    fn test_char_to_string_utf8() {
1203        unsafe {
1204            let stack = crate::stack::alloc_test_stack();
1205            let stack = push(stack, Value::Int(233)); // 'é' = U+00E9
1206
1207            let stack = char_to_string(stack);
1208
1209            let (_stack, result) = pop(stack);
1210            assert_eq!(result, Value::String(global_string("é".to_owned())));
1211        }
1212    }
1213
1214    #[test]
1215    fn test_char_to_string_newline() {
1216        unsafe {
1217            let stack = crate::stack::alloc_test_stack();
1218            let stack = push(stack, Value::Int(10)); // '\n'
1219
1220            let stack = char_to_string(stack);
1221
1222            let (_stack, result) = pop(stack);
1223            assert_eq!(result, Value::String(global_string("\n".to_owned())));
1224        }
1225    }
1226
1227    #[test]
1228    fn test_char_to_string_invalid() {
1229        unsafe {
1230            let stack = crate::stack::alloc_test_stack();
1231            let stack = push(stack, Value::Int(-1)); // Invalid
1232
1233            let stack = char_to_string(stack);
1234
1235            let (_stack, result) = pop(stack);
1236            assert_eq!(result, Value::String(global_string("".to_owned())));
1237        }
1238    }
1239
1240    #[test]
1241    fn test_string_find_found() {
1242        unsafe {
1243            let stack = crate::stack::alloc_test_stack();
1244            let stack = push(
1245                stack,
1246                Value::String(global_string("hello world".to_owned())),
1247            );
1248            let stack = push(stack, Value::String(global_string("world".to_owned())));
1249
1250            let stack = string_find(stack);
1251
1252            let (_stack, result) = pop(stack);
1253            assert_eq!(result, Value::Int(6)); // "world" starts at index 6
1254        }
1255    }
1256
1257    #[test]
1258    fn test_string_find_not_found() {
1259        unsafe {
1260            let stack = crate::stack::alloc_test_stack();
1261            let stack = push(
1262                stack,
1263                Value::String(global_string("hello world".to_owned())),
1264            );
1265            let stack = push(stack, Value::String(global_string("xyz".to_owned())));
1266
1267            let stack = string_find(stack);
1268
1269            let (_stack, result) = pop(stack);
1270            assert_eq!(result, Value::Int(-1));
1271        }
1272    }
1273
1274    #[test]
1275    fn test_string_find_first_match() {
1276        // Should return first occurrence
1277        unsafe {
1278            let stack = crate::stack::alloc_test_stack();
1279            let stack = push(stack, Value::String(global_string("hello".to_owned())));
1280            let stack = push(stack, Value::String(global_string("l".to_owned())));
1281
1282            let stack = string_find(stack);
1283
1284            let (_stack, result) = pop(stack);
1285            assert_eq!(result, Value::Int(2)); // First 'l' is at index 2
1286        }
1287    }
1288
1289    #[test]
1290    fn test_string_find_utf8() {
1291        // Find in UTF-8 string - returns character index, not byte index
1292        unsafe {
1293            let stack = crate::stack::alloc_test_stack();
1294            let stack = push(
1295                stack,
1296                Value::String(global_string("héllo wörld".to_owned())),
1297            );
1298            let stack = push(stack, Value::String(global_string("wörld".to_owned())));
1299
1300            let stack = string_find(stack);
1301
1302            let (_stack, result) = pop(stack);
1303            assert_eq!(result, Value::Int(6)); // Character index, not byte index
1304        }
1305    }
1306
1307    // JSON Escape Tests
1308
1309    #[test]
1310    fn test_json_escape_quotes() {
1311        unsafe {
1312            let stack = crate::stack::alloc_test_stack();
1313            let stack = push(
1314                stack,
1315                Value::String(global_string("hello \"world\"".to_owned())),
1316            );
1317
1318            let stack = json_escape(stack);
1319
1320            let (_stack, result) = pop(stack);
1321            assert_eq!(
1322                result,
1323                Value::String(global_string("hello \\\"world\\\"".to_owned()))
1324            );
1325        }
1326    }
1327
1328    #[test]
1329    fn test_json_escape_backslash() {
1330        unsafe {
1331            let stack = crate::stack::alloc_test_stack();
1332            let stack = push(
1333                stack,
1334                Value::String(global_string("path\\to\\file".to_owned())),
1335            );
1336
1337            let stack = json_escape(stack);
1338
1339            let (_stack, result) = pop(stack);
1340            assert_eq!(
1341                result,
1342                Value::String(global_string("path\\\\to\\\\file".to_owned()))
1343            );
1344        }
1345    }
1346
1347    #[test]
1348    fn test_json_escape_newline_tab() {
1349        unsafe {
1350            let stack = crate::stack::alloc_test_stack();
1351            let stack = push(
1352                stack,
1353                Value::String(global_string("line1\nline2\ttabbed".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("line1\\nline2\\ttabbed".to_owned()))
1362            );
1363        }
1364    }
1365
1366    #[test]
1367    fn test_json_escape_carriage_return() {
1368        unsafe {
1369            let stack = crate::stack::alloc_test_stack();
1370            let stack = push(
1371                stack,
1372                Value::String(global_string("line1\r\nline2".to_owned())),
1373            );
1374
1375            let stack = json_escape(stack);
1376
1377            let (_stack, result) = pop(stack);
1378            assert_eq!(
1379                result,
1380                Value::String(global_string("line1\\r\\nline2".to_owned()))
1381            );
1382        }
1383    }
1384
1385    #[test]
1386    fn test_json_escape_control_chars() {
1387        unsafe {
1388            let stack = crate::stack::alloc_test_stack();
1389            // Test backspace (0x08) and form feed (0x0C)
1390            let stack = push(
1391                stack,
1392                Value::String(global_string("a\x08b\x0Cc".to_owned())),
1393            );
1394
1395            let stack = json_escape(stack);
1396
1397            let (_stack, result) = pop(stack);
1398            assert_eq!(result, Value::String(global_string("a\\bb\\fc".to_owned())));
1399        }
1400    }
1401
1402    #[test]
1403    fn test_json_escape_unicode_control() {
1404        unsafe {
1405            let stack = crate::stack::alloc_test_stack();
1406            // Test null character (0x00) - should be escaped as \u0000 (uppercase hex per RFC 8259)
1407            let stack = push(stack, Value::String(global_string("a\x00b".to_owned())));
1408
1409            let stack = json_escape(stack);
1410
1411            let (_stack, result) = pop(stack);
1412            assert_eq!(result, Value::String(global_string("a\\u0000b".to_owned())));
1413        }
1414    }
1415
1416    #[test]
1417    fn test_json_escape_mixed_special_chars() {
1418        // Test combination of multiple special characters
1419        unsafe {
1420            let stack = crate::stack::alloc_test_stack();
1421            let stack = push(
1422                stack,
1423                Value::String(global_string("Line 1\nLine \"2\"\ttab\r\n".to_owned())),
1424            );
1425
1426            let stack = json_escape(stack);
1427
1428            let (_stack, result) = pop(stack);
1429            assert_eq!(
1430                result,
1431                Value::String(global_string(
1432                    "Line 1\\nLine \\\"2\\\"\\ttab\\r\\n".to_owned()
1433                ))
1434            );
1435        }
1436    }
1437
1438    #[test]
1439    fn test_json_escape_no_change() {
1440        // Normal string without special chars should pass through unchanged
1441        unsafe {
1442            let stack = crate::stack::alloc_test_stack();
1443            let stack = push(
1444                stack,
1445                Value::String(global_string("Hello, World!".to_owned())),
1446            );
1447
1448            let stack = json_escape(stack);
1449
1450            let (_stack, result) = pop(stack);
1451            assert_eq!(
1452                result,
1453                Value::String(global_string("Hello, World!".to_owned()))
1454            );
1455        }
1456    }
1457
1458    #[test]
1459    fn test_json_escape_empty_string() {
1460        unsafe {
1461            let stack = crate::stack::alloc_test_stack();
1462            let stack = push(stack, Value::String(global_string("".to_owned())));
1463
1464            let stack = json_escape(stack);
1465
1466            let (_stack, result) = pop(stack);
1467            assert_eq!(result, Value::String(global_string("".to_owned())));
1468        }
1469    }
1470
1471    // string->int tests
1472
1473    #[test]
1474    fn test_string_to_int_success() {
1475        unsafe {
1476            let stack = crate::stack::alloc_test_stack();
1477            let stack = push(stack, Value::String(global_string("42".to_owned())));
1478
1479            let stack = string_to_int(stack);
1480
1481            let (stack, success) = pop(stack);
1482            let (_stack, value) = pop(stack);
1483            assert_eq!(success, Value::Int(1));
1484            assert_eq!(value, Value::Int(42));
1485        }
1486    }
1487
1488    #[test]
1489    fn test_string_to_int_negative() {
1490        unsafe {
1491            let stack = crate::stack::alloc_test_stack();
1492            let stack = push(stack, Value::String(global_string("-99".to_owned())));
1493
1494            let stack = string_to_int(stack);
1495
1496            let (stack, success) = pop(stack);
1497            let (_stack, value) = pop(stack);
1498            assert_eq!(success, Value::Int(1));
1499            assert_eq!(value, Value::Int(-99));
1500        }
1501    }
1502
1503    #[test]
1504    fn test_string_to_int_with_whitespace() {
1505        unsafe {
1506            let stack = crate::stack::alloc_test_stack();
1507            let stack = push(stack, Value::String(global_string("  123  ".to_owned())));
1508
1509            let stack = string_to_int(stack);
1510
1511            let (stack, success) = pop(stack);
1512            let (_stack, value) = pop(stack);
1513            assert_eq!(success, Value::Int(1));
1514            assert_eq!(value, Value::Int(123));
1515        }
1516    }
1517
1518    #[test]
1519    fn test_string_to_int_failure() {
1520        unsafe {
1521            let stack = crate::stack::alloc_test_stack();
1522            let stack = push(
1523                stack,
1524                Value::String(global_string("not a number".to_owned())),
1525            );
1526
1527            let stack = string_to_int(stack);
1528
1529            let (stack, success) = pop(stack);
1530            let (_stack, value) = pop(stack);
1531            assert_eq!(success, Value::Int(0));
1532            assert_eq!(value, Value::Int(0));
1533        }
1534    }
1535
1536    #[test]
1537    fn test_string_to_int_empty() {
1538        unsafe {
1539            let stack = crate::stack::alloc_test_stack();
1540            let stack = push(stack, Value::String(global_string("".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(0));
1547            assert_eq!(value, Value::Int(0));
1548        }
1549    }
1550
1551    #[test]
1552    fn test_string_to_int_leading_zeros() {
1553        unsafe {
1554            let stack = crate::stack::alloc_test_stack();
1555            let stack = push(stack, Value::String(global_string("007".to_owned())));
1556
1557            let stack = string_to_int(stack);
1558
1559            let (stack, success) = pop(stack);
1560            let (_stack, value) = pop(stack);
1561            assert_eq!(success, Value::Int(1));
1562            assert_eq!(value, Value::Int(7));
1563        }
1564    }
1565}