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.
- checksums.yaml +4 -4
- data/README.md +76 -79
- data/lib/rigor/analysis/baseline.rb +347 -0
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +68 -3
- data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
- data/lib/rigor/analysis/project_scan.rb +39 -0
- data/lib/rigor/analysis/runner.rb +309 -22
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/static_return_refinements.rb +142 -0
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/cli/baseline_command.rb +377 -0
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +142 -13
- data/lib/rigor/configuration.rb +58 -2
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +67 -2
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +119 -9
- data/lib/rigor/flow_contribution/fact.rb +20 -10
- data/lib/rigor/inference/acceptance.rb +48 -3
- data/lib/rigor/inference/expression_typer.rb +64 -2
- data/lib/rigor/inference/hkt_body.rb +171 -0
- data/lib/rigor/inference/hkt_body_parser.rb +363 -0
- data/lib/rigor/inference/hkt_reducer.rb +256 -0
- data/lib/rigor/inference/hkt_registry.rb +223 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +125 -30
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +32 -11
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher.rb +174 -6
- data/lib/rigor/inference/narrowing.rb +103 -1
- data/lib/rigor/inference/project_patched_methods.rb +70 -0
- data/lib/rigor/inference/project_patched_scanner.rb +210 -0
- data/lib/rigor/inference/scope_indexer.rb +209 -19
- data/lib/rigor/inference/statement_evaluator.rb +172 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +94 -16
- data/lib/rigor/language_server/buffer_table.rb +63 -0
- data/lib/rigor/language_server/completion_provider.rb +438 -0
- data/lib/rigor/language_server/debouncer.rb +86 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
- data/lib/rigor/language_server/folding_range_provider.rb +75 -0
- data/lib/rigor/language_server/hover_provider.rb +74 -0
- data/lib/rigor/language_server/hover_renderer.rb +312 -0
- data/lib/rigor/language_server/loop.rb +71 -0
- data/lib/rigor/language_server/project_context.rb +145 -0
- data/lib/rigor/language_server/selection_range_provider.rb +93 -0
- data/lib/rigor/language_server/server.rb +384 -0
- data/lib/rigor/language_server/signature_help_provider.rb +249 -0
- data/lib/rigor/language_server/synchronized_writer.rb +28 -0
- data/lib/rigor/language_server/uri.rb +40 -0
- data/lib/rigor/language_server.rb +29 -0
- data/lib/rigor/plugin/base.rb +63 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +127 -13
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- data/lib/rigor/plugin/manifest.rb +54 -7
- data/lib/rigor/plugin/registry.rb +19 -0
- data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
- data/lib/rigor/rbs_extended.rb +82 -2
- data/lib/rigor/sig_gen/generator.rb +12 -3
- data/lib/rigor/type/app.rb +107 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +10 -4
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor.rbs +4 -1
- 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
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
# `
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|