diff --git a/src/twocore/frontend/js/lower.gleam b/src/twocore/frontend/js/lower.gleam index be1f19d..921952c 100644 --- a/src/twocore/frontend/js/lower.gleam +++ b/src/twocore/frontend/js/lower.gleam @@ -4358,6 +4358,24 @@ fn lower_call_fixed( ) if sym == "match" || sym == "replace" || sym == "search" || sym == "split" -> lower_regex_symbol_method(sym, object, arguments, env, ctx, ctr) + // `Number.prototype.toString(radix)` — `Number.prototype` is itself a Number + // object whose [[NumberData]] is +0 (ES2023 §21.1.3), so per §21.1.3.6 it + // renders as "0" in every radix (2–36) and for an absent/undefined/10 radix. + // Route it through `num_to_string_radix` with a literal 0 receiver; without + // this the receiver is the opaque `builtin_prototype("Number")` marker, whose + // ToNumber is NaN, so toString falls through to the generic object form + // "[object Object]". + ast.MemberExpression( + object: ast.MemberExpression( + object: ast.Identifier(name: "Number", ..), + property: ast.Identifier(name: "prototype", ..), + computed: False, + .., + ), + property: ast.Identifier(name: "toString", ..), + computed: False, + .., + ) -> lower_number_proto_tostring(arguments, env, ctx, ctr) // recv.method(args) — method dispatch (array/string methods). ast.MemberExpression( object:, @@ -5539,6 +5557,37 @@ fn lower_method( } } +/// `Number.prototype.toString(radix)` — render the [[NumberData]] of +/// `Number.prototype`, which is +0 (ES2023 §21.1.3), in `radix`. +/// +/// Parameters: `arguments` is the call's argument list; the first argument (if +/// present) is the radix and any excess is ignored. An absent or `undefined` +/// radix means base 10 (§21.1.3.6 step 3). +/// +/// Returns the lowered `#(binds, value, ctr)` for a `num_to_string_radix` call +/// whose receiver is a literal 0, so the result is always the string "0" +/// regardless of the radix (0 is the same digit in every base). Never returns +/// `Error` for this shape (the radix expression itself may still error while +/// lowering, propagated via `result_try`). +fn lower_number_proto_tostring( + arguments: List(ast.Expression), + env: Env, + ctx: Ctx, + ctr: Int, +) -> Result(#(List(Bind), ir.Value, Int), Error) { + use #(ba, argvals, ctr) <- result_try(lower_args(arguments, env, ctx, ctr)) + let radix = case argvals { + [r, ..] -> r + [] -> undefined() + } + let #(bz, zero, ctr) = number_literal(0.0, ctr) + Ok(bind_after( + list.append(ba, bz), + ir.CallHost("js", "num_to_string_radix", [zero, radix]), + ctr, + )) +} + /// The arity of `C.method` if `C` is a known class with static method `method`. fn static_method( object: ast.Expression, diff --git a/test/js_compiler_test.gleam b/test/js_compiler_test.gleam index f455b5a..0bad27e 100644 --- a/test/js_compiler_test.gleam +++ b/test/js_compiler_test.gleam @@ -7328,3 +7328,36 @@ pub fn wave14_arrow_callback_ignores_thisarg_test() { val("[1, 2, 3].map((x) => x * 2, { k: 9 }).join(',')") |> should.equal(dyn("2,4,6")) } + +// ==== Number (wave 15) ==== + +// `Number.prototype.toString()` — `Number.prototype` is itself a Number object whose +// [[NumberData]] is +0 (ES2023 §21.1.3), so §21.1.3.6 renders it as "0" for an absent +// radix (base 10). test262 S15.7.4.2_A1_T01. +pub fn number_proto_tostring_default_test() { + val("Number.prototype.toString()") |> should.equal(dyn("0")) +} + +// The same value with an explicit radix of 10 or `undefined` is still base 10 → "0". +// test262 S15.7.4.2_A1_T02 / _A1_T03. +pub fn number_proto_tostring_radix10_and_undefined_test() { + val("Number.prototype.toString(10)") |> should.equal(dyn("0")) + val("Number.prototype.toString(undefined)") |> should.equal(dyn("0")) +} + +// For every radix 2..36 the digit for 0 is "0", so `Number.prototype.toString(r)` +// is "0" throughout the whole legal radix range. test262 S15.7.4.2_A2_T01..T34. +pub fn number_proto_tostring_all_radixes_test() { + val("Number.prototype.toString(2)") |> should.equal(dyn("0")) + val("Number.prototype.toString(8)") |> should.equal(dyn("0")) + val("Number.prototype.toString(16)") |> should.equal(dyn("0")) + val("Number.prototype.toString(36)") |> should.equal(dyn("0")) +} + +// A plain number renders its own value in the requested radix (§21.1.3.6): the radix +// path is unaffected by the Number.prototype receiver special-case above. +pub fn number_tostring_radix_plain_value_test() { + val("(255).toString(16)") |> should.equal(dyn("ff")) + val("(5).toString(2)") |> should.equal(dyn("101")) + val("(-1).toString(2)") |> should.equal(dyn("-1")) +}