Skip to main content

submilli_engine/runtime/prelude/string/
install.rs

1//! ABI wiring for the Rust `String` methods: marshals `$string` ↔ [`Str`],
2//! registers each method under its dispatch key, and declares the value symbols
3//! codegen routes through. The string operations themselves live in the parent
4//! module — this file is the wasmtime boundary they're kept clear of.
5
6use crate::runtime::host::{abi_arg, abi_result};
7use wasmtime::{
8    ArrayRef, ArrayRefPre, ArrayType, Caller, FuncType, HeapType, Linker, RefType, Rooted,
9    StructRef, StructRefPre, StructType, Val, ValType,
10};
11
12use super::gc_text::{GcText, GcUnits, Run, allocate, fill_run};
13use super::transforms::{TrimEnds, to_lower_case, to_upper_case, trim_range};
14use super::{
15    RangeError, Str, at_index, cmp, code_point, concat_length, ends_with_window, from_char_code,
16    from_code_point, includes, index_of, is_well_formed, last_index_of, normalize, pad_length,
17    repeat_length, slice_range, starts_with_window, substring_range, to_well_formed, unit_index,
18};
19use crate::runtime::StoreData;
20use crate::runtime::fuel;
21use crate::runtime::host::{
22    fatal_host_error, intrinsic_string_type, range_error, read_code_unit, read_code_units,
23    read_code_units_range, register_host_fn, register_host_fn_async, string_array_type,
24    write_boxed_number_struct, write_submilli_string_struct, write_submilli_string_struct_units,
25};
26use crate::runtime::intrinsic_types::{build_intrinsic_types, intrinsic_types};
27use crate::runtime::prelude::{MODULE_NAME, declare_method};
28use crate::{MangledName, PackageDeclaration, Param, Type};
29
30/// The dispatch key codegen looks up for `String#<method>`: the prelude's
31/// `String` interface mangled name extended by the method. The host fn's linker
32/// field and the value symbol's mangled name must both be exactly this.
33fn method_key(method: &str) -> MangledName {
34    crate::mangle::extend(&crate::mangle::prelude("String"), method)
35}
36
37/// The dispatch key for a `StringConstructor` static (`String.fromCharCode`,
38/// `String.fromCodePoint`) — the `StringConstructor` interface mangled name
39/// extended by the method.
40fn ctor_key(method: &str) -> MangledName {
41    crate::mangle::extend(&crate::mangle::prelude("StringConstructor"), method)
42}
43
44pub fn install(linker: &mut Linker<StoreData>) -> wasmtime::Result<()> {
45    let engine = linker.engine().clone();
46    let abi = StringAbi::recover(&engine)?;
47
48    // Accessors read the few units they need straight from the payload
49    // instead of copying the whole string.
50    reg_unit_to_str(linker, &engine, &abi, "charAt", unit_index)?;
51    reg_unit_to_str_or_undefined(linker, &engine, &abi, "at", at_index)?;
52    reg_char_code_at(linker, &engine, &abi)?;
53    reg_code_point_at(linker, &engine, &abi)?;
54
55    reg_range_to_str(linker, &engine, &abi, "slice", slice_range)?;
56    reg_range_to_str(linker, &engine, &abi, "substring", substring_range)?;
57
58    reg_str_str_num_to_num(linker, &engine, &abi, "indexOf", index_of)?;
59    reg_str_str_num_to_num(linker, &engine, &abi, "lastIndexOf", last_index_of)?;
60    register_string_search_predicate(linker, &engine, &abi, "includes", includes)?;
61    reg_window_predicate(linker, &engine, &abi, "startsWith", starts_with_window)?;
62    reg_window_predicate(linker, &engine, &abi, "endsWith", ends_with_window)?;
63
64    reg_concat(linker, &engine, &abi)?;
65    reg_pad(linker, &engine, &abi, "padStart", PadSide::Start)?;
66    reg_pad(linker, &engine, &abi, "padEnd", PadSide::End)?;
67
68    reg_str_to_bool(linker, &engine, &abi, "isWellFormed", is_well_formed)?;
69    reg_str_to_str(linker, &engine, &abi, "toWellFormed", to_well_formed)?;
70
71    super::transforms::prepare_case_properties()?;
72    // Closures, because a generic `fn(&mut impl Text)` is not a fn pointer for
73    // every `GcText` lifetime.
74    reg_case_map(linker, &engine, &abi, "toUpperCase", |text| {
75        to_upper_case(text)
76    })?;
77    reg_case_map(linker, &engine, &abi, "toLowerCase", |text| {
78        to_lower_case(text)
79    })?;
80    reg_trim(linker, &engine, &abi, "trim", TrimEnds::Both)?;
81    reg_trim(linker, &engine, &abi, "trimStart", TrimEnds::Start)?;
82    reg_trim(linker, &engine, &abi, "trimEnd", TrimEnds::End)?;
83    reg_str_str_to_str_fallible(linker, &engine, &abi, "normalize", normalize)?;
84
85    reg_repeat(linker, &engine, &abi)?;
86
87    // StringConstructor statics: `Dispatch::Static`, so the receiver is dropped
88    // and the variadic args arrive packed into one `$Array` of boxed numbers.
89    let array_ref = ValType::Ref(RefType::new(
90        false,
91        HeapType::ConcreteStruct(build_intrinsic_types(&engine)?.array),
92    ));
93    register_host_fn(
94        linker,
95        MODULE_NAME,
96        ctor_key("fromCharCode"),
97        FuncType::new(&engine, [array_ref.clone()], [abi.value_type()]),
98        true,
99        move |caller, params, results| {
100            let codes = read_number_array(caller, abi_arg(params, 0)?, "String.fromCharCode")?;
101            let out = from_char_code(&codes);
102            let st = write_submilli_string_struct_units(caller, out.units())?;
103            *abi_result(results, 0)? = Val::AnyRef(Some(st.to_anyref()));
104            Ok(())
105        },
106    )?;
107    register_host_fn(
108        linker,
109        MODULE_NAME,
110        ctor_key("fromCodePoint"),
111        FuncType::new(&engine, [array_ref], [abi.value_type()]),
112        true,
113        move |caller, params, results| {
114            let codes = read_number_array(caller, abi_arg(params, 0)?, "String.fromCodePoint")?;
115            let out = from_code_point(&codes).map_err(throw)?;
116            let st = write_submilli_string_struct_units(caller, out.units())?;
117            *abi_result(results, 0)? = Val::AnyRef(Some(st.to_anyref()));
118            Ok(())
119        },
120    )?;
121
122    // Identity/serialization/comparison members (their vtable-slot twins live
123    // in `vtable.rs`; these are the direct-dispatch method keys).
124    register_host_fn(
125        linker,
126        MODULE_NAME,
127        method_key("toString"),
128        FuncType::new(&engine, [abi.value_type()], [abi.value_type()]),
129        true,
130        |_caller, params, results| {
131            *abi_result(results, 0)? = *abi_arg(params, 0)?;
132            Ok(())
133        },
134    )?;
135    let json_abi = abi.clone();
136    register_host_fn(
137        linker,
138        MODULE_NAME,
139        method_key("toJson"),
140        FuncType::new(&engine, [abi.value_type()], [abi.value_type()]),
141        true,
142        move |caller, params, results| {
143            let recv = json_abi.read(caller, abi_arg(params, 0)?, "String#toJson")?;
144            *abi_result(results, 0)? =
145                crate::runtime::prelude::vtable::quote_string(caller, recv.value.units())?;
146            Ok(())
147        },
148    )?;
149    let equals_abi = abi.clone();
150    register_host_fn(
151        linker,
152        MODULE_NAME,
153        method_key("equals"),
154        FuncType::new(
155            &engine,
156            [abi.value_type(), abi.value_type()],
157            [ValType::I32],
158        ),
159        true,
160        move |caller, params, results| {
161            *abi_result(results, 0)? = Val::I32(super::super::vtable::string_equals(
162                caller,
163                abi_arg(params, 0)?,
164                abi_arg(params, 1)?,
165                &equals_abi.string_ty,
166            )? as i32);
167            Ok(())
168        },
169    )?;
170    let lc_abi = abi.clone();
171    register_host_fn(
172        linker,
173        MODULE_NAME,
174        method_key("localeCompare"),
175        FuncType::new(
176            &engine,
177            [abi.value_type(), abi.value_type()],
178            [ValType::F64],
179        ),
180        true,
181        move |caller, params, results| {
182            let a = lc_abi.read(caller, abi_arg(params, 0)?, "String#localeCompare")?;
183            let b = lc_abi.read(caller, abi_arg(params, 1)?, "String#localeCompare")?;
184            *abi_result(results, 0)? = Val::F64(f64::from(cmp(&a.value, &b.value)).to_bits());
185            Ok(())
186        },
187    )?;
188
189    // `String(value)` — the conversion call-signature (`Dispatch::Static`, so
190    // only the value arrives). A bigint renders its decimal text directly (its
191    // vtable slots are host-owned but the Wasm path special-cased it, and the
192    // dispatch is cheaper); everything else re-enters its `toString` slot.
193    let obj_param = ValType::Ref(RefType::new(
194        true,
195        HeapType::ConcreteStruct(build_intrinsic_types(&engine)?.object),
196    ));
197    register_host_fn_async(
198        linker,
199        MODULE_NAME,
200        crate::mangle::extend(&crate::mangle::prelude("StringConstructor"), "@call"),
201        FuncType::new(&engine, [obj_param], [abi.value_type()]),
202        true,
203        |caller, params, results| {
204            Box::pin(async move {
205                *abi_result(results, 0)? = string_ctor_call(caller, abi_arg(params, 0)?).await?;
206                Ok(())
207            })
208        },
209    )?;
210
211    // `String#iterator` — the for-of cursor over code points.
212    let obj_ret = ValType::Ref(RefType::new(
213        true,
214        HeapType::ConcreteStruct(build_intrinsic_types(&engine)?.object),
215    ));
216    register_host_fn(
217        linker,
218        MODULE_NAME,
219        method_key("iterator"),
220        FuncType::new(&engine, [abi.value_type()], [obj_ret]),
221        true,
222        |caller, params, results| {
223            *abi_result(results, 0)? = crate::runtime::prelude::iterator::make_string_iterator(
224                caller,
225                *abi_arg(params, 0)?,
226            )?;
227            Ok(())
228        },
229    )?;
230
231    // Operator primitives — value symbols, not method keys: codegen emits
232    // direct calls to these for string `+`, `===`/`==`, and relational compares.
233    let s = abi.value_type();
234    let concat_abi = abi.clone();
235    register_host_fn(
236        linker,
237        MODULE_NAME,
238        crate::mangle::prelude("string_concat"),
239        FuncType::new(&engine, [s.clone(), s.clone()], [s.clone()]),
240        true,
241        move |caller, params, results| {
242            *abi_result(results, 0)? = concat_abi.concat(
243                caller,
244                abi_arg(params, 0)?,
245                abi_arg(params, 1)?,
246                "string_concat",
247            )?;
248            Ok(())
249        },
250    )?;
251    let eq_abi = abi.clone();
252    register_host_fn(
253        linker,
254        MODULE_NAME,
255        crate::mangle::prelude("string_eq"),
256        FuncType::new(&engine, [s.clone(), s.clone()], [ValType::I32]),
257        true,
258        move |caller, params, results| {
259            let equal = super::super::vtable::string_equals(
260                caller,
261                abi_arg(params, 0)?,
262                abi_arg(params, 1)?,
263                &eq_abi.string_ty,
264            )?;
265            *abi_result(results, 0)? = Val::I32(equal as i32);
266            Ok(())
267        },
268    )?;
269    let cmp_abi = abi.clone();
270    register_host_fn(
271        linker,
272        MODULE_NAME,
273        crate::mangle::prelude("string_cmp"),
274        FuncType::new(&engine, [s.clone(), s], [ValType::I32]),
275        true,
276        move |caller, params, results| {
277            let a = cmp_abi.read(caller, abi_arg(params, 0)?, "string_cmp")?;
278            let b = cmp_abi.read(caller, abi_arg(params, 1)?, "string_cmp")?;
279            *abi_result(results, 0)? = Val::I32(cmp(&a.value, &b.value));
280            Ok(())
281        },
282    )?;
283    Ok(())
284}
285
286pub fn declare(defs: &mut PackageDeclaration) {
287    let m = |defs: &mut PackageDeclaration, name: &str, params: Vec<Param>, ret: Type| {
288        declare_method(defs, name, method_key(name), params, ret);
289    };
290    let s = || Param::new("s", Type::String);
291
292    m(
293        defs,
294        "charAt",
295        vec![s(), Param::new("index", Type::Number)],
296        Type::String,
297    );
298    m(
299        defs,
300        "at",
301        vec![s(), Param::new("index", Type::Number)],
302        Type::Union(vec![Type::String, Type::Undefined]),
303    );
304    m(
305        defs,
306        "charCodeAt",
307        vec![s(), Param::new("index", Type::Number)],
308        Type::Number,
309    );
310    m(
311        defs,
312        "codePointAt",
313        vec![s(), Param::new("index", Type::Number)],
314        Type::Union(vec![Type::Number, Type::Undefined]),
315    );
316    m(
317        defs,
318        "slice",
319        vec![
320            s(),
321            Param::new("start", Type::Number),
322            Param::new("end", Type::Number),
323        ],
324        Type::String,
325    );
326    m(
327        defs,
328        "substring",
329        vec![
330            s(),
331            Param::new("start", Type::Number),
332            Param::new("end", Type::Number),
333        ],
334        Type::String,
335    );
336    m(
337        defs,
338        "indexOf",
339        vec![
340            s(),
341            Param::new("search", Type::String),
342            Param::new("fromIndex", Type::Number),
343        ],
344        Type::Number,
345    );
346    m(
347        defs,
348        "lastIndexOf",
349        vec![
350            s(),
351            Param::new("search", Type::String),
352            Param::new("fromIndex", Type::Number),
353        ],
354        Type::Number,
355    );
356    m(
357        defs,
358        "includes",
359        vec![
360            s(),
361            Param::new("search", Type::Unknown),
362            Param::new("fromIndex", Type::Unknown),
363        ],
364        Type::Boolean,
365    );
366    m(
367        defs,
368        "startsWith",
369        vec![
370            s(),
371            Param::new("search", Type::Unknown),
372            Param::new("position", Type::Unknown),
373        ],
374        Type::Boolean,
375    );
376    m(
377        defs,
378        "endsWith",
379        vec![
380            s(),
381            Param::new("search", Type::Unknown),
382            Param::new("endPosition", Type::Unknown),
383        ],
384        Type::Boolean,
385    );
386    m(
387        defs,
388        "concat",
389        vec![s(), Param::new("other", Type::String)],
390        Type::String,
391    );
392    m(
393        defs,
394        "padStart",
395        vec![
396            s(),
397            Param::new("targetLength", Type::Number),
398            Param::new("padString", Type::String),
399        ],
400        Type::String,
401    );
402    m(
403        defs,
404        "padEnd",
405        vec![
406            s(),
407            Param::new("targetLength", Type::Number),
408            Param::new("padString", Type::String),
409        ],
410        Type::String,
411    );
412    m(defs, "isWellFormed", vec![s()], Type::Boolean);
413    m(defs, "toWellFormed", vec![s()], Type::String);
414    m(defs, "toUpperCase", vec![s()], Type::String);
415    m(defs, "toLowerCase", vec![s()], Type::String);
416    m(defs, "trim", vec![s()], Type::String);
417    m(defs, "trimStart", vec![s()], Type::String);
418    m(defs, "trimEnd", vec![s()], Type::String);
419    m(
420        defs,
421        "normalize",
422        vec![s(), Param::new("form", Type::String)],
423        Type::String,
424    );
425    m(
426        defs,
427        "repeat",
428        vec![s(), Param::new("count", Type::Number)],
429        Type::String,
430    );
431
432    declare_method(
433        defs,
434        "fromCharCode",
435        ctor_key("fromCharCode"),
436        vec![Param::rest("codes", Type::Array(Box::new(Type::Number)))],
437        Type::String,
438    );
439    declare_method(
440        defs,
441        "fromCodePoint",
442        ctor_key("fromCodePoint"),
443        vec![Param::rest(
444            "codePoints",
445            Type::Array(Box::new(Type::Number)),
446        )],
447        Type::String,
448    );
449
450    m(
451        defs,
452        "iterator",
453        vec![s()],
454        Type::prelude_interface("Iterator".to_string(), vec![Type::String]),
455    );
456    m(defs, "toString", vec![s()], Type::String);
457    m(defs, "toJson", vec![s()], Type::String);
458    m(
459        defs,
460        "equals",
461        vec![s(), Param::new("other", Type::String)],
462        Type::Boolean,
463    );
464    m(
465        defs,
466        "localeCompare",
467        vec![s(), Param::new("other", Type::String)],
468        Type::Number,
469    );
470    declare_method(
471        defs,
472        "@call",
473        crate::mangle::extend(&crate::mangle::prelude("StringConstructor"), "@call"),
474        vec![Param::new("value", Type::Unknown)],
475        Type::String,
476    );
477
478    // Operator primitives (value symbols; `string_cmp`'s boolean return is an
479    // i32 sign, matching the prelude declaration it overrides).
480    let two_strings = || vec![Param::anon(Type::String), Param::anon(Type::String)];
481    declare_method(
482        defs,
483        "string_concat",
484        crate::mangle::prelude("string_concat"),
485        two_strings(),
486        Type::String,
487    );
488    declare_method(
489        defs,
490        "string_eq",
491        crate::mangle::prelude("string_eq"),
492        two_strings(),
493        Type::Boolean,
494    );
495    declare_method(
496        defs,
497        "string_cmp",
498        crate::mangle::prelude("string_cmp"),
499        two_strings(),
500        Type::Boolean,
501    );
502}
503
504/// `(string, f64) -> string`: `charAt`. `pick` names the unit for the index,
505/// if any; the result is that unit or `""`.
506fn reg_unit_to_str(
507    linker: &mut Linker<StoreData>,
508    engine: &wasmtime::Engine,
509    abi: &StringAbi,
510    name: &'static str,
511    pick: fn(usize, f64) -> Option<usize>,
512) -> wasmtime::Result<()> {
513    let s = abi.value_type();
514    let abi = abi.clone();
515    register_host_fn(
516        linker,
517        MODULE_NAME,
518        method_key(name),
519        FuncType::new(engine, [s.clone(), ValType::F64], [s]),
520        true,
521        move |caller, params, results| {
522            let recv = abi.payload(caller, abi_arg(params, 0)?, name)?;
523            let arg = number(abi_arg(params, 1)?, name)?;
524            let unit = pick(recv.len, arg)
525                .map(|i| read_code_unit(&mut *caller, recv.array, i))
526                .transpose()?;
527            let out = Str::from_units(unit.map_or_else(Vec::new, |unit| vec![unit]));
528            *abi_result(results, 0)? = abi.write(caller, recv.vtable, &out)?;
529            Ok(())
530        },
531    )
532}
533
534/// `(string, f64) -> string | undefined`: `at`, whose out-of-range answer is `undefined`.
535/// The result slot is the union lowering `(ref null $Object)`; a `$string` is an
536/// `$Object` subtype, so the hit case needs no extra boxing.
537fn reg_unit_to_str_or_undefined(
538    linker: &mut Linker<StoreData>,
539    engine: &wasmtime::Engine,
540    abi: &StringAbi,
541    name: &'static str,
542    pick: fn(usize, f64) -> Option<usize>,
543) -> wasmtime::Result<()> {
544    let s = abi.value_type();
545    let object = ValType::Ref(RefType::new(
546        true,
547        HeapType::ConcreteStruct(build_intrinsic_types(engine)?.object),
548    ));
549    let abi = abi.clone();
550    register_host_fn(
551        linker,
552        MODULE_NAME,
553        method_key(name),
554        FuncType::new(engine, [s, ValType::F64], [object]),
555        true,
556        move |caller, params, results| {
557            let recv = abi.payload(caller, abi_arg(params, 0)?, name)?;
558            let arg = number(abi_arg(params, 1)?, name)?;
559            *abi_result(results, 0)? = match pick(recv.len, arg) {
560                Some(i) => {
561                    let unit = read_code_unit(&mut *caller, recv.array, i)?;
562                    abi.write(caller, recv.vtable, &Str::from_units(vec![unit]))?
563                }
564                None => crate::runtime::prelude::undefined::value(caller)?,
565            };
566            Ok(())
567        },
568    )
569}
570
571/// `charCodeAt`: the code unit at the index, or `NaN` out of range.
572fn reg_char_code_at(
573    linker: &mut Linker<StoreData>,
574    engine: &wasmtime::Engine,
575    abi: &StringAbi,
576) -> wasmtime::Result<()> {
577    const NAME: &str = "charCodeAt";
578    let s = abi.value_type();
579    let abi = abi.clone();
580    register_host_fn(
581        linker,
582        MODULE_NAME,
583        method_key(NAME),
584        FuncType::new(engine, [s, ValType::F64], [ValType::F64]),
585        true,
586        move |caller, params, results| {
587            let recv = abi.payload(caller, abi_arg(params, 0)?, NAME)?;
588            let arg = number(abi_arg(params, 1)?, NAME)?;
589            let value = match unit_index(recv.len, arg) {
590                Some(i) => f64::from(read_code_unit(&mut *caller, recv.array, i)?),
591                None => f64::NAN,
592            };
593            *abi_result(results, 0)? = Val::F64(value.to_bits());
594            Ok(())
595        },
596    )
597}
598
599/// `codePointAt`: the code point starting at the index, joining a surrogate
600/// pair, or `undefined` out of range.
601fn reg_code_point_at(
602    linker: &mut Linker<StoreData>,
603    engine: &wasmtime::Engine,
604    abi: &StringAbi,
605) -> wasmtime::Result<()> {
606    const NAME: &str = "codePointAt";
607    let s = abi.value_type();
608    let object = ValType::Ref(RefType::new(
609        true,
610        HeapType::ConcreteStruct(build_intrinsic_types(engine)?.object),
611    ));
612    let abi = abi.clone();
613    register_host_fn(
614        linker,
615        MODULE_NAME,
616        method_key(NAME),
617        FuncType::new(engine, [s, ValType::F64], [object]),
618        true,
619        move |caller, params, results| {
620            let recv = abi.payload(caller, abi_arg(params, 0)?, NAME)?;
621            let arg = number(abi_arg(params, 1)?, NAME)?;
622            let Some(i) = unit_index(recv.len, arg) else {
623                *abi_result(results, 0)? = crate::runtime::prelude::undefined::value(caller)?;
624                return Ok(());
625            };
626            let unit = read_code_unit(&mut *caller, recv.array, i)?;
627            let next = if i + 1 < recv.len {
628                Some(read_code_unit(&mut *caller, recv.array, i + 1)?)
629            } else {
630                None
631            };
632            let boxed = write_boxed_number_struct(caller, code_point(unit, next))?;
633            *abi_result(results, 0)? = Val::AnyRef(Some(boxed.to_anyref()));
634            Ok(())
635        },
636    )
637}
638
639/// `repeat`: the receiver `count` times, built in place (see [`StringAbi::build`]).
640fn reg_repeat(
641    linker: &mut Linker<StoreData>,
642    engine: &wasmtime::Engine,
643    abi: &StringAbi,
644) -> wasmtime::Result<()> {
645    let s = abi.value_type();
646    let abi = abi.clone();
647    register_host_fn(
648        linker,
649        MODULE_NAME,
650        method_key("repeat"),
651        FuncType::new(engine, [s.clone(), ValType::F64], [s]),
652        true,
653        move |caller, params, results| {
654            let recv = abi.payload(caller, abi_arg(params, 0)?, "repeat")?;
655            let count = number(abi_arg(params, 1)?, "repeat")?;
656            fuel::charge(&mut *caller, fuel::COPY, recv.len as u64)?;
657            let total = repeat_length(recv.len, count).map_err(throw)?;
658            fuel::charge(&mut *caller, fuel::COPY, total as u64)?;
659            let runs = [recv.cycle(total)];
660            *abi_result(results, 0)? = abi.build(caller, recv.vtable, &runs)?;
661            Ok(())
662        },
663    )
664}
665
666/// `(string, f64, f64) -> string`: `slice`, `substring`. `range` names the
667/// units kept; only those are copied.
668fn reg_range_to_str(
669    linker: &mut Linker<StoreData>,
670    engine: &wasmtime::Engine,
671    abi: &StringAbi,
672    name: &'static str,
673    range: fn(usize, f64, f64) -> std::ops::Range<usize>,
674) -> wasmtime::Result<()> {
675    let s = abi.value_type();
676    let abi = abi.clone();
677    register_host_fn(
678        linker,
679        MODULE_NAME,
680        method_key(name),
681        FuncType::new(engine, [s.clone(), ValType::F64, ValType::F64], [s]),
682        true,
683        move |caller, params, results| {
684            let recv = abi.payload(caller, abi_arg(params, 0)?, name)?;
685            let a = number(abi_arg(params, 1)?, name)?;
686            let b = number(abi_arg(params, 2)?, name)?;
687            let kept = range(recv.len, a, b);
688            let units = read_code_units_range(&mut *caller, recv.array, kept.start, kept.len())?;
689            *abi_result(results, 0)? = abi.write(caller, recv.vtable, &Str::from_units(units))?;
690            Ok(())
691        },
692    )
693}
694
695/// `(string, string, f64) -> f64`: `indexOf`, `lastIndexOf`.
696fn reg_str_str_num_to_num(
697    linker: &mut Linker<StoreData>,
698    engine: &wasmtime::Engine,
699    abi: &StringAbi,
700    name: &'static str,
701    op: fn(&Str, &Str, f64) -> f64,
702) -> wasmtime::Result<()> {
703    let s = abi.value_type();
704    let abi = abi.clone();
705    register_host_fn(
706        linker,
707        MODULE_NAME,
708        method_key(name),
709        FuncType::new(engine, [s.clone(), s, ValType::F64], [ValType::F64]),
710        true,
711        move |caller, params, results| {
712            let recv = abi.read(caller, abi_arg(params, 0)?, name)?;
713            let search = abi.read(caller, abi_arg(params, 1)?, name)?;
714            let from = number(abi_arg(params, 2)?, name)?;
715            fuel::charge(
716                &mut *caller,
717                fuel::SCAN,
718                (recv.value.len() + search.value.len()) as u64,
719            )?;
720            *abi_result(results, 0)? = Val::F64(op(&recv.value, &search.value, from).to_bits());
721            Ok(())
722        },
723    )
724}
725
726/// Search and position stay boxed so RegExp rejection and coercions run in order.
727fn register_string_search_predicate(
728    linker: &mut Linker<StoreData>,
729    engine: &wasmtime::Engine,
730    abi: &StringAbi,
731    name: &'static str,
732    op: fn(&Str, &Str, f64) -> bool,
733) -> wasmtime::Result<()> {
734    let intr = build_intrinsic_types(engine)?;
735    let value = ValType::Ref(RefType::new(true, HeapType::ConcreteStruct(intr.object)));
736    let abi = abi.clone();
737    register_host_fn_async(
738        linker,
739        MODULE_NAME,
740        method_key(name),
741        FuncType::new(
742            engine,
743            [abi.value_type(), value.clone(), value],
744            [ValType::I32],
745        ),
746        true,
747        move |caller, params, results| {
748            let abi = abi.clone();
749            Box::pin(async move {
750                let recv = abi.read(caller, abi_arg(params, 0)?, name)?;
751                let search =
752                    super::super::value::search_string(caller, abi_arg(params, 1)?).await?;
753                let from = super::super::value::to_number(caller, abi_arg(params, 2)?).await?;
754                fuel::charge(
755                    &mut *caller,
756                    fuel::SCAN,
757                    (recv.value.len() + search.len()) as u64,
758                )?;
759                *abi_result(results, 0)? =
760                    Val::I32(op(&recv.value, &Str::from_units(search), from) as i32);
761                Ok(())
762            })
763        },
764    )
765}
766
767/// `startsWith`, `endsWith`: compare the needle with the one window of the
768/// receiver it could occupy, copying nothing else. Search and position stay
769/// boxed so RegExp rejection and coercions run in order.
770fn reg_window_predicate(
771    linker: &mut Linker<StoreData>,
772    engine: &wasmtime::Engine,
773    abi: &StringAbi,
774    name: &'static str,
775    window: fn(usize, usize, f64) -> Option<std::ops::Range<usize>>,
776) -> wasmtime::Result<()> {
777    let intr = build_intrinsic_types(engine)?;
778    let value = ValType::Ref(RefType::new(true, HeapType::ConcreteStruct(intr.object)));
779    let abi = abi.clone();
780    register_host_fn_async(
781        linker,
782        MODULE_NAME,
783        method_key(name),
784        FuncType::new(
785            engine,
786            [abi.value_type(), value.clone(), value],
787            [ValType::I32],
788        ),
789        true,
790        move |caller, params, results| {
791            let abi = abi.clone();
792            Box::pin(async move {
793                let recv = abi.payload(caller, abi_arg(params, 0)?, name)?;
794                let needle =
795                    super::super::value::search_string(caller, abi_arg(params, 1)?).await?;
796                let position = super::super::value::to_number(caller, abi_arg(params, 2)?).await?;
797                let found = match window(recv.len, needle.len(), position) {
798                    Some(range) => {
799                        read_code_units_range(caller, recv.array, range.start, range.len())?
800                            == needle
801                    }
802                    None => false,
803                };
804                *abi_result(results, 0)? = Val::I32(found as i32);
805                Ok(())
806            })
807        },
808    )
809}
810
811/// `String#concat`; the `+` operator registers the same operation as
812/// `string_concat`.
813fn reg_concat(
814    linker: &mut Linker<StoreData>,
815    engine: &wasmtime::Engine,
816    abi: &StringAbi,
817) -> wasmtime::Result<()> {
818    let s = abi.value_type();
819    let abi = abi.clone();
820    register_host_fn(
821        linker,
822        MODULE_NAME,
823        method_key("concat"),
824        FuncType::new(engine, [s.clone(), s.clone()], [s]),
825        true,
826        move |caller, params, results| {
827            *abi_result(results, 0)? =
828                abi.concat(caller, abi_arg(params, 0)?, abi_arg(params, 1)?, "concat")?;
829            Ok(())
830        },
831    )
832}
833
834/// Which side of the receiver a pad fills.
835#[derive(Clone, Copy)]
836enum PadSide {
837    Start,
838    End,
839}
840
841/// `padStart`, `padEnd`: repetitions of the pad, cut to fit, beside the receiver.
842fn reg_pad(
843    linker: &mut Linker<StoreData>,
844    engine: &wasmtime::Engine,
845    abi: &StringAbi,
846    name: &'static str,
847    side: PadSide,
848) -> wasmtime::Result<()> {
849    let s = abi.value_type();
850    let abi = abi.clone();
851    register_host_fn(
852        linker,
853        MODULE_NAME,
854        method_key(name),
855        FuncType::new(engine, [s.clone(), ValType::F64, s.clone()], [s]),
856        true,
857        move |caller, params, results| {
858            let recv = abi.payload(caller, abi_arg(params, 0)?, name)?;
859            let target = number(abi_arg(params, 1)?, name)?;
860            let pad = abi.payload(caller, abi_arg(params, 2)?, name)?;
861            fuel::charge(&mut *caller, fuel::COPY, recv.len as u64 + pad.len as u64)?;
862            let total = pad_length(recv.len, target, pad.len).map_err(throw)?;
863            fuel::charge(&mut *caller, fuel::COPY, total as u64)?;
864            // pad_length never shortens the receiver.
865            let fill_len = total
866                .checked_sub(recv.len)
867                .ok_or_else(|| fatal_host_error("padded string shorter than its receiver"))?;
868            let fill = pad.cycle(fill_len);
869            let runs = match side {
870                PadSide::Start => [fill, recv.whole()],
871                PadSide::End => [recv.whole(), fill],
872            };
873            *abi_result(results, 0)? = abi.build(caller, recv.vtable, &runs)?;
874            Ok(())
875        },
876    )
877}
878
879/// `(string, string) -> string` for a builder that can reject its input:
880/// `normalize` (invalid form throws).
881fn reg_str_str_to_str_fallible(
882    linker: &mut Linker<StoreData>,
883    engine: &wasmtime::Engine,
884    abi: &StringAbi,
885    name: &'static str,
886    op: fn(&Str, &Str) -> super::Result<Str>,
887) -> wasmtime::Result<()> {
888    let s = abi.value_type();
889    let abi = abi.clone();
890    register_host_fn(
891        linker,
892        MODULE_NAME,
893        method_key(name),
894        FuncType::new(engine, [s.clone(), s.clone()], [s]),
895        true,
896        move |caller, params, results| {
897            let recv = abi.read(caller, abi_arg(params, 0)?, name)?;
898            let arg = abi.read(caller, abi_arg(params, 1)?, name)?;
899            let out = op(&recv.value, &arg.value).map_err(throw)?;
900            *abi_result(results, 0)? = abi.write(caller, recv.vtable, &out)?;
901            Ok(())
902        },
903    )
904}
905
906/// `(string) -> boolean`: `isWellFormed`.
907fn reg_str_to_bool(
908    linker: &mut Linker<StoreData>,
909    engine: &wasmtime::Engine,
910    abi: &StringAbi,
911    name: &'static str,
912    op: fn(&Str) -> bool,
913) -> wasmtime::Result<()> {
914    let s = abi.value_type();
915    let abi = abi.clone();
916    register_host_fn(
917        linker,
918        MODULE_NAME,
919        method_key(name),
920        FuncType::new(engine, [s], [ValType::I32]),
921        true,
922        move |caller, params, results| {
923            let recv = abi.read(caller, abi_arg(params, 0)?, name)?;
924            *abi_result(results, 0)? = Val::I32(op(&recv.value) as i32);
925            Ok(())
926        },
927    )
928}
929
930/// `(string) -> string`: `toWellFormed`.
931fn reg_str_to_str(
932    linker: &mut Linker<StoreData>,
933    engine: &wasmtime::Engine,
934    abi: &StringAbi,
935    name: &'static str,
936    op: fn(&Str) -> Str,
937) -> wasmtime::Result<()> {
938    let s = abi.value_type();
939    let abi = abi.clone();
940    register_host_fn(
941        linker,
942        MODULE_NAME,
943        method_key(name),
944        FuncType::new(engine, [s.clone()], [s]),
945        true,
946        move |caller, params, results| {
947            let recv = abi.read(caller, abi_arg(params, 0)?, name)?;
948            // toWellFormed scans the input and its mutable copy.
949            fuel::charge(
950                &mut *caller,
951                fuel::SCAN,
952                2 * recv.value.units().len() as u64,
953            )?;
954            *abi_result(results, 0)? = abi.write(caller, recv.vtable, &op(&recv.value))?;
955            Ok(())
956        },
957    )
958}
959
960/// `toUpperCase`, `toLowerCase`: read the receiver and write the result in
961/// place (see [`GcText`]).
962fn reg_case_map(
963    linker: &mut Linker<StoreData>,
964    engine: &wasmtime::Engine,
965    abi: &StringAbi,
966    name: &'static str,
967    map: fn(&mut GcText<'_, '_>) -> wasmtime::Result<()>,
968) -> wasmtime::Result<()> {
969    let s = abi.value_type();
970    let abi = abi.clone();
971    register_host_fn(
972        linker,
973        MODULE_NAME,
974        method_key(name),
975        FuncType::new(engine, [s.clone()], [s]),
976        true,
977        move |caller, params, results| {
978            let recv = abi.payload(caller, abi_arg(params, 0)?, name)?;
979            // Read, map, and write a result of the receiver's length; a result
980            // of another length pays the difference in `GcText::finish`.
981            let len = recv.len as u64;
982            fuel::charge(&mut *caller, fuel::COPY, len)?;
983            fuel::charge(&mut *caller, fuel::SCAN, len)?;
984            fuel::charge(&mut *caller, fuel::COPY, len)?;
985            let guess = allocate(caller, &abi.payload_ty, recv.len)?;
986            let input = GcUnits::new(caller, recv.array, recv.len);
987            let mut text = GcText::new(input, abi.payload_ty.clone(), guess, recv.len);
988            map(&mut text)?;
989            let payload = text.finish()?;
990            *abi_result(results, 0)? = abi.wrap(caller, recv.vtable, payload)?;
991            Ok(())
992        },
993    )
994}
995
996/// `trim`, `trimStart`, `trimEnd`: scan the receiver's ends in place and copy
997/// the kept range.
998fn reg_trim(
999    linker: &mut Linker<StoreData>,
1000    engine: &wasmtime::Engine,
1001    abi: &StringAbi,
1002    name: &'static str,
1003    ends: TrimEnds,
1004) -> wasmtime::Result<()> {
1005    let s = abi.value_type();
1006    let abi = abi.clone();
1007    register_host_fn(
1008        linker,
1009        MODULE_NAME,
1010        method_key(name),
1011        FuncType::new(engine, [s.clone()], [s]),
1012        true,
1013        move |caller, params, results| {
1014            let recv = abi.payload(caller, abi_arg(params, 0)?, name)?;
1015            // The SUB-1292 price; the scan reads only the trimmed ends.
1016            fuel::charge(&mut *caller, fuel::COPY, recv.len as u64)?;
1017            fuel::charge(&mut *caller, fuel::SCAN, recv.len as u64)?;
1018            let kept = trim_range(&mut GcUnits::new(caller, recv.array, recv.len), ends)?;
1019            fuel::charge(&mut *caller, fuel::COPY, kept.len() as u64)?;
1020            *abi_result(results, 0)? = abi.build(caller, recv.vtable, &[recv.range_run(kept)])?;
1021            Ok(())
1022        },
1023    )
1024}
1025
1026/// The canonical `$string` ABI types, recovered once at install and reused to
1027/// build every result. `$string` is `(struct (field vtable) (field $rawString))`.
1028#[derive(Clone)]
1029struct StringAbi {
1030    string_ty: StructType,
1031    payload_ty: ArrayType,
1032}
1033
1034/// A `$string` receiver split into its vtable and code units. The result is
1035/// built with the *same* vtable: it's the one shared `$string` vtable singleton,
1036/// so reusing the receiver's avoids a prelude-instance lookup.
1037struct Receiver {
1038    vtable: Val,
1039    value: Str,
1040}
1041
1042/// A `$string` kept where it is: its payload array and length. Accessors read
1043/// a few units or a range from it; builders copy from it as the source of a run.
1044struct Payload {
1045    vtable: Val,
1046    array: Rooted<ArrayRef>,
1047    len: usize,
1048}
1049
1050impl Payload {
1051    /// The units in `range`, copied once.
1052    fn range_run(&self, range: std::ops::Range<usize>) -> Run {
1053        Run::copy(self.array, range)
1054    }
1055
1056    /// The whole string, copied once.
1057    fn whole(&self) -> Run {
1058        Run::copy(self.array, 0..self.len)
1059    }
1060
1061    /// `len` units cycling through this string from its start.
1062    fn cycle(&self, len: usize) -> Run {
1063        Run {
1064            source: self.array,
1065            start: 0,
1066            period: self.len,
1067            len,
1068        }
1069    }
1070
1071    fn read_all(&self, caller: &mut Caller<'_, StoreData>, name: &str) -> wasmtime::Result<Str> {
1072        Ok(Str::from_units(read_code_units(
1073            &mut *caller,
1074            self.array,
1075            name,
1076        )?))
1077    }
1078}
1079
1080impl StringAbi {
1081    fn recover(engine: &wasmtime::Engine) -> wasmtime::Result<Self> {
1082        Ok(Self {
1083            string_ty: intrinsic_string_type(engine)?,
1084            payload_ty: string_array_type(engine),
1085        })
1086    }
1087
1088    fn value_type(&self) -> ValType {
1089        ValType::Ref(RefType::new(
1090            false,
1091            HeapType::ConcreteStruct(self.string_ty.clone()),
1092        ))
1093    }
1094
1095    fn read(
1096        &self,
1097        caller: &mut Caller<'_, StoreData>,
1098        val: &Val,
1099        name: &str,
1100    ) -> wasmtime::Result<Receiver> {
1101        let payload = self.payload(caller, val, name)?;
1102        Ok(Receiver {
1103            vtable: payload.vtable,
1104            value: payload.read_all(caller, name)?,
1105        })
1106    }
1107
1108    fn payload(
1109        &self,
1110        caller: &mut Caller<'_, StoreData>,
1111        val: &Val,
1112        name: &str,
1113    ) -> wasmtime::Result<Payload> {
1114        let Val::AnyRef(Some(any)) = val else {
1115            return Err(wasmtime::Error::msg(format!(
1116                "{name} expects a string, got {val:?}"
1117            )));
1118        };
1119        let st = any
1120            .as_struct(&mut *caller)?
1121            .ok_or_else(|| wasmtime::Error::msg(format!("{name}: expected a $string struct")))?;
1122        let vtable = st.field(&mut *caller, 0)?;
1123        let array = match st.field(&mut *caller, 1)? {
1124            Val::AnyRef(Some(arr)) => arr.unwrap_array(&mut *caller)?,
1125            other => {
1126                return Err(wasmtime::Error::msg(format!(
1127                    "{name}: malformed $string payload {other:?}"
1128                )));
1129            }
1130        };
1131        let len = usize::try_from(array.len(&mut *caller)?)
1132            .map_err(crate::runtime::host::fatal_host_error)?;
1133        Ok(Payload { vtable, array, len })
1134    }
1135
1136    fn write(
1137        &self,
1138        caller: &mut Caller<'_, StoreData>,
1139        vtable: Val,
1140        s: &Str,
1141    ) -> wasmtime::Result<Val> {
1142        fuel::charge(&mut *caller, fuel::COPY, s.units().len() as u64)?;
1143        self.write_units(caller, vtable, s)
1144    }
1145
1146    fn write_units(
1147        &self,
1148        caller: &mut Caller<'_, StoreData>,
1149        vtable: Val,
1150        s: &Str,
1151    ) -> wasmtime::Result<Val> {
1152        let pre = ArrayRefPre::new(&mut *caller, self.payload_ty.clone());
1153        let payload = ArrayRef::new_from_i16_slice(&mut *caller, &pre, s.units())?;
1154        self.wrap(caller, vtable, payload)
1155    }
1156
1157    /// `a` followed by `b`, built in place.
1158    fn concat(
1159        &self,
1160        caller: &mut Caller<'_, StoreData>,
1161        a: &Val,
1162        b: &Val,
1163        name: &str,
1164    ) -> wasmtime::Result<Val> {
1165        let a = self.payload(caller, a, name)?;
1166        let b = self.payload(caller, b, name)?;
1167        fuel::charge(&mut *caller, fuel::COPY, a.len as u64 + b.len as u64)?;
1168        let total = concat_length(a.len, b.len)?;
1169        fuel::charge(&mut *caller, fuel::COPY, total as u64)?;
1170        self.build(caller, a.vtable, &[a.whole(), b.whole()])
1171    }
1172
1173    /// Builds a string from `runs` directly in a new GC array (see [`gc_text`]).
1174    fn build(
1175        &self,
1176        caller: &mut Caller<'_, StoreData>,
1177        vtable: Val,
1178        runs: &[Run],
1179    ) -> wasmtime::Result<Val> {
1180        let total = runs
1181            .iter()
1182            .try_fold(0usize, |total, run| total.checked_add(run.len))
1183            .ok_or_else(|| fatal_host_error("string length overflow"))?;
1184        let payload = allocate(caller, &self.payload_ty, total)?;
1185        let mut at = 0;
1186        for run in runs {
1187            fill_run(&mut *caller, payload, at, run)?;
1188            at += run.len;
1189        }
1190        self.wrap(caller, vtable, payload)
1191    }
1192
1193    fn wrap(
1194        &self,
1195        caller: &mut Caller<'_, StoreData>,
1196        vtable: Val,
1197        payload: Rooted<ArrayRef>,
1198    ) -> wasmtime::Result<Val> {
1199        let pre = StructRefPre::new(&mut *caller, self.string_ty.clone());
1200        let st = StructRef::new(
1201            &mut *caller,
1202            &pre,
1203            &[vtable, Val::AnyRef(Some(payload.to_anyref())), Val::I64(0)],
1204        )?;
1205        Ok(Val::AnyRef(Some(st.to_anyref())))
1206    }
1207}
1208
1209/// Read a `$Array` of boxed numbers (the packed rest args of a `fromCharCode` /
1210/// `fromCodePoint` call) into their `f64` payloads.
1211/// `String(value)`: null renders `"null"`, as ECMAScript's ToString does; a
1212/// bigint renders its decimal text; anything else re-enters its vtable
1213/// `toString` slot.
1214async fn string_ctor_call(
1215    caller: &mut Caller<'_, StoreData>,
1216    value: &Val,
1217) -> wasmtime::Result<Val> {
1218    let Val::AnyRef(Some(any)) = value else {
1219        let st = write_submilli_string_struct(caller, "null")?;
1220        return Ok(Val::AnyRef(Some(st.to_anyref())));
1221    };
1222    let intr = intrinsic_types(&mut *caller)?;
1223    if let Some(st) = any.as_struct(&mut *caller)?
1224        && StructType::eq(&st.ty(&*caller)?, &intr.bigint)
1225    {
1226        let (sign, limbs) = crate::runtime::prelude::bigint::ops::read_bigint_struct(
1227            caller,
1228            value,
1229            "String(bigint)",
1230        )?;
1231        let value = crate::runtime::prelude::bigint::ops::limbs_to_bigint(sign, &limbs)?;
1232        let text = crate::runtime::prelude::bigint::ops::format_bigint(caller, &value, 10)?;
1233        let st = write_submilli_string_struct(caller, &text)?;
1234        return Ok(Val::AnyRef(Some(st.to_anyref())));
1235    }
1236    crate::runtime::prelude::vtable::dispatch_vtable_slot(caller, value, 0, &[]).await
1237}
1238
1239fn read_number_array(
1240    caller: &mut Caller<'_, StoreData>,
1241    val: &Val,
1242    name: &str,
1243) -> wasmtime::Result<Vec<f64>> {
1244    let storage = crate::runtime::array_storage::ArrayStorage::read(caller, val)?;
1245    let backing = storage.backing;
1246    let len = storage.len;
1247    let mut nums = Vec::new();
1248    nums.try_reserve_exact(len as usize)
1249        .map_err(crate::runtime::host::fatal_host_error)?;
1250    for i in 0..len {
1251        let boxed = match backing.get(&mut *caller, i)? {
1252            Val::AnyRef(Some(b)) => b
1253                .as_struct(&mut *caller)?
1254                .ok_or_else(|| wasmtime::Error::msg(format!("{name}: arg {i} not a struct")))?,
1255            other => {
1256                return Err(wasmtime::Error::msg(format!(
1257                    "{name}: arg {i} is {other:?}, not a boxed number"
1258                )));
1259            }
1260        };
1261        match boxed.field(&mut *caller, 1)? {
1262            Val::F64(bits) => nums.push(f64::from_bits(bits)),
1263            other => {
1264                return Err(wasmtime::Error::msg(format!(
1265                    "{name}: arg {i} payload {other:?} is not f64"
1266                )));
1267            }
1268        }
1269    }
1270    Ok(nums)
1271}
1272
1273fn number(val: &Val, name: &str) -> wasmtime::Result<f64> {
1274    match val {
1275        Val::F64(bits) => Ok(f64::from_bits(*bits)),
1276        other => Err(wasmtime::Error::msg(format!(
1277            "{name} expects f64, got {other:?}"
1278        ))),
1279    }
1280}
1281
1282fn throw(e: RangeError) -> wasmtime::Error {
1283    range_error(e.message())
1284}
1285
1286/// The type/interface surface this module implements — its slice of the
1287/// prelude declaration (see `declaration::prelude_package_declaration`).
1288#[allow(clippy::too_many_lines)]
1289pub(crate) fn declare_types(defs: &mut crate::PackageDeclaration) {
1290    use crate::runtime::prelude::declaration::doc;
1291    use crate::{
1292        Dispatch, MethodSig, Param, PropertySig, Span, Type, TypeKind, TypeSymbol, ValueKind,
1293        ValueSymbol,
1294    };
1295    use std::collections::BTreeMap;
1296    defs.values.insert(
1297        "string_concat".to_string(),
1298        ValueSymbol {
1299            name: "string_concat".to_string(),
1300            mangled_name: crate::mangle::prelude("string_concat"),
1301            declaration_span: Span::at(crate::FileId::PRELUDE),
1302            kind: ValueKind::Function {
1303                generics: Vec::new(),
1304                params: vec![Param::anon(Type::String), Param::anon(Type::String)],
1305                ret: Type::String,
1306                type_predicate: None,
1307                doc: doc("/** Internal: concatenate two strings. */"),
1308            },
1309        },
1310    );
1311    defs.values.insert(
1312        "string_eq".to_string(),
1313        ValueSymbol {
1314            name: "string_eq".to_string(),
1315            mangled_name: crate::mangle::prelude("string_eq"),
1316            declaration_span: Span::at(crate::FileId::PRELUDE),
1317            kind: ValueKind::Function {
1318                generics: Vec::new(),
1319                params: vec![Param::anon(Type::String), Param::anon(Type::String)],
1320                ret: Type::Boolean,
1321                type_predicate: None,
1322                doc: doc("/** Internal: structural string equality. */"),
1323            },
1324        },
1325    );
1326    defs.values.insert(
1327        "string_length".to_string(),
1328        ValueSymbol {
1329            name: "string_length".to_string(),
1330            mangled_name: crate::mangle::prelude("string_length"),
1331            declaration_span: Span::at(crate::FileId::PRELUDE),
1332            kind: ValueKind::Function {
1333                generics: Vec::new(),
1334                params: vec![Param::anon(Type::String)],
1335                ret: Type::Number,
1336                type_predicate: None,
1337                doc: doc("/** Internal: UTF-16 code-unit length. */"),
1338            },
1339        },
1340    );
1341    // `ret: Boolean` only to get an i32 result; the value is a comparison sign,
1342    // never read as a boolean and never user-callable.
1343    defs.values.insert(
1344        "string_cmp".to_string(),
1345        ValueSymbol {
1346            name: "string_cmp".to_string(),
1347            mangled_name: crate::mangle::prelude("string_cmp"),
1348            declaration_span: Span::at(crate::FileId::PRELUDE),
1349            kind: ValueKind::Function {
1350                generics: Vec::new(),
1351                params: vec![Param::anon(Type::String), Param::anon(Type::String)],
1352                ret: Type::Boolean,
1353                type_predicate: None,
1354                doc: doc("/** Internal: lexicographic compare; i32 sign result. */"),
1355            },
1356        },
1357    );
1358
1359    for name in [
1360        "string_true",
1361        "string_false",
1362        "string_comma",
1363        "string_object_object",
1364        "string_object_function",
1365        "string_null",
1366    ] {
1367        defs.values.insert(
1368            name.to_string(),
1369            ValueSymbol {
1370                name: name.to_string(),
1371                mangled_name: crate::mangle::prelude(name),
1372                declaration_span: Span::at(crate::FileId::PRELUDE),
1373                kind: ValueKind::Const {
1374                    ty: Type::String,
1375                    doc: None,
1376                },
1377            },
1378        );
1379    }
1380    defs.types.insert(
1381        "String".to_string(),
1382        TypeSymbol {
1383            name: "String".to_string(),
1384            mangled_name: crate::mangle::prelude("String"),
1385            declaration_span: Span::at(crate::FileId::PRELUDE),
1386            kind: TypeKind::Interface { index: None,
1387                generics: Vec::new(),
1388                methods: BTreeMap::from([
1389                    (
1390                        "toString".to_string(),
1391                        MethodSig {
1392                            optional: false,
1393                            generics: Vec::new(),
1394                            params: Vec::new(),
1395                            ret: Type::String,
1396                            predicate: None,
1397                            doc: doc("/** Returns this string unchanged (identity). */"),
1398                        },
1399                    ),
1400                    (
1401                        "toJson".to_string(),
1402                        MethodSig {
1403                            optional: false,
1404                            generics: Vec::new(),
1405                            params: Vec::new(),
1406                            ret: Type::String,
1407                            predicate: None,
1408                            doc: doc(
1409                                "/** Returns this string wrapped in `\"…\"` with JSON escapes applied (`\\\"`, `\\\\`, `\\b`, `\\f`, `\\n`, `\\r`, `\\t`, and `\\u00XX` for control codepoints). */",
1410                            ),
1411                        },
1412                    ),
1413                    (
1414                        "concat".to_string(),
1415                        MethodSig {
1416                            optional: false,
1417                            generics: Vec::new(),
1418                            params: vec![Param::new("other", Type::String)],
1419                            ret: Type::String,
1420                            predicate: None,
1421                            doc: doc(
1422                                "/**\n * Returns a new string with `other` appended.\n * The receiver is not modified.\n * @param other String to append.\n */",
1423                            ),
1424                        },
1425                    ),
1426                    (
1427                        "equals".to_string(),
1428                        MethodSig {
1429                            optional: false,
1430                            generics: Vec::new(),
1431                            params: vec![Param::new("other", Type::String)],
1432                            ret: Type::Boolean,
1433                            predicate: None,
1434                            doc: doc(
1435                                "/**\n * Returns `true` when both strings have identical code units.\n * @param other String to compare against.\n */",
1436                            ),
1437                        },
1438                    ),
1439                    (
1440                        "localeCompare".to_string(),
1441                        MethodSig {
1442                            optional: false,
1443                            generics: Vec::new(),
1444                            params: vec![Param::new("other", Type::String)],
1445                            ret: Type::Number,
1446                            predicate: None,
1447                            doc: doc(
1448                                "/**\n * Compares this string with `other` in UTF-16 code-unit order.\n * @param other The string to compare against.\n * @returns A negative number if this sorts before `other`, `0` if equal, a positive number if after. No locale awareness — pure code-unit order.\n */",
1449                            ),
1450                        },
1451                    ),
1452                    (
1453                        "iterator".to_string(),
1454                        MethodSig {
1455                            optional: false,
1456                            generics: Vec::new(),
1457                            params: Vec::new(),
1458                            ret: Type::prelude_interface("Iterator".to_string(), vec![Type::String]),
1459                            predicate: None,
1460                            doc: doc(
1461                                "/** Returns an `Iterator<string>` of CODE POINTS — surrogate pairs arrive as one two-unit string. Drives `for-of` over a string. */",
1462                            ),
1463                        },
1464                    ),
1465                    (
1466                        "isWellFormed".to_string(),
1467                        MethodSig {
1468                            optional: false,
1469                            generics: Vec::new(),
1470                            params: Vec::new(),
1471                            ret: Type::Boolean,
1472                            predicate: None,
1473                            doc: doc(
1474                                "/** Returns `true` when the string contains no lone surrogates (it round-trips losslessly through UTF-8). */",
1475                            ),
1476                        },
1477                    ),
1478                    (
1479                        "toWellFormed".to_string(),
1480                        MethodSig {
1481                            optional: false,
1482                            generics: Vec::new(),
1483                            params: Vec::new(),
1484                            ret: Type::String,
1485                            predicate: None,
1486                            doc: doc(
1487                                "/** Returns a copy with every lone surrogate replaced by U+FFFD (`\u{fffd}`); well-formed strings come back unchanged. */",
1488                            ),
1489                        },
1490                    ),
1491                    (
1492                        "charAt".to_string(),
1493                        MethodSig {
1494                            optional: false,
1495                            generics: Vec::new(),
1496                            params: vec![Param::new("index", Type::Number)],
1497                            ret: Type::String,
1498                            predicate: None,
1499                            doc: doc(
1500                                "/**\n * Returns the UTF-16 code unit at `index` as a single-character string.\n * Returns `\"\"` if `index` is negative or beyond the string length.\n * @param index Zero-based index of the code unit.\n */",
1501                            ),
1502                        },
1503                    ),
1504                    (
1505                        "at".to_string(),
1506                        MethodSig {
1507                            optional: false,
1508                            generics: Vec::new(),
1509                            params: vec![Param::new("index", Type::Number)],
1510                            ret: Type::Union(vec![Type::String, Type::Undefined]),
1511                            predicate: None,
1512                            doc: doc(
1513                                "/**\n * Returns the code unit at `index` as a single-character string. Negative `index` counts from the end.\n * Returns `undefined` on out-of-range. Use `charAt` for the `\"\"`-on-miss form.\n * @param index Zero-based index; negatives count from the end.\n */",
1514                            ),
1515                        },
1516                    ),
1517                    (
1518                        "charCodeAt".to_string(),
1519                        MethodSig {
1520                            optional: false,
1521                            generics: Vec::new(),
1522                            params: vec![Param::new("index", Type::Number)],
1523                            ret: Type::Number,
1524                            predicate: None,
1525                            doc: doc(
1526                                "/**\n * Returns the UTF-16 code unit at `index` as an integer (0..65535), or `NaN` if out of range.\n * @param index Zero-based index of the code unit.\n */",
1527                            ),
1528                        },
1529                    ),
1530                    (
1531                        "codePointAt".to_string(),
1532                        MethodSig {
1533                            optional: false,
1534                            generics: Vec::new(),
1535                            params: vec![Param::new("index", Type::Number)],
1536                            ret: Type::Union(vec![Type::Number, Type::Undefined]),
1537                            predicate: None,
1538                            doc: doc(
1539                                "/**\n * Returns the Unicode code point starting at `index`, decoding surrogate pairs into values up to 0x10FFFF.\n * Returns `undefined` if `index` is out of range.\n * @param index Zero-based index of the first code unit of the code point.\n */",
1540                            ),
1541                        },
1542                    ),
1543                    (
1544                        "slice".to_string(),
1545                        MethodSig {
1546                            optional: false,
1547                            generics: Vec::new(),
1548                            params: vec![
1549                                Param::with_default(
1550                                    "start",
1551                                    Type::Number,
1552                                    crate::DefaultValue::Number(0.0),
1553                                ),
1554                                // `end` omitted means "to the end".
1555                                Param::with_default(
1556                                    "end",
1557                                    Type::Number,
1558                                    crate::DefaultValue::Number(f64::INFINITY),
1559                                ),
1560                            ],
1561                            ret: Type::String,
1562                            predicate: None,
1563                            doc: doc(
1564                                "/**\n * Returns a new string containing the code units from `start` (inclusive) to `end` (exclusive).\n * Negative values count from the end. The receiver is not modified.\n * @param start First index to copy. Defaults to `0`.\n * @param end Index one past the last to copy. Defaults to the string length.\n */",
1565                            ),
1566                        },
1567                    ),
1568                    (
1569                        "substring".to_string(),
1570                        MethodSig {
1571                            optional: false,
1572                            generics: Vec::new(),
1573                            params: vec![
1574                                Param::with_default(
1575                                    "start",
1576                                    Type::Number,
1577                                    crate::DefaultValue::Number(0.0),
1578                                ),
1579                                Param::with_default(
1580                                    "end",
1581                                    Type::Number,
1582                                    crate::DefaultValue::Number(f64::INFINITY),
1583                                ),
1584                            ],
1585                            ret: Type::String,
1586                            predicate: None,
1587                            doc: doc(
1588                                "/**\n * Like `slice`, but negative values clamp to `0`, and `start` and `end` are swapped when `start > end`.\n * @param start First index to copy. Defaults to `0`.\n * @param end Index one past the last to copy. Defaults to the string length.\n */",
1589                            ),
1590                        },
1591                    ),
1592                    (
1593                        "indexOf".to_string(),
1594                        MethodSig {
1595                            optional: false,
1596                            generics: Vec::new(),
1597                            params: vec![
1598                                Param::new("search", Type::String),
1599                                Param::with_default(
1600                                    "fromIndex",
1601                                    Type::Number,
1602                                    crate::DefaultValue::Number(0.0),
1603                                ),
1604                            ],
1605                            ret: Type::Number,
1606                            predicate: None,
1607                            doc: doc(
1608                                "/**\n * Returns the index of the first occurrence of `search` at or after `fromIndex`, or `-1` if none.\n * An empty `search` returns `min(fromIndex, length)`.\n * @param search Substring to search for.\n * @param fromIndex Index to begin searching from.\n */",
1609                            ),
1610                        },
1611                    ),
1612                    (
1613                        "lastIndexOf".to_string(),
1614                        MethodSig {
1615                            optional: false,
1616                            generics: Vec::new(),
1617                            params: vec![
1618                                Param::new("search", Type::String),
1619                                Param::with_default(
1620                                    "fromIndex",
1621                                    Type::Number,
1622                                    crate::DefaultValue::Number(f64::INFINITY),
1623                                ),
1624                            ],
1625                            ret: Type::Number,
1626                            predicate: None,
1627                            doc: doc(
1628                                "/**\n * Returns the index of the last occurrence of `search` at or before `fromIndex`, or `-1` if none.\n * @param search Substring to search for.\n * @param fromIndex Highest valid start position to consider. Defaults to the string length.\n */",
1629                            ),
1630                        },
1631                    ),
1632                    (
1633                        "includes".to_string(),
1634                        MethodSig {
1635                            optional: false,
1636                            generics: Vec::new(),
1637                            params: vec![
1638                                Param::new("search", Type::String),
1639                                Param::with_default(
1640                                    "fromIndex",
1641                                    Type::Number,
1642                                    crate::DefaultValue::Number(0.0),
1643                                ),
1644                            ],
1645                            ret: Type::Boolean,
1646                            predicate: None,
1647                            doc: doc(
1648                                "/**\n * Returns `true` if `search` occurs at or after `fromIndex`.\n * @param search Substring to search for.\n * @param fromIndex Index to begin searching from.\n */",
1649                            ),
1650                        },
1651                    ),
1652                    (
1653                        "startsWith".to_string(),
1654                        MethodSig {
1655                            optional: false,
1656                            generics: Vec::new(),
1657                            params: vec![
1658                                Param::new("search", Type::String),
1659                                Param::with_default(
1660                                    "position",
1661                                    Type::Number,
1662                                    crate::DefaultValue::Number(0.0),
1663                                ),
1664                            ],
1665                            ret: Type::Boolean,
1666                            predicate: None,
1667                            doc: doc(
1668                                "/**\n * Returns `true` if the substring starting at `position` begins with `search`.\n * @param search Prefix to test for.\n * @param position Index at which to start the comparison.\n */",
1669                            ),
1670                        },
1671                    ),
1672                    (
1673                        "endsWith".to_string(),
1674                        MethodSig {
1675                            optional: false,
1676                            generics: Vec::new(),
1677                            params: vec![
1678                                Param::new("search", Type::String),
1679                                Param::with_default(
1680                                    "endPosition",
1681                                    Type::Number,
1682                                    crate::DefaultValue::Number(f64::INFINITY),
1683                                ),
1684                            ],
1685                            ret: Type::Boolean,
1686                            predicate: None,
1687                            doc: doc(
1688                                "/**\n * Returns `true` if the substring ending at `endPosition` ends with `search`.\n * @param search Suffix to test for.\n * @param endPosition Index marking the end (exclusive) of the substring considered.\n */",
1689                            ),
1690                        },
1691                    ),
1692                    (
1693                        "repeat".to_string(),
1694                        MethodSig {
1695                            optional: false,
1696                            generics: Vec::new(),
1697                            params: vec![Param::new("count", Type::Number)],
1698                            ret: Type::String,
1699                            predicate: None,
1700                            doc: doc(
1701                                "/**\n * Returns a new string containing `count` copies of this string concatenated.\n * Zero `count` (or an empty receiver) returns `\"\"`; a negative `count` throws a `RangeError`.\n * @param count Number of repetitions.\n */",
1702                            ),
1703                        },
1704                    ),
1705                    (
1706                        "padStart".to_string(),
1707                        MethodSig {
1708                            optional: false,
1709                            generics: Vec::new(),
1710                            params: vec![
1711                                Param::new("targetLength", Type::Number),
1712                                Param::with_default(
1713                                    "padString",
1714                                    Type::String,
1715                                    crate::DefaultValue::String(" ".to_string()),
1716                                ),
1717                            ],
1718                            ret: Type::String,
1719                            predicate: None,
1720                            doc: doc(
1721                                "/**\n * Pads this string with `padString` on the left until the result reaches `targetLength` code units. Returns this string unchanged when no padding is needed or when `padString` is empty.\n * @param targetLength Length of the padded result.\n * @param padString Filler repeated to reach the target.\n */",
1722                            ),
1723                        },
1724                    ),
1725                    (
1726                        "padEnd".to_string(),
1727                        MethodSig {
1728                            optional: false,
1729                            generics: Vec::new(),
1730                            params: vec![
1731                                Param::new("targetLength", Type::Number),
1732                                Param::with_default(
1733                                    "padString",
1734                                    Type::String,
1735                                    crate::DefaultValue::String(" ".to_string()),
1736                                ),
1737                            ],
1738                            ret: Type::String,
1739                            predicate: None,
1740                            doc: doc(
1741                                "/**\n * Pads this string with `padString` on the right until the result reaches `targetLength` code units. Returns this string unchanged when no padding is needed or when `padString` is empty.\n * @param targetLength Length of the padded result.\n * @param padString Filler repeated to reach the target.\n */",
1742                            ),
1743                        },
1744                    ),
1745                    (
1746                        "toUpperCase".to_string(),
1747                        MethodSig {
1748                            optional: false,
1749                            generics: Vec::new(),
1750                            params: Vec::new(),
1751                            ret: Type::String,
1752                            predicate: None,
1753                            doc: doc(
1754                                "/**\n * Returns a new string with every character converted to its Unicode uppercase form.\n */",
1755                            ),
1756                        },
1757                    ),
1758                    (
1759                        "toLowerCase".to_string(),
1760                        MethodSig {
1761                            optional: false,
1762                            generics: Vec::new(),
1763                            params: Vec::new(),
1764                            ret: Type::String,
1765                            predicate: None,
1766                            doc: doc(
1767                                "/**\n * Returns a new string with every character converted to its Unicode lowercase form.\n */",
1768                            ),
1769                        },
1770                    ),
1771                    (
1772                        "trim".to_string(),
1773                        MethodSig {
1774                            optional: false,
1775                            generics: Vec::new(),
1776                            params: Vec::new(),
1777                            ret: Type::String,
1778                            predicate: None,
1779                            doc: doc(
1780                                "/**\n * Returns a new string with Unicode whitespace removed from both ends.\n */",
1781                            ),
1782                        },
1783                    ),
1784                    (
1785                        "trimStart".to_string(),
1786                        MethodSig {
1787                            optional: false,
1788                            generics: Vec::new(),
1789                            params: Vec::new(),
1790                            ret: Type::String,
1791                            predicate: None,
1792                            doc: doc(
1793                                "/**\n * Returns a new string with Unicode whitespace removed from the start.\n */",
1794                            ),
1795                        },
1796                    ),
1797                    (
1798                        "trimEnd".to_string(),
1799                        MethodSig {
1800                            optional: false,
1801                            generics: Vec::new(),
1802                            params: Vec::new(),
1803                            ret: Type::String,
1804                            predicate: None,
1805                            doc: doc(
1806                                "/**\n * Returns a new string with Unicode whitespace removed from the end.\n */",
1807                            ),
1808                        },
1809                    ),
1810                    (
1811                        "normalize".to_string(),
1812                        MethodSig {
1813                            optional: false,
1814                            generics: Vec::new(),
1815                            params: vec![Param::with_default(
1816                                "form",
1817                                Type::String,
1818                                crate::DefaultValue::String("NFC".to_string()),
1819                            )],
1820                            ret: Type::String,
1821                            predicate: None,
1822                            doc: doc(
1823                                "/**\n * Returns a new string in the specified Unicode normalization form.\n * @param form One of `\"NFC\"`, `\"NFD\"`, `\"NFKC\"`, `\"NFKD\"`; any other value throws `Error`. Defaults to `\"NFC\"`.\n */",
1824                            ),
1825                        },
1826                    ),
1827                    // regex-arm String overloads. `match`
1828                    // and `search` re-use the `RegExp.exec` host fn;
1829                    // `matchAll` / `split` / `replace` / `replaceAll`
1830                    // are tracked separately.
1831                    (
1832                        "match".to_string(),
1833                        MethodSig {
1834                            optional: false,
1835                            generics: Vec::new(),
1836                            params: vec![Param::new(
1837                                "re",
1838                                Type::prelude_interface("RegExp".to_string(), Vec::new()),
1839                            )],
1840                            ret: Type::Union(vec![
1841                                Type::prelude_interface("RegExpMatch".to_string(), Vec::new()),
1842                                Type::Null,
1843                            ]),
1844                            predicate: None,
1845                            doc: doc(
1846                                "/**\n * Find the next match of `re` in this string. Equivalent to `re.exec(this)` (v1; the `g`-flag array shape is a follow-up).\n * @param re The pattern to search for.\n */",
1847                            ),
1848                        },
1849                    ),
1850                    (
1851                        "search".to_string(),
1852                        MethodSig {
1853                            optional: false,
1854                            generics: Vec::new(),
1855                            params: vec![Param::new(
1856                                "re",
1857                                Type::prelude_interface("RegExp".to_string(), Vec::new()),
1858                            )],
1859                            ret: Type::Number,
1860                            predicate: None,
1861                            doc: doc(
1862                                "/**\n * Index of the first match of `re` in this string, or `-1` if no match.\n * @param re The pattern to search for.\n */",
1863                            ),
1864                        },
1865                    ),
1866                    (
1867                        "replace".to_string(),
1868                        MethodSig {
1869                            optional: false,
1870                            generics: Vec::new(),
1871                            params: vec![
1872                                Param::new(
1873                                    "search",
1874                                    Type::Union(vec![
1875                                        Type::String,
1876                                        Type::prelude_interface("RegExp".to_string(), Vec::new()),
1877                                    ]),
1878                                ),
1879                                Param::new("replacement", Type::String),
1880                            ],
1881                            ret: Type::String,
1882                            predicate: None,
1883                            doc: doc(
1884                                "/**\n * Replace the first match of `search` with `replacement`. For a `RegExp` `search`, the replacement may reference numbered captures (`$1`, `$2`) and named captures (`$<name>`); for both `string` and `RegExp` arms, the special tokens `$$` (literal `$`), `$&` (matched substring), `` $` `` (substring before the match), and `$'` (substring after the match) are honoured.\n * @param search Substring or pattern to match.\n * @param replacement The replacement string.\n */",
1885                            ),
1886                        },
1887                    ),
1888                    (
1889                        "replaceAll".to_string(),
1890                        MethodSig {
1891                            optional: false,
1892                            generics: Vec::new(),
1893                            params: vec![
1894                                Param::new(
1895                                    "search",
1896                                    Type::Union(vec![
1897                                        Type::String,
1898                                        Type::prelude_interface("RegExp".to_string(), Vec::new()),
1899                                    ]),
1900                                ),
1901                                Param::new("replacement", Type::String),
1902                            ],
1903                            ret: Type::String,
1904                            predicate: None,
1905                            doc: doc(
1906                                "/**\n * Replace every non-overlapping match of `search` with `replacement`. Replacement-token semantics match `replace`.\n * @param search Substring or pattern to match.\n * @param replacement The replacement string.\n */",
1907                            ),
1908                        },
1909                    ),
1910                    (
1911                        "split".to_string(),
1912                        MethodSig {
1913                            optional: false,
1914                            generics: Vec::new(),
1915                            params: vec![
1916                                Param::new(
1917                                    "separator",
1918                                    Type::Union(vec![
1919                                        Type::String,
1920                                        Type::prelude_interface("RegExp".to_string(), Vec::new()),
1921                                    ]),
1922                                ),
1923                                Param::with_default(
1924                                    "limit",
1925                                    Type::Number,
1926                                    crate::DefaultValue::Number(f64::INFINITY),
1927                                ),
1928                            ],
1929                            ret: Type::Array(Box::new(Type::String)),
1930                            predicate: None,
1931                            doc: doc(
1932                                "/**\n * Split this string into pieces by matches of `separator`. For an empty string `separator`, returns each code unit as a separate part. For a `RegExp` `separator`, each match's captured groups are inserted after the part before it, as in JavaScript (`undefined` for a group that didn't participate).\n * @param separator Substring or pattern to split on.\n * @param limit Optional cap on the number of returned parts (default: no limit).\n */",
1933                            ),
1934                        },
1935                    ),
1936                    (
1937                        "matchAll".to_string(),
1938                        MethodSig {
1939                            optional: false,
1940                            generics: Vec::new(),
1941                            params: vec![Param::new(
1942                                "re",
1943                                Type::prelude_interface("RegExp".to_string(), Vec::new()),
1944                            )],
1945                            ret: Type::Array(Box::new(Type::prelude_interface("RegExpMatch".to_string(), Vec::new()))),
1946                            predicate: None,
1947                            doc: doc(
1948                                "/**\n * Find every non-overlapping match of `re` in this string. Zero-length matches advance by one to avoid infinite loops, per ECMA-262.\n * @param re The pattern to match.\n */",
1949                            ),
1950                        },
1951                    ),
1952                ]),
1953                properties: BTreeMap::from([(
1954                    "length".to_string(),
1955                    PropertySig {
1956                        ty: Type::Number,
1957                        readonly: true,
1958                        intrinsic: true,
1959                        optional: false,
1960                        doc: doc("/** The number of UTF-16 code units in the string. */"),
1961                    },
1962                )]),
1963                dispatch: Dispatch::Direct,
1964                doc: doc("/** The UTF-16 string type. */"),
1965            },
1966        },
1967    );
1968    defs.types.insert(
1969        "StringConstructor".to_string(),
1970        TypeSymbol {
1971            name: "StringConstructor".to_string(),
1972            mangled_name: crate::mangle::prelude("StringConstructor"),
1973            declaration_span: Span::at(crate::FileId::PRELUDE),
1974            kind: TypeKind::Interface { index: None,
1975                generics: Vec::new(),
1976                methods: BTreeMap::from([
1977                    (
1978                        "@call".to_string(),
1979                        MethodSig {
1980                            optional: false,
1981                            generics: Vec::new(),
1982                            params: vec![Param::new("value", Type::Unknown)],
1983                            ret: Type::String,
1984                            predicate: None,
1985                            doc: doc(
1986                                "/**\n * Convert any value to its string representation: `\"null\"` and `\"undefined\"` for those values, otherwise the value's `toString()` method.\n */",
1987                            ),
1988                        },
1989                    ),
1990                    (
1991                        "fromCharCode".to_string(),
1992                        MethodSig {
1993                            optional: false,
1994                            generics: Vec::new(),
1995                            params: vec![Param::rest("codes", Type::Array(Box::new(Type::Number)))],
1996                            ret: Type::String,
1997                            predicate: None,
1998                            doc: doc(
1999                                "/**\n * Builds a string from UTF-16 code units — one unit per argument.\n * Each value is truncated and reduced modulo 65536, as JavaScript does, so `-1` is U+FFFF.\n * @param codes Zero or more UTF-16 code units (0–65535).\n */",
2000                            ),
2001                        },
2002                    ),
2003                    (
2004                        "fromCodePoint".to_string(),
2005                        MethodSig {
2006                            optional: false,
2007                            generics: Vec::new(),
2008                            params: vec![Param::rest(
2009                                "codePoints",
2010                                Type::Array(Box::new(Type::Number)),
2011                            )],
2012                            ret: Type::String,
2013                            predicate: None,
2014                            doc: doc(
2015                                "/**\n * Builds a string from Unicode code points; astral code points (> U+FFFF) encode as surrogate pairs.\n * Throws a `RangeError` on a negative, non-integer, or > U+10FFFF input.\n * @param codePoints Zero or more Unicode code points (0–0x10FFFF).\n */",
2016                            ),
2017                        },
2018                    ),
2019                ]),
2020                properties: BTreeMap::new(),
2021                dispatch: Dispatch::Static,
2022                doc: doc(
2023                    "/** Constructor object for `string`. Accessed via the global `String` binding. */",
2024                ),
2025            },
2026        },
2027    );
2028    defs.values.insert(
2029        "String".to_string(),
2030        ValueSymbol {
2031            name: "String".to_string(),
2032            mangled_name: crate::mangle::prelude("String"),
2033            declaration_span: Span::at(crate::FileId::PRELUDE),
2034            kind: ValueKind::Const {
2035                ty: Type::prelude_interface("StringConstructor".to_string(), Vec::new()),
2036                doc: doc(
2037                    "/** The `String` constructor — call `String(x)` to convert any value to a string via its `toString()` method. */",
2038                ),
2039            },
2040        },
2041    );
2042}