Skip to content

Commit 9681f27

Browse files
authored
Merge pull request #2657 from Shopify/codex/extract-generic-type-fix
Fix generic type registry cache collisions
2 parents 9f5d95a + d847321 commit 9681f27

5 files changed

Lines changed: 107 additions & 32 deletions

File tree

lib/tapioca/internal.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
require "tapioca/sorbet_ext/backcompat_patches"
1313
require "tapioca/sorbet_ext/name_patch"
1414
require "tapioca/sorbet_ext/generic_name_patch"
15+
require "tapioca/sorbet_ext/generic_type_patch"
1516
require "tapioca/sorbet_ext/proc_bind_patch"
1617
require "tapioca/sorbet_ext/void_patch"
1718
require "tapioca/runtime/generic_type_registry"

lib/tapioca/runtime/generic_type_registry.rb

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module Runtime
66
# This class is responsible for storing and looking up information related to generic types.
77
#
88
# The class stores 2 different kinds of data, in two separate lookup tables:
9-
# 1. a lookup of generic type instances by name: `@generic_instances`
9+
# 1. a lookup of generic type instances by constant and name: `@generic_instances`
1010
# 2. a lookup of type variable serializer by constant and type variable
1111
# instance: `@type_variables`
1212
#
@@ -21,7 +21,7 @@ module Runtime
2121
# variable to type variable serializers. This allows us to associate type variables
2222
# to the constant names that represent them, easily.
2323
module GenericTypeRegistry
24-
@generic_instances = {} #: Hash[String, Module[top]]
24+
@generic_instances = {}.compare_by_identity #: Hash[Module[top], Hash[String, Module[top]]]
2525

2626
@type_variables = {}.compare_by_identity #: Hash[Module[top], Array[TypeVariableModule]]
2727

@@ -45,8 +45,9 @@ class << self
4545
# and cloning the given constant so that we can return a type that is the same
4646
# as the current type but is a different instance and has a different name method.
4747
#
48-
# We cache those cloned instances by their name in `@generic_instances`, so that
49-
# we don't keep instantiating a new type every single time it is referenced.
48+
# We cache those cloned instances by their original constant and their name in
49+
# `@generic_instances`, so that we don't keep instantiating a new type every single
50+
# time it is referenced.
5051
# For example, `[Foo[Integer], Foo[Integer], Foo[Integer], Foo[String]]` will only
5152
# result in 2 clones (1 for `Foo[Integer]` and another for `Foo[String]`) and
5253
# 2 hash lookups (for the other two `Foo[Integer]`s).
@@ -64,12 +65,15 @@ def register_type(constant, types)
6465
#
6566
# Also, we try to memoize the generic type based on the name, so that
6667
# we don't have to keep recreating them all the time.
67-
@generic_instances[name] ||= create_generic_type(constant, name)
68+
generic_instances = @generic_instances[constant] ||= {}
69+
generic_instances[name] ||= create_generic_type(constant, name)
6870
end
6971

7072
#: (Object instance) -> bool
7173
def generic_type_instance?(instance)
72-
@generic_instances.values.any? { |generic_type| generic_type === instance }
74+
@generic_instances.values.any? do |generic_instances|
75+
generic_instances.values.any? { |generic_type| generic_type === instance }
76+
end
7377
end
7478

7579
#: (Module[top] constant) -> Array[TypeVariableModule]?
@@ -88,7 +92,7 @@ def lookup_type_variables(constant)
8892
# can return it from the original methods as well.
8993
#: (untyped constant, TypeVariableModule type_variable) -> void
9094
def register_type_variable(constant, type_variable)
91-
type_variables = lookup_or_initialize_type_variables(constant)
95+
type_variables = @type_variables[constant] ||= []
9296

9397
type_variables << type_variable
9498
end
@@ -163,11 +167,6 @@ def create_safe_subclass(constant)
163167
owner.send(:define_method, :inherited, inherited_method)
164168
end
165169
end
166-
167-
#: (Module[top] constant) -> Array[TypeVariableModule]
168-
def lookup_or_initialize_type_variables(constant)
169-
@type_variables[constant] ||= []
170-
end
171170
end
172171
end
173172
end

lib/tapioca/sorbet_ext/generic_name_patch.rb

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -82,26 +82,6 @@ def name
8282
prepend GenericPatch
8383
end
8484
end
85-
86-
module Utils
87-
module Private
88-
module PrivateCoercePatch
89-
def coerce_and_check_module_types(val, check_val, check_module_type)
90-
if val.is_a?(Tapioca::TypeVariableModule)
91-
val.coerce_to_type_variable
92-
elsif val.respond_to?(:__tapioca_override_type)
93-
val.__tapioca_override_type
94-
else
95-
super
96-
end
97-
end
98-
end
99-
100-
class << self
101-
prepend(PrivateCoercePatch)
102-
end
103-
end
104-
end
10585
end
10686

10787
module Tapioca
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# typed: true
2+
# frozen_string_literal: true
3+
4+
module T
5+
module Utils
6+
module Private
7+
# Preserve Tapioca's generic type variables and instantiated generic
8+
# names when Sorbet coerces them into runtime types.
9+
module TapiocaGenericTypeCoercePatch
10+
def coerce_and_check_module_types(val, check_val, check_module_type)
11+
if val.is_a?(Tapioca::TypeVariableModule)
12+
val.coerce_to_type_variable
13+
elsif val.respond_to?(:__tapioca_override_type)
14+
val.__tapioca_override_type
15+
else
16+
super
17+
end
18+
end
19+
end
20+
21+
class << self
22+
prepend(TapiocaGenericTypeCoercePatch)
23+
end
24+
end
25+
end
26+
27+
module Private
28+
module Casts
29+
module TapiocaGenericTypeCastPatch
30+
# https://github.com/sorbet/sorbet/commit/b8d64c7fd9a08e2b9159b5d592bc2de6d586b44a
31+
# inlines the Module fast path in `T.let`, `T.cast`, `T.bind`, and
32+
# `T.assert_type!`, so generic module clones can reach this cast path
33+
# without going through `T::Utils::Private::TapiocaGenericTypeCoercePatch`.
34+
def cast(value, type, cast_method)
35+
if type.respond_to?(:__tapioca_override_type)
36+
type = type.__tapioca_override_type
37+
end
38+
39+
super(value, type, cast_method)
40+
end
41+
end
42+
43+
class << self
44+
prepend(TapiocaGenericTypeCastPatch)
45+
end
46+
end
47+
end
48+
end

spec/tapioca/runtime/generic_type_registry_spec.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,28 @@ class GenericTypeRegistrySpec < Minitest::Spec
5757
end
5858
end
5959

60+
describe ".register_type" do
61+
it "allows generic interface implementations to be cast to generic interface types" do
62+
T.let(SampleGenericInterfaceImplementation.new, SampleGenericInterface[Object])
63+
end
64+
65+
it "does not reuse generic instances for redefined constants with the same name" do
66+
first_constant, first_generic_type = register_reloadable_generic
67+
68+
self.class.send(:remove_const, :ReloadableGeneric) # rubocop:disable RSpec/RemoveConst
69+
70+
second_constant, second_generic_type = register_reloadable_generic
71+
72+
refute_same(first_generic_type, second_generic_type)
73+
assert_operator(first_generic_type, :<, first_constant)
74+
assert_operator(second_generic_type, :<, second_constant)
75+
ensure
76+
if self.class.const_defined?(:ReloadableGeneric, false)
77+
self.class.send(:remove_const, :ReloadableGeneric) # rubocop:disable RSpec/RemoveConst
78+
end
79+
end
80+
end
81+
6082
describe "the patch for .inherited on generic classes" do
6183
# This is more of an internal detail and cross-cutting concern of all the public APIs,
6284
# but it's easier to test here on its own.
@@ -74,12 +96,37 @@ class GenericTypeRegistrySpec < Minitest::Spec
7496
end
7597
end
7698

99+
def register_reloadable_generic
100+
constant = T.let(Class.new, T.untyped)
101+
constant.extend(T::Generic)
102+
constant.const_set(:Element, constant.type_member)
103+
104+
self.class.const_set(:ReloadableGeneric, constant)
105+
[constant, constant[Object]]
106+
end
107+
77108
class SampleGenericClass
78109
extend T::Generic
79110

80111
Element = type_member
81112
end
82113

114+
module SampleGenericInterface
115+
extend T::Generic
116+
117+
interface!
118+
119+
Element = type_member
120+
end
121+
122+
class SampleGenericInterfaceImplementation
123+
extend T::Generic
124+
125+
include SampleGenericInterface
126+
127+
Element = type_member
128+
end
129+
83130
class RaisesInInheritedCallback
84131
extend T::Generic
85132

0 commit comments

Comments
 (0)