Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/twocore/frontend/js/lower.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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:,
Expand Down Expand Up @@ -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,
Expand Down
33 changes: 33 additions & 0 deletions test/js_compiler_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
Loading