Skip to main content

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