Skip to content

Commit d7e471f

Browse files
committed
Add keyword argument type checking to overload resolution
1 parent 33f9440 commit d7e471f

3 files changed

Lines changed: 90 additions & 8 deletions

File tree

lib/typeprof/core/graph/box.rb

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,19 @@ def match_arguments?(genv, changes, param_map, a_args, method_type)
179179
end
180180
end
181181

182+
# Check keyword arguments by inspecting the keywords vertex types
183+
# directly. We avoid get_keyword_arg here because it creates a fresh
184+
# Vertex each call, which would destabilize the change-set edges and
185+
# cause oscillation when match_arguments? runs on every box re-eval.
186+
if a_args.keywords
187+
method_type.req_keyword_keys.zip(method_type.req_keyword_values) do |key, ty|
188+
return false unless keyword_arg_typecheck?(genv, changes, a_args.keywords, key, ty, param_map)
189+
end
190+
method_type.opt_keyword_keys.zip(method_type.opt_keyword_values) do |key, ty|
191+
return false unless keyword_arg_typecheck?(genv, changes, a_args.keywords, key, ty, param_map)
192+
end
193+
end
194+
182195
return true
183196
end
184197

@@ -260,21 +273,25 @@ def resolve_overloads(changes, genv, node, param_map, a_args, ret, &blk)
260273
#
261274
# Top-level empty vertices are always uninformative. For type
262275
# parameter vertices (e.g., Array[T], Hash[K,V], tuples), we
263-
# only recurse when overloads differ in their positional parameter
264-
# types -- otherwise empty type params (like those of `{}`) cannot
265-
# cause oscillation and should not trigger bail-out.
266-
overloads_differ_in_positionals = !@method_types.each_cons(2).all? {|mt1, mt2|
267-
positionals_match?(mt1, mt2)
276+
# only recurse when overloads differ in their positional or keyword
277+
# parameter types -- otherwise empty type params (like those of
278+
# `{}`) cannot cause oscillation and should not trigger bail-out.
279+
overloads_differ = !@method_types.each_cons(2).all? {|mt1, mt2|
280+
positionals_match?(mt1, mt2) && keywords_match?(mt1, mt2)
268281
}
269-
has_uninformative_args = if overloads_differ_in_positionals
270-
a_args.positionals.any? {|vtx| vertex_uninformative?(genv, vtx) }
282+
has_uninformative_args = if overloads_differ
283+
a_args.positionals.any? {|vtx| vertex_uninformative?(genv, vtx) } ||
284+
(a_args.keywords && vertex_uninformative?(genv, a_args.keywords))
271285
else
272-
a_args.positionals.any? {|vtx| vtx.types.empty? }
286+
a_args.positionals.any? {|vtx| vtx.types.empty? } ||
287+
(a_args.keywords && a_args.keywords.types.empty?)
273288
end
274289
if has_uninformative_args
275290
a_args.positionals.each do |vtx|
276291
changes.add_edge(genv, vtx, changes.target)
277292
end
293+
# Note: keywords already have a permanent edge to the box
294+
# (established in MethodCallBox#initialize), so no extra edge needed.
278295
return
279296
end
280297

@@ -303,6 +320,22 @@ def vertex_uninformative?(genv, vtx, depth = 0)
303320
false
304321
end
305322

323+
# Typecheck a single keyword argument value against the expected type
324+
# by directly inspecting the pre-existing value vertices in the
325+
# keywords vertex's types (Record, Hash, Instance).
326+
def keyword_arg_typecheck?(genv, changes, keywords_vtx, key, expected_ty, param_map)
327+
keywords_vtx.each_type do |kw_ty|
328+
val_vtx = case kw_ty
329+
when Type::Hash then kw_ty.get_value(key)
330+
when Type::Record then kw_ty.get_value(key)
331+
when Type::Instance then kw_ty.mod == genv.mod_hash ? kw_ty.args[1] : nil
332+
else nil
333+
end
334+
return false if val_vtx && !expected_ty.typecheck(genv, changes, val_vtx, param_map)
335+
end
336+
true
337+
end
338+
306339
# Check if two method types have structurally identical positional
307340
# parameter types (req, opt, rest).
308341
def positionals_match?(mt1, mt2)
@@ -314,6 +347,17 @@ def positionals_match?(mt1, mt2)
314347
(mt1.rest_positionals.nil? || sig_types_match?(mt1.rest_positionals, mt2.rest_positionals))
315348
end
316349

350+
# Check if two method types have structurally identical keyword
351+
# parameter types (req, opt, rest).
352+
def keywords_match?(mt1, mt2)
353+
return false unless mt1.req_keyword_keys == mt2.req_keyword_keys
354+
return false unless mt1.opt_keyword_keys == mt2.opt_keyword_keys
355+
return false unless mt1.rest_keywords.nil? == mt2.rest_keywords.nil?
356+
mt1.req_keyword_values.zip(mt2.req_keyword_values).all? {|a, b| sig_types_match?(a, b) } &&
357+
mt1.opt_keyword_values.zip(mt2.opt_keyword_values).all? {|a, b| sig_types_match?(a, b) } &&
358+
(mt1.rest_keywords.nil? || sig_types_match?(mt1.rest_keywords, mt2.rest_keywords))
359+
end
360+
317361
# Structural equality check for two SigTyNode objects.
318362
def sig_types_match?(a, b)
319363
return false unless a.class == b.class

scenario/rbs/keyword-overload.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## update: test.rbs
2+
class C
3+
def foo: (key: Integer) -> Integer | (key: String) -> String
4+
end
5+
6+
## update: test.rb
7+
class C
8+
def bar
9+
foo(key: 1)
10+
end
11+
def baz
12+
foo(key: "s")
13+
end
14+
end
15+
16+
## assert: test.rb
17+
class C
18+
def bar: -> Integer
19+
def baz: -> String
20+
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## update: test.rbs
2+
class Foo
3+
def self.f: (key: Array[Integer]) -> String | (key: Array[String]) -> Symbol
4+
end
5+
6+
## update: test.rb
7+
# Keyword arguments with generic types could cause oscillation
8+
# if the keyword arg has empty type parameter vertices.
9+
def check
10+
@x = Foo.f(key: [@x])
11+
end
12+
13+
## assert
14+
class Object
15+
def check: -> untyped
16+
end
17+
18+
## diagnostics

0 commit comments

Comments
 (0)