Bug #20917
openredo/next in nested begin block causes wrong order of execution
Description
It seems that redo/next in a nested begin block can cause the wrong order of execution.
For example:
It prints:
But I think it should print:
Because execution order should be:
puts 0puts 1puts 2redo- unwind to nested
ensureblock puts 3- unwind to outer
ensureblock puts 4break- end of loop
Interestingly enough, if we add an empty rescue block before any of the ensure blocks, then the execution order is correct.
Updated by Eregon (Benoit Daloze) almost 2 years ago
Indeed, it seems like a bug, I think as well the "redo jump/unwind/exception" should run ensure's and get to the "break jump/unwind/exception" which should override the redo like when a Ruby exception overrides another.
FWIW, this is 0 1 2 3 4 on both TruffleRuby and JRuby.
Updated by kddnewton (Kevin Newton) over 1 year ago
Agreed this is a bug. For additional context, this is the same on parse.y and prism.
Updated by wanabe (_ wanabe) 3 days ago
I simplified the script to make it easier to read in ISEQ.
$ ./miniruby -v --dump=insn a.rb
ruby 4.1.0dev (2026-08-30T06:52:45Z :detached: a889da909c) +PRISM [x86_64-linux]
== disasm: #<ISeq:<main>@a.rb:1 (1,0)-(9,3)>
== catch table
| catch type: break st: 0000 ed: 0004 sp: 0000 cont: 0004
| == disasm: #<ISeq:block in <main>@a.rb:1 (1,8)-(9,3)>
| == catch table
| | catch type: ensure st: 0001 ed: 0001 sp: 0001 cont: 0014
| | == disasm: #<ISeq:ensure in block in <main>@a.rb:4 (4,2)-(5,5)>
| | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
| | [ 1] "$!"@0
| | 0000 getlocal_WC_0 "$!"@0 ( 4)
| | 0002 throw 0
| | catch type: ensure st: 0001 ed: 0013 sp: 0001 cont: 0014
| | catch type: ensure st: 0001 ed: 0001 sp: 0001 cont: 0023
| | == disasm: #<ISeq:ensure in block in <main>@a.rb:7 (7,2)-(8,7)>
| | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
| | [ 1] "$!"@0
| | 0000 putself ( 7)[Li]
| | 0001 putobject_INT2FIX_1_
| | 0002 opt_send_without_block <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
| | 0004 pop
| | 0005 putnil ( 8)[Li]
| | 0006 throw 2
| | 0008 pop
| | 0009 getlocal_WC_0 "$!"@0 ( 6)
| | 0011 throw 0
| | catch type: ensure st: 0010 ed: 0014 sp: 0001 cont: 0023
| | catch type: redo st: 0001 ed: 0023 sp: 0000 cont: 0001
| | catch type: next st: 0001 ed: 0023 sp: 0000 cont: 0023
| |------------------------------------------------------------------------
| 0000 nop ( 1)[Bc]
| 0001 putself ( 7)[Li]
| 0002 putobject_INT2FIX_1_
| 0003 opt_send_without_block <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
| 0005 pop
| 0006 putnil ( 8)[Li]
| 0007 throw 2
| 0009 pop
| 0010 jump 1 ( 3)
| 0012 putnil
| 0013 nop
| 0014 putself ( 7)[Li]
| 0015 putobject_INT2FIX_1_
| 0016 opt_send_without_block <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
| 0018 pop
| 0019 putnil ( 8)[Li]
| 0020 throw 2
| 0022 pop
| 0023 leave ( 9)[Br]
|------------------------------------------------------------------------
0000 putobject_INT2FIX_1_ ( 1)[Li]
0001 send <calldata!mid:times, argc:0>, block in <main>
0004 leave
catch type: ensure st: 0001 ed: 0013 sp: 0001 cont: 0014 catches throw 2 but I guess it is unexpected.
For example, if I make a change like this, it seems like it would work in this case, but I can't tell if there are any side effects.
(To be honest, Gemini AI wrote this code for me, so I don't really understand what it means.)
diff --git a/compile.c b/compile.c
index 2ff207928a5..d92770e9b63 100644
--- a/compile.c
+++ b/compile.c
@@ -6675,7 +6675,13 @@ add_ensure_iseq(LINK_ANCHOR *const ret, rb_iseq_t *iseq, int is_return)
LABEL *lend = NEW_LABEL(0);
INIT_ANCHOR(ensure_part);
- add_ensure_range(iseq, enlp->erange, lstart, lend);
+ struct iseq_compile_data_ensure_node_stack *e = prev_enlp;
+ while (e) {
+ if (e->erange != NULL) {
+ add_ensure_range(iseq, e->erange, lstart, lend);
+ }
+ e = e->prev;
+ }
ISEQ_COMPILE_DATA(iseq)->ensure_node_stack = enlp->prev;
ADD_LABEL(ensure_part, lstart);
diff --git a/prism_compile.c b/prism_compile.c
index 69e7d535491..9402395ace5 100644
--- a/prism_compile.c
+++ b/prism_compile.c
@@ -4751,7 +4751,13 @@ pm_add_ensure_iseq(LINK_ANCHOR *const ret, rb_iseq_t *iseq, int is_return, pm_sc
LABEL *lstart = NEW_LABEL(0);
LABEL *lend = NEW_LABEL(0);
- add_ensure_range(iseq, enlp->erange, lstart, lend);
+ struct iseq_compile_data_ensure_node_stack *e = prev_enlp;
+ while (e) {
+ if (e->erange != NULL) {
+ add_ensure_range(iseq, e->erange, lstart, lend);
+ }
+ e = e->prev;
+ }
ISEQ_COMPILE_DATA(iseq)->ensure_node_stack = enlp->prev;
PUSH_LABEL(ensure_part, lstart);
$ make -j miniruby 2>&1 >/dev/null && ./miniruby a.rb && ./miniruby -v --dump=insn a.rb
1
ruby 4.1.0dev (2026-08-30T06:52:45Z :detached: a889da909c) +PRISM [x86_64-linux]
== disasm: #<ISeq:<main>@a.rb:1 (1,0)-(9,3)>
== catch table
| catch type: break st: 0000 ed: 0004 sp: 0000 cont: 0004
| == disasm: #<ISeq:block in <main>@a.rb:1 (1,8)-(9,3)>
| == catch table
| | catch type: ensure st: 0001 ed: 0001 sp: 0001 cont: 0014
| | == disasm: #<ISeq:ensure in block in <main>@a.rb:4 (4,2)-(5,5)>
| | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
| | [ 1] "$!"@0
| | 0000 getlocal_WC_0 "$!"@0 ( 4)
| | 0002 throw 0
| | catch type: ensure st: 0001 ed: 0001 sp: 0001 cont: 0014
| | catch type: ensure st: 0010 ed: 0013 sp: 0001 cont: 0014
| | catch type: ensure st: 0001 ed: 0001 sp: 0001 cont: 0023
| | == disasm: #<ISeq:ensure in block in <main>@a.rb:7 (7,2)-(8,7)>
| | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
| | [ 1] "$!"@0
| | 0000 putself ( 7)[Li]
| | 0001 putobject_INT2FIX_1_
| | 0002 opt_send_without_block <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
| | 0004 pop
| | 0005 putnil ( 8)[Li]
| | 0006 throw 2
| | 0008 pop
| | 0009 getlocal_WC_0 "$!"@0 ( 6)
| | 0011 throw 0
| | catch type: ensure st: 0001 ed: 0001 sp: 0001 cont: 0023
| | catch type: ensure st: 0010 ed: 0014 sp: 0001 cont: 0023
| | catch type: redo st: 0001 ed: 0023 sp: 0000 cont: 0001
| | catch type: next st: 0001 ed: 0023 sp: 0000 cont: 0023
| |------------------------------------------------------------------------
| 0000 nop ( 1)[Bc]
| 0001 putself ( 7)[Li]
| 0002 putobject_INT2FIX_1_
| 0003 opt_send_without_block <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
| 0005 pop
| 0006 putnil ( 8)[Li]
| 0007 throw 2
| 0009 pop
| 0010 jump 1 ( 3)
| 0012 putnil
| 0013 nop
| 0014 putself ( 7)[Li]
| 0015 putobject_INT2FIX_1_
| 0016 opt_send_without_block <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
| 0018 pop
| 0019 putnil ( 8)[Li]
| 0020 throw 2
| 0022 pop
| 0023 leave ( 9)[Br]
|------------------------------------------------------------------------
0000 putobject_INT2FIX_1_ ( 1)[Li]
0001 send <calldata!mid:times, argc:0>, block in <main>
0004 leave