rigortype 0.0.3 → 0.0.4

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.
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "method_catalog"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module Builtins
8
+ # `Time` catalog. Singleton — load once, consult during dispatch.
9
+ #
10
+ # Time is a pure-C built-in: the Init block in
11
+ # `references/ruby/time.c` registers the bulk of the surface,
12
+ # and the Ruby-side prelude `references/ruby/timev.rb`
13
+ # contributes the class-side constructors (`Time.now`,
14
+ # `Time.at`, `Time.new`) through Primitive cexpr stubs.
15
+ #
16
+ # Time receivers are not lifted to a `Constant` carrier today
17
+ # (there is no `Time` literal node — the closest is
18
+ # `Time.now` / `Time.new(...)`, which produce `Nominal[Time]`).
19
+ # The catalog wiring therefore mostly governs:
20
+ #
21
+ # 1. The size-projection-equivalent reader surface (`#year`,
22
+ # `#month`, `#hour`, `#sec`, `#wday`, …) — RBS-declared
23
+ # `Integer` is preserved through dispatch.
24
+ # 2. The blocklist below, which keeps the indirect-mutator
25
+ # methods that the C-body classifier mis-flagged as
26
+ # `:leaf` from ever folding through a hypothetical future
27
+ # `Constant<Time>` carrier.
28
+ #
29
+ # The blocklist captures the false-positive `:leaf` entries
30
+ # whose helper functions the regex classifier did not
31
+ # recognise as mutators.
32
+ TIME_CATALOG = MethodCatalog.new(
33
+ path: File.expand_path(
34
+ "../../../../data/builtins/ruby_core/time.yml",
35
+ __dir__
36
+ ),
37
+ mutating_selectors: {
38
+ "Time" => Set[
39
+ # `time_init_copy` writes the `timew` and `vtm` slots on
40
+ # the receiver via `time_set_timew` / `time_set_vtm`.
41
+ # Classed `:leaf` because those setters are not in the
42
+ # mutator regex's helper list. Blocked for symmetry with
43
+ # String / Array / Range / Set initialize_copy entries.
44
+ :initialize_copy,
45
+ # `time_localtime_m` -> `time_localtime` calls
46
+ # `time_modify(time)` to mark the receiver mutable
47
+ # before rewriting its `vtm` cache and `tzmode`. The
48
+ # docstring is explicit ("converts time to local time
49
+ # in place"). The C-body classifier mis-flagged it as
50
+ # `:leaf` because `time_modify` is not in its mutator
51
+ # regex.
52
+ :localtime,
53
+ # `time_gmtime` (registered as both `gmtime` and `utc`
54
+ # against `rb_cTime`) follows the same in-place pattern
55
+ # as `time_localtime`: `time_modify(time)` then a
56
+ # `time_set_vtm` write and `TZMODE_SET_UTC`. Both
57
+ # selectors share the cfunc, so both must be blocked.
58
+ :gmtime, :utc
59
+ ]
60
+ }
61
+ )
62
+ end
63
+ end
64
+ end
@@ -4,6 +4,10 @@ require_relative "../../type"
4
4
  require_relative "../builtins/numeric_catalog"
5
5
  require_relative "../builtins/string_catalog"
6
6
  require_relative "../builtins/array_catalog"
7
+ require_relative "../builtins/hash_catalog"
8
+ require_relative "../builtins/range_catalog"
9
+ require_relative "../builtins/set_catalog"
10
+ require_relative "../builtins/time_catalog"
7
11
 
8
12
  module Rigor
9
13
  module Inference
@@ -626,17 +630,33 @@ module Rigor
626
630
  catalog.safe_for_folding?(class_name, method_name)
627
631
  end
628
632
 
633
+ # `(catalog, class_name)` per receiver class. The class_name
634
+ # is what each catalog's RBS-rooted entries are keyed by.
635
+ # `catalog_for` walks this table in declaration order so
636
+ # subclasses (Symbol < String) hit their dedicated entry
637
+ # before any base-class fallback would, and adding a new
638
+ # class is a one-line addition rather than another `when`
639
+ # arm on a growing case statement.
640
+ CATALOG_BY_CLASS = [
641
+ [Integer, [Builtins::NumericCatalog, "Integer"]],
642
+ [Float, [Builtins::NumericCatalog, "Float"]],
643
+ [String, [Builtins::STRING_CATALOG, "String"]],
644
+ [Symbol, [Builtins::STRING_CATALOG, "Symbol"]],
645
+ [Array, [Builtins::ARRAY_CATALOG, "Array"]],
646
+ [Hash, [Builtins::HASH_CATALOG, "Hash"]],
647
+ [Range, [Builtins::RANGE_CATALOG, "Range"]],
648
+ [::Set, [Builtins::SET_CATALOG, "Set"]],
649
+ [Time, [Builtins::TIME_CATALOG, "Time"]]
650
+ ].freeze
651
+ private_constant :CATALOG_BY_CLASS
652
+
629
653
  # Returns `[catalog, class_name]` for receivers we have a
630
- # catalog for; nil otherwise. The class_name is what the
631
- # catalog's RBS-rooted entries are keyed by.
654
+ # catalog for; nil otherwise.
632
655
  def catalog_for(receiver_value)
633
- case receiver_value
634
- when Integer then [Builtins::NumericCatalog, "Integer"]
635
- when Float then [Builtins::NumericCatalog, "Float"]
636
- when String then [Builtins::STRING_CATALOG, "String"]
637
- when Symbol then [Builtins::STRING_CATALOG, "Symbol"]
638
- when Array then [Builtins::ARRAY_CATALOG, "Array"]
656
+ CATALOG_BY_CLASS.each do |klass, entry|
657
+ return entry if receiver_value.is_a?(klass)
639
658
  end
659
+ nil
640
660
  end
641
661
 
642
662
  def unary_ops_for(receiver_value)
@@ -27,7 +27,7 @@ module Rigor
27
27
  # - `a.downto(b) { |i| … }` yields the same domain `[b, a]`,
28
28
  # just iterated in reverse. Lower bound from the
29
29
  # argument, upper bound from the receiver.
30
- module IteratorDispatch
30
+ module IteratorDispatch # rubocop:disable Metrics/ModuleLength
31
31
  module_function
32
32
 
33
33
  # @return [Array<Rigor::Type>, nil] block-param types, or
@@ -37,6 +37,7 @@ module Rigor
37
37
  when :times then times_block_params(receiver)
38
38
  when :upto then upto_block_params(receiver, args.first)
39
39
  when :downto then downto_block_params(receiver, args.first)
40
+ when :each_with_index then each_with_index_block_params(receiver)
40
41
  end
41
42
  end
42
43
 
@@ -62,6 +63,107 @@ module Rigor
62
63
  [build_index_range(lower_bound_of(end_arg), upper_bound_of(receiver))]
63
64
  end
64
65
 
66
+ # Generalised iterator: every Enumerable-shaped collection
67
+ # in v0.0.4 yields `(element, index)` where the index is
68
+ # always `non-negative-int`. The element comes from the
69
+ # receiver's shape:
70
+ #
71
+ # - `Array[T]` / `Set[T]` / `Range[T]` → T
72
+ # - `Tuple[A, B, C]` → A | B | C
73
+ # (empty tuple cannot iterate, but we conservatively
74
+ # fall through to RBS so a missing rule never throws)
75
+ # - `Hash[K, V]` / `HashShape{...}` → Tuple[K, V]
76
+ # (Ruby yields `[key, value]` pairs as the element)
77
+ # - `Constant<Array>` / `Constant<Range>` / `Constant<Set>`
78
+ # → corresponding Constant element
79
+ #
80
+ # Receivers we cannot project (Top, Dynamic, unknown
81
+ # nominals, IO, …) decline so the RBS tier still answers
82
+ # — its element type is correct, only the index would
83
+ # widen to plain Integer.
84
+ def each_with_index_block_params(receiver)
85
+ element = element_type_of(receiver)
86
+ return nil if element.nil?
87
+
88
+ [element, Type::Combinator.non_negative_int]
89
+ end
90
+
91
+ ELEMENT_BY_NOMINAL = {
92
+ "Array" => :nominal_unary_element,
93
+ "Set" => :nominal_unary_element,
94
+ "Range" => :nominal_unary_element,
95
+ "Hash" => :nominal_hash_pair_element
96
+ }.freeze
97
+ private_constant :ELEMENT_BY_NOMINAL
98
+
99
+ def element_type_of(receiver)
100
+ case receiver
101
+ when Type::Tuple then tuple_element(receiver)
102
+ when Type::HashShape then hash_shape_pair_element(receiver)
103
+ when Type::Nominal then nominal_element(receiver)
104
+ when Type::Constant then constant_element(receiver)
105
+ end
106
+ end
107
+
108
+ def tuple_element(tuple)
109
+ return nil if tuple.elements.empty?
110
+ return tuple.elements.first if tuple.elements.size == 1
111
+
112
+ Type::Combinator.union(*tuple.elements)
113
+ end
114
+
115
+ def hash_shape_pair_element(shape)
116
+ return nil if shape.pairs.empty?
117
+
118
+ key = Type::Combinator.union(*shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) })
119
+ value = Type::Combinator.union(*shape.pairs.values)
120
+ Type::Combinator.tuple_of(key, value)
121
+ end
122
+
123
+ def nominal_element(nominal)
124
+ handler = ELEMENT_BY_NOMINAL[nominal.class_name]
125
+ return nil unless handler
126
+
127
+ send(handler, nominal)
128
+ end
129
+
130
+ def nominal_unary_element(nominal)
131
+ nominal.type_args.first
132
+ end
133
+
134
+ def nominal_hash_pair_element(nominal)
135
+ key, value = nominal.type_args
136
+ return nil if key.nil? || value.nil?
137
+
138
+ Type::Combinator.tuple_of(key, value)
139
+ end
140
+
141
+ def constant_element(constant)
142
+ case constant.value
143
+ when Array
144
+ return nil if constant.value.empty?
145
+
146
+ Type::Combinator.union(*constant.value.map { |v| Type::Combinator.constant_of(v) })
147
+ when Range
148
+ range_constant_element(constant.value)
149
+ end
150
+ end
151
+
152
+ def range_constant_element(range)
153
+ beg = range.begin
154
+ en = range.end
155
+ return Type::Combinator.constant_of(beg) if beg.is_a?(Integer) && beg == en
156
+
157
+ if beg.is_a?(Integer) && en.is_a?(Integer)
158
+ upper = range.exclude_end? ? en - 1 : en
159
+ return build_index_range(beg, upper)
160
+ end
161
+
162
+ # Mixed / non-integer ranges decline: the dispatcher
163
+ # falls through to RBS's element-type answer.
164
+ nil
165
+ end
166
+
65
167
  # `Constant<Integer>`, `IntegerRange`, and `Nominal[Integer]`
66
168
  # all participate. Non-integer types (Float, String, …) and
67
169
  # `Top`/`Dynamic` decline so the RBS tier answers.
@@ -53,13 +53,22 @@ module Rigor
53
53
  overloads = method_definition.method_types
54
54
  return nil if overloads.empty?
55
55
 
56
+ # `rigor:v1:param: <name> <refinement>` annotations on
57
+ # this method override the RBS-declared parameter type
58
+ # at the matching name. The map is consumed inside
59
+ # `accepts_param?` so overload selection sees the
60
+ # tighter type when filtering candidates by argument
61
+ # compatibility.
62
+ param_overrides = RbsExtended.param_type_override_map(method_definition)
63
+
56
64
  match = find_matching_overload(
57
65
  overloads,
58
66
  arg_types: arg_types,
59
67
  self_type: self_type,
60
68
  instance_type: instance_type,
61
69
  type_vars: type_vars,
62
- block_required: block_required
70
+ block_required: block_required,
71
+ param_overrides: param_overrides
63
72
  )
64
73
  return match if match
65
74
  return overloads.find { |mt| overload_has_block?(mt) } if block_required
@@ -76,7 +85,8 @@ module Rigor
76
85
  private
77
86
 
78
87
  # rubocop:disable Metrics/ParameterLists
79
- def find_matching_overload(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required:)
88
+ def find_matching_overload(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required:,
89
+ param_overrides:)
80
90
  overloads.find do |method_type|
81
91
  next false if block_required && !OverloadSelector.overload_has_block?(method_type)
82
92
 
@@ -85,13 +95,15 @@ module Rigor
85
95
  arg_types,
86
96
  self_type: self_type,
87
97
  instance_type: instance_type,
88
- type_vars: type_vars
98
+ type_vars: type_vars,
99
+ param_overrides: param_overrides
89
100
  )
90
101
  end
91
102
  end
92
103
  # rubocop:enable Metrics/ParameterLists
93
104
 
94
- def matches?(method_type, arg_types, self_type:, instance_type:, type_vars:)
105
+ # rubocop:disable Metrics/ParameterLists
106
+ def matches?(method_type, arg_types, self_type:, instance_type:, type_vars:, param_overrides:)
95
107
  return false if method_type.respond_to?(:type_params) && rejects_keyword_required?(method_type)
96
108
 
97
109
  fun = method_type.type
@@ -104,10 +116,12 @@ module Rigor
104
116
  arg,
105
117
  self_type: self_type,
106
118
  instance_type: instance_type,
107
- type_vars: type_vars
119
+ type_vars: type_vars,
120
+ param_overrides: param_overrides
108
121
  )
109
122
  end
110
123
  end
124
+ # rubocop:enable Metrics/ParameterLists
111
125
 
112
126
  # Slice 4 phase 2c does not pass keyword arguments through the
113
127
  # call site (caller passes only positional `arg_types`). An
@@ -152,8 +166,9 @@ module Rigor
152
166
  head
153
167
  end
154
168
 
155
- def accepts_param?(param, arg, self_type:, instance_type:, type_vars:)
156
- param_type = RbsTypeTranslator.translate(
169
+ # rubocop:disable Metrics/ParameterLists
170
+ def accepts_param?(param, arg, self_type:, instance_type:, type_vars:, param_overrides:)
171
+ param_type = param_overrides[param.name] || RbsTypeTranslator.translate(
157
172
  param.type,
158
173
  self_type: self_type,
159
174
  instance_type: instance_type,
@@ -162,6 +177,7 @@ module Rigor
162
177
  result = param_type.accepts(arg, mode: :gradual)
163
178
  result.yes? || result.maybe?
164
179
  end
180
+ # rubocop:enable Metrics/ParameterLists
165
181
  end
166
182
  end
167
183
  end
@@ -88,14 +88,25 @@ module Rigor
88
88
 
89
89
  # @return [Rigor::Type, nil] the precise element/value type, or
90
90
  # `nil` to defer to the next dispatcher tier.
91
+ # Per-carrier dispatch table. Adding a new carrier here
92
+ # is a one-row change; the helper methods stay private.
93
+ # Anonymous Type subclasses are not expected.
94
+ RECEIVER_HANDLERS = {
95
+ Type::Tuple => :dispatch_tuple,
96
+ Type::HashShape => :dispatch_hash_shape,
97
+ Type::Nominal => :dispatch_nominal_size,
98
+ Type::Difference => :dispatch_difference,
99
+ Type::Refined => :dispatch_refined,
100
+ Type::Intersection => :dispatch_intersection
101
+ }.freeze
102
+ private_constant :RECEIVER_HANDLERS
103
+
91
104
  def try_dispatch(receiver:, method_name:, args:)
92
105
  args ||= []
93
- case receiver
94
- when Type::Tuple then dispatch_tuple(receiver, method_name, args)
95
- when Type::HashShape then dispatch_hash_shape(receiver, method_name, args)
96
- when Type::Nominal then dispatch_nominal_size(receiver, method_name, args)
97
- when Type::Difference then dispatch_difference(receiver, method_name, args)
98
- end
106
+ handler = RECEIVER_HANDLERS[receiver.class]
107
+ return nil unless handler
108
+
109
+ send(handler, receiver, method_name, args)
99
110
  end
100
111
 
101
112
  # Tightens `Array#size` / `Array#length` / `String#length` /
@@ -220,6 +231,124 @@ module Rigor
220
231
  Type::Combinator.positive_int
221
232
  end
222
233
 
234
+ # Predicate-subset projections over a `Refined[base,
235
+ # predicate]` receiver. Today the catalogue is the
236
+ # String case-normalisation pair: `s.downcase` over a
237
+ # `lowercase-string` receiver folds to the same
238
+ # carrier (already lowercase), and `s.upcase` lifts a
239
+ # `lowercase-string` to `uppercase-string`. Symmetric
240
+ # rules apply with the predicates swapped. Numeric-
241
+ # string idempotence over `#downcase` / `#upcase` is
242
+ # also recognised because a numeric string equals its
243
+ # own case-normalisation.
244
+ #
245
+ # For methods this tier does not have a refinement-
246
+ # specific rule for, projection delegates to
247
+ # `dispatch_nominal_size` so size-returning calls on
248
+ # a `Refined[String, *]` still tighten to
249
+ # `non_negative_int`.
250
+ REFINED_STRING_PROJECTIONS = {
251
+ %i[lowercase downcase] => :refined_self,
252
+ %i[lowercase upcase] => :uppercase_string,
253
+ %i[uppercase upcase] => :refined_self,
254
+ %i[uppercase downcase] => :lowercase_string,
255
+ %i[numeric downcase] => :refined_self,
256
+ %i[numeric upcase] => :refined_self,
257
+ # Digit-only strings are case-invariant; the prefix
258
+ # letters in `0o…` / `0x…` are accepted by the
259
+ # predicate in either case so the predicate-subset
260
+ # is preserved across `#downcase` / `#upcase` even
261
+ # though the value-set element changes.
262
+ %i[decimal_int downcase] => :refined_self,
263
+ %i[decimal_int upcase] => :refined_self,
264
+ %i[octal_int downcase] => :refined_self,
265
+ %i[octal_int upcase] => :refined_self,
266
+ %i[hex_int downcase] => :refined_self,
267
+ %i[hex_int upcase] => :refined_self
268
+ }.freeze
269
+ private_constant :REFINED_STRING_PROJECTIONS
270
+
271
+ def dispatch_refined(refined, method_name, args)
272
+ base = refined.base
273
+ return nil unless base.is_a?(Type::Nominal)
274
+
275
+ if base.class_name == "String" && args.empty?
276
+ precise = refined_string_projection(refined, method_name)
277
+ return precise if precise
278
+ end
279
+
280
+ dispatch_nominal_size(base, method_name, args)
281
+ end
282
+
283
+ def refined_string_projection(refined, method_name)
284
+ handler = REFINED_STRING_PROJECTIONS[[refined.predicate_id, method_name]]
285
+ return nil unless handler
286
+
287
+ case handler
288
+ when :refined_self then refined
289
+ when :uppercase_string then Type::Combinator.uppercase_string
290
+ when :lowercase_string then Type::Combinator.lowercase_string
291
+ end
292
+ end
293
+
294
+ # Projects a method call over an `Intersection[M1, …]`
295
+ # receiver by collecting each member's projection and
296
+ # combining the results. The set-theoretic identity is
297
+ # `M(A ∩ B) ⊆ M(A) ∩ M(B)`, so the meet of the per-member
298
+ # projections is sound. Combining is best-effort:
299
+ #
300
+ # - If every result is a `Type::IntegerRange`, return
301
+ # their bounded-integer meet (max of lower bounds, min
302
+ # of upper bounds). This catches the common
303
+ # `(non_empty_string ∩ lowercase_string).size`
304
+ # pattern where one member projects to `positive-int`
305
+ # and the other to `non-negative-int`; the meet is
306
+ # `positive-int`.
307
+ # - Otherwise return the first non-nil result. A richer
308
+ # meet (e.g. of Difference + Refined results when both
309
+ # project) is left for a future slice; the carrier
310
+ # stays sound because every member's projection is
311
+ # already a superset of the true intersection.
312
+ #
313
+ # Returns nil when no member projects, so the caller
314
+ # falls through to the next dispatcher tier.
315
+ def dispatch_intersection(intersection, method_name, args)
316
+ results = intersection.members.filter_map do |member|
317
+ ShapeDispatch.try_dispatch(receiver: member, method_name: method_name, args: args)
318
+ end
319
+
320
+ case results.size
321
+ when 0 then nil
322
+ when 1 then results.first
323
+ else combine_intersection_results(results)
324
+ end
325
+ end
326
+
327
+ def combine_intersection_results(results)
328
+ return narrow_integer_ranges(results) if results.all?(Type::IntegerRange)
329
+
330
+ results.first
331
+ end
332
+
333
+ # Compute the bounded-integer meet of two or more
334
+ # `IntegerRange` carriers. We compare via the numeric
335
+ # `lower` / `upper` accessors (`-Float::INFINITY` /
336
+ # `Float::INFINITY` for the symbolic ends), then map
337
+ # back to the symbolic-bound representation
338
+ # `IntegerRange.new` expects. The disjoint-meet case
339
+ # cannot arise from sound member-wise projections in
340
+ # v0.0.4 but is guarded defensively to keep the
341
+ # carrier total.
342
+ def narrow_integer_ranges(ranges)
343
+ numeric_low = ranges.map(&:lower).max
344
+ numeric_high = ranges.map(&:upper).min
345
+ return Type::Combinator.bot if numeric_low > numeric_high
346
+
347
+ min = numeric_low == -Float::INFINITY ? Type::IntegerRange::NEG_INFINITY : numeric_low.to_i
348
+ max = numeric_high == Float::INFINITY ? Type::IntegerRange::POS_INFINITY : numeric_high.to_i
349
+ Type::Combinator.integer_range(min, max)
350
+ end
351
+
223
352
  def tuple_first(tuple, _method_name, args)
224
353
  return nil unless args.empty?
225
354
  return Type::Combinator.constant_of(nil) if tuple.elements.empty?
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "../type"
6
+ require_relative "../rbs_extended"
6
7
  require_relative "rbs_type_translator"
7
8
 
8
9
  module Rigor
@@ -59,10 +60,13 @@ module Rigor
59
60
  rbs_method = lookup_rbs_method(def_node)
60
61
  return types unless rbs_method
61
62
 
62
- method_types = rbs_method.method_types
63
- return types if method_types.empty?
64
-
65
- apply_rbs_overloads(types, slots, method_types)
63
+ apply_rbs_overloads(types, slots, rbs_method.method_types) unless rbs_method.method_types.empty?
64
+ # `rigor:v1:param: <name> <refinement>` annotations
65
+ # tighten the bound type for matching slots. Applied
66
+ # after the RBS-overload pass so the override is the
67
+ # authoritative answer regardless of what the RBS
68
+ # signature declared.
69
+ apply_param_overrides(types, slots, rbs_method)
66
70
  types
67
71
  end
68
72
 
@@ -165,6 +169,27 @@ module Rigor
165
169
  end
166
170
  end
167
171
 
172
+ # Reads the override map off the method's annotations and
173
+ # replaces the binding for any slot whose name appears in
174
+ # the map. Anonymous slots are skipped (no name to match).
175
+ # The override is used verbatim — no `:rest_*` re-wrapping —
176
+ # so authors who tighten a `*rest` parameter to e.g.
177
+ # `non-empty-array[Integer]` describe the parameter binding
178
+ # they actually want, not its element type.
179
+ def apply_param_overrides(types, slots, rbs_method)
180
+ override_map = RbsExtended.param_type_override_map(rbs_method)
181
+ return if override_map.empty?
182
+
183
+ slots.each do |slot|
184
+ next if slot.name.nil?
185
+
186
+ override = override_map[slot.name]
187
+ next if override.nil?
188
+
189
+ types[slot.name] = override
190
+ end
191
+ end
192
+
168
193
  def collect_translated_types(method_types, slot)
169
194
  rbs_types = method_types.flat_map do |mt|
170
195
  t = rbs_type_for_slot(mt.type, slot)
@@ -1029,6 +1029,8 @@ module Rigor
1029
1029
  # the effect's `negative?` flag. Shared between
1030
1030
  # predicate-if-* and assert-if-* application paths.
1031
1031
  def narrow_for_effect(current, effect, environment)
1032
+ return effect.refinement_type if effect.respond_to?(:refinement?) && effect.refinement?
1033
+
1032
1034
  if effect.negative?
1033
1035
  narrow_not_class(current, effect.class_name, exact: false, environment: environment)
1034
1036
  else
@@ -894,6 +894,8 @@ module Rigor
894
894
  end
895
895
 
896
896
  def narrow_for_assert_effect(current_type, effect, environment)
897
+ return effect.refinement_type if effect.refinement?
898
+
897
899
  if effect.negative?
898
900
  Narrowing.narrow_not_class(current_type, effect.class_name, exact: false, environment: environment)
899
901
  else