diff --git a/gc.c b/gc.c index 5a18afa4a71417..341fb2066731fe 100644 --- a/gc.c +++ b/gc.c @@ -991,8 +991,7 @@ newobj_of(rb_ractor_t *cr, VALUE klass, VALUE flags, bool wb_protected, size_t s gc_validate_pc(); if (UNLIKELY(rb_gc_event_hook_required_p(RUBY_INTERNAL_EVENT_NEWOBJ))) { - unsigned int lev; - RB_VM_LOCK_ENTER_CR_LEV(cr, &lev); + int lev = RB_GC_VM_LOCK_NO_BARRIER(); { memset((char *)obj + RVALUE_SIZE, 0, rb_gc_obj_slot_size(obj) - RVALUE_SIZE); @@ -1007,7 +1006,7 @@ newobj_of(rb_ractor_t *cr, VALUE klass, VALUE flags, bool wb_protected, size_t s } if (!gc_disabled) rb_gc_enable(); } - RB_VM_LOCK_LEAVE_CR_LEV(cr, &lev); + RB_GC_VM_UNLOCK_NO_BARRIER(lev); } #if RGENGC_CHECK_MODE diff --git a/lib/erb.rb b/lib/erb.rb index e72fe95572bdf9..71ee8861f39dd2 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -777,12 +777,7 @@ class ERB # :call-seq: # self.version -> string # - # Returns the string revision for \ERB: - # - # ``` - # ERB.version # => "4.0.4" - # ``` - # + # Returns the string \ERB version. def self.version VERSION end @@ -815,7 +810,9 @@ def self.version # **Keyword Argument `eoutvar`** # # The string value of keyword argument `eoutvar` specifies the name of the variable - # that method #result uses to construct its result string. + # that method #result uses to construct its result string; + # see #src. + # # This is useful when you need to run multiple \ERB templates through the same binding # and/or when you want to control where output ends up. # @@ -868,25 +865,82 @@ def initialize(str, safe_level=NOT_GIVEN, legacy_trim_mode=NOT_GIVEN, legacy_eou @lineno = 0 @_init = self.class.singleton_class end + + # :markup: markdown + # + # Placeholder constant; used as default value for certain method arguments. NOT_GIVEN = defined?(Ractor) ? Ractor.make_shareable(Object.new) : Object.new private_constant :NOT_GIVEN - ## - # Creates a new compiler for ERB. See ERB::Compiler.new for details + # :markup: markdown + # + # :call-seq: + # make_compiler -> erb_compiler + # + # Returns a new ERB::Compiler with the given `trim_mode`; + # for `trim_mode` values, see ERB.new: + # + # ``` + # ERB.new('').make_compiler(nil) + # # => # + # ``` + # def make_compiler(trim_mode) ERB::Compiler.new(trim_mode) end - # The Ruby code generated by ERB + # :markup: markdown + # + # Returns a string containing the Ruby code that, when executed, generates the result; + # the code is executed by method #result, + # and by its wrapper methods #result_with_hash and #run: + # + # ``` + # s = 'The time is <%= Time.now %>.' + # template = ERB.new(s) + # template.src + # # => "#coding:UTF-8\n_erbout = +''; _erbout.<< \"The time is \".freeze; _erbout.<<(( Time.now ).to_s); _erbout.<< \".\".freeze; _erbout" + # template.result + # # => "The time is 2025-09-18 15:58:08 -0500." + # ``` + # + # In a more readable format: + # + # ``` + # # puts template.src.split('; ') + # # #coding:UTF-8 + # # _erbout = +'' + # # _erbout.<< "The time is ".freeze + # # _erbout.<<(( Time.now ).to_s) + # # _erbout.<< ".".freeze + # # _erbout + # ``` + # + # Variable `_erbout` is used to store the intermediate results in the code; + # the name `_erbout` is the default in ERB.new, + # and can be changed via keyword argument `eoutvar`: + # + # ``` + # template = ERB.new(s, eoutvar: '_foo') + # puts template.src.split('; ') + # #coding:UTF-8 + # _foo = +'' + # _foo.<< "The time is ".freeze + # _foo.<<(( Time.now ).to_s) + # _foo.<< ".".freeze + # _foo + # ``` + # attr_reader :src # :markup: markdown # # Returns the encoding of `self`; - # see [encoding][encoding]. + # see [Encodings][encodings]: + # + # [encodings]: rdoc-ref:ERB@Encodings # - # [encoding]: https://docs.ruby-lang.org/en/master/Encoding.html attr_reader :encoding # :markup: markdown @@ -920,10 +974,35 @@ def location=((filename, lineno)) @lineno = lineno if lineno end + # :markup: markdown # - # Can be used to set _eoutvar_ as described in ERB::new. It's probably - # easier to just use the constructor though, since calling this method - # requires the setup of an ERB _compiler_ object. + # :call-seq: + # set_eoutvar(compiler, eoutvar = '_erbout') -> [eoutvar] + # + # Sets the `eoutvar` value in the ERB::Compiler object `compiler`; + # returns a 1-element array containing the value of `eoutvar`: + # + # ``` + # template = ERB.new('') + # compiler = template.make_compiler(nil) + # pp compiler + # # + # template.set_eoutvar(compiler, '_foo') # => ["_foo"] + # pp compiler + # # + # ``` # def set_eoutvar(compiler, eoutvar = '_erbout') compiler.put_cmd = "#{eoutvar}.<<" diff --git a/lib/erb/version.rb b/lib/erb/version.rb index 1c2c6fe1641bbb..7d0b384f71c741 100644 --- a/lib/erb/version.rb +++ b/lib/erb/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class ERB - # The version string + # The string \ERB version. VERSION = '5.0.2' end diff --git a/test/objspace/test_ractor.rb b/test/objspace/test_ractor.rb index a14604c1e7c05f..eb3044cda3c3c3 100644 --- a/test/objspace/test_ractor.rb +++ b/test/objspace/test_ractor.rb @@ -2,10 +2,6 @@ class TestObjSpaceRactor < Test::Unit::TestCase def test_tracing_does_not_crash - # https://ci.rvm.jp/results/trunk-random1@ruby-sp2-noble-docker/5954509 - # https://ci.rvm.jp/results/trunk-random0@ruby-sp2-noble-docker/5954501 - omit "crashes frequently on CI but not able to reproduce locally" - assert_ractor(<<~RUBY, require: 'objspace') ObjectSpace.trace_object_allocations do r = Ractor.new do diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index a3026890ff9a84..ea1481ca2d65c1 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -478,7 +478,7 @@ def test(...) = 1 end def test_sendforward - assert_runs '[1, 2]', %q{ + assert_compiles '[1, 2]', %q{ def callee(a, b) = [a, b] def test(...) = callee(...) test(1, 2) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 741c2397e7d011..15c6d1a2ed70f4 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -365,6 +365,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::IfTrue { val, target } => no_output!(gen_if_true(jit, asm, opnd!(val), target)), Insn::IfFalse { val, target } => no_output!(gen_if_false(jit, asm, opnd!(val), target)), &Insn::Send { cd, blockiseq, state, .. } => gen_send(jit, asm, cd, blockiseq, &function.frame_state(state)), + &Insn::SendForward { cd, blockiseq, state, .. } => gen_send_forward(jit, asm, cd, blockiseq, &function.frame_state(state)), Insn::SendWithoutBlock { cd, state, def_type, .. } => gen_send_without_block(jit, asm, *cd, *def_type, &function.frame_state(*state)), // Give up SendWithoutBlockDirect for 6+ args since asm.ccall() doesn't support it. Insn::SendWithoutBlockDirect { cd, state, args, .. } if args.len() + 1 > C_ARG_OPNDS.len() => // +1 for self @@ -1041,6 +1042,29 @@ fn gen_send( ) } +/// Compile a dynamic dispatch with `...` +fn gen_send_forward( + jit: &mut JITState, + asm: &mut Assembler, + cd: *const rb_call_data, + blockiseq: IseqPtr, + state: &FrameState, +) -> lir::Opnd { + gen_incr_counter(asm, Counter::dynamic_send_count); + gen_incr_counter(asm, Counter::dynamic_send_type_send_forward); + + gen_prepare_non_leaf_call(jit, asm, state); + + asm_comment!(asm, "call #{} with dynamic dispatch", ruby_call_method_name(cd)); + unsafe extern "C" { + fn rb_vm_sendforward(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE; + } + asm.ccall( + rb_vm_sendforward as *const u8, + vec![EC, CFP, (cd as usize).into(), VALUE(blockiseq as usize).into()], + ) +} + /// Compile a dynamic dispatch without block fn gen_send_without_block( jit: &mut JITState, diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 543ed186c5fdaa..48cbd423b3cd95 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -642,6 +642,7 @@ pub enum Insn { state: InsnId, }, Send { recv: InsnId, cd: *const rb_call_data, blockiseq: IseqPtr, args: Vec, state: InsnId }, + SendForward { recv: InsnId, cd: *const rb_call_data, blockiseq: IseqPtr, args: Vec, state: InsnId }, InvokeSuper { recv: InsnId, cd: *const rb_call_data, blockiseq: IseqPtr, args: Vec, state: InsnId }, InvokeBlock { cd: *const rb_call_data, args: Vec, state: InsnId }, @@ -902,6 +903,13 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { } Ok(()) } + Insn::SendForward { cd, args, blockiseq, .. } => { + write!(f, "SendForward {:p}, :{}", self.ptr_map.map_ptr(blockiseq), ruby_call_method_name(*cd))?; + for arg in args { + write!(f, ", {arg}")?; + } + Ok(()) + } Insn::InvokeSuper { recv, blockiseq, args, .. } => { write!(f, "InvokeSuper {recv}, {:p}", self.ptr_map.map_ptr(blockiseq))?; for arg in args { @@ -1422,6 +1430,13 @@ impl Function { args: find_vec!(args), state, }, + &SendForward { recv, cd, blockiseq, ref args, state } => SendForward { + recv: find!(recv), + cd, + blockiseq, + args: find_vec!(args), + state, + }, &InvokeSuper { recv, cd, blockiseq, ref args, state } => InvokeSuper { recv: find!(recv), cd, @@ -1552,6 +1567,7 @@ impl Function { Insn::SendWithoutBlock { .. } => types::BasicObject, Insn::SendWithoutBlockDirect { .. } => types::BasicObject, Insn::Send { .. } => types::BasicObject, + Insn::SendForward { .. } => types::BasicObject, Insn::InvokeSuper { .. } => types::BasicObject, Insn::InvokeBlock { .. } => types::BasicObject, Insn::InvokeBuiltin { return_type, .. } => return_type.unwrap_or(types::BasicObject), @@ -2430,6 +2446,7 @@ impl Function { worklist.push_back(state); } &Insn::Send { recv, ref args, state, .. } + | &Insn::SendForward { recv, ref args, state, .. } | &Insn::SendWithoutBlock { recv, ref args, state, .. } | &Insn::CCallVariadic { recv, ref args, state, .. } | &Insn::SendWithoutBlockDirect { recv, ref args, state, .. } @@ -3774,13 +3791,44 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let send = fun.push_insn(block, Insn::Send { recv, cd, blockiseq, args, state: exit_id }); state.stack_push(send); - // Reload locals that may have been modified by the blockiseq. - // TODO: Avoid reloading locals that are not referenced by the blockiseq - // or not used after this. Max thinks we could eventually DCE them. - for local_idx in 0..state.locals.len() { - let ep_offset = local_idx_to_ep_offset(iseq, local_idx) as u32; - let val = fun.push_insn(block, Insn::GetLocal { ep_offset, level: 0 }); - state.setlocal(ep_offset, val); + if !blockiseq.is_null() { + // Reload locals that may have been modified by the blockiseq. + // TODO: Avoid reloading locals that are not referenced by the blockiseq + // or not used after this. Max thinks we could eventually DCE them. + for local_idx in 0..state.locals.len() { + let ep_offset = local_idx_to_ep_offset(iseq, local_idx) as u32; + let val = fun.push_insn(block, Insn::GetLocal { ep_offset, level: 0 }); + state.setlocal(ep_offset, val); + } + } + } + YARVINSN_sendforward => { + 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) }; + let flags = unsafe { rb_vm_ci_flag(call_info) }; + let forwarding = (flags & VM_CALL_FORWARDING) != 0; + if let Err(call_type) = unhandled_call_type(flags) { + // Can't handle the call type; 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) }); + break; // End the block + } + let argc = unsafe { vm_ci_argc((*cd).ci) }; + + let args = state.stack_pop_n(argc as usize + usize::from(forwarding))?; + let recv = state.stack_pop()?; + let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); + let send_forward = fun.push_insn(block, Insn::SendForward { recv, cd, blockiseq, args, state: exit_id }); + state.stack_push(send_forward); + + if !blockiseq.is_null() { + // Reload locals that may have been modified by the blockiseq. + for local_idx in 0..state.locals.len() { + let ep_offset = local_idx_to_ep_offset(iseq, local_idx) as u32; + let val = fun.push_insn(block, Insn::GetLocal { ep_offset, level: 0 }); + state.setlocal(ep_offset, val); + } } } YARVINSN_invokesuper => { @@ -5315,7 +5363,6 @@ mod tests { fn test@:2: bb0(v0:BasicObject, v1:BasicObject): v6:BasicObject = Send v0, 0x1000, :foo, v1 - v7:BasicObject = GetLocal l0, EP@3 CheckInterrupts Return v6 "); @@ -5457,14 +5504,33 @@ mod tests { } #[test] - fn test_cant_compile_forwarding() { + fn test_compile_forwarding() { eval(" def test(...) = foo(...) "); assert_snapshot!(hir_string("test"), @r" fn test@:2: bb0(v0:BasicObject, v1:BasicObject): - SideExit UnhandledYARVInsn(sendforward) + v6:BasicObject = SendForward 0x1000, :foo, v1 + CheckInterrupts + Return v6 + "); + } + + #[test] + fn test_compile_triple_dots_with_positional_args() { + eval(" + def test(a, ...) = foo(a, ...) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(v0:BasicObject, v1:BasicObject, v2:ArrayExact, v3:BasicObject, v4:BasicObject): + v5:NilClass = Const Value(nil) + v10:ArrayExact = ToArray v2 + PatchPoint NoEPEscape(test) + GuardBlockParamProxy l0 + v15:BasicObject[BlockParamProxy] = Const Value(VALUE(0x1000)) + SideExit UnhandledYARVInsn(splatkw) "); } @@ -8274,7 +8340,6 @@ mod opt_tests { GuardBlockParamProxy l0 v7:BasicObject[BlockParamProxy] = Const Value(VALUE(0x1000)) v9:BasicObject = Send v0, 0x1008, :tap, v7 - v10:BasicObject = GetLocal l0, EP@3 CheckInterrupts Return v9 "); diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 1daf1fda4a9171..a47e78939addb7 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -133,6 +133,7 @@ make_counters! { dynamic_send_count, dynamic_send_type_send_without_block, dynamic_send_type_send, + dynamic_send_type_send_forward, dynamic_send_type_invokeblock, dynamic_send_type_invokesuper,