From 82928739c3045ae895675194b9c9f03566ce3e86 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Tue, 19 Aug 2025 10:33:46 -0400 Subject: [PATCH 01/14] ZJIT: Check for VM stack overflow Previously, the included test crashed or turned into an infinite loop due to the missing check. --- test/ruby/test_zjit.rb | 20 ++++++++++++++++++++ zjit/src/codegen.rs | 17 +++++++++++++++-- zjit/src/hir.rs | 1 + zjit/src/stats.rs | 2 ++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 2db45453b5fc50..87d8b06ece2a8d 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -2584,6 +2584,26 @@ def test(x) }, insns: [:opt_case_dispatch] end + def test_stack_overflow + assert_compiles 'nil', %q{ + def recurse(n) + return if n == 0 + recurse(n-1) + nil # no tail call + end + + recurse(2) + recurse(2) + begin + recurse(20_000) + rescue SystemStackError + # Not asserting an exception is raised here since main + # thread stack size is environment-sensitive. Only + # that we don't crash or infinite loop. + end + }, call_threshold: 2 + end + def test_invokeblock assert_compiles '42', %q{ def test diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 73f0ab5f85d79a..2a581d8baddac8 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -20,6 +20,7 @@ use crate::hir::{iseq_to_hir, Block, BlockId, BranchEdge, Invariant, RangeType, use crate::hir::{Const, FrameState, Function, Insn, InsnId}; use crate::hir_type::{types, Type}; use crate::options::get_option; +use crate::cast::IntoUsize; /// Ephemeral code generation state struct JITState { @@ -1042,6 +1043,19 @@ fn gen_send_without_block_direct( args: Vec, state: &FrameState, ) -> lir::Opnd { + let local_size = unsafe { get_iseq_body_local_table_size(iseq) }.as_usize(); + // Stack overflow check: fails if CFP<=SP at any point in the callee. + asm_comment!(asm, "stack overflow check"); + let stack_growth = state.stack_size() + local_size + unsafe { get_iseq_body_stack_max(iseq) }.as_usize(); + // vm_push_frame() checks it against a decremented cfp, and CHECK_VM_STACK_OVERFLOW0 + // adds to the margin another control frame with `&bounds[1]`. + const { assert!(RUBY_SIZEOF_CONTROL_FRAME % SIZEOF_VALUE == 0, "sizeof(rb_control_frame_t) is a multiple of sizeof(VALUE)"); } + let cfp_growth = 2 * (RUBY_SIZEOF_CONTROL_FRAME / SIZEOF_VALUE); + let peak_offset = SIZEOF_VALUE * (stack_growth + cfp_growth); + let stack_limit = asm.add(SP, peak_offset.into()); + asm.cmp(CFP, stack_limit); + asm.jbe(side_exit(jit, state, StackOverflow)); + // Save cfp->pc and cfp->sp for the caller frame gen_prepare_call_with_gc(asm, state); gen_save_sp(asm, state.stack().len() - args.len() - 1); // -1 for receiver @@ -1059,8 +1073,7 @@ fn gen_send_without_block_direct( }); asm_comment!(asm, "switch to new SP register"); - let local_size = unsafe { get_iseq_body_local_table_size(iseq) } as usize; - let sp_offset = (state.stack().len() + local_size - args.len() + VM_ENV_DATA_SIZE as usize) * SIZEOF_VALUE; + let sp_offset = (state.stack().len() + local_size - args.len() + VM_ENV_DATA_SIZE.as_usize()) * SIZEOF_VALUE; let new_sp = asm.add(SP, sp_offset.into()); asm.mov(SP, new_sp); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 932164b550d450..4ade541922641f 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -464,6 +464,7 @@ pub enum SideExitReason { Interrupt, BlockParamProxyModified, BlockParamProxyNotIseqOrIfunc, + StackOverflow, } impl std::fmt::Display for SideExitReason { diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 471704bc06a38e..d1a6d584b9c020 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -102,6 +102,7 @@ make_counters! { exit_callee_side_exit, exit_obj_to_string_fallback, exit_interrupt, + exit_stackoverflow, exit_optional_arguments, exit_block_param_proxy_modified, exit_block_param_proxy_not_iseq_or_ifunc, @@ -232,6 +233,7 @@ pub fn exit_counter_ptr(reason: crate::hir::SideExitReason) -> *mut u64 { CalleeSideExit => exit_callee_side_exit, ObjToStringFallback => exit_obj_to_string_fallback, Interrupt => exit_interrupt, + StackOverflow => exit_stackoverflow, BlockParamProxyModified => exit_block_param_proxy_modified, BlockParamProxyNotIseqOrIfunc => exit_block_param_proxy_not_iseq_or_ifunc, }; From 0b264a25c6959bfa2bffe3484e5921fa5fd72088 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 11 Sep 2025 23:24:28 +0900 Subject: [PATCH 02/14] ABI version is defined only in master --- .github/actions/capiext/action.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/actions/capiext/action.yml b/.github/actions/capiext/action.yml index e8ea87e61019d7..a43d0890ef1255 100644 --- a/.github/actions/capiext/action.yml +++ b/.github/actions/capiext/action.yml @@ -26,7 +26,10 @@ runs: run: | eval $(grep -e '^arch *=' -e '^ruby_version *=' -e '^DLEXT *=' Makefile | sed 's/ *= */=/') - key=capiexts-${arch}-${ruby_version} + case "${ruby_version}" in + *+*) key=capiexts-${arch}-${ruby_version};; + *) key=;; + esac echo key=$key >> $GITHUB_OUTPUT echo DLEXT=$DLEXT >> $GITHUB_OUTPUT working-directory: ${{ inputs.builddir }} @@ -37,6 +40,7 @@ runs: with: path: ${{ inputs.builddir }}/spec/ruby/optional/capi/ext/ key: ${{ steps.config.outputs.key }} + if: ${{ steps.config.outputs.key }} - name: Run test-spec with previous CAPI extension binaries shell: bash From 0dbf3c080b62ad83dbb2db78e442c24ab856454b Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 12 Sep 2025 11:02:30 +0900 Subject: [PATCH 03/14] Show annotation --- .github/actions/capiext/action.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/actions/capiext/action.yml b/.github/actions/capiext/action.yml index a43d0890ef1255..a41202c7a4053a 100644 --- a/.github/actions/capiext/action.yml +++ b/.github/actions/capiext/action.yml @@ -30,6 +30,7 @@ runs: *+*) key=capiexts-${arch}-${ruby_version};; *) key=;; esac + echo version=$ruby_version >> $GITHUB_OUTPUT echo key=$key >> $GITHUB_OUTPUT echo DLEXT=$DLEXT >> $GITHUB_OUTPUT working-directory: ${{ inputs.builddir }} @@ -43,6 +44,7 @@ runs: if: ${{ steps.config.outputs.key }} - name: Run test-spec with previous CAPI extension binaries + id: check shell: bash run: | touch spec/ruby/optional/capi/ext/*.$DLEXT @@ -52,3 +54,10 @@ runs: DLEXT: ${{ steps.config.outputs.DLEXT }} working-directory: ${{ inputs.builddir }} if: ${{ steps.cache.outputs.cache-hit }} + + - shell: bash + run: | + echo "::error::Change from ${prev} detected; bump up ABI version" + env: + prev: ${{ steps.config.outputs.version }} + if: ${{ always() && steps.check.outcome == 'failure' }} From ca70f442b8a49cd8fe6b62abbf067cd953d29664 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 12 Sep 2025 11:28:59 +0900 Subject: [PATCH 04/14] Matrix for extra checks --- .github/workflows/macos.yml | 3 ++- .github/workflows/ubuntu.yml | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index d22ada675545bb..0b9d5d049d635e 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -45,6 +45,7 @@ jobs: os: macos-14 - test_task: check os: macos-15 + extra_checks: [capi] capi_check: capi - test_task: check os: macos-13 @@ -166,7 +167,7 @@ jobs: builddir: build env: RUBY_TESTOPTS: '-v --tty=no' - if: ${{ matrix.capi_check }} + if: ${{ contains(matrix.extra_checks, 'capi') }} - uses: ./.github/actions/slack with: diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 5c8a072a16742c..6249418a9abafb 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -39,7 +39,7 @@ jobs: - test_task: test-bundled-gems - test_task: check os: ubuntu-24.04 - capi_check: capi + extra_checks: [capi] # ubuntu-24.04-arm jobs don't start on ruby/ruby as of 2025-09-04 #- test_task: check # os: ubuntu-24.04-arm @@ -160,7 +160,7 @@ jobs: make: '$SETARCH make' env: RUBY_TESTOPTS: '-v --tty=no' - if: ${{ matrix.capi_check }} + if: ${{ contains(matrix.extra_checks, 'capi') }} - uses: ./.github/actions/slack with: From 4131ace07ab604a39acd56de3597ea3e1f785c4e Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 11 Sep 2025 21:50:27 -0700 Subject: [PATCH 05/14] ZJIT, YJIT: Drop "// From xxx.h" comments in bindgen (#14519) --- yjit/bindgen/src/main.rs | 99 -------------------------------------- zjit/bindgen/src/main.rs | 100 --------------------------------------- 2 files changed, 199 deletions(-) diff --git a/yjit/bindgen/src/main.rs b/yjit/bindgen/src/main.rs index 2fc85431e0439d..b62c637e1dbc4e 100644 --- a/yjit/bindgen/src/main.rs +++ b/yjit/bindgen/src/main.rs @@ -65,35 +65,24 @@ fn main() { // Import YARV bytecode instruction constants .allowlist_type("ruby_vminsn_type") - // From include/ruby/internal/special_consts.h .allowlist_type("ruby_special_consts") - - // From include/ruby/internal/intern/string.h .allowlist_function("rb_utf8_str_new") .allowlist_function("rb_str_buf_append") .allowlist_function("rb_str_dup") - - // From encindex.h .allowlist_type("ruby_preserved_encindex") - - // From include/ruby/ruby.h .allowlist_function("rb_class2name") // This struct is public to Ruby C extensions - // From include/ruby/internal/core/rbasic.h .allowlist_type("RBasic") - // From include/ruby/internal/core/rstring.h .allowlist_type("ruby_rstring_flags") - // From internal.h // This function prints info about a value and is useful for debugging .allowlist_function("rb_obj_info_dump") // For crashing .allowlist_function("rb_bug") - // From shape.h .allowlist_function("rb_obj_shape_id") .allowlist_function("rb_shape_id_offset") .allowlist_function("rb_shape_get_iv_index") @@ -103,36 +92,20 @@ fn main() { .allowlist_function("rb_yjit_shape_index") .allowlist_var("SHAPE_ID_NUM_BITS") .allowlist_var("SHAPE_ID_HAS_IVAR_MASK") - - // From ruby/internal/eval.h .allowlist_function("rb_funcall") - - // From ruby/internal/intern/object.h .allowlist_function("rb_obj_is_kind_of") .allowlist_function("rb_obj_frozen_p") - - // From ruby/internal/encoding/encoding.h .allowlist_type("ruby_encoding_consts") - - // From include/hash.h .allowlist_function("rb_hash_new") - - // From internal/hash.h .allowlist_function("rb_hash_new_with_size") .allowlist_function("rb_hash_resurrect") .allowlist_function("rb_hash_stlike_foreach") .allowlist_function("rb_to_hash_type") - - // From include/ruby/st.h .allowlist_type("st_retval") - - // From include/ruby/internal/intern/hash.h .allowlist_function("rb_hash_aset") .allowlist_function("rb_hash_aref") .allowlist_function("rb_hash_bulk_insert") .allowlist_function("rb_hash_stlike_lookup") - - // From include/ruby/internal/intern/array.h .allowlist_function("rb_ary_new_capa") .allowlist_function("rb_ary_store") .allowlist_function("rb_ary_resurrect") @@ -142,26 +115,17 @@ fn main() { .allowlist_function("rb_ary_push") .allowlist_function("rb_ary_unshift_m") .allowlist_function("rb_yjit_rb_ary_subseq_length") - - // From internal/array.h .allowlist_function("rb_ec_ary_new_from_values") .allowlist_function("rb_ary_tmp_new_from_values") - - // From include/ruby/internal/intern/class.h .allowlist_function("rb_class_attached_object") .allowlist_function("rb_singleton_class") - - // From include/ruby/internal/core/rclass.h .allowlist_function("rb_class_get_superclass") - - // From include/ruby/internal/gc.h .allowlist_function("rb_gc_mark") .allowlist_function("rb_gc_mark_movable") .allowlist_function("rb_gc_location") .allowlist_function("rb_gc_writebarrier") // VALUE variables for Ruby class objects - // From include/ruby/internal/globals.h .allowlist_var("rb_cBasicObject") .allowlist_var("rb_cModule") .allowlist_var("rb_cNilClass") @@ -178,84 +142,53 @@ fn main() { .allowlist_var("rb_cHash") .allowlist_var("rb_cClass") - // From include/ruby/internal/fl_type.h .allowlist_type("ruby_fl_type") .allowlist_type("ruby_fl_ushift") - - // From include/ruby/internal/core/robject.h .allowlist_type("ruby_robject_flags") - - // From include/ruby/internal/core/rarray.h .allowlist_type("ruby_rarray_flags") .allowlist_type("ruby_rarray_consts") - - // From include/ruby/internal/core/rclass.h .allowlist_type("ruby_rmodule_flags") - - // From ruby/internal/globals.h .allowlist_var("rb_mKernel") - - // From vm_callinfo.h .allowlist_type("vm_call_flag_bits") .allowlist_type("rb_call_data") .blocklist_type("rb_callcache.*") // Not used yet - opaque to make it easy to import rb_call_data .opaque_type("rb_callcache.*") .allowlist_type("rb_callinfo") - - // From vm_insnhelper.h .allowlist_var("VM_ENV_DATA_INDEX_ME_CREF") .allowlist_var("rb_block_param_proxy") - - // From include/ruby/internal/intern/range.h .allowlist_function("rb_range_new") - - // From include/ruby/internal/symbol.h .allowlist_function("rb_intern") .allowlist_function("rb_intern2") .allowlist_function("rb_id2sym") .allowlist_function("rb_id2name") .allowlist_function("rb_sym2id") .allowlist_function("rb_str_intern") - - // From internal/numeric.h .allowlist_function("rb_fix_aref") .allowlist_function("rb_float_plus") .allowlist_function("rb_float_minus") .allowlist_function("rb_float_mul") .allowlist_function("rb_float_div") - - // From internal/string.h .allowlist_type("ruby_rstring_private_flags") .allowlist_function("rb_ec_str_resurrect") .allowlist_function("rb_str_concat_literals") .allowlist_function("rb_obj_as_string_result") .allowlist_function("rb_str_byte_substr") .allowlist_function("rb_str_substr_two_fixnums") - - // From include/ruby/internal/intern/parse.h .allowlist_function("rb_backref_get") - - // From include/ruby/internal/intern/re.h .allowlist_function("rb_reg_last_match") .allowlist_function("rb_reg_match_pre") .allowlist_function("rb_reg_match_post") .allowlist_function("rb_reg_match_last") .allowlist_function("rb_reg_nth_match") - - // From internal/re.h .allowlist_function("rb_reg_new_ary") // `ruby_value_type` is a C enum and this stops it from // prefixing all the members with the name of the type .prepend_enum_name(false) .translate_enum_integer_types(true) // so we get fixed width Rust types for members - // From include/ruby/internal/value_type.h .allowlist_type("ruby_value_type") // really old C extension API - // From include/ruby/internal/hash.h .allowlist_type("ruby_rhash_flags") // really old C extension API - - // From method.h .allowlist_type("rb_method_visibility_t") .allowlist_type("rb_method_type_t") .allowlist_type("method_optimized_type") @@ -266,11 +199,7 @@ fn main() { .blocklist_type("rb_method_cfunc_t") .blocklist_type("rb_method_definition_.*") // Large struct with a bitfield and union of many types - don't import (yet?) .opaque_type("rb_method_definition_.*") - - // From numeric.c .allowlist_function("rb_float_new") - - // From vm_core.h .allowlist_var("rb_cRubyVM") .allowlist_var("rb_mRubyVMFrozenCore") .allowlist_var("VM_BLOCK_HANDLER_NONE") @@ -309,8 +238,6 @@ fn main() { .allowlist_type("vm_check_match_type") .allowlist_type("vm_opt_newarray_send_type") .allowlist_type("rb_iseq_type") - - // From yjit.c .allowlist_function("rb_object_shape_count") .allowlist_function("rb_ivar_get_at") .allowlist_function("rb_ivar_get_at_no_ractor_check") @@ -347,8 +274,6 @@ fn main() { .allowlist_function("rb_yjit_set_exception_return") .allowlist_function("rb_yjit_str_concat_codepoint") .allowlist_type("rstring_offsets") - - // From jit.c .allowlist_function("rb_assert_holding_vm_lock") .allowlist_function("rb_jit_shape_too_complex_p") .allowlist_function("rb_jit_multi_ractor_p") @@ -356,58 +281,35 @@ fn main() { .allowlist_function("rb_jit_vm_unlock") .allowlist_function("rb_jit_for_each_iseq") .allowlist_type("robject_offsets") - - // from vm_sync.h .allowlist_function("rb_vm_barrier") // Not sure why it's picking these up, but don't. .blocklist_type("FILE") .blocklist_type("_IO_.*") - // From internal/compile.h .allowlist_function("rb_vm_insn_decode") - - // from internal/cont.h .allowlist_function("rb_jit_cont_each_iseq") - - // From iseq.h .allowlist_function("rb_vm_insn_addr2opcode") .allowlist_function("rb_iseqw_to_iseq") .allowlist_function("rb_iseq_label") .allowlist_function("rb_iseq_line_no") .allowlist_type("defined_type") - - // From builtin.h .allowlist_type("rb_builtin_function.*") - - // From internal/variable.h .allowlist_function("rb_gvar_(get|set)") .allowlist_function("rb_ensure_iv_list_size") - - // From include/ruby/internal/intern/variable.h .allowlist_function("rb_attr_get") .allowlist_function("rb_ivar_defined") .allowlist_function("rb_ivar_get") .allowlist_function("rb_mod_name") .allowlist_function("rb_const_get") - - // From internal/vm.h .allowlist_var("rb_vm_insn_count") - - // From include/ruby/internal/intern/vm.h .allowlist_function("rb_get_alloc_func") - - // From internal/object.h .allowlist_function("rb_class_allocate_instance") .allowlist_function("rb_obj_equal") .allowlist_function("rb_class_new_instance_pass_kw") .allowlist_function("rb_obj_alloc") - - // From gc.h and internal/gc.h .allowlist_function("rb_obj_info") .allowlist_function("ruby_xfree") - - // From include/ruby/debug.h .allowlist_function("rb_profile_frames") // Functions used for code generation @@ -491,7 +393,6 @@ fn main() { // We define VALUE manually, don't import it .blocklist_type("VALUE") - // From iseq.h .opaque_type("rb_iseq_t") .blocklist_type("rb_iseq_t") diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 56400e20cd742e..26bdfd2848373f 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -69,32 +69,21 @@ fn main() { // Import YARV bytecode instruction constants .allowlist_type("ruby_vminsn_type") - // From include/ruby/internal/special_consts.h .allowlist_type("ruby_special_consts") - - // From include/ruby/internal/intern/string.h .allowlist_function("rb_utf8_str_new") .allowlist_function("rb_str_buf_append") .allowlist_function("rb_str_dup") - - // From encindex.h .allowlist_type("ruby_preserved_encindex") - - // From include/ruby/ruby.h .allowlist_function("rb_class2name") // This struct is public to Ruby C extensions - // From include/ruby/internal/core/rbasic.h .allowlist_type("RBasic") - // From include/ruby/internal/core/rstring.h .allowlist_type("ruby_rstring_flags") - // From internal.h // This function prints info about a value and is useful for debugging .allowlist_function("rb_obj_info_dump") - // For testing .allowlist_function("ruby_init") .allowlist_function("ruby_init_stack") .allowlist_function("ruby_options") @@ -106,40 +95,25 @@ fn main() { // For crashing .allowlist_function("rb_bug") - // From shape.h .allowlist_function("rb_obj_shape_id") .allowlist_function("rb_shape_id_offset") .allowlist_function("rb_shape_get_iv_index") .allowlist_function("rb_shape_transition_add_ivar_no_warnings") .allowlist_var("SHAPE_ID_NUM_BITS") - - // From ruby/internal/intern/object.h .allowlist_function("rb_obj_is_kind_of") .allowlist_function("rb_obj_frozen_p") .allowlist_function("rb_class_inherited_p") - - // From ruby/internal/encoding/encoding.h .allowlist_type("ruby_encoding_consts") - - // From include/hash.h .allowlist_function("rb_hash_new") - - // From internal/hash.h .allowlist_function("rb_hash_new_with_size") .allowlist_function("rb_hash_resurrect") .allowlist_function("rb_hash_stlike_foreach") .allowlist_function("rb_to_hash_type") - - // From include/ruby/st.h .allowlist_type("st_retval") - - // From include/ruby/internal/intern/hash.h .allowlist_function("rb_hash_aset") .allowlist_function("rb_hash_aref") .allowlist_function("rb_hash_bulk_insert") .allowlist_function("rb_hash_stlike_lookup") - - // From include/ruby/internal/intern/array.h .allowlist_function("rb_ary_new_capa") .allowlist_function("rb_ary_store") .allowlist_function("rb_ary_resurrect") @@ -149,20 +123,12 @@ fn main() { .allowlist_function("rb_ary_dup") .allowlist_function("rb_ary_push") .allowlist_function("rb_ary_unshift_m") - - // From internal/array.h .allowlist_function("rb_ec_ary_new_from_values") .allowlist_function("rb_ary_tmp_new_from_values") - - // From include/ruby/internal/intern/class.h .allowlist_function("rb_class_attached_object") .allowlist_function("rb_singleton_class") .allowlist_function("rb_define_class") - - // From include/ruby/internal/core/rclass.h .allowlist_function("rb_class_get_superclass") - - // From include/ruby/internal/gc.h .allowlist_function("rb_gc_mark") .allowlist_function("rb_gc_mark_movable") .allowlist_function("rb_gc_location") @@ -170,7 +136,6 @@ fn main() { .allowlist_function("rb_gc_writebarrier_remember") // VALUE variables for Ruby class objects - // From include/ruby/internal/globals.h .allowlist_var("rb_cBasicObject") .allowlist_var("rb_cObject") .allowlist_var("rb_cModule") @@ -192,38 +157,21 @@ fn main() { .allowlist_var("rb_cRegexp") .allowlist_var("rb_cISeq") - // From include/ruby/internal/fl_type.h .allowlist_type("ruby_fl_type") .allowlist_type("ruby_fl_ushift") - - // From include/ruby/internal/core/robject.h .allowlist_type("ruby_robject_flags") - - // From include/ruby/internal/core/rarray.h .allowlist_type("ruby_rarray_flags") .allowlist_type("ruby_rarray_consts") - - // From include/ruby/internal/core/rclass.h .allowlist_type("ruby_rmodule_flags") - - // From ruby/internal/globals.h .allowlist_var("rb_mKernel") - - // From vm_callinfo.h .allowlist_type("vm_call_flag_bits") .allowlist_type("rb_call_data") .blocklist_type("rb_callcache.*") // Not used yet - opaque to make it easy to import rb_call_data .opaque_type("rb_callcache.*") .allowlist_type("rb_callinfo") - - // From vm_insnhelper.h .allowlist_var("VM_ENV_DATA_INDEX_ME_CREF") .allowlist_var("rb_block_param_proxy") - - // From include/ruby/internal/intern/range.h .allowlist_function("rb_range_new") - - // From include/ruby/internal/symbol.h .allowlist_function("rb_intern") .allowlist_function("rb_intern2") .allowlist_function("rb_id2sym") @@ -231,38 +179,26 @@ fn main() { .allowlist_function("rb_str_intern") .allowlist_function("rb_id2str") .allowlist_function("rb_sym2str") - - // From internal/numeric.h .allowlist_function("rb_fix_aref") .allowlist_function("rb_float_plus") .allowlist_function("rb_float_minus") .allowlist_function("rb_float_mul") .allowlist_function("rb_float_div") - - // From internal/string.h .allowlist_type("ruby_rstring_private_flags") .allowlist_function("rb_ec_str_resurrect") .allowlist_function("rb_str_concat_literals") .allowlist_function("rb_obj_as_string_result") .allowlist_function("rb_str_byte_substr") .allowlist_function("rb_str_substr_two_fixnums") - - // From include/ruby/internal/intern/parse.h .allowlist_function("rb_backref_get") - - // From include/ruby/internal/intern/re.h .allowlist_function("rb_reg_last_match") .allowlist_function("rb_reg_match_pre") .allowlist_function("rb_reg_match_post") .allowlist_function("rb_reg_match_last") .allowlist_function("rb_reg_nth_match") - - // From internal/re.h .allowlist_function("rb_reg_new_ary") .allowlist_var("ARG_ENCODING_FIXED") .allowlist_var("ARG_ENCODING_NONE") - - // From include/ruby/onigmo.h .allowlist_var("ONIG_OPTION_IGNORECASE") .allowlist_var("ONIG_OPTION_EXTEND") .allowlist_var("ONIG_OPTION_MULTILINE") @@ -271,13 +207,9 @@ fn main() { // prefixing all the members with the name of the type .prepend_enum_name(false) .translate_enum_integer_types(true) // so we get fixed width Rust types for members - // From include/ruby/internal/value_type.h .allowlist_type("ruby_value_type") // really old C extension API - // From include/ruby/internal/hash.h .allowlist_type("ruby_rhash_flags") // really old C extension API - - // From method.h .allowlist_type("rb_method_visibility_t") .allowlist_type("rb_method_type_t") .allowlist_type("method_optimized_type") @@ -288,11 +220,7 @@ fn main() { .blocklist_type("rb_method_cfunc_t") .blocklist_type("rb_method_definition_.*") // Large struct with a bitfield and union of many types - don't import (yet?) .opaque_type("rb_method_definition_.*") - - // From numeric.c .allowlist_function("rb_float_new") - - // From vm_core.h .allowlist_var("rb_mRubyVMFrozenCore") .allowlist_var("VM_BLOCK_HANDLER_NONE") .allowlist_type("vm_frame_env_flags") @@ -331,8 +259,6 @@ fn main() { .allowlist_type("vm_opt_newarray_send_type") .allowlist_type("rb_iseq_type") .allowlist_type("rb_event_flag_t") - - // From zjit.c .allowlist_function("rb_object_shape_count") .allowlist_function("rb_iseq_(get|set)_zjit_payload") .allowlist_function("rb_iseq_pc_at_idx") @@ -362,8 +288,6 @@ fn main() { .allowlist_type("robject_offsets") .allowlist_type("rstring_offsets") .allowlist_var("RB_INVALID_SHAPE_ID") - - // From jit.c .allowlist_function("rb_assert_holding_vm_lock") .allowlist_function("rb_jit_shape_too_complex_p") .allowlist_function("rb_jit_multi_ractor_p") @@ -372,59 +296,36 @@ fn main() { .allowlist_function("rb_jit_for_each_iseq") .allowlist_function("rb_iseq_reset_jit_func") .allowlist_type("robject_offsets") - - // from vm_sync.h .allowlist_function("rb_vm_barrier") // Not sure why it's picking these up, but don't. .blocklist_type("FILE") .blocklist_type("_IO_.*") - // From internal/compile.h .allowlist_function("rb_vm_insn_decode") - - // from internal/cont.h .allowlist_function("rb_jit_cont_each_iseq") - - // From iseq.h .allowlist_function("rb_vm_insn_addr2opcode") .allowlist_function("rb_iseqw_to_iseq") .allowlist_function("rb_iseq_label") .allowlist_function("rb_iseq_line_no") .allowlist_function("rb_iseq_defined_string") .allowlist_type("defined_type") - - // From builtin.h .allowlist_type("rb_builtin_function.*") - - // From internal/variable.h .allowlist_function("rb_gvar_(get|set)") .allowlist_function("rb_ensure_iv_list_size") - - // From include/ruby/internal/intern/variable.h .allowlist_function("rb_attr_get") .allowlist_function("rb_ivar_defined") .allowlist_function("rb_ivar_get") .allowlist_function("rb_ivar_set") .allowlist_function("rb_mod_name") - - // From internal/vm.h .allowlist_var("rb_vm_insn_count") - - // From include/ruby/internal/intern/vm.h .allowlist_function("rb_get_alloc_func") - - // From internal/object.h .allowlist_function("rb_class_allocate_instance") .allowlist_function("rb_obj_equal") .allowlist_function("rb_class_new_instance_pass_kw") .allowlist_function("rb_obj_alloc") - - // From gc.h and internal/gc.h .allowlist_function("rb_obj_info") .allowlist_function("ruby_xfree") - - // From include/ruby/debug.h .allowlist_function("rb_profile_frames") // Functions used for code generation @@ -505,7 +406,6 @@ fn main() { .blocklist_type("VALUE") .blocklist_type("ID") - // From iseq.h .opaque_type("rb_iseq_t") .blocklist_type("rb_iseq_t") From 30f85ce5302d4e8da5852726db9a2bc607b5aeec Mon Sep 17 00:00:00 2001 From: Aiden Fox Ivey Date: Thu, 11 Sep 2025 23:29:30 -0400 Subject: [PATCH 06/14] YJIT: Remove cargo from release builds * Release builds depend only on `rustc` for sake of packaging. Removing it from the image ensures that passing the CI implies that only `rustc` is required. --- .github/workflows/yjit-ubuntu.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/yjit-ubuntu.yml b/.github/workflows/yjit-ubuntu.yml index 1961d1262ab3a0..3ff3310a44a5a0 100644 --- a/.github/workflows/yjit-ubuntu.yml +++ b/.github/workflows/yjit-ubuntu.yml @@ -145,6 +145,12 @@ jobs: if: ${{ matrix.rust_version }} run: rustup install ${{ matrix.rust_version }} --profile minimal + - name: Remove cargo + # Since this tests a `rustc` build for release, remove `cargo` to ensure + # that only `rustc` is used. + if: ${{ contains(matrix.configure, 'rustc') }} + run: sudo rm $(which -a cargo | uniq) + - name: Run configure run: ../src/configure -C --disable-install-doc --prefix=$(pwd)/install ${{ matrix.configure }} From f75e1cb362f57aa9e18c42026adec39b86d4e5c6 Mon Sep 17 00:00:00 2001 From: Aiden Fox Ivey Date: Thu, 11 Sep 2025 23:32:06 -0400 Subject: [PATCH 07/14] ZJIT: Move jit.rs to ruby.rs and create a shared crate `jit` * ruby.rs should hold the main entrypoint to YJIT and ZJIT * The crate jit will hold code shared between them --- Cargo.lock | 16 +++++++++++----- Cargo.toml | 6 +++--- configure.ac | 4 ++-- defs/jit.mk | 2 +- jit/Cargo.toml | 6 ++++++ jit/src/lib.rs | 37 +++++++++++++++++++++++++++++++++++++ jit.rs => ruby.rs | 0 yjit/Cargo.toml | 1 + zjit/Cargo.toml | 1 + 9 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 jit/Cargo.toml create mode 100644 jit/src/lib.rs rename jit.rs => ruby.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 6312cb46a9b2ca..9a4b2ebbbaf2ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,11 +62,7 @@ dependencies = [ [[package]] name = "jit" -version = "0.0.0" -dependencies = [ - "yjit", - "zjit", -] +version = "0.1.0" [[package]] name = "lazy_static" @@ -86,6 +82,14 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "ruby" +version = "0.0.0" +dependencies = [ + "yjit", + "zjit", +] + [[package]] name = "shlex" version = "1.3.0" @@ -176,6 +180,7 @@ name = "yjit" version = "0.1.0" dependencies = [ "capstone", + "jit", ] [[package]] @@ -184,4 +189,5 @@ version = "0.0.1" dependencies = [ "capstone", "insta", + "jit", ] diff --git a/Cargo.toml b/Cargo.toml index 3f373fdace9cbf..ec2ce880ca4c48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,10 @@ # TODO(alan) notes about rust version requirements. Undecided yet. [workspace] -members = ["zjit", "yjit"] +members = ["zjit", "yjit", "jit"] [package] -name = "jit" +name = "ruby" version = "0.0.0" edition = "2024" rust-version = "1.85.0" @@ -18,7 +18,7 @@ zjit = { path = "zjit", optional = true } [lib] crate-type = ["staticlib"] -path = "jit.rs" +path = "ruby.rs" [features] disasm = ["yjit?/disasm", "zjit?/disasm"] diff --git a/configure.ac b/configure.ac index 47f8b79b038fbe..8cb30429dcddca 100644 --- a/configure.ac +++ b/configure.ac @@ -4019,9 +4019,9 @@ AS_IF([test x"$JIT_CARGO_SUPPORT" != "xno" -o \( x"$YJIT_SUPPORT" != "xno" -a x" ]) CARGO_BUILD_ARGS="--profile ${JIT_CARGO_SUPPORT} --features ${rb_cargo_features}" AS_IF([test "${JIT_CARGO_SUPPORT}" = "dev"], [ - RUST_LIB="target/debug/libjit.a" + RUST_LIB="target/debug/libruby.a" ], [ - RUST_LIB="target/${JIT_CARGO_SUPPORT}/libjit.a" + RUST_LIB="target/${JIT_CARGO_SUPPORT}/libruby.a" ]) ]) diff --git a/defs/jit.mk b/defs/jit.mk index 28d8f2da3a87f7..a537d803002856 100644 --- a/defs/jit.mk +++ b/defs/jit.mk @@ -17,7 +17,7 @@ CARGO_VERBOSE = $(CARGO_VERBOSE_$(V)) # ld: warning: object file (target/debug/libjit.a()) was built for # newer macOS version (15.2) than being linked (15.0) # This limits us to an older set of macOS API in the rust code, but we don't use any. -$(RUST_LIB): $(srcdir)/jit.rs +$(RUST_LIB): $(srcdir)/ruby.rs $(Q)if [ '$(ZJIT_SUPPORT)' != no -a '$(YJIT_SUPPORT)' != no ]; then \ echo 'building YJIT and ZJIT ($(JIT_CARGO_SUPPORT:yes=release) mode)'; \ elif [ '$(ZJIT_SUPPORT)' != no ]; then \ diff --git a/jit/Cargo.toml b/jit/Cargo.toml new file mode 100644 index 00000000000000..530fe3674b0345 --- /dev/null +++ b/jit/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "jit" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/jit/src/lib.rs b/jit/src/lib.rs new file mode 100644 index 00000000000000..6079d00f2fd886 --- /dev/null +++ b/jit/src/lib.rs @@ -0,0 +1,37 @@ +//! Shared code between YJIT and ZJIT. + +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::alloc::{GlobalAlloc, Layout, System}; + +#[global_allocator] +pub static GLOBAL_ALLOCATOR: StatsAlloc = StatsAlloc { alloc_size: AtomicUsize::new(0) }; + +pub struct StatsAlloc { + pub alloc_size: AtomicUsize, +} + +unsafe impl GlobalAlloc for StatsAlloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + self.alloc_size.fetch_add(layout.size(), Ordering::SeqCst); + unsafe { System.alloc(layout) } + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + self.alloc_size.fetch_sub(layout.size(), Ordering::SeqCst); + unsafe { System.dealloc(ptr, layout) } + } + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + self.alloc_size.fetch_add(layout.size(), Ordering::SeqCst); + unsafe { System.alloc_zeroed(layout) } + } + + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + if new_size > layout.size() { + self.alloc_size.fetch_add(new_size - layout.size(), Ordering::SeqCst); + } else if new_size < layout.size() { + self.alloc_size.fetch_sub(layout.size() - new_size, Ordering::SeqCst); + } + unsafe { System.realloc(ptr, layout, new_size) } + } +} diff --git a/jit.rs b/ruby.rs similarity index 100% rename from jit.rs rename to ruby.rs diff --git a/yjit/Cargo.toml b/yjit/Cargo.toml index ad7dd35ecfb173..af9c18c0dc1028 100644 --- a/yjit/Cargo.toml +++ b/yjit/Cargo.toml @@ -13,6 +13,7 @@ publish = false # Don't publish to crates.io # No required dependencies to simplify build process. TODO: Link to yet to be # written rationale. Optional For development and testing purposes capstone = { version = "0.13.0", optional = true } +jit = { version = "0.1.0", path = "../jit" } # NOTE: Development builds select a set of these via configure.ac # For debugging, `make V=1` shows exact cargo invocation. diff --git a/zjit/Cargo.toml b/zjit/Cargo.toml index 7334d465c28ba8..617cd11916db49 100644 --- a/zjit/Cargo.toml +++ b/zjit/Cargo.toml @@ -9,6 +9,7 @@ publish = false # Don't publish to crates.io # No required dependencies to simplify build process. TODO: Link to yet to be # written rationale. Optional For development and testing purposes capstone = { version = "0.13.0", optional = true } +jit = { version = "0.1.0", path = "../jit" } [dev-dependencies] insta = "1.43.1" From cd85fe5a87f3f7cac92b132f9f195a3d39685a37 Mon Sep 17 00:00:00 2001 From: Aiden Fox Ivey Date: Thu, 11 Sep 2025 23:33:27 -0400 Subject: [PATCH 08/14] ZJIT: Add support for stats_allocator * Using the shared jit crate, support for a single global_allocator can function * Solves --zjit-mem-size --- common.mk | 2 ++ yjit/Cargo.toml | 1 + yjit/src/stats.rs | 43 ++++++------------------------------------ yjit/src/virtualmem.rs | 5 ++++- yjit/yjit.mk | 1 + zjit/Cargo.toml | 1 + zjit/src/options.rs | 10 +++++++--- zjit/src/state.rs | 2 +- zjit/src/stats.rs | 7 ++++++- zjit/src/virtualmem.rs | 11 ++++++++++- zjit/zjit.mk | 1 + 11 files changed, 40 insertions(+), 44 deletions(-) diff --git a/common.mk b/common.mk index 5cc7886796243f..ef7eb6ab58ca1e 100644 --- a/common.mk +++ b/common.mk @@ -266,6 +266,7 @@ MAKE_LINK = $(MINIRUBY) -rfileutils -e "include FileUtils::Verbose" \ YJIT_RUSTC_ARGS = --crate-name=yjit \ --crate-type=staticlib \ --edition=2021 \ + --cfg 'feature="stats_allocator"' \ -g \ -C lto=thin \ -C opt-level=3 \ @@ -276,6 +277,7 @@ YJIT_RUSTC_ARGS = --crate-name=yjit \ ZJIT_RUSTC_ARGS = --crate-name=zjit \ --crate-type=staticlib \ --edition=2024 \ + --cfg 'feature="stats_allocator"' \ -g \ -C lto=thin \ -C opt-level=3 \ diff --git a/yjit/Cargo.toml b/yjit/Cargo.toml index af9c18c0dc1028..e2f1d84ffd3d8c 100644 --- a/yjit/Cargo.toml +++ b/yjit/Cargo.toml @@ -25,3 +25,4 @@ disasm = ["capstone"] # from cfg!(debug_assertions) so that we can see disasm of the code # that would run in the release mode. runtime_checks = [] +stats_allocator = [] diff --git a/yjit/src/stats.rs b/yjit/src/stats.rs index ea6130973d22e8..09971c5b3afb48 100644 --- a/yjit/src/stats.rs +++ b/yjit/src/stats.rs @@ -1,9 +1,8 @@ //! Everything related to the collection of runtime stats in YJIT //! See the --yjit-stats command-line option -use std::alloc::{GlobalAlloc, Layout, System}; use std::ptr::addr_of_mut; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::Ordering; use std::time::Instant; use std::collections::HashMap; @@ -12,6 +11,10 @@ use crate::cruby::*; use crate::options::*; use crate::yjit::{yjit_enabled_p, YJIT_INIT_TIME}; +#[cfg(feature = "stats_allocator")] +#[path = "../../jit/src/lib.rs"] +mod jit; + /// Running total of how many ISeqs are in the system. #[no_mangle] pub static mut rb_yjit_live_iseq_count: u64 = 0; @@ -20,43 +23,9 @@ pub static mut rb_yjit_live_iseq_count: u64 = 0; #[no_mangle] pub static mut rb_yjit_iseq_alloc_count: u64 = 0; -/// A middleware to count Rust-allocated bytes as yjit_alloc_size. -#[global_allocator] -static GLOBAL_ALLOCATOR: StatsAlloc = StatsAlloc { alloc_size: AtomicUsize::new(0) }; - -pub struct StatsAlloc { - alloc_size: AtomicUsize, -} - -unsafe impl GlobalAlloc for StatsAlloc { - unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - self.alloc_size.fetch_add(layout.size(), Ordering::SeqCst); - System.alloc(layout) - } - - unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - self.alloc_size.fetch_sub(layout.size(), Ordering::SeqCst); - System.dealloc(ptr, layout) - } - - unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { - self.alloc_size.fetch_add(layout.size(), Ordering::SeqCst); - System.alloc_zeroed(layout) - } - - unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { - if new_size > layout.size() { - self.alloc_size.fetch_add(new_size - layout.size(), Ordering::SeqCst); - } else if new_size < layout.size() { - self.alloc_size.fetch_sub(layout.size() - new_size, Ordering::SeqCst); - } - System.realloc(ptr, layout, new_size) - } -} - /// The number of bytes YJIT has allocated on the Rust heap. pub fn yjit_alloc_size() -> usize { - GLOBAL_ALLOCATOR.alloc_size.load(Ordering::SeqCst) + jit::GLOBAL_ALLOCATOR.alloc_size.load(Ordering::SeqCst) } /// Mapping of C function / ISEQ name to integer indices diff --git a/yjit/src/virtualmem.rs b/yjit/src/virtualmem.rs index aa6d21f21035d4..97409c796cbca7 100644 --- a/yjit/src/virtualmem.rs +++ b/yjit/src/virtualmem.rs @@ -7,6 +7,9 @@ use std::{cell::RefCell, ptr::NonNull}; use crate::{backend::ir::Target, stats::yjit_alloc_size, utils::IntoUsize}; +#[cfg(test)] +use crate::options::get_option; + #[cfg(not(test))] pub type VirtualMem = VirtualMemory; @@ -411,7 +414,7 @@ pub mod tests { PAGE_SIZE.try_into().unwrap(), NonNull::new(mem_start as *mut u8).unwrap(), mem_size, - 128 * 1024 * 1024, + get_option!(mem_size), ) } diff --git a/yjit/yjit.mk b/yjit/yjit.mk index 6b22a15960c1f3..cf68edb29770b8 100644 --- a/yjit/yjit.mk +++ b/yjit/yjit.mk @@ -6,6 +6,7 @@ YJIT_SRC_FILES = $(wildcard \ $(top_srcdir)/yjit/src/*/*.rs \ $(top_srcdir)/yjit/src/*/*/*.rs \ $(top_srcdir)/yjit/src/*/*/*/*.rs \ + $(top_srcdir)/jit/src/lib.rs \ ) # Because of Cargo cache, if the actual binary is not changed from the diff --git a/zjit/Cargo.toml b/zjit/Cargo.toml index 617cd11916db49..c97c845a6eff7f 100644 --- a/zjit/Cargo.toml +++ b/zjit/Cargo.toml @@ -20,3 +20,4 @@ insta = "1.43.1" # Support --yjit-dump-disasm and RubyVM::YJIT.disasm using libcapstone. disasm = ["capstone"] runtime_checks = [] +stats_allocator = [] diff --git a/zjit/src/options.rs b/zjit/src/options.rs index dbb6ee8ebbdf24..2a9b2e7d279055 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -35,6 +35,10 @@ pub struct Options { /// Note that the command line argument is expressed in MiB and not bytes. pub exec_mem_bytes: usize, + /// Hard limit of ZJIT's total memory usage. + /// Note that the command line argument is expressed in MiB and not bytes. + pub mem_bytes: usize, + /// Number of times YARV instructions should be profiled. pub num_profiles: u8, @@ -79,6 +83,7 @@ impl Default for Options { fn default() -> Self { Options { exec_mem_bytes: 64 * 1024 * 1024, + mem_bytes: 128 * 1024 * 1024, num_profiles: DEFAULT_NUM_PROFILES, stats: false, print_stats: false, @@ -100,9 +105,8 @@ impl Default for Options { /// Note that --help allows only 80 chars per line, including indentation, and it also puts the /// description in a separate line if the option name is too long. 80-char limit --> | (any character beyond this `|` column fails the test) pub const ZJIT_OPTIONS: &[(&str, &str)] = &[ - // TODO: Hide --zjit-exec-mem-size from ZJIT_OPTIONS once we add --zjit-mem-size (Shopify/ruby#686) - ("--zjit-exec-mem-size=num", - "Size of executable memory block in MiB (default: 64)."), + ("--zjit-mem-size=num", + "Max amount of memory that ZJIT can use (in MiB)."), ("--zjit-call-threshold=num", "Number of calls to trigger JIT (default: 2)."), ("--zjit-num-profiles=num", diff --git a/zjit/src/state.rs b/zjit/src/state.rs index 02bba3b7a3e8a8..da97829e43629b 100644 --- a/zjit/src/state.rs +++ b/zjit/src/state.rs @@ -85,7 +85,7 @@ impl ZJITState { page_size, NonNull::new(virt_block).unwrap(), exec_mem_bytes, - exec_mem_bytes, // TODO: change this to --zjit-mem-size (Shopify/ruby#686) + get_option!(mem_bytes) ); let mem_block = Rc::new(RefCell::new(mem_block)); diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index d1a6d584b9c020..98ddc20226621b 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -1,6 +1,11 @@ //! Counters and associated methods for events when ZJIT is run. use std::time::Instant; +use std::sync::atomic::Ordering; + +#[cfg(feature = "stats_allocator")] +#[path = "../../jit/src/lib.rs"] +mod jit; use crate::{cruby::*, hir::ParseError, options::get_option, state::{zjit_enabled_p, ZJITState}}; @@ -353,5 +358,5 @@ pub fn with_time_stat(counter: Counter, func: F) -> R where F: FnOnce() -> /// The number of bytes ZJIT has allocated on the Rust heap. pub fn zjit_alloc_size() -> usize { - 0 // TODO: report the actual memory usage to support --zjit-mem-size (Shopify/ruby#686) + jit::GLOBAL_ALLOCATOR.alloc_size.load(Ordering::SeqCst) } diff --git a/zjit/src/virtualmem.rs b/zjit/src/virtualmem.rs index 42ce525fde7a6c..11de4e08afe962 100644 --- a/zjit/src/virtualmem.rs +++ b/zjit/src/virtualmem.rs @@ -7,6 +7,9 @@ use std::ptr::NonNull; use crate::stats::zjit_alloc_size; +#[cfg(test)] +use crate::options::get_option; + #[cfg(not(test))] pub type VirtualMem = VirtualMemory; @@ -369,6 +372,12 @@ pub mod tests { // Fictional architecture where each page is 4 bytes long const PAGE_SIZE: usize = 4; fn new_dummy_virt_mem() -> VirtualMemory { + unsafe { + if crate::options::OPTIONS.is_none() { + crate::options::OPTIONS = Some(crate::options::Options::default()); + } + } + let mem_size = PAGE_SIZE * 10; let alloc = TestingAllocator::new(mem_size); let mem_start: *const u8 = alloc.mem_start(); @@ -378,7 +387,7 @@ pub mod tests { PAGE_SIZE.try_into().unwrap(), NonNull::new(mem_start as *mut u8).unwrap(), mem_size, - 128 * 1024 * 1024, + get_option!(mem_bytes), ) } diff --git a/zjit/zjit.mk b/zjit/zjit.mk index be989bdecd41c3..f0bf1b0da59fb2 100644 --- a/zjit/zjit.mk +++ b/zjit/zjit.mk @@ -9,6 +9,7 @@ ZJIT_SRC_FILES = $(wildcard \ $(top_srcdir)/zjit/src/*/*.rs \ $(top_srcdir)/zjit/src/*/*/*.rs \ $(top_srcdir)/zjit/src/*/*/*/*.rs \ + $(top_srcdir)/jit/src/lib.rs \ ) $(RUST_LIB): $(ZJIT_SRC_FILES) From 6d2c7d7d0ff7029c39f5137a228aa15e45f01d7b Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Wed, 10 Sep 2025 14:12:39 -0300 Subject: [PATCH 09/14] [rubygems/rubygems] Raise error on missing version file If the file option is given but the file not found, raise a GemfileError with a message indicating the file was not found. Currently this is raising a generic Errno::ENOENT error. https://github.com/rubygems/rubygems/commit/db61de6b21 --- lib/bundler/ruby_dsl.rb | 2 ++ spec/bundler/bundler/ruby_dsl_spec.rb | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/lib/bundler/ruby_dsl.rb b/lib/bundler/ruby_dsl.rb index cd88253f463374..db4d5521e54925 100644 --- a/lib/bundler/ruby_dsl.rb +++ b/lib/bundler/ruby_dsl.rb @@ -57,6 +57,8 @@ def normalize_ruby_file(filename) else file_content.strip end + rescue Errno::ENOENT + raise GemfileError, "Could not find version file #{filename}" end end end diff --git a/spec/bundler/bundler/ruby_dsl_spec.rb b/spec/bundler/bundler/ruby_dsl_spec.rb index 2607f746e76653..0d02542fb595b3 100644 --- a/spec/bundler/bundler/ruby_dsl_spec.rb +++ b/spec/bundler/bundler/ruby_dsl_spec.rb @@ -210,6 +210,16 @@ class MockDSL it_behaves_like "it stores the ruby version" end end + + context "when the file does not exist" do + let(:ruby_version_file_path) { nil } + let(:ruby_version_arg) { nil } + let(:file) { "nonexistent.txt" } + + it "raises an error" do + expect { subject }.to raise_error(Bundler::GemfileError, /Could not find version file nonexistent.txt/) + end + end end end end From e6ca24f1166d7f4840ca8c78a2933f86d7c03a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= <2887858+deivid-rodriguez@users.noreply.github.com> Date: Thu, 11 Sep 2025 08:00:00 +0200 Subject: [PATCH 10/14] [rubygems/rubygems] If name is to be unlocked, we can skip converging the spec https://github.com/rubygems/rubygems/commit/744b35412e --- lib/bundler/definition.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index e400c38cec54dc..bdde7f253aaeea 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -1036,6 +1036,8 @@ def converge_specs(specs) specs.each do |s| name = s.name + next if @gems_to_unlock.include?(name) + dep = @dependencies.find {|d| s.satisfies?(d) } lockfile_source = s.source @@ -1054,7 +1056,7 @@ def converge_specs(specs) next if @sources_to_unlock.include?(source.name) # Path sources have special logic - if source.instance_of?(Source::Path) || source.instance_of?(Source::Gemspec) || (source.instance_of?(Source::Git) && !@gems_to_unlock.include?(name) && deps.include?(dep)) + if source.instance_of?(Source::Path) || source.instance_of?(Source::Gemspec) || (source.instance_of?(Source::Git) && deps.include?(dep)) new_spec = source.specs[s].first if new_spec s.runtime_dependencies.replace(new_spec.runtime_dependencies) From 2330033a90f580be47a58748c178570ab5c3de3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= <2887858+deivid-rodriguez@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:33:49 +0200 Subject: [PATCH 11/14] [rubygems/rubygems] Remove unnecessary condition It sounds like this should apply to all git sources at this point. https://github.com/rubygems/rubygems/commit/b1817f91de --- lib/bundler/definition.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index bdde7f253aaeea..3f406079eb990b 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -1056,7 +1056,7 @@ def converge_specs(specs) next if @sources_to_unlock.include?(source.name) # Path sources have special logic - if source.instance_of?(Source::Path) || source.instance_of?(Source::Gemspec) || (source.instance_of?(Source::Git) && deps.include?(dep)) + if source.instance_of?(Source::Path) || source.instance_of?(Source::Gemspec) || source.instance_of?(Source::Git) new_spec = source.specs[s].first if new_spec s.runtime_dependencies.replace(new_spec.runtime_dependencies) From 92bddb4529b7c788cc627b11a90917d31b33072b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= <2887858+deivid-rodriguez@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:42:47 +0200 Subject: [PATCH 12/14] [rubygems/rubygems] Consolidate condition on path sources It matches the comment above more naturally and it's consistent with how the same thing is checked in other places. https://github.com/rubygems/rubygems/commit/59ec6b4b29 --- lib/bundler/definition.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 3f406079eb990b..8b1e082ae9928d 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -1056,7 +1056,7 @@ def converge_specs(specs) next if @sources_to_unlock.include?(source.name) # Path sources have special logic - if source.instance_of?(Source::Path) || source.instance_of?(Source::Gemspec) || source.instance_of?(Source::Git) + if source.is_a?(Source::Path) new_spec = source.specs[s].first if new_spec s.runtime_dependencies.replace(new_spec.runtime_dependencies) From 98beabd66687b741cd2d72d7e0517ae23100d2ff Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:18:36 +0200 Subject: [PATCH 13/14] [rubygems/rubygems] Fix outdated lockfile during `bundle lock` when source changes When the source used to be git and switches back to rubygems, it is possible that the git source contains a version that ruybgems doesn't know about yet. So don't add the locked spec to the base resolve, and also don't add a lower bound requirement on the version, since the version in the new source may actually be lower. https://github.com/rubygems/rubygems/commit/85514e3a1e --- lib/bundler/definition.rb | 3 ++- spec/bundler/lock/git_spec.rb | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 8b1e082ae9928d..e177b6e39673c7 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -1051,6 +1051,7 @@ def converge_specs(specs) # Replace the locked dependency's source with the equivalent source from the Gemfile s.source = replacement_source || default_source + next if s.source_changed? source = s.source next if @sources_to_unlock.include?(source.name) @@ -1138,7 +1139,7 @@ def lockfiles_equal?(current, proposed, preserve_unknown_sections) def additional_base_requirements_to_prevent_downgrades(resolution_base) return resolution_base unless @locked_gems && !sources.expired_sources?(@locked_gems.sources) @originally_locked_specs.each do |locked_spec| - next if locked_spec.source.is_a?(Source::Path) + next if locked_spec.source.is_a?(Source::Path) || locked_spec.source_changed? name = locked_spec.name next if @changed_dependencies.include?(name) diff --git a/spec/bundler/lock/git_spec.rb b/spec/bundler/lock/git_spec.rb index 49c0a2af1c20fc..4e416518305aa8 100644 --- a/spec/bundler/lock/git_spec.rb +++ b/spec/bundler/lock/git_spec.rb @@ -220,4 +220,39 @@ expect(lockfile).to include("securerandom (0.3.2)") end + + it "does not lock versions that don't exist in the repository when changing a GIT direct dep to a GEM direct dep" do + build_repo4 do + build_gem "ruby-lsp", "0.16.1" + end + + path = lib_path("ruby-lsp") + revision = build_git("ruby-lsp", "0.16.2", path: path).ref_for("HEAD") + + lockfile <<~L + GIT + remote: #{path} + revision: #{revision} + specs: + ruby-lsp (0.16.2) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ruby-lsp! + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<~G + source "https://gem.repo4" + gem "ruby-lsp" + G + + bundle "lock" + + expect(lockfile).to include("ruby-lsp (0.16.1)") + end end From 09ae509c1f5f6804d8f43564d9e60a7d711074a7 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 11 Sep 2025 10:04:40 +0200 Subject: [PATCH 14/14] [ruby/json] Enable coverage before loading simplecov Fix: https://github.com/ruby/json/pull/853 Simplecov end up requiring json so we need to start collecting coverage before. https://github.com/ruby/json/commit/ca72019fd3 --- test/json/test_helper.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/json/test_helper.rb b/test/json/test_helper.rb index cf592debfd8c43..a788804ddeca4e 100644 --- a/test/json/test_helper.rb +++ b/test/json/test_helper.rb @@ -1,5 +1,8 @@ $LOAD_PATH.unshift(File.expand_path('../../../ext', __FILE__), File.expand_path('../../../lib', __FILE__)) +require 'coverage' +Coverage.start + begin require 'simplecov' rescue LoadError