From 061f9b8bfdde47371df97a1d3d6a4c4eec06b723 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Mon, 15 Sep 2025 17:48:56 -0500 Subject: [PATCH 1/3] [ruby/erb] [DOC] More on class ERB (https://github.com/ruby/erb/pull/69) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [DOC] More on class …ERB * [DOC] More on class …ERB * More * More * More https://github.com/ruby/erb/commit/d9d73ed58e --- lib/erb.rb | 121 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 109 insertions(+), 12 deletions(-) diff --git a/lib/erb.rb b/lib/erb.rb index d4f43e0772341b..f3f2ae3f471811 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -189,7 +189,17 @@ # # When you call method #result, # the method executes the code and removes the entire execution tag -# (generating no text in the result). +# (generating no text in the result): +# +# ``` +# ERB.new('foo <% Dir.chdir("C:/") %> bar').result # => "foo bar" +# ``` +# +# Whitespace before and after the embedded code is optional: +# +# ``` +# ERB.new('foo <%Dir.chdir("C:/")%> bar').result # => "foo bar" +# ``` # # You can interleave text with execution tags to form a control structure # such as a conditional, a loop, or a `case` statements. @@ -261,7 +271,7 @@ # # #### Shorthand Format for Execution Tags # -# You can give `trim_mode: '%'` to enable a shorthand format for execution tags; +# You can use keyword argument `trim_mode: '%'` to enable a shorthand format for execution tags; # this example uses the shorthand format `% _code_` instead of `<% _code_ %>`: # # ``` @@ -283,6 +293,90 @@ # Note that in the shorthand format, the character `'%'` must be the first character in the code line # (no leading whitespace). # +# #### Suppressing Unwanted Blank Lines +# +# With keyword argument `trim_mode` not given, +# all blank lines go into the result: +# +# ``` +# s = < +# <%= RUBY_VERSION %> +# <% end %> +# EOT +# ERB.new(s).result.lines.each {|line| puts line.inspect } +# "\n" +# "3.4.5\n" +# "\n" +# ``` +# +# You can give `trim_mode: '-'`, you can suppress each blank line +# whose source line ends with `-%>` (instead of `%>`): +# +# ``` +# s = < +# <%= RUBY_VERSION %> +# <% end -%> +# EOT +# ERB.new(s, trim_mode: '-').result.lines.each {|line| puts line.inspect } +# "3.4.5\n" +# ``` +# +# It is an error to use the trailing `'-%>'` notation without `trim_mode: '-'`: +# +# ``` +# ERB.new(s).result.lines.each {|line| puts line.inspect } # Raises SyntaxError. +# ``` +# +# #### Suppressing Unwanted Newlines +# +# Consider this input string: +# +# ``` +# s = < +# <%= RUBY_VERSION %> +# foo <% RUBY_VERSION %> +# foo <%= RUBY_VERSION %> +# EOT +# ``` +# +# With keyword argument `trim_mode` not given, all newlines go into the result: +# +# ``` +# ERB.new(s).result.lines.each {|line| puts line.inspect } +# "\n" +# "3.4.5\n" +# "foo \n" +# "foo 3.4.5\n" +# ``` +# +# You can give `trim_mode: '>'` to suppress the trailing newline +# for each line that ends with `'%<'` (regardless of its beginning): +# +# ``` +# ERB.new(s, trim_mode: '>').result.lines.each {|line| puts line.inspect } +# "3.4.5foo foo 3.4.5" +# ``` +# +# You can give `trim_mode: '<>'` to suppress the trailing newline +# for each line that both begins with `'<%'` and ends with `'%<'`: +# +# ``` +# ERB.new(s, trim_mode: '<>').result.lines.each {|line| puts line.inspect } +# "3.4.5foo \n" +# "foo 3.4.5\n" +# ``` +# +# #### Combining Trim Modes +# +# You can combine certain trim modes: +# +# - `'%-'`: Enable shorthand and omit each blank line ending with `'%>'`. +# - `'%>'`: Enable shorthand and omit newline for each line ending with `'%>'`. +# - `'%<>'`: Enable shorthand and omit newline for each line starting with `'<%'` and ending with `'%>'`. +# # ### Comment Tags # # You can embed a comment in a template using a *comment tag*; @@ -537,19 +631,19 @@ def self.version # # **Keyword Argument `trim_mode`** # - # When keyword argument `trim_mode` has a string value, - # that value may be one of: + # You can use keyword argument `trim_mode: '%'` + # to enable the [shorthand format][shorthand format] for execution tags. + # + # This value allows [blank line control][blank line control]: # - # - `'%'`: Enable [shorthand format][shorthand format] for execution tags. # - `'-'`: Omit each blank line ending with `'%>'`. + # + # Other values allow [newline control][newline control]: + # # - `'>'`: Omit newline for each line ending with `'%>'`. # - `'<>'`: Omit newline for each line starting with `'<%'` and ending with `'%>'`. # - # The value may also be certain combinations of the above. - # - # - `'%-'`: Enable shorthand and omit each blank line ending with `'%>'`. - # - `'%>'`: Enable shorthand and omit newline for each line ending with `'%>'`. - # - `'%<>'`: Enable shorthand and omit newline for each line starting with `'<%'` and ending with `'%>'`. + # You can also [combine trim modes][combine trim modes]. # # **Keyword Argument `eoutvar`** # @@ -578,9 +672,12 @@ def self.version # However, their values, if given, are handled thus: # # - `safe_level`: ignored. - # - `legacy_trim_mode: overrides keyword argument `trim_mode`. - # - `legacy_eoutvar: overrides keyword argument `eoutvar`. + # - `legacy_trim_mode`: overrides keyword argument `trim_mode`. + # - `legacy_eoutvar`: overrides keyword argument `eoutvar`. # + # [blank line control]: rdoc-ref:ERB@Suppressing+Unwanted+Blank+Lines + # [combine trim modes]: rdoc-ref:ERB@Combining+Trim+Modes + # [newline control]: rdoc-ref:ERB@Suppressing+Unwanted+Newlines # [shorthand format]: rdoc-ref:ERB@Shorthand+Format+for+Execution+Tags # def initialize(str, safe_level=NOT_GIVEN, legacy_trim_mode=NOT_GIVEN, legacy_eoutvar=NOT_GIVEN, trim_mode: nil, eoutvar: '_erbout') From e4f09a8c94e6e6d21a6dfa43f71d52e4096234d6 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Mon, 15 Sep 2025 14:30:33 -0700 Subject: [PATCH 2/3] Remove next field and unused method from tmpbuf These used to be used by the parser --- imemo.c | 15 +-------------- internal/imemo.h | 2 -- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/imemo.c b/imemo.c index 7cec33bc1edf00..74eae5678f3532 100644 --- a/imemo.c +++ b/imemo.c @@ -94,17 +94,6 @@ rb_free_tmp_buffer(volatile VALUE *store) } } -rb_imemo_tmpbuf_t * -rb_imemo_tmpbuf_parser_heap(void *buf, rb_imemo_tmpbuf_t *old_heap, size_t cnt) -{ - rb_imemo_tmpbuf_t *tmpbuf = (rb_imemo_tmpbuf_t *)rb_imemo_tmpbuf_new(); - tmpbuf->ptr = buf; - tmpbuf->next = old_heap; - tmpbuf->cnt = cnt; - - return tmpbuf; -} - static VALUE imemo_fields_new(VALUE owner, size_t capa) { @@ -478,9 +467,7 @@ rb_imemo_mark_and_move(VALUE obj, bool reference_updating) const rb_imemo_tmpbuf_t *m = (const rb_imemo_tmpbuf_t *)obj; if (!reference_updating) { - do { - rb_gc_mark_locations(m->ptr, m->ptr + m->cnt); - } while ((m = m->next) != NULL); + rb_gc_mark_locations(m->ptr, m->ptr + m->cnt); } break; diff --git a/internal/imemo.h b/internal/imemo.h index de617d94c1bf5c..eee3dd71854a8e 100644 --- a/internal/imemo.h +++ b/internal/imemo.h @@ -94,7 +94,6 @@ struct vm_ifunc { struct rb_imemo_tmpbuf_struct { VALUE flags; VALUE *ptr; /* malloc'ed buffer */ - struct rb_imemo_tmpbuf_struct *next; /* next imemo */ size_t cnt; /* buffer size in VALUE */ }; @@ -133,7 +132,6 @@ struct MEMO { typedef struct rb_imemo_tmpbuf_struct rb_imemo_tmpbuf_t; #endif VALUE rb_imemo_tmpbuf_new(void); -rb_imemo_tmpbuf_t *rb_imemo_tmpbuf_parser_heap(void *buf, rb_imemo_tmpbuf_t *old_heap, size_t cnt); struct vm_ifunc *rb_vm_ifunc_new(rb_block_call_func_t func, const void *data, int min_argc, int max_argc); static inline enum imemo_type imemo_type(VALUE imemo); static inline int imemo_type_p(VALUE imemo, enum imemo_type imemo_type); From 6c5960ae1955b66b4a13e03012d53853ee1fd1de Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Mon, 15 Sep 2025 17:43:41 -0700 Subject: [PATCH 3/3] ZJIT: Support compiling block args (#14537) --- test/ruby/test_zjit.rb | 15 ++++++++ zjit/src/backend/lir.rs | 7 +--- zjit/src/hir.rs | 80 +++++++++++++++++++++++------------------ zjit/src/stats.rs | 4 +-- 4 files changed, 63 insertions(+), 43 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 87d8b06ece2a8d..fc79bdda6ea231 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -439,6 +439,21 @@ def entry = [test(1), test(3, 4)] }, call_threshold: 2 end + def test_send_nil_block_arg + assert_compiles 'false', %q{ + def test = block_given? + def entry = test(&nil) + test + } + end + + def test_send_symbol_block_arg + assert_compiles '["1", "2"]', %q{ + def test = [1, 2].map(&:to_s) + test + } + end + def test_forwardable_iseq assert_compiles '1', %q{ def test(...) = 1 diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index c67bf8c9ba01e8..5f96d0718a9d9f 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -6,7 +6,7 @@ use crate::cruby::{Qundef, RUBY_OFFSET_CFP_PC, RUBY_OFFSET_CFP_SP, SIZEOF_VALUE_ use crate::hir::SideExitReason; use crate::options::{debug, get_option}; use crate::cruby::VALUE; -use crate::stats::{exit_counter_ptr, exit_counter_ptr_for_call_type, exit_counter_ptr_for_opcode, CompileError}; +use crate::stats::{exit_counter_ptr, exit_counter_ptr_for_opcode, CompileError}; use crate::virtualmem::CodePtr; use crate::asm::{CodeBlock, Label}; @@ -1601,11 +1601,6 @@ impl Assembler self.load_into(SCRATCH_OPND, Opnd::const_ptr(exit_counter_ptr_for_opcode(opcode))); self.incr_counter_with_reg(Opnd::mem(64, SCRATCH_OPND, 0), 1.into(), C_RET_OPND); } - if let SideExitReason::UnhandledCallType(call_type) = reason { - asm_comment!(self, "increment an unknown call type counter"); - self.load_into(SCRATCH_OPND, Opnd::const_ptr(exit_counter_ptr_for_call_type(call_type))); - self.incr_counter_with_reg(Opnd::mem(64, SCRATCH_OPND, 0), 1.into(), C_RET_OPND); - } } asm_comment!(self, "exit to the interpreter"); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 3726d8ec0e44d5..46bf38dcc6a4d4 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -447,10 +447,10 @@ impl PtrPrintMap { #[derive(Debug, Clone, Copy)] pub enum SideExitReason { UnknownNewarraySend(vm_opt_newarray_send_type), - UnhandledCallType(CallType), UnknownSpecialVariable(u64), UnhandledHIRInsn(InsnId), UnhandledYARVInsn(u32), + UnhandledTailCall, FixnumAddOverflow, FixnumSubOverflow, FixnumMultOverflow, @@ -2986,10 +2986,8 @@ fn num_locals(iseq: *const rb_iseq_t) -> usize { } /// If we can't handle the type of send (yet), bail out. -fn unknown_call_type(flag: u32) -> Result<(), CallType> { - if (flag & VM_CALL_ARGS_BLOCKARG) != 0 { return Err(CallType::BlockArg); } - if (flag & VM_CALL_TAILCALL) != 0 { return Err(CallType::Tailcall); } - Ok(()) +fn is_tailcall(flags: u32) -> bool { + (flags & VM_CALL_TAILCALL) != 0 } /// We have IseqPayload, which keeps track of HIR Types in the interpreter, but this is not useful @@ -3501,10 +3499,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { // NB: opt_neq has two cd; get_arg(0) is for eq and get_arg(1) is for neq let cd: *const rb_call_data = get_arg(pc, 1).as_ptr(); let call_info = unsafe { rb_get_call_data_ci(cd) }; - if let Err(call_type) = unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) { - // Unknown call type; side-exit into the interpreter + let flags = unsafe { rb_vm_ci_flag(call_info) }; + if is_tailcall(flags) { + // Can't handle tailcall; side-exit into the interpreter let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledTailCall }); break; // End the block } let argc = unsafe { vm_ci_argc((*cd).ci) }; @@ -3522,10 +3521,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { // NB: these instructions have the recv for the call at get_arg(0) let cd: *const rb_call_data = get_arg(pc, 1).as_ptr(); let call_info = unsafe { rb_get_call_data_ci(cd) }; - if let Err(call_type) = unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) { - // Unknown call type; side-exit into the interpreter + let flags = unsafe { rb_vm_ci_flag(call_info) }; + if is_tailcall(flags) { + // Can't handle tailcall; side-exit into the interpreter let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledTailCall }); break; // End the block } let argc = unsafe { vm_ci_argc((*cd).ci) }; @@ -3580,10 +3580,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { YARVINSN_opt_send_without_block => { let cd: *const rb_call_data = get_arg(pc, 0).as_ptr(); let call_info = unsafe { rb_get_call_data_ci(cd) }; - if let Err(call_type) = unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) { - // Unknown call type; side-exit into the interpreter + let flags = unsafe { rb_vm_ci_flag(call_info) }; + if is_tailcall(flags) { + // Can't handle tailcall; side-exit into the interpreter let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledTailCall }); break; // End the block } let argc = unsafe { vm_ci_argc((*cd).ci) }; @@ -3598,15 +3599,17 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let cd: *const rb_call_data = get_arg(pc, 0).as_ptr(); let blockiseq: IseqPtr = get_arg(pc, 1).as_iseq(); let call_info = unsafe { rb_get_call_data_ci(cd) }; - if let Err(call_type) = unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) { - // Unknown call type; side-exit into the interpreter + let flags = unsafe { rb_vm_ci_flag(call_info) }; + if is_tailcall(flags) { + // Can't handle tailcall; side-exit into the interpreter let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledTailCall }); break; // End the block } let argc = unsafe { vm_ci_argc((*cd).ci) }; + let block_arg = (flags & VM_CALL_ARGS_BLOCKARG) != 0; - let args = state.stack_pop_n(argc as usize)?; + let args = state.stack_pop_n(argc as usize + usize::from(block_arg))?; let recv = state.stack_pop()?; let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); let send = fun.push_insn(block, Insn::Send { recv, cd, blockiseq, args, state: exit_id }); @@ -3624,14 +3627,16 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { YARVINSN_invokesuper => { let cd: *const rb_call_data = get_arg(pc, 0).as_ptr(); let call_info = unsafe { rb_get_call_data_ci(cd) }; - if let Err(call_type) = unknown_call_type(unsafe { rb_vm_ci_flag(call_info) } & !VM_CALL_SUPER & !VM_CALL_ZSUPER) { - // Unknown call type; side-exit into the interpreter + let flags = unsafe { rb_vm_ci_flag(call_info) }; + if is_tailcall(flags) { + // Can't handle tailcall; side-exit into the interpreter let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledTailCall }); break; // End the block } let argc = unsafe { vm_ci_argc((*cd).ci) }; - let args = state.stack_pop_n(argc as usize)?; + let block_arg = (flags & VM_CALL_ARGS_BLOCKARG) != 0; + let args = state.stack_pop_n(argc as usize + usize::from(block_arg))?; let recv = state.stack_pop()?; let blockiseq: IseqPtr = get_arg(pc, 1).as_ptr(); let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); @@ -3652,14 +3657,16 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { YARVINSN_invokeblock => { let cd: *const rb_call_data = get_arg(pc, 0).as_ptr(); let call_info = unsafe { rb_get_call_data_ci(cd) }; - if let Err(call_type) = unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) { - // Unknown call type; side-exit into the interpreter + let flags = unsafe { rb_vm_ci_flag(call_info) }; + if is_tailcall(flags) { + // Can't handle tailcall; side-exit into the interpreter let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); - fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledTailCall }); break; // End the block } let argc = unsafe { vm_ci_argc((*cd).ci) }; - let args = state.stack_pop_n(argc as usize)?; + let block_arg = (flags & VM_CALL_ARGS_BLOCKARG) != 0; + let args = state.stack_pop_n(argc as usize + usize::from(block_arg))?; let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); let result = fun.push_insn(block, Insn::InvokeBlock { cd, args, state: exit_id }); state.stack_push(result); @@ -3767,11 +3774,6 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } YARVINSN_objtostring => { let cd: *const rb_call_data = get_arg(pc, 0).as_ptr(); - let call_info = unsafe { rb_get_call_data_ci(cd) }; - - if let Err(call_type) = unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) { - panic!("objtostring should not have unknown call type {call_type:?}"); - } let argc = unsafe { vm_ci_argc((*cd).ci) }; assert_eq!(0, argc, "objtostring should not have args"); @@ -5115,14 +5117,17 @@ mod tests { } #[test] - fn test_cant_compile_block_arg() { + fn test_compile_block_arg() { eval(" def test(a) = foo(&a) "); assert_snapshot!(hir_string("test"), @r" fn test@:2: bb0(v0:BasicObject, v1:BasicObject): - SideExit UnhandledCallType(BlockArg) + v6:BasicObject = Send v0, 0x1000, :foo, v1 + v7:BasicObject = GetLocal l0, EP@3 + CheckInterrupts + Return v6 "); } @@ -5194,7 +5199,9 @@ mod tests { fn test@:2: bb0(v0:BasicObject): v4:NilClass = Const Value(nil) - SideExit UnhandledCallType(BlockArg) + v6:BasicObject = InvokeSuper v0, 0x1000, v4 + CheckInterrupts + Return v6 "); } @@ -8074,7 +8081,10 @@ mod opt_tests { fn test@:2: bb0(v0:BasicObject, v1:BasicObject): v6:BasicObject = GetBlockParamProxy l0 - SideExit UnhandledCallType(BlockArg) + v8:BasicObject = Send v0, 0x1000, :tap, v6 + v9:BasicObject = GetLocal l0, EP@3 + CheckInterrupts + Return v8 "); } diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 8a7073dd977c94..69748f4ebc9cbd 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -92,7 +92,7 @@ make_counters! { // exit_: Side exits reasons exit_compile_error, exit_unknown_newarray_send, - exit_unhandled_call_type, + exit_unhandled_tailcall, exit_unknown_special_variable, exit_unhandled_hir_insn, exit_unhandled_yarv_insn, @@ -209,7 +209,7 @@ pub fn exit_counter_ptr(reason: crate::hir::SideExitReason) -> *mut u64 { use crate::stats::Counter::*; let counter = match reason { UnknownNewarraySend(_) => exit_unknown_newarray_send, - UnhandledCallType(_) => exit_unhandled_call_type, + UnhandledTailCall => exit_unhandled_tailcall, UnknownSpecialVariable(_) => exit_unknown_special_variable, UnhandledHIRInsn(_) => exit_unhandled_hir_insn, UnhandledYARVInsn(_) => exit_unhandled_yarv_insn,