Skip to content

Bump WebKit (oven-sh/WebKit#977 preview): a function-typed field that holds a promise's resolve is read without a test - #47683

Open
robobun wants to merge 2 commits into
aot-follow-up-1031from
robobun/a72f4800/promise-resolving-functions
Open

robobun wants to merge 2 commits into
aot-follow-up-1031from
robobun/a72f4800/promise-resolving-functions

Conversation

@robobun

@robobun robobun commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #47599

Problem

  • bun build --compile --aot --explain reports missed-optimization three times for a class field typed as a function that holds the resolve of a new Promise(executor): the read in the settle loop, the store in the executor, and the declaration. The field is read behind a HasBirthFieldsFilled test with a by-name arm. This is the request-batching pattern.
  • The engine typed the resolving functions as objects it knew nothing of, so the store could leave the slot (oven-sh/WebKit, aot/valueflow/AOTValueFlowRulesPromises.cpp:180 and AOTValueFlowPositions.cpp:495).

Fix

Background

  • WEBKIT_VERSION (scripts/build/deps/webkit.ts) names the oven-sh/WebKit release the build links. autobuild-preview-pr-<n>-<sha> is the build of a pull request there.
  • --aot compiles the program to machine code at build time. The value-flow analysis of the engine gives each object a class where it can. A class with closure instances stands for function objects a built-in makes that flow does not follow into: %BoundFunction% was the only one, and this adds %PromiseResolvingFunction%.
  • Considered and set aside in [JSC] AOT: the resolving functions of a promise are functions to flow WebKit#977: typing the functions as the compiled closures of the Promise constructor, a "surely a function" set beside the universe, a new object kind.

Downsides

  • None in Bun's code: the diff is the pin and the three programs. In the engine: one object more per compile in flow's universe, +150 bytes of table data, and a heat threshold effect on one 25-line probe ([JSC] AOT: the resolving functions of a promise are functions to flow WebKit#977, Downsides).
  • The unknown-callee entry that --explain prints at every new Promise(executor) stays. It is the executor call inside the compiled Promise constructor, on every engine, and not this change's subject.
  • The fix is in WebKit: with only src/ back on main, the programs still pass. The proof is the pin.
Notes

The report

@romulovalez, bun 1.4.4-canary.1+da867c60b, darwin arm64. class Call { resolve!: (v: number) => void; promise: Promise<number>; constructor() { this.promise = new Promise<number>((r) => { this.resolve = r; }); } } and settle(list, values) calling list[j].resolve(values[j]). --explain reports three missed-optimization entries (UnprovenCause::LeftByPasses), with types: 'Call'.

What was found on the way

  • The same report's smaller reduction (a number field that a method called from the constructor stores) has another cause: the birth undefined of a non-nullable field counts as observed once the constructor hands this to a method or a closure (AOTValueFlowUnobservedDefinitions.cpp, ConstructionWalk). Not fixed here.
  • Promise.withResolvers() and a custom thenable's then(res, rej) still hand objects nobody knows. Not fixed here.
  • A 25-line DataLoader probe that pushes { key, resolve, reject } literals into a queue runs 2,000 branches more with the change: load() moves from the hottest fifth of the code to warm because the executor now carries two AssumeType values, and the heat graph counts values, not code. With five more small functions in the program both engines compile it the same. Follow-up in the engine.

Measurements (engine 4d45755fed with and without the change, Bun 0903892 built against each with the same recipe)

  • test/aot/perf/compare-builds.ts, 12,612 rows, no servers: 0 slower, 0 faster.
  • test/aot/tools/same-output.ts: 283 of 289 programs compile to identical code function by function; the other 6 store the resolve of a promise in library code (node:stream operators) or moved with it.
  • Compile time of test/aot/servers/app/api-bun.ts: median 806 ms > 810 ms, within the noise.
  • Engine tests on the merge commit of refactor: create a high-level property iterator #972 with the change: testaot run tests 5803 passed, 0 failed; testaot native 3053 passed, 0 failed.

Self-review

11 concerns raised, 9 addressed (the base, the count bounds, negative tests, the super() and Reflect.construct paths, the measured delta in place of a count of reports, the DataLoader record form). Not done: the heat graph's size measure, a truthful --explain cause for stores of callables that built-ins make.


no test proof · iteration 0 · no src or test change; test-proof not applicable

…mise is read without a test

Bump WebKit to the preview build of oven-sh/WebKit#977: the engine of #972, where this branch pins it, plus that change. Flow knows the two resolving functions that `new Promise(executor)` hands its executor as functions, so a field typed as a function that stores one keeps its declared type from birth, and `bun build --compile --aot` reads it with one load.

Three programs under test/aot/parallel/field-writers/ pin the count of branches: the engine of main runs one test more for each read.
@robobun

robobun commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:27 PM PT - Nov 1st, 2026

❌ @robobun, your commit c0ecfc6 has 15 failures in Build #136682 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 47683

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

bun-47683 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🔴 test/aot/harness/flow-claims.ts — CI running test/aot/what-flow-says-of-an-entered-function-holds.test.ts fails after this bump for the "an executor" way, where the base passes. The bumped engine types the resolve and reject that new Promise(T) hands T as a new class with closure instances, but admits at test/aot/harness/flow-claims.ts:114 accepts only a class named Proxy, Function or BoundFunction for a value seen as a function. Fix: make admits accept the engine's new resolving-function class as well (extend the regex at flow-claims.ts:114, or accept any class token the engine marks as having closure instances), and run that test against the pinned testaot before landing. Condition: the class prints under a name not starting with those three words, as %PromiseResolvingFunction% does.

    Why this was flagged

    test/aot/ways-to-enter-a-function.ts:396 lists the way new Promise(T);. fileOf in test/aot/harness/flow-claims.ts:34-58 makes w_<name>(a, b) hand a and b to k_<name>, and also calls w_ with symbols (lines 47-50), so at run time a is seen as "symbol" and "function". test/aot/what-flow-says-of-an-entered-function-holds.test.ts:66-81 reads the flow type of k_'s parameters from testaot print and calls admits(type, value) for each seen kind. With the engine of oven-sh/WebKit#977 the executor's parameters carry the new class. admits at flow-claims.ts:108-115 returns true for kind "function" only when a part is "function", "object", f, builtin(...), or matches /^(Proxy|Function|BoundFunction)\b/; a part such as PromiseResolvingFunction matches none, so withoutRoom gets "an executor (strict): the first parameter was function, the analysis says ..." and expect(withoutRoom).toEqual([]) at line 81 fails. On the base engine the same parameters were "objects it knew nothing of", which admits as "object". The test is not skipped on CI: harness/printed-ir.ts:33-39 skips only when no testaot is found, and the prebuilt tarball ships one.

    Verification: test/aot/harness/flow-claims.ts:108-115: for kind "function" the only accepted parts are function, object, /^f\d+$/, builtin(, or /^(Proxy|Function|BoundFunction)\b/. PromiseResolvingFunction matches none, so for the "an executor" way the first parameter's type has no part that admits "function", and the test fails. On the base pin the engine typed these as objects it knew nothing of, which line 111 admits.

Comment thread scripts/build/deps/webkit.ts Resolved
Four stored IR dumps print the executor's parameters, a captured resolve and an array of resolving functions as what the engine of oven-sh/WebKit#977 proves: undefined|function, not an expected undefined|null|function or an object.
@robobun

robobun commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

On the two findings:

  • flow-claims.ts: the new class never prints under its own name. A class with closure instances becomes function before a type is written (AOTValueFlowPositions.cpp, the Site and ResultOfBuiltin arms), so the executor's parameters print as undefined|function, which admits accepts. what-flow-says-of-an-entered-function-holds.test.ts passes on a Bun of this branch built against the pinned engine (1 pass, 7 expect calls).
  • The pin: as the description says, it moves to a commit of oven-sh/WebKit main with the change before this lands. The base of this PR pins a preview of the same kind, and this PR is sequenced behind it.

The CI failures of the first run that are not on the base branch's own build (#136420) were four stored IR dumps under test/aot/engine-tests/ that print what the engine now proves. c0ecfc6 updates them. The other failures are the base branch's.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

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