String and Number built-ins

The member functions a string answers, reached from string in ../lambda/string.rs, and a number's formatters, from number in ../lambda/number.rs with their arithmetic in ../number/format.rs. Each is ECMAScript 2025's algorithm on every input a module can build, pinned by the shared corpus in fjs/nanvm, and the completeness table in fjs/nanvm/methods keeps this set equal to what the compiler admits. The same arrangement for Array is ../array/README.md.

filebuilt-ins
reads.rsat, charAt, charCodeAt, isWellFormed, toWellFormed
search.rsincludes, indexOf, lastIndexOf, endsWith
building.rsslice, substring, concat, repeat, padStart, padEnd, trim, trimStart, trimEnd
patterns.rsreplace, replaceAll, split
casing.rstoLowerCase, toUpperCase

Answered by the runtime

startsWith has no entry here: its body is written once, in fjs/js/runtime, which npm run gen compiles into ../gen.runtime.rs, and the call step reaches it through ../runtime.rs after the table above has no entry. The names still here have no body yet: the primitives a body is written over, and the names whose bodies wait on a primitive the language has not admitted. A name moves out of this table when its body lands, or stays beside the body where a measurement justifies the second implementation.

What is out, by design

The language refuses these names on a string (fjs/js/prototype/README.md), and this crate grows no entry for any of them:

nameswhy
match, matchAll, searchCoerce the argument to a RegExp, a type the language lacks.
normalizeDepends on the engine's Unicode version, which nothing pins for it (case conversion pins one for toLowerCase and toUpperCase).
toLocaleLowerCase, toLocaleUpperCase, localeCompareRead the host locale.
substr, trimLeft, trimRightAnnex B aliases.
anchor, big, blink, bold, fixed, fontcolor, fontsize, italics, link, small, strike, sub, supAnnex B HTML wrappers.
valueOf, constructorThe coercion protocol, and a data property.

Several branches of the algorithms have no input here, and the code does not model them:

  • Regular expressions. A module cannot spell a RegExp or a symbol, so every pattern replace, replaceAll and split see is a value converted by ToString, and includes, startsWith and endsWith never meet the IsRegExp throw.
  • Capture groups. A string pattern has none, so GetSubstitution substitutes $$, $&, $` and $' and leaves $1 and $<name> as written, as JavaScript does.

Code units, not code points

Every position, length and index counts UTF-16 code units, as JavaScript does: a surrogate pair is two positions, "😀".split("") is two lone surrogates. Only isWellFormed and toWellFormed look at pairs here; codePointAt does in its body in the runtime, over the intrinsic charCodeAt. A lone surrogate has no Rust &str spelling, so the corpus and fjs compile write such a string as its code units, string_any_utf16(&[…]).

Case conversion

toLowerCase and toUpperCase are the Unicode Default Case Conversion, full mappings included — "ß".toUpperCase() is "SS", a final sigma reads its context — through str::to_lowercase and str::to_uppercase, at the Unicode version the language pins: casing.rs names it and asserts char::UNICODE_VERSION against it at compile time, so a build whose standard library carries another version does not build (fjs/js/prototype/README.md). The receiver is converted a well-formed run at a time, a lone surrogate copied unchanged between runs: it has no case, and the lossy conversion to a Rust string would make it U+FFFD. A lone surrogate is neither cased nor case-ignorable, so a run ending at one leaves the final-sigma context as JavaScript reads it. A full mapping can lengthen the result, which is counted and refused past the limit like any other.

When an argument is read

  • lastIndexOf ToNumbers its position first and reads NaN — undefined included — as the end, unlike Array's, which reads a passed undefined as 0.
  • padStart and padEnd convert the fill only when padding is needed.
  • split reads its limit, then converts its separator, and only then does a limit of 0 answer [], as ECMAScript orders it: a separator whose conversion throws throws even with a limit of 0.
  • replace and replaceAll decide whether the replacement is a function, and convert a template, before any match is looked for.
  • toFixed checks its digit range before the number, so Infinity.toFixed(101) throws. toExponential and toPrecision keep an undefined argument apart, absent or passed, for its own meaning; any other they convert first, so NaN is 0, "2.9" is 2, and a bigint throws even on Infinity; then they look at the number before the range, so Infinity.toExponential(101) is "Infinity".

Exact rounding

toFixed, toExponential and toPrecision round the double's exact binary value, the larger digit string on a tie: (2.5).toFixed(0) is "3", and (1.005).toFixed(2) is "1.00" because the double nearest 1.005 is below it. All three round the magnitude and put the sign back, so (-2.5).toFixed(0) is "-3", (-2.5).toExponential(0) is "-3e+0" and (-2.5).toPrecision(1) is "-3". Only -0 itself stays unsigned: "0.00", "0.00e+0", "0.00". Rust's format!("{:.0}", 2.5) rounds half to even and answers "2", so the arithmetic is BigInt<A>'s.

Implementation-defined results

  • The longest string. String<A> holds at most 2³² − 1 code units, and create.rs refuses a longer result with the RangeError engines throw. V8's limit is far lower, so a length between the two answers here and throws there.
  • Radix digits of a fraction. ECMAScript leaves (0.5).toString(2) to the engine, so NaNVM refuses a fraction with a radix other than ten. An integer, and every bigint, converts exactly.
  • Radix digits of a large integer. Exact is not what V8 answers: above 2⁵³, in a radix that is not a power of two, its Number.prototype.toString can lose digits — (1e21).toString(36) is "5v1j4f4ds7c000" there, and "5v1j4f4ds79m9s" here and in BigInt(1e21).toString(36). The shared corpus is checked against Node, so such a case belongs in the corpus only with a rust note, or as a bigint.

vm/number/format.rs holds the formatters' arithmetic, and ../bigint/radix.rs the radix digits.