Skip to content

Commit d7c8ce4

Browse files
committed
Skip unrelated assignments when narrowing property accesses
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%
1 parent 319f2f4 commit d7c8ce4

2 files changed

Lines changed: 54 additions & 13 deletions

File tree

‎tsc/internal/ast/flow.go‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,28 +46,42 @@ const (
4646
FlowAssignmentTargetNotIdentifier uint32 = 1
4747
)
4848

49+
// FlowAssignmentTargetAccess marks an AssignmentTarget whose target is a property
50+
// or element access chain rooted at the hashed identifier.
51+
const FlowAssignmentTargetAccess uint32 = 1 << 31
52+
4953
var flowIdentifierHashSeed = maphash.MakeSeed()
5054

5155
// FlowIdentifierHash hashes an identifier name for comparison with
52-
// FlowNode.AssignmentTarget. The result is never one of the reserved values.
56+
// FlowNode.AssignmentTarget. The result is never a reserved value and never
57+
// has FlowAssignmentTargetAccess set.
5358
func FlowIdentifierHash(name string) uint32 {
5459
h := uint32(maphash.String(flowIdentifierHashSeed, name))
60+
h &^= FlowAssignmentTargetAccess
5561
if h <= FlowAssignmentTargetNotIdentifier {
5662
h += 2
5763
}
5864
return h
5965
}
6066

61-
// FlowAssignmentTargetHash summarizes which identifier references an assignment
62-
// flow node can affect, so that walks for an identifier can skip assignments to
63-
// other names without visiting their syntax. It mirrors the checker's matching of
64-
// identifier references against assignment targets.
67+
// FlowAssignmentTargetHash summarizes which references an assignment flow node
68+
// can affect, so that flow walks can skip assignments to unrelated names without
69+
// visiting their syntax. An identifier or declaration target records the hash of
70+
// its name. An access chain target records the hash of the identifier it is
71+
// rooted at, with FlowAssignmentTargetAccess set. This mirrors the checker's
72+
// matching of references against assignment targets, which only matches
73+
// references rooted at the same identifier.
6574
func FlowAssignmentTargetHash(target *Node) uint32 {
75+
access := uint32(0)
6676
for {
6777
switch target.Kind {
6878
case KindParenthesizedExpression, KindNonNullExpression:
6979
target = target.Expression()
7080
continue
81+
case KindPropertyAccessExpression, KindElementAccessExpression:
82+
access = FlowAssignmentTargetAccess
83+
target = target.Expression()
84+
continue
7185
case KindBinaryExpression:
7286
bin := target.AsBinaryExpression()
7387
switch {
@@ -80,8 +94,11 @@ func FlowAssignmentTargetHash(target *Node) uint32 {
8094
}
8195
return FlowAssignmentTargetNotIdentifier
8296
case KindIdentifier:
83-
return FlowIdentifierHash(target.AsIdentifier().Text)
97+
return FlowIdentifierHash(target.AsIdentifier().Text) | access
8498
case KindVariableDeclaration, KindBindingElement:
99+
if access != 0 {
100+
return FlowAssignmentTargetNotIdentifier
101+
}
85102
// A for-in variable also narrows the iterated expression.
86103
if IsVariableDeclaration(target) && target.Parent != nil && target.Parent.Parent != nil && IsForInStatement(target.Parent.Parent) {
87104
return FlowAssignmentTargetUnknown

‎tsc/internal/checker/flow.go‎

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ type FlowState struct {
4343
initialType *Type
4444
flowContainer *ast.Node
4545
refKey CacheHashKey
46-
refNameHash uint32 // FlowIdentifierHash of an identifier reference, else 0
46+
refNameHash uint32 // FlowIdentifierHash of the identifier a reference is rooted at, else 0
47+
refIsAccess bool // Whether the reference is an access chain rooted at that identifier
4748
depth int
4849
sharedFlowStart int
4950
reduceLabels []*ast.FlowReduceLabelData
@@ -95,10 +96,7 @@ func (c *Checker) getFlowTypeOfReferenceEx(reference *ast.Node, declaredType *Ty
9596
f.initialType = core.Coalesce(initialType, declaredType)
9697
f.flowContainer = flowContainer
9798
f.sharedFlowStart = len(c.sharedFlows)
98-
f.refNameHash = 0
99-
if ast.IsIdentifier(reference) && !ast.IsThisInTypeQuery(reference) {
100-
f.refNameHash = ast.FlowIdentifierHash(reference.Text())
101-
}
99+
f.refNameHash, f.refIsAccess = getFlowReferenceRootHash(reference)
102100
c.flowInvocationCount++
103101
evolvedType := c.getTypeAtFlowNode(f, flowNode).t
104102
c.sharedFlows = c.sharedFlows[:f.sharedFlowStart]
@@ -119,6 +117,31 @@ func (c *Checker) getFlowTypeOfReferenceEx(reference *ast.Node, declaredType *Ty
119117
return resultType
120118
}
121119

120+
// getFlowReferenceRootHash returns the FlowIdentifierHash of the identifier a reference
121+
// is rooted at, and whether the reference is an access chain on it. An identifier is
122+
// only affected by assignments to that identifier. An access chain is also affected by
123+
// assignments to access chains rooted at it. Other references return zero and are
124+
// matched against every assignment.
125+
func getFlowReferenceRootHash(reference *ast.Node) (uint32, bool) {
126+
isAccess := false
127+
for {
128+
switch reference.Kind {
129+
case ast.KindIdentifier:
130+
if ast.IsThisInTypeQuery(reference) {
131+
return 0, false
132+
}
133+
return ast.FlowIdentifierHash(reference.Text()), isAccess
134+
case ast.KindPropertyAccessExpression, ast.KindElementAccessExpression:
135+
isAccess = true
136+
reference = reference.Expression()
137+
case ast.KindParenthesizedExpression, ast.KindNonNullExpression, ast.KindSatisfiesExpression:
138+
reference = reference.Expression()
139+
default:
140+
return 0, false
141+
}
142+
}
143+
}
144+
122145
func (c *Checker) getTypeAtFlowNode(f *FlowState, flow *ast.FlowNode) FlowType {
123146
if f.depth == 2000 {
124147
// 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 {
149172
var t FlowType
150173
switch {
151174
case flags&ast.FlowFlagsAssignment != 0:
152-
if f.refNameHash != 0 && flow.AssignmentTarget != ast.FlowAssignmentTargetUnknown && flow.AssignmentTarget != f.refNameHash {
153-
// The assignment targets a different name, so it cannot affect this reference.
175+
if f.refNameHash != 0 && flow.AssignmentTarget != ast.FlowAssignmentTargetUnknown &&
176+
(flow.AssignmentTarget != f.refNameHash && (!f.refIsAccess || flow.AssignmentTarget != f.refNameHash|ast.FlowAssignmentTargetAccess)) {
177+
// The assignment is rooted at a different name, so it cannot affect this reference.
154178
flow = flow.Antecedent
155179
continue
156180
}

0 commit comments

Comments
 (0)