Repository navigation
Train perf 5: the variadic check skips its walk for typed programs, CI's self-compile check fails when a listed compiler class becomes a dynamic value, and the LLVM emitter leaves the dynamic tier of the compiler's own build (4.7% fewer functions) - #3406
Conversation
…unction The check that runs when lowering ends (variadic-functions.ts, #3379) walks every expression of the finished program. Each of its refusals needs one of three things: an expression of a variadic function type, a store lowering noted, or an adapter whose call is a store. Most typed programs have none of them. Devirtualization is the last step before the check on the build path, and its rewrite passes every expression of the bodies it leaves through one callback. It now says whether one of them has a variadic function type, and the check returns without a walk when none has, lowering noted no store and no storing adapter exists. The longer calls lowering notes are read only for a store, so they do not keep the walk. The other emit path (the historical-order relower, coverage) walks as before. SCRIPTC_CHECK_VARIADIC_SKIP=1 walks every program the check skips and fails the build when the walk finds something. The test suites run with it, so a refusal added to the walk without its condition fails there. No program's output changes. Co-Authored-By: Claude Code <noreply@anthropic.com>
The compiler's Lowerer became a dynamic value in the compiler's own build three times in five days (#2878, #3192, #2567): each time one helper took an interface the Lowerer satisfies, each cost about 6% of the functions of that build, and each landed green. The output-growth rule sees growth, not state, so it also misses a second such place that lands while a first one is in. The budget file now lists the classes that must stay typed (neverDynamicClasses: the Lowerer). SCRIPTC_DYNAMIC_CLASSES makes a build report, per lowering pass, every place of the finished program that makes an instance of a named class a dynamic value: a conversion of the class or of a type that holds it, a dynamic function checked into a type that takes it, a checked cast into it, and the class dispatch pass's own verdict when no place explains it. scripts/self-compile-budget.mjs sets the variable for --check and --native-build (the build CI's native compiler job runs), fails on a reported place and prints it with its source line, and fails when the build printed no report at all. The repository's gate asks its traced self-compile for the same report. Co-Authored-By: Claude Code <noreply@anthropic.com>
…uild The compiler compiles its own source, and in that build the LlEmitter was a dynamic value: the keyed tables, checked casts and converters of untyped code were compiled for it and for every class its fields reach. Three places made it one, which the build's own report lists (SCRIPTC_DYNAMIC_CLASSES=LlEmitter): - emit-function.ts handed the emitter to emitIntegerClone and emitIntegerDispatcher, whose host parameter is the IntegerCloneHost interface, kept in a field of the clone emitter. They now get emitter.integerCloneHost(), a record of the seven members the clone emitter reads, as the Lowerer's callers got bindingScope() in #3394. - wrapping-sums.ts called the function-valued member int32Elements as host.int32Elements(...), a member call, which hands the function the emitter as its receiver. The function is read into a local first. LlEmitter joins the Lowerer in the budget's neverDynamicClasses, so the self-compile check fails on the next such place and names it. No program's output changes: the same methods run, through a record or a local instead of through the emitter. Co-Authored-By: Claude Code <noreply@anthropic.com>
One conflict, in Lowerer.run: main adds the function views pass's state on the line after the class dispatch pass's, which the second commit of this train moves out of the block. The dispatch pass stays above the block and the function views stay in it. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The LLVM emitter leaves the dynamic tier of the compiler's own build. No conflict. Co-Authored-By: Claude Code <noreply@anthropic.com>
CopyScan.layoutPairs (width-copies.ts) called its function-valued member as this.unionArms(...). A member call hands the function the scan as its receiver, which makes the class a dynamic value when the compiler compiles its own source, as wrapping-sums.ts did for the LLVM emitter. The function is read into a local first. CopyScan joins the budget's neverDynamicClasses. The list checks the classes it names and no others: this class was not caught because nobody had named it. Co-Authored-By: Claude Code <noreply@anthropic.com>
One conflict, in Lowerer.finishModule: main settles the worker module's shareEnv entry as the function's first step, and this train adds two parameters to it. Both kept. That step runs between devirtualization and the variadic check and stores a boxed symbol, no function value; the comments that said nothing runs between the two now name it. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Results that were still running at the second push (head
CI on this head: the red jobs are the ones that build runtime packs and native artifacts ( |
This is a landing train with one member, validated as a whole in Vercel Sandboxes: four commits about the compiler compiling its own source. One removes a whole-program walk that typed programs never needed. One makes CI's own self-compile job fail when a compiler class that must stay typed becomes a dynamic value, and say where. Two use that report: the LLVM emitter and one smaller class leave the dynamic tier of the compiler's own build, which then has 4.7 % fewer functions. The member's signed commits are merged with main, not rebased.
The three numbers
6da2be6a1b(139,652 functions, 1,025,578,714 bytes, 14,238). On the merged tree the last commit takes 145 more functions out (135,862 to 135,717).What lands
The variadic check skips its walk when there is nothing to refuse. The check added with Train perf 2: anchored and non-ASCII regular expressions without a scan, class tables selected by the class, two emitter passes in one walk, variadic functions through fixed slots #3379 (
variadic-functions.ts) walks every expression of the finished program. Each of its refusals needs an expression of a variadic function type, a store lowering noted, or an adapter whose call is a store, and most typed programs have none. Devirtualization is the last step before the check and already passes every expression of the final bodies through one callback; it now says whether one of them has a variadic function type, and the check returns without a walk when none has, no store was noted and no storing adapter exists. Of the corpus's 5,771 programs that reach the check, 5,610 skip the walk; of the 5,451 generated callback programs of Train perf 2: anchored and non-ASCII regular expressions without a scan, class tables selected by the class, two emitter passes in one walk, variadic functions through fixed slots #3379's tables, 4,036 still take it.SCRIPTC_CHECK_VARIADIC_SKIP=1walks every skipped program anyway and fails the build when the walk finds something.vitest.config.tssets it for every suite, which costs the lanes nothing they did not pay before; a refusal added to the walk later without its condition fails there first.The self-compile budget fails when a listed class becomes a dynamic value. The Lowerer became a dynamic value in the compiler's own build three times in five days (Memoize the island-boundary and type-provenance predicates #2878, Find lifted functions by name through the index, not by scanning the list #3192, Bound computed import() specifiers and load bounded builtins natively; require in ES modules throws #2567), each time through one helper's parameter type, each worth about 6 % of that build's functions, and each landed green; Train perf 3: the Lowerer leaves the dynamic tier of the compiler's own build (6% fewer functions), and a test keeps it out #3394 took the last one out and added a test that reads the source for such calls. This adds the build's own answer.
scripts/self-compile-budget.jsonlists the classes (neverDynamicClasses: the Lowerer).SCRIPTC_DYNAMIC_CLASSES=<names>makes a build report every place of the finished program that makes an instance of a named class a dynamic value.scripts/self-compile-budget.mjs --checkand--native-build(what CI'scheck (native compiler profile)job runs) set it, fail on a reported place with its file, line and source text, and fail when the build printed no report.--checkexits 1 and lists those 27 calls, for examplepackages/compiler/src/frontend/lowering/lower-stmts.ts:9315:31: a Lowerer becomes a dynamic value | if (!bindingNeverReassigned(lowerer, sym, decl)) return false;. On this branch it passes:keeps Lowerer out of the dynamic tier: no instance becomes a dynamic value.The LLVM emitter leaves the dynamic tier of the compiler's own build. With
SCRIPTC_DYNAMIC_CLASSES=LlEmitterthe build lists three places in the compiler's source, not the thirteen of an earlier list (the other ten were where shared dispatch helpers happen to be located).emit-function.tshanded the emitter toemitIntegerCloneandemitIntegerDispatcher, whose host parameter is theIntegerCloneHostinterface; they now getemitter.integerCloneHost(), a record of the seven members the clone emitter reads, as the Lowerer's callers gotbindingScope()in Train perf 3: the Lowerer leaves the dynamic tier of the compiler's own build (6% fewer functions), and a test keeps it out #3394.wrapping-sums.tscalled the function-valued memberint32Elementsashost.int32Elements(…), a member call, which hands the function the emitter as its receiver; it is read into a local first.LlEmitterjoins the list.CopyScanthe same way.CopyScan.layoutPairs(width-copies.ts, on main since Landing rollup: lang 2, startup 2, islands 3 #3395) called its function-valued member asthis.unionArms(…). The report names both lines once the class is watched (width-copies.ts:328:48and:329:46); read into a local, none.CopyScanjoins the list. The list checks the classes it names and no others: this one was not caught because nobody had named it, and the test of Train perf 3: the Lowerer leaves the dynamic tier of the compiler's own build (6% fewer functions), and a test keeps it out #3394 does not see this form. A general rule (every compiler class that is a dynamic value must be listed) is written up for a later change.Under Node nothing changes in either: the same functions run, through a record or a local.
Validation
6da2be6a1b,--gate-corpus=all, no allow list. The first two commits (7e5ca59110): PASS in 44.5 minutes. With the emitter commit (0080bd728f): PASS in 48.3 minutes. In each:keeps Lowerer, LlEmitter out of the dynamic tier: no instance becomes a dynamic value.commandsandrebuild, sanitizedcommands,rebuild-emitandrebuild-frontend. CI does not run these for this pull request (it touches no bootstrap path), so these runs are the evidence.61712ea6f2) is the member merged with main twice, plus theCopyScancommit. First main5f8c88e410(one conflict inLowerer.run: main adds the function views pass's state on the line after the class dispatch pass's, which the second commit moves out of the block; both kept). Then main2cea97f430(one conflict inLowerer.finishModule: main settles the worker module'sshareEnventry as the function's first step, and this train adds two parameters; both kept. That step runs between devirtualization and the variadic check and stores a boxed symbol, no function value; the comments now name it).6b56f87160(everything but the second merge of main) against main5f8c88e410, whole corpus, no allow list: PASS. 5,602 programs identical, 315 fail to build on both sides, none differ, no runtime-only row. Compile time: benchmarks −0.44 % [−1.77 %, +1.11 %], self −0.84 % [−3.82 %, +0.53 %] (inconclusive), fixtures +1.69 % [−0.37 %, +3.76 %] (inconclusive, one timing run). Self-compile budget: 135,717 functions (−4.6 % against main's source), 1,027,681,096 LLVM bytes (−1.6 %), 11,881 dynamic-tier functions (−2,635), andkeeps Lowerer, LlEmitter, CopyScan out of the dynamic tier.CopyScancommit: 135,862 to 135,717 functions, 1,028,098,564 to 1,027,681,096 LLVM bytes, dynamic tier 11,962 to 11,881; the report lists the two calls before and nothing after.node scripts/self-compile-budget.mjs --native-buildwith the profile check, on6b56f87160: exit 0 after 1,024 s,keeps Lowerer, LlEmitter, CopyScan out of the dynamic tier: no instance becomes a dynamic value, 148 bytes of heap per source byte (budget 146, tolerance 5 %). It reports that the committed profile matches 76.0 % of its functions on this tree (a warning, not a failure; the nightly refresh moves the profile).variadic,rest-,arguments,callback,virtualoroverridein its name) and 275 of them sanitized pass. On the pushed tree: build, lint, format, the generated-file checks, the baseline check, the policy tests, main's guard tests and this train's tests pass.2cea97f430, and the sanitized bootstrap phasesrebuild-frontendandrebuild-emiton6b56f87160.Not run
What is left
object.member(…), and 486 KB instead of 107 KB when it hands a class to an interface with a method. Design and measurements are in the fleet's notes; neither is a small change.🤖 Generated with Claude Code