sorbet-runtime 0.6.13298 → 0.6.13299

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77e84ed751208f67520eb84021e42fba0b7d943201e6db5f06ab88811b09e0dc
4
- data.tar.gz: c33769e6654d425826518c165d443e11df10f0ae8d18ebd26c4f35dd340979f1
3
+ metadata.gz: 346fdbb1a44759ff22aa86b6583474a790fb38fea59464ecee830b515521a4cf
4
+ data.tar.gz: 5a3a3fb1fcd5b8801ea0070631caa24a1decb95df7c9d09e506f16910ffb819b
5
5
  SHA512:
6
- metadata.gz: c33e48d57dce556e8e343eb0aa7d05e959349fe7c1c0ff627b0f9e1889b9a99da0d0f42f965205f9f6eae8b21bee9f980f9a91236898bc32859a3f6ac3b3747a
7
- data.tar.gz: b0f749c7283fd536f85ec41646f5760e2df50d8b72400d29ce0123964dbf48bc7620f5491e76c2d98a864242fb955129968b4ea46576aee96db7c0dff797136d
6
+ metadata.gz: 87b79a12e41334bee22330e734b8613515da22e3d7d1843d60584e20ff41dd0fb5ab896265e7d1434cb1d09c681ed51c603b0ae4c6c976afabbf8c18ec7021e2
7
+ data.tar.gz: aa62c20849e24e1453150fd7e4cf354bf36d5ee24c355a3b35a4e39d0263a2821e3c1f9cbec4521245e45b2639000441ccb85567a5558525896faae3ab62b720
@@ -51,7 +51,6 @@ require_relative 'types/types/type_parameter'
51
51
  require_relative 'types/types/typed_enumerator'
52
52
  require_relative 'types/types/typed_enumerator_chain'
53
53
  require_relative 'types/types/typed_enumerator_lazy'
54
- require_relative 'types/types/typed_hash'
55
54
  require_relative 'types/types/typed_range'
56
55
  require_relative 'types/types/typed_set'
57
56
  require_relative 'types/types/union'
@@ -87,6 +86,10 @@ require_relative 'types/utils'
87
86
  require_relative 'types/boolean'
88
87
 
89
88
  # Depends on types/utils
89
+ # typed_hash must load after untyped + utils so TypedHash::Untyped::Private::INSTANCE
90
+ # (a frozen, shared T::Hash[T.untyped, T.untyped]) can be built at load time, the same
91
+ # way TypedArray::Untyped::Private::INSTANCE is.
92
+ require_relative 'types/types/typed_hash'
90
93
  require_relative 'types/types/typed_array'
91
94
  require_relative 'types/types/typed_module'
92
95
  require_relative 'types/types/typed_class'
data/lib/types/_types.rb CHANGED
@@ -134,6 +134,18 @@ module T
134
134
  def self.cast(value, type, checked: true)
135
135
  return value unless checked
136
136
 
137
+ # Happy paths for the two dominant type shapes at inline-cast sites,
138
+ # duplicated from Private::Casts.cast to skip its call frame: Module
139
+ # literals (e.g. `T.cast(x, Foo)`) and the SimplePairUnion that
140
+ # `T.nilable(SomeModule)` produces. Failures and other type shapes take
141
+ # the full path below.
142
+ case type
143
+ when ::Module
144
+ return value if value.is_a?(type)
145
+ when T::Private::Types::SimplePairUnion
146
+ return value if type.valid?(value)
147
+ end
148
+
137
149
  Private::Casts.cast(value, type, "T.cast")
138
150
  end
139
151
 
@@ -149,6 +161,14 @@ module T
149
161
  def self.let(value, type, checked: true)
150
162
  return value unless checked
151
163
 
164
+ # Happy paths for Module literals and T.nilable; see T.cast.
165
+ case type
166
+ when ::Module
167
+ return value if value.is_a?(type)
168
+ when T::Private::Types::SimplePairUnion
169
+ return value if type.valid?(value)
170
+ end
171
+
152
172
  Private::Casts.cast(value, type, "T.let")
153
173
  end
154
174
 
@@ -168,6 +188,14 @@ module T
168
188
  def self.bind(value, type, checked: true)
169
189
  return value unless checked
170
190
 
191
+ # Happy paths for Module literals and T.nilable; see T.cast.
192
+ case type
193
+ when ::Module
194
+ return value if value.is_a?(type)
195
+ when T::Private::Types::SimplePairUnion
196
+ return value if type.valid?(value)
197
+ end
198
+
171
199
  Private::Casts.cast(value, type, "T.bind")
172
200
  end
173
201
 
@@ -178,6 +206,14 @@ module T
178
206
  def self.assert_type!(value, type, checked: true)
179
207
  return value unless checked
180
208
 
209
+ # Happy paths for Module literals and T.nilable; see T.cast.
210
+ case type
211
+ when ::Module
212
+ return value if value.is_a?(type)
213
+ when T::Private::Types::SimplePairUnion
214
+ return value if type.valid?(value)
215
+ end
216
+
181
217
  Private::Casts.cast(value, type, "T.assert_type!")
182
218
  end
183
219
 
@@ -293,7 +329,7 @@ module T
293
329
  module Hash
294
330
  def self.[](keys, values)
295
331
  if keys.is_a?(T::Types::Untyped) && values.is_a?(T::Types::Untyped)
296
- T::Types::TypedHash::Untyped.new
332
+ T::Types::TypedHash::Untyped::Private::INSTANCE
297
333
  else
298
334
  T::Types::TypedHash.new(keys: keys, values: values)
299
335
  end
data/lib/types/enum.rb CHANGED
@@ -48,18 +48,22 @@ class T::Enum
48
48
  private_constant :SerializedVal
49
49
 
50
50
  ### Enum class methods ###
51
- sig { returns(T::Array[T.attached_class]) }
51
+ # WithoutRuntime on the hot accessors below installs no method wrapper at all
52
+ # (not even the one-time wrap a `checked(:never)` sig still pays on first
53
+ # call). Their bodies are type-safe by construction, so the wrapper validated
54
+ # nothing useful; Sorbet still reads these sigs statically. The
55
+ # override/overridable annotations remain as static-only signal.
56
+ T::Sig::WithoutRuntime.sig { returns(T::Array[T.attached_class]) }
52
57
  def self.values
53
58
  if @values.nil?
54
- raise "Attempting to access values of #{self.class} before it has been initialized." \
55
- " Enums are not initialized until the 'enums do' block they are defined in has finished running."
59
+ raise(UNBOUND_VALUES_MESSAGE % self.class)
56
60
  end
57
61
  @values
58
62
  end
59
63
 
60
64
  # This exists for compatibility with the interface of `Hash` & mostly to support
61
65
  # the HashEachMethods Rubocop.
62
- sig { params(blk: T.nilable(T.proc.params(arg0: T.attached_class).void)).returns(T.any(T::Enumerator[T.attached_class], T::Array[T.attached_class])) }
66
+ T::Sig::WithoutRuntime.sig { params(blk: T.nilable(T.proc.params(arg0: T.attached_class).void)).returns(T.any(T::Enumerator[T.attached_class], T::Array[T.attached_class])) }
63
67
  def self.each_value(&blk)
64
68
  if blk
65
69
  values.each(&blk)
@@ -72,11 +76,10 @@ class T::Enum
72
76
  #
73
77
  # Note: It would have been nice to make this method final before people started overriding it.
74
78
  # Note: Failed CriticalMethodsNoRuntimeTypingTest
75
- sig { params(serialized_val: SerializedVal).returns(T.nilable(T.attached_class)).checked(:never) }
79
+ T::Sig::WithoutRuntime.sig { params(serialized_val: SerializedVal).returns(T.nilable(T.attached_class)) }
76
80
  def self.try_deserialize(serialized_val)
77
81
  if @mapping.nil?
78
- raise "Attempting to access serialization map of #{self.class} before it has been initialized." \
79
- " Enums are not initialized until the 'enums do' block they are defined in has finished running."
82
+ raise(UNBOUND_SERIALIZATION_MAP_MESSAGE % self.class)
80
83
  end
81
84
  @mapping[serialized_val]
82
85
  end
@@ -88,7 +91,7 @@ class T::Enum
88
91
  #
89
92
  # @return [self]
90
93
  # @raise [KeyError] if serialized value does not match any instance.
91
- sig { overridable.params(serialized_val: SerializedVal).returns(T.attached_class).checked(:never) }
94
+ T::Sig::WithoutRuntime.sig { overridable.params(serialized_val: SerializedVal).returns(T.attached_class) }
92
95
  def self.from_serialized(serialized_val)
93
96
  res = try_deserialize(serialized_val)
94
97
  if res.nil?
@@ -99,17 +102,16 @@ class T::Enum
99
102
 
100
103
  # Note: It would have been nice to make this method final before people started overriding it.
101
104
  # @return [Boolean] Does the given serialized value correspond with any of this enum's values.
102
- sig { overridable.params(serialized_val: SerializedVal).returns(T::Boolean).checked(:never) }
105
+ T::Sig::WithoutRuntime.sig { overridable.params(serialized_val: SerializedVal).returns(T::Boolean) }
103
106
  def self.has_serialized?(serialized_val)
104
107
  if @mapping.nil?
105
- raise "Attempting to access serialization map of #{self.class} before it has been initialized." \
106
- " Enums are not initialized until the 'enums do' block they are defined in has finished running."
108
+ raise(UNBOUND_SERIALIZATION_MAP_MESSAGE % self.class)
107
109
  end
108
110
  @mapping.include?(serialized_val)
109
111
  end
110
112
 
111
113
  # Note: Failed CriticalMethodsNoRuntimeTypingTest
112
- sig { override.params(instance: T.nilable(T::Enum)).returns(SerializedVal).checked(:never) }
114
+ T::Sig::WithoutRuntime.sig { override.params(instance: T.nilable(T::Enum)).returns(SerializedVal) }
113
115
  def self.serialize(instance)
114
116
  # This is needed otherwise if a Chalk::ODM::Document with a property of the shape
115
117
  # T::Hash[T.nilable(MyEnum), Integer] and a value that looks like {nil => 0} is
@@ -126,7 +128,7 @@ class T::Enum
126
128
  end
127
129
 
128
130
  # Note: Failed CriticalMethodsNoRuntimeTypingTest
129
- sig { override.params(mongo_value: SerializedVal).returns(T.attached_class).checked(:never) }
131
+ T::Sig::WithoutRuntime.sig { override.params(mongo_value: SerializedVal).returns(T.attached_class) }
130
132
  def self.deserialize(mongo_value)
131
133
  if self == T::Enum
132
134
  raise "Cannot call T::Enum.deserialize directly. You must call on a specific child class."
@@ -146,10 +148,28 @@ class T::Enum
146
148
  self
147
149
  end
148
150
 
151
+ # Format strings (`%s` is the class) for the "accessed before initialized"
152
+ # errors, so the various raise sites can't drift from each other.
153
+ UNBOUND_VALUE_MESSAGE = "Attempting to access Enum value on %s before it has been initialized." \
154
+ " Enums are not initialized until the 'enums do' block they are defined in has finished running."
155
+ private_constant :UNBOUND_VALUE_MESSAGE
156
+
157
+ UNBOUND_VALUES_MESSAGE = "Attempting to access values of %s before it has been initialized." \
158
+ " Enums are not initialized until the 'enums do' block they are defined in has finished running."
159
+ private_constant :UNBOUND_VALUES_MESSAGE
160
+
161
+ UNBOUND_SERIALIZATION_MAP_MESSAGE = "Attempting to access serialization map of %s before it has been initialized." \
162
+ " Enums are not initialized until the 'enums do' block they are defined in has finished running."
163
+ private_constant :UNBOUND_SERIALIZATION_MAP_MESSAGE
164
+
149
165
  # Note: Failed CriticalMethodsNoRuntimeTypingTest
150
- sig { returns(SerializedVal).checked(:never) }
166
+ T::Sig::WithoutRuntime.sig { returns(SerializedVal) }
151
167
  def serialize
152
- assert_bound!
168
+ # Same check and message as assert_bound!, open-coded to avoid the extra
169
+ # method frame on this hot path.
170
+ if @const_name.nil?
171
+ raise(UNBOUND_VALUE_MESSAGE % self.class)
172
+ end
153
173
  @serialized_val
154
174
  end
155
175
 
@@ -165,17 +185,20 @@ class T::Enum
165
185
  serialized_val.as_json(*args)
166
186
  end
167
187
 
168
- sig { returns(String) }
188
+ # `to_s` keeps delegating to `inspect` rather than `alias_method :to_s,
189
+ # :inspect` so a subclass that overrides `inspect` still has its `to_s`
190
+ # reflect the override.
191
+ T::Sig::WithoutRuntime.sig { returns(String) }
169
192
  def to_s
170
193
  inspect
171
194
  end
172
195
 
173
- sig { returns(String) }
196
+ T::Sig::WithoutRuntime.sig { returns(String) }
174
197
  def inspect
175
198
  "#<#{self.class.name}::#{@const_name || '__UNINITIALIZED__'}>"
176
199
  end
177
200
 
178
- sig { params(other: BasicObject).returns(T.nilable(Integer)) }
201
+ T::Sig::WithoutRuntime.sig { params(other: BasicObject).returns(T.nilable(Integer)) }
179
202
  def <=>(other)
180
203
  case other
181
204
  when self.class
@@ -292,11 +315,10 @@ class T::Enum
292
315
  self.class._register_instance(self)
293
316
  end
294
317
 
295
- sig { returns(NilClass).checked(:never) }
318
+ T::Sig::WithoutRuntime.sig { returns(NilClass) }
296
319
  private def assert_bound!
297
320
  if @const_name.nil?
298
- raise "Attempting to access Enum value on #{self.class} before it has been initialized." \
299
- " Enums are not initialized until the 'enums do' block they are defined in has finished running."
321
+ raise(UNBOUND_VALUE_MESSAGE % self.class)
300
322
  end
301
323
  end
302
324
 
@@ -5,8 +5,29 @@ module T::Private
5
5
  module Casts
6
6
  def self.cast(value, type, cast_method)
7
7
  begin
8
- coerced_type = T::Utils::Private.coerce_and_check_module_types(type, value, true)
9
- return value unless coerced_type
8
+ # Every caller (T.cast/let/bind/assert_type! in _types.rb) inlines the
9
+ # value check for the two dominant shapes -- a Module literal and the
10
+ # SimplePairUnion that `T.nilable(SomeModule)` produces -- and only
11
+ # falls through to here when that check already failed. So skip
12
+ # straight to coercing: re-checking `value.is_a?(type)` /
13
+ # `type.valid?(value)` here would always be false.
14
+ case type
15
+ when ::Module
16
+ coerced_type = T::Types::Simple::Private::Pool.type_for_module(type)
17
+ when T::Types::Base
18
+ # Mirrors the T::Types::Base branch of coerce_and_check_module_types,
19
+ # kept inline to avoid its call frame for every already-coerced type.
20
+ coerced_type =
21
+ case type
22
+ when T::Private::Types::TypeAlias
23
+ type.aliased_type
24
+ else
25
+ type
26
+ end
27
+ else
28
+ coerced_type = T::Utils::Private.coerce_and_check_module_types(type, value, true)
29
+ return value unless coerced_type
30
+ end
10
31
 
11
32
  error = coerced_type.error_message_for_obj(value)
12
33
  return value unless error
@@ -44,9 +44,10 @@ module T::Private::ClassUtils
44
44
  end
45
45
 
46
46
  if block && block.arity < 0 && respond_to?(:ruby2_keywords, true)
47
- m = instance_method(name)
48
- has_rest = m.parameters.any? { |type, _| type == :rest }
49
- has_keywords = m.parameters.any? { |type, _| %i[keyrest keyreq key].include?(type) }
47
+ # Fetch .parameters once: each call builds a fresh array.
48
+ parameters = instance_method(name).parameters
49
+ has_rest = parameters.any? { |kind, _| kind == :rest }
50
+ has_keywords = parameters.any? { |kind, _| kind == :keyrest || kind == :keyreq || kind == :key }
50
51
  ruby2_keywords(name) if has_rest && !has_keywords
51
52
  end
52
53
  end
@@ -67,18 +68,23 @@ module T::Private::ClassUtils
67
68
  original_visibility = visibility_method_name(mod, name)
68
69
  original_owner = original_method.owner
69
70
 
70
- mod.ancestors.each do |ancestor|
71
- break if ancestor == mod
72
- if ancestor == original_owner
73
- # If we get here, that means the method we're trying to replace exists on a *prepended*
74
- # mixin, which means in order to supersede it, we'd need to create a method on a new
75
- # module that we'd prepend before `ancestor`. The problem with that approach is there'd
76
- # be no way to remove that new module after prepending it, so we'd be left with these
77
- # empty anonymous modules in the ancestor chain after calling `restore`.
78
- #
79
- # That's not necessarily a deal breaker, but for now, we're keeping it as unsupported.
80
- raise "You're trying to replace `#{name}` on `#{mod}`, but that method exists in a " \
81
- "prepended module (#{ancestor}), which we don't currently support."
71
+ # In the common case the method is owned by `mod` itself, in which case the loop below
72
+ # would always `break` before the raise could match (`mod` precedes any non-prepended
73
+ # ancestor in its own ancestor chain), so skip computing `mod.ancestors` entirely.
74
+ if original_owner != mod
75
+ mod.ancestors.each do |ancestor|
76
+ break if ancestor == mod
77
+ if ancestor == original_owner
78
+ # If we get here, that means the method we're trying to replace exists on a *prepended*
79
+ # mixin, which means in order to supersede it, we'd need to create a method on a new
80
+ # module that we'd prepend before `ancestor`. The problem with that approach is there'd
81
+ # be no way to remove that new module after prepending it, so we'd be left with these
82
+ # empty anonymous modules in the ancestor chain after calling `restore`.
83
+ #
84
+ # That's not necessarily a deal breaker, but for now, we're keeping it as unsupported.
85
+ raise "You're trying to replace `#{name}` on `#{mod}`, but that method exists in a " \
86
+ "prepended module (#{ancestor}), which we don't currently support."
87
+ end
82
88
  end
83
89
  end
84
90
 
@@ -299,11 +299,14 @@ module T::Private::Methods
299
299
 
300
300
  # Only public because it needs to get called below inside the replace_method blocks below.
301
301
  def self._on_method_added(hook_mod, mod, method_name)
302
- if T::Private::DeclState.current.skip_on_method_added
302
+ # The thread-local DeclState object is stable for the duration of this call
303
+ # (nothing reassigns `DeclState.current=`), so fetch it once.
304
+ decl_state = T::Private::DeclState.current
305
+ if decl_state.skip_on_method_added
303
306
  return
304
307
  end
305
308
 
306
- current_declaration = T::Private::DeclState.current.consume!
309
+ current_declaration = decl_state.consume!
307
310
 
308
311
  if T::Private::Final.final_module?(mod) && (current_declaration.nil? || !current_declaration.final)
309
312
  raise "#{mod} was declared as final but its method `#{method_name}` was not declared as final"
@@ -80,7 +80,8 @@ class T::Private::Methods::Signature
80
80
  # If sig params are declared but there is a single parameter with a missing name
81
81
  # **and** the method ends with a "=", assume it is a writer method generated
82
82
  # by attr_writer or attr_accessor
83
- writer_method = !(raw_arg_types.size == 1 && raw_arg_types.key?(nil)) && parameters == UNNAMED_REQUIRED_PARAMETERS && method_name[-1] == "="
83
+ # (Checks are ordered so that the common, non-writer case short-circuits without allocating.)
84
+ writer_method = method_name.end_with?("=") && parameters == UNNAMED_REQUIRED_PARAMETERS && !(raw_arg_types.size == 1 && raw_arg_types.key?(nil))
84
85
  # For writer methods, map the single parameter to the method name without the "=" at the end
85
86
  parameters = [[:req, method_name[0...-1].to_sym]] if writer_method
86
87
  is_name_missing = parameters.any? { |_, name| !raw_arg_types.key?(name) }
@@ -181,7 +181,10 @@ module T::Types
181
181
  def ==(other)
182
182
  case other
183
183
  when T::Types::Base
184
- other.name == self.name
184
+ # Performance fast path: pooled and memoized type instances (e.g. the
185
+ # results of repeated T.nilable(X) calls) are the same object, so they
186
+ # can compare equal without computing and comparing their names.
187
+ other.equal?(self) || other.name == self.name
185
188
  else
186
189
  false
187
190
  end
@@ -26,13 +26,15 @@ module T::Types
26
26
 
27
27
  # overrides Base
28
28
  def recursively_valid?(obj)
29
- if obj.is_a?(Array) && obj.length == types.length
30
- i = 0
31
- while i < types.length
32
- if !types.fetch(i).recursively_valid?(obj[i])
29
+ element_types = types
30
+ length = element_types.length
31
+ if obj.is_a?(Array) && obj.length == length
32
+ index = 0
33
+ while index < length
34
+ if !element_types.fetch(index).recursively_valid?(obj[index])
33
35
  return false
34
36
  end
35
- i += 1
37
+ index += 1
36
38
  end
37
39
  true
38
40
  else
@@ -42,13 +44,15 @@ module T::Types
42
44
 
43
45
  # overrides Base
44
46
  def valid?(obj)
45
- if obj.is_a?(Array) && obj.length == types.length
46
- i = 0
47
- while i < types.length
48
- if !types.fetch(i).valid?(obj[i])
47
+ element_types = types
48
+ length = element_types.length
49
+ if obj.is_a?(Array) && obj.length == length
50
+ index = 0
51
+ while index < length
52
+ if !element_types.fetch(index).valid?(obj[index])
49
53
  return false
50
54
  end
51
- i += 1
55
+ index += 1
52
56
  end
53
57
  true
54
58
  else
@@ -26,16 +26,30 @@ module T::Types
26
26
  # overrides Base
27
27
  def recursively_valid?(obj)
28
28
  return false unless obj.is_a?(Hash)
29
- return false if types.any? { |key, type| !type.recursively_valid?(obj[key]) }
30
- return false if obj.any? { |key, _| !types[key] }
29
+ field_types = types
30
+ field_types.each_pair do |key, type|
31
+ return false unless type.recursively_valid?(obj[key])
32
+ end
33
+ # Pigeonhole: more entries than declared keys guarantees an undeclared key.
34
+ return false if obj.size > field_types.size
35
+ obj.each_key do |key|
36
+ return false unless field_types.key?(key)
37
+ end
31
38
  true
32
39
  end
33
40
 
34
41
  # overrides Base
35
42
  def valid?(obj)
36
43
  return false unless obj.is_a?(Hash)
37
- return false if types.any? { |key, type| !type.valid?(obj[key]) }
38
- return false if obj.any? { |key, _| !types[key] }
44
+ field_types = types
45
+ field_types.each_pair do |key, type|
46
+ return false unless type.valid?(obj[key])
47
+ end
48
+ # Pigeonhole: more entries than declared keys guarantees an undeclared key.
49
+ return false if obj.size > field_types.size
50
+ obj.each_key do |key|
51
+ return false unless field_types.key?(key)
52
+ end
39
53
  true
40
54
  end
41
55
 
@@ -32,12 +32,24 @@ module T::Types
32
32
 
33
33
  # overrides Base
34
34
  def recursively_valid?(obj)
35
- types.all? { |type| type.recursively_valid?(obj) }
35
+ members = types
36
+ index = 0
37
+ while index < members.length
38
+ return false unless members.fetch(index).recursively_valid?(obj)
39
+ index += 1
40
+ end
41
+ true
36
42
  end
37
43
 
38
44
  # overrides Base
39
45
  def valid?(obj)
40
- types.all? { |type| type.valid?(obj) }
46
+ members = types
47
+ index = 0
48
+ while index < members.length
49
+ return false unless members.fetch(index).valid?(obj)
50
+ index += 1
51
+ end
52
+ true
41
53
  end
42
54
 
43
55
  # overrides Base
@@ -31,6 +31,14 @@ module T::Types
31
31
  # rubocop:enable Performance/BindCall
32
32
  end
33
33
 
34
+ # overrides Base
35
+ #
36
+ # Identical to valid?; defined directly so the leaf-type hot path (every
37
+ # element check in a typed collection walk) skips the Base delegator frame.
38
+ def recursively_valid?(obj)
39
+ obj.is_a?(@raw_type)
40
+ end
41
+
34
42
  # overrides Base
35
43
  def valid?(obj)
36
44
  obj.is_a?(@raw_type)
@@ -93,7 +101,7 @@ module T::Types
93
101
  type = if mod == ::Array
94
102
  TypedArray::Untyped::Private::INSTANCE
95
103
  elsif mod == ::Hash
96
- TypedHash::Untyped.new
104
+ TypedHash::Untyped::Private::INSTANCE
97
105
  elsif mod == ::Enumerable
98
106
  TypedEnumerable::Untyped.new
99
107
  elsif mod == ::Enumerator
@@ -38,10 +38,12 @@ module T::Types
38
38
  return false unless obj.is_a?(Enumerable)
39
39
  case obj
40
40
  when Array
41
- it = 0
42
- while it < obj.count
43
- return false unless type.recursively_valid?(obj[it])
44
- it += 1
41
+ element_type = type
42
+ length = obj.count
43
+ index = 0
44
+ while index < length
45
+ return false unless element_type.recursively_valid?(obj[index])
46
+ index += 1
45
47
  end
46
48
  true
47
49
  when Hash
@@ -47,12 +47,23 @@ module T::Types
47
47
 
48
48
  class Untyped < TypedHash
49
49
  def initialize
50
- super(keys: T.untyped, values: T.untyped)
50
+ # Use the INSTANCE constant directly (rather than `T.untyped`) so this
51
+ # can be built at load time, mirroring TypedArray::Untyped.
52
+ super(keys: T::Types::Untyped::Private::INSTANCE, values: T::Types::Untyped::Private::INSTANCE)
51
53
  end
52
54
 
53
55
  def valid?(obj)
54
56
  obj.is_a?(Hash)
55
57
  end
58
+
59
+ def freeze
60
+ build_type # force lazy initialization before freezing the object
61
+ super
62
+ end
63
+
64
+ module Private
65
+ INSTANCE = Untyped.new.freeze
66
+ end
56
67
  end
57
68
  end
58
69
  end
@@ -11,15 +11,30 @@ module T::Types
11
11
  end
12
12
 
13
13
  def types
14
- @types ||= @inner_types.flat_map do |type|
15
- type = T::Utils.coerce(type)
16
- if type.is_a?(Union)
17
- # Simplify nested unions (mostly so `name` returns a nicer value)
18
- type.types
19
- else
20
- type
14
+ @types ||= begin
15
+ flattened = @inner_types.flat_map do |type|
16
+ type = T::Utils.coerce(type)
17
+ if type.is_a?(Union)
18
+ # Simplify nested unions (mostly so `name` returns a nicer value)
19
+ type.types
20
+ else
21
+ type
22
+ end
23
+ end.uniq
24
+ # When every member is a plain Simple (whose valid? is exactly
25
+ # `obj.is_a?(raw_type)`), precompute the members' raw modules so
26
+ # valid? can skip per-member dispatch. instance_of? (the is_a? only
27
+ # narrows for static checking) so that any Simple subclass overriding
28
+ # valid? would be excluded. Note this snapshots the members at build
29
+ # time.
30
+ member_modules = []
31
+ all_simple = flattened.all? do |type|
32
+ type.is_a?(T::Types::Simple) && type.instance_of?(T::Types::Simple) &&
33
+ member_modules << type.raw_type
21
34
  end
22
- end.uniq
35
+ @member_modules = all_simple ? member_modules.freeze : false
36
+ flattened
37
+ end
23
38
  end
24
39
 
25
40
  def build_type
@@ -58,12 +73,52 @@ module T::Types
58
73
 
59
74
  # overrides Base
60
75
  def recursively_valid?(obj)
61
- types.any? { |type| type.recursively_valid?(obj) }
76
+ member_modules = @member_modules
77
+ if member_modules.nil?
78
+ # Force the lazy types builder, which also computes @member_modules
79
+ types
80
+ member_modules = @member_modules
81
+ end
82
+ index = 0
83
+ if member_modules
84
+ # For an all-Simple union, recursively_valid? and valid? coincide
85
+ # (Simple's recursively_valid? is exactly `obj.is_a?(raw_type)`).
86
+ while index < member_modules.length
87
+ return true if obj.is_a?(member_modules[index])
88
+ index += 1
89
+ end
90
+ else
91
+ members = types
92
+ while index < members.length
93
+ return true if members.fetch(index).recursively_valid?(obj)
94
+ index += 1
95
+ end
96
+ end
97
+ false
62
98
  end
63
99
 
64
100
  # overrides Base
65
101
  def valid?(obj)
66
- types.any? { |type| type.valid?(obj) }
102
+ member_modules = @member_modules
103
+ if member_modules.nil?
104
+ # Force the lazy types builder, which also computes @member_modules
105
+ types
106
+ member_modules = @member_modules
107
+ end
108
+ index = 0
109
+ if member_modules
110
+ while index < member_modules.length
111
+ return true if obj.is_a?(member_modules[index])
112
+ index += 1
113
+ end
114
+ else
115
+ members = types
116
+ while index < members.length
117
+ return true if members.fetch(index).valid?(obj)
118
+ index += 1
119
+ end
120
+ end
121
+ false
67
122
  end
68
123
 
69
124
  # overrides Base
@@ -109,9 +164,13 @@ module T::Types
109
164
  end
110
165
 
111
166
  begin
112
- if type_b == T::Utils::Nilable::NIL_TYPE
167
+ # The `equal?` checks are an identity fast path: NIL_TYPE is the pooled
168
+ # `coerce(NilClass)` instance, so it hits on every normal `T.nilable` call.
169
+ # The `==` fallbacks preserve semantics for hand-constructed
170
+ # `Simple.new(NilClass)` instances that bypass the pool.
171
+ if type_b.equal?(T::Utils::Nilable::NIL_TYPE) || type_b == T::Utils::Nilable::NIL_TYPE
113
172
  type_a.to_nilable
114
- elsif type_a == T::Utils::Nilable::NIL_TYPE
173
+ elsif type_a.equal?(T::Utils::Nilable::NIL_TYPE) || type_a == T::Utils::Nilable::NIL_TYPE
115
174
  type_b.to_nilable
116
175
  else
117
176
  T::Private::Types::SimplePairUnion.new(type_a, type_b)
data/lib/types/utils.rb CHANGED
@@ -3,6 +3,13 @@
3
3
 
4
4
  module T::Utils
5
5
  module Private
6
+ # NOTE: the Module and SimplePairUnion branches of this method are inlined
7
+ # for speed in several hot paths. The `T.cast` / `T.let` / `T.bind` /
8
+ # `T.assert_type!` happy paths in `_types.rb` inline the value check and
9
+ # return early on success; `T::Private::Casts.cast` is only reached after
10
+ # that check has already failed, so it reproduces the coercion branches but
11
+ # skips the (now always-false) value check. If you change the behavior
12
+ # here, update those callers too.
6
13
  def self.coerce_and_check_module_types(val, check_val, check_module_type)
7
14
  # rubocop:disable Style/CaseLikeIf
8
15
  if val.is_a?(T::Types::Base)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sorbet-runtime
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.13298
4
+ version: 0.6.13299
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stripe