Skip to main content

seq_runtime/string_ops/
access.rs

1//! Character access, slicing, searching, and splitting/joining.
2
3use crate::seqstring::{global_bytes, 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            // Byte-clean split: separate the haystack at every byte
23            // occurrence of the needle. The result is byte-faithful —
24            // splitting an OSC payload on its NUL padding, splitting a
25            // network frame on a binary delimiter, etc. all work.
26            let bytes = s.as_bytes();
27            let needle = d.as_bytes();
28            let parts: Vec<Vec<u8>> = if needle.is_empty() {
29                // Mirror Rust's `&str::split("")` shape: empty leading
30                // and trailing pieces, one piece per byte in between.
31                let mut parts = Vec::with_capacity(bytes.len() + 2);
32                parts.push(Vec::new());
33                for b in bytes {
34                    parts.push(vec![*b]);
35                }
36                parts.push(Vec::new());
37                parts
38            } else {
39                let mut parts: Vec<Vec<u8>> = Vec::new();
40                let mut last = 0usize;
41                let mut i = 0usize;
42                while i + needle.len() <= bytes.len() {
43                    if &bytes[i..i + needle.len()] == needle {
44                        parts.push(bytes[last..i].to_vec());
45                        i += needle.len();
46                        last = i;
47                    } else {
48                        i += 1;
49                    }
50                }
51                parts.push(bytes[last..].to_vec());
52                parts
53            };
54
55            let fields: Vec<Value> = parts
56                .into_iter()
57                .map(|part| Value::String(global_bytes(part)))
58                .collect();
59
60            // Create a Variant with :List tag and the split parts as fields
61            let variant = Value::Variant(Arc::new(VariantData::new(
62                global_string("List".to_string()),
63                fields,
64            )));
65
66            unsafe { push(stack, variant) }
67        }
68        _ => panic!("string_split: expected two strings on stack"),
69    }
70}
71
72/// Check if a string is empty
73///
74/// Stack effect: ( str -- bool )
75///
76/// # Safety
77/// Stack must have a String value on top
78#[unsafe(no_mangle)]
79pub unsafe extern "C" fn patch_seq_string_contains(stack: Stack) -> Stack {
80    assert!(!stack.is_null(), "string_contains: stack is empty");
81
82    let (stack, substring_val) = unsafe { pop(stack) };
83    assert!(!stack.is_null(), "string_contains: need two strings");
84    let (stack, str_val) = unsafe { pop(stack) };
85
86    match (str_val, substring_val) {
87        (Value::String(s), Value::String(sub)) => {
88            // Byte-clean substring search: scan the haystack for the
89            // needle's bytes. Works on any input — text or binary.
90            let contains = byte_contains(s.as_bytes(), sub.as_bytes());
91            unsafe { push(stack, Value::Bool(contains)) }
92        }
93        _ => panic!("string_contains: expected two strings on stack"),
94    }
95}
96
97/// Byte-level substring search. Empty needle is contained in any
98/// haystack (matches Rust's `&str::contains` for the same convention).
99fn byte_contains(haystack: &[u8], needle: &[u8]) -> bool {
100    if needle.is_empty() {
101        return true;
102    }
103    haystack.windows(needle.len()).any(|w| w == needle)
104}
105
106/// Check if a string starts with a prefix
107///
108/// Stack effect: ( str prefix -- bool )
109///
110/// # Safety
111/// Stack must have two String values on top
112#[unsafe(no_mangle)]
113pub unsafe extern "C" fn patch_seq_string_starts_with(stack: Stack) -> Stack {
114    assert!(!stack.is_null(), "string_starts_with: stack is empty");
115
116    let (stack, prefix_val) = unsafe { pop(stack) };
117    assert!(!stack.is_null(), "string_starts_with: need two strings");
118    let (stack, str_val) = unsafe { pop(stack) };
119
120    match (str_val, prefix_val) {
121        (Value::String(s), Value::String(prefix)) => {
122            // Byte-clean prefix check.
123            let starts = s.as_bytes().starts_with(prefix.as_bytes());
124            unsafe { push(stack, Value::Bool(starts)) }
125        }
126        _ => panic!("string_starts_with: expected two strings on stack"),
127    }
128}
129
130/// Concatenate two strings
131///
132/// Stack effect: ( str1 str2 -- result )
133///
134/// # Safety
135/// Stack must have two String values on top
136#[unsafe(no_mangle)]
137pub unsafe extern "C" fn patch_seq_string_char_at(stack: Stack) -> Stack {
138    assert!(!stack.is_null(), "string_char_at: stack is empty");
139
140    let (stack, index_val) = unsafe { pop(stack) };
141    assert!(!stack.is_null(), "string_char_at: need string and index");
142    let (stack, str_val) = unsafe { pop(stack) };
143
144    match (str_val, index_val) {
145        (Value::String(s), Value::Int(index)) => {
146            let result = if index < 0 {
147                -1
148            } else {
149                s.as_str_or_empty()
150                    .chars()
151                    .nth(index as usize)
152                    .map(|c| c as i64)
153                    .unwrap_or(-1)
154            };
155            unsafe { push(stack, Value::Int(result)) }
156        }
157        _ => panic!("string_char_at: expected String and Int on stack"),
158    }
159}
160
161/// Extract a substring using character indices
162///
163/// Stack effect: ( str start len -- str )
164///
165/// Arguments:
166/// - str: The source string
167/// - start: Starting character index
168/// - len: Number of characters to extract
169///
170/// Edge cases:
171/// - Start beyond end: returns empty string
172/// - Length extends past end: clamps to available
173///
174/// # Safety
175/// Stack must have String, Int, Int on top
176#[unsafe(no_mangle)]
177pub unsafe extern "C" fn patch_seq_string_substring(stack: Stack) -> Stack {
178    assert!(!stack.is_null(), "string_substring: stack is empty");
179
180    let (stack, len_val) = unsafe { pop(stack) };
181    assert!(
182        !stack.is_null(),
183        "string_substring: need string, start, len"
184    );
185    let (stack, start_val) = unsafe { pop(stack) };
186    assert!(
187        !stack.is_null(),
188        "string_substring: need string, start, len"
189    );
190    let (stack, str_val) = unsafe { pop(stack) };
191
192    match (str_val, start_val, len_val) {
193        (Value::String(s), Value::Int(start), Value::Int(len)) => {
194            let result = if start < 0 || len < 0 {
195                String::new()
196            } else {
197                s.as_str_or_empty()
198                    .chars()
199                    .skip(start as usize)
200                    .take(len as usize)
201                    .collect()
202            };
203            unsafe { push(stack, Value::String(global_string(result))) }
204        }
205        _ => panic!("string_substring: expected String, Int, Int on stack"),
206    }
207}
208
209/// Convert a Unicode code point to a single-character string
210///
211/// Stack effect: ( int -- str )
212///
213/// Creates a string containing the single character represented by the code point.
214/// Panics if the code point is invalid.
215///
216/// # Safety
217/// Stack must have an Int on top
218#[unsafe(no_mangle)]
219pub unsafe extern "C" fn patch_seq_char_to_string(stack: Stack) -> Stack {
220    assert!(!stack.is_null(), "char_to_string: stack is empty");
221
222    let (stack, code_point_val) = unsafe { pop(stack) };
223
224    match code_point_val {
225        Value::Int(code_point) => {
226            let result = if !(0..=0x10FFFF).contains(&code_point) {
227                // Invalid code point - return empty string
228                String::new()
229            } else {
230                match char::from_u32(code_point as u32) {
231                    Some(c) => c.to_string(),
232                    None => String::new(), // Invalid code point (e.g., surrogate)
233                }
234            };
235            unsafe { push(stack, Value::String(global_string(result))) }
236        }
237        _ => panic!("char_to_string: expected Int on stack"),
238    }
239}
240
241/// Find the first occurrence of a substring
242///
243/// Stack effect: ( str needle -- int )
244///
245/// Returns the character index of the first occurrence of needle in str.
246/// Returns -1 if not found.
247///
248/// # Safety
249/// Stack must have two Strings on top
250#[unsafe(no_mangle)]
251pub unsafe extern "C" fn patch_seq_string_find(stack: Stack) -> Stack {
252    assert!(!stack.is_null(), "string_find: stack is empty");
253
254    let (stack, needle_val) = unsafe { pop(stack) };
255    assert!(!stack.is_null(), "string_find: need string and needle");
256    let (stack, str_val) = unsafe { pop(stack) };
257
258    match (str_val, needle_val) {
259        (Value::String(haystack), Value::String(needle)) => {
260            let haystack_str = haystack.as_str_or_empty();
261            let needle_str = needle.as_str_or_empty();
262
263            // Find byte position then convert to character position
264            let result = match haystack_str.find(needle_str) {
265                Some(byte_pos) => {
266                    // Count characters up to byte_pos
267                    haystack_str[..byte_pos].chars().count() as i64
268                }
269                None => -1,
270            };
271            unsafe { push(stack, Value::Int(result)) }
272        }
273        _ => panic!("string_find: expected two Strings on stack"),
274    }
275}
276
277/// Trim whitespace from both ends of a string
278///
279/// Stack effect: ( str -- trimmed )
280///
281/// # Safety
282/// Stack must have a String value on top
283#[unsafe(no_mangle)]
284pub unsafe extern "C" fn patch_seq_string_join(stack: Stack) -> Stack {
285    unsafe {
286        // Pop separator
287        let (stack, sep_val) = pop(stack);
288        let sep = match &sep_val {
289            Value::String(s) => s.as_str_or_empty().to_owned(),
290            _ => panic!("string.join: expected String separator, got {:?}", sep_val),
291        };
292
293        // Pop list (variant)
294        let (stack, list_val) = pop(stack);
295        let variant_data = match &list_val {
296            Value::Variant(v) => v,
297            _ => panic!("string.join: expected Variant (list), got {:?}", list_val),
298        };
299
300        // Convert each element to string and join
301        let parts: Vec<String> = variant_data
302            .fields
303            .iter()
304            .map(|v| match v {
305                Value::String(s) => s.as_str_or_empty().to_owned(),
306                Value::Int(n) => n.to_string(),
307                Value::Float(f) => f.to_string(),
308                Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
309                Value::Symbol(s) => format!(":{}", s.as_str_or_empty()),
310                _ => format!("{:?}", v),
311            })
312            .collect();
313
314        let result = parts.join(&sep);
315        push(stack, Value::String(global_string(result)))
316    }
317}