diff --git a/lib/typeprof/core/ast/call.rb b/lib/typeprof/core/ast/call.rb index 3fbaba7d..2d794240 100644 --- a/lib/typeprof/core/ast/call.rb +++ b/lib/typeprof/core/ast/call.rb @@ -131,13 +131,8 @@ def install0(genv) include_leading_positionals: @forwarding_arguments != :rest, activation_required: @forwarding_arguments == :rest, ) - leading_args = @positional_args.map do |arg| - if arg.is_a?(DummyNilNode) - @lenv.get_var(:"*anonymous_rest") - else - arg.install(genv) - end - end + # An anonymous rest cannot appear here: `bar(*, ...)` is a syntax error + leading_args = @positional_args.map {|arg| arg.install(genv) } a_args = forward_a_args.prepend_positionals(leading_args, @splat_flags) a_args = a_args.with_keywords(@keyword_args.install(genv)) if @keyword_args else diff --git a/lib/typeprof/core/env/method.rb b/lib/typeprof/core/env/method.rb index e1b31d9d..97d701cf 100644 --- a/lib/typeprof/core/env/method.rb +++ b/lib/typeprof/core/env/method.rb @@ -387,7 +387,7 @@ def accept_actual_arguments(genv, changes, a_args) private def build_keyword_args(genv, changes, node) - opt_keyword_pairs = @opt_keyword_pairs.reject {|_name, vtx| vtx.types.empty? } + opt_keyword_pairs = @opt_keyword_pairs if @req_keyword_pairs.empty? && opt_keyword_pairs.empty? return @rest_keywords, !!@rest_keywords @@ -414,6 +414,9 @@ def build_keyword_args(genv, changes, node) if literal_pairs.empty? [Source.new(base_hash_type), false] + elsif @rest_keywords + fallback = Source.new(Type::Record.new(genv, literal_pairs, base_hash_type)) + [changes.add_keyword_merge_box(genv, @rest_keywords, literal_pairs, fallback).ret, false] else [Source.new(Type::Record.new(genv, literal_pairs, base_hash_type)), false] end diff --git a/lib/typeprof/core/graph/box.rb b/lib/typeprof/core/graph/box.rb index 70c508c2..57f7e26e 100644 --- a/lib/typeprof/core/graph/box.rb +++ b/lib/typeprof/core/graph/box.rb @@ -683,6 +683,33 @@ def run0(genv, changes) end end + # Merges the keywords being forwarded into the `**rest` hash while keeping the + # rest's own fields, so that the callee can still tell the two apart. + class KeywordMergeBox < Box + def initialize(node, genv, rest, literal_pairs, fallback) + super(node) + @rest = rest + @literal_pairs = literal_pairs + @fallback = fallback + @rest.add_edge(genv, self) + @ret = Vertex.new(node) + end + + attr_reader :ret + + def run0(genv, changes) + merged = false + @rest.each_type do |ty| + if ty.is_a?(Type::Record) + fields = ty.fields.merge(@literal_pairs) + changes.add_edge(genv, Source.new(Type::Record.new(genv, fields, ty.base_type(genv))), @ret) + merged = true + end + end + changes.add_edge(genv, @fallback, @ret) unless merged + end + end + class HashSplatBox < Box def initialize(node, genv, hsh, unified_key, unified_val) super(node) @@ -1077,7 +1104,7 @@ def run0(genv, changes) error_count = 0 resolve(genv, changes) do |me, ty, mid, orig_ty| if @node.is_a?(AST::YieldNode) && mid == :call && orig_ty.is_a?(Type::Symbol) - box = add_symbol_proc_call_box(changes, genv, orig_ty.sym, @a_args.positionals, @a_args.keywords) + box = add_symbol_proc_call_box(changes, genv, orig_ty.sym, a_args.positionals, a_args.keywords) changes.add_edge(genv, box.ret, @ret) if box elsif !me if @unresolved_recv diff --git a/lib/typeprof/core/graph/change_set.rb b/lib/typeprof/core/graph/change_set.rb index 64d22c1c..d6816355 100644 --- a/lib/typeprof/core/graph/change_set.rb +++ b/lib/typeprof/core/graph/change_set.rb @@ -86,6 +86,11 @@ def add_splat_box(genv, arg, idx = nil, unresolved_recv = nil) new_boxes[key] ||= SplatBox.new(@node, genv, arg, idx, unresolved_recv) end + def add_keyword_merge_box(genv, rest, literal_pairs, fallback) + key = [:kw_merge, rest, literal_pairs, fallback] + new_boxes[key] ||= KeywordMergeBox.new(@node, genv, rest, literal_pairs, fallback) + end + def add_hash_splat_box(genv, arg, unified_key, unified_val) key = [:hash_splat, arg, unified_key, unified_val] new_boxes[key] ||= HashSplatBox.new(@node, genv, arg, unified_key, unified_val) diff --git a/scenario/misc/super_keywords.rb b/scenario/misc/super_keywords.rb new file mode 100644 index 00000000..b2416b9d --- /dev/null +++ b/scenario/misc/super_keywords.rb @@ -0,0 +1,33 @@ +## update +class B + def foo(a, b: 1) = [a, b] +end +class C < B + def foo(a, b: 2) = super +end +C.new.foo(1, b: "s") + +## assert +class B + def foo: (Integer, ?b: Integer | String) -> [Integer, Integer | String] +end +class C < B + def foo: (Integer, ?b: Integer | String) -> [Integer, Integer | String] +end + +## update +class B + def foo(b: 1, **r) = [b, r] +end +class C < B + def foo(b: 1, **r) = super +end +C.new.foo(b: "s", z: 3) + +## assert +class B + def foo: (?b: Integer | String, **Integer) -> [Integer | String, { z: Integer }] +end +class C < B + def foo: (?b: Integer | String, **Integer) -> [Integer | String, { z: Integer }] +end