Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,10 @@ type Checker struct {
SignatureCount uint32
TotalInstantiationCount uint32
instantiationCount uint32
instantiationCountResets uint32
diagnosticAdds uint32
transientReads uint32
inferMemo inferMemo
instantiationStack []*Type
conditionalConstraintDepth uint32
inlineLevel int
Expand Down Expand Up @@ -2311,6 +2315,7 @@ func (c *Checker) checkSourceElement(node *ast.Node) bool {
saveWithinUnreachableCode := c.withinUnreachableCode
c.currentNode = node
c.instantiationCount = 0
c.instantiationCountResets++
c.checkSourceElementWorker(node)
c.currentNode = saveCurrentNode
c.withinUnreachableCode = saveWithinUnreachableCode
Expand Down Expand Up @@ -2574,6 +2579,7 @@ func (c *Checker) checkDeferredNode(node *ast.Node) {
saveCurrentNode := c.currentNode
c.currentNode = node
c.instantiationCount = 0
c.instantiationCountResets++
switch node.Kind {
case ast.KindCallExpression, ast.KindNewExpression, ast.KindTaggedTemplateExpression, ast.KindDecorator, ast.KindJsxOpeningElement:
// These node kinds are deferred checked when overload resolution fails. To save on work,
Expand Down Expand Up @@ -7540,6 +7546,7 @@ func (c *Checker) getTypeOfExpression(node *ast.Node) *Type {
}
// If a type has been cached for the node, return it.
if cachedType := c.flowTypeCache[node]; cachedType != nil {
c.noteTransientRead()
return cachedType
}
startInvocationCount := c.flowInvocationCount
Expand Down Expand Up @@ -7759,6 +7766,7 @@ func (c *Checker) checkExpressionEx(node *ast.Node, checkMode CheckMode) *Type {
saveCurrentNode := c.currentNode
c.currentNode = node
c.instantiationCount = 0
c.instantiationCountResets++
uninstantiatedType := c.checkExpressionWorker(node, checkMode)
t := c.instantiateTypeWithSingleGenericCallSignature(node, uninstantiatedType, checkMode)
if isConstEnumObjectType(t) {
Expand Down Expand Up @@ -8618,6 +8626,9 @@ func (c *Checker) getResolvedSignature(node *ast.Node, candidatesOutArray *[]*Si
if cached != nil && cached != c.resolvingSignature && candidatesOutArray == nil {
return cached
}
if cached == c.resolvingSignature {
c.noteTransientRead()
}
saveResolutionStart := c.resolutionStart
if cached == nil {
// If we haven't already done so, temporarily reset the resolution stack. This allows us to
Expand Down Expand Up @@ -14241,6 +14252,7 @@ func (c *Checker) GetGlobalDiagnostics() []*ast.Diagnostic {
}

func (c *Checker) addDeferredDiagnostic(callback func()) {
c.diagnosticAdds++
c.deferredDiagnosticCallbacks = append(c.deferredDiagnosticCallbacks, callback)
}

Expand All @@ -14253,6 +14265,7 @@ func (c *Checker) produceDeferredDiagnostics() {

func (c *Checker) addDiagnostic(diagnostic *ast.Diagnostic) *ast.Diagnostic {
// Discard diagnostics created while at the maximum number of recursive TypeToString invocations.
c.diagnosticAdds++
if c.serializationLevel < maxSerializationLevel {
return c.diagnostics.Add(diagnostic)
}
Expand All @@ -14261,6 +14274,7 @@ func (c *Checker) addDiagnostic(diagnostic *ast.Diagnostic) *ast.Diagnostic {

func (c *Checker) addSuggestionDiagnostic(diagnostic *ast.Diagnostic) *ast.Diagnostic {
// Discard diagnostics created while at the maximum number of recursive TypeToString invocations.
c.diagnosticAdds++
if c.serializationLevel < maxSerializationLevel {
return c.suggestionDiagnostics.Add(diagnostic)
}
Expand Down Expand Up @@ -19223,6 +19237,7 @@ func (c *Checker) findResolutionCycleStartIndex(target TypeSystemEntity, propert
return -1
}
if resolution.target == target && resolution.propertyName == propertyName {
c.noteTransientRead()
return i
}
}
Expand Down Expand Up @@ -30428,6 +30443,7 @@ func (c *Checker) getContextualTypeForArgumentAtIndex(callTarget *ast.Node, argI
// that could cause infinite recursion. Instead, return anySignature.
var signature *Signature
if c.signatureLinks.Get(callTarget).resolvedSignature == c.resolvingSignature {
c.noteTransientRead()
signature = c.resolvingSignature
} else {
signature = c.getResolvedSignature(callTarget, nil, CheckModeNormal)
Expand Down
8 changes: 7 additions & 1 deletion tsc/internal/checker/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func (c *Checker) getFlowTypeOfReference(reference *ast.Node, declaredType *Type

func (c *Checker) getFlowTypeOfReferenceEx(reference *ast.Node, declaredType *Type, initialType *Type, flowContainer *ast.Node, flowNode *ast.FlowNode) *Type {
if c.flowAnalysisDisabled {
c.noteTransientRead()
return c.errorType
}
if flowNode == nil {
Expand Down Expand Up @@ -122,6 +123,7 @@ func (c *Checker) getTypeAtFlowNode(f *FlowState, flow *ast.FlowNode) FlowType {
tr.Instant(tracing.PhaseCheckTypes, "getTypeAtFlowNode_DepthLimit", map[string]any{"depth": f.depth})
}
c.flowAnalysisDisabled = true
c.noteTransientRead()
c.reportFlowControlError(f.reference)
return FlowType{t: c.errorType}
}
Expand Down Expand Up @@ -383,7 +385,10 @@ func (c *Checker) narrowType(f *FlowState, t *Type, expr *ast.Node, assumeTrue b
case ast.KindIdentifier:
// When narrowing a reference to a const variable, non-assigned parameter, or readonly property, we inline
// up to five levels of aliased conditional expressions that are themselves declared as const variables.
if !c.isMatchingReference(f.reference, expr) && c.inlineLevel < 5 {
if c.inlineLevel >= 5 {
// Whether this inlines depends on how deep the enclosing inlining is.
c.noteTransientRead()
} else if !c.isMatchingReference(f.reference, expr) {
symbol := c.getResolvedSymbol(expr)
if c.isConstantVariable(symbol) {
declaration := symbol.ValueDeclaration()
Expand Down Expand Up @@ -1346,6 +1351,7 @@ func (c *Checker) getTypeAtFlowLoopLabel(f *FlowState, flow *ast.FlowNode) FlowT
// path that leads to the top.
for _, loopInfo := range c.flowLoopStack {
if loopInfo.key == key && len(loopInfo.types) != 0 {
c.noteTransientRead()
return c.newFlowType(c.getUnionOrEvolvingArrayType(f, loopInfo.types, UnionReductionLiteral), true /*incomplete*/)
}
}
Expand Down
3 changes: 2 additions & 1 deletion tsc/internal/checker/inference.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@ func (c *Checker) inferTypes(inferences []*InferenceInfo, originalSource *Type,
n.priority = priority
n.inferencePriority = InferencePriorityMaxValue
n.contravariant = contravariant
c.inferFromTypes(n, originalSource, originalTarget)
c.inferFromTypesMemo(n, originalSource, originalTarget)
c.putInferenceState(n)
}

func (c *Checker) inferFromTypes(n *InferenceState, source *Type, target *Type) {
c.inferMemo.steps++
if !c.couldContainTypeVariables(target) || c.isNoInferType(target) {
return
}
Expand Down
133 changes: 133 additions & 0 deletions tsc/internal/checker/infermemo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package checker

import (
"encoding/binary"
"slices"

"github.com/microsoft/TypeScript/tsc/internal/core"
)

// The inference walk memo.
//
// Some walks at the top of inferTypes are long and are repeated with the same inputs, for example
// inferring from a schema constraint type to the return type of a schema combinator, once per
// call to that combinator. When every inference info is still in its initial state, a walk's
// inputs are just the source, the target, the priority, the contravariance and the type
// parameters, and its outcome is the candidates, priority and topLevel it writes to each info.
//
// A walk's outcome is stored only when the walk computed nothing new and read no transient
// state: it created no type, symbol or signature, instantiated nothing, added no diagnostic,
// observed no in-progress resolution, signature resolution, flow loop or flow type cache entry,
// and did not start inside an instantiation or variance computation. Such a walk read only
// finished values besides its inputs, so the same inputs take the same path again and write
// the same outcome. Walks shorter than inferMemoMinSteps are cheaper to repeat than to store.

const inferMemoMinSteps = 16

type inferMemoOutcome struct {
candidates []*Type
contraCandidates []*Type
priority InferencePriority
topLevel bool
}

type inferMemo struct {
entries map[string][]inferMemoOutcome
key []byte
steps uint32
}

// The checker state a walk must leave alone to be stored.
type inferMemoEffects struct {
created uint32
instantiationCount uint32
totalInstantiationCount uint32
diagnostics uint32
transientReads uint32
instantiationCountReset uint32
}

func (c *Checker) inferMemoEffects() inferMemoEffects {
return inferMemoEffects{
created: c.TypeCount + c.SymbolCount + c.SignatureCount,
instantiationCount: c.instantiationCount,
totalInstantiationCount: c.TotalInstantiationCount,
diagnostics: c.diagnosticAdds,
transientReads: c.transientReads,
instantiationCountReset: c.instantiationCountResets,
}
}

// Records that the current computation observed state that is only valid for the moment, such
// as an in-progress resolution. Results computed with such reads must not be memoized.
func (c *Checker) noteTransientRead() {
c.transientReads++
}

func (c *Checker) inferFromTypesMemo(n *InferenceState, source *Type, target *Type) {
m := &c.inferMemo
// Relations made inside a variance computation report reliability to it.
if len(c.varianceStack) != 0 || c.skipDirectInferenceNodes.Len() != 0 ||
!core.Every(n.inferences, isInitialInferenceInfo) {
c.inferFromTypes(n, source, target)
return
}
m.key = binary.LittleEndian.AppendUint32(m.key[:0], uint32(source.id))
m.key = binary.LittleEndian.AppendUint32(m.key, uint32(target.id))
m.key = binary.LittleEndian.AppendUint32(m.key, uint32(n.priority))
if n.contravariant {
m.key = append(m.key, 1)
} else {
m.key = append(m.key, 0)
}
for _, info := range n.inferences {
m.key = binary.LittleEndian.AppendUint32(m.key, uint32(info.typeParameter.id))
}
if outcomes, ok := m.entries[string(m.key)]; ok {
changed := false
for i, info := range n.inferences {
outcome := &outcomes[i]
if outcome.candidates != nil || outcome.contraCandidates != nil ||
outcome.priority != info.priority || outcome.topLevel != info.topLevel {
changed = true
}
info.candidates = outcome.candidates
info.contraCandidates = outcome.contraCandidates
info.priority = outcome.priority
info.topLevel = outcome.topLevel
}
if changed {
clearCachedInferences(n.inferences)
}
return
}
// The walk may infer into nested calls that reuse the key buffer.
key := string(m.key)
// Inside an instantiation the walk may take cached instantiations without counting them,
// while the same walk elsewhere would make and count them.
inInstantiation := len(c.activeMappers) != 0
before := c.inferMemoEffects()
stepsBefore := m.steps
c.inferFromTypes(n, source, target)
if m.steps-stepsBefore < inferMemoMinSteps || inInstantiation || c.inferMemoEffects() != before {
return
}
outcomes := make([]inferMemoOutcome, len(n.inferences))
for i, info := range n.inferences {
outcomes[i] = inferMemoOutcome{
candidates: slices.Clip(slices.Clone(info.candidates)),
contraCandidates: slices.Clip(slices.Clone(info.contraCandidates)),
priority: info.priority,
topLevel: info.topLevel,
}
}
if m.entries == nil {
m.entries = make(map[string][]inferMemoOutcome)
}
m.entries[key] = outcomes
}

func isInitialInferenceInfo(info *InferenceInfo) bool {
return info.typeParameter != nil && info.candidates == nil && info.contraCandidates == nil && !info.isFixed &&
info.priority == InferencePriorityMaxValue && info.topLevel && info.impliedArity == -1
}
1 change: 1 addition & 0 deletions tsc/internal/checker/relater.go
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,7 @@ func (c *Checker) getVariancesWorker(symbol *ast.Symbol, typeParameters []*Type)
c.resolutionStart = saveResolutionStart
}
} else {
c.noteTransientRead()
// We've detected a circularity. Since we may compute different variances depending on where
// we enter a circularity, we find the generic type with the "smallest" symbol in the circular
// region of the variance stack and restart the computation from there if necessary. This
Expand Down
Loading