proc.c: mark Proc#dup / Proc#clone copies as orphan blocks

A copied Proc now always carries `MRB_PROC_ORPHAN`, so calling a
`dup`'d block that contains `break` or `return` raises
`LocalJumpError` even while the original yielding method is still on
the stack.

This is stricter than CRuby — which only marks the copy orphan once
the original yielding method returns — but matches mruby's
memory-first design: tracking the original via a back pointer in
RProc would also enlarge the GC mark set. dearblue's option (1) in
the linked issue, accepted for the simpler RProc layout.

Document the divergence in `doc/limitations.md` and add a regression
test in `test/t/proc.rb`.

close #6345

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-05-11 15:06:24 +09:00
parent 322642364a
commit 16151a0daa
3 changed files with 54 additions and 0 deletions
+34
View File
@@ -370,3 +370,37 @@ SomeClass.new.nested # NoMethodError
Writing nested `def` like this is unusual; this difference rarely
surfaces in practical code.
## `Proc#dup` / `Proc#clone` is Always Orphan
A `dup` or `clone` of a block given to a method is always treated as
an orphan block in mruby — calling it raises `LocalJumpError` if the
block contains `break` or `return`. CRuby is finer-grained: the copy
inherits the orphan status of its original, so the copy only becomes
orphan once the original yielding method returns.
```ruby
def m(&b)
b.dup
end
x = m { break 1 }
x.call
```
#### CRuby
```
LocalJumpError # raised only after m returns; if called inside m,
# the dup is still a live block
```
#### mruby
```
LocalJumpError # always raised — the dup is orphan from the moment
# it is created
```
mruby's stricter rule keeps `RProc` from needing a back-pointer to
the original block (which would also enlarge the GC mark set).
+8
View File
@@ -322,6 +322,14 @@ mrb_proc_init_copy(mrb_state *mrb, mrb_value self)
check_proc(mrb, proc);
mrb_proc_copy(mrb, mrb_proc_ptr(self), mrb_proc_ptr(proc));
/* A copied Proc is always treated as an orphan block: it cannot
`break` / `return` from the original yielding method. This is
stricter than CRuby (which only marks the copy orphan once the
original becomes orphan), but matches mruby's memory-first
design — tracking the original via a back pointer would grow
RProc and the GC mark set. See limitations.md for the spec
divergence note. */
mrb_proc_ptr(self)->flags |= MRB_PROC_ORPHAN;
return self;
}
+12
View File
@@ -179,6 +179,18 @@ assert('Creation of a proc through the block of a method') do
end
end
assert('#6345: dup of a block from method is treated as orphan') do
def m(&b) b.dup end
# The dup is orphan, so calling it raises LocalJumpError on break.
assert_raise LocalJumpError do
m { break 1 }.call
end
# A dup of a block without break still returns normally.
assert_equal 42, m { 42 }.call
end
assert('identity check for proc object') do
b = []
t = 2