diff --git a/doc/limitations.md b/doc/limitations.md index 07789fa88..8fae52116 100644 --- a/doc/limitations.md +++ b/doc/limitations.md @@ -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). diff --git a/src/proc.c b/src/proc.c index 963841689..af0dc6810 100644 --- a/src/proc.c +++ b/src/proc.c @@ -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; } diff --git a/test/t/proc.rb b/test/t/proc.rb index 417213dc2..f9c4b4be1 100644 --- a/test/t/proc.rb +++ b/test/t/proc.rb @@ -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