Repository navigation
[experiment] fully order independent type checking - #65064
Jake Bailey (jakebailey) wants to merge 24 commits into
Conversation
|
TypeScript Bot (@typescript-bot) test it |
|
Hey Jake Bailey (@jakebailey), the results of running the DT tests are ready. There were interesting changes: Branch only errors:
|
|
Jake Bailey (@jakebailey) Here are the results of running the user tests with tsc comparing Something interesting changed - please have a look. Details
|
|
Jake Bailey (@jakebailey) Here they are:
tscComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
Developer Information: |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Well, that's a little bleak. |
7eda73a to
1846356
Compare
|
Not sure why top400 didn't come back either |
|
TypeScript Bot (@typescript-bot) test it |
1846356 to
c7ee0b9
Compare
|
Hey Jake Bailey (@jakebailey), this PR is in an unmergable state, so is missing a merge commit to run against; please resolve conflicts and try again. |
|
TypeScript Bot (@typescript-bot) test it |
|
Hey Jake Bailey (@jakebailey), the results of running the DT tests are ready. There were interesting changes: Branch only errors:
|
|
Jake Bailey (@jakebailey) Here are the results of running the user tests with tsc comparing Something interesting changed - please have a look. Details
|
|
Jake Bailey (@jakebailey) Here they are:
tscComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
Developer Information: |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Now we're talking |
|
Jake Bailey (@jakebailey) Here are the results of running the top 400 repos with tsc comparing Something interesting changed - please have a look. Details
|
c7ee0b9 to
c501b1f
Compare
|
TypeScript Bot (@typescript-bot) test it |
|
Hey Jake Bailey (@jakebailey), the results of running the DT tests are ready. There were interesting changes: Branch only errors:
|
|
Jake Bailey (@jakebailey) Here are the results of running the user tests with tsc comparing Something interesting changed - please have a look. Details
|
|
Jake Bailey (@jakebailey) Here they are:
tscComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
Developer Information: |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Jake Bailey (@jakebailey) Here are the results of running the top 400 repos with tsc comparing Something interesting changed - please have a look. Details
|
|
TypeScript Bot (@typescript-bot) test it |
Checker results should not depend on which files were checked first, on which types an editor happened to ask for before diagnostics were requested, or on whether a file was emitted before it was checked. Tests had no way to show such dependence, so fixes for it could only be described, not demonstrated. The harness-only checkOrder option queries the checker in a different order: files in reverse, each file in its own program, or every expression's type, symbol, contextual type, resolved signature, and the types of names in type annotations before any diagnostics. The same orders apply to emit. Listing several orders produces a baseline for each, so order dependence shows up as differing baselines. Fixed orders over whole files miss results that depend on which single query a checker answers first. With TS_CHECK_ORDER_FUZZ=n, each test without a checkOrder option is checked again n times in fresh programs that first answer a seeded random selection of queries and single file emits. Any change in diagnostics, emitted output, or the type or symbol of a node fails the test with the seed and a minimized list of queries that reproduces it. Symbol IDs in printed names are ignored, as they follow creation order by design. The fuzzer is off by default, so it does not slow the normal suite.
These tests record checker results that depend on the order in which files, declarations, and queries are checked, so that each later fix shows up as a reviewable baseline change. Most list several checkOrder variants; where the variants disagree, that disagreement is the bug. Some existing tests also gain the typesfirst order, as asking for types and symbols first changes what they report. The cases cover: - variance measured through cycles of recursive references, which depended on which type in the cycle was measured first (64891, and a reduction of the react-relay DefinitelyTyped tests); - circularities whose blamed location is whichever use resolved first: circular type parameter defaults and constraints, a cycle through namespace spreads, awaiting a promise that refers back to itself, and import elision computing types before checking; - instantiation limit and excessive depth errors, reported only in whichever file reached the limit first; - late-bound members, computed enum members, and aliased module.exports members, which differ when queried before checking; - relation results cached against truncated or in-progress comparisons; - the order of types in unions, which followed type ids, and so creation order, for object literals, rest types, indexed accesses, type parameters, and iterator method types; - return type cycles through callbacks and subtype reduction, JSDoc type tags on contextually typed functions, a scope whose thunks refer to the scope, a never narrowing after a loop, and a const assertion inferred before its binding pattern (seen in OmniRoute, effect, webpack, arktype, uglify-js, and check order fuzzing). The circularity tests from PR 64834 are copied unchanged, to compare against how that PR handles the same cases.
A spread that produces a tuple too large to represent reports the error at the node being checked. When the type is first requested through the API, before checking has begun, there is no such node and reporting the error crashed instead. (seen in excessivelyLargeTupleSpread)
checkAssertion runs in several check modes, such as when a call is first checked without its context sensitive arguments, and each run overwrote the operand type kept for the deferred comparability check. That check used whichever was stored last, so its result depended on the order in which the assertion was checked. The deferred check now uses the cached normal type of the operand. The assertion itself only needs the operand type for `as const`, and the operand of a non-const assertion is contextually typed by the asserted type alone, so in any check mode the assertion computes the cached type the deferred check will read, with inference for an enclosing call hidden from the operand. This is the result the deferred check would produce on its own, and checks each operand once rather than again with fresh flow analysis in the deferred check.
The type of a destructured parameter's binding elements depended on
whether the parameter's type happened to be cached already. For
example, in non-strict mode
(({ [a()?.d]: c = "" }) => {})();
reported TS2537 when 'c' was queried before the IIFE call resolved
the parameter (its declared type is the undefined widening type),
but not otherwise. Conversely, in
f7({ a: {} });
function f7({ a: { x = 0, y = 0 } } = { a: {} }) {}
calling f7 first made 'x' and 'y' errors of type any.
Make the choice depend only on the declaration. Parameters of
context-sensitive functions take their type from contextual
assignment, so ask for the parameter's type. Everything else uses
the type derived from the declaration, as it did when nothing was
cached. Asking for the parameter's type unconditionally instead
introduces circularities in patterns whose initializers reference
sibling elements.
A context-sensitive function is typed in parts: the enclosing call
assigns its parameters their contextual types, completes its signature
with the type parameters of a generic contextual signature, and checks
it. Queries that reached the function before or during those steps
saw it incomplete and cached what they derived from it.
The first query of such a parameter returned the type it computed
itself from the final signature, while every later query returned the
type the enclosing call assigned. In
declare function fun<T>(g: (x: T) => T, h: (y: T) => T, x: T): T;
fun((Math.random() < 0.5 ? (x => x) : (x => undefined)), x => x, 10);
the last 'x' was number when the call was checked first, but any when
'x' was queried first. The assigned type is now returned whenever one
exists, as the original comment intended.
Type aliases in the body read the signature's type parameters as outer
type parameters, so querying such an alias first resolved its
conditional types eagerly. In
declare function f(fun: <T>(t: T) => void): void
f(function self(t) {
const s = self;
type P = Parameters<typeof s>;
});
P was [t: unknown] when the call was checked first, but [t: T] when P
was asked for first. Assigning the contextual parameter types, which
completes the signature, is now split from the rest of the contextual
check, and every way to obtain the signature requests it: the type of
the function's symbol, before anything resolves its members, and the
signature of its declaration. Contextual typing can reach the
enclosing call while the signature is being created, so the signature
created there is kept rather than replacing the one that was
contextually typed.
When resolving a function expression's members reached the function
again through its enclosing call, the contextual check saw an empty
signature list and gave up, yet left the node marked as checked, so
implicit any errors could be lost depending on which query got there
first. The deferred function check now requests the contextual check,
so it is performed at least once.
Generic calls nested in the arguments of other generic calls, or in context-sensitive functions passed to them, depend on the enclosing call for their contextual type and parameter types, and the enclosing call infers from them in turn. When the inner call was queried first, it was already marked as resolving by the time its contextual type resolved the enclosing call, so the enclosing inference saw a placeholder and both calls cached results that a check in source order never produces. Computing the contextual type of a call nested in an argument of an enclosing call before resolving it resolves the enclosing call first, as source order does. Other contexts are not resolved ahead of time, since computing them may evaluate expressions that checking does not, such as the target of a destructuring assignment. Resolving a call's signature can also be requested while checking one of its arguments. Errors that resolution reports at the current node, such as an instantiation depth error, were located at whichever node asked first, so checking an argument ahead of its call moved the error onto the argument. Resolution is now located at the call.
Asking for the symbol of a property access that hits an index signature stored the index symbol as the access's resolved symbol. Checking a delete expression reads that link to decide whether the operand must be optional, so a prior symbol query turned `delete a.x` on an index signature into an error that a plain check does not report. The type of the index symbol at such an access, which hovers show, now looks the index symbol up again rather than reading it from links. A single signature type produced by higher order inference is instantiated with the full mapper combined in, since its signature can reference type parameters other than the outer type parameters of its declaration. The instantiation cache was keyed only by the mapped outer type parameters, so two instantiations differing in another type parameter (here the this type) shared whichever result was computed first, and checking `outer.log` gave it the type of `inner.log`. Such instantiations are no longer cached.
Explaining why a name failed to resolve looks up other symbols, for example to find a spelling suggestion, and that can resolve aliases. When the failed name was the target of an alias, the suggestion lookup re-entered the alias being resolved, or skipped it, depending on which aliases were already in progress. In declarationEmitUnknownImport that decided whether two "Circular definition of import alias" errors were reported: they appeared when the import was checked first, and not when the export specifier was resolved first. Defer the explanation until no resolution is in progress, so it only observes finished results. The circularity errors that were artifacts of the suggestion lookup are no longer reported.
b4fe268 to
e8dc435
Compare
When one reference hit the flow analysis depth limit, flow analysis was disabled for every later reference in the same function or module body, so the type of a reference depended on whether another reference in the body had been analyzed before it. Each reference now gives up only when its own analysis hits the limit. Shared flow node results are cached for the duration of one flow analysis, including incomplete results computed under a loop back edge. Once that loop's back edge has been exited and the loop label has a complete type, they are stale. Reusing them made a reference's type depend on which reference was analyzed first: in exhaustiveSwitchCheckCircularity, querying the reference inside the loop before the switch expression produced `never` instead of "bbb". An incomplete shared result is now reused only while the loop that was innermost when it was computed is still being analyzed.
A computed property name stored a placeholder type while its
expression was checked, and returned the placeholder when re-entered.
That kept the cycle off the resolution stack, so the computations in
between did not fail, and the placeholder `{}` escaped into messages
when checking entered through the property name, while `any` did when
it entered through the variable it references. In
blockScopedBindingUsedBeforeDef, the error read "Type '{}' cannot be
used as an index type" or "Type 'any' ..." depending on what was
checked first.
Resolve computed property names on the resolution stack, failing with
the error type on re-entry like other circular resolutions.
e8dc435 to
7afb732
Compare
CompareTypes orders the constituents of unions, and so printed types,
union deduplication, and everything derived from them. Many types it
cannot tell apart fell through to the type id, which follows creation
order, so these results depended on what had been checked before.
Most such ties were copies: checking the same expression again
(inference passes, overload resolution, contextual re-checks) created
a new type with identical content every time.
Types that are the same by content are now interned, so the same
inputs yield the same type whenever and however often they are
computed: object literal types, spread results and the property
symbols they synthesize, widened, regular, and padded copies of object
literals, object rest types, types implied by binding patterns,
intersections with the same constituents, flags, and alias, and
deferred indexed accesses, which are keyed by the flags they record
rather than by transient ones. Interned object types are keyed by
their members in table order, so spreads such as { ...a, ...b } and
{ ...b, ...a } stay distinct and each keeps its own property order.
Types that remain distinct are ordered by how they were derived:
- type parameters sharing a symbol (tuple elements, clones made for
signature and mapped type instantiation, distributive and
restrictive copies) by the kind of copy, then by their origin and
the instantiation they were made for;
- fresh and regular literal types, array literal types after the
references they were cloned from, and object literal copies by the
flags they were created with;
- object literal, rest, and binding pattern types by their members,
compared by declaration and name rather than by symbol id;
- tuple targets by their label declarations.
isDeeplyNestedType counts only occurrences of a recursion identity
newer than the previous one, using type ids for "newer", so whether a
comparison gave up as deeply nested depended on what had been checked
before. It now uses a type's generation, one more than that of the
types it was built from, which captures the same ordering wherever it
follows from how the types are built, and nothing else.
The pseudo-type printer reused the declared type of an inferred
property assignment when the expression's widened type matched, but
that type is widened without the context of its object literal and
can differ from the real one. It is now reused only when equivalent.
A relation that succeeds while an outer comparison is still in progress has only been shown to hold under the outer comparison's assumption. Caching it globally made later lookups depend on which comparison happened to run first. The same was true for results cut short by depth limits measured against outer resolutions. Each result now tracks the lowest resolution it depends on and whether it was truncated. Results are cached globally only when the comparison that introduced their assumptions completes, and truncated results only at their own head. Failures remain cached immediately, since assumptions only ever make a relation more likely to succeed. A comparison that only relates by assuming comparisons within it is reported as related once it completes, whether it is computed or read from the cache. Comparisons of types referencing constrained type parameters are considered related while the same comparison with unconstrained type parameters is in progress, since constraints only make more types related. A completed comparison found to succeed now counts the same way. That only went unnoticed because completed comparisons used to stay in progress until the outermost one completed; without it, relating structurally identical generic interfaces creates ten times the types.
Instantiating a signature copies a parameter unless the parameter's type has already been resolved, so whether the parameter in a signature's printed scope is the declared one depends on what was checked before. Both name the same declaration, so treating only the declared one as the same reference made printing a reused annotation depend on check order.
Computing the values of an enum's members type-checked each computed initializer to report one that is not a number. The initializer may refer to the enum itself, whose type then reads member values that are still being computed, so the enum's type depended on whether its values were first computed by checking it or by asking for its type. The values do not depend on the initializer's type, so the check now happens when the member is checked, as for any other error in an initializer.
The checker caches nearly everything it computes, and many results depend on what happened to be cached when they were computed: which file was checked first, which types an editor asked for before requesting diagnostics, whether a file was emitted before it was checked. The check order tests and fuzzer show hundreds of such differences on main. As long as they exist, checkers cannot share results, a cached result cannot be dropped and recomputed later, and concurrent checking needs separate checkers with fixed file assignments to stay deterministic. The goal is a checker that behaves like a pure function with a cache: the answer to any query is the answer of computing it from scratch. The cached state of a checker moves to a memo store, leaving the Checker with only the state of the computation in progress. Every cached result is a memo: a cell, a keyed map, or a check-once set. Reading one records it as a dependency of the computation in progress. Computing a result can produce more than the result: diagnostics, reference marks for unused checks, deferred nodes, required emit helpers. When a query computed a result first, those effects were either reported for code checking never visits or lost when checking reused the result. A query now records them on the result instead, along with the results it reused, and checking replays them, exactly once, when it reuses the result, as if it had computed it. Effects produced outside any memoized computation belong only to the query and are dropped. Circularities are where most of the order dependence lives. A computation that re-enters one in progress sees a provisional state, so whatever is cached until the outermost computation of the cycle finishes depends on where the cycle was entered. Those results are now discarded when it finishes, and recomputed when next requested. The computations in progress when the cycle was detected keep their fallbacks, as each reaches the cycle when computed from scratch too. Results that define identity, such as declared types and instantiations, are kept, so no second, distinct result can exist. Main detects many re-entries silently, through markers and probes rather than the resolution stack; those now go through the resolution stack too, so their cycles are seen. Where a cycle's result depends on which member was entered, the cycle is resolved from a canonical entry, the member declared first, as when checking in program order: cycles through the types of symbols, mutually dependent member tables and module tables, and type parameter defaults. Variances of circular types are measured with TypeScript's rule from the canonical type of each strongly connected component, so that they no longer depend on which type was measured first. Instantiation depth is measured from the start of each memoized computation, and a reused result adds none, so whether a result reaches the depth limit does not depend on what was cached. The count limit, which only stops runaway instantiations, still counts per statement or expression, and starts again for each outermost computation; its error is reported once, where it is reached. Smaller fixes needed for the same reason are part of this change: errors for accessor pairs are reported by each accessor without a flag, type alias type parameters are read through the declared type, name lookups that cannot match a late-bound member do not resolve late-bound members, symbols derived from the same declaration are ordered without ids, awaited types that refer back to themselves fail as a cycle, and relation complexity overflows are reported in every comparison that needs them.
A declared type parameter with a circular constraint is circular no matter how it is used, and checking its declaration reports it. Which use happened to resolve the constraint before that is an accident of check order, so pointing at it as the origin of the cycle made the diagnostic depend on that order. Only an instantiated type parameter has a cycle that originates at a use.
A mapped type resolves its constraint eagerly so that errors in the constraint are reported at the mapped type. It did so only when the type was created, so when the type was first created while checking another node, the mapped type itself never reported those errors.
A cycle through return types gave different results depending on where it was entered. Entered at rebuild's return type, re-entering it fails rebuild, so both members report an implicit any. Entered at b's return type, subtype reduction of rebuild's return type finds b in progress and compares as if it returned any, without failing anything, so rebuild returns B and was kept as a participant of the cycle, though computing it from scratch fails. Cycles through the types of symbols are already resolved from the member declared first (see yieldCycle). The same now applies to the return types of declared signatures, both when a cycle re-enters a return type and when it only observes one in progress, so that each order gives the result of resolving the first declared member first (seen in effect's SchemaAST). Contextually checking a function expression infers its return type from its body, and that inference was pushed as a return type resolution of the function's signature. When the body calls a function whose return type depends on the call enclosing the function expression, resolving that call requests the function expression's return type, which, seen in progress, returned any without detecting a cycle, so the enclosing function's return type was inferred from a provisional any instead of reporting the circularity (seen in OmniRoute). The inference now runs without marking the signature in progress, as in TypeScript: the nested request computes the return type itself, reaches the enclosing function, and the cycle is reported on both. The result is cached only if no request has already cached one, the same as when the return type is requested directly. The flag marking a function as contextually checked is set before its return type is inferred, and is TypeScript's guard against re-checking the function from within its own check, so it is never discarded. The inferred return type is now kept whenever the flag is, so that a discarded cycle in between cannot leave the flag without the return type, which made a const assertion in the body readonly when queried before its binding pattern.
Completing contextual signatures before anyone reads them switched parameter assignment from the signature of the function's symbol to the signature built from its declaration. In JavaScript the symbol's signature comes from a @type tag when one is present, and the function's parameters take their types from that tag. Using the declaration's signature wrote the outer contextual types onto those parameters instead, producing errors inside the function body that the tag rules out (seen in webpack). Prefer the tag's signature, as before, and fall back to the declaration's signature otherwise.
An instantiation stopped by the limit on the depth of the whole instantiation names every type that recurs in it, in the order in which they recur. Which type of the cycle the instantiation entered first depends on what was cached before, so checkers that had checked different files named the same cycle starting from different types, and reported the same error twice with different messages (seen in zod, with checkers running concurrently). The names now start from the type declared first, so the message is the same wherever the cycle is entered. The limit on the depth of a single computation already names only what that computation instantiated, which does not depend on where it was entered, so its messages keep the order in which the types recur.
While the type implied by a binding pattern is computed as the
contextual type of its initializer, references to the pattern's
elements from the elements' defaults are any. The calls and types
computed from them were cached anyway, so asking for the contextual
type of the initializer first left `extra` in
const { kind, payload, extra = kind === "a" ? ... : ... } = u;
as any rather than unknown. TypeScript behaves the same way.
Track the patterns as resolutions in progress rather than a separate
stack, so that a reference returning any marks the computation
circular and what was cached from it is discarded.
(seen in check order fuzzing)
A call resolution hides the resolutions below it, so a symbol whose initializer reaches itself through a call argument is computed again, nested in itself, as in main. The nested computation observes things in progress that the outer one entered: here, a mapped property whose contextual type is skipped, and the reverse mapping whose depth cutoff it reaches. Those mark everything since as circular, so the nested result was discarded, and the outer computation, reaching the same types again outside that context, then failed with a circularity. When everything the nested computation observed in progress was entered by the outermost hidden resolution of the same target, the nested computation is what that one always computes first from scratch, whichever entry led to it, so its result is kept as a cycle participant, as main keeps the first result set. Main also keeps the reverse-mapped property type computed under the depth cutoff, so it types the property as unknown. That result still depends on the cutoff and is discarded, so the property is computed again from scratch afterwards, giving its full type: an improvement over main rather than a match (seen in arktype).
When a query enters a cycle somewhere other than its program-order entry, the cycle yields to its first member and recomputes it with the resolutions below hidden. Two things made that recomputation differ from computing the member first: - The reverse mapping stacks, contextual types and inference contexts of the hidden resolutions stayed visible, so the recomputation saw contexts that a computation from scratch never has. A yield now hides them along with the resolutions and restores them when it ends. - Results were kept as cycle participants only by comparing the outermost cycle head in progress against the resolution. A cycle headed below the yield, which the yield cannot observe, caused the yield root and the hidden re-entries of itself to be discarded, so the member was computed again and genuinely re-entered itself. Each type resolution now tracks the lowest cycle head it observed, and a result is kept when what it observed was entered from scratch: by the outer resolution of itself, by the yield it is the root of, or by the outermost measurement of variances. (seen in circularScopeThunkReverseMapped)
7afb732 to
a7534e3
Compare
There's too much here to explain, but at a high level, this is a massive change which aims to make the checker entirely order independent, that is, it doesn't matter when you call the checker, which nodes you walk first, if you do or do not pull something, check some other file, helicopter into the wrong part of a cycle, etc, the behavior, diagnostics, everything, will be the same.
Looking at individual commits is my suggested way to look inside, not the entire diff, which will surely crash your browser.
This is all AI driven, so it's a bit hard to unbox (I plan to break this apart since some stuff can be done early), but some interesting tidbits (handwritten by me!)
There's a new
checkOrdertest option, which makes certain tests variant on interesting orderings, like "what if we visit every single node before checking" or "what if we reverse the order of the files" or even "what if we use PRNG to randomly walk nodes and then check". This up front found on the order of 500 failing tests we already had that were secretly order dependent. These were all fuzzed out and kept.There are a significant number of new tests which all show various ordering bugs never before discovered.
The first chunk of the PR are fixing places where code peeked at caches that "happened to be filled" instead of calling the actual function to pull on them.
Then, all of the ID-using code gets removed. At the end everything now has an ID independent ordering based on content, flags, the works. This caught a bunch of places where we were not caching things and ending up with types that were by all rights identical but had different IDs and then were unstable.
The big and scary change is the introduction of a new state management type dubbed "memo". Every single piece of checker state was moved into memos on a sidecar object on the checker (maybe for future sharing??). This memo system keeps track of the current computation, what depends on what else, eliminating basically all flags/bools that track "computed" or "resolved"-ness. This tracking also records side effects, including diagnostics, "referenced" marks, deferred checks, emit helper checks. The result is that if you query via the API and that calculates some error, those same errors will appear later if the checker runs.
With those data structures in place, now all of the cycles can be handled. Once a cycle is discovered, the checker starts logging what it's doing, and throws all of that info away assuming it's been poisoned.
All of the different recursion stacks get pulled into a single memo stack, which is a cool simplification too.
Variance is now a fixpoint algorithm, proceeding in rounds until stability. In practice, it doesn't iterate much.
Instantiation limits also stop being global numbers (since of course, that is order dependent side effect stuff). Instead, things get limited by the amount of work that's been done, and if you then pull on caches related to work, it tallies that work up to error consistently when needed. (I don't think this is fully done yet, though, since there are depth limits that are really hard to get right. Still thinking about that.)
In some cases, cycles still can't be fixed up, and so they're made consistent by doing what we've done previously for these cases, which is to pick a canonical place to restart to at least get it consistent.
Unfortunately, this all has a cost; the end of the PR tries to claw back perf, but at the end of the day, if we do more work tracking, we spend more CPU and memory tracking. It's still an open question whether this can be cheapened any more. But, my hope is that this idea is minimal enough that we could then optimize TS as a whole assuming this order-independence, e.g. work stealing, checker recycling, shared state, maybe even using LRU caches, or dumping data to disk.
But, it could also well be that this is slop and impossible to manage. But I don't want to sit on it anymore and want to see some real world effects.
Also, this is really meant to be rebased, so expect this all to be nasty nasty force pushes all the time as this gets tweaked. Unfortunately right now that's like 125+ commits, so that sucks.
However, I think that this will fix countless issues on the tracker. I have no idea how to evaluate all 5k issues to see if this PR fixes them. But it's gotta be a lot.