Skip to main content

seq_runtime/string_ops/
access.rs

1//! Character access, slicing, searching, and splitting/joining.
2
3use crate::seqstring::global_string;
4use crate::stack::{Stack, pop, push};
5use crate::value::Value;
6use std::sync::Arc;
7
8/// # Safety
9/// Stack must have the expected values on top for this operation.
10#[unsafe(no_mangle)]
11pub unsafe extern "C" fn patch_seq_string_split(stack: Stack) -> Stack {
12    use crate::value::VariantData;
13
14    assert!(!stack.is_null(), "string_split: stack is empty");
15
16    let (stack, delim_val) = unsafe { pop(stack) };
17    assert!(!stack.is_null(), "string_split: need two strings");
18    let (stack, str_val) = unsafe { pop(stack) };
19
20    match (str_val, delim_val) {
21        (Value::String(s), Value::String(d)) => {
22            // Split and collect into Value::String instances
23            let fields: Vec<Value> = s
24                .as_str()
25                .split(d.as_str())
26                .map(|part| Value::String(global_string(part.to_owned())))
27                .collect();
28
29            // Create a Variant with :List tag and the split parts as fields
30            let variant = Value::Variant(Arc::new(VariantData::new(
31                global_string("List".to_string()),
32                fields,
33            )));
34
35            unsafe { push(stack, variant) }
36        }
37        _ => panic!("string_split: expected two strings on stack"),
38    }
39}
40
41/// Check if a string is empty
42///
43/// Stack effect: ( str -- bool )
44///
45/// # Safety
46/// Stack must have a String value on top
47#[unsafe(no_mangle)]
48pub unsafe extern "C" fn patch_seq_string_contains(stack: Stack) -> Stack {
49    assert!(!stack.is_null(), "string_contains: stack is empty");
50
51    let (stack, substring_val) = unsafe { pop(stack) };
52    assert!(!stack.is_null(), "string_contains: need two strings");
53    let (stack, str_val) = unsafe { pop(stack) };
54
55    match (str_val, substring_val) {
56        (Value::String(s), Value::String(sub)) => {
57            let contains = s.as_str().contains(sub.as_str());
58            unsafe { push(stack, Value::Bool(contains)) }
59        }
60        _ => panic!("string_contains: expected two strings on stack"),
61    }
62}
63
64/// Check if a string starts with a prefix
65///
66/// Stack effect: ( str prefix -- bool )
67///
68/// # Safety
69/// Stack must have two String values on top
70#[unsafe(no_mangle)]
71pub unsafe extern "C" fn patch_seq_string_starts_with(stack: Stack) -> Stack {
72    assert!(!stack.is_null(), "string_starts_with: stack is empty");
73
74    let (stack, prefix_val) = unsafe { pop(stack) };
75    assert!(!stack.is_null(), "string_starts_with: need two strings");
76    let (stack, str_val) = unsafe { pop(stack) };
77
78    match (str_val, prefix_val) {
79        (Value::String(s), Value::String(prefix)) => {
80            let starts = s.as_str().starts_with(prefix.as_str());
81            unsafe { push(stack, Value::Bool(starts)) }
82        }
83        _ => panic!("string_starts_with: expected two strings on stack"),
84    }
85}
86
87/// Concatenate two strings
88///
89/// Stack effect: ( str1 str2 -- result )
90///
91/// # Safety
92/// Stack must have two String values on top
93#[unsafe(no_mangle)]
94pub unsafe extern "C" fn patch_seq_string_char_at(stack: Stack) -> Stack {
95    assert!(!stack.is_null(), "string_char_at: stack is empty");
96
97    let (stack, index_val) = unsafe { pop(stack) };
98    assert!(!stack.is_null(), "string_char_at: need string and index");
99    let (stack, str_val) = unsafe { pop(stack) };
100
101    match (str_val, index_val) {
102        (Value::String(s), Value::Int(index)) => {
103            let result = if index < 0 {
104                -1
105            } else {
106                s.as_str()
107                    .chars()
108                    .nth(index as usize)
109                    .map(|c| c as i64)
110                    .unwrap_or(-1)
111            };
112            unsafe { push(stack, Value::Int(result)) }
113        }
114        _ => panic!("string_char_at: expected String and Int on stack"),
115    }
116}
117
118/// Extract a substring using character indices
119///
120/// Stack effect: ( str start len -- str )
121///
122/// Arguments:
123/// - str: The source string
124/// - start: Starting character index
125/// - len: Number of characters to extract
126///
127/// Edge cases:
128/// - Start beyond end: returns empty string
129/// - Length extends past end: clamps to available
130///
131/// # Safety
132/// Stack must have String, Int, Int on top
133#[unsafe(no_mangle)]
134pub unsafe extern "C" fn patch_seq_string_substring(stack: Stack) -> Stack {
135    assert!(!stack.is_null(), "string_substring: stack is empty");
136
137    let (stack, len_val) = unsafe { pop(stack) };
138    assert!(
139        !stack.is_null(),
140        "string_substring: need string, start, len"
141    );
142    let (stack, start_val) = unsafe { pop(stack) };
143    assert!(
144        !stack.is_null(),
145        "string_substring: need string, start, len"
146    );
147    let (stack, str_val) = unsafe { pop(stack) };
148
149    match (str_val, start_val, len_val) {
150        (Value::String(s), Value::Int(start), Value::Int(len)) => {
151            let result = if start < 0 || len < 0 {
152                String::new()
153            } else {
154                s.as_str()
155                    .chars()
156                    .skip(start as usize)
157                    .take(len as usize)
158                    .collect()
159            };
160            unsafe { push(stack, Value::String(global_string(result))) }
161        }
162        _ => panic!("string_substring: expected String, Int, Int on stack"),
163    }
164}
165
166/// Convert a Unicode code point to a single-character string
167///
168/// Stack effect: ( int -- str )
169///
170/// Creates a string containing the single character represented by the code point.
171/// Panics if the code point is invalid.
172///
173/// # Safety
174/// Stack must have an Int on top
175#[unsafe(no_mangle)]
176pub unsafe extern "C" fn patch_seq_char_to_string(stack: Stack) -> Stack {
177    assert!(!stack.is_null(), "char_to_string: stack is empty");
178
179    let (stack, code_point_val) = unsafe { pop(stack) };
180
181    match code_point_val {
182        Value::Int(code_point) => {
183            let result = if !(0..=0x10FFFF).contains(&code_point) {
184                // Invalid code point - return empty string
185                String::new()
186            } else {
187                match char::from_u32(code_point as u32) {
188                    Some(c) => c.to_string(),
189                    None => String::new(), // Invalid code point (e.g., surrogate)
190                }
191            };
192            unsafe { push(stack, Value::String(global_string(result))) }
193        }
194        _ => panic!("char_to_string: expected Int on stack"),
195    }
196}
197
198/// Find the first occurrence of a substring
199///
200/// Stack effect: ( str needle -- int )
201///
202/// Returns the character index of the first occurrence of needle in str.
203/// Returns -1 if not found.
204///
205/// # Safety
206/// Stack must have two Strings on top
207#[unsafe(no_mangle)]
208pub unsafe extern "C" fn patch_seq_string_find(stack: Stack) -> Stack {
209    assert!(!stack.is_null(), "string_find: stack is empty");
210
211    let (stack, needle_val) = unsafe { pop(stack) };
212    assert!(!stack.is_null(), "string_find: need string and needle");
213    let (stack, str_val) = unsafe { pop(stack) };
214
215    match (str_val, needle_val) {
216        (Value::String(haystack), Value::String(needle)) => {
217            let haystack_str = haystack.as_str();
218            let needle_str = needle.as_str();
219
220            // Find byte position then convert to character position
221            let result = match haystack_str.find(needle_str) {
222                Some(byte_pos) => {
223                    // Count characters up to byte_pos
224                    haystack_str[..byte_pos].chars().count() as i64
225                }
226                None => -1,
227            };
228            unsafe { push(stack, Value::Int(result)) }
229        }
230        _ => panic!("string_find: expected two Strings on stack"),
231    }
232}
233
234/// Trim whitespace from both ends of a string
235///
236/// Stack effect: ( str -- trimmed )
237///
238/// # Safety
239/// Stack must have a String value on top
240#[unsafe(no_mangle)]
241pub unsafe extern "C" fn patch_seq_string_join(stack: Stack) -> Stack {
242    unsafe {
243        // Pop separator
244        let (stack, sep_val) = pop(stack);
245        let sep = match &sep_val {
246            Value::String(s) => s.as_str().to_owned(),
247            _ => panic!("string.join: expected String separator, got {:?}", sep_val),
248        };
249
250        // Pop list (variant)
251        let (stack, list_val) = pop(stack);
252        let variant_data = match &list_val {
253            Value::Variant(v) => v,
254            _ => panic!("string.join: expected Variant (list), got {:?}", list_val),
255        };
256
257        // Convert each element to string and join
258        let parts: Vec<String> = variant_data
259            .fields
260            .iter()
261            .map(|v| match v {
262                Value::String(s) => s.as_str().to_owned(),
263                Value::Int(n) => n.to_string(),
264                Value::Float(f) => f.to_string(),
265                Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
266                Value::Symbol(s) => format!(":{}", s.as_str()),
267                _ => format!("{:?}", v),
268            })
269            .collect();
270
271        let result = parts.join(&sep);
272        push(stack, Value::String(global_string(result)))
273    }
274}