rigortype 0.1.5 → 0.1.7

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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -79
  3. data/lib/rigor/analysis/baseline.rb +347 -0
  4. data/lib/rigor/analysis/buffer_binding.rb +36 -0
  5. data/lib/rigor/analysis/check_rules.rb +68 -3
  6. data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
  7. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
  9. data/lib/rigor/analysis/project_scan.rb +39 -0
  10. data/lib/rigor/analysis/runner.rb +309 -22
  11. data/lib/rigor/analysis/worker_session.rb +14 -2
  12. data/lib/rigor/builtins/hkt_builtins.rb +342 -0
  13. data/lib/rigor/builtins/static_return_refinements.rb +142 -0
  14. data/lib/rigor/cache/store.rb +33 -3
  15. data/lib/rigor/cli/baseline_command.rb +377 -0
  16. data/lib/rigor/cli/lsp_command.rb +129 -0
  17. data/lib/rigor/cli/type_of_command.rb +44 -5
  18. data/lib/rigor/cli.rb +142 -13
  19. data/lib/rigor/configuration.rb +58 -2
  20. data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
  21. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  22. data/lib/rigor/environment/rbs_loader.rb +67 -2
  23. data/lib/rigor/environment/reporters.rb +40 -0
  24. data/lib/rigor/environment.rb +119 -9
  25. data/lib/rigor/flow_contribution/fact.rb +20 -10
  26. data/lib/rigor/inference/acceptance.rb +48 -3
  27. data/lib/rigor/inference/expression_typer.rb +64 -2
  28. data/lib/rigor/inference/hkt_body.rb +171 -0
  29. data/lib/rigor/inference/hkt_body_parser.rb +363 -0
  30. data/lib/rigor/inference/hkt_reducer.rb +256 -0
  31. data/lib/rigor/inference/hkt_registry.rb +223 -0
  32. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +125 -30
  33. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +32 -11
  34. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
  35. data/lib/rigor/inference/method_dispatcher.rb +174 -6
  36. data/lib/rigor/inference/narrowing.rb +103 -1
  37. data/lib/rigor/inference/project_patched_methods.rb +70 -0
  38. data/lib/rigor/inference/project_patched_scanner.rb +210 -0
  39. data/lib/rigor/inference/scope_indexer.rb +209 -19
  40. data/lib/rigor/inference/statement_evaluator.rb +172 -11
  41. data/lib/rigor/inference/synthetic_method_scanner.rb +94 -16
  42. data/lib/rigor/language_server/buffer_table.rb +63 -0
  43. data/lib/rigor/language_server/completion_provider.rb +438 -0
  44. data/lib/rigor/language_server/debouncer.rb +86 -0
  45. data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
  46. data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
  47. data/lib/rigor/language_server/folding_range_provider.rb +75 -0
  48. data/lib/rigor/language_server/hover_provider.rb +74 -0
  49. data/lib/rigor/language_server/hover_renderer.rb +312 -0
  50. data/lib/rigor/language_server/loop.rb +71 -0
  51. data/lib/rigor/language_server/project_context.rb +145 -0
  52. data/lib/rigor/language_server/selection_range_provider.rb +93 -0
  53. data/lib/rigor/language_server/server.rb +384 -0
  54. data/lib/rigor/language_server/signature_help_provider.rb +249 -0
  55. data/lib/rigor/language_server/synchronized_writer.rb +28 -0
  56. data/lib/rigor/language_server/uri.rb +40 -0
  57. data/lib/rigor/language_server.rb +29 -0
  58. data/lib/rigor/plugin/base.rb +63 -0
  59. data/lib/rigor/plugin/macro/heredoc_template.rb +127 -13
  60. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  61. data/lib/rigor/plugin/manifest.rb +54 -7
  62. data/lib/rigor/plugin/registry.rb +19 -0
  63. data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
  64. data/lib/rigor/rbs_extended.rb +82 -2
  65. data/lib/rigor/sig_gen/generator.rb +12 -3
  66. data/lib/rigor/type/app.rb +107 -0
  67. data/lib/rigor/type.rb +1 -0
  68. data/lib/rigor/version.rb +1 -1
  69. data/sig/rigor/environment.rbs +10 -4
  70. data/sig/rigor/inference.rbs +2 -0
  71. data/sig/rigor.rbs +4 -1
  72. metadata +56 -1
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "hkt_body"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # ADR-20 § "Decision D1 / D2" — registry of Lightweight HKT
8
+ # tag registrations + type-function bodies parsed off the
9
+ # `%a{rigor:v1:hkt_register: ...}` /
10
+ # `%a{rigor:v1:hkt_define: ...}` annotations in shipped
11
+ # `.rbs` files.
12
+ #
13
+ # Slice 1 keeps the registry **opaque**: it stores the
14
+ # registration metadata (arity, variance, bound) and the
15
+ # un-evaluated definition body (a raw String — Slice 2
16
+ # introduces the conditional / indexed-access evaluator that
17
+ # parses the body and reduces `Type::App` instances against
18
+ # it). The carrier never needs to read from the registry
19
+ # because Slice 1's `Type::App` carries its `bound` directly;
20
+ # the registry exists at this slice solely so the parser
21
+ # round-trip and downstream slices have a stable target API.
22
+ #
23
+ # The registry is immutable after construction. Callers that
24
+ # need to extend it (e.g. plugin registrations layered on top
25
+ # of stdlib registrations) MUST build a new registry via
26
+ # `merge` rather than mutating an existing one. This keeps the
27
+ # registry shareable across Ractor boundaries per ADR-15.
28
+ class HktRegistry
29
+ # Frozen value object recording one tag registration.
30
+ #
31
+ # - `uri`: namespaced Symbol per ADR-20 WD1 (must include
32
+ # `"::"`).
33
+ # - `arity`: positive Integer — the number of formal
34
+ # parameters the registered constructor takes.
35
+ # - `variance`: ordered Array of Symbols, one per
36
+ # parameter, each `:out` (covariant), `:in`
37
+ # (contravariant), or `:inv` (invariant; default).
38
+ # - `bound`: a `Rigor::Type` to erase to when an `App`
39
+ # referring to this URI cannot be reduced. Defaults to
40
+ # `Dynamic[Top]` (the parser fills in the default when
41
+ # the annotation omits `bound:`).
42
+ Registration = Data.define(:uri, :arity, :variance, :bound) do
43
+ def initialize(uri:, arity:, variance:, bound:)
44
+ raise ArgumentError, "uri must be a Symbol, got #{uri.class}" unless uri.is_a?(Symbol)
45
+ raise ArgumentError, "uri must be namespaced as `:a::b`, got #{uri.inspect}" unless uri.to_s.include?("::")
46
+ unless arity.is_a?(Integer) && arity.positive?
47
+ raise ArgumentError,
48
+ "arity must be a positive Integer, got #{arity.inspect}"
49
+ end
50
+ raise ArgumentError, "variance must be an Array, got #{variance.class}" unless variance.is_a?(Array)
51
+ raise ArgumentError, "variance must have #{arity} entries, got #{variance.size}" unless variance.size == arity
52
+
53
+ variance.each do |v|
54
+ unless %i[out in inv].include?(v)
55
+ raise ArgumentError, "variance entries must be :out, :in, or :inv, got #{v.inspect}"
56
+ end
57
+ end
58
+ raise ArgumentError, "bound must not be nil" if bound.nil?
59
+
60
+ super(uri: uri, arity: arity, variance: variance.dup.freeze, bound: bound)
61
+ end
62
+ end
63
+
64
+ # Frozen value object recording one type-function
65
+ # definition.
66
+ #
67
+ # `body` is the raw String payload from the `%a{...}`
68
+ # annotation (Slice 1's parser populates it). It stays
69
+ # opaque until Slice 2b's body-string parser lands.
70
+ #
71
+ # `body_tree` is the optional evaluable form: a
72
+ # `Rigor::Inference::HktBody::*` node tree the Slice 2a
73
+ # reducer walks against the application's concrete
74
+ # arguments. Plugin and Rigor-bundled overlay authors
75
+ # construct it programmatically through
76
+ # {with_body_tree}; the Slice 2b string parser will set
77
+ # it from `body` once it ships. The reducer treats a
78
+ # `nil` `body_tree` as "definition not yet evaluable"
79
+ # and returns the registered bound.
80
+ Definition = Data.define(:uri, :params, :body, :body_tree, :source_path, :source_line) do
81
+ def initialize(uri:, params:, body:, body_tree: nil, source_path: nil, source_line: nil)
82
+ raise ArgumentError, "uri must be a Symbol, got #{uri.class}" unless uri.is_a?(Symbol)
83
+ raise ArgumentError, "params must be an Array, got #{params.class}" unless params.is_a?(Array)
84
+
85
+ params.each do |p|
86
+ raise ArgumentError, "params entries must be Symbols, got #{p.inspect}" unless p.is_a?(Symbol)
87
+ end
88
+ raise ArgumentError, "body must be a String, got #{body.class}" unless body.is_a?(String)
89
+
90
+ super(
91
+ uri: uri,
92
+ params: params.dup.freeze,
93
+ body: body,
94
+ body_tree: body_tree,
95
+ source_path: source_path,
96
+ source_line: source_line
97
+ )
98
+ end
99
+ end
100
+
101
+ # Convenience constructor for callers that have a body
102
+ # tree but no raw String — typically Rigor-bundled HKT
103
+ # overlays that build the body programmatically. The
104
+ # raw `body` slot is filled with an empty placeholder
105
+ # so existing consumers keep their type contract.
106
+ def self.definition_with_body_tree(uri:, params:, body_tree:, source_path: nil, source_line: nil)
107
+ Definition.new(
108
+ uri: uri,
109
+ params: params,
110
+ body: "",
111
+ body_tree: body_tree,
112
+ source_path: source_path,
113
+ source_line: source_line
114
+ )
115
+ end
116
+
117
+ attr_reader :registrations, :definitions
118
+
119
+ # @param registrations [Array<Registration>]
120
+ # @param definitions [Array<Definition>]
121
+ def initialize(registrations: [], definitions: [])
122
+ @registrations = registrations.to_h { |r| [r.uri, r] }.freeze
123
+ @definitions = definitions.to_h { |d| [d.uri, d] }.freeze
124
+ freeze
125
+ end
126
+
127
+ def registered?(uri)
128
+ @registrations.key?(uri)
129
+ end
130
+
131
+ def defined?(uri)
132
+ @definitions.key?(uri)
133
+ end
134
+
135
+ def registration(uri)
136
+ @registrations[uri]
137
+ end
138
+
139
+ def definition(uri)
140
+ @definitions[uri]
141
+ end
142
+
143
+ # @return [HktRegistry] a new registry whose entries are
144
+ # the union of this registry's and `other`'s. On URI
145
+ # collisions `other`'s entries win (last-write-wins; OQ3
146
+ # tentative).
147
+ def merge(other)
148
+ raise ArgumentError, "merge target must be an HktRegistry, got #{other.class}" unless other.is_a?(HktRegistry)
149
+
150
+ self.class.new(
151
+ registrations: @registrations.merge(other.registrations).values,
152
+ definitions: @definitions.merge(other.definitions).values
153
+ )
154
+ end
155
+
156
+ def empty?
157
+ @registrations.empty? && @definitions.empty?
158
+ end
159
+
160
+ # ADR-20 Slice 2a — reduce an `App` against this
161
+ # registry. Convenience wrapper around `HktReducer.new(self).reduce`.
162
+ # Each call allocates a fresh reducer; concurrent
163
+ # reductions are safe.
164
+ def reduce(app, fuel: HktReducer::DEFAULT_FUEL)
165
+ HktReducer.new(self).reduce(app, fuel: fuel)
166
+ end
167
+
168
+ # ADR-20 slice 2e — scan a Rigor RbsLoader for
169
+ # `rigor:v1:hkt_register` / `rigor:v1:hkt_define`
170
+ # annotations attached to class- or module-level
171
+ # declarations in the loaded RBS env, parse them via
172
+ # {Rigor::RbsExtended::HktDirectives}, and return a new
173
+ # registry that is the union of `base` and every parsed
174
+ # entry. Last-write-wins on URI collisions per
175
+ # {#merge}'s contract. Fail-soft on per-annotation parse
176
+ # errors (the reporter records an `:info` entry; the
177
+ # other annotations still apply).
178
+ #
179
+ # @param rbs_loader [Rigor::Environment::RbsLoader]
180
+ # @param base [HktRegistry] starting registry (typically
181
+ # the bundled `Rigor::Builtins::HktBuiltins.registry`).
182
+ # @param name_scope [Rigor::Environment::NameScope, nil]
183
+ # threaded through to the bound resolver for class-name
184
+ # lookups; safe to omit during scanning since hkt
185
+ # bounds are typically `untyped` or stdlib classes.
186
+ # @param reporter [#record, nil] same fail-soft reporter
187
+ # contract the other RBS-extended parsers use.
188
+ def self.scan_rbs_loader(rbs_loader, base: EMPTY, name_scope: nil, reporter: nil)
189
+ return base if rbs_loader.nil?
190
+
191
+ # Required lazily here to avoid a hard circular
192
+ # require between hkt_registry / hkt_directives;
193
+ # HktDirectives requires HktRegistry to construct its
194
+ # value objects.
195
+ require_relative "../rbs_extended/hkt_directives"
196
+
197
+ registrations = []
198
+ definitions = []
199
+
200
+ rbs_loader.each_class_decl_annotation do |annotation_string, source_location|
201
+ reg = Rigor::RbsExtended::HktDirectives.parse_register(
202
+ annotation_string, name_scope: name_scope, reporter: reporter, source_location: source_location
203
+ )
204
+ registrations << reg if reg
205
+
206
+ defn = Rigor::RbsExtended::HktDirectives.parse_define(
207
+ annotation_string, reporter: reporter, source_location: source_location
208
+ )
209
+ definitions << defn if defn
210
+ end
211
+
212
+ return base if registrations.empty? && definitions.empty?
213
+
214
+ overlay = new(registrations: registrations, definitions: definitions)
215
+ base.merge(overlay)
216
+ end
217
+
218
+ EMPTY = new.freeze
219
+ end
220
+ end
221
+ end
222
+
223
+ require_relative "hkt_reducer"
@@ -3,6 +3,7 @@
3
3
  require_relative "../../type"
4
4
  require_relative "../acceptance"
5
5
  require_relative "../rbs_type_translator"
6
+ require_relative "receiver_affinity"
6
7
 
7
8
  module Rigor
8
9
  module Inference
@@ -44,6 +45,33 @@ module Rigor
44
45
  module OverloadSelector
45
46
  module_function
46
47
 
48
+ # Canonical RBS-core aliases shipped by `core/builtin.rbs`
49
+ # whose body is `<Nominal> | _DuckType`. Matching an
50
+ # overload against an Integer literal should pick the
51
+ # `(int) -> Array[Elem]` body over the `(string) -> String`
52
+ # body because Integer satisfies `int`'s strict arm and
53
+ # not `string`'s. The translator collapses both aliases
54
+ # to `Dynamic[Top]` (interfaces are not structurally
55
+ # matched yet), so a dedicated pass 1.5 between strict
56
+ # and gradual consults this map to pick the alias whose
57
+ # strict arm matches.
58
+ #
59
+ # Symbol keys are the alias names as they appear under
60
+ # `RBS::Types::Alias#name.to_s` (the `name` is a
61
+ # `TypeName` whose `to_s` includes the `::` prefix).
62
+ # Values are an Array of class names whose Nominal[..]
63
+ # form is the alias's strict-arm matcher.
64
+ ALIAS_STRICT_NOMINALS = {
65
+ "::int" => ["Integer"],
66
+ "::string" => ["String"],
67
+ "::interned" => %w[Symbol String],
68
+ "::io" => ["IO"],
69
+ "::encoding" => %w[Encoding String],
70
+ "::path" => ["String"],
71
+ "::boolean" => %w[TrueClass FalseClass]
72
+ }.freeze
73
+ private_constant :ALIAS_STRICT_NOMINALS
74
+
47
75
  # @param method_definition [RBS::Definition::Method]
48
76
  # @param arg_types [Array<Rigor::Type>] caller-provided types in
49
77
  # positional order. Empty when there are no arguments.
@@ -77,36 +105,19 @@ module Rigor
77
105
  # compatibility.
78
106
  param_overrides = RbsExtended.param_type_override_map(method_definition, environment: environment)
79
107
 
80
- # Pass 1: prefer overloads whose param types stay strict —
81
- # no translator-induced `Dynamic[Top]` from Alias /
82
- # Interface / Intersection. The pass is skipped
83
- # entirely when any arg is `Dynamic[Top]` (literally
84
- # `untyped`), because gradual acceptance against an
85
- # untyped arg accepts every param indiscriminately and
86
- # would let pass 1 lock in an arbitrary strict overload
87
- # (e.g. `Regexp#=~(nil) -> nil` over the
88
- # `(::interned?) -> Integer?` overload). Pass 2 falls
89
- # back to the original gradual matcher so overloads
90
- # that legitimately rely on duck-typed params still
91
- # resolve when nothing stricter applies.
92
- match = find_matching_overload(
93
- overloads,
94
- arg_types: arg_types,
95
- self_type: self_type,
96
- instance_type: instance_type,
97
- type_vars: type_vars,
98
- block_required: block_required,
99
- param_overrides: param_overrides,
100
- strict: true
101
- ) || find_matching_overload(
102
- overloads,
103
- arg_types: arg_types,
104
- self_type: self_type,
105
- instance_type: instance_type,
106
- type_vars: type_vars,
107
- block_required: block_required,
108
- param_overrides: param_overrides,
109
- strict: false
108
+ # Pre-sort: demote overloads whose param class is a
109
+ # disjoint sibling of the receiver class (e.g.
110
+ # `Integer#+(BigDecimal) -> BigDecimal` from the
111
+ # `bigdecimal` RBS reopen). Honors the coerce
112
+ # convention so `5 + ?` for unknown `?` resolves to
113
+ # the receiver-class-preserving arm rather than an
114
+ # arbitrary sibling-class arm that only wins by
115
+ # overload-list position.
116
+ overloads = ReceiverAffinity.reorder(overloads, self_type: self_type, environment: environment)
117
+
118
+ match = run_selection_passes(
119
+ overloads, arg_types: arg_types, self_type: self_type, instance_type: instance_type,
120
+ type_vars: type_vars, block_required: block_required, param_overrides: param_overrides
110
121
  )
111
122
  return match if match
112
123
  return overloads.find { |mt| overload_has_block?(mt) } if block_required
@@ -121,6 +132,30 @@ module Rigor
121
132
  class << self
122
133
  private
123
134
 
135
+ # Three-pass overload search:
136
+ # - Pass 1 (strict): skipped when any arg is
137
+ # `Dynamic[Top]`, because gradual acceptance against
138
+ # an untyped arg accepts every param indiscriminately
139
+ # and would let pass 1 lock in an arbitrary strict
140
+ # overload (e.g. `Regexp#=~(nil) -> nil` over the
141
+ # `(::interned?) -> Integer?` overload).
142
+ # - Pass 1.5 (alias-resolved): consults each `RBS::Types::Alias`'s
143
+ # strict arm so e.g. `Array#*(int)` wins over the
144
+ # `Array#*(string) -> String` overload for Integer args.
145
+ # - Pass 2 (gradual): the original gradual matcher so
146
+ # overloads that legitimately rely on duck-typed
147
+ # params still resolve when nothing stricter applies.
148
+ def run_selection_passes(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required:,
149
+ param_overrides:)
150
+ shared = {
151
+ arg_types: arg_types, self_type: self_type, instance_type: instance_type,
152
+ type_vars: type_vars, block_required: block_required, param_overrides: param_overrides
153
+ }
154
+ find_matching_overload(overloads, **shared, strict: true) ||
155
+ find_matching_overload_via_aliases(overloads, arg_types: arg_types, block_required: block_required) ||
156
+ find_matching_overload(overloads, **shared, strict: false)
157
+ end
158
+
124
159
  # rubocop:disable Metrics/ParameterLists
125
160
  def find_matching_overload(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required:,
126
161
  param_overrides:, strict:)
@@ -150,6 +185,66 @@ module Rigor
150
185
  type.is_a?(Type::Dynamic) && type.static_facet.is_a?(Type::Top)
151
186
  end
152
187
 
188
+ # Pass 1.5: for arity-compatible overloads whose every
189
+ # positional param is either a strict nominal OR a
190
+ # well-known core alias (`int` / `string` / `interned`
191
+ # / etc.), check the arg against the alias's STRICT
192
+ # arm. An Integer literal arg matches `int` here but
193
+ # not `string`, so `Array#*(int)` wins over the
194
+ # `Array#*(string) -> String` overload — even though
195
+ # both translate to `Dynamic[Top]` at the param level.
196
+ # Only fires when EVERY positional param has a known
197
+ # alias-or-strict shape; otherwise gradual matching
198
+ # takes over.
199
+ def find_matching_overload_via_aliases(overloads, arg_types:, block_required:)
200
+ overloads.find do |method_type|
201
+ next false if block_required && !OverloadSelector.overload_has_block?(method_type)
202
+
203
+ fun = method_type.type
204
+ next false unless arity_compatible?(fun, arg_types.size)
205
+
206
+ params = positional_params_for(fun, arg_types.size)
207
+ next false unless params.size == arg_types.size
208
+
209
+ params.zip(arg_types).all? { |param, arg| alias_param_accepts?(param.type, arg) }
210
+ end
211
+ end
212
+
213
+ # Checks the param's RBS type against an arg using
214
+ # alias-strict-arm matching. Optional / Union wrappers
215
+ # are flattened; alias resolution is one level deep
216
+ # (the canonical core aliases all have non-alias
217
+ # strict arms).
218
+ def alias_param_accepts?(rbs_type, arg)
219
+ nominal_names = strict_nominal_names_for(rbs_type)
220
+ return false if nominal_names.nil? || nominal_names.empty?
221
+
222
+ nominal_names.any? do |class_name|
223
+ result = Type::Combinator.nominal_of(class_name).accepts(arg, mode: :gradual)
224
+ result.yes? || result.maybe?
225
+ end
226
+ end
227
+
228
+ # Returns the candidate class names a param's RBS type
229
+ # accepts under alias-resolved strict matching, or nil
230
+ # when the shape cannot be reduced to a closed set of
231
+ # nominals (e.g. an Interface or an unrecognised alias).
232
+ def strict_nominal_names_for(rbs_type)
233
+ case rbs_type
234
+ when RBS::Types::ClassInstance
235
+ [rbs_type.name.to_s.delete_prefix("::")]
236
+ when RBS::Types::Alias
237
+ ALIAS_STRICT_NOMINALS[rbs_type.name.to_s]
238
+ when RBS::Types::Optional
239
+ strict_nominal_names_for(rbs_type.type)
240
+ when RBS::Types::Union
241
+ parts = rbs_type.types.map { |t| strict_nominal_names_for(t) }
242
+ return nil if parts.any?(&:nil?)
243
+
244
+ parts.flatten
245
+ end
246
+ end
247
+
153
248
  # Returns true when every positional param the call
154
249
  # site engages translates to a non-`Dynamic[Top]`
155
250
  # carrier. Alias / Interface / Intersection RBS types
@@ -74,10 +74,21 @@ module Rigor
74
74
  # and binds the method-level type parameter that the
75
75
  # block's return type references to `block_type` (Slice 6
76
76
  # phase C sub-phase 2).
77
+ # @param self_type_override [Rigor::Type, nil] when set,
78
+ # the substitution for `Bases::Self` in the method's
79
+ # return type. Used by `MethodDispatcher#try_user_class_fallback`
80
+ # to preserve the ORIGINAL receiver as the substitute
81
+ # for `self` even though the dispatch is routed through
82
+ # `Nominal[Object]` — so that `Bundler::URI::Generic.dup`
83
+ # (which resolves through the `Object` fallback because
84
+ # `Bundler::URI::Generic` lacks RBS) returns
85
+ # `Bundler::URI::Generic` per `Kernel#dup: () -> self`
86
+ # rather than `Object`. Defaults to nil (compute self
87
+ # from the resolved class_name as before).
77
88
  # @return [Rigor::Type, nil] inferred return type, or `nil`
78
89
  # when no rule resolves (no class name, no method, dispatch
79
90
  # on a Top/Dynamic[Top] receiver, etc.).
80
- def try_dispatch(receiver:, method_name:, args:, environment:, block_type: nil)
91
+ def try_dispatch(receiver:, method_name:, args:, environment:, block_type: nil, self_type_override: nil)
81
92
  return nil if environment.nil?
82
93
  return nil unless environment.rbs_loader
83
94
 
@@ -86,7 +97,8 @@ module Rigor
86
97
  method_name: method_name,
87
98
  args: args,
88
99
  environment: environment,
89
- block_type: block_type
100
+ block_type: block_type,
101
+ self_type_override: self_type_override
90
102
  )
91
103
  end
92
104
 
@@ -128,26 +140,26 @@ module Rigor
128
140
  class << self
129
141
  private
130
142
 
131
- def dispatch_for(receiver:, method_name:, args:, environment:, block_type:)
143
+ def dispatch_for(receiver:, method_name:, args:, environment:, block_type:, self_type_override: nil)
132
144
  args ||= []
133
145
  case receiver
134
146
  when Type::Union
135
- dispatch_union(receiver, method_name, args, environment, block_type)
147
+ dispatch_union(receiver, method_name, args, environment, block_type, self_type_override)
136
148
  else
137
- dispatch_one(receiver, method_name, args, environment, block_type)
149
+ dispatch_one(receiver, method_name, args, environment, block_type, self_type_override)
138
150
  end
139
151
  end
140
152
 
141
- def dispatch_union(receiver, method_name, args, environment, block_type)
153
+ def dispatch_union(receiver, method_name, args, environment, block_type, self_type_override = nil)
142
154
  results = receiver.members.map do |member|
143
- dispatch_one(member, method_name, args, environment, block_type)
155
+ dispatch_one(member, method_name, args, environment, block_type, self_type_override)
144
156
  end
145
157
  return nil if results.any?(&:nil?)
146
158
 
147
159
  Type::Combinator.union(*results)
148
160
  end
149
161
 
150
- def dispatch_one(receiver, method_name, args, environment, block_type)
162
+ def dispatch_one(receiver, method_name, args, environment, block_type, self_type_override = nil)
151
163
  descriptor = receiver_descriptor(receiver)
152
164
  return nil unless descriptor
153
165
 
@@ -163,7 +175,8 @@ module Rigor
163
175
  args: args,
164
176
  type_vars: type_vars,
165
177
  block_type: block_type,
166
- environment: environment
178
+ environment: environment,
179
+ self_type_override: self_type_override
167
180
  )
168
181
  rescue StandardError
169
182
  # Defensive: if RBS' definition builder raises on a broken
@@ -254,8 +267,10 @@ module Rigor
254
267
  param_names.zip(receiver_args).to_h
255
268
  end
256
269
 
270
+ # rubocop:disable Metrics/ParameterLists
257
271
  def translate_return_type(method_definition, class_name:, kind:, args:, type_vars:, block_type:,
258
- environment: nil)
272
+ environment: nil, self_type_override: nil)
273
+ # rubocop:enable Metrics/ParameterLists
259
274
  # Slice 4b-3 (ADR-7 § "Slice 4-A/4-B") — read the
260
275
  # return-type override through the merger so future
261
276
  # plugin / `:rbs_extended` bundles that also assert a
@@ -266,11 +281,17 @@ module Rigor
266
281
  return override if override
267
282
 
268
283
  instance_type = Type::Combinator.nominal_of(class_name)
269
- self_type =
284
+ resolved_self_type =
270
285
  case kind
271
286
  when :singleton then Type::Combinator.singleton_of(class_name)
272
287
  else instance_type
273
288
  end
289
+ # `self_type_override` lets the user-class fallback
290
+ # path preserve the ORIGINAL receiver as the substitute
291
+ # for `Bases::Self` — so `Kernel#dup: () -> self`
292
+ # resolved through the Object fallback returns the
293
+ # caller's type, not Object.
294
+ self_type = self_type_override || resolved_self_type
274
295
 
275
296
  method_type = OverloadSelector.select(
276
297
  method_definition,
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # Stable-sort an overload list so that "receiver-affinity"
9
+ # arms come first. An overload is receiver-affinity-matching
10
+ # when every positional param's class equals `self_type`'s
11
+ # class name OR is one of its proper RBS ancestors. The
12
+ # canonical case the helper exists for: when `bigdecimal`'s
13
+ # stdlib RBS reopens `Integer#+` at the FRONT of the
14
+ # overload list with `(BigDecimal) -> BigDecimal`, that
15
+ # disjoint-sibling arm would win every dispatch for
16
+ # `Integer#+(?)` by overload-list position alone, returning
17
+ # a spurious `BigDecimal` for plain integer arithmetic.
18
+ # Demoting the arm honours the coerce convention: when the
19
+ # arg type is unknown or itself an Integer, the
20
+ # receiver-preserving `(Integer) -> Integer` arm should win.
21
+ #
22
+ # No-op when (a) the environment can't answer
23
+ # `class_ordering` (nil env), or (b) the receiver isn't a
24
+ # nominal / singleton carrying a class name. The partition
25
+ # is stable, so within each bucket the RBS-declared order
26
+ # is preserved.
27
+ module ReceiverAffinity
28
+ module_function
29
+
30
+ def reorder(overloads, self_type:, environment:)
31
+ return overloads if environment.nil?
32
+
33
+ self_class_name = self_type_class_name(self_type)
34
+ return overloads if self_class_name.nil?
35
+
36
+ affinity, other = overloads.partition do |mt|
37
+ overload_param_classes_in_ancestry?(mt, self_class_name, environment)
38
+ end
39
+ affinity + other
40
+ end
41
+
42
+ class << self
43
+ private
44
+
45
+ def self_type_class_name(self_type)
46
+ case self_type
47
+ when Type::Nominal, Type::Singleton then self_type.class_name
48
+ end
49
+ end
50
+
51
+ def overload_param_classes_in_ancestry?(method_type, self_class_name, environment)
52
+ fun = method_type.type
53
+ params = fun.required_positionals + fun.optional_positionals + fun.trailing_positionals
54
+ return false if params.empty?
55
+
56
+ params.all? { |param| param_class_in_ancestry?(param.type, self_class_name, environment) }
57
+ end
58
+
59
+ # Walks Optional and Union one level so `(Numeric?)` and
60
+ # `(Integer | Float)` still classify when every branch
61
+ # sits in the ancestry. Non-`ClassInstance` shapes
62
+ # (Alias / Interface / Intersection / type variables)
63
+ # don't carry a clean class identity and therefore
64
+ # disqualify the overload from the affinity bucket.
65
+ def param_class_in_ancestry?(rbs_type, self_class_name, environment)
66
+ case rbs_type
67
+ when RBS::Types::ClassInstance
68
+ class_in_ancestry?(rbs_type.name.to_s.delete_prefix("::"), self_class_name, environment)
69
+ when RBS::Types::Optional
70
+ param_class_in_ancestry?(rbs_type.type, self_class_name, environment)
71
+ when RBS::Types::Union
72
+ rbs_type.types.all? { |t| param_class_in_ancestry?(t, self_class_name, environment) }
73
+ else
74
+ false
75
+ end
76
+ end
77
+
78
+ def class_in_ancestry?(param_class_name, self_class_name, environment)
79
+ return true if param_class_name == self_class_name
80
+
81
+ environment.class_ordering(self_class_name, param_class_name) == :subclass
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end