From 3c100019a4a1b0ef66db76ec3b14f63eac51dd4d Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 30 Jan 2026 09:41:27 +0100 Subject: [PATCH 1/5] st.c: Use ruby_sized_xfree Followup: https://github.com/ruby/ruby/pull/15989 This feeds the GC with useful allocation metrics, but also ensure we're correctly keeping track of our allocation sizes which is beneficial for safety. It also allows to use C23's `free_sized`. --- depend | 34 +++++++++++++++++++ st.c | 103 ++++++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 106 insertions(+), 31 deletions(-) diff --git a/depend b/depend index 7372902666b97b..2be15478970953 100644 --- a/depend +++ b/depend @@ -16298,16 +16298,28 @@ sprintf.$(OBJEXT): {$(VPATH)}st.h sprintf.$(OBJEXT): {$(VPATH)}subst.h sprintf.$(OBJEXT): {$(VPATH)}util.h sprintf.$(OBJEXT): {$(VPATH)}vsnprintf.c +st.$(OBJEXT): $(CCAN_DIR)/check_type/check_type.h +st.$(OBJEXT): $(CCAN_DIR)/container_of/container_of.h +st.$(OBJEXT): $(CCAN_DIR)/list/list.h +st.$(OBJEXT): $(CCAN_DIR)/str/str.h st.$(OBJEXT): $(hdrdir)/ruby/ruby.h +st.$(OBJEXT): $(top_srcdir)/internal/array.h +st.$(OBJEXT): $(top_srcdir)/internal/basic_operators.h st.$(OBJEXT): $(top_srcdir)/internal/bits.h +st.$(OBJEXT): $(top_srcdir)/internal/box.h st.$(OBJEXT): $(top_srcdir)/internal/compilers.h +st.$(OBJEXT): $(top_srcdir)/internal/gc.h st.$(OBJEXT): $(top_srcdir)/internal/hash.h +st.$(OBJEXT): $(top_srcdir)/internal/imemo.h st.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h +st.$(OBJEXT): $(top_srcdir)/internal/serial.h st.$(OBJEXT): $(top_srcdir)/internal/set_table.h st.$(OBJEXT): $(top_srcdir)/internal/st.h st.$(OBJEXT): $(top_srcdir)/internal/static_assert.h +st.$(OBJEXT): $(top_srcdir)/internal/vm.h st.$(OBJEXT): $(top_srcdir)/internal/warnings.h st.$(OBJEXT): {$(VPATH)}assert.h +st.$(OBJEXT): {$(VPATH)}atomic.h st.$(OBJEXT): {$(VPATH)}backward/2/assume.h st.$(OBJEXT): {$(VPATH)}backward/2/attributes.h st.$(OBJEXT): {$(VPATH)}backward/2/bool.h @@ -16319,6 +16331,9 @@ st.$(OBJEXT): {$(VPATH)}backward/2/stdalign.h st.$(OBJEXT): {$(VPATH)}backward/2/stdarg.h st.$(OBJEXT): {$(VPATH)}config.h st.$(OBJEXT): {$(VPATH)}defines.h +st.$(OBJEXT): {$(VPATH)}encoding.h +st.$(OBJEXT): {$(VPATH)}id.h +st.$(OBJEXT): {$(VPATH)}id_table.h st.$(OBJEXT): {$(VPATH)}intern.h st.$(OBJEXT): {$(VPATH)}internal.h st.$(OBJEXT): {$(VPATH)}internal/abi.h @@ -16392,6 +16407,15 @@ st.$(OBJEXT): {$(VPATH)}internal/core/rtypeddata.h st.$(OBJEXT): {$(VPATH)}internal/ctype.h st.$(OBJEXT): {$(VPATH)}internal/dllexport.h st.$(OBJEXT): {$(VPATH)}internal/dosish.h +st.$(OBJEXT): {$(VPATH)}internal/encoding/coderange.h +st.$(OBJEXT): {$(VPATH)}internal/encoding/ctype.h +st.$(OBJEXT): {$(VPATH)}internal/encoding/encoding.h +st.$(OBJEXT): {$(VPATH)}internal/encoding/pathname.h +st.$(OBJEXT): {$(VPATH)}internal/encoding/re.h +st.$(OBJEXT): {$(VPATH)}internal/encoding/sprintf.h +st.$(OBJEXT): {$(VPATH)}internal/encoding/string.h +st.$(OBJEXT): {$(VPATH)}internal/encoding/symbol.h +st.$(OBJEXT): {$(VPATH)}internal/encoding/transcode.h st.$(OBJEXT): {$(VPATH)}internal/error.h st.$(OBJEXT): {$(VPATH)}internal/eval.h st.$(OBJEXT): {$(VPATH)}internal/event.h @@ -16463,11 +16487,21 @@ st.$(OBJEXT): {$(VPATH)}internal/value_type.h st.$(OBJEXT): {$(VPATH)}internal/variable.h st.$(OBJEXT): {$(VPATH)}internal/warning_push.h st.$(OBJEXT): {$(VPATH)}internal/xmalloc.h +st.$(OBJEXT): {$(VPATH)}method.h st.$(OBJEXT): {$(VPATH)}missing.h +st.$(OBJEXT): {$(VPATH)}node.h +st.$(OBJEXT): {$(VPATH)}onigmo.h +st.$(OBJEXT): {$(VPATH)}oniguruma.h st.$(OBJEXT): {$(VPATH)}ruby_assert.h +st.$(OBJEXT): {$(VPATH)}ruby_atomic.h +st.$(OBJEXT): {$(VPATH)}rubyparser.h st.$(OBJEXT): {$(VPATH)}st.c st.$(OBJEXT): {$(VPATH)}st.h st.$(OBJEXT): {$(VPATH)}subst.h +st.$(OBJEXT): {$(VPATH)}thread_$(THREAD_MODEL).h +st.$(OBJEXT): {$(VPATH)}thread_native.h +st.$(OBJEXT): {$(VPATH)}vm_core.h +st.$(OBJEXT): {$(VPATH)}vm_opts.h strftime.$(OBJEXT): $(hdrdir)/ruby/ruby.h strftime.$(OBJEXT): $(top_srcdir)/internal/compilers.h strftime.$(OBJEXT): $(top_srcdir)/internal/encoding.h diff --git a/st.c b/st.c index 8937f7935f6b22..7891947549aa31 100644 --- a/st.c +++ b/st.c @@ -107,6 +107,7 @@ #elif defined RUBY_EXPORT #include "internal.h" #include "internal/bits.h" +#include "internal/gc.h" #include "internal/hash.h" #include "internal/sanitizers.h" #include "internal/set_table.h" @@ -173,7 +174,14 @@ static const struct st_hash_type type_strcasehash = { #define malloc ruby_xmalloc #define calloc ruby_xcalloc #define realloc ruby_xrealloc +#define sized_realloc ruby_sized_xrealloc #define free ruby_xfree +#define sized_free ruby_sized_xfree +#define free_fixed_ptr(v) ruby_sized_xfree((v), sizeof(*(v))) +#else +#define sized_realloc(ptr, new_size, old_size) realloc(ptr, new_size) +#define sized_free(v, s) free(v) +#define free_fixed_ptr(v) free(v) #endif #define EQUAL(tab,x,y) ((x) == (y) || (*(tab)->type->compare)((x),(y)) == 0) @@ -551,7 +559,7 @@ st_init_existing_table_with_size(st_table *tab, const struct st_hash_type *type, tab->bins = (st_index_t *) malloc(bins_size(tab)); #ifndef RUBY if (tab->bins == NULL) { - free(tab); + free_fixed_ptr(tab); return NULL; } #endif @@ -585,7 +593,7 @@ st_init_table_with_size(const struct st_hash_type *type, st_index_t size) st_init_existing_table_with_size(tab, type, size); #else if (st_init_existing_table_with_size(tab, type, size) == NULL) { - free(tab); + free_fixed_ptr(tab); return NULL; } #endif @@ -661,13 +669,36 @@ st_clear(st_table *tab) tab->rebuilds_num++; } +static inline size_t +st_entries_memsize(const st_table *tab) +{ + return get_allocated_entries(tab) * sizeof(st_table_entry); +} + +static inline size_t +st_bins_memsize(const st_table *tab) +{ + return tab->bins == NULL ? 0 : bins_size(tab); +} + +static inline void +st_free_entries(const st_table *tab) +{ + sized_free(tab->entries, st_entries_memsize(tab)); +} + +static inline void +st_free_bins(const st_table *tab) +{ + sized_free(tab->bins, st_bins_memsize(tab)); +} /* Free table TAB space. */ void st_free_table(st_table *tab) { - free(tab->bins); - free(tab->entries); - free(tab); + st_free_bins(tab); + st_free_entries(tab); + free_fixed_ptr(tab); } /* Return byte size of memory allocated for table TAB. */ @@ -676,8 +707,8 @@ st_memsize(const st_table *tab) { RUBY_ASSERT(tab != NULL); return(sizeof(st_table) - + (tab->bins == NULL ? 0 : bins_size(tab)) - + get_allocated_entries(tab) * sizeof(st_table_entry)); + + st_bins_memsize(tab) + + st_entries_memsize(tab)); } static st_index_t @@ -799,14 +830,15 @@ rebuild_table_with(st_table *const new_tab, st_table *const tab) static void rebuild_move_table(st_table *const new_tab, st_table *const tab) { + st_free_bins(tab); + st_free_entries(tab); + tab->entry_power = new_tab->entry_power; tab->bin_power = new_tab->bin_power; tab->size_ind = new_tab->size_ind; - free(tab->bins); tab->bins = new_tab->bins; - free(tab->entries); tab->entries = new_tab->entries; - free(new_tab); + free_fixed_ptr(new_tab); } static void @@ -2135,16 +2167,17 @@ st_expand_table(st_table *tab, st_index_t siz) tmp = st_init_table_with_size(tab->type, siz); n = get_allocated_entries(tab); MEMCPY(tmp->entries, tab->entries, st_table_entry, n); - free(tab->entries); - free(tab->bins); - free(tmp->bins); + st_free_bins(tab); + st_free_entries(tab); + st_free_bins(tmp); + tab->entry_power = tmp->entry_power; tab->bin_power = tmp->bin_power; tab->size_ind = tmp->size_ind; tab->entries = tmp->entries; tab->bins = NULL; tab->rebuilds_num++; - free(tmp); + free_fixed_ptr(tmp); } /* Rehash using linear search. Return TRUE if we found that the table @@ -2156,7 +2189,7 @@ st_rehash_linear(st_table *tab) st_index_t i, j; st_table_entry *p, *q; - free(tab->bins); + st_free_bins(tab); tab->bins = NULL; for (i = tab->entries_start; i < tab->entries_bound; i++) { @@ -2188,10 +2221,11 @@ st_rehash_indexed(st_table *tab) { int eq_p, rebuilt_p; st_index_t i; - st_index_t const n = bins_size(tab); + + if (!tab->bins) { + tab->bins = malloc(bins_size(tab)); + } unsigned int const size_ind = get_size_ind(tab); - st_index_t *bins = realloc(tab->bins, n); - tab->bins = bins; initialize_bins(tab); for (i = tab->entries_start; i < tab->entries_bound; i++) { st_table_entry *p = &tab->entries[i]; @@ -2207,10 +2241,10 @@ st_rehash_indexed(st_table *tab) ind = hash_bin(p->hash, tab); for (;;) { - st_index_t bin = get_bin(bins, size_ind, ind); + st_index_t bin = get_bin(tab->bins, size_ind, ind); if (EMPTY_OR_DELETED_BIN_P(bin)) { /* ok, new room */ - set_bin(bins, size_ind, ind, i + ENTRY_BASE); + set_bin(tab->bins, size_ind, ind, i + ENTRY_BASE); break; } else { @@ -2446,6 +2480,16 @@ set_make_tab_empty(set_table *tab) set_initialize_bins(tab); } +static inline size_t +set_entries_memsize(set_table *tab) +{ + size_t memsize = set_get_allocated_entries(tab) * sizeof(set_table_entry); + if (set_has_bins(tab)) { + memsize += set_bins_size(tab); + } + return memsize; +} + static set_table * set_init_existing_table_with_size(set_table *tab, const struct st_hash_type *type, st_index_t size) { @@ -2471,12 +2515,7 @@ set_init_existing_table_with_size(set_table *tab, const struct st_hash_type *typ tab->bin_power = features[n].bin_power; tab->size_ind = features[n].size_ind; - size_t memsize = 0; - if (set_has_bins(tab)) { - memsize += set_bins_size(tab); - } - memsize += set_get_allocated_entries(tab) * sizeof(set_table_entry); - tab->entries = (set_table_entry *)malloc(memsize); + tab->entries = (set_table_entry *)malloc(set_entries_memsize(tab)); set_make_tab_empty(tab); tab->rebuilds_num = 0; return tab; @@ -2526,8 +2565,8 @@ set_table_clear(set_table *tab) void set_free_table(set_table *tab) { - free(tab->entries); - free(tab); + sized_free(tab->entries, set_entries_memsize(tab)); + free_fixed_ptr(tab); } /* Return byte size of memory allocated for table TAB. */ @@ -2625,12 +2664,14 @@ set_rebuild_table_with(set_table *const new_tab, set_table *const tab) static void set_rebuild_move_table(set_table *const new_tab, set_table *const tab) { + sized_free(tab->entries, set_entries_memsize(tab)); + tab->entries = new_tab->entries; + tab->entry_power = new_tab->entry_power; tab->bin_power = new_tab->bin_power; tab->size_ind = new_tab->size_ind; - free(tab->entries); - tab->entries = new_tab->entries; - free(new_tab); + + free_fixed_ptr(new_tab); } static void From 9be01bc70dca0e727fe1f518ebae1f6f72405b84 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 30 Jan 2026 13:09:10 +0100 Subject: [PATCH 2/5] gc.c: also verify sized_xrealloc old size --- gc/default/default.c | 7 ++++++- parse.y | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index aaf6f56092b360..a6572b6f4d7ceb 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -8270,7 +8270,7 @@ rb_gc_impl_free(void *objspace_ptr, void *ptr, size_t old_size) struct malloc_obj_info *info = (struct malloc_obj_info *)ptr - 1; #if VERIFY_FREE_SIZE if (old_size && (old_size + sizeof(struct malloc_obj_info)) != info->size) { - rb_bug("buffer %p freed with size %lu, but was allocated with size %lu", ptr, old_size, info->size - sizeof(struct malloc_obj_info)); + rb_bug("buffer %p freed with old_size=%lu, but was allocated with size=%lu", ptr, old_size, info->size - sizeof(struct malloc_obj_info)); } #endif ptr = info; @@ -8379,6 +8379,11 @@ rb_gc_impl_realloc(void *objspace_ptr, void *ptr, size_t new_size, size_t old_si struct malloc_obj_info *info = (struct malloc_obj_info *)ptr - 1; new_size += sizeof(struct malloc_obj_info); ptr = info; +#if VERIFY_FREE_SIZE + if (old_size && (old_size + sizeof(struct malloc_obj_info)) != info->size) { + rb_bug("buffer %p realloced with old_size=%lu, but was allocated with size=%lu", ptr, old_size, info->size - sizeof(struct malloc_obj_info)); + } +#endif old_size = info->size; } #endif diff --git a/parse.y b/parse.y index 03dd1c6f926067..7ca1197b37339a 100644 --- a/parse.y +++ b/parse.y @@ -1995,7 +1995,7 @@ parser_memhash(const void *ptr, long len) #define STRING_TERM_LEN(str) (1) #define STRING_TERM_FILL(str) (str->ptr[str->len] = '\0') #define PARSER_STRING_RESIZE_CAPA_TERM(p,str,capacity,termlen) do {\ - SIZED_REALLOC_N(str->ptr, char, (size_t)total + termlen, STRING_SIZE(str)); \ + REALLOC_N(str->ptr, char, (size_t)total + termlen); \ str->len = total; \ } while (0) #define STRING_SET_LEN(str, n) do { \ From 1298f9ac1ad390594815bc4d3739eb312bca8887 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 30 Jan 2026 12:29:15 -0500 Subject: [PATCH 3/5] ZJIT: Support CFunc inlining in InvokeSuper (#16004) Also generally make the CFunc process look more like `optimize_c_calls`. --- zjit/src/codegen.rs | 20 +++++ zjit/src/hir.rs | 179 +++++++++++++++++++++++++------------- zjit/src/hir/opt_tests.rs | 51 +++++++++-- 3 files changed, 182 insertions(+), 68 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 2038be808dc633..a3068ff23dfea4 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -2330,6 +2330,26 @@ fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, guard let tag = asm.and(flags, Opnd::UImm(RUBY_T_MASK as u64)); asm.cmp(tag, Opnd::UImm(RUBY_T_STRING as u64)); asm.jne(side); + } else if guard_type.is_subtype(types::Array) { + let side = side_exit(jit, state, GuardType(guard_type)); + + // Check special constant + asm.test(val, Opnd::UImm(RUBY_IMMEDIATE_MASK as u64)); + asm.jnz(side.clone()); + + // Check false + asm.cmp(val, Qfalse.into()); + asm.je(side.clone()); + + let val = match val { + Opnd::Reg(_) | Opnd::VReg { .. } => val, + _ => asm.load(val), + }; + + let flags = asm.load(Opnd::mem(VALUE_BITS, val, RUBY_OFFSET_RBASIC_FLAGS)); + let tag = asm.and(flags, Opnd::UImm(RUBY_T_MASK as u64)); + asm.cmp(tag, Opnd::UImm(RUBY_T_ARRAY as u64)); + asm.jne(side); } else if guard_type.bit_equal(types::HeapBasicObject) { let side_exit = side_exit(jit, state, GuardType(guard_type)); asm.cmp(val, Opnd::Value(Qfalse)); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 901beffea02772..9aa70b5d34d12e 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -3564,13 +3564,7 @@ impl Function { assert!(flags & VM_CALL_FCALL != 0); // Reject calls with complex argument handling. - let complex_arg_types = VM_CALL_ARGS_SPLAT - | VM_CALL_KW_SPLAT - | VM_CALL_KWARG - | VM_CALL_ARGS_BLOCKARG - | VM_CALL_FORWARDING; - - if (flags & complex_arg_types) != 0 { + if unspecializable_c_call_type(flags) { self.push_insn_id(block, insn_id); self.set_dynamic_send_reason(insn_id, SuperComplexArgsPass); continue; @@ -3608,14 +3602,18 @@ impl Function { } // Look up the super method. - let super_cme = unsafe { rb_callable_method_entry(superclass, mid) }; + let mut super_cme = unsafe { rb_callable_method_entry(superclass, mid) }; if super_cme.is_null() { self.push_insn_id(block, insn_id); self.set_dynamic_send_reason(insn_id, SuperTargetNotFound); continue; } - let def_type = unsafe { get_cme_def_type(super_cme) }; + let mut def_type = unsafe { get_cme_def_type(super_cme) }; + while def_type == VM_METHOD_TYPE_ALIAS { + super_cme = unsafe { rb_aliased_callable_method_entry(super_cme) }; + def_type = unsafe { get_cme_def_type(super_cme) }; + } if def_type == VM_METHOD_TYPE_ISEQ { // Check if the super method's parameters support direct send. @@ -3653,6 +3651,12 @@ impl Function { let cfunc_argc = unsafe { get_mct_argc(cfunc) }; let cfunc_ptr = unsafe { get_mct_func(cfunc) }.cast(); + let props = ZJITState::get_method_annotations().get_cfunc_properties(super_cme); + if props.is_none() && get_option!(stats) { + self.count_not_annotated_cfunc(block, super_cme); + } + let props = props.unwrap_or_default(); + match cfunc_argc { // C function with fixed argument count. 0.. => { @@ -3665,20 +3669,48 @@ impl Function { emit_super_call_guards(self, block, super_cme, current_cme, mid, state); + // Try inlining the cfunc into HIR + let tmp_block = self.new_block(u32::MAX); + if let Some(replacement) = (props.inline)(self, tmp_block, recv, &args, state) { + // Copy contents of tmp_block to block + assert_ne!(block, tmp_block); + let insns = std::mem::take(&mut self.blocks[tmp_block.0].insns); + self.blocks[block.0].insns.extend(insns); + self.push_insn(block, Insn::IncrCounter(Counter::inline_cfunc_optimized_send_count)); + self.make_equal_to(insn_id, replacement); + if self.type_of(replacement).bit_equal(types::Any) { + // Not set yet; infer type + self.insn_types[replacement.0] = self.infer_type(replacement); + } + self.remove_block(tmp_block); + continue; + } + // Use CCallWithFrame for the C function. let name = rust_str_to_id(&qualified_method_name(unsafe { (*super_cme).owner }, unsafe { (*super_cme).called_id })); - let ccall = self.push_insn(block, Insn::CCallWithFrame { - cd, - cfunc: cfunc_ptr, - recv, - args: args.clone(), - cme: super_cme, - name, - state, - return_type: types::BasicObject, - elidable: false, - blockiseq: None, - }); + let return_type = props.return_type; + let elidable = props.elidable; + // Filter for a leaf and GC free function + let ccall = if props.leaf && props.no_gc { + self.push_insn(block, Insn::IncrCounter(Counter::inline_cfunc_optimized_send_count)); + self.push_insn(block, Insn::CCall { cfunc: cfunc_ptr, recv, args, name, return_type, elidable }) + } else { + if get_option!(stats) { + self.count_not_inlined_cfunc(block, super_cme); + } + self.push_insn(block, Insn::CCallWithFrame { + cd, + cfunc: cfunc_ptr, + recv, + args: args.clone(), + cme: super_cme, + name, + state, + return_type: types::BasicObject, + elidable: false, + blockiseq: None, + }) + }; self.make_equal_to(insn_id, ccall); } @@ -3686,19 +3718,48 @@ impl Function { -1 => { emit_super_call_guards(self, block, super_cme, current_cme, mid, state); + // Try inlining the cfunc into HIR + let tmp_block = self.new_block(u32::MAX); + if let Some(replacement) = (props.inline)(self, tmp_block, recv, &args, state) { + // Copy contents of tmp_block to block + assert_ne!(block, tmp_block); + emit_super_call_guards(self, block, super_cme, current_cme, mid, state); + let insns = std::mem::take(&mut self.blocks[tmp_block.0].insns); + self.blocks[block.0].insns.extend(insns); + self.push_insn(block, Insn::IncrCounter(Counter::inline_cfunc_optimized_send_count)); + self.make_equal_to(insn_id, replacement); + if self.type_of(replacement).bit_equal(types::Any) { + // Not set yet; infer type + self.insn_types[replacement.0] = self.infer_type(replacement); + } + self.remove_block(tmp_block); + continue; + } + // Use CCallVariadic for the variadic C function. let name = rust_str_to_id(&qualified_method_name(unsafe { (*super_cme).owner }, unsafe { (*super_cme).called_id })); - let ccall = self.push_insn(block, Insn::CCallVariadic { - cfunc: cfunc_ptr, - recv, - args: args.clone(), - cme: super_cme, - name, - state, - return_type: types::BasicObject, - elidable: false, - blockiseq: None, - }); + let return_type = props.return_type; + let elidable = props.elidable; + // Filter for a leaf and GC free function + let ccall = if props.leaf && props.no_gc { + self.push_insn(block, Insn::IncrCounter(Counter::inline_cfunc_optimized_send_count)); + self.push_insn(block, Insn::CCall { cfunc: cfunc_ptr, recv, args, name, return_type, elidable }) + } else { + if get_option!(stats) { + self.count_not_inlined_cfunc(block, super_cme); + } + self.push_insn(block, Insn::CCallVariadic { + cfunc: cfunc_ptr, + recv, + args: args.clone(), + cme: super_cme, + name, + state, + return_type: types::BasicObject, + elidable: false, + blockiseq: None, + }) + }; self.make_equal_to(insn_id, ccall); } @@ -3981,6 +4042,28 @@ impl Function { self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass: recv_class, method: method_id, cme }, state }); } + fn count_not_inlined_cfunc(&mut self, block: BlockId, cme: *const rb_callable_method_entry_t) { + let owner = unsafe { (*cme).owner }; + let called_id = unsafe { (*cme).called_id }; + let qualified_method_name = qualified_method_name(owner, called_id); + let not_inlined_cfunc_counter_pointers = ZJITState::get_not_inlined_cfunc_counter_pointers(); + let counter_ptr = not_inlined_cfunc_counter_pointers.entry(qualified_method_name.clone()).or_insert_with(|| Box::new(0)); + let counter_ptr = &mut **counter_ptr as *mut u64; + + self.push_insn(block, Insn::IncrCounterPtr { counter_ptr }); + } + + fn count_not_annotated_cfunc(&mut self, block: BlockId, cme: *const rb_callable_method_entry_t) { + let owner = unsafe { (*cme).owner }; + let called_id = unsafe { (*cme).called_id }; + let qualified_method_name = qualified_method_name(owner, called_id); + let not_annotated_cfunc_counter_pointers = ZJITState::get_not_annotated_cfunc_counter_pointers(); + let counter_ptr = not_annotated_cfunc_counter_pointers.entry(qualified_method_name.clone()).or_insert_with(|| Box::new(0)); + let counter_ptr = &mut **counter_ptr as *mut u64; + + self.push_insn(block, Insn::IncrCounterPtr { counter_ptr }); + } + /// Optimize Send/SendWithoutBlock that land in a C method to a direct CCall without /// runtime lookup. fn optimize_c_calls(&mut self) { @@ -4124,7 +4207,7 @@ impl Function { } if get_option!(stats) { - count_not_inlined_cfunc(fun, block, cme); + fun.count_not_inlined_cfunc(block, cme); } let ccall = fun.push_insn(block, Insn::CCallVariadic { @@ -4238,7 +4321,7 @@ impl Function { let props = ZJITState::get_method_annotations().get_cfunc_properties(cme); if props.is_none() && get_option!(stats) { - count_not_annotated_cfunc(fun, block, cme); + fun.count_not_annotated_cfunc(block, cme); } let props = props.unwrap_or_default(); @@ -4277,7 +4360,7 @@ impl Function { fun.make_equal_to(send_insn_id, ccall); } else { if get_option!(stats) { - count_not_inlined_cfunc(fun, block, cme); + fun.count_not_inlined_cfunc(block, cme); } let ccall = fun.push_insn(block, Insn::CCallWithFrame { cd, @@ -4326,7 +4409,7 @@ impl Function { let cfunc = unsafe { get_mct_func(cfunc) }.cast(); let props = ZJITState::get_method_annotations().get_cfunc_properties(cme); if props.is_none() && get_option!(stats) { - count_not_annotated_cfunc(fun, block, cme); + fun.count_not_annotated_cfunc(block, cme); } let props = props.unwrap_or_default(); @@ -4349,7 +4432,7 @@ impl Function { // No inlining; emit a call if get_option!(stats) { - count_not_inlined_cfunc(fun, block, cme); + fun.count_not_inlined_cfunc(block, cme); } let return_type = props.return_type; let elidable = props.elidable; @@ -4383,28 +4466,6 @@ impl Function { Err(()) } - fn count_not_inlined_cfunc(fun: &mut Function, block: BlockId, cme: *const rb_callable_method_entry_t) { - let owner = unsafe { (*cme).owner }; - let called_id = unsafe { (*cme).called_id }; - let qualified_method_name = qualified_method_name(owner, called_id); - let not_inlined_cfunc_counter_pointers = ZJITState::get_not_inlined_cfunc_counter_pointers(); - let counter_ptr = not_inlined_cfunc_counter_pointers.entry(qualified_method_name.clone()).or_insert_with(|| Box::new(0)); - let counter_ptr = &mut **counter_ptr as *mut u64; - - fun.push_insn(block, Insn::IncrCounterPtr { counter_ptr }); - } - - fn count_not_annotated_cfunc(fun: &mut Function, block: BlockId, cme: *const rb_callable_method_entry_t) { - let owner = unsafe { (*cme).owner }; - let called_id = unsafe { (*cme).called_id }; - let qualified_method_name = qualified_method_name(owner, called_id); - let not_annotated_cfunc_counter_pointers = ZJITState::get_not_annotated_cfunc_counter_pointers(); - let counter_ptr = not_annotated_cfunc_counter_pointers.entry(qualified_method_name.clone()).or_insert_with(|| Box::new(0)); - let counter_ptr = &mut **counter_ptr as *mut u64; - - fun.push_insn(block, Insn::IncrCounterPtr { counter_ptr }); - } - for block in self.rpo() { let old_insns = std::mem::take(&mut self.blocks[block.0].insns); assert!(self.blocks[block.0].insns.is_empty()); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index de4e2ec39db7a7..8dec65fed634e6 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -11408,21 +11408,20 @@ mod hir_opt_tests { #[test] fn test_invokesuper_to_cfunc_optimizes_to_ccall() { eval(" - class MyArray < Array - def length + class C < Hash + def size super end end - MyArray.new.length; MyArray.new.length + C.new.size "); - let hir = hir_string_proc("MyArray.new.method(:length)"); + let hir = hir_string_proc("C.new.method(:size)"); assert!(!hir.contains("InvokeSuper "), "Expected unoptimized InvokeSuper but got:\n{hir}"); - assert!(hir.contains("CCallWithFrame"), "Should optimize to CCallWithFrame for non-variadic cfunc:\n{hir}"); - assert_snapshot!(hir, @" - fn length@:4: + assert_snapshot!(hir, @r" + fn size@:4: bb0(): EntryPoint interpreter v1:BasicObject = LoadSelf @@ -11431,12 +11430,46 @@ mod hir_opt_tests { EntryPoint JIT(0) Jump bb2(v4) bb2(v6:BasicObject): - PatchPoint MethodRedefined(Array@0x1000, length@0x1008, cme:0x1010) + PatchPoint MethodRedefined(Hash@0x1000, size@0x1008, cme:0x1010) + v17:CPtr = GetLEP + GuardSuperMethodEntry v17, 0x1038 + v19:RubyValue = GetBlockHandler v17 + v20:FalseClass = GuardBitEquals v19, Value(false) + IncrCounter inline_cfunc_optimized_send_count + v22:Fixnum = CCall v6, :Hash#size@0x1040 + CheckInterrupts + Return v22 + "); + } + + #[test] + fn test_inline_invokesuper_to_basicobject_initialize() { + eval(" + class C + def initialize + super + end + end + + C.new + "); + assert_snapshot!(hir_string_proc("C.instance_method(:initialize)"), @r" + fn initialize@:4: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + PatchPoint MethodRedefined(BasicObject@0x1000, initialize@0x1008, cme:0x1010) v17:CPtr = GetLEP GuardSuperMethodEntry v17, 0x1038 v19:RubyValue = GetBlockHandler v17 v20:FalseClass = GuardBitEquals v19, Value(false) - v21:BasicObject = CCallWithFrame v6, :Array#length@0x1040 + v21:NilClass = Const Value(nil) + IncrCounter inline_cfunc_optimized_send_count CheckInterrupts Return v21 "); From d7553015cc63258919f28b41c642eca66d35f8bf Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Thu, 29 Jan 2026 20:39:23 +0100 Subject: [PATCH 4/5] [ruby/prism] Handle String-like objects for Ripper.sexp * RSpec relies on this in https://github.com/rspec/rspec/blob/rspec-support-v3.13.6/rspec-support/lib/rspec/support/source.rb#L57 which is given an RSpec::Support::EncodedString. * CI failure caused by this on truffleruby: https://github.com/sporkmonger/addressable/actions/runs/21457707372/job/61802608154#step:7:14 https://github.com/ruby/prism/commit/a83cb7b350 --- lib/prism/translation/ripper.rb | 12 +++++++++++- test/prism/ruby/ripper_test.rb | 30 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/lib/prism/translation/ripper.rb b/lib/prism/translation/ripper.rb index 054ad88ce3e8a3..5b2aa37833207e 100644 --- a/lib/prism/translation/ripper.rb +++ b/lib/prism/translation/ripper.rb @@ -480,7 +480,17 @@ def self.lex_state_name(state) # Create a new Translation::Ripper object with the given source. def initialize(source, filename = "(ripper)", lineno = 1) - @source = source + if source.is_a?(IO) + @source = source.read + elsif source.respond_to?(:gets) + @source = +"" + while line = source.gets + @source << line + end + else + @source = source.to_str + end + @filename = filename @lineno = lineno @column = 0 diff --git a/test/prism/ruby/ripper_test.rb b/test/prism/ruby/ripper_test.rb index a89a9503b98fd4..52a5ad7ef4e2f9 100644 --- a/test/prism/ruby/ripper_test.rb +++ b/test/prism/ruby/ripper_test.rb @@ -145,6 +145,36 @@ def test_tokenize assert_equal(Ripper.tokenize(source), Translation::Ripper.tokenize(source)) end + def test_sexp_coercion + string_like = Object.new + def string_like.to_str + "a" + end + assert_equal Ripper.sexp(string_like), Translation::Ripper.sexp(string_like) + + File.open(__FILE__) do |file1| + File.open(__FILE__) do |file2| + assert_equal Ripper.sexp(file1), Translation::Ripper.sexp(file2) + end + end + + File.open(__FILE__) do |file1| + File.open(__FILE__) do |file2| + object1_with_gets = Object.new + object1_with_gets.define_singleton_method(:gets) do + file1.gets + end + + object2_with_gets = Object.new + object2_with_gets.define_singleton_method(:gets) do + file2.gets + end + + assert_equal Ripper.sexp(object1_with_gets), Translation::Ripper.sexp(object2_with_gets) + end + end + end + # Check that the hardcoded values don't change without us noticing. def test_internals actual = Translation::Ripper.constants.select { |name| name.start_with?("EXPR_") }.sort From 737809bf25e1c8babd3c8280a5fac162354f906b Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 30 Jan 2026 17:05:11 -0500 Subject: [PATCH 5/5] [DOC] Fix link in ARGF --- io.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io.c b/io.c index 8563fa6536c02f..ca97e321f7179a 100644 --- a/io.c +++ b/io.c @@ -14956,7 +14956,7 @@ set_LAST_READ_LINE(VALUE val, ID _x, VALUE *_y) * * When no character '-' is given, stream $stdin is ignored * (exception: - * see {Specifying $stdin in ARGV}[rdoc-ref:ARGF@Specifying+-24stdin+in+ARGV]): + * see {Specifying $stdin in ARGV}[rdoc-ref:ARGF@Specifying+stdin+in+ARGV]): * * - Command and output: *