Skip to content

builtins: fix BigInt prototype methods panicking on a primitive receiver - #45

Draft
mparrett wants to merge 2 commits into
nooga:mainfrom
mparrett:fix/bigint-prototype-this-unwrap
Draft

builtins: fix BigInt prototype methods panicking on a primitive receiver#45
mparrett wants to merge 2 commits into
nooga:mainfrom
mparrett:fix/bigint-prototype-this-unwrap

Conversation

@mparrett

@mparrett mparrett commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

10n.toString() prints nothing and exits 0. So do valueOf(), toLocaleString(), and BigInt(10n) — every BigInt prototype method is unusable on a primitive receiver.

Two commits: the panic fix, and a separable radix fix that the first one makes reachable.

1. The panic

All four sites inlined the same object-wrapper unwrap:

if v.Type() == vm.TypeBigInt {
    // For BigInt object wrappers, extract the primitive value from [[BigIntData]]
    if po := v.AsPlainObject(); po != nil {

AsPlainObject panics unless the value is TypeObject (value.go:968-971), and a BigInt object wrapper is TypeObject — never TypeBigInt. The guard admits exactly the values the body cannot handle: the wrapper branch was unreachable, and every primitive receiver panicked.

It reads as a silent stop rather than a crash because VM.run's recover discards the panic and returns InterpretOK — filed separately as #44.

Replaced all four copies with one thisBigIntValue helper implementing ThisBigIntValue. Four hand-inlined copies of a subtle unwrap is what produced four identical bugs.

The slot name matters here. The spec calls it [[BigIntData]], and that is what the old code looked up — but nothing in this repo ever writes that name. Object(bigint) stores [[PrimitiveValue]] (object_init.go:944), so searching for the spec name would have left the wrapper arm dead in a new place. The helper uses [[PrimitiveValue]] and checks the stored value's type, since Number/String/Boolean/Symbol wrappers share that slot.

2. The radix

toString's radix was coerced with a bare ToFloat and clamped into 2..36, with a comment noting real JS throws. Now runs the package's existing toIntegerOrInfinityWithVM and throws RangeError outside 2..36.

Separable, but included because commit 1 is what makes these paths reachable — see the test262 accounting below. Happy to split it out if you'd rather.

Verification

Primitive and wrapper receivers, all matching Node:

10n.toString()                          10
10n.toString(16)                        a
BigInt("-255").toString(16)             -ff
10n.valueOf()                           10n   (typeof bigint)
10n.toLocaleString()                    10
BigInt(10n)                             10n
Object(10n).toString()                  10
Object(10n).toLocaleString()            10
BigInt.prototype.valueOf.call(Object(10n))  10n
BigInt(Object(10n))                     10n

Non-BigInt receiver fallbacks are unchanged: toString/toLocaleString stringify, valueOf throws TypeError.

tests/scripts/bigint_prototype_methods.ts covers all of the above. With commit 1 reverted it fails with "undefined" — the script produces nothing at all.

go build/vet/gofmt clean; TestScripts, pkg/vm, pkg/builtins, pkg/compiler pass.

test262

built-ins/BigInt: 41 → 41, and language: 23222/23648 with zero per-test diffs.

The count is flat but the composition changed, so the per-test diff is worth stating plainly:

  • +1 prototype/valueOf/cross-realm.js — genuinely fixed by the wrapper unwrap.
  • −1 prototype/toString/radix-tointegerorinfinity-throws-toprimitive-or-bigint.js — this was a false pass. The panic ended the script before its assertion ran, and the harness scored the silence as green. It now fails for a real, pre-existing reason: toIntegerOrInfinityWithVM doesn't reject a BigInt returned from ToPrimitive. That helper is shared with Array, so fixing it belongs in its own change where the Array impact can be measured — deliberately not touched here.

Two other radix tests (radix-err.js, radix-tointegerorinfinity-throws-symbol.js) were false passes for the same reason; commit 2 turns those into genuine passes, which is why the total lands back at 41 rather than 38.

Note on the test

It calls through any. The checker rejects property access on a bigint receiver outright ("property access is not supported on type bigint"), rejects unary - on bigint, and types a bigint literal as a number literal (const b: bigint = 10n fails). All pre-existing and unrelated to this PR — happy to file separately if you want them tracked.

🤖 Generated with Claude Code

mparrett and others added 2 commits July 25, 2026 15:40
BigInt.prototype.toString/toLocaleString/valueOf and the BigInt()
constructor each inlined the same object-wrapper unwrap:

    if v.Type() == vm.TypeBigInt {
        if po := v.AsPlainObject(); po != nil {   // panics

AsPlainObject panics unless the value is TypeObject, and a BigInt object
wrapper is TypeObject - never TypeBigInt. So the guard admitted exactly
the values the body could not handle: the wrapper branch was unreachable
and every primitive receiver panicked. `10n.toString()` printed nothing
and exited 0.

Replace all four copies with one thisBigIntValue helper implementing the
spec's ThisBigIntValue. The wrapper slot is spelled [[PrimitiveValue]],
which is what object_init.go's Object(bigint) case actually writes - the
spec calls it [[BigIntData]], and looking for that name is why the
wrapper arm never matched. Number/String/Boolean/Symbol wrappers share
the slot, so the stored value's type is checked before unwrapping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The radix argument was coerced with a bare ToFloat and then clamped into
2..36, with a comment noting that real JS throws. Spec runs
ToIntegerOrInfinity - which rejects a Symbol, and a ToPrimitive that
returns a BigInt - and then throws RangeError outside 2..36.

Uses the package's existing toIntegerOrInfinityWithVM. Separable from the
panic fix, but included here because that fix is what makes these paths
reachable at all: three test262 radix tests previously "passed" only
because the panic ended the script before their assertions ran.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant