feat: compile-check compiles only the dependents that use what a public API change touched
Summary
- When an assembly's public API changes, compile-check now compiles only the dependents that write one of the names the change touches. Every other dependent is left out, the way it is when the API did not change.
- A change no name can tell (an operator, a base type, an abstract member added, a member C# binds by pattern, and the like) still compiles every dependent.
- Builds on #3347, which supplies the names.
User Impact
Before: adding a public property to an assembly that 67 others reference compiled all 67, on a project with 561 assemblies. That took about 28 seconds, of which about 18 were the dependents.
After: only the dependents that write the changed type's or member's name compile. In measurements on that project:
- a specific type or member name is written by 0 to 2 of the 67 dependents;
- the expected time for such an edit is 12 to 15 seconds instead of 28.
Some edits are not faster, but nothing is missed:
- short common names (written by about 50 of the 67);
- constructors (target-typed
new(is written by 59); - changes to a type's shape.
Changes
-
The identifiers of every unit that references another unit of the plan are read as the run starts:
- cut by the characters C# allows (a letter, letter number or
_, then also digits, combining and spacing marks, connectors and formatting characters, the last left out of the name), with comments and strings included; - plus
new(for a target-typed new; - also the generator output the unit's last full compile saved. That output is read before any compile starts, because a unit's compile removes and rewrites it; only the sources, which nothing removes, are read in the background.
- A unit whose sources cannot be read, or that has none, has no index and compiles on any change.
- cut by the characters C# allows (a letter, letter number or
-
When a reference becomes final, each unit reading it learns whether the verdict touches it:
- never when kept;
- always when the change is unnamed, when nothing compared the reference, or when the unit has no index;
- otherwise only when the unit writes one of the changed names.
Only touching references count as changed inputs. So a dependent nothing touches is skipped (and counts as keeping its surface for the units below it). A unit that started early assuming the reference kept its surface keeps that compile.
-
Unchanged: which reference assembly a unit reads when it compiles (
KeptSurfaceReferences, the input key), when a unit may start early, and when a stale compile is redone. -
Each result lists the references whose change counted for it, with the first name it writes (
changed_byin the VibeLog). The run reports the units left out by names (name_skips) and what reading the names took (name_index). The JSON response is unchanged. -
docs/compile-check.mdstep 3 explains the narrowing; Limitations adds:- generators that write a name the dependent's sources never write. Leaving a dependent out removes its saved generator output, so in steady use the names come from hand-written sources alone and
name_index.with_generatedstays near 0. Closing that gap needs left-out dependents to keep their outputs and record; - every change to a type in
SystemorMicrosoft.CodeAnalysis(and namespaces under them) compiles every dependent; \u-escaped identifiers.
- generators that write a name the dependent's sources never write. Leaving a dependent out removes its saved generator output, so in steady use the names come from hand-written sources alone and
-
docs/vibe-logs.mdlists the new fields.
Verification
-
New tests:
- a dependent that writes no changed name is skipped and reported;
- one that writes the member's or only the type's name compiles and says which;
- an unreadable dependent compiles;
- an unnamed change compiles every dependent, even when names come with it;
- an early compile is kept when the assumed reference changed only names the unit never writes (as a dependent and as a unit selected for its own sources), and is redone when it writes one;
- a unit selected for its own sources still reads the produced reference assembly;
- only the touching references are named;
- the chain below a name skip is skipped;
- a name only in saved generator output makes the dependent compile;
- a unit that started early compiles again on a name only its saved generator output writes, although its own compile removes that output;
- the generator output is read before
startreturns: a FIFO source holds the background read until the directory is gone (not run on Windows); - identifiers with a spacing mark, combining mark, connector, letter number and zero-width joiner; a target-typed new with and without white space;
- identifier scanning, reading sources and generated code, and the background read being over once the run returns;
- the VibeLog fields, and their absence when there is nothing to say.
-
Mutations, each tried and then restored:
Mutation Fails A named change always touches skip by names, early compile kept, chain skip, plus 4 more A named change never touches dependent that writes a name, compiles again, generated code name An unreadable index does not touch unreadable dependent namedChange()ignores the unnamed reasonunnamed change with names, TestOnlyANamedVerdictIsANamedChangeSaved generator output is not read generated code name, TestReadIdentifiersReadsSourcesAndGeneratedCodeRead the generator output in the background after the sources TestNameIndexReadsGeneratedCodeBeforeStartReturns(3 of 3)Drop combining / spacing marks and formatting characters from identifier parts identifier characters test Keep formatting characters in the name identifier characters test Letter numbers do not start an identifier identifier characters test (#3347) A different answer without names reads as named TestVerdictFromWorkerTreatsADifferenceWithoutNamesAsUnnamed(#3347) Record format version stays 1 TestReusableSurfaceVerdictRejectsAVersionOneRecord -
In
cli/dispatcher:go test -race -count=1 ./internal/compilecheck/... ./internal/dispatcher/...passes;- the new scheduler tests pass 10 times in a row under
-race.
-
In each of the four Go modules:
gofmt -lis empty;go vetpasses;golangci-lint runreports 0 issues.
-
go test ./...passes incli/dispatcherand incli/release-automation(including./internal/architecture/...). Incli/commonandcli/project-runner, one test each fails only because the local sandbox refuses a directory under/tmpand a Unix socket bind; neither module is touched here. -
scripts/check-code-complexity.shandscripts/check-file-length.shpass. The longest production Go file touched has 447 lines. -
BenchmarkScanIdentifiers(a file of 40,000new Thing()expressions, about 840 KB): 13 ms per scan, against 1.07 s with the previousnew(check that copied the rest of the file. -
Not verified here: the timing on a large project. That is measured after merge.