Skip to content

Commit 0d25672

Browse files
authored
[fix] define_method captured block propagation in JIT mode (jruby#9310)
1 parent c7470c5 commit 0d25672

4 files changed

Lines changed: 60 additions & 4 deletions

File tree

core/src/main/java/org/jruby/internal/runtime/methods/DefineMethodMethod.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ public DefineMethodMethod(IRScope method, Visibility visibility, RubyModule impl
2222
this.capturedBlock = capturedBlock;
2323
}
2424

25+
public Block getCapturedBlock() {
26+
return capturedBlock;
27+
}
28+
2529
@Override
2630
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args, Block block) {
2731
return super.call(context, self, clazz, name, args, capturedBlock);

core/src/main/java/org/jruby/ir/IRClosure.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,6 @@ public boolean isNestedClosuresSafeForMethodConversion() {
196196
public IRMethod convertToMethod(ByteList name) {
197197
// We want variable scoping to be the same as a method and not see outside itself.
198198
if (source == null ||
199-
isWithinMethod() || // def foo; define_method {}; end has potential to implicitly access parents var yield
200199
accessesParentsLocalVariables() || // Built methods cannot search down past method scope
201200
receivesClosureArg() || // we pass in captured block at define_method as block so explicits ones not supported
202201
usesZSuper() || // methods defined from closures cannot use zsuper

core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.jruby.internal.runtime.methods.AttrReaderMethod;
2222
import org.jruby.internal.runtime.methods.AttrWriterMethod;
2323
import org.jruby.internal.runtime.methods.CompiledIRMethod;
24+
import org.jruby.internal.runtime.methods.DefineMethodMethod;
2425
import org.jruby.internal.runtime.methods.DynamicMethod;
2526
import org.jruby.internal.runtime.methods.HandleMethod;
2627
import org.jruby.internal.runtime.methods.MixedModeIRMethod;
@@ -361,8 +362,8 @@ private MethodHandle createAttrWriterHandle(IRubyObject self, RubyClass cls, Var
361362
return setValue;
362363
}
363364

364-
MethodHandle buildJittedHandle(CacheEntry entry, boolean blockGiven) {
365-
MethodHandle mh = null;
365+
MethodHandle buildJittedHandle(final CacheEntry entry, final boolean blockGiven) {
366+
MethodHandle mh;
366367
SmartBinder binder;
367368
CompiledIRMethod compiledIRMethod = null;
368369
DynamicMethod method = entry.method;
@@ -410,7 +411,12 @@ MethodHandle buildJittedHandle(CacheEntry entry, boolean blockGiven) {
410411
}
411412
}
412413

413-
if (!blockGiven) {
414+
if (method instanceof DefineMethodMethod defineMethod) {
415+
// DefineMethodMethod captures the block from define_method's enclosing scope;
416+
// always use that captured block for yield, regardless of caller's block
417+
if (blockGiven) binder = binder.drop("block");
418+
binder = binder.append("block", Block.class, defineMethod.getCapturedBlock());
419+
} else if (!blockGiven) {
414420
binder = binder.append("block", Block.class, Block.NULL_BLOCK);
415421
}
416422

@@ -424,6 +430,8 @@ MethodHandle buildJittedHandle(CacheEntry entry, boolean blockGiven) {
424430
if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) {
425431
LOG.info(name() + "\tbound directly to jitted method " + Bootstrap.logMethod(method));
426432
}
433+
} else {
434+
mh = null;
427435
}
428436

429437
return mh;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# https://github.com/jruby/jruby/issues/8946
2+
#
3+
# When define_method is called within another method and the block uses yield,
4+
# the captured block must be used for yield even after JIT + invokedynamic
5+
# optimization (which previously bypassed DefineMethodMethod.call and lost
6+
# the captured block).
7+
8+
describe 'define_method with yield inside another method' do
9+
it 'yields to the captured block after JIT optimization' do
10+
cls = Class.new do
11+
class << self
12+
def go(&blk)
13+
define_method(:foo) { yield }
14+
end
15+
end
16+
end
17+
18+
results = []
19+
cls.go { results << :called }
20+
21+
obj = cls.new
22+
# call enough times to trigger JIT and indy binding
23+
10.times { obj.foo }
24+
25+
expect(results).to eq(Array.new(10, :called))
26+
end
27+
28+
it 'yields to the captured block with arguments' do
29+
cls = Class.new do
30+
class << self
31+
def go(&blk)
32+
define_method(:bar) { |x| yield(x) }
33+
end
34+
end
35+
end
36+
37+
results = []
38+
cls.go { |v| results << v }
39+
40+
obj = cls.new
41+
10.times { |i| obj.bar(i) }
42+
43+
expect(results).to eq((0..9).to_a)
44+
end
45+
end

0 commit comments

Comments
 (0)