summaryrefslogtreecommitdiff
path: root/test/ruby
diff options
context:
space:
mode:
authorKevin Menard <kevin@nirvdrum.com>2026-01-14 19:10:06 -0500
committerGitHub <noreply@github.com>2026-01-14 19:10:06 -0500
commit4a21b83693fdc0e976da209047ba286b2f4084e5 (patch)
treed7976abd267ba43eafee2aa24285a67cdd29f6d9 /test/ruby
parentcdb2b0eed50e1c837adeb85ef8978e533f056327 (diff)
ZJIT: Optimize common `invokesuper` cases (#15816)
* ZJIT: Profile `invokesuper` instructions * ZJIT: Introduce the `InvokeSuperDirect` HIR instruction The new instruction is an optimized version of `InvokeSuper` when we know the `super` target is an ISEQ. * ZJIT: Expand definition of unspecializable to more complex cases * ZJIT: Ensure `invokesuper` optimization works when the inheritance hierarchy is modified * ZJIT: Simplify `invokesuper` specialization to most common case Looking at ruby-bench, most `super` calls don't pass a block, which means we can use the already optimized `SendWithoutBlockDirect`. * ZJIT: Track `super` method entries directly to avoid GC issues Because the method entry isn't typed as a `VALUE`, we set up barriers on its `VALUE` fields. But, that was insufficient as the method entry itself could be collected in certain cases, resulting in dangling objects. Now we track the method entry as a `VALUE` and can more naturally mark it and its children. * ZJIT: Optimize `super` calls with simple argument forms * ZJIT: Report the reason why we can't optimize an `invokesuper` instance * ZJIT: Revise send fallback reasons for `super` calls * ZJIT: Assert `super` calls are `FCALL` and don't need visibily checks
Diffstat (limited to 'test/ruby')
-rw-r--r--test/ruby/test_zjit.rb477
1 files changed, 477 insertions, 0 deletions
diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb
index cf3c46b3ed..bc4f5f2ae8 100644
--- a/test/ruby/test_zjit.rb
+++ b/test/ruby/test_zjit.rb
@@ -843,6 +843,483 @@ class TestZJIT < Test::Unit::TestCase
}
end
+ def test_invokesuper_to_iseq
+ assert_compiles '["B", "A"]', %q{
+ class A
+ def foo
+ "A"
+ end
+ end
+
+ class B < A
+ def foo
+ ["B", super]
+ end
+ end
+
+ def test
+ B.new.foo
+ end
+
+ test # profile invokesuper
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ def test_invokesuper_with_args
+ assert_compiles '["B", 11]', %q{
+ class A
+ def foo(x)
+ x * 2
+ end
+ end
+
+ class B < A
+ def foo(x)
+ ["B", super(x) + 1]
+ end
+ end
+
+ def test
+ B.new.foo(5)
+ end
+
+ test # profile invokesuper
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ # Test super with explicit args when callee has rest parameter.
+ # This should fall back to dynamic dispatch since we can't handle rest params yet.
+ def test_invokesuper_with_args_to_rest_param
+ assert_compiles '["B", "a", ["b", "c"]]', %q{
+ class A
+ def foo(x, *rest)
+ [x, rest]
+ end
+ end
+
+ class B < A
+ def foo(x, y, z)
+ ["B", *super(x, y, z)]
+ end
+ end
+
+ def test
+ B.new.foo("a", "b", "c")
+ end
+
+ test # profile invokesuper
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ def test_invokesuper_with_block
+ assert_compiles '["B", "from_block"]', %q{
+ class A
+ def foo
+ block_given? ? yield : "no_block"
+ end
+ end
+
+ class B < A
+ def foo
+ ["B", super { "from_block" }]
+ end
+ end
+
+ def test
+ B.new.foo
+ end
+
+ test # profile invokesuper
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ def test_invokesuper_to_cfunc
+ assert_compiles '["MyArray", 3]', %q{
+ class MyArray < Array
+ def length
+ ["MyArray", super]
+ end
+ end
+
+ def test
+ MyArray.new([1, 2, 3]).length
+ end
+
+ test # profile invokesuper
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ def test_invokesuper_multilevel
+ assert_compiles '["C", ["B", "A"]]', %q{
+ class A
+ def foo
+ "A"
+ end
+ end
+
+ class B < A
+ def foo
+ ["B", super]
+ end
+ end
+
+ class C < B
+ def foo
+ ["C", super]
+ end
+ end
+
+ def test
+ C.new.foo
+ end
+
+ test # profile invokesuper
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ # Test implicit block forwarding - super without explicit block should forward caller's block
+ # Note: We call test twice to ensure ZJIT compiles it before the final call that we check
+ def test_invokesuper_forwards_block_implicitly
+ assert_compiles '["B", "forwarded_block"]', %q{
+ class A
+ def foo
+ block_given? ? yield : "no_block"
+ end
+ end
+
+ class B < A
+ def foo
+ ["B", super] # should forward the block from caller
+ end
+ end
+
+ def test
+ B.new.foo { "forwarded_block" }
+ end
+
+ test # profile invokesuper
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ # Test implicit block forwarding with explicit arguments
+ def test_invokesuper_forwards_block_implicitly_with_args
+ assert_compiles '["B", ["arg_value", "forwarded"]]', %q{
+ class A
+ def foo(x)
+ [x, (block_given? ? yield : "no_block")]
+ end
+ end
+
+ class B < A
+ def foo(x)
+ ["B", super(x)] # explicit args, but block should still be forwarded
+ end
+ end
+
+ def test
+ B.new.foo("arg_value") { "forwarded" }
+ end
+
+ test # profile
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ # Test implicit block forwarding when no block is given (should not fail)
+ def test_invokesuper_forwards_block_implicitly_no_block_given
+ assert_compiles '["B", "no_block"]', %q{
+ class A
+ def foo
+ block_given? ? yield : "no_block"
+ end
+ end
+
+ class B < A
+ def foo
+ ["B", super] # no block given by caller
+ end
+ end
+
+ def test
+ B.new.foo # called without a block
+ end
+
+ test # profile
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ # Test implicit block forwarding through multiple inheritance levels
+ def test_invokesuper_forwards_block_implicitly_multilevel
+ assert_compiles '["C", ["B", "deep_block"]]', %q{
+ class A
+ def foo
+ block_given? ? yield : "no_block"
+ end
+ end
+
+ class B < A
+ def foo
+ ["B", super] # forwards block to A
+ end
+ end
+
+ class C < B
+ def foo
+ ["C", super] # forwards block to B, which forwards to A
+ end
+ end
+
+ def test
+ C.new.foo { "deep_block" }
+ end
+
+ test # profile
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ # Test implicit block forwarding with block parameter syntax
+ def test_invokesuper_forwards_block_param
+ assert_compiles '["B", "block_param_forwarded"]', %q{
+ class A
+ def foo
+ block_given? ? yield : "no_block"
+ end
+ end
+
+ class B < A
+ def foo(&block)
+ ["B", super] # should forward &block implicitly
+ end
+ end
+
+ def test
+ B.new.foo { "block_param_forwarded" }
+ end
+
+ test # profile
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ def test_invokesuper_with_blockarg
+ assert_compiles '["B", "different block"]', %q{
+ class A
+ def foo
+ block_given? ? yield : "no block"
+ end
+ end
+
+ class B < A
+ def foo(&blk)
+ other_block = proc { "different block" }
+ ["B", super(&other_block)]
+ end
+ end
+
+ def test
+ B.new.foo { "passed block" }
+ end
+
+ test # profile
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ def test_invokesuper_with_symbol_to_proc
+ assert_compiles '["B", [3, 5, 7]]', %q{
+ class A
+ def foo(items, &blk)
+ items.map(&blk)
+ end
+ end
+
+ class B < A
+ def foo(items)
+ ["B", super(items, &:succ)]
+ end
+ end
+
+ def test
+ B.new.foo([2, 4, 6])
+ end
+
+ test # profile
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ def test_invokesuper_with_splat
+ assert_compiles '["B", 6]', %q{
+ class A
+ def foo(a, b, c)
+ a + b + c
+ end
+ end
+
+ class B < A
+ def foo(*args)
+ ["B", super(*args)]
+ end
+ end
+
+ def test
+ B.new.foo(1, 2, 3)
+ end
+
+ test # profile
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ def test_invokesuper_with_kwargs
+ assert_compiles '["B", "x=1, y=2"]', %q{
+ class A
+ def foo(x:, y:)
+ "x=#{x}, y=#{y}"
+ end
+ end
+
+ class B < A
+ def foo(x:, y:)
+ ["B", super(x: x, y: y)]
+ end
+ end
+
+ def test
+ B.new.foo(x: 1, y: 2)
+ end
+
+ test # profile
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ def test_invokesuper_with_kw_splat
+ assert_compiles '["B", "x=1, y=2"]', %q{
+ class A
+ def foo(x:, y:)
+ "x=#{x}, y=#{y}"
+ end
+ end
+
+ class B < A
+ def foo(**kwargs)
+ ["B", super(**kwargs)]
+ end
+ end
+
+ def test
+ B.new.foo(x: 1, y: 2)
+ end
+
+ test # profile
+ test # compile + run compiled code
+ }, call_threshold: 2
+ end
+
+ # Test that including a module after compilation correctly changes the super target.
+ # The included module's method should be called, not the original super target.
+ def test_invokesuper_with_include
+ assert_compiles '["B", "M"]', %q{
+ class A
+ def foo
+ "A"
+ end
+ end
+
+ class B < A
+ def foo
+ ["B", super]
+ end
+ end
+
+ def test
+ B.new.foo
+ end
+
+ test # profile invokesuper (super -> A#foo)
+ test # compile with super -> A#foo
+
+ # Now include a module in B that defines foo - super should go to M#foo instead
+ module M
+ def foo
+ "M"
+ end
+ end
+ B.include(M)
+
+ test # should call M#foo, not A#foo
+ }, call_threshold: 2
+ end
+
+ # Test that prepending a module after compilation correctly changes the super target.
+ # The prepended module's method should be called, not the original super target.
+ def test_invokesuper_with_prepend
+ assert_compiles '["B", "M"]', %q{
+ class A
+ def foo
+ "A"
+ end
+ end
+
+ class B < A
+ def foo
+ ["B", super]
+ end
+ end
+
+ def test
+ B.new.foo
+ end
+
+ test # profile invokesuper (super -> A#foo)
+ test # compile with super -> A#foo
+
+ # Now prepend a module that defines foo - super should go to M#foo instead
+ module M
+ def foo
+ "M"
+ end
+ end
+ A.prepend(M)
+
+ test # should call M#foo, not A#foo
+ }, call_threshold: 2
+ end
+
+ # Test super with positional and keyword arguments (pattern from chunky_png)
+ def test_invokesuper_with_keyword_args
+ assert_compiles '{content: "image data"}', %q{
+ class A
+ def foo(attributes = {})
+ @attributes = attributes
+ end
+ end
+
+ class B < A
+ def foo(content = '')
+ super(content: content)
+ end
+ end
+
+ def test
+ B.new.foo("image data")
+ end
+
+ test
+ test
+ }, call_threshold: 2
+ end
+
def test_invokebuiltin
# Not using assert_compiles due to register spill
assert_runs '["."]', %q{