// Type-coercion helpers implementing the ES abstract operations used by the
// VM arithmetic and comparison paths.
//
// Scope: implement enough of `ToNumber` / `ToString` / `ToBoolean` /
// `ToInt32` / `ToUint32` / `ToPrimitive` to cover M1 AC — meaning primitives
// only. `ToPrimitive` on an Object throws `TypeError` for M1 because
// `valueOf` / `toString` method dispatch is a Step 8b + Step 9 concern.
//
// Design deviations noted in the check report:
//
// - `ToString(Number)` uses MoonBit's `Double::to_string` which formats
// doubles slightly differently from JS `Number.prototype.toString` (JS
// prints `1` where MoonBit prints `1.0`). We post-process the common
// cases: strip trailing `.0`, replace `nan` with `NaN`, `inf`/`-inf` with
// `Infinity`/`-Infinity`. Perfect round-tripping is deferred to M6.
// - `ToNumber(String)` uses `@string.parse_double` for numeric literals and
// returns `NaN` on parse failure. JS quirks like `""` → 0, whitespace
// trim, `"0x1F"` → 31, `"Infinity"` → Infinity are handled explicitly
// ahead of the parse.
///|
/// ES `ToBoolean(argument)`. See ECMA-262 7.1.2. Only the primitive branches
/// arise in M1 AC — an `Object(_)` / `Function(_)` value is always truthy
/// per JS.
fn to_boolean(v : @value.JSValue) -> Bool {
match v {
Undefined => false
Null => false
Bool(b) => b
Int32(0) => false
Int32(_) => true
// JS quirk: both +0 and -0 are falsy, NaN is falsy, everything else
// (including subnormals) is truthy.
Number(d) =>
if d.is_nan() {
false
} else if d == 0.0 {
false
} else {
true
}
Str(s) => s.length() > 0
Object(_) => true
Function(_) => true
NativeFn(_) => true
}
}
///|
/// ES `ToNumber(argument)`. See ECMA-262 7.1.4. M1 supports every primitive;
/// on an object it currently returns `NaN` (Step 8b will call user-visible
/// `valueOf` / `toString` via method dispatch — for now the object always
/// coerces to NaN which is what would happen if `valueOf` returned the
/// object itself and `toString` was missing).
fn to_number(v : @value.JSValue) -> Double {
match v {
Undefined => @double.not_a_number
Null => 0.0
Bool(true) => 1.0
Bool(false) => 0.0
Int32(i) => i.to_double()
Number(d) => d
Str(s) => string_to_number(s)
// Object → in real JS: ToPrimitive(hint Number) then ToNumber.
// For M1 without method dispatch, coerce to NaN. Documented deviation.
Object(_) => @double.not_a_number
// Function → similarly NaN under M1's no-method-dispatch policy.
Function(_) => @double.not_a_number
// NativeFn → same NaN treatment: JS `+f` where `f` is a function yields
// NaN because `ToPrimitive` on a function eventually calls `toString`
// whose result parses to NaN.
NativeFn(_) => @double.not_a_number
}
}
///|
/// Parse a JS numeric literal from a `String`. Implements the subset of
/// ECMA-262 7.1.4.1.1 (StringToNumber) that M1 AC exercises:
///
/// - Whitespace-trim leading and trailing (ASCII space / tab / newline).
/// - Empty (after trim) → 0.
/// - `"Infinity"` / `"+Infinity"` / `"-Infinity"` → ±Infinity.
/// - Hex `"0x..."` / `"0X..."` → parsed as unsigned base-16.
/// - Binary `"0b..."` / `"0B..."` → base-2.
/// - Octal `"0o..."` / `"0O..."` → base-8.
/// - Otherwise: delegate to `@string.parse_double`, `NaN` on failure.
fn string_to_number(s : String) -> Double {
// `String::trim` with no argument trims Unicode whitespace by default.
let trimmed_view = s.trim()
if trimmed_view.length() == 0 {
return 0.0
}
let trimmed = trimmed_view.to_owned()
// ±Infinity fast path.
if trimmed == "Infinity" || trimmed == "+Infinity" {
return @double.infinity
}
if trimmed == "-Infinity" {
return @double.neg_infinity
}
// Prefix bases: 0x/0X, 0b/0B, 0o/0O.
if trimmed.length() >= 2 && trimmed.charcode_at(0) == '0' {
let c = trimmed.charcode_at(1)
if c == 'x' || c == 'X' {
return parse_int_radix(trimmed[2:].to_owned(), 16)
}
if c == 'b' || c == 'B' {
return parse_int_radix(trimmed[2:].to_owned(), 2)
}
if c == 'o' || c == 'O' {
return parse_int_radix(trimmed[2:].to_owned(), 8)
}
}
// Otherwise: decimal float. `@string.parse_double` handles the general case
// (including exponents, decimals, leading sign). It raises on invalid input,
// in which case we return NaN per JS.
@string.parse_double(trimmed.view()) catch {
_ => @double.not_a_number
}
}
///|
/// Helper: parse `s` as an unsigned integer in `radix`, return `NaN` on
/// empty / invalid digits. Used by `string_to_number` for `0x..` / `0b..` /
/// `0o..` prefix literals.
fn parse_int_radix(s : String, radix : Int) -> Double {
if s.length() == 0 {
return @double.not_a_number
}
let mut acc = 0.0
let radix_d = radix.to_double()
for i in 0..= '0'.to_int() && ch <= '9'.to_int() {
ch - '0'.to_int()
} else if ch >= 'a'.to_int() && ch <= 'z'.to_int() {
ch - 'a'.to_int() + 10
} else if ch >= 'A'.to_int() && ch <= 'Z'.to_int() {
ch - 'A'.to_int() + 10
} else {
return @double.not_a_number
}
if d < 0 || d >= radix {
return @double.not_a_number
}
acc = acc * radix_d + d.to_double()
}
acc
}
///|
/// Helper: `String::charcode_at(i)` — return the UTF-16 code unit at index
/// `i` as `Int`. MoonBit's `String::at` returns `UInt16`; we always want to
/// compare against ASCII chars so an `Int` view is nicer.
fn String::charcode_at(self : String, i : Int) -> Int {
self.at(i).to_uint().reinterpret_as_int()
}
///|
/// ES `ToInt32(argument)`. See ECMA-262 7.1.6. Converts to Number then
/// truncates to a signed 32-bit integer. NaN / ±Infinity → 0.
fn to_int32(v : @value.JSValue) -> Int {
match v {
Int32(i) => i
_ => double_to_int32(to_number(v))
}
}
///|
/// The Number → Int32 conversion per ES `ToInt32` steps 4–7: truncate toward
/// zero, take modulo 2^32, then interpret as signed 32-bit.
fn double_to_int32(d : Double) -> Int {
if d.is_nan() || d.is_inf() {
return 0
}
// Truncate toward zero.
let truncated = d.trunc()
// Take modulo 2^32.
let two32 = 4294967296.0
let modded = truncated - two32 * (truncated / two32).floor()
// Reinterpret as signed 32-bit.
if modded >= 2147483648.0 {
(modded - two32).to_int()
} else {
modded.to_int()
}
}
///|
/// ES `ToUint32(argument)`. Same as `ToInt32` but reinterpret the low 32
/// bits as unsigned.
fn to_uint32(v : @value.JSValue) -> UInt {
let i = to_int32(v)
i.reinterpret_as_uint()
}
///|
/// ES `ToString(argument)`. Primitives only in M1.
fn to_string(v : @value.JSValue) -> String {
match v {
Undefined => "undefined"
Null => "null"
Bool(true) => "true"
Bool(false) => "false"
Int32(i) => i.to_string()
Number(d) => number_to_string(d)
Str(s) => s
// M1: Object → "[object Object]" stub. Real toString method dispatch
// arrives in Step 9 with Object.prototype.
Object(_) => "[object Object]"
// Function → source-form stub. `Function.prototype.toString` in real
// JS returns the function's source; for M1 we return a synthetic form.
Function(f) => "function " + f.name() + "() { [native code] }"
// NativeFn → same synthetic form as Function; native code has no
// ECMAScript source to return anyway.
NativeFn(nf) => "function " + nf.name() + "() { [native code] }"
}
}
///|
/// Format a JS Number to its `Number.prototype.toString` string form.
/// Handles the common cases MoonBit's `Double::to_string` doesn't get right:
///
/// - `NaN`, `Infinity`, `-Infinity`.
/// - Integer values print without a trailing `.0`.
/// - Negative zero prints as `"0"` (not `"-0"`).
///
/// General-purpose scientific-notation formatting for very large / small
/// doubles is deferred to M6 alignment work; MoonBit's default output is
/// used as-is for that case.
fn number_to_string(d : Double) -> String {
if d.is_nan() {
return "NaN"
}
if d.is_pos_inf() {
return "Infinity"
}
if d.is_neg_inf() {
return "-Infinity"
}
if d == 0.0 {
return "0"
}
// If the value is an integer and fits in Int64 range, print as integer.
let trunc = d.trunc()
if trunc == d && d >= -9.223372036854775e18 && d <= 9.223372036854775e18 {
// Try to fit into Int64 for stable integer formatting.
let i64_val = d.to_int64()
if i64_val.to_double() == d {
return i64_val.to_string()
}
}
// Fallback: MoonBit's default double formatting. May emit `1.0e100` etc.
d.to_string()
}
// Note: `ToPrimitive` (hint Number / String / Default) is not implemented
// in 8a because it requires method dispatch (`Symbol.toPrimitive`,
// `valueOf`, `toString`) which lands with Step 8b's call/return + Step 9's
// Object.prototype builtins. For 8a all `to_*` coercions on an Object
// argument return the "unavailable object" sentinel: `to_number(Object)` →
// NaN; `to_string(Object)` → "[object Object]"; `to_boolean(Object)` →
// true. These are the correct results after a hypothetical `ToPrimitive`
// call whose downstream `valueOf`/`toString` are absent, which is what
// M1 has today.