From 319f2f49087566426125159ae3ab07b1c7c0beeb Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 17 Oct 2026 10:00:54 -0700 Subject: [PATCH 1/2] Skip assignments to other names when narrowing identifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Narrowing an identifier walks back through every assignment on its flow path. In long straight-line functions each reference walks past every earlier declaration, so the walk length grows with the function and most of the time goes to cache misses loading each declaration and its name, only to find a different name. The binder now records a hash of the identifier each assignment flow node can target, in what was padding in FlowNode. Walks for an identifier skip assignments whose recorded name differs without touching their syntax. Assignments that could matter for any reference, like for-in variables, are recorded as unknown and still checked in full. --checkers 4, n=8: │ before │ after │ │ sec/op │ sec/op vs base │ Tsc/bluesky 2.794 ± 3% 2.757 ± 2% ~ (p=0.195 n=8) Tsc/sentry 11.02 ± 2% 11.17 ± 3% ~ (p=0.505 n=8) Tsc/t3code-server 13.10 ± 2% 12.04 ± 2% -8.14% (p=0.000 n=8) geomean 7.388 7.184 -2.77% │ before │ after │ │ rss-B │ rss-B vs base │ Tsc/bluesky 1.193Gi ± 2% 1.191Gi ± 1% ~ (p=0.574 n=8) Tsc/sentry 3.357Gi ± 4% 3.626Gi ± 8% ~ (p=0.105 n=8) Tsc/t3code-server 2.888Gi ± 3% 2.855Gi ± 3% -1.16% (p=0.038 n=8) geomean 2.262Gi 2.310Gi +2.14% │ before │ after │ │ heap-B │ heap-B vs base │ Tsc/bluesky 934.7Mi ± 0% 934.8Mi ± 0% ~ (p=0.645 n=8) Tsc/sentry 2.609Gi ± 0% 2.608Gi ± 0% ~ (p=0.130 n=8) Tsc/t3code-server 2.302Gi ± 0% 2.303Gi ± 0% ~ (p=0.878 n=8) geomean 1.763Gi 1.763Gi -0.02% --- tsc/internal/ast/flow.go | 72 ++++++++++++++++++++++++++++++++--- tsc/internal/binder/binder.go | 3 ++ tsc/internal/checker/flow.go | 10 +++++ 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/tsc/internal/ast/flow.go b/tsc/internal/ast/flow.go index 43a4bc2f0dfea..02ccafa16ca27 100644 --- a/tsc/internal/ast/flow.go +++ b/tsc/internal/ast/flow.go @@ -1,6 +1,9 @@ package ast -import "unsafe" +import ( + "hash/maphash" + "unsafe" +) // FlowFlags @@ -27,10 +30,69 @@ const ( // FlowNode type FlowNode struct { - Flags FlowFlags - Node *Node // Associated AST node - Antecedent *FlowNode // Antecedent for all but FlowLabel - Antecedents *FlowList // Linked list of antecedents for FlowLabel + Flags FlowFlags + // For assignment nodes, describes the identifier the assignment may target + // (see FlowAssignmentTargetHash). Fits in padding after Flags. + AssignmentTarget uint32 + Node *Node // Associated AST node + Antecedent *FlowNode // Antecedent for all but FlowLabel + Antecedents *FlowList // Linked list of antecedents for FlowLabel +} + +const ( + // The assignment must be checked against every reference. + FlowAssignmentTargetUnknown uint32 = 0 + // The assignment cannot target an identifier reference. + FlowAssignmentTargetNotIdentifier uint32 = 1 +) + +var flowIdentifierHashSeed = maphash.MakeSeed() + +// FlowIdentifierHash hashes an identifier name for comparison with +// FlowNode.AssignmentTarget. The result is never one of the reserved values. +func FlowIdentifierHash(name string) uint32 { + h := uint32(maphash.String(flowIdentifierHashSeed, name)) + if h <= FlowAssignmentTargetNotIdentifier { + h += 2 + } + return h +} + +// FlowAssignmentTargetHash summarizes which identifier references an assignment +// flow node can affect, so that walks for an identifier can skip assignments to +// other names without visiting their syntax. It mirrors the checker's matching of +// identifier references against assignment targets. +func FlowAssignmentTargetHash(target *Node) uint32 { + for { + switch target.Kind { + case KindParenthesizedExpression, KindNonNullExpression: + target = target.Expression() + continue + case KindBinaryExpression: + bin := target.AsBinaryExpression() + switch { + case IsAssignmentExpression(target, false): + target = bin.Left + continue + case bin.OperatorToken.Kind == KindCommaToken: + target = bin.Right + continue + } + return FlowAssignmentTargetNotIdentifier + case KindIdentifier: + return FlowIdentifierHash(target.AsIdentifier().Text) + case KindVariableDeclaration, KindBindingElement: + // A for-in variable also narrows the iterated expression. + if IsVariableDeclaration(target) && target.Parent != nil && target.Parent.Parent != nil && IsForInStatement(target.Parent.Parent) { + return FlowAssignmentTargetUnknown + } + if name := target.Name(); name != nil && IsIdentifier(name) { + return FlowIdentifierHash(name.AsIdentifier().Text) + } + return FlowAssignmentTargetNotIdentifier + } + return FlowAssignmentTargetNotIdentifier + } } type FlowList struct { diff --git a/tsc/internal/binder/binder.go b/tsc/internal/binder/binder.go index a95bb19ecdff7..2eb5a9db6f60a 100644 --- a/tsc/internal/binder/binder.go +++ b/tsc/internal/binder/binder.go @@ -500,6 +500,9 @@ func (b *Binder) createFlowMutation(flags ast.FlowFlags, antecedent *ast.FlowNod setFlowNodeReferenced(antecedent) b.hasFlowEffects = true result := b.newFlowNodeEx(flags, node, antecedent) + if flags&ast.FlowFlagsAssignment != 0 { + result.AssignmentTarget = ast.FlowAssignmentTargetHash(node) + } if b.currentExceptionTarget != nil { b.addAntecedent(b.currentExceptionTarget, result) } diff --git a/tsc/internal/checker/flow.go b/tsc/internal/checker/flow.go index d2831231772fe..928915f266bbc 100644 --- a/tsc/internal/checker/flow.go +++ b/tsc/internal/checker/flow.go @@ -43,6 +43,7 @@ type FlowState struct { initialType *Type flowContainer *ast.Node refKey CacheHashKey + refNameHash uint32 // FlowIdentifierHash of an identifier reference, else 0 depth int sharedFlowStart int reduceLabels []*ast.FlowReduceLabelData @@ -94,6 +95,10 @@ func (c *Checker) getFlowTypeOfReferenceEx(reference *ast.Node, declaredType *Ty f.initialType = core.Coalesce(initialType, declaredType) f.flowContainer = flowContainer f.sharedFlowStart = len(c.sharedFlows) + f.refNameHash = 0 + if ast.IsIdentifier(reference) && !ast.IsThisInTypeQuery(reference) { + f.refNameHash = ast.FlowIdentifierHash(reference.Text()) + } c.flowInvocationCount++ evolvedType := c.getTypeAtFlowNode(f, flowNode).t c.sharedFlows = c.sharedFlows[:f.sharedFlowStart] @@ -144,6 +149,11 @@ func (c *Checker) getTypeAtFlowNode(f *FlowState, flow *ast.FlowNode) FlowType { var t FlowType switch { case flags&ast.FlowFlagsAssignment != 0: + if f.refNameHash != 0 && flow.AssignmentTarget != ast.FlowAssignmentTargetUnknown && flow.AssignmentTarget != f.refNameHash { + // The assignment targets a different name, so it cannot affect this reference. + flow = flow.Antecedent + continue + } t = c.getTypeAtFlowAssignment(f, flow) if t.isNil() { flow = flow.Antecedent From d7c8ce4005714101d68764a58deb4078fb1d01c8 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 17 Oct 2026 10:14:28 -0700 Subject: [PATCH 2/2] Skip unrelated assignments when narrowing property accesses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Narrowing a property access like x.y walks the same long assignment chains as narrowing an identifier, and was still visiting the syntax of every assignment on the way. Such a reference can only be affected by assignments to x itself or to access chains rooted at x. Record the root identifier of access chain assignment targets as well, marked so that identifier references still ignore them, and let walks for access chains rooted at an identifier skip assignments rooted at any other name. --checkers 4, n=6: │ before │ after │ │ sec/op │ sec/op vs base │ Tsc/bluesky 2.750 ± 6% 2.814 ± 4% ~ (p=0.084 n=6) Tsc/sentry 11.28 ± 2% 11.34 ± 2% ~ (p=0.818 n=6) Tsc/t3code-server 12.19 ± 9% 11.01 ± 4% -9.71% (p=0.002 n=6) Tsc/vscode 12.93 ± 5% 12.90 ± 1% ~ (p=0.589 n=6) geomean 8.363 8.203 -1.91% │ before │ after │ │ rss-B │ rss-B vs base │ Tsc/bluesky 1.197Gi ± 2% 1.200Gi ± 2% ~ (p=0.818 n=6) Tsc/sentry 3.527Gi ± 5% 3.512Gi ± 5% ~ (p=0.937 n=6) Tsc/t3code-server 2.859Gi ± 3% 2.840Gi ± 2% ~ (p=0.818 n=6) Tsc/vscode 4.826Gi ± 3% 4.969Gi ± 3% +2.96% (p=0.026 n=6) geomean 2.763Gi 2.777Gi +0.51% │ before │ after │ │ heap-B │ heap-B vs base │ Tsc/bluesky 934.7Mi ± 0% 934.7Mi ± 0% ~ (p=0.937 n=6) Tsc/sentry 2.607Gi ± 0% 2.608Gi ± 0% ~ (p=0.937 n=6) Tsc/t3code-server 2.303Gi ± 0% 2.303Gi ± 0% ~ (p=0.589 n=6) Tsc/vscode 4.104Gi ± 0% 4.104Gi ± 0% ~ (p=0.589 n=6) geomean 2.178Gi 2.178Gi +0.01% --- tsc/internal/ast/flow.go | 29 +++++++++++++++++++++------ tsc/internal/checker/flow.go | 38 +++++++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/tsc/internal/ast/flow.go b/tsc/internal/ast/flow.go index 02ccafa16ca27..739876adc1b38 100644 --- a/tsc/internal/ast/flow.go +++ b/tsc/internal/ast/flow.go @@ -46,28 +46,42 @@ const ( FlowAssignmentTargetNotIdentifier uint32 = 1 ) +// FlowAssignmentTargetAccess marks an AssignmentTarget whose target is a property +// or element access chain rooted at the hashed identifier. +const FlowAssignmentTargetAccess uint32 = 1 << 31 + var flowIdentifierHashSeed = maphash.MakeSeed() // FlowIdentifierHash hashes an identifier name for comparison with -// FlowNode.AssignmentTarget. The result is never one of the reserved values. +// FlowNode.AssignmentTarget. The result is never a reserved value and never +// has FlowAssignmentTargetAccess set. func FlowIdentifierHash(name string) uint32 { h := uint32(maphash.String(flowIdentifierHashSeed, name)) + h &^= FlowAssignmentTargetAccess if h <= FlowAssignmentTargetNotIdentifier { h += 2 } return h } -// FlowAssignmentTargetHash summarizes which identifier references an assignment -// flow node can affect, so that walks for an identifier can skip assignments to -// other names without visiting their syntax. It mirrors the checker's matching of -// identifier references against assignment targets. +// FlowAssignmentTargetHash summarizes which references an assignment flow node +// can affect, so that flow walks can skip assignments to unrelated names without +// visiting their syntax. An identifier or declaration target records the hash of +// its name. An access chain target records the hash of the identifier it is +// rooted at, with FlowAssignmentTargetAccess set. This mirrors the checker's +// matching of references against assignment targets, which only matches +// references rooted at the same identifier. func FlowAssignmentTargetHash(target *Node) uint32 { + access := uint32(0) for { switch target.Kind { case KindParenthesizedExpression, KindNonNullExpression: target = target.Expression() continue + case KindPropertyAccessExpression, KindElementAccessExpression: + access = FlowAssignmentTargetAccess + target = target.Expression() + continue case KindBinaryExpression: bin := target.AsBinaryExpression() switch { @@ -80,8 +94,11 @@ func FlowAssignmentTargetHash(target *Node) uint32 { } return FlowAssignmentTargetNotIdentifier case KindIdentifier: - return FlowIdentifierHash(target.AsIdentifier().Text) + return FlowIdentifierHash(target.AsIdentifier().Text) | access case KindVariableDeclaration, KindBindingElement: + if access != 0 { + return FlowAssignmentTargetNotIdentifier + } // A for-in variable also narrows the iterated expression. if IsVariableDeclaration(target) && target.Parent != nil && target.Parent.Parent != nil && IsForInStatement(target.Parent.Parent) { return FlowAssignmentTargetUnknown diff --git a/tsc/internal/checker/flow.go b/tsc/internal/checker/flow.go index 928915f266bbc..f0d4433161442 100644 --- a/tsc/internal/checker/flow.go +++ b/tsc/internal/checker/flow.go @@ -43,7 +43,8 @@ type FlowState struct { initialType *Type flowContainer *ast.Node refKey CacheHashKey - refNameHash uint32 // FlowIdentifierHash of an identifier reference, else 0 + refNameHash uint32 // FlowIdentifierHash of the identifier a reference is rooted at, else 0 + refIsAccess bool // Whether the reference is an access chain rooted at that identifier depth int sharedFlowStart int reduceLabels []*ast.FlowReduceLabelData @@ -95,10 +96,7 @@ func (c *Checker) getFlowTypeOfReferenceEx(reference *ast.Node, declaredType *Ty f.initialType = core.Coalesce(initialType, declaredType) f.flowContainer = flowContainer f.sharedFlowStart = len(c.sharedFlows) - f.refNameHash = 0 - if ast.IsIdentifier(reference) && !ast.IsThisInTypeQuery(reference) { - f.refNameHash = ast.FlowIdentifierHash(reference.Text()) - } + f.refNameHash, f.refIsAccess = getFlowReferenceRootHash(reference) c.flowInvocationCount++ evolvedType := c.getTypeAtFlowNode(f, flowNode).t c.sharedFlows = c.sharedFlows[:f.sharedFlowStart] @@ -119,6 +117,31 @@ func (c *Checker) getFlowTypeOfReferenceEx(reference *ast.Node, declaredType *Ty return resultType } +// getFlowReferenceRootHash returns the FlowIdentifierHash of the identifier a reference +// is rooted at, and whether the reference is an access chain on it. An identifier is +// only affected by assignments to that identifier. An access chain is also affected by +// assignments to access chains rooted at it. Other references return zero and are +// matched against every assignment. +func getFlowReferenceRootHash(reference *ast.Node) (uint32, bool) { + isAccess := false + for { + switch reference.Kind { + case ast.KindIdentifier: + if ast.IsThisInTypeQuery(reference) { + return 0, false + } + return ast.FlowIdentifierHash(reference.Text()), isAccess + case ast.KindPropertyAccessExpression, ast.KindElementAccessExpression: + isAccess = true + reference = reference.Expression() + case ast.KindParenthesizedExpression, ast.KindNonNullExpression, ast.KindSatisfiesExpression: + reference = reference.Expression() + default: + return 0, false + } + } +} + func (c *Checker) getTypeAtFlowNode(f *FlowState, flow *ast.FlowNode) FlowType { if f.depth == 2000 { // We have made 2000 recursive invocations. To avoid overflowing the call stack we report an error @@ -149,8 +172,9 @@ func (c *Checker) getTypeAtFlowNode(f *FlowState, flow *ast.FlowNode) FlowType { var t FlowType switch { case flags&ast.FlowFlagsAssignment != 0: - if f.refNameHash != 0 && flow.AssignmentTarget != ast.FlowAssignmentTargetUnknown && flow.AssignmentTarget != f.refNameHash { - // The assignment targets a different name, so it cannot affect this reference. + if f.refNameHash != 0 && flow.AssignmentTarget != ast.FlowAssignmentTargetUnknown && + (flow.AssignmentTarget != f.refNameHash && (!f.refIsAccess || flow.AssignmentTarget != f.refNameHash|ast.FlowAssignmentTargetAccess)) { + // The assignment is rooted at a different name, so it cannot affect this reference. flow = flow.Antecedent continue }