diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index e58e96048e94b..c5edb7727fc10 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -21893,24 +21893,35 @@ func (c *Checker) getPropertyOfUnionOrIntersectionType(t *Type, name string, ski // these partial properties when identifying discriminant properties, but otherwise they are filtered out // and do not appear to be present in the union type. func (c *Checker) getUnionOrIntersectionProperty(t *Type, name string, skipObjectFunctionPropertyAugment bool) *ast.Symbol { - var cache ast.SymbolTable + data := t.AsUnionOrIntersectionType() if skipObjectFunctionPropertyAugment { - cache = ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCacheWithoutFunctionPropertyAugment) + if prop := data.propertyCacheWithoutFunctionPropertyAugment[name]; prop != nil { + return prop + } } else { - cache = ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCache) - } - if prop := cache[name]; prop != nil { - return prop + if data.propertyCache != nil { + if prop := data.propertyCache[name]; prop != nil { + return prop + } + } + // Complete non-augmented properties are also valid for augmented + // lookup. Share the entry instead of retaining it in both tables. + if prop := data.propertyCacheWithoutFunctionPropertyAugment[name]; prop != nil && prop.CheckFlags()&ast.CheckFlagsPartial == 0 { + return prop + } } prop := c.createUnionOrIntersectionProperty(t, name, skipObjectFunctionPropertyAugment) if prop != nil { - cache[name] = prop - // Propagate an entry from the non-augmented cache to the augmented cache unless the property is partial. - if skipObjectFunctionPropertyAugment && prop.CheckFlags()&ast.CheckFlagsPartial == 0 { - augmentedCache := ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCache) - if augmentedCache[name] == nil { - augmentedCache[name] = prop + if skipObjectFunctionPropertyAugment { + ast.GetSymbolTable(&data.propertyCacheWithoutFunctionPropertyAugment)[name] = prop + if prop.CheckFlags()&ast.CheckFlagsPartial == 0 && data.propertyCache[name] == prop { + delete(data.propertyCache, name) + if len(data.propertyCache) == 0 { + data.propertyCache = nil + } } + } else { + ast.GetSymbolTable(&data.propertyCache)[name] = prop } } return prop diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index a3cfe28c2e735..f2352544ba52a 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -1362,7 +1362,7 @@ type EvolvingArrayType struct { type UnionOrIntersectionType struct { StructuredType types []*Type - propertyCache ast.SymbolTable + propertyCache ast.SymbolTable // Augmented entries not shared with the non-augmented cache. propertyCacheWithoutFunctionPropertyAugment ast.SymbolTable resolvedProperties []*ast.Symbol } diff --git a/tsc/internal/checker/types_test.go b/tsc/internal/checker/types_test.go index 86de2b089e06e..4856d3bf66d64 100644 --- a/tsc/internal/checker/types_test.go +++ b/tsc/internal/checker/types_test.go @@ -507,6 +507,84 @@ func TestCompactValueSymbolLinksLayout(t *testing.T) { assert.Equal(t, links.getContainingType(), types[2]) } +func TestUnionIntersectionPropertyCacheSharing(t *testing.T) { + t.Parallel() + for _, intersection := range []bool{false, true} { + for _, augmentedFirst := range []bool{false, true} { + t.Run(strconv.FormatBool(intersection)+"/"+strconv.FormatBool(augmentedFirst), func(t *testing.T) { + t.Parallel() + c, _ := newCompositeTestChecker() + members := make(ast.SymbolTable) + for i := range 300 { + name := strconv.Itoa(i) + members[name] = c.newSymbol(ast.SymbolFlagsProperty, name) + } + members[""] = c.newSymbol(ast.SymbolFlagsProperty, "") + object := c.newAnonymousType(nil, members, nil, nil, nil) + var typ *Type + if intersection { + typ = c.newIntersectionType(ObjectFlagsNone, []*Type{object, object}) + } else { + typ = c.newUnionType(ObjectFlagsNone, []*Type{object, object}) + } + data := typ.AsUnionOrIntersectionType() + for name, symbol := range members { + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, !augmentedFirst), symbol) + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, augmentedFirst), symbol) + } + assert.Equal(t, len(data.propertyCacheWithoutFunctionPropertyAugment), len(members)) + assert.Assert(t, data.propertyCache == nil) + runtime.GC() + for name, symbol := range members { + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, true), symbol) + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, false), symbol) + } + runtime.KeepAlive(c) + }) + } + } +} + +func TestUnionIntersectionPropertyCacheModes(t *testing.T) { + t.Parallel() + for _, intersection := range []bool{false, true} { + t.Run(strconv.FormatBool(intersection), func(t *testing.T) { + t.Parallel() + c, _ := newCompositeTestChecker() + var typ *Type + if intersection { + typ = c.newIntersectionType(ObjectFlagsNone, nil) + } else { + typ = c.newUnionType(ObjectFlagsNone, nil) + } + data := typ.AsUnionOrIntersectionType() + assert.Assert(t, c.getUnionOrIntersectionProperty(typ, "missing", true) == nil) + assert.Assert(t, c.getUnionOrIntersectionProperty(typ, "missing", false) == nil) + assert.Assert(t, data.propertyCache == nil) + assert.Assert(t, data.propertyCacheWithoutFunctionPropertyAugment == nil) + data.propertyCacheWithoutFunctionPropertyAugment = make(ast.SymbolTable) + data.propertyCache = make(ast.SymbolTable) + for _, flags := range []ast.CheckFlags{ast.CheckFlagsReadPartial, ast.CheckFlagsWritePartial} { + name := strconv.Itoa(int(flags)) + partial := c.newSymbolEx(ast.SymbolFlagsProperty, name, flags) + complete := c.newSymbol(ast.SymbolFlagsProperty, name) + data.propertyCacheWithoutFunctionPropertyAugment[name] = partial + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, true), partial) + assert.Assert(t, c.getUnionOrIntersectionProperty(typ, name, false) == nil) + assert.Equal(t, c.getPropertyOfUnionOrIntersectionType(typ, name, true) == nil, flags&ast.CheckFlagsReadPartial != 0) + data.propertyCache[name] = complete + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, false), complete) + } + nonAugmented := c.newSymbol(ast.SymbolFlagsProperty, "different") + augmented := c.newSymbol(ast.SymbolFlagsProperty, "different") + data.propertyCacheWithoutFunctionPropertyAugment["different"] = nonAugmented + data.propertyCache["different"] = augmented + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, "different", true), nonAugmented) + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, "different", false), augmented) + }) + } +} + func TestCompactStructuredTypeLayout(t *testing.T) { t.Parallel() assert.Equal(t, unsafe.Sizeof(StructuredType{}), unsafe.Sizeof(ConstrainedType{})+5*unsafe.Sizeof(uintptr(0)))