submilli_engine/runtime/prelude/string/
mod.rs1mod gc_text;
12mod install;
13pub(crate) mod search;
14mod transforms;
15
16pub use install::{declare, install};
17pub(crate) use install::{declare_types, string_ctor_call};
18
19use unicode_normalization::UnicodeNormalization;
20
21use crate::runtime::host::fatal_host_error;
22
23pub struct Str(Vec<u16>);
27
28impl Str {
29 pub fn from_units(units: Vec<u16>) -> Self {
30 Self(units)
31 }
32
33 pub fn units(&self) -> &[u16] {
34 &self.0
35 }
36
37 pub fn len(&self) -> usize {
38 self.0.len()
39 }
40
41 pub fn is_empty(&self) -> bool {
42 self.0.is_empty()
43 }
44}
45
46#[derive(Debug)]
49pub struct RangeError(&'static str);
50
51impl RangeError {
52 pub fn message(&self) -> &'static str {
53 self.0
54 }
55}
56
57pub type Result<T> = std::result::Result<T, RangeError>;
58
59const MAX_RESULT_UNITS: usize = 32 * 1024 * 1024;
63
64pub(crate) fn repeat_length(len: usize, count: f64) -> Result<usize> {
74 let n = count.trunc();
75 if n <= -1.0 {
76 return Err(RangeError("Invalid count value"));
77 }
78 if n.is_nan() || n < 1.0 || len == 0 {
79 return Ok(0);
80 }
81 len.checked_mul(n as usize)
83 .filter(|&total| total <= MAX_RESULT_UNITS)
84 .ok_or(RangeError("Invalid count value"))
85}
86
87fn trunc_sat_i32(x: f64) -> i32 {
91 x as i32
92}
93
94fn checked_len(n: usize) -> Result<usize> {
97 if n > MAX_RESULT_UNITS {
98 Err(RangeError("Invalid string length"))
99 } else {
100 Ok(n)
101 }
102}
103
104fn clamp_to_len(x: i32, len: usize) -> usize {
106 if x < 0 { 0 } else { (x as usize).min(len) }
107}
108
109fn normalize_slice_index(x: i32, len: usize) -> usize {
112 if x < 0 {
113 let from_end = x as i64 + len as i64;
114 if from_end < 0 {
115 0
116 } else {
117 (from_end as usize).min(len)
118 }
119 } else {
120 (x as usize).min(len)
121 }
122}
123
124fn raw_index_of(haystack: &[u16], needle: &[u16], from: usize) -> Option<usize> {
127 search::Search::new(needle, false).find(haystack, from.min(haystack.len()))
128}
129
130pub fn char_at(s: &Str, index: f64) -> Str {
132 let units = s.units();
133 match unit_index(units.len(), index) {
134 Some(i) => Str::from_units(vec![units[i]]),
135 None => Str::from_units(Vec::new()),
136 }
137}
138
139pub fn at(s: &Str, index: f64) -> Option<Str> {
143 let units = s.units();
144 at_index(units.len(), index).map(|i| Str::from_units(vec![units[i]]))
145}
146
147pub fn char_code_at(s: &Str, index: f64) -> f64 {
149 let units = s.units();
150 unit_index(units.len(), index).map_or(f64::NAN, |i| units[i] as f64)
151}
152
153pub fn slice(s: &Str, start: f64, end: f64) -> Str {
156 let units = s.units();
157 Str::from_units(units[slice_range(units.len(), start, end)].to_vec())
158}
159
160pub fn substring(s: &Str, start: f64, end: f64) -> Str {
163 let units = s.units();
164 Str::from_units(units[substring_range(units.len(), start, end)].to_vec())
165}
166
167pub fn index_of(s: &Str, search: &Str, from: f64) -> f64 {
169 let haystack = s.units();
170 let from = clamp_to_len(trunc_sat_i32(from), haystack.len());
171 match raw_index_of(haystack, search.units(), from) {
172 Some(i) => i as f64,
173 None => -1.0,
174 }
175}
176
177pub fn last_index_of(s: &Str, search: &Str, from: f64) -> f64 {
179 let haystack = s.units();
180 let needle = search.units();
181 if needle.len() > haystack.len() {
182 return -1.0;
183 }
184 let max_start = haystack.len() - needle.len();
185 let from = clamp_to_len(trunc_sat_i32(from), max_start);
186 if needle.is_empty() {
187 return from as f64;
188 }
189 let end = from + needle.len();
190 search::Search::new(needle, true)
191 .find(&haystack[..end], 0)
192 .map_or(-1.0, |i| (end - i - needle.len()) as f64)
193}
194
195pub fn includes(s: &Str, search: &Str, from: f64) -> bool {
197 let haystack = s.units();
198 let from = clamp_to_len(trunc_sat_i32(from), haystack.len());
199 raw_index_of(haystack, search.units(), from).is_some()
200}
201
202pub fn starts_with(s: &Str, search: &Str, position: f64) -> bool {
204 let haystack = s.units();
205 let needle = search.units();
206 starts_with_window(haystack.len(), needle.len(), position)
207 .is_some_and(|window| &haystack[window] == needle)
208}
209
210pub fn ends_with(s: &Str, search: &Str, end_position: f64) -> bool {
212 let haystack = s.units();
213 let needle = search.units();
214 ends_with_window(haystack.len(), needle.len(), end_position)
215 .is_some_and(|window| &haystack[window] == needle)
216}
217
218pub(super) fn unit_index(len: usize, index: f64) -> Option<usize> {
220 let i = trunc_sat_i32(index);
221 (i >= 0 && (i as usize) < len).then_some(i as usize)
222}
223
224pub(super) fn string_index_unit(len: usize, index: f64) -> Option<usize> {
227 (index >= 0.0 && index < len as f64 && index == index.floor()).then_some(index as usize)
228}
229
230pub(super) fn at_index(len: usize, index: f64) -> Option<usize> {
232 let len = len as i64;
233 let mut i = trunc_sat_i32(index) as i64;
234 if i < 0 {
235 i += len;
236 }
237 (i >= 0 && i < len).then_some(i as usize)
238}
239
240pub(super) fn code_point(hi: u16, next: Option<u16>) -> f64 {
243 match next {
244 Some(lo) if (0xD800..=0xDBFF).contains(&hi) && (0xDC00..=0xDFFF).contains(&lo) => {
245 (0x10000 + (((hi as u32) - 0xD800) << 10) + ((lo as u32) - 0xDC00)) as f64
246 }
247 _ => hi as f64,
248 }
249}
250
251pub(super) fn slice_range(len: usize, start: f64, end: f64) -> std::ops::Range<usize> {
253 let si = normalize_slice_index(trunc_sat_i32(start), len);
254 let ei = normalize_slice_index(trunc_sat_i32(end), len);
255 si..ei.max(si)
256}
257
258pub(super) fn substr_range(len: usize, start: f64, length: f64) -> std::ops::Range<usize> {
261 let from = normalize_slice_index(trunc_sat_i32(start), len);
262 let count = clamp_to_len(trunc_sat_i32(length), len - from);
263 from..from + count
264}
265
266pub(super) fn substring_range(len: usize, start: f64, end: f64) -> std::ops::Range<usize> {
269 let a = clamp_to_len(trunc_sat_i32(start), len);
270 let b = clamp_to_len(trunc_sat_i32(end), len);
271 a.min(b)..a.max(b)
272}
273
274pub(super) fn starts_with_window(
277 len: usize,
278 needle_len: usize,
279 position: f64,
280) -> Option<std::ops::Range<usize>> {
281 let pos = clamp_to_len(trunc_sat_i32(position), len);
282 (pos + needle_len <= len).then_some(pos..pos + needle_len)
283}
284
285pub(super) fn ends_with_window(
288 len: usize,
289 needle_len: usize,
290 end_position: f64,
291) -> Option<std::ops::Range<usize>> {
292 let end = clamp_to_len(trunc_sat_i32(end_position), len);
293 end.checked_sub(needle_len).map(|start| start..end)
294}
295
296pub fn eq(a: &Str, b: &Str) -> bool {
298 a.units() == b.units()
299}
300
301pub fn cmp(a: &Str, b: &Str) -> i32 {
305 for (&au, &bu) in a.units().iter().zip(b.units()) {
306 let diff = i32::from(au) - i32::from(bu);
307 if diff != 0 {
308 return diff;
309 }
310 }
311 a.len() as i32 - b.len() as i32
312}
313
314pub(crate) fn concat_length(left: usize, right: usize) -> wasmtime::Result<usize> {
316 left.checked_add(right)
317 .ok_or_else(|| fatal_host_error("string length overflow"))
318}
319
320pub(crate) fn pad_length(len: usize, target_length: f64, pad_len: usize) -> Result<usize> {
324 let target = trunc_sat_i32(target_length);
325 if target <= 0 || target as usize <= len || pad_len == 0 {
326 return Ok(len);
327 }
328 checked_len(target as usize)
329}
330
331pub fn is_well_formed(s: &Str) -> bool {
333 let u = s.units();
334 let mut i = 0;
335 while i < u.len() {
336 let c = u[i];
337 if (c & 0xFC00) == 0xD800 {
338 if i + 1 < u.len() && (u[i + 1] & 0xFC00) == 0xDC00 {
339 i += 2;
340 } else {
341 return false;
342 }
343 } else if (c & 0xFC00) == 0xDC00 {
344 return false;
345 } else {
346 i += 1;
347 }
348 }
349 true
350}
351
352pub fn to_well_formed(s: &Str) -> Str {
354 let mut out = s.units().to_vec();
355 let len = out.len();
356 let mut i = 0;
357 while i < len {
358 let c = out[i];
359 if (c & 0xFC00) == 0xD800 {
360 if i + 1 < len && (out[i + 1] & 0xFC00) == 0xDC00 {
361 i += 2;
362 } else {
363 out[i] = 0xFFFD;
364 i += 1;
365 }
366 } else if (c & 0xFC00) == 0xDC00 {
367 out[i] = 0xFFFD;
368 i += 1;
369 } else {
370 i += 1;
371 }
372 }
373 Str::from_units(out)
374}
375
376pub fn normalize(s: &Str, form: &Str) -> Result<Str> {
381 let form: fn(&str) -> String = match String::from_utf16_lossy(form.units()).as_str() {
382 "NFC" => |text| text.nfc().collect(),
383 "NFD" => |text| text.nfd().collect(),
384 "NFKC" => |text| text.nfkc().collect(),
385 "NFKD" => |text| text.nfkd().collect(),
386 _ => {
387 return Err(RangeError(
388 "String.normalize: invalid form — must be 'NFC', 'NFD', 'NFKC', or 'NFKD'",
389 ));
390 }
391 };
392 let mut out = Vec::with_capacity(s.units().len());
393 let mut run = String::new();
394 for scalar in char::decode_utf16(s.units().iter().copied()) {
395 match scalar {
396 Ok(c) => run.push(c),
397 Err(lone) => {
398 out.extend(form(&run).encode_utf16());
399 run.clear();
400 out.push(lone.unpaired_surrogate());
401 }
402 }
403 }
404 out.extend(form(&run).encode_utf16());
405 Ok(Str::from_units(out))
406}
407
408pub fn from_char_code(codes: &[f64]) -> Str {
411 Str::from_units(
412 codes
413 .iter()
414 .map(|&x| crate::runtime::number::to_uint16(x))
415 .collect(),
416 )
417}
418
419pub fn from_code_point(codes: &[f64]) -> Result<Str> {
422 let mut out = Vec::with_capacity(codes.len());
423 for &cpf in codes {
424 let cp = trunc_sat_i32(cpf);
425 if !(0..=0x10FFFF).contains(&cp) || cpf != cp as f64 {
426 return Err(RangeError("invalid code point"));
427 }
428 if cp < 0x10000 {
429 out.push(cp as u16);
430 } else {
431 let v = (cp - 0x10000) as u32;
432 out.push((0xD800 + (v >> 10)) as u16);
433 out.push((0xDC00 + (v & 0x3FF)) as u16);
434 }
435 }
436 Ok(Str::from_units(out))
437}
438
439#[cfg(test)]
440mod length_tests {
441 use super::*;
442
443 #[test]
444 fn length_validation_preserves_range_boundaries() {
445 assert_eq!(
446 repeat_length(1, MAX_RESULT_UNITS as f64).unwrap(),
447 MAX_RESULT_UNITS
448 );
449 for count in [
450 -1.0,
451 f64::NEG_INFINITY,
452 f64::INFINITY,
453 f64::MAX,
454 (MAX_RESULT_UNITS + 1) as f64,
455 ] {
456 assert!(repeat_length(1, count).is_err());
457 }
458 for count in [-0.5, 0.0, 0.5, f64::NAN] {
459 assert_eq!(repeat_length(1, count).unwrap(), 0);
460 }
461 assert_eq!(repeat_length(2, 2.9).unwrap(), 4);
462 assert_eq!(repeat_length(0, f64::INFINITY).unwrap(), 0);
463 assert!(repeat_length(usize::MAX, 2.0).is_err());
464 assert_eq!(
465 pad_length(1, MAX_RESULT_UNITS as f64, 1).unwrap(),
466 MAX_RESULT_UNITS
467 );
468 assert!(pad_length(1, (MAX_RESULT_UNITS + 1) as f64, 1).is_err());
469 assert_eq!(pad_length(2, f64::INFINITY, 0).unwrap(), 2);
470 assert_eq!(pad_length(2, f64::NAN, 1).unwrap(), 2);
471 assert_eq!(pad_length(2, -1.0, 1).unwrap(), 2);
472 assert_eq!(pad_length(2, 4.9, 1).unwrap(), 4);
473 }
474
475 #[test]
476 fn concat_length_overflow_is_fatal() {
477 assert_eq!(concat_length(2, 3).unwrap(), 5);
478 assert!(concat_length(usize::MAX, 1).is_err());
479 }
480}