Repository navigation
Conversation
…and constructor functions The flow analysis makes a class of each Object.create call site whose objects get one prototype object. Where that object is made at another such call site, the class of that call site is the expected base. The loop in noteClassesOfCreatedObjects refused only a call site whose base was itself. Two or more call sites whose classes stood above each other stayed classes, and the image held that ring. The run time walks the classes below a class without a bound when a prototype object changes (forEachDescendant in AOTMemberGuards.cpp), so the executable never returned from a store to such an object. A call site is now a class only where the row of bases above it ends in a class. One walk up the row from each call site decides it, and a call site whose base is itself falls under the same rule. A constructor function whose base the front end names could close the same kind of ring with a class that extends the function. The rule that drops a base which the program links now drops that base too. The verifier refuses a ring of expected bases that has a base which flow names. It refused only a ring of proven bases before. A ring of bases as read stays valid.
Collaborator
Author
|
Status
|
|
Preview build of d5d6e38: |
robobun
marked this pull request as ready for review
November 1, 2026 18:50
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 13 minutes. View limit details
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. |
robobun
added a commit
to oven-sh/bun
that referenced
this pull request
Nov 2, 2026
…ip ci] Twelve programs for the engine change of oven-sh/WebKit#980: Object.create call sites whose classes would be each other's base, and a constructor function below a class that extends it. As an executable each one never returns on the engine that main pins, so CI is skipped until a pin holds that change. Two more programs hold rings that stay after that change. They are marked Todo, and their builds end with InvalidLayout under --aot-validate once the pin holds the change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
bun build --compile --aotexecutable never returns from a store into an object of a ten-line scope chain (Notes). It spins inforEachDescendant(aot/object/AOTMemberGuards.cpp:512).noteClassesOfCreatedObjects(aot/valueflow/AOTValueFlowResultsClasses.cpp) refused only anObject.createcall site whose base is itself. Two call sites stayed classes, each the base of the other.--aotis canary only.Fix
main:testaot all8873 passed, 0 failed.Background
Object.create()call site makes a class. The first object it is handed becomes the prototype object.forEachDescendant: the image keeps the ring and every executable pays.Downsides
Notes
The programs
A scope chain with a factory for function scopes and one for block scopes.
bun scope.jsprints1 2 true false. The executable ofbun build --compile --aot scope.jsnever returns fromglobalScope.x = 1.The smallest one:
The one that the fuzz source wrote (
test/aot/fuzz/places-of-object-create-handed-values-in-every-order.tsof oven-sh/bun#47588). #969 names the hang under "Related" as left for another change.All three hang on Bun
4f7acde0fwith the engine at4d45755fed, also when built with--aot-validate, and on canary1.4.4-canary.1+da867c60b. No issue in either repository reports the hang. The fuzz source with seeds 1000 to 1479: 3 of 480 programs hang on the old engine in all four ways that it builds them, and none with the first version of this change.Where it spins
The first two programs, at the store:
The third, at the last
make():deletereaches the loop throughdidChangeMember,Object.setPrototypeOfandObject.freezethroughdidChangeEveryMember.The cause
giveLayoutsToCreatedObjectsgives eachObject.create(x)call site a layout withIsOfCreatedObject.noteClassesOfCreatedObjectstakes the layout of that object for the base. Its loop took a class back where its base was no class, or was the class itself. In the second program the base ofoneistwoand the base oftwoisone. Each was the reason for the other to stay.expectedBaseOf()answersclassFlow.prototypeBasefor such a layout, andencodeLayoutRecordswritesexpectedBaseand the lists of classes below from it.testaot print a.ir --until=ApplyFlowResultson4d45755fed:layout L6 .. classFlow={evaluations=AtMostOnce prototypeBase=L7},layout L7 .. classFlow={evaluations=AtMostOnce prototypeBase=L6}. With this change neither line has aclassFlow, and the tables sayexpectedBase=-for both.The new loop walks up the row of bases from each call site once and marks what it passes. A row that ends at a class without a base is a row of classes. A row that comes back to a mark of the same walk is a ring, and the walk takes back every call site that it passed. The two marks are two more values of the byte that each layout had.
What decides is the row of bases, not who is handed whose objects. In
places-of-object-create-handed-each-others-objects-are-classes-where-the-row-above-them-ends.irtwo call sites are handed each other's objects and all three call sites stay classes, before and after.What a call site on a ring makes now
What a call site makes whose base is itself, or that is handed several objects:
restoreCallsOfObjectCreateputs the call ofObject.createback, and the objects are ordinary objects. A call site below a ring gets the same (a-place-of-object-create-below-a-ring-of-places-is-no-class.ir).Constructor functions
Plain Bun prints
true. The front end namesXas the base ofF, and flow proves thatXextendsF.computeClassResultscopied the base as read intoprototypeBase, anddropLinkedBasesInCyclespassed over a layout that has one. The executable hung in the same loop. The rule now looks at every constructor function: the base that closes the ring is dropped, and the base that flow has proven stays.Tests
Engine
4d45755fed, release, x64 Linux. "Old" is that engine, "new" is the same with this change.pipeline/places-of-object-create-above-each-other-are-no-classes.irclassFlowon both layouts,printed nothing for 100 secondspipeline/places-of-object-create-in-a-ring-of-three-are-no-classes.irpipeline/a-place-of-object-create-below-a-ring-of-places-is-no-class.irpipeline/a-place-of-object-create-below-a-ring-of-places-is-no-class-where-its-row-is-walked-first.irpipeline/a-constructor-function-named-below-a-class-that-extends-it-has-no-base.irprototypeBase,printed nothing for 100 secondsvalueflow/results/base-named-for-a-constructor-function-that-closes-a-cycle-names-no-base-class.irprototypeBaseanswers/object-create-places-handed-each-others-objects.ir(the fuzz program, closed)verifier/invalid-layout-ring-of-classes-of-created-objects.irverifier/invalid-layout-ring-of-bases-that-flow-closes.irpipeline/places-of-object-create-in-a-row-that-ends-below-a-literal-are-classes.irpipeline/places-of-object-create-handed-each-others-objects-are-classes-where-the-row-above-them-ends.irThe ring tests hold
STATISTIC: CopyClosureFactoriesChanges = 0: a copy of a maker for each call would be a call site of its own, and there would be no ring to test.testaot allonaot/tests, new: 8700 passed, 0 failed, 42 todo.testaot runontest/aot/engine-testsof oven-sh/bun, new: 1852 passed, 0 failed, 1 todo.test/aot/run-parallel.tson all 7184 programs oftest/aot/parallelin oven-sh/bun, new: 7172 pass, 12 are skipped by their own lines, 0 fail. With--more-flags="--aot-check=verify-each-stage", so that the wider check of the verifier sees each of them, new: 7172 pass, 12 skipped, 0 fail.test/aot/parallelin oven-sh/bun (branchrobobun/9c88a8fe/aot-object-create-places-in-a-ring): 12 programs exit with 124 undertimeout 10on the old engine and print what plain Bun prints on the new one. They cover a new property,delete,Object.freezeandObject.setPrototypeOfon such an object, a ring of three, a ring that a loop closes, a ring that no run closes, a call site below a ring, the scope chain, three forms of the fuzz program, and the constructor function. Two more programs hold rings that stay, marked// Todo:. The branch becomes a pull request when a pin of Bun holds this change.Measurements
Old is Bun
4f7acde0fwith the engine at4d45755fed. New is the same with this change. Both are release builds from source, x64 Linux. Where a line says "first version", the build had the rule forObject.createcall sites and the verifier, and not yet the rule for constructor functions.test/aot/tools/same-output.ts: IDENTICAL on 865 of 865 programs (the 289 ofperf/idioms, thenparallel/objects,parallel/constructor-functions,parallel/function-prototypes,parallel/classesandservers/typescript-check.ts).ApplyFlowResultsare the same for 904 of 904 programs oftest/aot/parallelandtest/aot/perfthat nameObject.create(1,277 files).test/aot/perf/compare-builds.ts --servers none, every benchmark ofidioms/andeveryday/in three program sizes: 12,612 rows compared, 0 slower, 0 faster. 2 rows moved among the benchmarks that the tool lists as unsteady (stringify-a-long-text-with-nothing-to-escape-4.1%,weak-set-has-18.3%). The servers were not measured.BUN_JSC_logAOTPhaseTimes=1,--aot-threads=1, linepass AnalyzeFlow | classes, least of 5 runs taken in turns. A program of 1,294 layouts and 29 call sites: 21,629,538 to 21,625,425 instructions, and all 5 new readings lie below all 5 old ones. A small program: 260,704 to 260,813, where the old readings spread over 1,243. The old loop tested every layout once, and once more for each round that took a class back. The new one walks from the 29 call sites and allocates nothing. The visits were not counted one by one:gdbcannot read the debug information of this binary here (DWARF Error: Cannot find DIE), so the instruction count of the step stands for them.size: text 136,063,048 bytes and data 217,612 bytes in both.(I(n) - I(n/4)) / (3n/4)with n = 400,000, the same in 3 of 3 runs, first version: a read of an own field 7.0 to 29.0, a read of a name above 46.0 to 50.0, a store to an own field 5.0 to 37.0, two objects made 4,808.4 to 3,655.2. None of the 904 programs above is one.--aot-validate, the firstVerifyProgramline, a program of 1,294 layouts, least of 5, first version: 4,194,558 to 4,201,922 instructions, where the old readings spread over 318. A build without the checks prints noVerifyProgramline. A validating build ofservers/typescript-check.tson one thread takes 23 minutes here and was not measured five times.What stays
A class takes its base as read from the type of its
extendsexpression (find_base_class,src/aot/lower/layouts/classes.rsin oven-sh/bun). A cast can give that expression the type of a class that the value is not. Such a base can close a ring with a base that flow proves, or with another base as read. These three programs print under plain Bun and never return as an executable, before and after this change. Measured on Bun4f7acde0fwith the engine at4d45755fedplus this change, and on Bun at oven-sh/bun#47571 (79841cf54) with the engine at5869a3b791plus the first version.true null.--aot-validatenow ends the build withInvalidLayout: 'X' (L0) is not a valid layout, where it passed before.4 1 2 1 0 3 1.--aot-validatenow ends the build withInvalidLayout: 'B' (L2) is not a valid layout.4 1 2 0. No check sees this one: the verifier cannot tell which classes a program defines, so it leaves a ring of bases as read valid, asverifier/invalid-layout-cyclic-base.irsays.What would close the class:
constructor_function_parent(src/sema/check/aot/constructor_function_parents.rs) does that for constructor functions: a chain of parents that does not end names no parent.computeBaseResultslooks at proven bases alone when it looks for a ring. A ring of a proven base and a base as read passes it.checkLayoutRecords,place/AOTProgramTableCheck.cpp) would be the exact check that an image holds no ring, and would see the third program too.None of the three is part of this change. The first two programs are
// Todo:programs on the Bun branch. No issue tracks them: this account cannot file one.belongsTo()inselect/AOTPlannedSlotRules.cppwalks the expected bases without a bound at build time. This change takes two kinds of ring from it, and not those three.The walks of the run time stay as they are:
forEachDescendant,isDeclaredBetweenand the loop inleaveGuardsOfStoresForLater. The image must hold no ring.The branch
The parent is
mainat9f5001ee2c. The change was written and measured on4d45755fed, which Bunmainpins: no Bun front end matches the IR schema ofmaintoday, so no whole program can be built againstmainhere.noteClassesOfCreatedObjectsand the rule for constructor functions are the same text on both commits, and the commit applies tomainwithout a conflict. Every number above is from the build on4d45755fed. TheTestjobs of this pull request were the first run oftestaot allonmainwith the change: 8873 passed, 0 failed, 42 todo, and 33 left out that need entries of Bun (Test bun-webkit-linux-amd64-lto-aot). All sevenTestjobs pass, on x64 and arm64.An earlier
main(5869a3b791, with Bun at oven-sh/bun#47571) was built with the first version. Its tests passed, the programs printed what plain Bun prints, andtestaot allwas 8793 passed, 2 failed. The 2 failed the same way without the change in that build (property-lookup/absence-filter-names-what-stops-stores,valueflow/modules/text-file-answers-the-text-to-a-linked-require-and-its-namespace-to-the-loader.ir).Self-review
Of 18 concerns, 15 are addressed:
main, and the Bun half is tests alone.// Todo:programs on the Bun branch.CopyClosureFactoriesChanges = 0), a call site below a ring in both orders of the walk, and call sites that are handed each other's objects and stay classes.Two are declined. A test of a call site that is handed
nulland then a literal: this change does not touch that case, and #969 adds tests of it. A comment on the Bun pull request that moves the pin: the next pin after this merges holds the change.One is addressed in part: the review asked for a tracking issue for the rings that stay. This account cannot file issues, so the programs and the three ways to close the class are under "What stays".