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