diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index d2759e02c2bf9..93277337f31f2 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -1066,11 +1066,7 @@ func NewChecker(program Program, tracer *Tracer) (*Checker, *sync.Mutex) { c.flowLoopCache = make(map[FlowLoopKey]*Type) c.flowNodeReachable = make(map[*ast.FlowNode]bool) c.flowNodePostSuper = make(map[*ast.FlowNode]bool) - c.subtypeRelation = &Relation{} - c.strictSubtypeRelation = &Relation{} - c.assignableRelation = &Relation{} - c.comparableRelation = &Relation{} - c.identityRelation = &Relation{} + c.subtypeRelation, c.strictSubtypeRelation, c.assignableRelation, c.comparableRelation, c.identityRelation = newRelations() c.enumRelation = make(map[EnumRelationKey]RelationComparisonResult) c.moduleImportAttributesTypes = make(map[*ast.Symbol]*Type) c.getGlobalESSymbolType = c.getGlobalTypeResolver("Symbol", 0 /*arity*/, false /*reportErrors*/) diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index a9bc484fef1d2..0d33ff9e318d7 100644 --- a/tsc/internal/checker/relater.go +++ b/tsc/internal/checker/relater.go @@ -1,6 +1,7 @@ package checker import ( + "math/bits" "slices" "strconv" "strings" @@ -64,12 +65,18 @@ const ( type RelationComparisonResult uint32 const ( + relationComparisonResultSucceededBitShift = 0 + relationComparisonResultFailedBitShift = 1 + relationComparisonResultReportsUnmeasurableBitShift = 3 + relationComparisonResultReportsUnreliableBitShift = 4 + relationComparisonResultOverflowBitShift = 5 + RelationComparisonResultNone RelationComparisonResult = 0 - RelationComparisonResultSucceeded RelationComparisonResult = 1 << 0 - RelationComparisonResultFailed RelationComparisonResult = 1 << 1 - RelationComparisonResultReportsUnmeasurable RelationComparisonResult = 1 << 3 - RelationComparisonResultReportsUnreliable RelationComparisonResult = 1 << 4 - RelationComparisonResultComplexityOverflow RelationComparisonResult = 1 << 5 + RelationComparisonResultSucceeded RelationComparisonResult = 1 << relationComparisonResultSucceededBitShift + RelationComparisonResultFailed RelationComparisonResult = 1 << relationComparisonResultFailedBitShift + RelationComparisonResultReportsUnmeasurable RelationComparisonResult = 1 << relationComparisonResultReportsUnmeasurableBitShift + RelationComparisonResultReportsUnreliable RelationComparisonResult = 1 << relationComparisonResultReportsUnreliableBitShift + RelationComparisonResultComplexityOverflow RelationComparisonResult = 1 << relationComparisonResultOverflowBitShift RelationComparisonResultReportsMask = RelationComparisonResultReportsUnmeasurable | RelationComparisonResultReportsUnreliable RelationComparisonResultOverflow = RelationComparisonResultComplexityOverflow ) @@ -96,22 +103,99 @@ func asRecursionId[T *ast.Node | *ast.Symbol | *Type](value T) RecursionId { } type Relation struct { - results map[CacheHashKey]RelationComparisonResult + packedResults *packedRelationCache + packedShift uint8 +} + +type packedRelationCache struct { + results map[CacheHashKey]uint32 + counts [packedRelationCount]int +} + +func newRelations() (subtype *Relation, strictSubtype *Relation, assignable *Relation, comparableRelation *Relation, identity *Relation) { + packedResults := &packedRelationCache{} + identity = newRelation(packedResults, 0) + strictSubtype = newRelation(packedResults, 6) + subtype = newRelation(packedResults, 12) + assignable = newRelation(packedResults, 18) + comparableRelation = newRelation(packedResults, 24) + return subtype, strictSubtype, assignable, comparableRelation, identity +} + +func newRelation(packedResults *packedRelationCache, packedShift uint8) *Relation { + return &Relation{packedResults: packedResults, packedShift: packedShift} } func (r *Relation) get(key CacheHashKey) RelationComparisonResult { - return r.results[key] + packed := r.packedResults.results[key] + if packed == 0 { + return RelationComparisonResultNone + } + result := RelationComparisonResult(packed>>r.packedShift) & relationComparisonResultMask + if result != RelationComparisonResultNone { + return result + } + + // A failure in a laxer relation implies failure in this relation. Align each + // slot's failure and overflow bits with its respective slot marker, then keep + // failures that did not result from an overflow. The mask excludes this and + // all stricter relations. + laxerSlots := packedRelationSlotMask &^ (uint32(1)<<(r.packedShift+packedRelationResultBits) - 1) + if failedSlots := ((packed >> relationComparisonResultFailedBitShift) &^ (packed >> relationComparisonResultOverflowBitShift)) & laxerSlots; failedSlots != 0 { + // The lowest set marker is the nearest laxer relation with a usable failure. + shift := bits.TrailingZeros32(failedSlots) + result = RelationComparisonResult(packed>>shift) & relationComparisonResultMask + return r.setPacked(key, packed, result) + } + + // A success in a stricter relation implies success in this relation. Align + // each slot's success bit with its respective slot marker, then exclude this + // and all laxer relations. + stricterSlots := packedRelationSlotMask & (uint32(1)<> relationComparisonResultSucceededBitShift) & stricterSlots; succeededSlots != 0 { + // Prefer the strictest available success, matching the previous scan order. + shift := bits.TrailingZeros32(succeededSlots) + result = RelationComparisonResult(packed>>shift) & relationComparisonResultMask + return r.setPacked(key, packed, result) + } + return RelationComparisonResultNone } func (r *Relation) set(key CacheHashKey, result RelationComparisonResult) { - if r.results == nil { - r.results = make(map[CacheHashKey]RelationComparisonResult) + if r.packedResults.results == nil { + r.packedResults.results = make(map[CacheHashKey]uint32) } - r.results[key] = result + r.setPacked(key, r.packedResults.results[key], result) +} + +const relationComparisonResultMask = RelationComparisonResultSucceeded | + RelationComparisonResultFailed | + RelationComparisonResultReportsUnmeasurable | + RelationComparisonResultReportsUnreliable | + RelationComparisonResultComplexityOverflow + +// Each map value contains one six-bit result slot per relation, ordered from +// strictest to laxest: identity, strict subtype, subtype, assignable, comparable. +const ( + packedRelationResultBits = 6 + packedRelationCount = 5 + // Marks the low bit of every slot, which is also its success bit. + packedRelationSlotMask = 1 | 1<<6 | 1<<12 | 1<<18 | 1<<24 +) + +func (r *Relation) setPacked(key CacheHashKey, packed uint32, result RelationComparisonResult) RelationComparisonResult { + result &= relationComparisonResultMask + shiftedResult := uint32(result&relationComparisonResultMask) << r.packedShift + mask := uint32(relationComparisonResultMask) << r.packedShift + if packed&mask == 0 { + r.packedResults.counts[r.packedShift/packedRelationResultBits]++ + } + r.packedResults.results[key] = packed&^mask | shiftedResult + return result } func (r *Relation) size() int { - return len(r.results) + return r.packedResults.counts[r.packedShift/packedRelationResultBits] } func (c *Checker) isTypeIdenticalTo(source *Type, target *Type) bool { diff --git a/tsc/internal/checker/relater_test.go b/tsc/internal/checker/relater_test.go new file mode 100644 index 0000000000000..cdfa3fe224a38 --- /dev/null +++ b/tsc/internal/checker/relater_test.go @@ -0,0 +1,264 @@ +package checker + +import ( + "strconv" + "testing" + + "gotest.tools/v3/assert" +) + +func TestSubtypeRelationImplications(t *testing.T) { + t.Parallel() + + subtype, strictSubtype, _, _, _ := newRelations() + strictSuccessKey := CacheHashKey{Lo: 1} + strictFailureKey := CacheHashKey{Lo: 2} + subtypeSuccessKey := CacheHashKey{Lo: 3} + subtypeFailureKey := CacheHashKey{Lo: 4} + subtypeOverflowKey := CacheHashKey{Lo: 5} + + strictSubtype.set(strictSuccessKey, RelationComparisonResultSucceeded|RelationComparisonResultReportsUnreliable) + strictSubtype.set(strictFailureKey, RelationComparisonResultFailed) + subtype.set(subtypeSuccessKey, RelationComparisonResultSucceeded) + subtype.set(subtypeFailureKey, RelationComparisonResultFailed|RelationComparisonResultReportsUnmeasurable) + subtype.set(subtypeOverflowKey, RelationComparisonResultFailed|RelationComparisonResultComplexityOverflow) + + assert.Equal(t, subtype.get(strictSuccessKey), RelationComparisonResultSucceeded|RelationComparisonResultReportsUnreliable) + assert.Equal(t, subtype.get(strictFailureKey), RelationComparisonResultNone) + assert.Equal(t, strictSubtype.get(subtypeSuccessKey), RelationComparisonResultNone) + assert.Equal(t, strictSubtype.get(subtypeFailureKey), RelationComparisonResultFailed|RelationComparisonResultReportsUnmeasurable) + assert.Equal(t, strictSubtype.get(subtypeOverflowKey), RelationComparisonResultNone) + assert.Equal(t, subtype.size(), 4) + assert.Equal(t, strictSubtype.size(), 3) + assert.Equal(t, len(subtype.packedResults.results), 5) +} + +func TestSubtypeRelationLocalResultTakesPrecedence(t *testing.T) { + t.Parallel() + + subtype, strictSubtype, _, _, _ := newRelations() + key := CacheHashKey{Lo: 1} + + strictSubtype.set(key, RelationComparisonResultSucceeded) + subtype.set(key, RelationComparisonResultFailed) + + assert.Equal(t, subtype.get(key), RelationComparisonResultFailed) + assert.Equal(t, strictSubtype.get(key), RelationComparisonResultSucceeded) +} + +func TestPackedRelationImplicationsAreLazy(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + subtype, strictSubtype, assignable, comparableRelation, identity := newRelations() + key := CacheHashKey{Lo: 1} + result := RelationComparisonResultSucceeded | RelationComparisonResultReportsUnreliable + + identity.set(key, result) + assert.Equal(t, identity.size(), 1) + assert.Equal(t, strictSubtype.size(), 0) + assert.Equal(t, subtype.size(), 0) + assert.Equal(t, assignable.size(), 0) + assert.Equal(t, comparableRelation.size(), 0) + + assert.Equal(t, assignable.get(key), result) + assert.Equal(t, identity.size(), 1) + assert.Equal(t, strictSubtype.size(), 0) + assert.Equal(t, subtype.size(), 0) + assert.Equal(t, assignable.size(), 1) + assert.Equal(t, comparableRelation.size(), 0) + + assert.Equal(t, assignable.get(key), result) + assert.Equal(t, assignable.size(), 1) + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + subtype, strictSubtype, assignable, comparableRelation, identity := newRelations() + key := CacheHashKey{Lo: 1} + result := RelationComparisonResultFailed | RelationComparisonResultReportsUnmeasurable + + comparableRelation.set(key, result) + assert.Equal(t, identity.size(), 0) + assert.Equal(t, strictSubtype.size(), 0) + assert.Equal(t, subtype.size(), 0) + assert.Equal(t, assignable.size(), 0) + assert.Equal(t, comparableRelation.size(), 1) + + assert.Equal(t, strictSubtype.get(key), result) + assert.Equal(t, identity.size(), 0) + assert.Equal(t, strictSubtype.size(), 1) + assert.Equal(t, subtype.size(), 0) + assert.Equal(t, assignable.size(), 0) + assert.Equal(t, comparableRelation.size(), 1) + + assert.Equal(t, strictSubtype.get(key), result) + assert.Equal(t, strictSubtype.size(), 1) + }) +} + +func TestPackedRelationImplicationPrecedence(t *testing.T) { + t.Parallel() + + subtype, strictSubtype, _, comparableRelation, identity := newRelations() + failureKey := CacheHashKey{Lo: 1} + successKey := CacheHashKey{Lo: 2} + overflowKey := CacheHashKey{Lo: 3} + + subtype.set(failureKey, RelationComparisonResultFailed|RelationComparisonResultReportsUnreliable) + comparableRelation.set(failureKey, RelationComparisonResultFailed|RelationComparisonResultReportsUnmeasurable) + assert.Equal(t, strictSubtype.get(failureKey), RelationComparisonResultFailed|RelationComparisonResultReportsUnreliable) + + identity.set(successKey, RelationComparisonResultSucceeded|RelationComparisonResultReportsUnreliable) + subtype.set(successKey, RelationComparisonResultSucceeded|RelationComparisonResultReportsUnmeasurable) + assert.Equal(t, comparableRelation.get(successKey), RelationComparisonResultSucceeded|RelationComparisonResultReportsUnreliable) + + subtype.set(overflowKey, RelationComparisonResultFailed|RelationComparisonResultComplexityOverflow) + comparableRelation.set(overflowKey, RelationComparisonResultFailed|RelationComparisonResultReportsUnmeasurable) + assert.Equal(t, strictSubtype.get(overflowKey), RelationComparisonResultFailed|RelationComparisonResultReportsUnmeasurable) +} + +func TestIdentityRelationDoesNotCountDirectionalImplication(t *testing.T) { + t.Parallel() + + _, _, assignable, _, identity := newRelations() + lowerId := &Type{id: 1} + higherId := &Type{id: 2} + directionalKey, _ := getRelationKey(higherId, lowerId, IntersectionStateNone, false, false) + identityKey, _ := getRelationKey(higherId, lowerId, IntersectionStateNone, true, false) + + assert.Assert(t, directionalKey != identityKey) + assignable.set(directionalKey, RelationComparisonResultFailed) + + assert.Equal(t, identity.size(), 0) + assert.Equal(t, identity.get(identityKey), RelationComparisonResultNone) + assert.Equal(t, identity.size(), 0) +} + +func TestIndependentRelationCache(t *testing.T) { + t.Parallel() + + relation, _, _, _, _ := newRelations() + key := CacheHashKey{Lo: 1} + + relation.set(key, RelationComparisonResultSucceeded) + assert.Equal(t, relation.get(key), RelationComparisonResultSucceeded) + assert.Equal(t, relation.size(), 1) +} + +func TestPackedRelationSlots(t *testing.T) { + t.Parallel() + + subtype, strictSubtype, assignable, comparableRelation, identity := newRelations() + key := CacheHashKey{Lo: 1} + relations := []*Relation{subtype, strictSubtype, assignable, comparableRelation, identity} + results := []RelationComparisonResult{ + RelationComparisonResultSucceeded, + RelationComparisonResultFailed, + RelationComparisonResultSucceeded | RelationComparisonResultReportsUnmeasurable, + RelationComparisonResultFailed | RelationComparisonResultReportsUnreliable, + RelationComparisonResultFailed | RelationComparisonResultComplexityOverflow, + } + + for index, relation := range relations { + relation.set(key, results[index]) + } + for index, relation := range relations { + assert.Equal(t, relation.get(key), results[index]) + assert.Equal(t, relation.size(), 1) + } + assert.Equal(t, len(subtype.packedResults.results), 1) +} + +func TestPackedRelationImplications(t *testing.T) { + t.Parallel() + + for sourceRank := range packedRelationCount { + t.Run("success/"+strconv.Itoa(sourceRank), func(t *testing.T) { + t.Parallel() + + subtype, strictSubtype, assignable, comparableRelation, identity := newRelations() + relations := []*Relation{identity, strictSubtype, subtype, assignable, comparableRelation} + key := CacheHashKey{Lo: 1} + result := RelationComparisonResultSucceeded | RelationComparisonResultReportsUnreliable + relations[sourceRank].set(key, result) + for rank, relation := range relations { + expected := RelationComparisonResultNone + if rank >= sourceRank { + expected = result + } + assert.Equal(t, relation.get(key), expected) + } + }) + + t.Run("failure/"+strconv.Itoa(sourceRank), func(t *testing.T) { + t.Parallel() + + subtype, strictSubtype, assignable, comparableRelation, identity := newRelations() + relations := []*Relation{identity, strictSubtype, subtype, assignable, comparableRelation} + key := CacheHashKey{Lo: 1} + result := RelationComparisonResultFailed | RelationComparisonResultReportsUnmeasurable + relations[sourceRank].set(key, result) + for rank, relation := range relations { + expected := RelationComparisonResultNone + if rank <= sourceRank { + expected = result + } + assert.Equal(t, relation.get(key), expected) + } + }) + } +} + +func BenchmarkSubtypeRelationCacheStorage(b *testing.B) { + const ( + subtypeSize = 277_973 + strictSubtypeSize = 262_657 + intersection = 90_772 + differingResults = 333 + ) + + for _, test := range []struct { + name string + new func() (*Relation, *Relation) + }{ + { + name: "independent", + new: func() (*Relation, *Relation) { + subtype, _, _, _, _ := newRelations() + strictSubtype, _, _, _, _ := newRelations() + return subtype, strictSubtype + }, + }, + { + name: "packed", + new: func() (*Relation, *Relation) { + subtype, strictSubtype, _, _, _ := newRelations() + return subtype, strictSubtype + }, + }, + } { + b.Run(test.name, func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + subtype, strictSubtype := test.new() + for i := range subtypeSize { + subtype.set(CacheHashKey{Lo: uint64(i)}, RelationComparisonResultFailed) + } + for i := range intersection { + result := RelationComparisonResultFailed + if i < differingResults { + result = RelationComparisonResultSucceeded + } + strictSubtype.set(CacheHashKey{Lo: uint64(i)}, result) + } + for i := intersection; i < strictSubtypeSize; i++ { + strictSubtype.set(CacheHashKey{Lo: uint64(subtypeSize + i - intersection)}, RelationComparisonResultFailed) + } + } + }) + } +}