Skip to content

fix(resolution): what a walk answers doesn't depend on who asked first - #3215

Open
colbymchenry wants to merge 1 commit into
mainfrom
claude/spd2-order-flips
Open

colbymchenry wants to merge 1 commit into
mainfrom
claude/spd2-order-flips

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

Four lookups kept an answer that depended on who asked first. The memo audit made each one flip on a small fixture with the reversed-batch probe. Now a chain of aliases answers the same from any point on it, however long it is, and a C rename answers for the asking file alone.

Root cause

tsAliasedType, luaModuleMember, Pascal's classNamed. Each stopped after a few steps and stored what it had, under a key without the depth. Take type A = B; B = C; C = D; D = E; E = F; class F. Asked from A, the walk gives up at E, 4 steps down, and stores null for E, so a value typed E gets nothing. Asked from E first, E finds F and A then reads that answer and gets F. So one project's calls came and went by which reference resolved first.

cMacroRename. The answer is cached per file and name, but read memo.byName, which grew as each source file first asked. That growth exists because a source file's own #define matters to that file, and reading every source up front is too much. A source another file includes was caught by it by accident: it counted for its includer only once it had asked, on that worker.

The fix

Lookup Can it branch? What it does now
tsAliasedType No, an alias names one thing Follows the chain to its end, with a visited set. No cap
luaModuleMember No, a step's second option runs only when the first isn't an alias at all, which the text decides Follows the chain to its end. The memo's in-progress entry is the visited set. No cap
Pascal classNamed Yes, a name can be an alias in each arm of an {$IFDEF}, and the first that leads to a class wins A walk with a seen set shared across branches, which bounds it, so no cap. A step's result can lean on where the walk came from, so nothing from inside the walk is stored or read: only the answer for where it started

A cycle gives nothing in all three, from anywhere on it.

cMacroRename: the index is now fixed once built. Headers go in as before, then the source files something #includes by a source name (impl.c, tables.inc), which reach their includers as a header does. They share the existing 16 MB budget, read after the headers so no header's place in it moves, and it's a per-thread read, outside #3182's main-thread scan. A source past the budget gives its includers no rename, the same in every order. Any other source reaches itself alone, so its renames sit in a per-file set that no other file reads.

A config name that isn't a regular file. moduleConfigEntry read whatever statSync found, so a FIFO under one of these names would park readFileSync, and #3214 took that hazard from tsconfig and go.mod to Cargo manifests and compile databases. Such a name now counts as absent and is never read. loadCppIncludeDirsFromCompileDB and the resolver's diskContext read the same names and had the same gap, so they get the same check, and a folder named compile_commands.json no longer hides the one in build/. A FIFO can't be made on this machine, so the test puts a folder under both names, and 2 mutations turn it red. It isn't in the CHANGELOG, since #3214 hasn't shipped.

Validation

Windows 11.

Both orders. resolution-order-independence.test.ts asks each pair in both orders, each on a fresh resolver, as #3194's test does: 10 tests, 6 red on main, and the other 4 guard that a cycle names nothing and a file nothing includes renames for itself alone. 8 mutations all turn a test red: each cap put back, each cycle guard removed, the first alias only, included sources unread, own renames unread.

Reversed-batch probe, on one thread, each batch resolved last to first:

Fixture Main This branch
TS alias chain 2 edges differ same graph both ways
Lua re-export chain 2 edges differ same graph both ways
Pascal alias chain 2 edges differ same graph both ways
C #include "impl.c" rename 1 edge retargets same graph both ways

A/B against main, every dump compared byte for byte. No edge moved on any of them, so no chain or rename on these repos reaches a different answer:

Repo Language Full index Reversed-batch probe
vscode-be TypeScript identical not run, for time
excalidraw TypeScript identical same both ways
redis C identical same both ways
rocksdb C++ identical not run, for time
hammerspoon Lua, Objective-C identical same both ways
SynEdit Pascal identical same both ways
DelphiMVCFramework Pascal identical same both ways
flask Python, control identical not run
express JavaScript, control identical not run

Cost. 8 interleaved rounds against main, medians, wall s and CPU s:

Repo Step Main This branch
rocksdb index 31.62, 110.76 31.37, 110.38
rocksdb sync after an edit 7.59, 8.19 7.76, 8.30
vscode-be index 12.82, 44.88 12.92, 45.29
vscode-be sync after an edit 4.29, 4.51 4.27, 4.55

All within this box's noise, which is wider than 1%. Those rounds ran before the last small tidy of cMacroRename and the regular-file check; neither adds work on these repos beyond the stat already made.

Suite on this head, with a kernel built from it: 881 files passed, 6 skipped; 11,053 passed, 2 expected fail, 87 skipped, 0 failed; 2 workers, by the memory floor, Vitest 4.1.11.

🤖 Generated with Claude Code

Three walks stopped after a few steps and kept what they had: asked from the
top of a chain of type aliases or Lua re-exports, the walk stopped short and
stored a null for the middle of the chain, which the next asker took. The
TypeScript and Lua walks have one successor at each step, so they now follow a
chain to its end, with a visited set to end a cycle. Pascal's can branch, an
alias declared in each arm of an {$IFDEF}, so it walks with a seen set and
keeps only the answer for where it started.

cMacroRename's index grew as source files asked, so a rename in a file another
includes counted only once that file had asked, per worker. Sources something
includes now go in the index up front within the budget, the index is fixed
once built, and any other source's renames count for itself alone.

A config name that isn't a regular file now counts as absent and is never read:
a FIFO would park readFileSync, and #3214 took that to manifests and databases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant