From 004244798f399eaa2db2588b9f3588e9d80986bd Mon Sep 17 00:00:00 2001 From: Takumi Shotoku Date: Sun, 27 Sep 2026 18:16:19 +0900 Subject: [PATCH 1/2] Pass the current values of the parameters in a bare super A bare `super` passes the current values of the parameters, including defaults and reassignments, but TypeProf forwarded only what the caller gave, as `...` does. So `def foo(x = 0) = super` called without arguments was reported as a wrong number of arguments for the parent. `...` and anonymous or destructured parameters have no variable the method can reassign, so they are still forwarded as given. A block or lambda parameter can shadow a parameter of the method, so a `super` in it still passes the variable of the method. A named `**rest` that stays empty never ran the box merging the keywords into it, so a keyword default did not reach the parent either. The box now runs once when it is created. --- lib/typeprof/core/ast/call.rb | 16 +-- lib/typeprof/core/ast/method.rb | 11 ++ lib/typeprof/core/env.rb | 9 +- lib/typeprof/core/env/method.rb | 75 ++++++++++--- lib/typeprof/core/graph/box.rb | 2 + scenario/misc/super.rb | 192 ++++++++++++++++++++++++++++++++ scenario/misc/super_keywords.rb | 34 ++++++ 7 files changed, 315 insertions(+), 24 deletions(-) diff --git a/lib/typeprof/core/ast/call.rb b/lib/typeprof/core/ast/call.rb index 73ae903fe..5282574e5 100644 --- a/lib/typeprof/core/ast/call.rb +++ b/lib/typeprof/core/ast/call.rb @@ -61,6 +61,9 @@ def formal_names def install0(genv) blenv = @body.lenv blenv.forward_args = @lenv.forward_args + # An outer block may have shadowed the same name already; its entry is + # the method's variable, so it must win over the outer block's own one + blenv.shadowed_vars = @lenv.locals.slice(*@tbl).merge(@lenv.shadowed_vars) @lenv.locals.each {|var, vtx| blenv.locals[var] = vtx } @tbl.each {|var| blenv.locals[var] = Source.new(genv.nil_type) } blenv.locals[:"*self"] = blenv.cref.get_self(genv) @@ -225,13 +228,12 @@ def install0(genv) end if forward_args - forward_a_args = forward_args.to_actual_arguments( - genv, - @changes, - self, - include_leading_positionals: @forwarding_arguments != :rest, - activation_required: @forwarding_arguments == :rest, - ) + forward_a_args = + if @forwarding_arguments == :all + forward_args.to_implicit_super_arguments(genv, @changes, self, @lenv) + else + forward_args.to_actual_arguments(genv, @changes, self) + 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) diff --git a/lib/typeprof/core/ast/method.rb b/lib/typeprof/core/ast/method.rb index d6f33d704..0ece6aab0 100644 --- a/lib/typeprof/core/ast/method.rb +++ b/lib/typeprof/core/ast/method.rb @@ -300,6 +300,17 @@ def install0(genv) forward_rest_keywords, forward_block, forward_activation, + { + req_positionals: @req_positionals, + opt_positionals: @opt_positionals, + # The variables of `...` and anonymous parameters hold only a + # placeholder, so what the caller passed is forwarded instead + rest_positionals: [:"...", :"*anonymous_rest"].include?(@rest_positionals) ? nil : @rest_positionals, + post_positionals: @post_positionals, + req_keywords: @req_keywords, + opt_keywords: @opt_keywords, + rest_keywords: [:"...", :"**anonymous_keyword"].include?(@rest_keywords) ? nil : @rest_keywords, + }, ) if @body diff --git a/lib/typeprof/core/env.rb b/lib/typeprof/core/env.rb index 82f5adac9..d1b8ce836 100644 --- a/lib/typeprof/core/env.rb +++ b/lib/typeprof/core/env.rb @@ -374,12 +374,13 @@ def initialize(file_context, cref, locals, return_boxes, forward_args = nil, sig @ivar_narrowings = {} @strict_const_scope = false @forward_args = forward_args + @shadowed_vars = {} # [cpath, names] of the type parameters of the enclosing RBS declaration @sig_type_params = sig_type_params end attr_reader :file_context, :cref, :locals, :return_boxes, :break_vtx, :next_boxes, :strict_const_scope, :sig_type_params - attr_accessor :module_function, :forward_args + attr_accessor :module_function, :forward_args, :shadowed_vars def path = @file_context&.path def code_range_from_node(node) @@ -398,6 +399,12 @@ def get_var(name) @locals[name] || raise("#{ name }") end + # A bare `super` passes the variables of the method, not the block + # parameters that shadow them + def get_method_var(name) + @shadowed_vars[name] || get_var(name) + end + def exist_var?(name) !!@locals[name] end diff --git a/lib/typeprof/core/env/method.rb b/lib/typeprof/core/env/method.rb index 654d6e316..6fdef35dc 100644 --- a/lib/typeprof/core/env/method.rb +++ b/lib/typeprof/core/env/method.rb @@ -384,7 +384,7 @@ def empty_omittable_splat_argument?(genv, arg) end class ForwardingArguments - def initialize(req_positionals, opt_positionals, rest_positionals, post_positionals, req_keyword_pairs, opt_keyword_pairs, rest_keywords, block, activation) + def initialize(req_positionals, opt_positionals, rest_positionals, post_positionals, req_keyword_pairs, opt_keyword_pairs, rest_keywords, block, activation, param_names) @req_positionals = req_positionals @opt_positionals = opt_positionals @rest_positionals = rest_positionals @@ -394,12 +394,14 @@ def initialize(req_positionals, opt_positionals, rest_positionals, post_position @rest_keywords = rest_keywords @block = block @activation = activation + @param_names = param_names end - def to_actual_arguments(genv, changes, node, include_leading_positionals: true, activation_required: false) - positionals = include_leading_positionals ? @req_positionals.dup : [] - splat_flags = ::Array.new(positionals.size, false) - positionals_omittable = ::Array.new(positionals.size, false) + # The arguments that `...` forwards: only the ones the caller passed. + def to_actual_arguments(genv, changes, node) + positionals = [] + splat_flags = [] + positionals_omittable = [] @opt_positionals.each do |elem_vtx| positionals << Source.new(genv.gen_ary_type(elem_vtx)) @@ -419,8 +421,51 @@ def to_actual_arguments(genv, changes, node, include_leading_positionals: true, positionals_omittable << false end - keywords, keywords_omittable = build_keyword_args(genv, changes, node) - ForwardingActualArguments.new(positionals, splat_flags, keywords, @block, positionals_omittable, keywords_omittable, true, @activation, activation_required) + # `...` cannot follow keyword parameters, so only the rest keywords remain + ForwardingActualArguments.new(positionals, splat_flags, @rest_keywords, @block, positionals_omittable, !!@rest_keywords, true, @activation, true) + end + + # A bare `super` passes the current values of the parameters, which + # include the defaults of omitted ones and any reassignment. + def to_implicit_super_arguments(genv, changes, node, lenv) + positionals = [] + splat_flags = [] + positionals_omittable = [] + + # A destructured parameter has no name, so it is passed as it was given + @param_names[:req_positionals].zip(@req_positionals) do |name, vtx| + positionals << (name ? lenv.get_method_var(name) : vtx) + splat_flags << false + positionals_omittable << false + end + + @param_names[:opt_positionals].each do |name| + positionals << lenv.get_method_var(name) + splat_flags << false + positionals_omittable << false + end + + if @param_names[:rest_positionals] + positionals << lenv.get_method_var(@param_names[:rest_positionals]) + splat_flags << true + positionals_omittable << true + elsif @rest_positionals + positionals << Source.new(genv.gen_ary_type(@rest_positionals)) + splat_flags << true + positionals_omittable << true + end + + @param_names[:post_positionals].zip(@post_positionals) do |name, vtx| + positionals << (name ? lenv.get_method_var(name) : vtx) + splat_flags << false + positionals_omittable << false + end + + req_keyword_pairs = @param_names[:req_keywords].map {|name| [name, lenv.get_method_var(name)] } + opt_keyword_pairs = @param_names[:opt_keywords].map {|name| [name, lenv.get_method_var(name)] } + rest_keywords = @param_names[:rest_keywords] ? lenv.get_method_var(@param_names[:rest_keywords]) : @rest_keywords + keywords, keywords_omittable = build_keyword_args(genv, changes, node, req_keyword_pairs, opt_keyword_pairs, rest_keywords) + ForwardingActualArguments.new(positionals, splat_flags, keywords, @block, positionals_omittable, keywords_omittable, true, @activation, false) end def accept_actual_arguments(genv, changes, a_args) @@ -525,18 +570,16 @@ def accept_actual_arguments(genv, changes, a_args) private - def build_keyword_args(genv, changes, node) - opt_keyword_pairs = @opt_keyword_pairs - - if @req_keyword_pairs.empty? && opt_keyword_pairs.empty? - return @rest_keywords, !!@rest_keywords + def build_keyword_args(genv, changes, node, req_keyword_pairs, opt_keyword_pairs, rest_keywords) + if req_keyword_pairs.empty? && opt_keyword_pairs.empty? + return rest_keywords, !!rest_keywords end unified_key = Vertex.new(node) unified_val = Vertex.new(node) literal_pairs = {} - @req_keyword_pairs.each do |name, vtx| + req_keyword_pairs.each do |name, vtx| changes.add_edge(genv, Source.new(Type::Symbol.new(genv, name)), unified_key) changes.add_edge(genv, vtx, unified_val) literal_pairs[name] = vtx @@ -549,13 +592,13 @@ def build_keyword_args(genv, changes, node) end base_hash_type = genv.gen_hash_type(unified_key, unified_val) - changes.add_hash_splat_box(genv, @rest_keywords, unified_key, unified_val) if @rest_keywords + changes.add_hash_splat_box(genv, rest_keywords, unified_key, unified_val) if rest_keywords if literal_pairs.empty? [Source.new(base_hash_type), false] - elsif @rest_keywords + 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] + [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 25cce3d97..938e7ee15 100644 --- a/lib/typeprof/core/graph/box.rb +++ b/lib/typeprof/core/graph/box.rb @@ -680,6 +680,8 @@ def initialize(node, genv, rest, literal_pairs, fallback) @fallback = fallback @rest.add_edge(genv, self) @ret = Vertex.new(node) + # An empty rest never triggers a run, but the fallback still has to flow + genv.add_run(self) end attr_reader :ret diff --git a/scenario/misc/super.rb b/scenario/misc/super.rb index f434ba354..6bea9259e 100644 --- a/scenario/misc/super.rb +++ b/scenario/misc/super.rb @@ -151,3 +151,195 @@ def foo: (Integer, Integer) -> [Integer, Integer] class SuperChild < SuperBase def foo: (Integer, *Integer, **untyped) -> [Integer, Integer] end + +## update +class SuperBase + def foo(a, b) + [a, b] + end +end + +class SuperChild < SuperBase + def foo(a = 1, b = "str") + super + end +end + +SuperChild.new.foo + +## assert +class SuperBase + def foo: (Integer, String) -> [Integer, String] +end +class SuperChild < SuperBase + def foo: (?Integer, ?String) -> [Integer, String] +end + +## diagnostics + +## update +class SuperBase + def foo(a, b, k:) + [a, b, k] + end +end + +class SuperChild < SuperBase + def foo(a, b = 1, k:) + a = "str" + [1].each { b = :sym } + k = 1.0 + super + end +end + +SuperChild.new.foo(1, k: 1) + +## assert +class SuperBase + def foo: (String, :sym | Integer, k: Float) -> [String, :sym | Integer, Float] +end +class SuperChild < SuperBase + def foo: (Integer, ?Integer, k: Integer) -> [String, :sym | Integer, Float] +end + +## update +class SuperBase + def foo(a, *b) + [a, b] + end +end + +class SuperChild < SuperBase + def foo(a, *b) + b = [:sym] + super + end +end + +SuperChild.new.foo(1, "str") + +## assert +class SuperBase + def foo: (Integer, *:sym) -> [Integer, Array[:sym]] +end +class SuperChild < SuperBase + def foo: (Integer, *String) -> [Integer, Array[:sym]] +end + +## update +class SuperBase + def foo(a, b) + [a, b] + end +end + +class SuperChild < SuperBase + def foo(a = 1, b) + b = "str" + super + end +end + +SuperChild.new.foo(:sym) + +## assert +class SuperBase + def foo: (Integer, String) -> [Integer, String] +end +class SuperChild < SuperBase + def foo: (?Integer, :sym) -> [Integer, String] +end + +## update +class SuperBase + def foo(a, b) + [a, b] + end +end + +class SuperChild < SuperBase + def foo((a, b), c = "str") + super + end +end + +SuperChild.new.foo([1, 2]) + +## assert +class SuperBase + def foo: ([Integer, Integer], String) -> [[Integer, Integer], String] +end +class SuperChild < SuperBase + def foo: ([Integer, Integer], ?String) -> [[Integer, Integer], String] +end + +## update +class SuperBase + def foo(*a, **k) + [a, k] + end +end + +class SuperChild < SuperBase + def foo(*, **) + super + end +end + +SuperChild.new.foo(1, x: 2) + +## assert +class SuperBase + def foo: (*Integer, **Integer) -> [Array[Integer], { x: Integer }] +end +class SuperChild < SuperBase + def foo: (*Integer, **untyped | Integer) -> [Array[Integer], { x: Integer }] +end + +## update +class SuperBase + def foo(a, b) + [a, b] + end +end + +class SuperChild < SuperBase + def foo(...) + super + end +end + +SuperChild.new.foo(1, 2) + +## assert +class SuperBase + def foo: (Integer, Integer) -> [Integer, Integer] +end +class SuperChild < SuperBase + def foo: (*Integer, **untyped) -> [Integer, Integer] +end + +## update +class SuperBase + def foo(a) + a + end +end + +class SuperChild < SuperBase + def foo(a) + f = ->(a) { a = "str"; super } + f.call(1) + end +end + +SuperChild.new.foo(1) + +## assert +class SuperBase + def foo: (Integer) -> Integer +end +class SuperChild < SuperBase + def foo: (Integer) -> Integer +end diff --git a/scenario/misc/super_keywords.rb b/scenario/misc/super_keywords.rb index b2416b9d5..9d51dbbef 100644 --- a/scenario/misc/super_keywords.rb +++ b/scenario/misc/super_keywords.rb @@ -31,3 +31,37 @@ def foo: (?b: Integer | String, **Integer) -> [Integer | String, { z: Integer }] class C < B def foo: (?b: Integer | String, **Integer) -> [Integer | String, { z: Integer }] end + +## update +class B + def foo(b:) = b +end +class C < B + def foo(b: 1) = super +end +C.new.foo + +## assert +class B + def foo: (b: Integer) -> Integer +end +class C < B + def foo: (?b: Integer) -> Integer +end + +## update +class B + def foo(b: 1) = b +end +class C < B + def foo(b: "str", **r) = super +end +C.new.foo + +## assert +class B + def foo: (?b: Integer | String) -> (Integer | String) +end +class C < B + def foo: (?b: String, **untyped) -> (Integer | String) +end From 12858f35183dbd80840929997ee4ba1534f4b524 Mon Sep 17 00:00:00 2001 From: Takumi Shotoku Date: Mon, 28 Sep 2026 01:40:21 +0900 Subject: [PATCH 2/2] Resolve super in a block to the enclosing method A block took the name of the method it was passed to, so `[1].map { super }` in `def foo` looked up `map` instead of `foo`. It now takes the name of the enclosing method, as a lambda literal already did. A block outside any method then has no method name, so its `super` is skipped instead of reported as an undefined method with an empty name. A block given to define_method or define_singleton_method is the body of another method, so it has no method name either, and a bare `super` in it is reported as not supported, as Ruby raises for it at runtime. --- lib/typeprof/core/ast/call.rb | 15 +++-- lib/typeprof/core/ast/value.rb | 4 -- lib/typeprof/core/graph/box.rb | 2 + scenario/diagnostics/super-outside-method.rb | 22 +++++- scenario/misc/super.rb | 71 ++++++++++++++++++++ 5 files changed, 105 insertions(+), 9 deletions(-) diff --git a/lib/typeprof/core/ast/call.rb b/lib/typeprof/core/ast/call.rb index 5282574e5..4c1f0bd06 100644 --- a/lib/typeprof/core/ast/call.rb +++ b/lib/typeprof/core/ast/call.rb @@ -2,11 +2,15 @@ module TypeProf::Core class AST # @lenv is the scope the block closes over; the body has its own LocalEnv. class BlockNode < Node - def initialize(raw_node, lenv, mid) + def initialize(raw_node, lenv, method_body: false) super(raw_node, lenv) @tbl = raw_node.locals - ncref = CRef.new(lenv.cref.cpath, :instance, mid, lenv.cref) + @method_body = method_body + # A `super` in a block calls the super of the enclosing method, but a + # block given to define_method is the body of another method, whose name + # is not known here + ncref = CRef.new(lenv.cref.cpath, :instance, method_body ? nil : lenv.cref.mid, lenv.cref) # A `return` in a block exits the enclosing method, so the body writes into # its return boxes. A lambda's `return` exits the lambda, so it gets its own. nlenv = LocalEnv.new(lenv.file_context, ncref, {}, lambda? ? [] : lenv.return_boxes) @@ -60,7 +64,8 @@ def formal_names def install0(genv) blenv = @body.lenv - blenv.forward_args = @lenv.forward_args + # Ruby rejects a bare `super` in a define_method block at runtime + blenv.forward_args = @lenv.forward_args unless @method_body # An outer block may have shadowed the same name already; its entry is # the method's variable, so it must win over the outer block's own one blenv.shadowed_vars = @lenv.locals.slice(*@tbl).merge(@lenv.shadowed_vars) @@ -143,6 +148,8 @@ def ret_code_range = @body.ret_code_range end class CallBaseNode < Node + DEFINE_METHOD_MIDS = [:define_method, :define_singleton_method].freeze + def initialize(raw_node, recv, mid, mid_code_range_loc, raw_args, last_arg, raw_block, lenv, forwarding_arguments: false) super(raw_node, lenv) @@ -194,7 +201,7 @@ def initialize(raw_node, recv, mid, mid_code_range_loc, raw_args, last_arg, raw_ @anonymous_block_forwarding = true end else - @block = BlockNode.new(raw_block, lenv, @mid) + @block = BlockNode.new(raw_block, lenv, method_body: DEFINE_METHOD_MIDS.include?(@mid)) end end diff --git a/lib/typeprof/core/ast/value.rb b/lib/typeprof/core/ast/value.rb index dce4344a1..d5547ee2e 100644 --- a/lib/typeprof/core/ast/value.rb +++ b/lib/typeprof/core/ast/value.rb @@ -330,10 +330,6 @@ def install0(genv) # A lambda literal is a block without a call: `->` never dispatches to a # user-defined `lambda` method. class LambdaNode < BlockNode - def initialize(raw_node, lenv) - super(raw_node, lenv, lenv.cref.mid) - end - def lambda? = true # A lambda is entered like a method, so every parameter kind binds, not just diff --git a/lib/typeprof/core/graph/box.rb b/lib/typeprof/core/graph/box.rb index 938e7ee15..9883abc7d 100644 --- a/lib/typeprof/core/graph/box.rb +++ b/lib/typeprof/core/graph/box.rb @@ -1027,6 +1027,8 @@ def resolve(genv, changes, &blk) next if orig_ty == genv.bot_type if @mid == :"*super" mid = @node.lenv.cref.mid + # Outside a method or in a define_method block, the method is unknown + next unless mid skip = true else mid = @mid diff --git a/scenario/diagnostics/super-outside-method.rb b/scenario/diagnostics/super-outside-method.rb index a7d7e2b17..142f4d32a 100644 --- a/scenario/diagnostics/super-outside-method.rb +++ b/scenario/diagnostics/super-outside-method.rb @@ -6,4 +6,24 @@ ## diagnostics (1,0)-(1,13): undefined method: Object#define_method (2,2)-(2,7): implicit argument passing of super is not supported here -(2,2)-(2,7): undefined method: Object#define_method + +## update +class Foo + define_method(:foo) { super() } +end + +## diagnostics + +## update +class Base + def bar = 1 +end +class Foo < Base + def self.make + define_method(:bar) { super() } + define_singleton_method(:baz) { super } + end +end + +## diagnostics +(7,36)-(7,41): implicit argument passing of super is not supported here diff --git a/scenario/misc/super.rb b/scenario/misc/super.rb index 6bea9259e..f6aa1001f 100644 --- a/scenario/misc/super.rb +++ b/scenario/misc/super.rb @@ -327,6 +327,77 @@ def foo(a) end end +class SuperChild < SuperBase + def foo(a) + [1].map { super } + end +end + +SuperChild.new.foo("str") + +## assert +class SuperBase + def foo: (String) -> String +end +class SuperChild < SuperBase + def foo: (String) -> Array[String] +end + +## diagnostics + +## update +class SuperBase + def foo(a) + a + end +end + +class SuperChild < SuperBase + def foo(a) + [1].each {|a| a = "str"; [2].each { super } } + end +end + +SuperChild.new.foo(1) + +## assert +class SuperBase + def foo: (Integer) -> Integer +end +class SuperChild < SuperBase + def foo: (Integer) -> Array[Integer] +end + +## update +class SuperBase + def foo(a) + a + end +end + +class SuperChild < SuperBase + def foo(a) + [1].each {|a| [2].each {|a| a = "str"; super } } + end +end + +SuperChild.new.foo(1) + +## assert +class SuperBase + def foo: (Integer) -> Integer +end +class SuperChild < SuperBase + def foo: (Integer) -> Array[Integer] +end + +## update +class SuperBase + def foo(a) + a + end +end + class SuperChild < SuperBase def foo(a) f = ->(a) { a = "str"; super }