seq_runtime/
string_ops.rs

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