diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 4c57dd37910f2..d690aee14d66e 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -599,6 +599,10 @@ type Checker struct { TotalInstantiationCount uint32 instantiationCount uint32 memberLayoutCount uint32 + instantiationCountResets uint32 + diagnosticAdds uint32 + transientReads uint32 + inferMemo inferMemo instantiationStack []*Type conditionalConstraintDepth uint32 inlineLevel int @@ -2316,6 +2320,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 @@ -2579,6 +2584,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, @@ -7545,6 +7551,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 @@ -7764,6 +7771,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) { @@ -8623,6 +8631,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 @@ -14246,6 +14257,7 @@ func (c *Checker) GetGlobalDiagnostics() []*ast.Diagnostic { } func (c *Checker) addDeferredDiagnostic(callback func()) { + c.diagnosticAdds++ c.deferredDiagnosticCallbacks = append(c.deferredDiagnosticCallbacks, callback) } @@ -14258,6 +14270,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) } @@ -14266,6 +14279,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) } @@ -19228,6 +19242,7 @@ func (c *Checker) findResolutionCycleStartIndex(target TypeSystemEntity, propert return -1 } if resolution.target == target && resolution.propertyName == propertyName { + c.noteTransientRead() return i } } @@ -30554,6 +30569,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) diff --git a/tsc/internal/checker/flow.go b/tsc/internal/checker/flow.go index d2831231772fe..e228e48849cf2 100644 --- a/tsc/internal/checker/flow.go +++ b/tsc/internal/checker/flow.go @@ -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 { @@ -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} } @@ -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() @@ -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*/) } } diff --git a/tsc/internal/checker/inference.go b/tsc/internal/checker/inference.go index 02c7d009b9ec5..ed69d9c3c1c50 100644 --- a/tsc/internal/checker/inference.go +++ b/tsc/internal/checker/inference.go @@ -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 } diff --git a/tsc/internal/checker/infermemo.go b/tsc/internal/checker/infermemo.go new file mode 100644 index 0000000000000..4f0d3cfd40f7c --- /dev/null +++ b/tsc/internal/checker/infermemo.go @@ -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 +} diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index 007fba2511a13..d8c595c725a7e 100644 --- a/tsc/internal/checker/relater.go +++ b/tsc/internal/checker/relater.go @@ -1410,6 +1410,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