Repository navigation
AOT: crashes and wrong answers found since #942, and faster builds of large functions - #967
Conversation
… large functions
Its tests that need Bun are in the Bun PR that goes with this one.
Crashes of compiled programs:
let b = null, c = null;
if (n) c = { k: 1.5 };
while (b !== b) b = [b];
if (y != b) { [b, y] = [y, b]; return c === y ? c : 1; }
ended by SIGSEGV, and ten lines of the same kind aborted the compiler. SinkAllocations analyses a function,
counts and then rewrites it, and all three must decide alike. A helper read constants out of the function as
it stood, so the rewriting saw constants that the analysis had not seen. #942 mended one of its five callers.
Now the question is answered from a table that is filled before anything is rewritten, and that mend is gone.
try { null.x } catch (e) { e.constructor.list = { length: 3 } } // TypeError.list was an array before
ended by a signal, gave `undefined`, or called the function that had been replaced. Flow took the stores that
it could follow for all the stores there are. A store through an object that nobody knows is now looked for on
every built-in class, and on the prototypes above crypto.subtle, Bun.s3 and a request's signal.
class Holder { held = stream[Symbol.for("nodejs.stream.readable")]; } // a ReadableStream
aborted, and in a function the read gave `undefined`. A symbol out of Symbol.for() was treated like one out of
Symbol(), which the host cannot reach.
Wrong answers:
- After `delete A.prototype.twice`, `new A(8).twice()` still answered. With a setter on B.prototype alone,
`const o = k === 0 ? new A(k) : new B(k); o.y = 5` in a loop ran B's setter for the first A. B3 moved the
load of a class's guard bits across the allocation that makes the class.
- `require("./config.json")` in a .cjs file that runs while the graph loads answered the ES namespace.
- `Object.assign({}, source)` stored names before symbols for a source with room for elements.
- Closures of one function that call each other in tail position: a stack frame had the name of the
function object that was called, not of the one that ran.
- With one optional pass off, `o.a` was folded where `o` may be null, and another pass aborted the build.
- Two runs of one executable made different caching decisions: a table was hashed by an address.
Memory: the slow path of a stub has a frame as large as what it saves. Objects were kept alive longer than in
plain Bun. A store of "" or of a string of one Latin-1 character needs no write barrier.
Build time. A generated function of 1,000 statements builds in 7,891 M instructions, from 12,473 M, with the
same output: liveness gave up only after a fixed number of visits and then started over, a dataflow kept a
counter for each condition in each block, and the guards' analysis asked each write about each proof, where
4,304 writes had 28 different answers.
Speed: `a[i] === b[j]`, case conversion sixteen characters at a step, a read by name of a slot that holds a
raw double, and 226 stepped benchmark rows through the stubs' new form.
Checks: the proof checks compare a function's identity and are no longer skipped in forwarding code.
`--aot-check=ClaimsOfFlow` tests every claim of flow where it is made. Under the verifying flags
SinkAllocations checks that its three steps decided alike.
There was a problem hiding this comment.
Beyond the inline findings, I also checked the for (;;) in copyLoopsForFunctionsTheyCall (AOTLoopPasses.cpp) for non-termination — after a copy, the original loop's listed callees all fail findEarlierTest and each copy has a single proven callee, so the search finds nothing more — and the new RELEASE_ASSERT in ProgramIdentifiers::uidSlowCase (AOTProgramIdentifiers.cpp:117), which only guards the VM-shared object of a multi-instance image while realm-created symbols come from the per-realm object, so it is not reached in the multi-realm path.
Extended reasoning...
This is a 538-file, ~18.7k-line change across the JSC AOT compiler, runtime, stubs and tests plus small edits in yarr, WTF and bmalloc, with no auth, crypto or injection surface. Four inline findings were posted; the loop-copying pass termination and the new identifiers assertion were also examined and ruled out as described in the body.
Findings marked 🟡 are optional suggestions and need no follow-up push.
| for (StringID pattern : patterns.strings) | ||
| addCandidates(inputs.program, inputs.context, RegExpRequestOrigin::Candidate, pattern, flagSets, site, found); |
There was a problem hiding this comment.
🟡 (optional) Hard stop removed: nothing now bounds how many predicted RegExp texts one call site compiles into the image, so a build whose pattern texts come from a large list gets an image and compile time that grow with that list, where the base stopped at 64 texts and 64 KiB of code per site. The three limits RegExpCandidatesPerSite, RegExpCandidateCodeBytesPerSite and RegExpCandidateTextLength are deleted from limits.def and every text flow or the constant walk finds is added at AOTRegExpRequests.cpp:3756 and compiled at AOTRegExpCompiler.cpp:541, with only the 2 GiB RegExpSectionSize left as a backstop. Fix: restore a per-site (or per-program) cap on predicted texts and on their code bytes, applied both to flow candidates and to texts from the constant walk, and decline the rest as before.
Why this was flagged
A program whose RegExp text is predicted from many values reaches collectCandidates and addCandidatesFromConstants in AOTRegExpRequests.cpp: a wrapper function re(s) { return new RegExp(s) } called from thousands of sites with distinct literals (AOTValueFlowResultsSinks.cpp:171 collects every literal), or list.map(s => new RegExp(s)) over a data array, whose elements are produced by elementOf at AOTRegExpRequests.cpp:1883-1886 without charging the walk budget. On the base branch takesTextsOfFlow refused a site with more than RegExpCandidatesPerSite (64) texts, add() refused the 65th alternative, and applyCandidateBudget dropped code past RegExpCandidateCodeBytesPerSite (64 KiB) per site. After the change add() at AOTRegExpRequests.cpp:538-542 always accepts, every pattern is appended at AOTRegExpRequests.cpp:3756 and :3877, and compileRegExps at AOTRegExpCompiler.cpp:532-541 compiles each one with applyCandidateBudget removed; the only remaining stop is applySectionSizeLimit at 2 GiB. No code names a bound on the number of predicted texts or their bytes per site or per program.
Verification: limits.def deletes RegExpCandidateCodeBytesPerSite (65536), RegExpCandidatesPerSite (64) and RegExpCandidateTextLength. AOTRegExpCompiler.cpp removes applyCandidateBudget entirely, and compileRegExps now goes straight to applySectionSizeLimit, whose only bound is 2 GiB (AOTRegExpCompiler.cpp:456). collectCandidates now adds every flow string unconditionally at AOTRegExpRequests.cpp:3755-3756.
| // The walk for a caller of the function that holds the call is a walk in its own right: there are as many as the program has such calls, and each is as small as what that caller | ||
| // passes. The chains above one caller share its budget: there can be as many of them as there are ways through the program. | ||
| if (chain.size() == 1) | ||
| m_valuesFollowed = 0; |
There was a problem hiding this comment.
🟡 (optional) Builds of programs with a RegExp helper that has many direct callers now spend up to callers x 16384 value walks on one site, where the base spent one budget per site. AOTRegExpRequests.cpp:385-386 resets m_valuesFollowed to 0 for every direct caller at chain depth 1, and the base's m_worlds counter (bounded by maxAlternatives) that capped the number of worlds explored per site was removed. Fix: restore a per-site total on walks (or a cap on the number of callers walked) so build time for one call site stays bounded regardless of caller count.
Why this was flagged
A helper like function re(s, f) { return new RegExp(s, f) } called from K sites, each passing its own constant, enters exploreForEachCaller (AOTRegExpRequests.cpp:375). For each DirectCall at :381 the budget is reset at :385-386 when chain.size() == 1, so the m_valuesFollowed >= m_budget cut at :387 never fires across callers; each caller may consume the full maxValuesFollowed (1 << 14, :254) and recursive chains above it. The base (saved copy at :161, :315) reset the budget once in patternsOf and additionally stopped after maxAlternatives() worlds via m_worlds; both totals are gone (m_worlds removed in the diff). This entry was dismissed as covered by the candidate at AOTRegExpCompiler.cpp:541, but that candidate was REFUTED, so no filed finding addresses the per-caller walk cost; the CONFIRMED finding at AOTRegExpRequests.cpp:3756 is about how many predicted texts get compiled, a different mechanism and fix. Every build of a program with a widely used RegExp wrapper pays this.
Verification: Triggering condition: a new RegExp(...) site in a helper with many direct callers. At AOTRegExpRequests.cpp:385-386 if (chain.size() == 1) m_valuesFollowed = 0; runs before the m_valuesFollowed >= m_budget check at :387, so that check can never cut the loop. The base reset m_valuesFollowed = 0 once per site, so per-site work goes from ~16384 walks to (number of direct callers) x 16384.
|
Preview build of 43ccc98: |
…crash
Crashes
- `Bun.gzipSync.list = [1, 2]; Bun[JSON.parse('"gzipSync"')].list = { length: 3 }`
and then `Bun.gzipSync.list.length` segfaulted. So did the same through
`Event.prototype[name]`, names out of `Object.keys(Bun)`, and
`Bun.Glob.prototype` reached by three computed names.
`Object.setPrototypeOf(Object.getPrototypeOf(Bun.s3), { .. })` was not seen.
A read under a computed key, or of `prototype`, off an unknown object in the
program's own code now reaches the host prototype objects. This replaces the
wider rule of the previous commit, which also counted such reads in library
code and cost `await request.json()` its typed parse.
- The compiler died with "Floating point error" on
`while (a == 65536 && i0 < 9) { i0++; c = c > 0 ? d : c; }`.
B3VectorizeLoops divided by the number of memory accesses of a loop that has
none.
- A loop counter beyond int32, read from a property and used to index a typed
array, trapped.
- `RegExp::matchWithImageCode` dereferenced a null image record when jsc runs
its tests with regular expressions compiled ahead of time (six JSTests).
Wrong answers
- `/^(?=(.*))\1$/` said "no match" for a subject with characters outside
Latin-1. The pattern was taken to consume 8-bit characters only: what a
lookahead captures counts once a back reference can consume it.
- `new RegExp("\\B" + text, "u")`: `\B` matched inside a surrogate pair.
- `function make(k) { return k === 0 ? new A(0) : new B(1) }` with a getter on
`B.prototype` alone: `make(0).size` ran `B`'s getter when the first `A` was
made behind a test of both classes.
- `x.f += n` where `f` starts on the prototype: the arm that reads `f` by name
may run a getter, and came back into code that relied on a guard from before
the read.
- `Interface.prototype[Symbol.asyncIterator]` read in a function was a
different function object than `Reflect.get()` returned (node:readline).
- `await import("sass")` in an executable threw "Cannot assign to
Function.prototype.call$0". Programs out of dart2js add such names.
- `function twice(f, n) { return f(f(n)) } twice({}, 1)` quoted `f` instead of
`f(n)` in the error message.
- `import * as ns`; `Reflect.setPrototypeOf(ns, ..)`, `Object.freeze(ns)`,
`"added" in ns`: a guard for compiled objects read a module namespace.
Windows
- `hasChanged |= std::exchange(member, copyOfClass(member)) != member` compares
the old value with itself where the right operand is evaluated first, as on
both Windows targets. Copied classes and closure factories kept the types of
the original there: correct, but less precise.
Memory
- `for (let i = 0; i < n; i++) a.push(f(i))` reserved the whole array up front
even where `f` may throw and end the loop.
Faster
- `let current = first; while (current) current = current.run(this)` with
`first` typed by an interface: objects that have the interface's members but
none of its layouts stay in the typed loop. Effect's run loop: -20%
instructions per turn.
Smaller
- The compiler's rarely run files are built for size: -600 KB.
Tools
- The map and `--aot-report` say "proven" or "missed optimization" where the
final IR knows the receiver's class, instead of "unknown".
- False alarms under `--aot-validate` and `--aot-verify-each-pass`: a field
that holds its own object, what an `await` holds, `Object.keys()` of an
object with a computed key.
The previous commit added a bool at the end of FlowSiteLevel. With the Microsoft ABI the two bit-fields at its start take a unit of their own, so the struct was already 24 bytes there and grew to 28, and the static_assert failed every Windows build. The bool now stands behind the two uint8_t members, in what was padding on every target.
…by a test's own edge `bun test --aot` on NestJS's suite stopped with "Stopped while compiling printMessages .. in PlaceTypeTests". A block's parameters are its explicit arguments followed by the results of the edges that enter it, such as the narrowed value of a type test. Two places indexed an edge's arguments with a parameter's index, which is out of bounds for an edge result. Both now ask incomingValueOf(). An edge's own result counts as proven when a rejoin is planned, and as unknown when the first values of loop variables are looked for. The second commit of this pull request brought this.
…ith a long chain of classes Two fixes for two red tests of oven-sh/bun#47571. The first error at a place costs the same in a longer file Forty places inside one `switch` of 12,000 cases each throw their first TypeError ("undefined is not an object (evaluating 'a.b')"). To quote the source in such a message the run time decodes the entries that say what was printed in place of the original text. It decoded every entry of the kept piece of source at each first error, and a long statement or function is one piece. The entries of a piece are now decoded once and kept, however many places in it throw. A compiled test run decoded every entry of the module each time: now once. The long file decodes 2,162 entries where it decoded about 357,000. Forty places in one statement: 2.3 > 1.5 ms. In a test run: 4.05 > 0.59 ms. With AddressSanitizer it was 130 to 141 ms against 2 to 3 ms in the short file, which is what failed the-first-error-at-a-place-costs-the-same-in-a-longer-file on the x64 ASAN lane. No message changes. The counts file has a new line, `decoded:TextPrintedInPlace`. A build keeps the partition of a call's receivers as words `function ask_i(x) { return x.weigh() + ask_(i-1)(x) }`, 800 in a chain, each given an instance of one class more. No two calls of `weigh` see the same classes, so the cache kept one partition per call and key, each with an arm per class: 320,000 arms of 112 bytes. The cache now keeps a partition packed: 24 bytes for an arm with one method and one layout. And "may this call enter a table look-up" built and kept a whole partition to answer one bool: it now asks class by class, stops at the first no and keeps nothing. 800 classes: 189 > 166 MB, 20.14 > 20.01 G instructions. The generated code is identical on 1.16 million functions. build-scaling-4 failed on macOS arm64 with "twice as many classes cost 2.58 times the peakBytes; allowed: 2.5". On Linux that ratio goes from 2.29 to 1.85. Not measured on macOS.
| for (const TextThatWaits& text : waiting) { | ||
| if (comesToEarlierCall && text.waitsApart) | ||
| levelsAddedToPlace = std::max<uint32_t>(levelsAddedToPlace, (text.place >> 33) - state.turnOfTexts); |
There was a problem hiding this comment.
🟡 nit (optional): Programs whose dynamic imports wait for text on another thread can see an import() settle ahead of an earlier call it shares modules with, which the code says must not happen. AOTModules.cpp:2399 computes (text.place >> 33) - state.turnOfTexts in unsigned arithmetic; once an earlier waiting-apart call's turn has already passed (several calls were queued in one turn and are delivered one per turn), the difference wraps to about 2^32 and std::max<uint32_t> picks it over a genuinely later call's level count. Fix: only add a level count when (text.place >> 33) > state.turnOfTexts (saturate at 0), so the max reflects the latest earlier call rather than a wrapped value.
Why this was flagged
Trigger: under bun run, several import() calls are held in one turn with waitsApart set at AOTModules.cpp:2380; deliverTextThatWaits at AOTModules.cpp:2479-2495 delivers one per turn and increments turnOfTexts each time, so a call X held at turn 10 (place >> 33 == 11) can still be waiting at turn 13. A new import() W that comesToEarlierCall and also shares modules with a call Y held at turn 13 with levelsAddedToPlace 2 (place >> 33 == 15) then runs the loop at AOTModules.cpp:2397-2400. For X the subtraction (11) - 13 is uint64 and wraps; converted to uint32 by std::max<uint32_t> it becomes 0xFFFFFFFE, which beats Y's 2, so at AOTModules.cpp:2401 place += 0xFFFFFFFE << 33 wraps mod 2^64 and W lands at turn level 11 instead of 15. W is therefore delivered before Y, although the comment at AOTModules.cpp:2396 states 'What it shares with them comes no sooner for it than for them'. The base branch had no waits-apart ordering, so this is a new, intended ordering guarantee that the wrap defeats; putCallsThatWaitApartInLine at AOTModules.cpp:2410-2421 subtracts the same wrapped count so no crash results, only a wrong settlement order.
Verification: Trigger: under a host with callInLaterTurn, two or more import() calls are held apart in one turn. state.turnOfTexts is uint32_t (Source/JavaScriptCore/aot/runtime/AOTVMState.h:238) and text.place is uint64_t, so (text.place >> 33) - state.turnOfTexts at Source/JavaScriptCore/aot/runtime/AOTModules.cpp:2399 wraps when the waiting text's turn is behind the current one; std::max<uint32_t> narrows it to ~2^32-k, which beats any genuine small offset from a later call.
Pins oven-sh/WebKit#967 at 43ccc98c356a5d1ff47392dde1ced58b51c5d20c. The build - `var holder = { other: 1 }; const kept = holder.link; holder.link = kept; holder.link(Child, Base)`: `bun build --compile --aot` never ended. So with `const original = A.make; .. A.make = original`. The walk from a name to what it stands for forgot the names it had visited when it went on to a stored member. It now remembers each store it has followed. The previous commit brought this. - The exported variables of a build's module records are looked up on the calling thread again. The previous commit put that on threads, which gained nothing on a large application. - `bun_aot` is built at opt-level "s": Bun is 475 KB smaller. `bun test --aot` - Product code finds only what an executable would find. With `exports.load = name => require(dir + name)` in `src/` or in a package, a test that has it load a file the build didn't see now fails as the executable would. Files of the tests load what they like, native addons load wherever the call stands, and `--aot-allow-code-from-disk` turns it off. - `delete require.cache[require.resolve("../src/state.js")]` and then a written `import("./state.js")` in product code runs the module again. - `--shard=k/n` deals the files by what they reach, so that a shard builds less. Every shard computes the same deal on any machine. Plain `bun test --shard` is unchanged. - The parts of a run are built slowest first. - `silence(console, levels)` and `spyAll(console, wanted)`, where a helper in another file stores or spies under a computed name, no longer fail with "Cannot assign to console.debug". - `--files-from=/dev/stdin` failed every file in a layered run. - One file that let go of its `module` or `require` made the run keep every export of every file. In express that was 838 functions and 78 files. - `--aot-memory-limit` says what it bounds: the build, not the tests that run. Module loading - Followed at build time: `require(__filename.replace("main.cjs", "first.cjs"))`, `replaceAll`, `__filename.slice(0, -8) + "third.cjs"`, `import(import.meta.url.replace(/module\.mjs$/, "ninth.mjs"))`. - `new Worker(file)` in a file loaded from disk by an executable built with `--aot-allow-code-from-disk` died on `globalThis.console = ..`. Wrong answers - `String(f)` in a `.ts` file: `() => <Foo>{}` read `() => {}` and `return<number>a` read `returna`. From the review of the previous commit - A `spyOn()` inside a template string with `${..}` for a name was missed: "Cannot spy on 'total'". - The memory of a worker's children was counted in pages of 4 KB. - A failed write into the program below was ignored. Three of our own tests opened that file read-only, so their writes had been failing silently. - A preload that a test file imports back was scanned twice. - A test that can't make its folder says skipped. Tests - The stage-log test used `O_DIRECT` by number, which is `O_DIRECTORY` on arm64. It listens on a datagram socket now. - A helper asked `.todo` of `test.skipIf(true)`, which failed eight files on the ASAN lane. - A stack-frames program cut its path at the first forward slash, which failed on Windows. - Five `gc/` programs are marked todo on Windows x64. - The thread pool's unit tests ran dozens of builds on a pool sized for one. - The test of how a run's parts are cut asked for the order in which three builds end, which is a race. It asks for the cut. - 113 new files, most of them stored programs compared with plain Bun.
Pins WebKit at 5869a3b791e68fd8e0e8059ff21e278760bcf4c6 (oven-sh/WebKit#967, merged) in place of the preview build. Five files conflicted with #44833 and #47070. In all five, main's lines stand as written. Fixes for what this branch brought - `bun test --aot --shard=k/n`: each shard dealt the test files by, among other things, its own `--parallel`. Two runners started with different `--parallel` computed different deals. With `--parallel=1` on one and `--parallel=16` on the other, 21 of 40 test files ran on neither, and both runs reported success. Found in review of this PR. Every shard now computes the deal from what all of them share: the paths, the sizes, what the files reach, and the timings file. How many files run at once is read from the timings file (`"workers"`, written by a compiled `--parallel` run under `--update-timings`). `--parallel` has no say. Each shard prints one dim line with a hash of the whole deal, so a mismatch can be seen. - 10,000 functions that call one another in a ring, each handing on what it got: `bun build --compile --aot` overflowed the stack under AddressSanitizer, and `bun test --aot` did so in release builds. The walk that finds which values a name may stand for recurses once per function of the ring. Where the stack runs low it now goes on in a thread with a fresh stack. 100,000 functions build. Build time - The checker walked, for every reference to a symbol, all call statements that mention it. `all.push({..})` 20,000 times in one function takes 2.6% fewer instructions to build. No answer of the checker changes. `bun check` shares the file. - `bun_aot` is built at its own optimization level again, not at "s". The smaller Bun cost up to 0.85% more instructions a build. - The front end keeps a tree that it will be asked for again instead of parsing the file anew. The output is identical. The first two turn `build-scaling-6` green on Alpine x64. Following #44833 (`bun test`: Vitest compatibility) - An import with a query follows main's rule: `raw` among the `&` parameters is the file's text, `url` is the file loader. - Which file is the entry point of a test run is asked of the runner, not compared with the main module's path. - Tests that state what plain `bun test` does now state what it does on main: a name that a mock leaves out is `undefined`, not a SyntaxError; a factory that failed throws again at each load; `spyOn(namespace, "absent")` throws a TypeError. - `bun test --aot` doesn't match plain `bun test` on module mocks yet. Each such case says so behind a `TODO(bun)` and stays a comparison with plain, so it goes red the day the two agree: a name that only a mock has, a name that a mock leaves out, the order of two link errors where a file may be mocked, the default of a mock that has none, a load above or behind a mock whose factory failed. - `compile-trim-capabilities`: its reader of machine code took every `e8` and four bytes for a call, also in the middle of another instruction (`80 bb e8 96 00 00 01` is `cmpb $1, 0x96e8(%rbx)`). It now asks that an instruction begins there. - The test that compares the functions of a test run with those of the executable leaves out the map's "outlined" rows: `bun:test` now brings its fake timers into every test run. - Two tests of the engine's own suites are listed as failing: they count the entries of a table that has three more since #44833. Their fix is in the engine and comes with its next commit. Tests - `a-line-of-the-stage-log-goes-out-in-one-write` is a todo where the running glibc is 2.37 or newer. The engine's long log lines still go out in pieces there. The fix is in the engine and comes with its next commit.
Follow-up to #942 for
bun build --compile --aotandbun test --aot. The Bun half, with the tests that need Bun, is a PR in oven-sh/bun that pins this one's preview build.Crashes of compiled programs
Eleven lines ended by SIGSEGV, and ten lines of the same kind aborted the compiler:
SinkAllocationsanalyses a function, counts, then rewrites it, and all three steps must decide alike. A helper read constants out of the function as it stood, so the rewriting saw constants that the analysis hadn't. AOT: per-global program state, mockable exports, heap-allocated generator frames, bug fixes #942 fixed one of its five callers. Now the question is answered from a table filled before anything is rewritten, and that fix is gone again.try { null.x } catch (e) { e.constructor.list = { length: 3 } }, whereTypeError.listwas an array before: a signal,undefined, or a call of the function that had been replaced. Flow analysis took the stores it could follow for all the stores there are. A store through an object nobody knows is now looked for on every built-in class.Bun.gzipSync.list = [1, 2]; Bun[JSON.parse('"gzipSync"')].list = { length: 3 }, thenBun.gzipSync.list.length: SIGSEGV. The same throughEvent.prototype[name], names out ofObject.keys(Bun), andBun.Glob.prototypereached by three computed names. A read under a computed key, or ofprototype, off an unknown object in the program's own code now reaches the host's prototype objects.A loop counter beyond int32, read from a property and used to index a typed array, trapped.
class Holder { held = stream[Symbol.for("nodejs.stream.readable")]; }with aReadableStreamaborted. In a function the read gaveundefined. A symbol fromSymbol.for()was treated like one fromSymbol(), which the host can't reach.Crashes of the compiler
while (a == 65536 && i0 < 9) { i0++; c = c > 0 ? d : c; }.B3VectorizeLoopsdivided by the number of memory accesses of a loop that has none.bun test --aoton NestJS's own suite stopped with "Stopped while compiling printMessages .. in PlaceTypeTests". So did tslog's (in webpack's_getRenamedInlineModule) and a project that uses knex. A block's parameters are its explicit arguments followed by the results of the edges that enter it, such as the narrowed value of a type test. Two places indexed an edge's arguments with a parameter's index, which is out of bounds for an edge's own result. An earlier commit of this PR brought it.Wrong answers
/^(?=(.*))\1$/said "no match" for a subject with characters outside Latin-1. The pattern was taken to consume 8-bit characters only. What a lookahead captures counts once a back reference can consume it.new RegExp("\\B" + text, "u"):\Bmatched inside a surrogate pair.function make(k) { return k === 0 ? new A(0) : new B(1) }with a getter onB.prototypealone:make(0).sizeranB's getter when the firstAwas made behind a test of both classes.x.f += nwherefstarts on the prototype: the arm that readsfby name may run a getter, and it came back into code that relied on a guard from before the read.Interface.prototype[Symbol.asyncIterator]read in a function was a different function object thanReflect.get()returned (node:readline).await import("sass")in an executable threw "Cannot assign to Function.prototype.call$0". Programs out of dart2js add such names.function twice(f, n) { return f(f(n)) } twice({}, 1): the error message quotedf, notf(n).delete A.prototype.twice,new A(8).twice()still answered.B.prototypealone,const o = k === 0 ? new A(k) : new B(k); o.y = 5in a loop ranB's setter for the firstA. B3 moved the load of a class's guard bits across the allocation that makes the class.require("./config.json")in a.cjsfile that runs while the graph loads answered the ES namespace.Object.assign({}, source)stored names before symbols for a source with room for elements.o.awas folded whereomay benull, and another pass aborted the build.Memory
""or of a one-character Latin-1 string needs no write barrier.for (let i = 0; i < n; i++) a.push(f(i))reserved the whole array up front even wherefmay throw and end the loop.function ask_i(x) { return x.weigh() + ask_(i-1)(x) }, 800 in a chain, each given an instance of one class more. No two calls ofweighsee the same classes, so the cache kept a partition of the receivers per call and key: 320,000 arms of 112 bytes. A partition is now kept packed, 24 bytes for an arm with one method and one layout, and "may this call enter a table look-up" asks class by class and keeps nothing. 189 > 166 MB, with identical output on 1.16 million functions.Build time
A generated function of 1,000 statements builds in 7,891 M instructions, down from 12,473 M, with identical output:
test/aot/build-scalingin Bun gets its first shapes with a warm body.Speed
a[i] === b[j].let current = first; while (current) current = current.run(this)withfirsttyped by an interface: objects that have the interface's members but none of its layouts stay in the typed loop. Effect's run loop takes 20% fewer instructions per turn.TypeErrorat a place quotes the source ("undefined is not an object (evaluating 'a.b')"). For that the run time decoded every entry of the kept piece of source at each first error, and a long statement is one piece. Now once per piece. Forty places in oneswitchof 12,000 cases: 2.3 > 1.5 ms, in a compiled test run 4.05 > 0.59 ms. No message changes.Windows
hasChanged |= std::exchange(member, copyOfClass(member)) != membercompares the old value with itself where the right operand is evaluated first, as on both Windows targets. Copied classes and closure factories kept the original's types there: correct, but less precise.Size
Checks
--aot-check=ClaimsOfFlowtests every claim of flow analysis where it's made.SinkAllocationschecks that its three steps decided alike.RegExp::matchWithImageCodedereferenced a null image record when jsc runs its tests with regular expressions compiled ahead of time. That was six failing JSTests on this PR's first commit.--aot-reportsay "proven" or "missed optimization" where the final IR knows the receiver's class, not "unknown".Changes outside
aot/22 files:
b3/andb3/air/(branches to selects, loop rotation, the loop vectorizer, the greedy register allocator, block order,AirReleaseDeadValues, threetestb3files),runtime/(ArrayPrototype,JSModuleLoader,JSObject,RegExp),yarr/(YarrJIT,YarrFlatProgram) andwtf/FrequencyEstimator.h.