mirror of
https://github.com/mruby/mruby
synced 2026-06-08 16:11:16 +00:00
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:
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user