Repository navigation
TypeScript 4.7.4: Exponential behavior in strict type checking #49845
Description
When upgrading from TypeScript 4.6.4 to 4.7.4 in a large Angular application, I'm seeing an infinite loop when trying to compile. Adding --trace-sigint to the node invocation, I see some traces like:
KEYBOARD_INTERRUPT: Script execution was interrupted by `SIGINT`
at isInParameterInitializerBeforeContainingFunction (/node_modules/typescript/lib/typescript.js:72539:13)
at tryGetThisTypeAt (/node_modules/typescript/lib/typescript.js:72003:19)
at checkThisExpression (/node_modules/typescript/lib/typescript.js:71979:24)
at checkExpressionWorker (/node_modules/typescript/lib/typescript.js:79915:28)
at checkExpression (/node_modules/typescript/lib/typescript.js:79860:38)
at checkParenthesizedExpression (/node_modules/typescript/lib/typescript.js:79895:20)
at checkExpressionWorker (/node_modules/typescript/lib/typescript.js:79960:28)
at checkExpression (/node_modules/typescript/lib/typescript.js:79860:38)
at checkNonNullExpression (/node_modules/typescript/lib/typescript.js:74469:37)
at checkPropertyAccessExpression (/node_modules/typescript/lib/typescript.js:74514:85)
KEYBOARD_INTERRUPT: Script execution was interrupted by `SIGINT`
at isWriteAccess (/node_modules/typescript/lib/typescript.js:19568:27)
at checkPropertyAccessExpressionOrQualifiedName (/node_modules/typescript/lib/typescript.js:74722:34)
at checkPropertyAccessExpression (/node_modules/typescript/lib/typescript.js:74514:17)
at checkExpressionWorker (/node_modules/typescript/lib/typescript.js:79945:28)
at checkExpression (/node_modules/typescript/lib/typescript.js:79860:38)
at checkParenthesizedExpression (/node_modules/typescript/lib/typescript.js:79895:20)
at checkExpressionWorker (/node_modules/typescript/lib/typescript.js:79960:28)
at checkExpression (/node_modules/typescript/lib/typescript.js:79860:38)
at checkParenthesizedExpression (/node_modules/typescript/lib/typescript.js:79895:20)
at checkExpressionWorker (/node_modules/typescript/lib/typescript.js:79960:28)
The trace is generally different each time, but usually in checkExpressionWorker somewhere. Unfortunately this is hardcoded to 10 calls in the trace. Sometimes the traces just have a lot of getTypeAtFlowNode without much additional context.
This is a large, proprietary application, so unfortunately I can't provide a repro. The project has a number of the "advanced" type checking bits enabled (and one disabled):
"strict": true,
"strictPropertyInitialization": false,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strictNullChecks": true,
I'm happy to apply any local patches to improve the ability to pinpoint the bit of code it's unhappy with, but I'm entirely unfamiliar with the codebase, so some hints could be useful. Also let me know if any additional info would be useful.
Node version v14.19.3, in case it matters.
Activity
sanzhar1310 commentedon Jul 10, 2022
same problem
imirkin commentedon Jul 11, 2022
Ryan Cavanaugh (@RyanCavanaugh) happy to provide more info, but I was hoping I could get some advice on how to collect it.
RyanCavanaugh commentedon Jul 11, 2022
Sorry, digging up my notes. Here's a doc I've been working on; let me know if it helps
Diagnosing Crashes for Report Purposes
Run
tsc(or equivalent) in a debuggerGenerally this would be
node --inspect-brk path/to/some/tsc.jsHit the crash
Note that you will generally want to turn off breaking on first-chance exceptions because many are hit during routine operation.
Check the call stack
Ideally, we can find a frame into one of these key functions and inspect their arguments to discover what's causing the crash:
checkExpressionandcheckSourceElementhave a Node callednodeisRelatedTohas two types calledoriginalSourceandoriginalTargetgetTypeOfSymbolhas a symbol calledsymbolDepending on the nature of the crash, you might not see any of these. If this happens, look for other frames with similarly-named arguments in scope, and examine those.
Print relevant info
Once we're in the stack frame with one of these objects, we can inspect them to figure out where in your source text they refer to.
Nodes
Generally, the best scenario is to get a node, since it will always point to some source text that you can use as a starting point.
Given a
node, like the one found incheckExpression, run this command in your debugger's evaluation console:This will print the source text of the expression that caused the crash. If this text is too short to tell where in the file it is, you can add
.parentto print the node's parent:You can keep adding
.parent.parent.parentas far as needed, up to the level of the entire source file. You can also runto get the path on disk to the file containing the expression.
Types
Types can also be helpful. Given a type like
originalSourceinisRelatedTo, you can (usually) run this command :or
It's possible that this also crashes for the same reason as the crash you're investigating, or prints an unhelpful output like
"any"(note that many types, such as the error type, might display"any"without actually being theanytype - don't get fooled). If this happens, try checking it for a.symbol, or.aliasSymboland use the Symbol instructions below.Symbol
A symbol in the TypeScript codebase is, roughly, an object representing something that was declared in a program.
You can run this command to see a string representing the symbol:
This will usually be enough of a clue to get you started. If you're not sure where this declaration is in your program,
Most of the time, a symbol has at least one
declarationwhich is aNode:Symbols can also point to other symbols related to their meaning via their
targetandparentproperties, which may or may not be present.imirkin commentedon Jul 11, 2022
Great, thanks, that should hopefully get me going. Since this isn't a crash but rather a infinite (or extremely long) loop, will start printing stuff and see where it's getting stuck.
imirkin commentedon Jul 11, 2022
Thanks for the help. FWIW I wasn't able to get
__debugGetText()going (looks like debugging needs to be enabled, and there's no easy way to flip it on that I could find?), but I was able to print enough stuff to get the gist of where it was going wrong. It looks like this is actually not (directly) a typescript issue, but rather something that the Angular Ivy compiler is doing withfullTemplateTypeCheck=trueenabled. Disabling this option, or messing with a particular template expression to reduce it in size fixes the infinite loop. I'll go file a bug there for now, and close this one. Perhaps it'll turn out to be a TS issue after-all, but that doesn't seem to be definitive at this point.imirkin commentedon Jul 12, 2022
I'm back :) The Angular folks have distilled a reproducer into something that happens with plain tsc. You can see the full comment here: angular/angular#46792 (comment)
Problematic sample reproduced here for completeness, courtesy of Joost Koehoorn (@JoostK):
This takes an unnecessarily long time, probably some sort of exponential situation going on. This happens with TS 4.7 and not TS 4.6.
node --cpu-prof ./node_modules/.bin/tsc index.ts --strict --noEmit --extendedDiagnosticsLet me know what, if any, additional info is necessary.
[-]TypeScript 4.7.4: Infinite(?) loop in type checking[/-][+]TypeScript 4.7.4: Exponential behavior in strict type checking[/+]JoostK commentedon Jul 12, 2022
I suspect this is caused by #42835. Looking at the change, it introduces a call to
checkTruthinessExpressionincheckTestingKnownTruthyCallableOrAwaitableType, triggeringcheckExpressionfor the LHS of the binary expression. However, that LHS has already been visited by the binary expression trampoline, so it now checks the LHS again. Due to the recursive nature this results in exponential growth.JoostK commentedon Jul 12, 2022
I opened #49881 with a fix.
fatcerberus commentedon Jul 12, 2022
That
arr.lengthrepro looks bonkers - what real code pattern does it correspond to? I couldn’t see ever nesting an expression that deep.JoostK commentedon Jul 12, 2022
Generated code may look like this.
imirkin commentedon Jul 12, 2022
In reality, they're different arrays, and the expression isn't nested but rather a simple
a && b && csort of thing. This happens in an angular template, and I'm guessing the nesting comes from the template expr -> real expression logic done by the Ivy AOT compiler logic. See the repro in angular/angular#46792 for how this happens (still a contrived example, but probably easier to see how this could happen in real life).