Repository navigation
Conversation
|
Preview build of e16b7a5: |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Resolver class and object identity Source/JavaScriptCore/aot/builtins/promise.builtins.ts, Source/JavaScriptCore/aot/valueflow/AOTValueFlowBodies.h, Source/JavaScriptCore/aot/valueflow/AOTValueFlowBodiesLink.cpp |
The builtin declarations add PromiseResolvingFunction. ValueFlowUniverse reserves and identifies a rule-created object for Promise resolving functions. |
Promise resolver classification and construction Source/JavaScriptCore/aot/valueflow/AOTValueFlowResultsBuiltins.cpp, Source/JavaScriptCore/aot/valueflow/AOTValueFlowRulesPromises.cpp, Source/JavaScriptCore/aot/valueflow/AOTValueFlowBodiesLink.cpp, Source/JavaScriptCore/aot/valueflow/AOTValueFlowBuiltins.cpp, Source/JavaScriptCore/aot/valueflow/AOTValueFlowPositions.cpp |
Builtin-result analysis classifies Promise resolver functions. Promise construction uses a site object for the resolve function when available and the shared resolving-function object otherwise. Function-class handling now uses closure-instance metadata. |
Resolver field value-flow fixtures Source/JavaScriptCore/aot/tests/valueflow/protocols/the-resolving-functions-of-a-promise-*.ir |
New fixtures check resolver fields across conditional constructors, Promise subclasses, and construction without a site object. |
Possibly related PRs
- oven-sh/WebKit#906: Changes value-set queries and AOT field-type inference used by the builtin-result class assignment updated here.
Suggested reviewers: jarred-sumner
Merge Risk: 
Minimal · up to e16b7
-
No identified issue prevents merging after normal checks.
Pre-merge checks: 
3 · 
1
-
Failed checks: 1 warningCheck name Status Explanation Resolution Description check The author-provided description is substantive and technical. It states the problem, the fix, the verification, and the trade-offs. It explains why the change is needed, names the files and functions … Rewrite the description to match the repository template. Specifically: (1) add a WebKit Bugzilla bug link in the form https://bugs.webkit.org/show_bug.cgi?id=##### and a bug title line; (2) add a 'Reviewed by' line (set to NOBODY (OOPS!) u… Passed checks: 3 passedFull details: Description check
-
Explanation
The author-provided description is substantive and technical. It states the problem, the fix, the verification, and the trade-offs. It explains why the change is needed, names the files and functions involved, and reports test results. It does not follow the WebKit template. It has no Bugzilla bug link (the template requires one under 'File a Bug'). It has no 'Reviewed by' line, and it does not use the template's commit-message layout with a bug title and a changed-files list. The description references an oven-sh/bun issue and a Bun PR instead of a WebKit bug. The content is strong, but the structural elements the repository template requires are missing.
Resolution
Rewrite the description to match the repository template. Specifically: (1) add a WebKit Bugzilla bug link in the form https://bugs.webkit.org/show_bug.cgi?id=##### and a bug title line; (2) add a 'Reviewed by' line (set to NOBODY (OOPS!) until a reviewer is assigned, or name the reviewer); (3) add the explanation section in the template's style, which can keep the current Problem/Fix/Background content; (4) add the changed-files list in the template format (for example, '* Source/JavaScriptCore/aot/builtins/promise.builtins.ts:' followed by the affected class or function entries). Keep the measurements and downsides, since they are useful to reviewers.
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it changes what the value-flow analysis is willing to prove about executor parameters (a wrong proof here would be a miscompile rather than a missed optimization), a human familiar with aot/valueflow should still take a look.
What was reviewed:
- The new universe object at
m_firstClosure - 1:kindOfclassifies it asResultOfBuiltin; the only reverse mappings over that range (answersFunctionBoundToReceiverbounds check,firstHostObjectcomparison in AOTValueFlowResultsAccesses.cpp) exclude it or treat it conservatively, andnumberOfObjectsMadeWithClassesalready counts it. - Mixed-callee constructions (
new (flag ? Promise : Foo), Promise-or-another-built-in): the subset check incomputeClassesOfBuiltinResultsplus the executor's other argument flow keep the slot tested, pinned by the two negative.irtests. - The
ResultOfBuiltinarm ofaddUnlessInstanceOfClassnow mirrors theSitearm exactly; the%BoundFunction%special cases replaced byhasClosureInstanceshave no other class with that flag besides the new one, and the generator'scheckClosureInstancesrequiresextends Function, which the new class satisfies.
Extended reasoning...
The change adds a %PromiseResolvingFunction% built-in class, one rule-owned object in the value-flow universe, and makes applyConstructionOfPromise hand the executor objects of that class so function-typed fields storing resolve/reject keep their stability facts; five new .ir protocol tests cover the direct, super, no-site and two negative cases. It touches no security-sensitive surface (no auth, injection or data exposure), only the AOT compiler's type-proof machinery. The source delta is small (about 30 lines across six files) and the reasoning checked out, but the code is specialized compiler analysis where an over-eager proof becomes a miscompile, the commit comes from a bot author, and there is no independent human review on the timeline, which is why this is a defer rather than an approve.
A class field typed as a function that stores the resolve or reject function of `new Promise(executor)` was read behind a test. Flow typed the function as an object it knew nothing of, so the store could leave the slot, and every read of the field tested the object first. `bun build --compile --aot --explain` reported the reads as a missed optimization. The two functions are now instances of a class with closure instances, %PromiseResolvingFunction%, as what bind() makes is an instance of %BoundFunction%. computeClassesOfBuiltinResults notes the class on the object of the call that the rule hands the executor as resolve, and on one object that stands for reject. That object is new. The second object of the Promise built-in, which the rule used for reject, is also what the rows of the table make on behalf of Promise, so a class noted there was withdrawn. A construction without a site of its own (Reflect.construct, a construction by native code) uses the new object for resolve too. The ResultOfBuiltin arm of ValueFlowPositions types an object of such a class as a function. The two checks that named %BoundFunction% ask for closure instances instead.
9b16286 to
e16b7a5
Compare
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.
Problem
resolveorrejectfunction ofnew Promise(executor)is read behind a test:HasBirthFieldsFilled, thenGetSlot level=Expected, with a by-name arm.bun build --compile --aot --explainreports each read, the store and the declaration asmissed-optimization(AOT: missed-optimization for a function-typed class field holding a Promise's resolve bun#47599, the request-batching pattern).applyConstructionOfPromise(aot/valueflow/AOTValueFlowRulesPromises.cpp:180) hands the executor two objects that have no class, soValueFlowPositions::addUnlessInstanceOfClasstypes themanyObject|anyFunction. A store of that into afunctionslot may leave the slot, so the field never getsIsNeverStoredWithOtherTypeand never becomesAlwaysFilled.Fix
%PromiseResolvingFunction%withhasClosureInstances, beside%BoundFunction%(promise.builtins.ts).computeClassesOfBuiltinResultsnotes it on the second object of every construction whose callees are a subset of{Promise}, and on one new object of the universe,resolvingFunctionOfPromises(), which the rule uses forrejectand, where the construction has no site of its own, forresolve.resultOf(Promise, 1)in the rule: that object is also what the table's rows allocate on behalf ofPromise(anallocatedAswrite namingAsyncFunctionwas found there), so a class noted on it was withdrawn.ResultOfBuiltinarm ofaddUnlessInstanceOfClasstypes an object of such a class as a function, as theSitearm does. The two checks that named%BoundFunction%ask forhasClosureInstancesinstead..irtests inaot/tests/valueflow/protocols/(three fail on main, two negative cases pass on both).testaot run tests: 5803 passed, 0 failed.testaot native: 3053 passed, 0 failed (on the merge commit of AOT: crashes and miscompilations found since #971, Map/Set index slots, a bounds test on split() #972, where AOT: hangs, wrong answers, failed builds;bun test --aotfollows #44833's mocks bun#47605 pins the engine). Self-reviewed: 11 concerns raised, 9 addressed (Notes).Background
hasClosureInstancesstands for function objects that a built-in makes and that flow does not follow into:%BoundFunction%was the only one. Such an object becomesType::function(), never a class part, so no instance test is ever generated for it.resultOf(builtin, n)are the two objects a built-in answers with where a call has no site. The new object sits at the end of that range: one id per compile.PromiseConstructor.ts(wrong for a native construction, and it would re-dispatch everyr(x)call), a "surely a function" set beside the universe (more code, same result), and a new object kind (+28% universe ids per compile). The class row is the one mechanism the engine already has.Downsides
load()drops from the hottest fifth of the code to warm, because the executor now carries twoAssumeTypevalues and the heat graph counts values, not code. +2,000 branches of 14,011 there. With five more small functions in the program both engines compile it the same (16,457 branches). Follow-up: the heat graph's size should not count values that emit no code.AOTBuiltinTableBuiltins.cpp.o+120,AOTBuiltinTableNames.cpp.o+30), 0 host functions. One universe object more per compile.Promiseor another constructor keeps the test, as before (the negative tests pin it).Notes
Measurements (engine 4d45755 with and without this change, Bun 0903892715 built against each with the same recipe, x64 release without LTO)
--explainreads9 free, 0 tested, 0 by name, was6 free, 3 tested. The threemissed-optimizationentries are gone. Theunknown-calleeentry atnew Promisestays: it is theexecutor(...)call inside the compiled Promise constructor, the same on both engines, and out of scope here.HasBirthFieldsFilledinsettleand in the executor: 1 + 1 > 0 + 0 (testaot print --until=PlaceTypeTests).--aot-count-branches,BUN_JSC_aotCountsPath): the issue program 38 > 32, run-time calls 15 > 14. The three companion programs (16 objects, 10,000 settle turns, 160,000 reads): resolve 830,158 > 670,158, reject 500,253 > 340,253, the| undefinedspelling 830,158 > 670,158. One branch per read, as expected.EntryCheckswrapper and generic version (two functions) go, for every typed executor: its parameters are proven.compare-builds.tson the idioms and everyday benchmarks, three sizes, no servers: 12,612 rows compared, 0 slower, 0 faster; 6 rows moved among the benchmarks the tool lists as unsteady (3 up, 3 down, 2.7% to 6.5%), 27 moved once and not the second time.same-output.ts: all 289 programs hand over another IR (the class ids are renumbered); the machine code of 283 is identical function by function. Six differ: fournode:eventsprograms in one library function (createOnWarning, 316 > 354 bytes) and in functions that moved,stream-new-readableinnode:stream'soperators(onCatch98 > 68,afterItemProcessed208 > 111,maybeResume160 > 70 bytes: they store the resolve of anew Promise), andinstanceof-responsein bytes only.test/aot/servers/app/api-bun.ts, five interleaved runs each: median 806 ms > 810 ms (runs 725 to 858 ms against 734 to 910 ms, the first cold run of each left out). Wall clock, so within the noise; the change adds one object id to the universe and one class row to the table, no pass.perf,valgrindorbloaty: none is installed here. Sizes are fromsize -A, counts from the run time's own counters.The reject object
The plan was to note the class on
resultOf(Promise, 1), which the rule used forreject. On the issue program the class was withdrawn at once: the reconciliation loop at the end ofcomputeClassesOfBuiltinResultsfound anallocatedAswrite on that object namingAsyncFunction, fromuseByHoston behalf ofPromise(actWithoutOverloadmakesresultOf(m_builtin, 1)the contents object of whatever the host's rows allocate). So the rule now has an object of its own. It also stands forresolvewhere the construction has no site object:Reflect.construct(Promise, [executor]), a boundPromise, a construction by native code. Before, those tookresultOf(Promise, 1)forresolveand stayed tested.The DataLoader record form
new Promise((resolve, reject) => queue.push({ key, resolve, reject })), settled byqueue[j].resolve(v): flow proves the literal's fields on both engines (5 free, 0 tested). On the 25-line probe the PR engine runs 16,011 branches against 14,011 and the run time growsqueueeleven times (no planned capacity). The cause is not the field:load()isHoton main (heat percentile 82) andWarmhere (81), and with it the executor's predicted call loses the budget that would have copied it into the loop. The heat graph sizes a function by its number of values (AOTHeatGraph.cpp,blockSize = block.values.size() + 1); the executor has 16 values now, 14 before (twoAssumeType, which emit no code), which movesload()across the threshold on a program of about 350 values in all. The same program with five more small functions: 16,457 branches on both engines, 58 run-time calls against 76.Tests
bun test --aotfollows #44833's mocks bun#47605) pins: Bun's table generator of that train reads this engine's built-ins, and Bun main's does not read AOT: follow-up to #972: crashes, wrong answers, bun test --aot, build time #974's yet. Rebasing onto main is one line inAOTValueFlowBodiesLink.cpp.the-resolving-functions-of-a-promise-fill-the-function-typed-fields-that-store-them.ir: resolve and reject stored into two fields of a class. Fails on main....-constructed-by-super-fill-the-fields-that-store-them.ir:class P extends Promise { constructor(executor) { super(executor) } }. Fails on main....-constructed-without-a-site-fill-the-fields-that-store-them.ir:Reflect.construct(Promise, [executor]). Fails on main....-are-not-known-where-the-callee-may-be-another-built-in.ir:new (flag ? Promise : Map)(executor). Passes on both: pins the subset test....-are-not-known-where-a-constructor-of-the-program-may-call-the-executor.ir:new (flag ? Promise : Foo)(executor)whereFoocalls the executor with a number. Passes on both.test/aot/parallel/field-writers/withCOUNT: branchesbounds that the engine of main fails.Self-review
11 concerns, 9 addressed: the rebase onto main (one conflict in
AOTValueFlowBodiesLink.cpp, resolved by hand), theCOUNTbounds (the field read now dominates the count), the negative tests, the super() test, the siteless construction (Reflect.construct) and the comment that described it, theunknown-calleeentry named as out of scope, the measured delta in place of the count of reports, the DataLoader record form (measured and explained above). Not done: a fix of the heat graph's size measure (a change of its own), and a truthful--explaincause for stores of callables that built-ins make (named as follow-up in oven-sh/bun#47599).Follow-ups named, not done here
Promise.withResolvers(): itsresolve/rejectcome from a table row withb.callableand are still objects nobody knows.then(res, rej)parameters of a custom thenable.unknown-calleeentry at everynew Promise(executor).ConstructionWalkinAOTValueFlowUnobservedDefinitions.cppobserves the birthundefined).