Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ They are still available on rubygems.org and can be installed with
* 4.0.3 to [v4.0.4][RubyGems-v4.0.4], [v4.0.5][RubyGems-v4.0.5], [v4.0.6][RubyGems-v4.0.6], [v4.0.7][RubyGems-v4.0.7], [v4.0.8][RubyGems-v4.0.8], [v4.0.9][RubyGems-v4.0.9], [v4.0.10][RubyGems-v4.0.10], [v4.0.11][RubyGems-v4.0.11], [v4.0.12][RubyGems-v4.0.12], [v4.0.13][RubyGems-v4.0.13], [v4.0.14][RubyGems-v4.0.14], [v4.0.15][RubyGems-v4.0.15], [v4.0.16][RubyGems-v4.0.16], [v4.0.17][RubyGems-v4.0.17]
* bundler 4.1.0.dev
* 4.0.3 to [v4.0.4][bundler-v4.0.4], [v4.0.5][bundler-v4.0.5], [v4.0.6][bundler-v4.0.6], [v4.0.7][bundler-v4.0.7], [v4.0.8][bundler-v4.0.8], [v4.0.9][bundler-v4.0.9], [v4.0.10][bundler-v4.0.10], [v4.0.11][bundler-v4.0.11], [v4.0.12][bundler-v4.0.12], [v4.0.13][bundler-v4.0.13], [v4.0.14][bundler-v4.0.14], [v4.0.15][bundler-v4.0.15], [v4.0.16][bundler-v4.0.16], [v4.0.17][bundler-v4.0.17]
* erb 6.0.6
* erb 6.0.7
* 6.0.1 to [v6.0.1.1][erb-v6.0.1.1], [v6.0.2][erb-v6.0.2], [v6.0.3][erb-v6.0.3], [v6.0.4][erb-v6.0.4], [v6.0.5][erb-v6.0.5], [v6.0.6][erb-v6.0.6]
* error_highlight 0.7.2
* ipaddr 1.2.9
Expand Down
1 change: 1 addition & 0 deletions internal/string.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ void rb_str_make_embedded(VALUE);
VALUE rb_str_upto_each(VALUE, VALUE, int, int (*each)(VALUE, VALUE), VALUE);
size_t rb_str_size_as_embedded(VALUE);
bool rb_str_reembeddable_p(VALUE);
bool rb_str_embedded_shared_root_p(VALUE);
VALUE rb_str_upto_endless_each(VALUE, int (*each)(VALUE, VALUE), VALUE);
VALUE rb_str_with_debug_created_info(VALUE, VALUE, int);
VALUE rb_str_frozen_bare_string(VALUE);
Expand Down
5 changes: 1 addition & 4 deletions lib/erb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -815,9 +815,6 @@
# [template processor]: https://en.wikipedia.org/wiki/Template_processor
#
class ERB
IDENTITY_METHOD = BasicObject.instance_method(:equal?) # :nodoc:
private_constant :IDENTITY_METHOD

# :markup: markdown
#
# :call-seq:
Expand Down Expand Up @@ -1117,7 +1114,7 @@ def new_toplevel(vars = nil)
private :new_toplevel

def initialized_by_new? # :nodoc:
IDENTITY_METHOD.bind_call(@_init, self.class.singleton_class)
self.class.singleton_class.equal? @_init
end
private :initialized_by_new?

Expand Down
13 changes: 4 additions & 9 deletions lib/erb/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,11 @@ def initialize(str)
end

class Scanner # :nodoc:
@scanner_map = defined?(Ractor) ? Ractor.make_shareable({}) : {}
@scanner_map = {}.freeze

class << self
if defined?(Ractor)
def register_scanner(klass, trim_mode, percent)
@scanner_map = Ractor.make_shareable({ **@scanner_map, [trim_mode, percent] => klass })
end
else
def register_scanner(klass, trim_mode, percent)
@scanner_map[[trim_mode, percent]] = klass
end
def register_scanner(klass, trim_mode, percent)
@scanner_map = @scanner_map.merge([trim_mode, percent].freeze => klass).freeze
end
alias :regist_scanner :register_scanner
end
Expand Down
2 changes: 1 addition & 1 deletion lib/erb/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true
class ERB
# The string \ERB version.
VERSION = '6.0.6'
VERSION = '6.0.7'
end
10 changes: 9 additions & 1 deletion ractor.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "internal/rational.h"
#include "internal/struct.h"
#include "internal/st.h"
#include "internal/string.h"
#include "internal/thread.h"
#include "variable.h"
#include "yjit.h"
Expand Down Expand Up @@ -2124,6 +2125,11 @@ move_leave(VALUE obj, struct obj_traverse_replace_data *data)
VALUE flags = T_OBJECT | FL_FREEZE | (RBASIC(obj)->flags & FL_PROMOTED);
shape_id_t shape_id = (RBASIC_SHAPE_ID(obj) & SHAPE_ID_CAPACITY_MASK) | ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_ROBJECT | SHAPE_ID_FL_FROZEN;

// A copy-on-write sharer reads its bytes straight out of an embedded root's slot
// (String#dup of a frozen string), and it outlives the move, so that body has to
// survive as it is.
bool wipe_body = !(RB_TYPE_P(obj, T_STRING) && rb_str_embedded_shared_root_p(obj));

// Avoid mutations using bind_call, etc.
size_t slot_size = rb_gc_obj_slot_size(obj);
MEMZERO((char *)obj, char, sizeof(struct RBasic));
Expand All @@ -2134,7 +2140,9 @@ move_leave(VALUE obj, struct obj_traverse_replace_data *data)
// but C code that held the object from before the move still reads it with its
// old type (an Array iteration in progress, the RMatch capa behind $~): a zeroed
// body makes those reads see an empty object instead of stale internals.
MEMZERO((char *)obj + sizeof(struct RBasic), char, slot_size - sizeof(struct RBasic));
if (wipe_body) {
MEMZERO((char *)obj + sizeof(struct RBasic), char, slot_size - sizeof(struct RBasic));
}

// The husk keeps its original (larger) slot, so give it a field-less shape
// sized to that slot; otherwise compaction's slot_size == shape_slot_size
Expand Down
8 changes: 8 additions & 0 deletions string.c
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ rb_str_reembeddable_p(VALUE str)
return !FL_TEST(str, STR_NOFREE|STR_SHARED_ROOT|STR_SHARED);
}

/* True when other strings read this string's bytes out of its own slot, so the slot
* contents must stay valid for as long as the object does. */
bool
rb_str_embedded_shared_root_p(VALUE str)
{
return STR_EMBED_P(str) && FL_TEST(str, STR_SHARED_ROOT);
}

static inline size_t
rb_str_embed_size(long capa, long termlen)
{
Expand Down
40 changes: 40 additions & 0 deletions test/erb/test_erb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -740,3 +740,43 @@ def teardown
ERB::Compiler::Scanner.instance_variable_set('@scanner_map', @save_map)
end
end

class TestERBRactor < Test::Unit::TestCase
def test_compile_and_result_in_ractor
assert_ractor(<<~RUBY, require: 'erb')
r = Ractor.new do
ERB.new("Hello, <%= 'world' %>!").result(binding)
end
assert_equal("Hello, world!", r.value)
RUBY
end

def test_trim_mode_in_ractor
assert_ractor(<<~RUBY, require: 'erb')
src = "<% [1, 2].each do |i| %>\\n<%= i %>\\n<% end %>\\n"
r = Ractor.new(src) { |s| ERB.new(s, trim_mode: '-').result(binding) }
assert_equal("\\n1\\n\\n2\\n\\n", r.value)

r = Ractor.new(src) { |s| ERB.new(s, trim_mode: '<>').result(binding) }
assert_equal("12", r.value)
RUBY
end

def test_frozen_erb_instance_reused_across_ractors
assert_ractor(<<~RUBY, require: 'erb')
erb = ERB.new("<%= 1 + 1 %>")
erb.freeze
rs = 2.times.map { Ractor.new(erb) { |e| e.result(binding) } }
assert_equal(["2", "2"], rs.map(&:value))
RUBY
end

def test_util_html_escape_in_ractor
assert_ractor(<<~RUBY, require: 'erb')
r = Ractor.new do
ERB::Util.html_escape("<script>")
end
assert_equal("&lt;script&gt;", r.value)
RUBY
end
end
18 changes: 18 additions & 0 deletions test/ruby/test_ractor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -491,4 +491,22 @@ def test_move_matchdata_kept_in_backref
r.value
RUBY
end

# String#dup of a frozen string shares the original's bytes, and for an embedded
# string those bytes live in its slot. Moving the original must leave that slot
# alone: the sharer reads it for as long as it lives.
def test_move_string_sharing_its_embedded_bytes
assert_ractor(<<~'RUBY', timeout: 60)
[24, 100, 300].each do |len|
r = Ractor.new { Ractor.receive }
str = "x" * len
str.instance_variable_set(:@iv, []) # unshareable, so it is moved
str.freeze
dup = str.dup # reads str's bytes in place
r.send(str, move: true)
assert_equal "x" * len, dup, "corrupted for length #{len}"
r.value
end
RUBY
end
end
9 changes: 7 additions & 2 deletions zjit/src/backend/lir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,10 @@ pub struct SideExitRecompile {
/// The compiled unit whose version must be invalidated to force a recompile. For inlined
/// methods, this will be the outer function it was inlined into.
pub compiled_iseq: Opnd,
/// The exiting frame's ISEQ, which owns the profile entry for `insn_idx`. For
/// an exit out of inlined code this is the inlined callee, not the compiled unit.
pub frame_iseq: Opnd,
/// The exiting frame's instruction index within `frame_iseq`.
pub insn_idx: u32,
}

Expand Down Expand Up @@ -2898,8 +2902,9 @@ impl Assembler
use crate::codegen::exit_recompile;
asm_comment!(asm, "profile and maybe recompile");
asm_ccall!(asm, exit_recompile,
EC,
recompile.compiled_iseq
recompile.compiled_iseq,
recompile.frame_iseq,
recompile.insn_idx.into()
);
}
}
Expand Down
17 changes: 15 additions & 2 deletions zjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3557,6 +3557,7 @@ fn side_exit_with_recompile(jit: &JITState, function: &Function, state: &FrameSt
let mut exit = build_side_exit(jit, function, state);
exit.recompile = recompile.map(|_| SideExitRecompile {
compiled_iseq: Opnd::Value(VALUE::from(jit.iseq())),
frame_iseq: Opnd::Value(VALUE::from(state.iseq)),
insn_idx: state.insn_idx() as u32,
});
Target::SideExit(Box::new(SideExitTarget { exit, reason }))
Expand Down Expand Up @@ -3623,7 +3624,14 @@ c_callable! {
/// of inlined code, the inliner folds the callee's body into the outer ISEQ, so
/// the outer ISEQ's version holds the failing guard and must be invalidated to
/// force a recompile. For non-inlined code, it is the same as the frame ISEQ.
pub(crate) fn exit_recompile(ec: EcPtr, compiled_iseq_raw: VALUE) {
///
/// `frame_iseq_raw` and `insn_idx` identify the instruction this exit came from,
/// whose re-profiling gates the recompile. Both are baked in at compile time,
/// where the exit already knows them, rather than read back out of the control
/// frame: the control frame describes the exiting frame only because the exit
/// wrote its ISEQ and PC there moments earlier, and an exit path that does not
/// write them would silently gate the recompile on an unrelated instruction.
pub(crate) fn exit_recompile(compiled_iseq_raw: VALUE, frame_iseq_raw: VALUE, insn_idx: u32) {
// Fast check before taking the VM lock: skip if the compiled unit is already
// invalidated or at the version limit. This avoids expensive lock acquisition
// on every shape guard exit after the recompile has already been triggered.
Expand All @@ -3643,7 +3651,8 @@ c_callable! {
let compiled_iseq: IseqPtr = compiled_iseq_raw.as_iseq();

let should_recompile = with_time_stat(Counter::profile_time_ns, || {
crate::profile::profile_recompile_insn(ec)
get_or_create_iseq_payload(frame_iseq_raw.as_iseq())
.profile.done_profiling_at(insn_idx as YarvInsnIdx)
});

// Once we have enough profiles, invalidate the compiled unit so it
Expand Down Expand Up @@ -3968,6 +3977,10 @@ pub fn gen_materialize_exit_trampoline(cb: &mut CodeBlock, exit_trampoline: Code

asm_comment!(asm, "clear JITFrame materialized by exit code");
asm.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_JIT_RETURN), 0.into());
// Clear cfp->block_code since it may have been left uninitialized by JITFrame mechanisms.
// Zero is the right value because we're dealing with the top most frame.
// Non-zero values are only set before pushing a frame.
asm.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_BLOCK_CODE), 0.into());

asm_comment!(asm, "materialize ZJIT frames");
asm_ccall!(asm, rb_zjit_materialize_frames, EC, CFP);
Expand Down
31 changes: 27 additions & 4 deletions zjit/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5521,7 +5521,7 @@ impl Function {
TDATA_OFFSET_FIELDS_OBJ
};

let fields_obj = self.load_field(block, self_val, FieldName::fields_obj, offset, types::RubyValue);
let fields_obj = self.load_field(block, self_val, FieldName::fields_obj, offset, types::IMemo);
// All fields objects are embedded
self.load_ivar_embedded(block, fields_obj, id, ivar_index)
},
Expand Down Expand Up @@ -5640,7 +5640,7 @@ impl Function {
(self_val, true)
},
ShapeLayout::Extended => {
let fields = self.load_field(block, self_val, FieldName::as_heap, ROBJECT_OFFSET_AS_HEAP_FIELDS, types::BasicObject);
let fields = self.load_field(block, self_val, FieldName::as_heap, ROBJECT_OFFSET_AS_HEAP_FIELDS, types::IMemo);
(fields, false)
},
ShapeLayout::Other | ShapeLayout::RClass => {
Expand Down Expand Up @@ -6922,8 +6922,8 @@ impl Function {
| Insn::NewRange { low: left, high: right, .. }
| Insn::CheckMatch { target: left, pattern: right, .. }
| Insn::WriteBarrier { recv: left, val: right } => {
self.assert_subtype(insn_id, left, types::BasicObject)?;
self.assert_subtype(insn_id, right, types::BasicObject)
self.assert_subtype(insn_id, left, types::RubyValue)?;
self.assert_subtype(insn_id, right, types::RubyValue)
}
Insn::GetConstant { klass, allow_nil, .. } => {
self.assert_subtype(insn_id, klass, types::BasicObject)?;
Expand Down Expand Up @@ -7819,6 +7819,23 @@ impl ProfileOracle {
self.types.entry(*snapshot).or_default().extend(entries.iter().cloned());
}
}

/// Copy the profile entries recorded for the `src` Snapshot to the `dst` Snapshot, excluding
/// entries for `exclude` (chased through guards). Used by polymorphic dispatch, where each
/// refined arm gets a fresh Snapshot: the receiver must resolve from its refined type rather
/// than the polymorphic profile, but the other operands' profiles should remain visible so
/// argument-profile-dependent specializations (e.g. Array#[]) still apply.
fn copy_entries_except(&mut self, src: InsnId, dst: InsnId, exclude: InsnId, fun: &Function) {
let Some(entries) = self.types.get(&src) else { return };
let exclude = fun.chase_insn(exclude);
let filtered: Vec<_> = entries.iter()
.filter(|(insn, _)| fun.chase_insn(*insn) != exclude)
.cloned()
.collect();
if !filtered.is_empty() {
self.types.insert(dst, filtered);
}
}
}

fn invalidates_locals(opcode: u32, operands: *const VALUE) -> bool {
Expand Down Expand Up @@ -9289,6 +9306,12 @@ fn add_iseq_to_hir(
// its refined, exact type instead of the polymorphic profile that is
// keyed at exit_id.
let snapshot = fun.push_insn(iftrue_block, Insn::Snapshot { state: Box::new(exit_state.clone()) });
// Keep the other operands' profile entries visible at the fresh
// Snapshot so the specialized send can still see argument profiles
// (e.g. Array#[] needs a Fixnum-profiled index to be inlined). Only
// the receiver's entry is dropped: it must resolve from its refined,
// exact type, and resolve_receiver_type prefers profiles over types.
profiles.copy_entries_except(exit_id, snapshot, recv, fun);
let refined_recv = fun.push_insn(iftrue_block, Insn::RefineType { val: recv, new_type: expected });
let send = fun.push_insn(iftrue_block, Insn::Send { recv: refined_recv, cd, block: None, args: args.clone(), state: snapshot, reason: Uncategorized(opcode) });
fun.push_insn(iftrue_block, Insn::Jump(BranchEdge { target: join_block, args: vec![send] }));
Expand Down
Loading