Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript 4.7.4: Exponential behavior in strict type checking #49845

Closed
imirkin opened this issue Jul 10, 2022 · 11 comments · Fixed by #49881
Closed

TypeScript 4.7.4: Exponential behavior in strict type checking #49845

imirkin opened this issue Jul 10, 2022 · 11 comments · Fixed by #49881
Labels
Needs More Info The issue still hasn't been fully clarified

Comments

@imirkin
Copy link

imirkin commented Jul 10, 2022

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.

@sanzhar1310
Copy link

same problem

@RyanCavanaugh RyanCavanaugh added the Needs More Info The issue still hasn't been fully clarified label Jul 11, 2022
@imirkin
Copy link
Author

imirkin commented Jul 11, 2022

@RyanCavanaugh happy to provide more info, but I was hoping I could get some advice on how to collect it.

@RyanCavanaugh
Copy link
Member

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 debugger

Generally this would be node --inspect-brk path/to/some/tsc.js

Hit 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:

  • checkExpression and checkSourceElement have a Node called node
  • isRelatedTo has two types called originalSource and originalTarget
  • getTypeOfSymbol has a symbol called symbol

Depending 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 in checkExpression, run this command in your debugger's evaluation console:

node.__debugGetText()

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 .parent to print the node's parent:

node.parent.__debugGetText()

You can keep adding .parent.parent.parent as far as needed, up to the level of the entire source file. You can also run

ts.getSourceFileOfNode(node).fileName

to get the path on disk to the file containing the expression.

Types

Types can also be helpful. Given a type like originalSource in isRelatedTo, you can (usually) run this command :

originalSource.__debugTypeToString()

or

originalSource.__tsDebuggerDisplay()

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 the any type - don't get fooled). If this happens, try checking it for a .symbol, or .aliasSymbol and 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:

symbol.__tsDebuggerDisplay()

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 declaration which is a Node:

symbol.declarations[0].__debugGetText()

Symbols can also point to other symbols related to their meaning via their target and parent properties, which may or may not be present.

@imirkin
Copy link
Author

imirkin commented 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
Copy link
Author

imirkin commented 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 with fullTemplateTypeCheck = true enabled. 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
Copy link
Author

imirkin commented 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 @JoostK:

declare const arr: string[];
  
(((((((((((((((((!arr.length) && !arr.length) && !arr.length) && !arr.length) && !arr.length) && !arr.length) && !arr.length) && !arr.length) && !arr.length) && !arr.length) && !arr.length) && !arr.length) && !arr.length) && !arr.length) && !arr.length) && !arr.length) && !arr.length);

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 --extendedDiagnostics

Let me know what, if any, additional info is necessary.

@imirkin imirkin reopened this Jul 12, 2022
@imirkin imirkin changed the title TypeScript 4.7.4: Infinite(?) loop in type checking TypeScript 4.7.4: Exponential behavior in strict type checking Jul 12, 2022
@JoostK
Copy link
Contributor

JoostK commented Jul 12, 2022

I suspect this is caused by #42835. Looking at the change, it introduces a call to checkTruthinessExpression in checkTestingKnownTruthyCallableOrAwaitableType, triggering checkExpression for 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
Copy link
Contributor

JoostK commented Jul 12, 2022

I opened #49881 with a fix.

@fatcerberus
Copy link

That arr.length repro looks bonkers - what real code pattern does it correspond to? I couldn’t see ever nesting an expression that deep.

@JoostK
Copy link
Contributor

JoostK commented Jul 12, 2022

That arr.length repro looks bonkers - what real code pattern does it correspond to? I couldn’t see ever nesting an expression that deep.

Generated code may look like this.

@imirkin
Copy link
Author

imirkin commented Jul 12, 2022

In reality, they're different arrays, and the expression isn't nested but rather a simple a && b && c sort 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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants