Skip to main content

aver/types/
string.rs

1/// String namespace — text manipulation helpers.
2///
3/// Methods:
4///   String.len(s)               → Int            — char count (code points)
5///   String.byteLength(s)        → Int            — byte count (UTF-8)
6///   String.startsWith(s, pre)   → Bool
7///   String.endsWith(s, suf)     → Bool
8///   String.contains(s, sub)     → Bool
9///   String.slice(s, from, to)   → String         — code-point based substring
10///   String.trim(s)              → String
11///   String.split(s, delim)      → List<String>
12///   String.replace(s, old, new) → String
13///   String.join(list, sep)      → String
14///   String.charAt(s, index)     → Option<String>  — O(1)-ish char at code-point index
15///   String.chars(s)             → List<String>   — each char as 1-char string
16///   String.fromInt(n)           → String
17///   String.fromFloat(f)         → String
18///   String.fromBool(b)          → String
19///   String.toLower(s)           → String         — lowercase (Unicode-aware)
20///   String.toUpper(s)           → String         — uppercase (Unicode-aware)
21///
22/// No effects required.
23use std::collections::HashMap;
24use std::sync::Arc as Rc;
25
26use crate::nan_value::{Arena, NanIntExt, NanString, NanValue};
27use crate::value::{RuntimeError, Value, list_from_vec, list_view};
28
29pub fn register(global: &mut HashMap<String, Value>) {
30    let mut members = HashMap::new();
31    for method in &[
32        "len",
33        "byteLength",
34        "startsWith",
35        "endsWith",
36        "contains",
37        "slice",
38        "trim",
39        "split",
40        "replace",
41        "join",
42        "charAt",
43        "chars",
44        "fromInt",
45        "fromFloat",
46        "fromBool",
47        "toLower",
48        "toUpper",
49    ] {
50        members.insert(
51            method.to_string(),
52            Value::Builtin(format!("String.{}", method)),
53        );
54    }
55    global.insert(
56        "String".to_string(),
57        Value::Namespace {
58            name: "String".to_string(),
59            members,
60        },
61    );
62}
63
64pub fn effects(_name: &str) -> &'static [&'static str] {
65    &[]
66}
67
68/// Returns `Some(result)` when `name` is owned by this namespace, `None` otherwise.
69pub fn call(name: &str, args: &[Value]) -> Option<Result<Value, RuntimeError>> {
70    match name {
71        "String.len" => Some(length(args)),
72        "String.byteLength" => Some(byte_length(args)),
73        "String.startsWith" => Some(starts_with(args)),
74        "String.endsWith" => Some(ends_with(args)),
75        "String.contains" => Some(contains(args)),
76        "String.slice" => Some(slice(args)),
77        "String.trim" => Some(trim(args)),
78        "String.split" => Some(split(args)),
79        "String.replace" => Some(replace(args)),
80        "String.join" => Some(join(args)),
81        "String.charAt" => Some(char_at(args)),
82        "String.chars" => Some(chars(args)),
83        "String.fromInt" => Some(from_int(args)),
84        "String.fromFloat" => Some(from_float(args)),
85        "String.fromBool" => Some(from_bool(args)),
86        "String.toLower" => Some(to_lower(args)),
87        "String.toUpper" => Some(to_upper(args)),
88        _ => None,
89    }
90}
91
92// ─── Implementations ────────────────────────────────────────────────────────
93
94fn length(args: &[Value]) -> Result<Value, RuntimeError> {
95    let [val] = one_arg("String.len", args)?;
96    let Value::Str(s) = val else {
97        return Err(RuntimeError::Error(
98            "String.len: argument must be a String".to_string(),
99        ));
100    };
101    Ok(Value::int(s.chars().count() as i64))
102}
103
104fn byte_length(args: &[Value]) -> Result<Value, RuntimeError> {
105    let [val] = one_arg("String.byteLength", args)?;
106    let Value::Str(s) = val else {
107        return Err(RuntimeError::Error(
108            "String.byteLength: argument must be a String".to_string(),
109        ));
110    };
111    Ok(Value::int(s.len() as i64))
112}
113
114fn starts_with(args: &[Value]) -> Result<Value, RuntimeError> {
115    let [a, b] = two_args("String.startsWith", args)?;
116    let (Value::Str(s), Value::Str(prefix)) = (a, b) else {
117        return Err(RuntimeError::Error(
118            "String.startsWith: both arguments must be String".to_string(),
119        ));
120    };
121    Ok(Value::Bool(s.starts_with(prefix.as_str())))
122}
123
124fn ends_with(args: &[Value]) -> Result<Value, RuntimeError> {
125    let [a, b] = two_args("String.endsWith", args)?;
126    let (Value::Str(s), Value::Str(suffix)) = (a, b) else {
127        return Err(RuntimeError::Error(
128            "String.endsWith: both arguments must be String".to_string(),
129        ));
130    };
131    Ok(Value::Bool(s.ends_with(suffix.as_str())))
132}
133
134fn contains(args: &[Value]) -> Result<Value, RuntimeError> {
135    let [a, b] = two_args("String.contains", args)?;
136    let (Value::Str(s), Value::Str(sub)) = (a, b) else {
137        return Err(RuntimeError::Error(
138            "String.contains: both arguments must be String".to_string(),
139        ));
140    };
141    Ok(Value::Bool(s.contains(sub.as_str())))
142}
143
144fn slice(args: &[Value]) -> Result<Value, RuntimeError> {
145    if args.len() != 3 {
146        return Err(RuntimeError::Error(format!(
147            "String.slice() takes 3 arguments (s, from, to), got {}",
148            args.len()
149        )));
150    }
151    let Value::Str(s) = &args[0] else {
152        return Err(RuntimeError::Error(
153            "String.slice: first argument must be a String".to_string(),
154        ));
155    };
156    let Value::Int(from) = &args[1] else {
157        return Err(RuntimeError::Error(
158            "String.slice: second argument must be an Int".to_string(),
159        ));
160    };
161    let Value::Int(to) = &args[2] else {
162        return Err(RuntimeError::Error(
163            "String.slice: third argument must be an Int".to_string(),
164        ));
165    };
166    // `string_slice` clamps to `[0, len]`, so a ℤ index outside `i64` simply
167    // saturates to the same edge it would reach as a machine int.
168    Ok(Value::Str(aver_rt::string_slice(
169        s,
170        saturate_index(from),
171        saturate_index(to),
172    )))
173}
174
175/// Saturate an `Int` index to `i64` for an API that clamps to a length:
176/// a magnitude past `i64` lands on the same boundary (`MIN`/`MAX`) the clamp
177/// would reach anyway. No truncation, no panic.
178fn saturate_index(n: &aver_rt::AverInt) -> i64 {
179    match n.to_i64() {
180        Some(v) => v,
181        None if *n > aver_rt::AverInt::zero() => i64::MAX,
182        None => i64::MIN,
183    }
184}
185
186fn trim(args: &[Value]) -> Result<Value, RuntimeError> {
187    let [val] = one_arg("String.trim", args)?;
188    let Value::Str(s) = val else {
189        return Err(RuntimeError::Error(
190            "String.trim: argument must be a String".to_string(),
191        ));
192    };
193    Ok(Value::Str(s.trim().to_string()))
194}
195
196fn split(args: &[Value]) -> Result<Value, RuntimeError> {
197    let [a, b] = two_args("String.split", args)?;
198    let (Value::Str(s), Value::Str(delim)) = (a, b) else {
199        return Err(RuntimeError::Error(
200            "String.split: both arguments must be String".to_string(),
201        ));
202    };
203    let parts: Vec<Value> = s
204        .split(delim.as_str())
205        .map(|p| Value::Str(p.to_string()))
206        .collect();
207    Ok(list_from_vec(parts))
208}
209
210fn replace(args: &[Value]) -> Result<Value, RuntimeError> {
211    if args.len() != 3 {
212        return Err(RuntimeError::Error(format!(
213            "String.replace() takes 3 arguments (s, old, new), got {}",
214            args.len()
215        )));
216    }
217    let (Value::Str(s), Value::Str(old), Value::Str(new)) = (&args[0], &args[1], &args[2]) else {
218        return Err(RuntimeError::Error(
219            "String.replace: all arguments must be String".to_string(),
220        ));
221    };
222    Ok(Value::Str(s.replace(old.as_str(), new.as_str())))
223}
224
225fn join(args: &[Value]) -> Result<Value, RuntimeError> {
226    let [a, b] = two_args("String.join", args)?;
227    let items = list_view(a).ok_or_else(|| {
228        RuntimeError::Error("String.join: first argument must be a List".to_string())
229    })?;
230    let Value::Str(sep) = b else {
231        return Err(RuntimeError::Error(
232            "String.join: second argument must be a String".to_string(),
233        ));
234    };
235    let strs: Result<Vec<String>, RuntimeError> = items
236        .iter()
237        .map(|v| match v {
238            Value::Str(s) => Ok(s.clone()),
239            _ => Err(RuntimeError::Error(
240                "String.join: list elements must be String".to_string(),
241            )),
242        })
243        .collect();
244    Ok(Value::Str(strs?.join(sep.as_str())))
245}
246
247fn char_at(args: &[Value]) -> Result<Value, RuntimeError> {
248    let [a, b] = two_args("String.charAt", args)?;
249    let Value::Str(s) = a else {
250        return Err(RuntimeError::Error(
251            "String.charAt: first argument must be a String".to_string(),
252        ));
253    };
254    let Value::Int(idx) = b else {
255        return Err(RuntimeError::Error(
256            "String.charAt: second argument must be an Int".to_string(),
257        ));
258    };
259    let Some(idx) = idx.to_usize() else {
260        return Ok(Value::None);
261    };
262    match s.chars().nth(idx) {
263        Some(c) => Ok(Value::Some(Box::new(Value::Str(c.to_string())))),
264        None => Ok(Value::None),
265    }
266}
267
268fn chars(args: &[Value]) -> Result<Value, RuntimeError> {
269    let [val] = one_arg("String.chars", args)?;
270    let Value::Str(s) = val else {
271        return Err(RuntimeError::Error(
272            "String.chars: argument must be a String".to_string(),
273        ));
274    };
275    let result: Vec<Value> = s.chars().map(|c| Value::Str(c.to_string())).collect();
276    Ok(list_from_vec(result))
277}
278
279fn from_int(args: &[Value]) -> Result<Value, RuntimeError> {
280    let [val] = one_arg("String.fromInt", args)?;
281    let Value::Int(n) = val else {
282        return Err(RuntimeError::Error(
283            "String.fromInt: argument must be an Int".to_string(),
284        ));
285    };
286    Ok(Value::Str(format!("{}", n)))
287}
288
289fn from_float(args: &[Value]) -> Result<Value, RuntimeError> {
290    let [val] = one_arg("String.fromFloat", args)?;
291    let Value::Float(f) = val else {
292        return Err(RuntimeError::Error(
293            "String.fromFloat: argument must be a Float".to_string(),
294        ));
295    };
296    Ok(Value::Str(format!("{}", f)))
297}
298
299fn from_bool(args: &[Value]) -> Result<Value, RuntimeError> {
300    let [val] = one_arg("String.fromBool", args)?;
301    let Value::Bool(b) = val else {
302        return Err(RuntimeError::Error(
303            "String.fromBool: argument must be a Bool".to_string(),
304        ));
305    };
306    Ok(Value::Str(if *b { "true" } else { "false" }.to_string()))
307}
308
309fn to_lower(args: &[Value]) -> Result<Value, RuntimeError> {
310    let [val] = one_arg("String.toLower", args)?;
311    let Value::Str(s) = val else {
312        return Err(RuntimeError::Error(
313            "String.toLower: argument must be a String".to_string(),
314        ));
315    };
316    Ok(Value::Str(s.to_lowercase()))
317}
318
319fn to_upper(args: &[Value]) -> Result<Value, RuntimeError> {
320    let [val] = one_arg("String.toUpper", args)?;
321    let Value::Str(s) = val else {
322        return Err(RuntimeError::Error(
323            "String.toUpper: argument must be a String".to_string(),
324        ));
325    };
326    Ok(Value::Str(s.to_uppercase()))
327}
328
329// ─── Helpers ────────────────────────────────────────────────────────────────
330
331fn one_arg<'a>(name: &str, args: &'a [Value]) -> Result<[&'a Value; 1], RuntimeError> {
332    if args.len() != 1 {
333        return Err(RuntimeError::Error(format!(
334            "{}() takes 1 argument, got {}",
335            name,
336            args.len()
337        )));
338    }
339    Ok([&args[0]])
340}
341
342fn two_args<'a>(name: &str, args: &'a [Value]) -> Result<[&'a Value; 2], RuntimeError> {
343    if args.len() != 2 {
344        return Err(RuntimeError::Error(format!(
345            "{}() takes 2 arguments, got {}",
346            name,
347            args.len()
348        )));
349    }
350    Ok([&args[0], &args[1]])
351}
352
353// ─── NanValue-native API ─────────────────────────────────────────────────────
354
355pub fn register_nv(global: &mut HashMap<String, NanValue>, arena: &mut Arena) {
356    let methods = &[
357        "len",
358        "byteLength",
359        "startsWith",
360        "endsWith",
361        "contains",
362        "slice",
363        "trim",
364        "split",
365        "replace",
366        "join",
367        "charAt",
368        "chars",
369        "fromInt",
370        "fromFloat",
371        "fromBool",
372        "toLower",
373        "toUpper",
374    ];
375    let mut members: Vec<(Rc<str>, NanValue)> = Vec::with_capacity(methods.len());
376    for method in methods {
377        let idx = arena.push_builtin(&format!("String.{}", method));
378        members.push((Rc::from(*method), NanValue::new_builtin(idx)));
379    }
380    let ns_idx = arena.push(crate::nan_value::ArenaEntry::Namespace {
381        name: Rc::from("String"),
382        members,
383    });
384    global.insert("String".to_string(), NanValue::new_namespace(ns_idx));
385}
386
387pub fn call_nv(
388    name: &str,
389    args: &[NanValue],
390    arena: &mut Arena,
391) -> Option<Result<NanValue, RuntimeError>> {
392    match name {
393        "String.len" => Some(length_nv(args, arena)),
394        "String.byteLength" => Some(byte_length_nv(args, arena)),
395        "String.startsWith" => Some(starts_with_nv(args, arena)),
396        "String.endsWith" => Some(ends_with_nv(args, arena)),
397        "String.contains" => Some(contains_nv(args, arena)),
398        "String.slice" => Some(slice_nv(args, arena)),
399        "String.trim" => Some(trim_nv(args, arena)),
400        "String.split" => Some(split_nv(args, arena)),
401        "String.replace" => Some(replace_nv(args, arena)),
402        "String.join" => Some(join_nv(args, arena)),
403        "String.charAt" => Some(char_at_nv(args, arena)),
404        "String.chars" => Some(chars_nv(args, arena)),
405        "String.fromInt" => Some(from_int_nv(args, arena)),
406        "String.fromFloat" => Some(from_float_nv(args, arena)),
407        "String.fromBool" => Some(from_bool_nv(args, arena)),
408        "String.toLower" => Some(to_lower_nv(args, arena)),
409        "String.toUpper" => Some(to_upper_nv(args, arena)),
410        _ => None,
411    }
412}
413
414fn nv_str(v: NanValue, arena: &Arena) -> Option<NanString<'_>> {
415    if v.is_string() {
416        Some(arena.get_string_value(v))
417    } else {
418        None
419    }
420}
421
422fn length_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
423    if args.len() != 1 {
424        return Err(RuntimeError::Error(format!(
425            "String.len() takes 1 argument, got {}",
426            args.len()
427        )));
428    }
429    let s = nv_str(args[0], arena)
430        .ok_or_else(|| RuntimeError::Error("String.len: argument must be a String".to_string()))?;
431    Ok(NanValue::new_int(s.chars().count() as i64, arena))
432}
433
434fn byte_length_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
435    if args.len() != 1 {
436        return Err(RuntimeError::Error(format!(
437            "String.byteLength() takes 1 argument, got {}",
438            args.len()
439        )));
440    }
441    let s = nv_str(args[0], arena).ok_or_else(|| {
442        RuntimeError::Error("String.byteLength: argument must be a String".to_string())
443    })?;
444    Ok(NanValue::new_int(s.len() as i64, arena))
445}
446
447fn starts_with_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
448    if args.len() != 2 {
449        return Err(RuntimeError::Error(format!(
450            "String.startsWith() takes 2 arguments, got {}",
451            args.len()
452        )));
453    }
454    if !args[0].is_string() || !args[1].is_string() {
455        return Err(RuntimeError::Error(
456            "String.startsWith: both arguments must be String".to_string(),
457        ));
458    }
459    let s = arena.get_string_value(args[0]);
460    let prefix = arena.get_string_value(args[1]);
461    let result = s.starts_with(prefix.as_str());
462    Ok(NanValue::new_bool(result))
463}
464
465fn ends_with_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
466    if args.len() != 2 {
467        return Err(RuntimeError::Error(format!(
468            "String.endsWith() takes 2 arguments, got {}",
469            args.len()
470        )));
471    }
472    if !args[0].is_string() || !args[1].is_string() {
473        return Err(RuntimeError::Error(
474            "String.endsWith: both arguments must be String".to_string(),
475        ));
476    }
477    let s = arena.get_string_value(args[0]);
478    let suffix = arena.get_string_value(args[1]);
479    let result = s.ends_with(suffix.as_str());
480    Ok(NanValue::new_bool(result))
481}
482
483fn contains_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
484    if args.len() != 2 {
485        return Err(RuntimeError::Error(format!(
486            "String.contains() takes 2 arguments, got {}",
487            args.len()
488        )));
489    }
490    if !args[0].is_string() || !args[1].is_string() {
491        return Err(RuntimeError::Error(
492            "String.contains: both arguments must be String".to_string(),
493        ));
494    }
495    let s = arena.get_string_value(args[0]);
496    let sub = arena.get_string_value(args[1]);
497    let result = s.contains(sub.as_str());
498    Ok(NanValue::new_bool(result))
499}
500
501fn slice_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
502    if args.len() != 3 {
503        return Err(RuntimeError::Error(format!(
504            "String.slice() takes 3 arguments (s, from, to), got {}",
505            args.len()
506        )));
507    }
508    if !args[0].is_string() {
509        return Err(RuntimeError::Error(
510            "String.slice: first argument must be a String".to_string(),
511        ));
512    }
513    if !args[1].is_int() {
514        return Err(RuntimeError::Error(
515            "String.slice: second argument must be an Int".to_string(),
516        ));
517    }
518    if !args[2].is_int() {
519        return Err(RuntimeError::Error(
520            "String.slice: third argument must be an Int".to_string(),
521        ));
522    }
523    let s = arena.get_string_value(args[0]).to_string();
524    let from = saturate_index(&args[1].as_aver_int(arena));
525    let to = saturate_index(&args[2].as_aver_int(arena));
526    let result = aver_rt::string_slice(&s, from, to);
527    Ok(NanValue::new_string_value(&result, arena))
528}
529
530fn trim_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
531    if args.len() != 1 {
532        return Err(RuntimeError::Error(format!(
533            "String.trim() takes 1 argument, got {}",
534            args.len()
535        )));
536    }
537    let s = nv_str(args[0], arena)
538        .ok_or_else(|| RuntimeError::Error("String.trim: argument must be a String".to_string()))?;
539    let result = s.trim().to_string();
540    Ok(NanValue::new_string_value(&result, arena))
541}
542
543fn split_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
544    if args.len() != 2 {
545        return Err(RuntimeError::Error(format!(
546            "String.split() takes 2 arguments, got {}",
547            args.len()
548        )));
549    }
550    if !args[0].is_string() || !args[1].is_string() {
551        return Err(RuntimeError::Error(
552            "String.split: both arguments must be String".to_string(),
553        ));
554    }
555    let s = arena.get_string_value(args[0]).to_string();
556    let delim = arena.get_string_value(args[1]).to_string();
557    let parts: Vec<NanValue> = s
558        .split(&*delim)
559        .map(|p| NanValue::new_string_value(p, arena))
560        .collect();
561    let list_idx = arena.push_list(parts);
562    Ok(NanValue::new_list(list_idx))
563}
564
565fn replace_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
566    if args.len() != 3 {
567        return Err(RuntimeError::Error(format!(
568            "String.replace() takes 3 arguments (s, old, new), got {}",
569            args.len()
570        )));
571    }
572    if !args[0].is_string() || !args[1].is_string() || !args[2].is_string() {
573        return Err(RuntimeError::Error(
574            "String.replace: all arguments must be String".to_string(),
575        ));
576    }
577    let s = arena.get_string_value(args[0]).to_string();
578    let old = arena.get_string_value(args[1]).to_string();
579    let new = arena.get_string_value(args[2]).to_string();
580    let result = s.replace(&*old, &new);
581    Ok(NanValue::new_string_value(&result, arena))
582}
583
584fn join_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
585    if args.len() != 2 {
586        return Err(RuntimeError::Error(format!(
587            "String.join() takes 2 arguments, got {}",
588            args.len()
589        )));
590    }
591    if !args[0].is_list() {
592        return Err(RuntimeError::Error(
593            "String.join: first argument must be a List".to_string(),
594        ));
595    }
596    if !args[1].is_string() {
597        return Err(RuntimeError::Error(
598            "String.join: second argument must be a String".to_string(),
599        ));
600    }
601    let items = arena.list_to_vec_value(args[0]);
602    let sep = arena.get_string_value(args[1]).to_string();
603    let mut strs: Vec<String> = Vec::with_capacity(items.len());
604    for item in &items {
605        if !item.is_string() {
606            return Err(RuntimeError::Error(
607                "String.join: list elements must be String".to_string(),
608            ));
609        }
610        strs.push(arena.get_string_value(*item).to_string());
611    }
612    let result = strs.join(&sep);
613    Ok(NanValue::new_string_value(&result, arena))
614}
615
616fn char_at_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
617    if args.len() != 2 {
618        return Err(RuntimeError::Error(format!(
619            "String.charAt() takes 2 arguments, got {}",
620            args.len()
621        )));
622    }
623    if !args[0].is_string() {
624        return Err(RuntimeError::Error(
625            "String.charAt: first argument must be a String".to_string(),
626        ));
627    }
628    if !args[1].is_int() {
629        return Err(RuntimeError::Error(
630            "String.charAt: second argument must be an Int".to_string(),
631        ));
632    }
633    let Some(idx_val) = args[1].as_aver_int(arena).to_usize() else {
634        return Ok(NanValue::NONE);
635    };
636    let s = arena.get_string_value(args[0]);
637    match s.chars().nth(idx_val) {
638        Some(c) => {
639            let cs = c.to_string();
640            let inner = NanValue::new_string_value(&cs, arena);
641            Ok(NanValue::new_some_value(inner, arena))
642        }
643        None => Ok(NanValue::NONE),
644    }
645}
646
647fn chars_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
648    if args.len() != 1 {
649        return Err(RuntimeError::Error(format!(
650            "String.chars() takes 1 argument, got {}",
651            args.len()
652        )));
653    }
654    if !args[0].is_string() {
655        return Err(RuntimeError::Error(
656            "String.chars: argument must be a String".to_string(),
657        ));
658    }
659    let s = arena.get_string_value(args[0]).to_string();
660    let items: Vec<NanValue> = s
661        .chars()
662        .map(|c| NanValue::new_string_value(&c.to_string(), arena))
663        .collect();
664    let list_idx = arena.push_list(items);
665    Ok(NanValue::new_list(list_idx))
666}
667
668fn from_int_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
669    if args.len() != 1 {
670        return Err(RuntimeError::Error(format!(
671            "String.fromInt() takes 1 argument, got {}",
672            args.len()
673        )));
674    }
675    if !args[0].is_int() {
676        return Err(RuntimeError::Error(
677            "String.fromInt: argument must be an Int".to_string(),
678        ));
679    }
680    // Exact decimal of the full ℤ value (bignum-aware via `AverInt: Display`).
681    let s = format!("{}", args[0].as_aver_int(arena));
682    Ok(NanValue::new_string_value(&s, arena))
683}
684
685fn from_float_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
686    if args.len() != 1 {
687        return Err(RuntimeError::Error(format!(
688            "String.fromFloat() takes 1 argument, got {}",
689            args.len()
690        )));
691    }
692    if !args[0].is_float() {
693        return Err(RuntimeError::Error(
694            "String.fromFloat: argument must be a Float".to_string(),
695        ));
696    }
697    let s = format!("{}", args[0].as_float());
698    Ok(NanValue::new_string_value(&s, arena))
699}
700
701fn from_bool_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
702    if args.len() != 1 {
703        return Err(RuntimeError::Error(format!(
704            "String.fromBool() takes 1 argument, got {}",
705            args.len()
706        )));
707    }
708    if !args[0].is_bool() {
709        return Err(RuntimeError::Error(
710            "String.fromBool: argument must be a Bool".to_string(),
711        ));
712    }
713    let s = if args[0].as_bool() { "true" } else { "false" };
714    Ok(NanValue::new_string_value(s, arena))
715}
716
717fn to_lower_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
718    if args.len() != 1 {
719        return Err(RuntimeError::Error(format!(
720            "String.toLower() takes 1 argument, got {}",
721            args.len()
722        )));
723    }
724    if !args[0].is_string() {
725        return Err(RuntimeError::Error(
726            "String.toLower: argument must be a String".to_string(),
727        ));
728    }
729    let s = arena.get_string_value(args[0]).to_lowercase();
730    Ok(NanValue::new_string_value(&s, arena))
731}
732
733fn to_upper_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
734    if args.len() != 1 {
735        return Err(RuntimeError::Error(format!(
736            "String.toUpper() takes 1 argument, got {}",
737            args.len()
738        )));
739    }
740    if !args[0].is_string() {
741        return Err(RuntimeError::Error(
742            "String.toUpper: argument must be a String".to_string(),
743        ));
744    }
745    let s = arena.get_string_value(args[0]).to_uppercase();
746    Ok(NanValue::new_string_value(&s, arena))
747}