Skip to content

Bump WebKit (oven-sh/WebKit#1023 preview): bind() takes over the name of a function that was named by a computed key - #48408

Draft
robobun wants to merge 2 commits into
mainfrom
robobun/cb47260c/bind-reads-run-time-name
Draft

robobun wants to merge 2 commits into
mainfrom
robobun/cb47260c/bind-reads-run-time-name

Conversation

@robobun

@robobun robobun commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

WEBKIT_VERSION is the preview build of oven-sh/WebKit#1023. Move it to a commit of oven-sh/WebKit main before this lands.

Problem

  • A decorated method loses its name once it is bound: a.one.bind(a).name is "bound " since 1.4.3 (1.4.2: "bound one").
  • Since js_parser: lower standard decorators without moving class members #40833 a decorated member has a key in brackets. JavaScriptCore keeps such a name only in the name property, and bind() took the name from the code. Any { [key]() {} } has it too.
  • Same release: class A { static get name() { return "custom" } @dec m() {} } binds as bound A.

Fix

  • [JSC] bind() reads the name of a target that was named at run time WebKit#1023 makes bind() read such a name, and defines a lone static accessor through JSFunction::defineOwnProperty. This PR pins its build.
  • Correct: ECMA-262 reads name inside bind(), and Node answers every row the same.
  • Verified with this pin: test/js/bun/jsc/bound-function-name.test.ts (22 of 23 fail on main), es-decorators.test.ts, two jsc-stress fixtures, four test/aot/parallel programs.

Background

  • WEBKIT_VERSION (scripts/build/deps/webkit.ts) names the oven-sh/WebKit release the build links.
  • bind() defers the name and length of its result while the target's are original.
  • Weighed: plain keys in the lowering (the brackets are on purpose), a rename in the decorator helper (decorated members only).

Downsides

Notes

Where this comes from. No user reported it. A comparison with Node found it, and there is no issue to link.

The pin. The preview build is the engine that main pins (ee59053d32) plus the two commits of oven-sh/WebKit#1023, so nothing else of the engine moves. All 7 Test jobs of that pull request pass on those commits.

The base moved once. #48120 moved main from the engine 5a61071f79 to ee59053d32. Both branches were rebased onto the new base. The two engine commits did not change (git range-diff), and the tests below ran again on the new base.

Rows. Standard decorators, run from a folder with no tsconfig. The last column is a debug build with this pin, and Node agrees with it where Node can run the row.

row 1.3.14 1.4.2 1.4.4 this pin
class A { @keep one() {} }, a.one.bind(a).name bound one bound one bound bound one
decorated static, async, generator, async generator method bound <name> bound <name> bound bound <name>
decorated getter, setter bound get g bound get g bound get bound get g
class A { @keep #one() {} call() {} }, a.call.bind(a).name bound call bound call bound bound call
addInitializer(function () { this[name] = this[name].bind(this) }), a.onTick.name bound onTick bound onTick bound bound onTick
getter of a decorated accessor bound get bound get bound get bound get acc
class A { static get name() { return "custom" } @keep m() {} }, A.bind().name bound custom bound custom bound A bound custom
class B { constructor(a, b) {} static get length() { return 9 } @keep m() {} }, B.bind(null, 1).length 8 8 1 8
({ [k]() {} })[k].bind(null).name bound bound bound bound one
Object.getOwnPropertyDescriptor(Request.prototype, "url").get.bind().name bound get url bound url bound url bound get url
a decorator that returns a wrapper the wrapper's name the same the same the same
  • Functions that can be reached from the global object and the built-in modules, with a bound name that is not "bound " + name: 632 of 4466 on a debug build of main, 0 of 4466 with this pin. Node v26.3.0: 0 of 3406.

Tests.

  • test/js/bun/jsc/bound-function-name.test.ts: 17 kinds of target through new Function (so that the transpiler leaves the keys as written), four native accessor functions, the name at the time of bind(), and the length.
  • test/bundler/transpiler/es-decorators.test.ts: every kind of decorated member, the member that holds the list of a #private neighbour, addInitializer with bind, a bind inside the decorator, and a class with a static accessor called name or length in front of a decorated member.
  • test/js/bun/jsc-stress/fixtures/: the two files of [JSC] bind() reads the name of a target that was named at run time WebKit#1023 as they are there, with // @bun as the first line. They run in every tier.
  • test/aot/parallel/inferred-names/a-bound-class-takes-the-name-and-the-length-that-its-static-accessors-answer.mjs is new: 24 rows of bound names and lengths, the executable against plain Bun.
  • Three programs lose what hid these rows: the stored output of a-bound-function-takes-the-name-that-a-computed-key-gave-its-target.mjs (plain Bun now prints it), a continue in closed-bugs/1600-bound-function-under-a-computed-key, and a return in answers-of-the-engine/function-objects-of-every-kind.mjs. All four pass in every way of the runner with this pin (21 builds). A run of 64 programs under test/aot/parallel that bind a function and read a name passes too (258 builds, three of the four among them).
  • test/aot/fuzz/generators/object-mutations.ts no longer leaves out the bind of a method under a computed key. 300 programs of that product (--holders=computed-method-in-literal --looks=bind-it --sample=300 --seed=1) print the same as Node v26.3.0 with this pin. 241 of them differ on main.
  • Existing suites on debug builds of main and of this pin, 1584 tests in 46 files (test/js/bun/util/inspect*, test/js/node/util/node-inspect-tests, test/js/bun/test/mock-fn.test.js, test/js/bun/jsc*, bundler_bytecode_portable.test.ts): no test fails with this pin and passes on main. 30 tests fail on both builds in the container that ran them: 28 pass a time limit of the debug build, and 2 fail behind such a timeout. With a longer limit one of the 30 still fails, on the stored hash of a bundle of libraries from test/node_modules. The text that the bundler writes for it differs in that container, before the engine sees it. Every other hash of bundler_bytecode_portable.test.ts is the stored one with this pin.

Text that changes. console.log(a.one.bind(a)) prints [Function: one] again, as 1.4.2 did. String(bound) gives function one() { [native code] } for these targets, as it does for a target with a plain key.

Cost. The numbers are from release builds of the engine at 5a61071f79 and at this change on that base. The files of the bind() path are the same at ee59053d32. oven-sh/WebKit#1023 has the full table. A function with a name in the code binds at the same cost (34.9 instructions in JIT code on both). Node v26.3.0 pays 21,901 instructions per instance for the class with 5 bound methods as Bun lowers it, and 2,339 with plain keys.

Left for other changes.


[policy-decision:webkit] gate passed · iteration 0 · 11 files touched

passes on PR (with fix)
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/bundler/transpiler/es-decorators.test.ts' 'test/js/bun/jsc-stress/jsc-stress.test.ts' 'test/js/bun/jsc/bound-function-name.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/bundler/transpiler/es-decorators.test.ts test/js/bun/jsc-stress/jsc-stress.test.ts test/js/bun/jsc/bound-function-name.test.ts
bun test v1.4.4 (367d939d9)

test/bundler/transpiler/es-decorators.test.ts:
(pass) standard decorators in a CommonJS module [380.42ms]
(pass) legacy decorators in a CommonJS module [356.45ms]
(pass) ES Decorators > class decorators > basic class decorator [263.05ms]
(pass) ES Decorators > class decorators > class decorator receives correct context [262.05ms]
(pass) ES Decorators > class decorators > class decorator can replace class [269.49ms]
(pass) ES Decorators > class decorators > multiple class decorators apply in reverse order [283.49ms]
(pass) ES Decorators > method decorators > instance method decorator [254.21ms]
(pass) ES Decorators > method decorators > static method decorator [273.09ms]
(pass) ES Decorators > method decorators > method decorator context has correct access [261.00ms]
(pass) ES Decorators > getter decorators > getter decorator [262.66ms]
(pass) ES Decorators > setter decorators > setter decorator [275.11ms]
(pass) ES Decorators > field decorators > field decorator receives undefined value [259.70ms]
(pass) ES Decorators > field decorators > multiple field decorators [248.72ms]
(pass) ES Decorators > field decorators > static field decorator [304.34ms]
(pass) ES Decorators > non-ASCII string-literal keys > Bun.Transpiler output preserves the key [110.07ms]
(pass) ES Decorators > non-ASCII string-literal keys > decorated instance field with supra-BMP key is assigned correctly [368.05ms]
(pass) ES Decorators > non-ASCII string-literal keys > decorated static field with supra-BMP key is assigned correctly [301.87ms]
(pass) ES Decorators > non-ASCII string-literal keys > decorated accessor with supra-BMP key works [278.97ms]
(pass) ES Decorators > non-ASCII string-literal keys > undecorated accessor with supra-BMP key in a de
... (truncated)
Exit: 0
diff hotspot
scripts/build/deps/webkit.ts                       |   2 +-
 test/aot/fuzz/generators/object-mutations.ts       |   8 +-
 .../function-objects-of-every-kind.mjs             |   2 +-
 .../names-without-bound-under-computed-key.mjs     |   2 -
 ...the-length-that-its-static-accessors-answer.mjs |  30 +++
 ...he-name-that-a-computed-key-gave-its-target.mjs |   9 -
 test/bundler/transpiler/es-decorators.test.ts      |  89 +++++++
 ...bound-function-name-of-run-time-named-target.js | 282 +++++++++++++++++++++
 ...nd-class-with-static-name-or-length-accessor.js |  96 +++++++
 test/js/bun/jsc-stress/jsc-stress.test.ts          |   2 +
 test/js/bun/jsc/bound-function-name.test.ts        | 132 ++++++++++
 11 files changed, 635 insertions(+), 19 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                                      reads  edits  tests
scripts/build/deps/webkit.ts                                  0      0     36
test/aot/fuzz/generators/object-mutations.ts                  0      0     42
…nswers-of-the-engine/function-objects-of-every-kind.mjs      0      0     39
…computed-key/names-without-bound-under-computed-key.mjs      0      0     43
…ame-and-the-length-that-its-static-accessors-answer.mjs      0      1     37
…-takes-the-name-that-a-computed-key-gave-its-target.mjs      0      0     43
test/bundler/transpiler/es-decorators.test.ts                 1      1     16
…ixtures/bound-function-name-of-run-time-named-target.js      0      0     38
…ction-bind-class-with-static-name-or-length-accessor.js      0      0     36
test/js/bun/jsc-stress/jsc-stress.test.ts                     0      0     19
test/js/bun/jsc/bound-function-name.test.ts                   0      1     21

@robobun

robobun commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:57 AM PT - Nov 10th, 2026

✅ @robobun, your commit 88c790d88905404c68944d52ed80c7ab473d6998 passed in Build #139643! 🎉


🧪   To try this PR locally:

bunx bun-pr 48408

That installs a local version of the PR into your bun-48408 executable, so you can run:

bun-48408 --bun

@robobun

robobun commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

  • Reproduced on Bun 1.4.4 and on a debug build of main (run from a folder with no tsconfig):
    function keep(f, c) {}
    class A {
      @keep one() {}
    }
    const a = new A();
    console.log(a.one.bind(a).name);
    It prints bound (with a space behind it). Bun 1.4.2 prints bound one.
  • bun bd test test/js/bun/jsc/bound-function-name.test.ts: 22 of 23 tests fail on main, and 23 pass with this pin.
  • The engine change is [JSC] bind() reads the name of a target that was named at run time WebKit#1023. This pull request is a draft until WEBKIT_VERSION names a commit of oven-sh/WebKit main.
  • AOT: the next base #48120 moved the engine of main to ee59053d32. This branch and [JSC] bind() reads the name of a target that was named at run time WebKit#1023 are rebased onto it. The pin is the preview build of that pull request on the new engine.
  • CI on 88c790d (build 139643): 184 of 188 jobs pass, and no test is red. The four macOS x64 test jobs did not start yet. That lane has a queue of several hours, and jobs that wait there expire and are scheduled again.

…has when bind() runs

The tests need two JavaScriptCore changes. Function.prototype.bind() reads the
name of a target that was named by a key in brackets or made for a native
accessor. A static accessor called name or length that is defined by itself
goes through the defineOwnProperty of the class. The next commit moves
WEBKIT_VERSION to a build that has both.
The engine that main pins (ee59053d32) plus that change: bind() reads the name
of a target that was named at run time, and a static accessor that is defined
by itself goes through the defineOwnProperty of the class.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants