From 522b7d823fb00821eea8d0cf13f33a73e91c0ab7 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Tue, 18 Nov 2025 21:18:26 +0900 Subject: [PATCH 01/15] [ruby/openssl] ssl: fix test_pqc_sigalg on RHEL 9.7 RHEL 9.7 ships OpenSSL 3.5.1 with ML-DSA support, but it is disabled for TLS by default, according to the system configuration file: /etc/crypto-policies/back-ends/opensslcnf.config Specify SSLContext#sigalgs to override the default list. https://github.com/ruby/openssl/commit/fac3a26748 --- test/openssl/test_ssl.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index 5082dadd1b7971..5d20ccd1f4b2e4 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -2084,6 +2084,7 @@ def test_pqc_sigalg ctx_proc = -> ctx { # Unset values set by start_server ctx.cert = ctx.key = ctx.extra_chain_cert = nil + ctx.sigalgs = "rsa_pss_rsae_sha256:mldsa65" ctx.add_certificate(mldsa_cert, mldsa) ctx.add_certificate(rsa_cert, rsa) } From c38486ffef14f4991288afe9c0d8d23f57b617fc Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Tue, 18 Nov 2025 13:44:40 +0100 Subject: [PATCH 02/15] ZJIT: Validate types for all instructions * This can catch subtle errors early, so avoid a fallback case and handle every instruction explicitly. --- zjit/src/hir.rs | 180 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 121 insertions(+), 59 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 4df3ddbb26f162..d68ddd2479ebd0 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -3828,33 +3828,132 @@ impl Function { let insn_id = self.union_find.borrow().find_const(insn_id); let insn = self.find(insn_id); match insn { - Insn::StringCopy { val, .. } => self.assert_subtype(insn_id, val, types::StringExact), - Insn::StringIntern { val, .. } => self.assert_subtype(insn_id, val, types::StringExact), - Insn::ArrayDup { val, .. } => self.assert_subtype(insn_id, val, types::ArrayExact), - Insn::StringAppend { recv, other, .. } => { - self.assert_subtype(insn_id, recv, types::StringExact)?; - self.assert_subtype(insn_id, other, types::String) + // Instructions with no InsnId operands (except state) or nothing to assert + Insn::Const { .. } + | Insn::Param + | Insn::PutSpecialObject { .. } + | Insn::LoadField { .. } + | Insn::GetConstantPath { .. } + | Insn::IsBlockGiven + | Insn::GetGlobal { .. } + | Insn::LoadPC + | Insn::LoadSelf + | Insn::Snapshot { .. } + | Insn::Jump { .. } + | Insn::EntryPoint { .. } + | Insn::GuardBlockParamProxy { .. } + | Insn::PatchPoint { .. } + | Insn::SideExit { .. } + | Insn::IncrCounter { .. } + | Insn::IncrCounterPtr { .. } + | Insn::CheckInterrupts { .. } + | Insn::CCall { .. } + | Insn::GetClassVar { .. } + | Insn::GetSpecialNumber { .. } + | Insn::GetSpecialSymbol { .. } + | Insn::GetLocal { .. } => { + Ok(()) + } + // Instructions with 1 Ruby object operand + Insn::Test { val } + | Insn::IsNil { val } + | Insn::IsMethodCfunc { val, .. } + | Insn::GuardShape { val, .. } + | Insn::GuardNotFrozen { val, .. } + | Insn::SetGlobal { val, .. } + | Insn::SetLocal { val, .. } + | Insn::SetClassVar { val, .. } + | Insn::Return { val } + | Insn::Throw { val, .. } + | Insn::ObjToString { val, .. } + | Insn::GuardType { val, .. } + | Insn::GuardTypeNot { val, .. } + | Insn::ToArray { val, .. } + | Insn::ToNewArray { val, .. } + | Insn::Defined { v: val, .. } + | Insn::ObjectAlloc { val, .. } + | Insn::DupArrayInclude { target: val, .. } + | Insn::GetIvar { self_val: val, .. } + | Insn::FixnumBitCheck { val, .. } // TODO (https://github.com/Shopify/ruby/issues/859) this should check Fixnum, but then test_checkkeyword_tests_fixnum_bit fails + | Insn::DefinedIvar { self_val: val, .. } => { + self.assert_subtype(insn_id, val, types::BasicObject) + } + // Instructions with 2 Ruby object operands + Insn::SetIvar { self_val: left, val: right, .. } + | Insn::SetInstanceVariable { self_val: left, val: right, .. } + | Insn::NewRange { low: left, high: right, .. } + | Insn::AnyToString { val: left, str: right, .. } => { + self.assert_subtype(insn_id, left, types::BasicObject)?; + self.assert_subtype(insn_id, right, types::BasicObject) + } + // Instructions with recv and a Vec of Ruby objects + Insn::SendWithoutBlock { recv, ref args, .. } + | Insn::SendWithoutBlockDirect { recv, ref args, .. } + | Insn::Send { recv, ref args, .. } + | Insn::SendForward { recv, ref args, .. } + | Insn::InvokeSuper { recv, ref args, .. } + | Insn::CCallVariadic { recv, ref args, .. } + | Insn::ArrayInclude { target: recv, elements: ref args, .. } => { + self.assert_subtype(insn_id, recv, types::BasicObject)?; + for &arg in args { + self.assert_subtype(insn_id, arg, types::BasicObject)?; + } + Ok(()) + } + // Instructions with a Vec of Ruby objects + Insn::CCallWithFrame { ref args, .. } + | Insn::InvokeBuiltin { ref args, .. } + | Insn::InvokeBlock { ref args, .. } + | Insn::NewArray { elements: ref args, .. } + | Insn::ArrayMax { elements: ref args, .. } => { + for &arg in args { + self.assert_subtype(insn_id, arg, types::BasicObject)?; + } + Ok(()) } Insn::NewHash { ref elements, .. } => { if elements.len() % 2 != 0 { return Err(ValidationError::MiscValidationError(insn_id, "NewHash elements length is not even".to_string())); } + for &element in elements { + self.assert_subtype(insn_id, element, types::BasicObject)?; + } + Ok(()) + } + Insn::StringConcat { ref strings, .. } + | Insn::ToRegexp { values: ref strings, .. } => { + for &string in strings { + self.assert_subtype(insn_id, string, types::String)?; + } Ok(()) } - Insn::NewRangeFixnum { low, high, .. } => { - self.assert_subtype(insn_id, low, types::Fixnum)?; - self.assert_subtype(insn_id, high, types::Fixnum) + // Instructions with String operands + Insn::StringCopy { val, .. } => self.assert_subtype(insn_id, val, types::StringExact), + Insn::StringIntern { val, .. } => self.assert_subtype(insn_id, val, types::StringExact), + Insn::StringAppend { recv, other, .. } => { + self.assert_subtype(insn_id, recv, types::StringExact)?; + self.assert_subtype(insn_id, other, types::String) } + // Instructions with Array operands + Insn::ArrayDup { val, .. } => self.assert_subtype(insn_id, val, types::ArrayExact), Insn::ArrayExtend { left, right, .. } => { // TODO(max): Do left and right need to be ArrayExact? self.assert_subtype(insn_id, left, types::Array)?; self.assert_subtype(insn_id, right, types::Array) } - Insn::ArrayPush { array, .. } => self.assert_subtype(insn_id, array, types::Array), - Insn::ArrayPop { array, .. } => self.assert_subtype(insn_id, array, types::Array), - Insn::ArrayLength { array, .. } => self.assert_subtype(insn_id, array, types::Array), + Insn::ArrayPush { array, .. } + | Insn::ArrayPop { array, .. } + | Insn::ArrayLength { array, .. } => { + self.assert_subtype(insn_id, array, types::Array) + } + Insn::ArrayArefFixnum { array, index } => { + self.assert_subtype(insn_id, array, types::Array)?; + self.assert_subtype(insn_id, index, types::Fixnum) + } + // Instructions with Hash operands Insn::HashAref { hash, .. } => self.assert_subtype(insn_id, hash, types::Hash), Insn::HashDup { val, .. } => self.assert_subtype(insn_id, val, types::HashExact), + // Other Insn::ObjectAllocClass { class, .. } => { let has_leaf_allocator = unsafe { rb_zjit_class_has_default_allocator(class) } || class_has_leaf_allocator(class); if !has_leaf_allocator { @@ -3862,9 +3961,6 @@ impl Function { } Ok(()) } - Insn::Test { val } => self.assert_subtype(insn_id, val, types::BasicObject), - Insn::IsNil { val } => self.assert_subtype(insn_id, val, types::BasicObject), - Insn::IsMethodCfunc { val, .. } => self.assert_subtype(insn_id, val, types::BasicObject), Insn::IsBitEqual { left, right } | Insn::IsBitNotEqual { left, right } => { if self.is_a(left, types::CInt) && self.is_a(right, types::CInt) { @@ -3878,41 +3974,15 @@ impl Function { return Err(ValidationError::MiscValidationError(insn_id, "IsBitEqual can only compare CInt/CInt or RubyValue/RubyValue".to_string())); } } - Insn::BoxBool { val } => self.assert_subtype(insn_id, val, types::CBool), - Insn::BoxFixnum { val, .. } => self.assert_subtype(insn_id, val, types::CInt64), - Insn::UnboxFixnum { val } => self.assert_subtype(insn_id, val, types::Fixnum), - Insn::SetGlobal { val, .. } => self.assert_subtype(insn_id, val, types::BasicObject), - Insn::GetIvar { self_val, .. } => self.assert_subtype(insn_id, self_val, types::BasicObject), - Insn::SetIvar { self_val, val, .. } => { - self.assert_subtype(insn_id, self_val, types::BasicObject)?; - self.assert_subtype(insn_id, val, types::BasicObject) - } - Insn::DefinedIvar { self_val, .. } => self.assert_subtype(insn_id, self_val, types::BasicObject), - Insn::SetLocal { val, .. } => self.assert_subtype(insn_id, val, types::BasicObject), - Insn::SetClassVar { val, .. } => self.assert_subtype(insn_id, val, types::BasicObject), - Insn::IfTrue { val, .. } | Insn::IfFalse { val, .. } => self.assert_subtype(insn_id, val, types::CBool), - Insn::SendWithoutBlock { recv, ref args, .. } - | Insn::SendWithoutBlockDirect { recv, ref args, .. } - | Insn::Send { recv, ref args, .. } - | Insn::SendForward { recv, ref args, .. } - | Insn::InvokeSuper { recv, ref args, .. } - | Insn::CCallVariadic { recv, ref args, .. } => { - self.assert_subtype(insn_id, recv, types::BasicObject)?; - for &arg in args { - self.assert_subtype(insn_id, arg, types::BasicObject)?; - } - Ok(()) + Insn::BoxBool { val } + | Insn::IfTrue { val, .. } + | Insn::IfFalse { val, .. } => { + self.assert_subtype(insn_id, val, types::CBool) } - Insn::CCallWithFrame { ref args, .. } - | Insn::InvokeBuiltin { ref args, .. } - | Insn::InvokeBlock { ref args, .. } => { - for &arg in args { - self.assert_subtype(insn_id, arg, types::BasicObject)?; - } - Ok(()) + Insn::BoxFixnum { val, .. } => self.assert_subtype(insn_id, val, types::CInt64), + Insn::UnboxFixnum { val } => { + self.assert_subtype(insn_id, val, types::Fixnum) } - Insn::Return { val } => self.assert_subtype(insn_id, val, types::BasicObject), - Insn::Throw { val, .. } => self.assert_subtype(insn_id, val, types::BasicObject), Insn::FixnumAdd { left, right, .. } | Insn::FixnumSub { left, right, .. } | Insn::FixnumMult { left, right, .. } @@ -3927,17 +3997,11 @@ impl Function { | Insn::FixnumAnd { left, right } | Insn::FixnumOr { left, right } | Insn::FixnumXor { left, right } + | Insn::NewRangeFixnum { low: left, high: right, .. } => { self.assert_subtype(insn_id, left, types::Fixnum)?; self.assert_subtype(insn_id, right, types::Fixnum) } - Insn::ObjToString { val, .. } => self.assert_subtype(insn_id, val, types::BasicObject), - Insn::AnyToString { val, str, .. } => { - self.assert_subtype(insn_id, val, types::BasicObject)?; - self.assert_subtype(insn_id, str, types::BasicObject) - } - Insn::GuardType { val, .. } => self.assert_subtype(insn_id, val, types::BasicObject), - Insn::GuardTypeNot { val, .. } => self.assert_subtype(insn_id, val, types::BasicObject), Insn::GuardBitEquals { val, expected, .. } => { match expected { Const::Value(_) => self.assert_subtype(insn_id, val, types::RubyValue), @@ -3954,9 +4018,8 @@ impl Function { Const::CPtr(_) => self.assert_subtype(insn_id, val, types::CPtr), } } - Insn::GuardShape { val, .. } => self.assert_subtype(insn_id, val, types::BasicObject), - Insn::GuardNotFrozen { val, .. } => self.assert_subtype(insn_id, val, types::BasicObject), - Insn::GuardLess { left, right, .. } | Insn::GuardGreaterEq { left, right, .. } => { + Insn::GuardLess { left, right, .. } + | Insn::GuardGreaterEq { left, right, .. } => { self.assert_subtype(insn_id, left, types::CInt64)?; self.assert_subtype(insn_id, right, types::CInt64) }, @@ -3969,7 +4032,6 @@ impl Function { self.assert_subtype(insn_id, index, types::Fixnum)?; self.assert_subtype(insn_id, value, types::Fixnum) } - _ => Ok(()), } } From f84bbb423836d9d0d018b8ab71ecceb5868fd5be Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Mon, 17 Nov 2025 08:15:16 -0800 Subject: [PATCH 03/15] ZJIT: add support for lazy `RubyVM::ZJIT.enable` This implements Shopify#854: - Splits boot-time and enable-time initialization, tracks progress with `InitializationState` enum - Introduces `RubyVM::ZJIT.enable` Ruby method for enabling the JIT lazily, if not already enabled - Introduces `--zjit-disable` flag, which can be used alongside the other `--zjit-*` flags but prevents enabling the JIT at boot time - Adds ZJIT infra to support JIT hooks, but this is not currently exercised (Shopify/ruby#667) Left for future enhancements: - Support kwargs for overriding the CLI flags in `RubyVM::ZJIT.enable` Closes Shopify#854 --- jit_hook.rb | 5 +- ruby.c | 19 ++++---- test/ruby/test_zjit.rb | 38 +++++++++++++++ version.c | 9 ++++ zjit.c | 1 + zjit.rb | 25 ++++++++++ zjit/src/options.rs | 25 ++++++---- zjit/src/state.rs | 103 ++++++++++++++++++++++++++++++++++++----- 8 files changed, 194 insertions(+), 31 deletions(-) diff --git a/jit_hook.rb b/jit_hook.rb index 487361c049ed32..346b7169480031 100644 --- a/jit_hook.rb +++ b/jit_hook.rb @@ -3,9 +3,8 @@ class Module # This method is removed in jit_undef.rb. private def with_jit(&block) # :nodoc: # ZJIT currently doesn't compile Array#each properly, so it's disabled for now. - if defined?(RubyVM::ZJIT) && Primitive.rb_zjit_option_enabled_p && false # TODO: remove `&& false` (Shopify/ruby#667) - # We don't support lazily enabling ZJIT yet, so we can call the block right away. - block.call + if defined?(RubyVM::ZJIT) && false # TODO: remove `&& false` (Shopify/ruby#667) + RubyVM::ZJIT.send(:add_jit_hook, block) elsif defined?(RubyVM::YJIT) RubyVM::YJIT.send(:add_jit_hook, block) end diff --git a/ruby.c b/ruby.c index 872a317e3bbf7e..f1089ca41e9173 100644 --- a/ruby.c +++ b/ruby.c @@ -1842,10 +1842,8 @@ ruby_opt_init(ruby_cmdline_options_t *opt) rb_yjit_init(opt->yjit); #endif #if USE_ZJIT - if (opt->zjit) { - extern void rb_zjit_init(void); - rb_zjit_init(); - } + extern void rb_zjit_init(bool); + rb_zjit_init(opt->zjit); #endif ruby_set_script_name(opt->script_name); @@ -2368,6 +2366,12 @@ process_options(int argc, char **argv, ruby_cmdline_options_t *opt) #if USE_ZJIT if (!FEATURE_USED_P(opt->features, zjit) && env_var_truthy("RUBY_ZJIT_ENABLE")) { FEATURE_SET(opt->features, FEATURE_BIT(zjit)); + + // When the --zjit flag is specified, we would have call setup_zjit_options(""), + // which would have called rb_zjit_prepare_options() internally. This ensures we + // go through the same set up but with less overhead than setup_zjit_options(""). + extern void rb_zjit_prepare_options(); + rb_zjit_prepare_options(); } #endif } @@ -2383,10 +2387,9 @@ process_options(int argc, char **argv, ruby_cmdline_options_t *opt) } #endif #if USE_ZJIT - if (FEATURE_SET_P(opt->features, zjit) && !opt->zjit) { - extern void rb_zjit_prepare_options(void); - rb_zjit_prepare_options(); - opt->zjit = true; + if (FEATURE_SET_P(opt->features, zjit)) { + bool rb_zjit_option_enable(void); + opt->zjit = rb_zjit_option_enable(); // set opt->zjit for Init_ruby_description() and calling rb_zjit_init() } #endif diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 13ee4a45c83188..2e04dbddd55b21 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -59,6 +59,44 @@ def test_enable_through_env end end + def test_zjit_enable + assert_separately([], <<~'RUBY') + refute_predicate RubyVM::ZJIT, :enabled? + refute_predicate RubyVM::ZJIT, :stats_enabled? + refute_includes RUBY_DESCRIPTION, "+ZJIT" + + RubyVM::ZJIT.enable + + assert_predicate RubyVM::ZJIT, :enabled? + refute_predicate RubyVM::ZJIT, :stats_enabled? + assert_includes RUBY_DESCRIPTION, "+ZJIT" + RUBY + end + + def test_zjit_disable + assert_separately(["--zjit", "--zjit-disable"], <<~'RUBY') + refute_predicate RubyVM::ZJIT, :enabled? + refute_includes RUBY_DESCRIPTION, "+ZJIT" + + RubyVM::ZJIT.enable + + assert_predicate RubyVM::ZJIT, :enabled? + assert_includes RUBY_DESCRIPTION, "+ZJIT" + RUBY + end + + def test_zjit_enable_respects_existing_options + assert_separately(['--zjit-disable', '--zjit-stats=quiet'], <<~RUBY) + refute_predicate RubyVM::ZJIT, :enabled? + assert_predicate RubyVM::ZJIT, :stats_enabled? + + RubyVM::ZJIT.enable + + assert_predicate RubyVM::ZJIT, :enabled? + assert_predicate RubyVM::ZJIT, :stats_enabled? + RUBY + end + def test_call_itself assert_compiles '42', <<~RUBY, call_threshold: 2 def test = 42.itself diff --git a/version.c b/version.c index b9c57a71b82b17..366ba1b2b5c6b7 100644 --- a/version.c +++ b/version.c @@ -276,6 +276,15 @@ ruby_set_yjit_description(void) define_ruby_description(YJIT_DESCRIPTION); } +void +ruby_set_zjit_description(void) +{ + VALUE mRuby = rb_path2class("Ruby"); + rb_const_remove(rb_cObject, rb_intern("RUBY_DESCRIPTION")); + rb_const_remove(mRuby, rb_intern("DESCRIPTION")); + define_ruby_description(ZJIT_DESCRIPTION); +} + void ruby_show_version(void) { diff --git a/zjit.c b/zjit.c index d1f192801a2ec8..b8a89aad1a8498 100644 --- a/zjit.c +++ b/zjit.c @@ -305,6 +305,7 @@ rb_zjit_class_has_default_allocator(VALUE klass) VALUE rb_vm_get_untagged_block_handler(rb_control_frame_t *reg_cfp); // Primitives used by zjit.rb. Don't put other functions below, which wouldn't use them. +VALUE rb_zjit_enable(rb_execution_context_t *ec, VALUE self); VALUE rb_zjit_assert_compiles(rb_execution_context_t *ec, VALUE self); VALUE rb_zjit_stats(rb_execution_context_t *ec, VALUE self, VALUE target_key); VALUE rb_zjit_reset_stats_bang(rb_execution_context_t *ec, VALUE self); diff --git a/zjit.rb b/zjit.rb index 7cdf84cfbe4328..f5ed347f2cb6ab 100644 --- a/zjit.rb +++ b/zjit.rb @@ -7,6 +7,8 @@ # This module may not exist if ZJIT does not support the particular platform # for which CRuby is built. module RubyVM::ZJIT + # Blocks that are called when YJIT is enabled + @jit_hooks = [] # Avoid calling a Ruby method here to avoid interfering with compilation tests if Primitive.rb_zjit_print_stats_p at_exit { print_stats } @@ -22,6 +24,18 @@ def enabled? Primitive.cexpr! 'RBOOL(rb_zjit_enabled_p)' end + # Enable ZJIT compilation. + def enable + return false if enabled? + + if Primitive.cexpr! 'RBOOL(rb_yjit_enabled_p)' + warn("Only one JIT can be enabled at the same time.") + return false + end + + Primitive.rb_zjit_enable + end + # Check if `--zjit-trace-exits` is used def trace_exit_locations_enabled? Primitive.rb_zjit_trace_exit_locations_enabled_p @@ -234,6 +248,17 @@ def assert_compiles # :nodoc: # :stopdoc: private + # Register a block to be called when ZJIT is enabled + def add_jit_hook(hook) + @jit_hooks << hook + end + + # Run ZJIT hooks registered by `#with_jit` + def call_jit_hooks + @jit_hooks.each(&:call) + @jit_hooks.clear + end + def print_counters(keys, buf:, stats:, right_align: false, base: nil) key_pad = keys.map { |key| key.to_s.sub(/_time_ns\z/, '_time').size }.max + 1 key_align = '-' unless right_align diff --git a/zjit/src/options.rs b/zjit/src/options.rs index cd3a6439719b3f..c165035eaa1af0 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -45,7 +45,7 @@ pub struct Options { /// Number of times YARV instructions should be profiled. pub num_profiles: NumProfiles, - /// Enable YJIT statsitics + /// Enable ZJIT statistics pub stats: bool, /// Print stats on exit (when stats is also true) @@ -54,6 +54,10 @@ pub struct Options { /// Enable debug logging pub debug: bool, + // Whether to enable JIT at boot. This option prevents other + // ZJIT tuning options from enabling ZJIT at boot. + pub disable: bool, + /// Turn off the HIR optimizer pub disable_hir_opt: bool, @@ -97,6 +101,7 @@ impl Default for Options { stats: false, print_stats: false, debug: false, + disable: false, disable_hir_opt: false, dump_hir_init: None, dump_hir_opt: None, @@ -123,6 +128,8 @@ pub const ZJIT_OPTIONS: &[(&str, &str)] = &[ ("--zjit-num-profiles=num", "Number of profiled calls before JIT (default: 5)."), ("--zjit-stats[=quiet]", "Enable collecting ZJIT statistics (=quiet to suppress output)."), + ("--zjit-disable", + "Disable ZJIT for lazily enabling it with RubyVM::ZJIT.enable."), ("--zjit-perf", "Dump ISEQ symbols into /tmp/perf-{}.map for Linux perf."), ("--zjit-log-compiled-iseqs=path", "Log compiled ISEQs to the file. The file will be truncated."), @@ -175,7 +182,7 @@ const DUMP_LIR_ALL: &[DumpLIR] = &[ DumpLIR::scratch_split, ]; -/// Mamximum value for --zjit-mem-size/--zjit-exec-mem-size in MiB. +/// Maximum value for --zjit-mem-size/--zjit-exec-mem-size in MiB. /// We set 1TiB just to avoid overflow. We could make it smaller. const MAX_MEM_MIB: usize = 1024 * 1024; @@ -319,6 +326,8 @@ fn parse_option(str_ptr: *const std::os::raw::c_char) -> Option<()> { ("debug", "") => options.debug = true, + ("disable", "") => options.disable = true, + ("disable-hir-opt", "") => options.disable_hir_opt = true, // --zjit-dump-hir dumps the actual input to the codegen, which is currently the same as --zjit-dump-hir-opt. @@ -442,15 +451,13 @@ macro_rules! debug { } pub(crate) use debug; -/// Return Qtrue if --zjit* has been specified. For the `#with_jit` hook, -/// this becomes Qtrue before ZJIT is actually initialized and enabled. +/// Return true if ZJIT should be enabled at boot. #[unsafe(no_mangle)] -pub extern "C" fn rb_zjit_option_enabled_p(_ec: EcPtr, _self: VALUE) -> VALUE { - // If any --zjit* option is specified, OPTIONS becomes Some. - if unsafe { OPTIONS.is_some() } { - Qtrue +pub extern "C" fn rb_zjit_option_enable() -> bool { + if unsafe { OPTIONS.as_ref() }.is_some_and(|opts| !opts.disable) { + true } else { - Qfalse + false } } diff --git a/zjit/src/state.rs b/zjit/src/state.rs index 3cb60cffcb6c52..06296eb8f20d08 100644 --- a/zjit/src/state.rs +++ b/zjit/src/state.rs @@ -1,11 +1,11 @@ //! Runtime state of ZJIT. use crate::codegen::{gen_entry_trampoline, gen_exit_trampoline, gen_exit_trampoline_with_counter, gen_function_stub_hit_trampoline}; -use crate::cruby::{self, rb_bug_panic_hook, rb_vm_insn_count, EcPtr, Qnil, rb_vm_insn_addr2opcode, rb_profile_frames, VALUE, VM_INSTRUCTION_SIZE, size_t, rb_gc_mark}; +use crate::cruby::{self, rb_bug_panic_hook, rb_vm_insn_count, src_loc, EcPtr, Qnil, Qtrue, rb_vm_insn_addr2opcode, rb_profile_frames, VALUE, VM_INSTRUCTION_SIZE, size_t, rb_gc_mark, with_vm_lock}; use crate::cruby_methods; use crate::invariants::Invariants; use crate::asm::CodeBlock; -use crate::options::get_option; +use crate::options::{get_option, rb_zjit_prepare_options}; use crate::stats::{Counters, InsnCounters, SideExitLocations}; use crate::virtualmem::CodePtr; use std::collections::HashMap; @@ -63,12 +63,42 @@ pub struct ZJITState { exit_locations: Option, } +/// Tracks the initialization progress +enum InitializationState { + Uninitialized, + + /// At boot time, rb_zjit_init will be called regardless of whether + /// ZJIT is enabled, in this phase we initialize any states that must + /// be captured at during boot. + Initialized(cruby_methods::Annotations), + + /// When ZJIT is enabled, either during boot with `--zjit`, or lazily + /// at a later time with `RubyVM::ZJIT.enable`, we perform the rest + /// of the initialization steps and produce the `ZJITState` instance. + Enabled(ZJITState), + + /// Indicates that ZJITState::init has panicked. Should never be + /// encountered in practice since we abort immediately when that + /// happens. + Panicked, +} + /// Private singleton instance of the codegen globals -static mut ZJIT_STATE: Option = None; +static mut ZJIT_STATE: InitializationState = InitializationState::Uninitialized; impl ZJITState { /// Initialize the ZJIT globals. Return the address of the JIT entry trampoline. pub fn init() -> *const u8 { + use InitializationState::*; + + let initialization_state = unsafe { + std::mem::replace(&mut ZJIT_STATE, Panicked) + }; + + let Initialized(method_annotations) = initialization_state else { + panic!("rb_zjit_init was never called"); + }; + let mut cb = { use crate::options::*; use crate::virtualmem::*; @@ -99,7 +129,7 @@ impl ZJITState { send_fallback_counters: [0; VM_INSTRUCTION_SIZE as usize], invariants: Invariants::default(), assert_compiles: false, - method_annotations: cruby_methods::init(), + method_annotations, exit_trampoline, function_stub_hit_trampoline, exit_trampoline_with_counter: exit_trampoline, @@ -107,7 +137,7 @@ impl ZJITState { not_annotated_frame_cfunc_counter_pointers: HashMap::new(), exit_locations, }; - unsafe { ZJIT_STATE = Some(zjit_state); } + unsafe { ZJIT_STATE = Enabled(zjit_state); } // With --zjit-stats, use a different trampoline on function stub exits // to count exit_compilation_failure. Note that the trampoline code depends @@ -123,12 +153,16 @@ impl ZJITState { /// Return true if zjit_state has been initialized pub fn has_instance() -> bool { - unsafe { ZJIT_STATE.as_mut().is_some() } + matches!(unsafe { &ZJIT_STATE }, InitializationState::Enabled(_)) } /// Get a mutable reference to the codegen globals instance fn get_instance() -> &'static mut ZJITState { - unsafe { ZJIT_STATE.as_mut().unwrap() } + if let InitializationState::Enabled(instance) = unsafe { &mut ZJIT_STATE } { + instance + } else { + panic!("ZJITState::get_instance called when ZJIT is not enabled") + } } /// Get a mutable reference to the inline code block @@ -249,14 +283,39 @@ impl ZJITState { } } -/// Initialize ZJIT +/// Initialize ZJIT at boot. This is called even if ZJIT is disabled. #[unsafe(no_mangle)] -pub extern "C" fn rb_zjit_init() { +pub extern "C" fn rb_zjit_init(zjit_enabled: bool) { + use InitializationState::*; + + debug_assert!( + matches!(unsafe { &ZJIT_STATE }, Uninitialized), + "rb_zjit_init should only be called once during boot", + ); + + // Initialize IDs and method annotations. + // cruby_methods::init() must be called at boot, + // as cmes could have been re-defined after boot. + cruby::ids::init(); + + let method_annotations = cruby_methods::init(); + + unsafe { ZJIT_STATE = Initialized(method_annotations); } + + // If --zjit, enable ZJIT immediately + if zjit_enabled { + zjit_enable(); + } +} + +/// Enable ZJIT compilation. +fn zjit_enable() { + // TODO: call RubyVM::ZJIT::call_jit_hooks here + // Catch panics to avoid UB for unwinding into C frames. // See https://doc.rust-lang.org/nomicon/exception-safety.html let result = std::panic::catch_unwind(|| { // Initialize ZJIT states - cruby::ids::init(); let zjit_entry = ZJITState::init(); // Install a panic hook for ZJIT @@ -271,11 +330,33 @@ pub extern "C" fn rb_zjit_init() { }); if result.is_err() { - println!("ZJIT: zjit_init() panicked. Aborting."); + println!("ZJIT: zjit_enable() panicked. Aborting."); std::process::abort(); } } +/// Enable ZJIT compilation, returning Qtrue if ZJIT was previously disabled +#[unsafe(no_mangle)] +pub extern "C" fn rb_zjit_enable(_ec: EcPtr, _self: VALUE) -> VALUE { + with_vm_lock(src_loc!(), || { + // Options would not have been initialized during boot if no flags were specified + rb_zjit_prepare_options(); + + // Initialize and enable ZJIT + zjit_enable(); + + // Add "+ZJIT" to RUBY_DESCRIPTION + unsafe { + unsafe extern "C" { + fn ruby_set_zjit_description(); + } + ruby_set_zjit_description(); + } + + Qtrue + }) +} + /// Assert that any future ZJIT compilation will return a function pointer (not fail to compile) #[unsafe(no_mangle)] pub extern "C" fn rb_zjit_assert_compiles(_ec: EcPtr, _self: VALUE) -> VALUE { From 0e10dfded0498cf71efb9fc61a804db6db540009 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Tue, 4 Nov 2025 20:37:57 +0100 Subject: [PATCH 04/15] ZJIT: Inline setting Struct fields * Add Insn::StoreField and Insn::WriteBarrier --- test/ruby/test_zjit.rb | 19 ++++++ zjit/src/codegen.rs | 17 ++++++ zjit/src/hir.rs | 122 +++++++++++++++++++++++--------------- zjit/src/hir/opt_tests.rs | 65 ++++++++++++++++++++ 4 files changed, 174 insertions(+), 49 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 2e04dbddd55b21..64372c231cf26f 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -2957,6 +2957,25 @@ def test } end + def test_struct_set + assert_compiles '[42, 42, :frozen_error]', %q{ + C = Struct.new(:foo).new(1) + + def test + C.foo = Object.new + 42 + end + + r = [test, test] + C.freeze + r << begin + test + rescue FrozenError + :frozen_error + end + }, call_threshold: 2 + end + def test_global_tracepoint assert_compiles 'true', %q{ def foo = 1 diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 7d72acfe14056f..fd72ea8a47f5f8 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -460,6 +460,8 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::LoadPC => gen_load_pc(asm), Insn::LoadSelf => gen_load_self(), &Insn::LoadField { recv, id, offset, return_type: _ } => gen_load_field(asm, opnd!(recv), id, offset), + &Insn::StoreField { recv, id, offset, val } => no_output!(gen_store_field(asm, opnd!(recv), id, offset, opnd!(val))), + &Insn::WriteBarrier { recv, val } => no_output!(gen_write_barrier(asm, opnd!(recv), opnd!(val), function.type_of(val))), &Insn::IsBlockGiven => gen_is_block_given(jit, asm), Insn::ArrayInclude { elements, target, state } => gen_array_include(jit, asm, opnds!(elements), opnd!(target), &function.frame_state(*state)), &Insn::DupArrayInclude { ary, target, state } => gen_dup_array_include(jit, asm, ary, opnd!(target), &function.frame_state(state)), @@ -1042,6 +1044,21 @@ fn gen_load_field(asm: &mut Assembler, recv: Opnd, id: ID, offset: i32) -> Opnd asm.load(Opnd::mem(64, recv, offset)) } +fn gen_store_field(asm: &mut Assembler, recv: Opnd, id: ID, offset: i32, val: Opnd) { + asm_comment!(asm, "Store field id={} offset={}", id.contents_lossy(), offset); + let recv = asm.load(recv); + asm.store(Opnd::mem(64, recv, offset), val); +} + +fn gen_write_barrier(asm: &mut Assembler, recv: Opnd, val: Opnd, val_type: Type) { + // See RB_OBJ_WRITE/rb_obj_write: it's just assignment and rb_obj_written()->rb_gc_writebarrier() + if !val_type.is_immediate() { + asm_comment!(asm, "Write barrier"); + let recv = asm.load(recv); + asm_ccall!(asm, rb_gc_writebarrier, recv, val); + } +} + /// Compile an interpreter entry block to be inserted into an ISEQ fn gen_entry_prologue(asm: &mut Assembler) { asm_comment!(asm, "ZJIT entry trampoline"); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index d68ddd2479ebd0..e1a61b23995eff 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -721,6 +721,10 @@ pub enum Insn { /// Load cfp->self LoadSelf, LoadField { recv: InsnId, id: ID, offset: i32, return_type: Type }, + /// Write `val` at an offset of `recv`. + /// When writing a Ruby object to a Ruby object, one must use GuardNotFrozen (or equivalent) before and WriteBarrier after. + StoreField { recv: InsnId, id: ID, offset: i32, val: InsnId }, + WriteBarrier { recv: InsnId, val: InsnId }, /// Get a local variable from a higher scope or the heap. /// If `use_sp` is true, it uses the SP register to optimize the read. @@ -908,7 +912,7 @@ impl Insn { | Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::SetClassVar { .. } | Insn::ArrayExtend { .. } | Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetGlobal { .. } | Insn::SetLocal { .. } | Insn::Throw { .. } | Insn::IncrCounter(_) | Insn::IncrCounterPtr { .. } - | Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. } | Insn::SetInstanceVariable { .. } => false, + | Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. } | Insn::SetInstanceVariable { .. } | Insn::StoreField { .. } | Insn::WriteBarrier { .. } => false, _ => true, } } @@ -1241,6 +1245,8 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::LoadPC => write!(f, "LoadPC"), Insn::LoadSelf => write!(f, "LoadSelf"), &Insn::LoadField { recv, id, offset, return_type: _ } => write!(f, "LoadField {recv}, :{}@{:p}", id.contents_lossy(), self.ptr_map.map_offset(offset)), + &Insn::StoreField { recv, id, offset, val } => write!(f, "StoreField {recv}, :{}@{:p}, {val}", id.contents_lossy(), self.ptr_map.map_offset(offset)), + &Insn::WriteBarrier { recv, val } => write!(f, "WriteBarrier {recv}, {val}"), Insn::SetIvar { self_val, id, val, .. } => write!(f, "SetIvar {self_val}, :{}, {val}", id.contents_lossy()), Insn::SetInstanceVariable { self_val, id, val, .. } => write!(f, "SetInstanceVariable {self_val}, :{}, {val}", id.contents_lossy()), Insn::GetGlobal { id, .. } => write!(f, "GetGlobal :{}", id.contents_lossy()), @@ -1888,6 +1894,8 @@ impl Function { &SetGlobal { id, val, state } => SetGlobal { id, val: find!(val), state }, &GetIvar { self_val, id, state } => GetIvar { self_val: find!(self_val), id, state }, &LoadField { recv, id, offset, return_type } => LoadField { recv: find!(recv), id, offset, return_type }, + &StoreField { recv, id, offset, val } => StoreField { recv: find!(recv), id, offset, val: find!(val) }, + &WriteBarrier { recv, val } => WriteBarrier { recv: find!(recv), val: find!(val) }, &SetIvar { self_val, id, val, state } => SetIvar { self_val: find!(self_val), id, val: find!(val), state }, &SetInstanceVariable { self_val, id, ic, val, state } => SetInstanceVariable { self_val: find!(self_val), id, ic, val: find!(val), state }, &GetClassVar { id, ic, state } => GetClassVar { id, ic, state }, @@ -1944,8 +1952,8 @@ impl Function { | Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::SetClassVar { .. } | Insn::ArrayExtend { .. } | Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetLocal { .. } | Insn::IncrCounter(_) | Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. } | Insn::IncrCounterPtr { .. } - | Insn::SetInstanceVariable { .. } => - panic!("Cannot infer type of instruction with no output: {}", self.insns[insn.0]), + | Insn::SetInstanceVariable { .. } | Insn::StoreField { .. } | Insn::WriteBarrier { .. } => + panic!("Cannot infer type of instruction with no output: {}. See Insn::has_output().", self.insns[insn.0]), Insn::Const { val: Const::Value(val) } => Type::from_value(*val), Insn::Const { val: Const::CBool(val) } => Type::from_cbool(*val), Insn::Const { val: Const::CInt8(val) } => Type::from_cint(types::CInt8, *val as i64), @@ -2463,53 +2471,62 @@ impl Function { self.push_insn(block, Insn::SetIvar { self_val: recv, id, val, state }); self.make_equal_to(insn_id, val); } else if def_type == VM_METHOD_TYPE_OPTIMIZED { - let opt_type = unsafe { get_cme_def_body_optimized_type(cme) }; - if opt_type == OPTIMIZED_METHOD_TYPE_STRUCT_AREF { - if unsafe { vm_ci_argc(ci) } != 0 { - self.push_insn_id(block, insn_id); continue; - } - let index: i32 = unsafe { get_cme_def_body_optimized_index(cme) } - .try_into() - .unwrap(); - // We are going to use an encoding that takes a 4-byte immediate which - // limits the offset to INT32_MAX. - { - let native_index = (index as i64) * (SIZEOF_VALUE as i64); - if native_index > (i32::MAX as i64) { + let opt_type: OptimizedMethodType = unsafe { get_cme_def_body_optimized_type(cme) }.into(); + match (opt_type, args.as_slice()) { + (OptimizedMethodType::StructAref, &[]) | (OptimizedMethodType::StructAset, &[_]) => { + let index: i32 = unsafe { get_cme_def_body_optimized_index(cme) } + .try_into() + .unwrap(); + // We are going to use an encoding that takes a 4-byte immediate which + // limits the offset to INT32_MAX. + { + let native_index = (index as i64) * (SIZEOF_VALUE as i64); + if native_index > (i32::MAX as i64) { + self.push_insn_id(block, insn_id); continue; + } + } + // Get the profiled type to check if the fields is embedded or heap allocated. + let Some(is_embedded) = self.profiled_type_of_at(recv, frame_state.insn_idx).map(|t| t.flags().is_struct_embedded()) else { + // No (monomorphic/skewed polymorphic) profile info self.push_insn_id(block, insn_id); continue; + }; + self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); + if klass.instance_can_have_singleton_class() { + self.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass }, state }); } - } - // Get the profiled type to check if the fields is embedded or heap allocated. - let Some(is_embedded) = self.profiled_type_of_at(recv, frame_state.insn_idx).map(|t| t.flags().is_struct_embedded()) else { - // No (monomorphic/skewed polymorphic) profile info + if let Some(profiled_type) = profiled_type { + recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); + } + // All structs from the same Struct class should have the same + // length. So if our recv is embedded all runtime + // structs of the same class should be as well, and the same is + // true of the converse. + // + // No need for a GuardShape. + let (target, offset) = if is_embedded { + let offset = RUBY_OFFSET_RSTRUCT_AS_ARY + (SIZEOF_VALUE_I32 * index); + (recv, offset) + } else { + let as_heap = self.push_insn(block, Insn::LoadField { recv, id: ID!(_as_heap), offset: RUBY_OFFSET_RSTRUCT_AS_HEAP_PTR, return_type: types::CPtr }); + let offset = SIZEOF_VALUE_I32 * index; + (as_heap, offset) + }; + + let replacement = if let (OptimizedMethodType::StructAset, &[val]) = (opt_type, args.as_slice()) { + self.push_insn(block, Insn::GuardNotFrozen { val: recv, state }); + self.push_insn(block, Insn::StoreField { recv: target, id: mid, offset, val }); + self.push_insn(block, Insn::WriteBarrier { recv, val }); + val + } else { // StructAref + self.push_insn(block, Insn::LoadField { recv: target, id: mid, offset, return_type: types::BasicObject }) + }; + self.make_equal_to(insn_id, replacement); + }, + _ => { + self.set_dynamic_send_reason(insn_id, SendWithoutBlockNotOptimizedMethodTypeOptimized(OptimizedMethodType::from(opt_type))); self.push_insn_id(block, insn_id); continue; - }; - self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); - if klass.instance_can_have_singleton_class() { - self.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass }, state }); - } - if let Some(profiled_type) = profiled_type { - recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); - } - // All structs from the same Struct class should have the same - // length. So if our recv is embedded all runtime - // structs of the same class should be as well, and the same is - // true of the converse. - // - // No need for a GuardShape. - let replacement = if is_embedded { - let offset = RUBY_OFFSET_RSTRUCT_AS_ARY + (SIZEOF_VALUE_I32 * index); - self.push_insn(block, Insn::LoadField { recv, id: mid, offset, return_type: types::BasicObject }) - } else { - let as_heap = self.push_insn(block, Insn::LoadField { recv, id: ID!(_as_heap), offset: RUBY_OFFSET_RSTRUCT_AS_HEAP_PTR, return_type: types::CPtr }); - let offset = SIZEOF_VALUE_I32 * index; - self.push_insn(block, Insn::LoadField { recv: as_heap, id: mid, offset, return_type: types::BasicObject }) - }; - self.make_equal_to(insn_id, replacement); - } else { - self.set_dynamic_send_reason(insn_id, SendWithoutBlockNotOptimizedMethodTypeOptimized(OptimizedMethodType::from(opt_type))); - self.push_insn_id(block, insn_id); continue; - } + }, + }; } else { self.set_dynamic_send_reason(insn_id, SendWithoutBlockNotOptimizedMethodType(MethodType::from(def_type))); self.push_insn_id(block, insn_id); continue; @@ -3511,6 +3528,11 @@ impl Function { &Insn::LoadField { recv, .. } => { worklist.push_back(recv); } + &Insn::StoreField { recv, val, .. } + | &Insn::WriteBarrier { recv, val } => { + worklist.push_back(recv); + worklist.push_back(val); + } &Insn::GuardBlockParamProxy { state, .. } | &Insn::GetGlobal { state, .. } | &Insn::GetSpecialSymbol { state, .. } | @@ -3851,7 +3873,8 @@ impl Function { | Insn::GetClassVar { .. } | Insn::GetSpecialNumber { .. } | Insn::GetSpecialSymbol { .. } - | Insn::GetLocal { .. } => { + | Insn::GetLocal { .. } + | Insn::StoreField { .. } => { Ok(()) } // Instructions with 1 Ruby object operand @@ -3882,7 +3905,8 @@ impl Function { Insn::SetIvar { self_val: left, val: right, .. } | Insn::SetInstanceVariable { self_val: left, val: right, .. } | Insn::NewRange { low: left, high: right, .. } - | Insn::AnyToString { val: left, str: right, .. } => { + | Insn::AnyToString { val: left, str: right, .. } + | Insn::WriteBarrier { recv: left, val: right } => { self.assert_subtype(insn_id, left, types::BasicObject)?; self.assert_subtype(insn_id, right, types::BasicObject) } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 2c56120bf621be..be770553767f0c 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -5335,6 +5335,71 @@ mod hir_opt_tests { "); } + #[test] + fn test_inline_struct_aset_embedded() { + eval(r#" + C = Struct.new(:foo) + def test(o, v) = o.foo = v + value = Object.new + test C.new, value + test C.new, value + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@5 + v3:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2, v3) + bb1(v6:BasicObject, v7:BasicObject, v8:BasicObject): + EntryPoint JIT(0) + Jump bb2(v6, v7, v8) + bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): + PatchPoint MethodRedefined(C@0x1000, foo=@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(C@0x1000) + v29:HeapObject[class_exact:C] = GuardType v11, HeapObject[class_exact:C] + v30:HeapObject[class_exact:C] = GuardNotFrozen v29 + StoreField v29, :foo=@0x1038, v12 + WriteBarrier v29, v12 + CheckInterrupts + Return v12 + "); + } + + #[test] + fn test_inline_struct_aset_heap() { + eval(r#" + C = Struct.new(*(0..1000).map {|i| :"a#{i}"}, :foo) + def test(o, v) = o.foo = v + value = Object.new + test C.new, value + test C.new, value + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@5 + v3:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2, v3) + bb1(v6:BasicObject, v7:BasicObject, v8:BasicObject): + EntryPoint JIT(0) + Jump bb2(v6, v7, v8) + bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): + PatchPoint MethodRedefined(C@0x1000, foo=@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(C@0x1000) + v29:HeapObject[class_exact:C] = GuardType v11, HeapObject[class_exact:C] + v30:CPtr = LoadField v29, :_as_heap@0x1038 + v31:HeapObject[class_exact:C] = GuardNotFrozen v29 + StoreField v30, :foo=@0x1039, v12 + WriteBarrier v29, v12 + CheckInterrupts + Return v12 + "); + } + #[test] fn test_array_reverse_returns_array() { eval(r#" From 79633437e1a971abd5dda54dc584eec3adb4e7a7 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Tue, 18 Nov 2025 16:41:15 +0100 Subject: [PATCH 05/15] ZJIT: Rename the operand of Insn::GuardNotFrozen from val to recv * When writing to an object, the receiver should be checked if it's frozen, not the value, so this avoids an error-prone autocomplete. --- zjit/src/codegen.rs | 8 ++++---- zjit/src/cruby_methods.rs | 4 ++-- zjit/src/hir.rs | 14 +++++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index fd72ea8a47f5f8..8fc66791a665ad 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -415,7 +415,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::GuardTypeNot { val, guard_type, state } => gen_guard_type_not(jit, asm, opnd!(val), *guard_type, &function.frame_state(*state)), Insn::GuardBitEquals { val, expected, state } => gen_guard_bit_equals(jit, asm, opnd!(val), *expected, &function.frame_state(*state)), &Insn::GuardBlockParamProxy { level, state } => no_output!(gen_guard_block_param_proxy(jit, asm, level, &function.frame_state(state))), - Insn::GuardNotFrozen { val, state } => gen_guard_not_frozen(jit, asm, opnd!(val), &function.frame_state(*state)), + Insn::GuardNotFrozen { recv, state } => gen_guard_not_frozen(jit, asm, opnd!(recv), &function.frame_state(*state)), &Insn::GuardLess { left, right, state } => gen_guard_less(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)), &Insn::GuardGreaterEq { left, right, state } => gen_guard_greater_eq(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)), Insn::PatchPoint { invariant, state } => no_output!(gen_patch_point(jit, asm, invariant, &function.frame_state(*state))), @@ -645,12 +645,12 @@ fn gen_guard_block_param_proxy(jit: &JITState, asm: &mut Assembler, level: u32, asm.jz(side_exit(jit, state, SideExitReason::BlockParamProxyNotIseqOrIfunc)); } -fn gen_guard_not_frozen(jit: &JITState, asm: &mut Assembler, val: Opnd, state: &FrameState) -> Opnd { - let ret = asm_ccall!(asm, rb_obj_frozen_p, val); +fn gen_guard_not_frozen(jit: &JITState, asm: &mut Assembler, recv: Opnd, state: &FrameState) -> Opnd { + let ret = asm_ccall!(asm, rb_obj_frozen_p, recv); asm_comment!(asm, "side-exit if rb_obj_frozen_p returns Qtrue"); asm.cmp(ret, Qtrue.into()); asm.je(side_exit(jit, state, GuardNotFrozen)); - val + recv } fn gen_guard_less(jit: &JITState, asm: &mut Assembler, left: Opnd, right: Opnd, state: &FrameState) -> Opnd { diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 8dc53302835d23..d2d6be2dec8009 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -306,7 +306,7 @@ fn inline_array_push(fun: &mut hir::Function, block: hir::BlockId, recv: hir::In fn inline_array_pop(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { // Only inline the case of no arguments. let &[] = args else { return None; }; - let arr = fun.push_insn(block, hir::Insn::GuardNotFrozen { val: recv, state }); + let arr = fun.push_insn(block, hir::Insn::GuardNotFrozen { recv, state }); Some(fun.push_insn(block, hir::Insn::ArrayPop { array: arr, state })) } @@ -367,7 +367,7 @@ fn inline_string_setbyte(fun: &mut hir::Function, block: hir::BlockId, recv: hir let unboxed_index = fun.push_insn(block, hir::Insn::GuardLess { left: unboxed_index, right: len, state }); let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) }); let _ = fun.push_insn(block, hir::Insn::GuardGreaterEq { left: unboxed_index, right: zero, state }); - let recv = fun.push_insn(block, hir::Insn::GuardNotFrozen { val: recv, state }); + let recv = fun.push_insn(block, hir::Insn::GuardNotFrozen { recv, state }); let _ = fun.push_insn(block, hir::Insn::StringSetbyteFixnum { string: recv, index, value }); // String#setbyte returns the fixnum provided as its `value` argument back to the caller. Some(value) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index e1a61b23995eff..b3c72393f20e32 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -879,7 +879,7 @@ pub enum Insn { /// is neither ISEQ nor ifunc, which makes it incompatible with rb_block_param_proxy. GuardBlockParamProxy { level: u32, state: InsnId }, /// Side-exit if val is frozen. - GuardNotFrozen { val: InsnId, state: InsnId }, + GuardNotFrozen { recv: InsnId, state: InsnId }, /// Side-exit if left is not greater than or equal to right (both operands are C long). GuardGreaterEq { left: InsnId, right: InsnId, state: InsnId }, /// Side-exit if left is not less than right (both operands are C long). @@ -1191,7 +1191,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::GuardBitEquals { val, expected, .. } => { write!(f, "GuardBitEquals {val}, {}", expected.print(self.ptr_map)) }, &Insn::GuardShape { val, shape, .. } => { write!(f, "GuardShape {val}, {:p}", self.ptr_map.map_shape(shape)) }, Insn::GuardBlockParamProxy { level, .. } => write!(f, "GuardBlockParamProxy l{level}"), - Insn::GuardNotFrozen { val, .. } => write!(f, "GuardNotFrozen {val}"), + Insn::GuardNotFrozen { recv, .. } => write!(f, "GuardNotFrozen {recv}"), Insn::GuardLess { left, right, .. } => write!(f, "GuardLess {left}, {right}"), Insn::GuardGreaterEq { left, right, .. } => write!(f, "GuardGreaterEq {left}, {right}"), Insn::PatchPoint { invariant, .. } => { write!(f, "PatchPoint {}", invariant.print(self.ptr_map)) }, @@ -1786,7 +1786,7 @@ impl Function { &GuardBitEquals { val, expected, state } => GuardBitEquals { val: find!(val), expected, state }, &GuardShape { val, shape, state } => GuardShape { val: find!(val), shape, state }, &GuardBlockParamProxy { level, state } => GuardBlockParamProxy { level, state: find!(state) }, - &GuardNotFrozen { val, state } => GuardNotFrozen { val: find!(val), state }, + &GuardNotFrozen { recv, state } => GuardNotFrozen { recv: find!(recv), state }, &GuardGreaterEq { left, right, state } => GuardGreaterEq { left: find!(left), right: find!(right), state }, &GuardLess { left, right, state } => GuardLess { left: find!(left), right: find!(right), state }, &FixnumAdd { left, right, state } => FixnumAdd { left: find!(left), right: find!(right), state }, @@ -2004,7 +2004,7 @@ impl Function { Insn::GuardTypeNot { .. } => types::BasicObject, Insn::GuardBitEquals { val, expected, .. } => self.type_of(*val).intersection(Type::from_const(*expected)), Insn::GuardShape { val, .. } => self.type_of(*val), - Insn::GuardNotFrozen { val, .. } => self.type_of(*val), + Insn::GuardNotFrozen { recv, .. } => self.type_of(*recv), Insn::GuardLess { left, .. } => self.type_of(*left), Insn::GuardGreaterEq { left, .. } => self.type_of(*left), Insn::FixnumAdd { .. } => types::Fixnum, @@ -2513,7 +2513,7 @@ impl Function { }; let replacement = if let (OptimizedMethodType::StructAset, &[val]) = (opt_type, args.as_slice()) { - self.push_insn(block, Insn::GuardNotFrozen { val: recv, state }); + self.push_insn(block, Insn::GuardNotFrozen { recv, state }); self.push_insn(block, Insn::StoreField { recv: target, id: mid, offset, val }); self.push_insn(block, Insn::WriteBarrier { recv, val }); val @@ -3402,7 +3402,7 @@ impl Function { | &Insn::GuardTypeNot { val, state, .. } | &Insn::GuardBitEquals { val, state, .. } | &Insn::GuardShape { val, state, .. } - | &Insn::GuardNotFrozen { val, state } + | &Insn::GuardNotFrozen { recv: val, state } | &Insn::ToArray { val, state } | &Insn::IsMethodCfunc { val, state, .. } | &Insn::ToNewArray { val, state } @@ -3882,7 +3882,7 @@ impl Function { | Insn::IsNil { val } | Insn::IsMethodCfunc { val, .. } | Insn::GuardShape { val, .. } - | Insn::GuardNotFrozen { val, .. } + | Insn::GuardNotFrozen { recv: val, .. } | Insn::SetGlobal { val, .. } | Insn::SetLocal { val, .. } | Insn::SetClassVar { val, .. } From ce73b6c0b6a81c70e5ac4f4e43dea05cd23bab20 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Tue, 18 Nov 2025 17:26:08 +0100 Subject: [PATCH 06/15] ZJIT: Pass the result of GuardNotFrozen to StoreField and WriteBarrier --- zjit/src/hir.rs | 5 ++++- zjit/src/hir/opt_tests.rs | 12 ++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index b3c72393f20e32..c96e0357a037ac 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2503,6 +2503,10 @@ impl Function { // true of the converse. // // No need for a GuardShape. + if let OptimizedMethodType::StructAset = opt_type { + recv = self.push_insn(block, Insn::GuardNotFrozen { recv, state }); + } + let (target, offset) = if is_embedded { let offset = RUBY_OFFSET_RSTRUCT_AS_ARY + (SIZEOF_VALUE_I32 * index); (recv, offset) @@ -2513,7 +2517,6 @@ impl Function { }; let replacement = if let (OptimizedMethodType::StructAset, &[val]) = (opt_type, args.as_slice()) { - self.push_insn(block, Insn::GuardNotFrozen { recv, state }); self.push_insn(block, Insn::StoreField { recv: target, id: mid, offset, val }); self.push_insn(block, Insn::WriteBarrier { recv, val }); val diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index be770553767f0c..e7aaba7fffb73b 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -5360,8 +5360,8 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(C@0x1000) v29:HeapObject[class_exact:C] = GuardType v11, HeapObject[class_exact:C] v30:HeapObject[class_exact:C] = GuardNotFrozen v29 - StoreField v29, :foo=@0x1038, v12 - WriteBarrier v29, v12 + StoreField v30, :foo=@0x1038, v12 + WriteBarrier v30, v12 CheckInterrupts Return v12 "); @@ -5391,10 +5391,10 @@ mod hir_opt_tests { PatchPoint MethodRedefined(C@0x1000, foo=@0x1008, cme:0x1010) PatchPoint NoSingletonClass(C@0x1000) v29:HeapObject[class_exact:C] = GuardType v11, HeapObject[class_exact:C] - v30:CPtr = LoadField v29, :_as_heap@0x1038 - v31:HeapObject[class_exact:C] = GuardNotFrozen v29 - StoreField v30, :foo=@0x1039, v12 - WriteBarrier v29, v12 + v30:HeapObject[class_exact:C] = GuardNotFrozen v29 + v31:CPtr = LoadField v30, :_as_heap@0x1038 + StoreField v31, :foo=@0x1039, v12 + WriteBarrier v30, v12 CheckInterrupts Return v12 "); From ff2d2fc1bd9aa6a768e85276d7ba69bbe5af9572 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Tue, 18 Nov 2025 14:24:49 -0500 Subject: [PATCH 07/15] YJIT: omit single ractor mode assumption for `proc#call` (#15092) The comptime receiver, which is a proc, is either shareable or from this ractor so we don't need to assume single-ractor mode. We should never get the "defined with an un-shareable Proc in a different ractor" error. --- bootstraptest/test_yjit.rb | 20 ++++++++++++++++++++ yjit/src/codegen.rs | 7 ------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb index 94bda9951eec2e..f9cdca6f28c76a 100644 --- a/bootstraptest/test_yjit.rb +++ b/bootstraptest/test_yjit.rb @@ -4081,6 +4081,26 @@ def bar(&block) bar { } } +# unshareable bmethod call through Method#to_proc#call +assert_equal '1000', %q{ + define_method(:bmethod) do + self + end + + Ractor.new do + errors = 0 + 1000.times do + p = method(:bmethod).to_proc + begin + p.call + rescue RuntimeError + errors += 1 + end + end + errors + end.value +} + # test for return stub lifetime issue assert_equal '1', %q{ def foo(n) diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index 6c42dd1713dd0a..a04c95ca09c16e 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -9335,13 +9335,6 @@ fn gen_send_general( return None; } - // Optimize for single ractor mode and avoid runtime check for - // "defined with an un-shareable Proc in a different Ractor" - if !assume_single_ractor_mode(jit, asm) { - gen_counter_incr(jit, asm, Counter::send_call_multi_ractor); - return None; - } - // If this is a .send call we need to adjust the stack if flags & VM_CALL_OPT_SEND != 0 { handle_opt_send_shift_stack(asm, argc); From 656600371239a4a62e7a26e148af70e98d0fa979 Mon Sep 17 00:00:00 2001 From: Shannon Skipper Date: Sun, 16 Nov 2025 08:21:20 -0800 Subject: [PATCH 08/15] ZJIT: Avoid `NaN%` ratio appearing in stats --- zjit.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/zjit.rb b/zjit.rb index f5ed347f2cb6ab..243e6bcf6de9bf 100644 --- a/zjit.rb +++ b/zjit.rb @@ -165,8 +165,12 @@ def stats_string buf = +"***ZJIT: Printing ZJIT statistics on exit***\n" stats = self.stats - stats[:guard_type_exit_ratio] = stats[:exit_guard_type_failure].to_f / stats[:guard_type_count] * 100 - stats[:guard_shape_exit_ratio] = stats[:exit_guard_shape_failure].to_f / stats[:guard_shape_count] * 100 + if stats[:guard_type_count].nonzero? + stats[:guard_type_exit_ratio] = stats[:exit_guard_type_failure].to_f / stats[:guard_type_count] * 100 + end + if stats[:guard_shape_count].nonzero? + stats[:guard_shape_exit_ratio] = stats[:exit_guard_shape_failure].to_f / stats[:guard_shape_count] * 100 + end # Show counters independent from exit_* or dynamic_send_* print_counters_with_prefix(prefix: 'not_inlined_cfuncs_', prompt: 'not inlined C methods', buf:, stats:, limit: 20) @@ -269,7 +273,10 @@ def print_counters(keys, buf:, stats:, right_align: false, base: nil) next unless stats.key?(key) value = stats[key] if base && key != base - ratio = " (%4.1f%%)" % (100.0 * value / stats[base]) + total = stats[base] + if total.nonzero? + ratio = " (%4.1f%%)" % (100.0 * value / total) + end end case key From cbe65ebbc3f2b77316d50b94e84df1c00822d0f2 Mon Sep 17 00:00:00 2001 From: Shannon Skipper Date: Sun, 16 Nov 2025 09:01:50 -0800 Subject: [PATCH 09/15] ZJIT: Skip empty counter sections in stats --- zjit.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zjit.rb b/zjit.rb index 243e6bcf6de9bf..bb6d4d3cdca16c 100644 --- a/zjit.rb +++ b/zjit.rb @@ -297,7 +297,7 @@ def print_counters(keys, buf:, stats:, right_align: false, base: nil) def print_counters_with_prefix(buf:, stats:, prefix:, prompt:, limit: nil) counters = stats.select { |key, value| key.start_with?(prefix) && value > 0 } - return if stats.empty? + return if counters.empty? counters.transform_keys! { |key| key.to_s.delete_prefix(prefix) } key_pad = counters.keys.map(&:size).max From f3f3e76882d01d5e0a006ff731b70053997396e8 Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 18 Nov 2025 17:32:11 -0500 Subject: [PATCH 10/15] Extract `KW_SPECIFIED_BITS_MAX` for JITs (GH-15039) Rename to `VM_KW_SPECIFIED_BITS_MAX` now that it's in `vm_core.h`. --- vm_args.c | 10 ++++------ vm_core.h | 2 ++ vm_insnhelper.c | 2 +- yjit/bindgen/src/main.rs | 1 + yjit/src/codegen.rs | 2 +- yjit/src/cruby_bindings.inc.rs | 1 + zjit/bindgen/src/main.rs | 1 + zjit/src/cruby_bindings.inc.rs | 1 + zjit/src/hir.rs | 3 +-- 9 files changed, 13 insertions(+), 10 deletions(-) diff --git a/vm_args.c b/vm_args.c index 5952b32f1fdb36..64ed88d0e1dcce 100644 --- a/vm_args.c +++ b/vm_args.c @@ -318,8 +318,6 @@ args_setup_kw_parameters_lookup(const ID key, VALUE *ptr, const VALUE *const pas return FALSE; } -#define KW_SPECIFIED_BITS_MAX (32-1) /* TODO: 32 -> Fixnum's max bits */ - static void args_setup_kw_parameters(rb_execution_context_t *const ec, const rb_iseq_t *const iseq, const rb_callable_method_entry_t *cme, VALUE *const passed_values, const int passed_keyword_len, const VALUE *const passed_keywords, @@ -355,7 +353,7 @@ args_setup_kw_parameters(rb_execution_context_t *const ec, const rb_iseq_t *cons if (UNDEF_P(default_values[di])) { locals[i] = Qnil; - if (LIKELY(i < KW_SPECIFIED_BITS_MAX)) { + if (LIKELY(i < VM_KW_SPECIFIED_BITS_MAX)) { unspecified_bits |= 0x01 << di; } else { @@ -364,7 +362,7 @@ args_setup_kw_parameters(rb_execution_context_t *const ec, const rb_iseq_t *cons int j; unspecified_bits_value = rb_hash_new(); - for (j=0; j Fixnum's max bits */ + # define CALLING_ARGC(calling) ((calling)->heap_argv ? RARRAY_LENINT((calling)->heap_argv) : (calling)->argc) struct rb_execution_context_struct; diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 63dcaba8a33bf5..1b1eeb69d9ffb4 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -5808,7 +5808,7 @@ vm_check_keyword(lindex_t bits, lindex_t idx, const VALUE *ep) if (FIXNUM_P(kw_bits)) { unsigned int b = (unsigned int)FIX2ULONG(kw_bits); - if ((idx < KW_SPECIFIED_BITS_MAX) && (b & (0x01 << idx))) + if ((idx < VM_KW_SPECIFIED_BITS_MAX) && (b & (0x01 << idx))) return Qfalse; } else { diff --git a/yjit/bindgen/src/main.rs b/yjit/bindgen/src/main.rs index 100abbb33fc8cb..67a461cd16d95c 100644 --- a/yjit/bindgen/src/main.rs +++ b/yjit/bindgen/src/main.rs @@ -155,6 +155,7 @@ fn main() { .opaque_type("rb_callcache.*") .allowlist_type("rb_callinfo") .allowlist_var("VM_ENV_DATA_INDEX_ME_CREF") + .allowlist_var("VM_KW_SPECIFIED_BITS_MAX") .allowlist_var("rb_block_param_proxy") .allowlist_function("rb_range_new") .allowlist_function("rb_intern") diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index a04c95ca09c16e..9eeccddf6ce490 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -2747,7 +2747,7 @@ fn gen_checkkeyword( ) -> Option { // When a keyword is unspecified past index 32, a hash will be used // instead. This can only happen in iseqs taking more than 32 keywords. - if unsafe { (*get_iseq_body_param_keyword(jit.iseq)).num >= 32 } { + if unsafe { (*get_iseq_body_param_keyword(jit.iseq)).num >= VM_KW_SPECIFIED_BITS_MAX.try_into().unwrap() } { return None; } diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 253598bce1465d..a6aef48313ad71 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -165,6 +165,7 @@ pub const NIL_REDEFINED_OP_FLAG: u32 = 512; pub const TRUE_REDEFINED_OP_FLAG: u32 = 1024; pub const FALSE_REDEFINED_OP_FLAG: u32 = 2048; pub const PROC_REDEFINED_OP_FLAG: u32 = 4096; +pub const VM_KW_SPECIFIED_BITS_MAX: u32 = 31; pub const VM_ENV_DATA_SIZE: u32 = 3; pub const VM_ENV_DATA_INDEX_ME_CREF: i32 = -2; pub const VM_ENV_DATA_INDEX_SPECVAL: i32 = -1; diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 95209375dcfd71..7873a209777605 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -101,6 +101,7 @@ fn main() { .allowlist_function("rb_shape_get_iv_index") .allowlist_function("rb_shape_transition_add_ivar_no_warnings") .allowlist_var("rb_invalid_shape_id") + .allowlist_var("VM_KW_SPECIFIED_BITS_MAX") .allowlist_var("SHAPE_ID_NUM_BITS") .allowlist_function("rb_obj_is_kind_of") .allowlist_function("rb_obj_frozen_p") diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 457cd584cf0e01..0fde4e3ab70a93 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -227,6 +227,7 @@ pub const NIL_REDEFINED_OP_FLAG: u32 = 512; pub const TRUE_REDEFINED_OP_FLAG: u32 = 1024; pub const FALSE_REDEFINED_OP_FLAG: u32 = 2048; pub const PROC_REDEFINED_OP_FLAG: u32 = 4096; +pub const VM_KW_SPECIFIED_BITS_MAX: u32 = 31; pub const VM_ENV_DATA_SIZE: u32 = 3; pub const VM_ENV_DATA_INDEX_ME_CREF: i32 = -2; pub const VM_ENV_DATA_INDEX_SPECVAL: i32 = -1; diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index c96e0357a037ac..58638f30f0264d 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -4848,8 +4848,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { // When a keyword is unspecified past index 32, a hash will be used instead. // This can only happen in iseqs taking more than 32 keywords. // In this case, we side exit to the interpreter. - // TODO(Jacob): Replace the magic number 32 with a named constant. (Can be completed after PR 15039) - if unsafe {(*rb_get_iseq_body_param_keyword(iseq)).num >= 32} { + if unsafe {(*rb_get_iseq_body_param_keyword(iseq)).num >= VM_KW_SPECIFIED_BITS_MAX.try_into().unwrap()} { fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::TooManyKeywordParameters }); break; } From d5d12efde75515997d046448aa36eb9ed893517b Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 18 Nov 2025 22:45:08 +0000 Subject: [PATCH 11/15] [ruby/json] parser.c: Remove unued JSON_ParserStruct.parsing_name https://github.com/ruby/json/commit/ab5efca015 --- ext/json/parser/parser.c | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 2bf3ae0eb38dd6..62ee1f24e71c40 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -336,7 +336,6 @@ typedef struct JSON_ParserStruct { int max_nesting; bool allow_nan; bool allow_trailing_comma; - bool parsing_name; bool symbolize_names; bool freeze; } JSON_ParserConfig; From 6f6a9ead961feb5c2d794bf9d1594c9e8e1de6ab Mon Sep 17 00:00:00 2001 From: eileencodes Date: Tue, 18 Nov 2025 11:56:09 -0500 Subject: [PATCH 12/15] [ruby/rubygems] Replace instance method look up in plugin installer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Gem::Installer.instance_methods(false).include?(:generate_plugins)` is 63x slower than `Gem::Installer.method_defined?(:generate_plugins)` in a microbenchmark. The latter is a direct lookup, whereas the former will create an array, which will be slower. ```ruby require "benchmark/ips" Benchmark.ips do |x| x.report "instance_methods" do Gem::Installer.instance_methods(false).include?(:generate_plugins) end x.report "method_defined" do Gem::Installer.method_defined?(:generate_plugins) end x.compare! end ``` ``` $ ruby -I lib/ benchmark_methods.rb ruby 3.4.4 (2025-05-14 revision https://github.com/ruby/rubygems/commit/a38531fd3f) +PRISM [arm64-darwin23] Warming up -------------------------------------- instance_methods 58.449k i/100ms method_defined 3.375M i/100ms Calculating ------------------------------------- instance_methods 541.874k (± 5.7%) i/s (1.85 μs/i) - 2.747M in 5.087825s method_defined 34.263M (± 1.1%) i/s (29.19 ns/i) - 172.135M in 5.024524s Comparison: method_defined: 34263189.1 i/s instance_methods: 541874.3 i/s - 63.23x slower ``` This does not make much difference in an overall benchmark or profile, but this is more idiomatic Ruby than the prior code. https://github.com/ruby/rubygems/commit/49dec52cb2 --- lib/bundler/rubygems_gem_installer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bundler/rubygems_gem_installer.rb b/lib/bundler/rubygems_gem_installer.rb index 1af1b85ff015a4..25ddbeaceb6da3 100644 --- a/lib/bundler/rubygems_gem_installer.rb +++ b/lib/bundler/rubygems_gem_installer.rb @@ -69,7 +69,7 @@ def ensure_writable_dir(dir) end def generate_plugins - return unless Gem::Installer.instance_methods(false).include?(:generate_plugins) + return unless Gem::Installer.method_defined?(:generate_plugins) latest = Gem::Specification.stubs_for(spec.name).first return if latest && latest.version > spec.version From 32b8f97b3438f74234b84f920085abc37e821164 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Tue, 18 Nov 2025 18:50:36 -0500 Subject: [PATCH 13/15] ZJIT: Delete outdated optional param test [ci skip] Name contradictory now, and we have other tests testing the same thing. --- zjit/src/hir/opt_tests.rs | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index e7aaba7fffb73b..f4afd656c7d533 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -2651,34 +2651,6 @@ mod hir_opt_tests { "); } - #[test] - fn dont_specialize_call_to_iseq_with_opt() { - eval(" - def foo(arg=1) = 1 - def test = foo 1 - test - test - "); - assert_snapshot!(hir_string("test"), @r" - fn test@:3: - bb0(): - EntryPoint interpreter - v1:BasicObject = LoadSelf - Jump bb2(v1) - bb1(v4:BasicObject): - EntryPoint JIT(0) - Jump bb2(v4) - bb2(v6:BasicObject): - v11:Fixnum[1] = Const Value(1) - PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - PatchPoint NoSingletonClass(Object@0x1000) - v20:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)] - v21:BasicObject = SendWithoutBlockDirect v20, :foo (0x1038), v11 - CheckInterrupts - Return v21 - "); - } - #[test] fn dont_specialize_call_to_iseq_with_block() { eval(" From 0f89fa97e3629af427282b5b6a800d2b97dd7d65 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 18 Nov 2025 16:36:29 -0800 Subject: [PATCH 14/15] ZJIT: Inline BasicObject#! (#15201) --- zjit/src/cruby_methods.rs | 15 +++++- zjit/src/hir/opt_tests.rs | 100 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index d2d6be2dec8009..7a4a11a8e18bfd 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -221,7 +221,7 @@ pub fn init() -> Annotations { annotate!(rb_mKernel, "nil?", inline_kernel_nil_p); annotate!(rb_mKernel, "respond_to?", inline_kernel_respond_to_p); annotate!(rb_cBasicObject, "==", inline_basic_object_eq, types::BoolExact, no_gc, leaf, elidable); - annotate!(rb_cBasicObject, "!", types::BoolExact, no_gc, leaf, elidable); + annotate!(rb_cBasicObject, "!", inline_basic_object_not, types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cBasicObject, "!=", inline_basic_object_neq, types::BoolExact); annotate!(rb_cBasicObject, "initialize", inline_basic_object_initialize); annotate!(rb_cInteger, "succ", inline_integer_succ); @@ -517,6 +517,19 @@ fn inline_basic_object_eq(fun: &mut hir::Function, block: hir::BlockId, recv: hi Some(result) } +fn inline_basic_object_not(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { + let &[] = args else { return None; }; + if fun.type_of(recv).is_known_truthy() { + let result = fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(Qfalse) }); + return Some(result); + } + if fun.type_of(recv).is_known_falsy() { + let result = fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(Qtrue) }); + return Some(result); + } + None +} + fn inline_basic_object_neq(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { let &[other] = args else { return None; }; let result = try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumNeq { left, right }, BOP_NEQ, recv, other, state); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index f4afd656c7d533..fadb6ced5f1c0a 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -4602,7 +4602,7 @@ mod hir_opt_tests { } #[test] - fn test_specialize_basicobject_not_to_ccall() { + fn test_specialize_basicobject_not_truthy() { eval(" def test(a) = !a @@ -4622,10 +4622,104 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, !@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) v23:ArrayExact = GuardType v9, ArrayExact + v24:FalseClass = Const Value(false) IncrCounter inline_cfunc_optimized_send_count - v25:BoolExact = CCall !@0x1038, v23 CheckInterrupts - Return v25 + Return v24 + "); + } + + #[test] + fn test_specialize_basicobject_not_false() { + eval(" + def test(a) = !a + + test(false) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + PatchPoint MethodRedefined(FalseClass@0x1000, !@0x1008, cme:0x1010) + v22:FalseClass = GuardType v9, FalseClass + v23:TrueClass = Const Value(true) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v23 + "); + } + + #[test] + fn test_specialize_basicobject_not_nil() { + eval(" + def test(a) = !a + + test(nil) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + PatchPoint MethodRedefined(NilClass@0x1000, !@0x1008, cme:0x1010) + v22:NilClass = GuardType v9, NilClass + v23:TrueClass = Const Value(true) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v23 + "); + } + + #[test] + fn test_specialize_basicobject_not_falsy() { + eval(" + def test(a) = !(if a then false else nil end) + + # TODO(max): Make this not GuardType NilClass and instead just reason + # statically + test(false) + test(true) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + CheckInterrupts + v15:CBool = Test v9 + IfFalse v15, bb3(v8, v9) + v18:FalseClass = Const Value(false) + CheckInterrupts + Jump bb4(v8, v9, v18) + bb3(v22:BasicObject, v23:BasicObject): + v26:NilClass = Const Value(nil) + Jump bb4(v22, v23, v26) + bb4(v28:BasicObject, v29:BasicObject, v30:NilClass|FalseClass): + PatchPoint MethodRedefined(NilClass@0x1000, !@0x1008, cme:0x1010) + v41:NilClass = GuardType v30, NilClass + v42:TrueClass = Const Value(true) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v42 "); } From 4423facbffd5bd523541ebf42dc274272b1de732 Mon Sep 17 00:00:00 2001 From: Go Sueyoshi Date: Wed, 19 Nov 2025 09:46:35 +0900 Subject: [PATCH 15/15] [ruby/rubygems] Add `--ext=go` to `bundle gem` (https://github.com/ruby/rubygems/pull/8183) * Add new gem templates * Add `--ext=go` in `bundle gem` * Add setup-go to .github/workflows/main.yml * Embed go version in go.mod * Use go in bundler CI * Add example method to template * Install Go in .circleci/config.yml * Install Go in .gitlab-ci.yml * Allow hard tabs in go template * Run `rake update_manifest` * Fix test * Move go_gem to gemspec Respect to 9b0ec80 * nits: :golf: * includes valid module name in go.mod * generate header file * Run `go mod tidy` to create `go.sum` * Check if `go.sum` is generated only when Go is installed To avoid test failure in environments where Go is not installed * Run CI * Workaround for hung up c.f. https://github.com/rubygems/rubygems/actions/runs/11639408044/job/32415545422 * Write man for --ext=go * Re-generate man with `./bin/rake man:build` * pinning :pushpin: * Update with `./bin/rake man:build` * nits: Extract to method * nits: Use `sys_exec` instead of `system` * Clean go module cache after test Workaround following error ``` 1) bundle gem gem naming with underscore --ext parameter set with go includes go_gem extension in extconf.rb Failure/Error: FileUtils.rm_r(dir) Errno::EACCES: Permission denied @ apply2files - /home/runner/work/rubygems/rubygems/bundler/tmp/2.2/home/go/pkg/mod/gopkg.in/yaml.v3@v3.0.1/decode_test.go # ./spec/support/helpers.rb:37:in `block in reset!' # ./spec/support/helpers.rb:21:in `each' # ./spec/support/helpers.rb:21:in `reset!' # ./spec/spec_helper.rb:130:in `block (2 levels) in ' # /home/runner/work/rubygems/rubygems/lib/rubygems.rb:303:in `load' # /home/runner/work/rubygems/rubygems/lib/rubygems.rb:303:in `activate_and_load_bin_path' ``` Files installed with `go get` have permissions set to 444 ref. https://github.com/golang/go/issues/35615 ``` $ ls -l /home/runner/work/rubygems/rubygems/bundler/tmp/2.2/home/go/pkg/mod/gopkg.in/yaml.v3@v3.0.1/decode_test.go -r--r--r-- 1 runner runner 42320 Nov 15 06:38 /home/runner/work/rubygems/rubygems/bundler/tmp/2.2/home/go/pkg/mod/gopkg.in/yaml.v3@v3.0.1/decode_test.go ``` So they cannot be deleted by `FileUtils.rm_r`. Therefore, this is necessary to execute `go clean -modcache` separately from `FileUtils.rm_r` to circumvent it. * Remove needless changes ref. https://github.com/ruby/rubygems/pull/8183#discussion_r2532902051 * ci: setup-go is needless * Don't run go command in `bundle gem` ref. https://github.com/ruby/rubygems/pull/8183#discussion_r2532765470 * Revert unrelated date changes --------- https://github.com/ruby/rubygems/commit/260d7d60b3 Co-authored-by: Hiroshi SHIBATA --- lib/bundler/cli.rb | 2 +- lib/bundler/cli/gem.rb | 16 +- lib/bundler/man/bundle-gem.1 | 4 +- lib/bundler/man/bundle-gem.1.ronn | 4 +- .../templates/newgem/circleci/config.yml.tt | 12 ++ .../newgem/ext/newgem/extconf-go.rb.tt | 11 ++ .../templates/newgem/ext/newgem/go.mod.tt | 5 + .../newgem/ext/newgem/newgem-go.c.tt | 2 + .../templates/newgem/ext/newgem/newgem.go.tt | 31 ++++ .../newgem/github/workflows/main.yml.tt | 6 + lib/bundler/templates/newgem/gitlab-ci.yml.tt | 9 ++ .../templates/newgem/newgem.gemspec.tt | 5 +- spec/bundler/commands/newgem_spec.rb | 151 ++++++++++++++++++ spec/bundler/quality_spec.rb | 3 + 14 files changed, 254 insertions(+), 7 deletions(-) create mode 100644 lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt create mode 100644 lib/bundler/templates/newgem/ext/newgem/go.mod.tt create mode 100644 lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt create mode 100644 lib/bundler/templates/newgem/ext/newgem/newgem.go.tt diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index af634291dd4737..86b86b359fcf82 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -11,7 +11,7 @@ class CLI < Thor AUTO_INSTALL_CMDS = %w[show binstubs outdated exec open console licenses clean].freeze PARSEABLE_COMMANDS = %w[check config help exec platform show version].freeze - EXTENSIONS = ["c", "rust"].freeze + EXTENSIONS = ["c", "rust", "go"].freeze COMMAND_ALIASES = { "check" => "c", diff --git a/lib/bundler/cli/gem.rb b/lib/bundler/cli/gem.rb index abfb095d469f81..236ce530eccb75 100644 --- a/lib/bundler/cli/gem.rb +++ b/lib/bundler/cli/gem.rb @@ -13,6 +13,8 @@ class CLI::Gem "test-unit" => "3.0", }.freeze + DEFAULT_GITHUB_USERNAME = "[USERNAME]" + attr_reader :options, :gem_name, :thor, :name, :target, :extension def initialize(options, gem_name, thor) @@ -72,7 +74,7 @@ def run bundle: options[:bundle], bundler_version: bundler_dependency_version, git: use_git, - github_username: github_username.empty? ? "[USERNAME]" : github_username, + github_username: github_username.empty? ? DEFAULT_GITHUB_USERNAME : github_username, required_ruby_version: required_ruby_version, rust_builder_required_rubygems_version: rust_builder_required_rubygems_version, minitest_constant_name: minitest_constant_name, @@ -231,6 +233,18 @@ def run ) end + if extension == "go" + templates.merge!( + "ext/newgem/go.mod.tt" => "ext/#{name}/go.mod", + "ext/newgem/extconf-go.rb.tt" => "ext/#{name}/extconf.rb", + "ext/newgem/newgem.h.tt" => "ext/#{name}/#{underscored_name}.h", + "ext/newgem/newgem.go.tt" => "ext/#{name}/#{underscored_name}.go", + "ext/newgem/newgem-go.c.tt" => "ext/#{name}/#{underscored_name}.c", + ) + + config[:go_module_username] = config[:github_username] == DEFAULT_GITHUB_USERNAME ? "username" : config[:github_username] + end + if target.exist? && !target.directory? Bundler.ui.error "Couldn't create a new gem named `#{gem_name}` because there's an existing file named `#{gem_name}`." exit Bundler::BundlerError.all_errors[Bundler::GenericSystemCallError] diff --git a/lib/bundler/man/bundle-gem.1 b/lib/bundler/man/bundle-gem.1 index 670a69d67e317b..85c0f57674a41c 100644 --- a/lib/bundler/man/bundle-gem.1 +++ b/lib/bundler/man/bundle-gem.1 @@ -38,8 +38,8 @@ Add a \fBCHANGELOG\.md\fR file to the root of the generated project\. If this op \fB\-\-no\-changelog\fR Do not create a \fBCHANGELOG\.md\fR (overrides \fB\-\-changelog\fR specified in the global config)\. .TP -\fB\-\-ext=c\fR, \fB\-\-ext=rust\fR -Add boilerplate for C or Rust (currently magnus \fIhttps://docs\.rs/magnus\fR based) extension code to the generated project\. This behavior is disabled by default\. +\fB\-\-ext=c\fR, \fB\-\-ext=go\fR, \fB\-\-ext=rust\fR +Add boilerplate for C, Go (currently go\-gem\-wrapper \fIhttps://github\.com/ruby\-go\-gem/go\-gem\-wrapper\fR based) or Rust (currently magnus \fIhttps://docs\.rs/magnus\fR based) extension code to the generated project\. This behavior is disabled by default\. .TP \fB\-\-no\-ext\fR Do not add extension code (overrides \fB\-\-ext\fR specified in the global config)\. diff --git a/lib/bundler/man/bundle-gem.1.ronn b/lib/bundler/man/bundle-gem.1.ronn index b71bde9f6506f1..488c8113e4f263 100644 --- a/lib/bundler/man/bundle-gem.1.ronn +++ b/lib/bundler/man/bundle-gem.1.ronn @@ -51,8 +51,8 @@ configuration file using the following names: Do not create a `CHANGELOG.md` (overrides `--changelog` specified in the global config). -* `--ext=c`, `--ext=rust`: - Add boilerplate for C or Rust (currently [magnus](https://docs.rs/magnus) based) extension code to the generated project. This behavior +* `--ext=c`, `--ext=go`, `--ext=rust`: + Add boilerplate for C, Go (currently [go-gem-wrapper](https://github.com/ruby-go-gem/go-gem-wrapper) based) or Rust (currently [magnus](https://docs.rs/magnus) based) extension code to the generated project. This behavior is disabled by default. * `--no-ext`: diff --git a/lib/bundler/templates/newgem/circleci/config.yml.tt b/lib/bundler/templates/newgem/circleci/config.yml.tt index f40f029bf130b5..c4dd9d06471eb8 100644 --- a/lib/bundler/templates/newgem/circleci/config.yml.tt +++ b/lib/bundler/templates/newgem/circleci/config.yml.tt @@ -6,6 +6,10 @@ jobs: <%- if config[:ext] == 'rust' -%> environment: RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN: 'true' +<%- end -%> +<%- if config[:ext] == 'go' -%> + environment: + GO_VERSION: '1.23.0' <%- end -%> steps: - checkout @@ -16,6 +20,14 @@ jobs: - run: name: Install a RubyGems version that can compile rust extensions command: gem update --system '<%= ::Gem.rubygems_version %>' +<%- end -%> +<%- if config[:ext] == 'go' -%> + - run: + name: Install Go + command: | + wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz + tar -C /usr/local -xzf /tmp/go.tar.gz + echo 'export PATH=/usr/local/go/bin:"$PATH"' >> "$BASH_ENV" <%- end -%> - run: name: Run the default task diff --git a/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt b/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt new file mode 100644 index 00000000000000..a689e21ebe9c3f --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "mkmf" +require "go_gem/mkmf" + +# Makes all symbols private by default to avoid unintended conflict +# with other gems. To explicitly export symbols you can use RUBY_FUNC_EXPORTED +# selectively, or entirely remove this flag. +append_cflags("-fvisibility=hidden") + +create_go_makefile(<%= config[:makefile_path].inspect %>) diff --git a/lib/bundler/templates/newgem/ext/newgem/go.mod.tt b/lib/bundler/templates/newgem/ext/newgem/go.mod.tt new file mode 100644 index 00000000000000..3f4819d0046509 --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/go.mod.tt @@ -0,0 +1,5 @@ +module github.com/<%= config[:go_module_username] %>/<%= config[:underscored_name] %> + +go 1.23 + +require github.com/ruby-go-gem/go-gem-wrapper latest diff --git a/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt b/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt new file mode 100644 index 00000000000000..119c0c96ea5a4c --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt @@ -0,0 +1,2 @@ +#include "<%= config[:underscored_name] %>.h" +#include "_cgo_export.h" diff --git a/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt b/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt new file mode 100644 index 00000000000000..f19b750e58c46b --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt @@ -0,0 +1,31 @@ +package main + +/* +#include "<%= config[:underscored_name] %>.h" + +VALUE rb_<%= config[:underscored_name] %>_sum(VALUE self, VALUE a, VALUE b); +*/ +import "C" + +import ( + "github.com/ruby-go-gem/go-gem-wrapper/ruby" +) + +//export rb_<%= config[:underscored_name] %>_sum +func rb_<%= config[:underscored_name] %>_sum(_ C.VALUE, a C.VALUE, b C.VALUE) C.VALUE { + longA := ruby.NUM2LONG(ruby.VALUE(a)) + longB := ruby.NUM2LONG(ruby.VALUE(b)) + + sum := longA + longB + + return C.VALUE(ruby.LONG2NUM(sum)) +} + +//export Init_<%= config[:underscored_name] %> +func Init_<%= config[:underscored_name] %>() { + rb_m<%= config[:constant_array].join %> := ruby.RbDefineModule(<%= config[:constant_name].inspect %>) + ruby.RbDefineSingletonMethod(rb_m<%= config[:constant_array].join %>, "sum", C.rb_<%= config[:underscored_name] %>_sum, 2) +} + +func main() { +} diff --git a/lib/bundler/templates/newgem/github/workflows/main.yml.tt b/lib/bundler/templates/newgem/github/workflows/main.yml.tt index 9224ee0ca27266..7f3e3a5b66f94f 100644 --- a/lib/bundler/templates/newgem/github/workflows/main.yml.tt +++ b/lib/bundler/templates/newgem/github/workflows/main.yml.tt @@ -34,6 +34,12 @@ jobs: with: ruby-version: ${{ matrix.ruby }} bundler-cache: true +<%- end -%> +<%- if config[:ext] == 'go' -%> + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: ext/<%= config[:underscored_name] %>/go.mod <%- end -%> - name: Run the default task run: bundle exec rake diff --git a/lib/bundler/templates/newgem/gitlab-ci.yml.tt b/lib/bundler/templates/newgem/gitlab-ci.yml.tt index d2e1f337362fb7..adbd70cbc05007 100644 --- a/lib/bundler/templates/newgem/gitlab-ci.yml.tt +++ b/lib/bundler/templates/newgem/gitlab-ci.yml.tt @@ -5,6 +5,11 @@ default: <%- if config[:ext] == 'rust' -%> - apt-get update && apt-get install -y clang - gem update --system '<%= ::Gem.rubygems_version %>' +<%- end -%> +<%- if config[:ext] == 'go' -%> + - wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz + - tar -C /usr/local -xzf /tmp/go.tar.gz + - export PATH=/usr/local/go/bin:$PATH <%- end -%> - gem install bundler -v <%= Bundler::VERSION %> - bundle install @@ -13,6 +18,10 @@ example_job: <%- if config[:ext] == 'rust' -%> variables: RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN: 'true' +<%- end -%> +<%- if config[:ext] == 'go' -%> + variables: + GO_VERSION: '1.23.0' <%- end -%> script: - bundle exec rake diff --git a/lib/bundler/templates/newgem/newgem.gemspec.tt b/lib/bundler/templates/newgem/newgem.gemspec.tt index c87abda8a01555..513875fd63ef07 100644 --- a/lib/bundler/templates/newgem/newgem.gemspec.tt +++ b/lib/bundler/templates/newgem/newgem.gemspec.tt @@ -38,7 +38,7 @@ Gem::Specification.new do |spec| spec.bindir = "exe" spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] -<%- if config[:ext] == 'c' || config[:ext] == 'rust' -%> +<%- if %w(c rust go).include?(config[:ext]) -%> spec.extensions = ["ext/<%= config[:underscored_name] %>/extconf.rb"] <%- end -%> @@ -47,6 +47,9 @@ Gem::Specification.new do |spec| <%- if config[:ext] == 'rust' -%> spec.add_dependency "rb_sys", "~> 0.9.91" <%- end -%> +<%- if config[:ext] == 'go' -%> + spec.add_dependency "go_gem", "~> 0.2" +<%- end -%> # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html diff --git a/spec/bundler/commands/newgem_spec.rb b/spec/bundler/commands/newgem_spec.rb index 1ce4a0da09cac1..7a837bd08f0112 100644 --- a/spec/bundler/commands/newgem_spec.rb +++ b/spec/bundler/commands/newgem_spec.rb @@ -31,6 +31,13 @@ def ignore_paths matched[:ignored]&.split(" ") end + def installed_go? + sys_exec("go version", raise_on_error: true) + true + rescue StandardError + false + end + let(:generated_gemspec) { Bundler.load_gemspec_uncached(bundled_app(gem_name).join("#{gem_name}.gemspec")) } let(:gem_name) { "mygem" } @@ -1748,6 +1755,150 @@ def create_temporary_dir(dir) expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) end end + + context "--ext parameter set with go" do + let(:flags) { "--ext=go" } + + before do + bundle ["gem", gem_name, flags].compact.join(" ") + end + + after do + sys_exec("go clean -modcache", raise_on_error: true) if installed_go? + end + + it "is not deprecated" do + expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated." + end + + it "builds ext skeleton" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod")).to exist + end + + it "includes extconf.rb in gem_name.gemspec" do + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include(%(spec.extensions = ["ext/#{gem_name}/extconf.rb"])) + end + + it "includes go_gem in gem_name.gemspec" do + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "go_gem", "~> 0.2"') + end + + it "includes go_gem extension in extconf.rb" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(<<~RUBY) + require "mkmf" + require "go_gem/mkmf" + RUBY + + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(%(create_go_makefile("#{gem_name}/#{gem_name}"))) + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).not_to include("create_makefile") + end + + it "includes go_gem extension in gem_name.c" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c").read).to eq(<<~C) + #include "#{gem_name}.h" + #include "_cgo_export.h" + C + end + + it "includes skeleton code in gem_name.go" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) + /* + #include "#{gem_name}.h" + + VALUE rb_#{gem_name}_sum(VALUE self, VALUE a, VALUE b); + */ + import "C" + GO + + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) + //export rb_#{gem_name}_sum + func rb_#{gem_name}_sum(_ C.VALUE, a C.VALUE, b C.VALUE) C.VALUE { + GO + + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) + //export Init_#{gem_name} + func Init_#{gem_name}() { + GO + end + + it "includes valid module name in go.mod" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/bundleuser/#{gem_name}") + end + + context "with --no-ci" do + let(:flags) { "--ext=go --no-ci" } + + it_behaves_like "CI config is absent" + end + + context "--ci set to github" do + let(:flags) { "--ext=go --ci=github" } + + it "generates .github/workflows/main.yml" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist + expect(bundled_app("#{gem_name}/.github/workflows/main.yml").read).to include("go-version-file: ext/#{gem_name}/go.mod") + end + end + + context "--ci set to circle" do + let(:flags) { "--ext=go --ci=circle" } + + it "generates a .circleci/config.yml" do + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist + + expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML.strip) + environment: + GO_VERSION: + YAML + + expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML) + - run: + name: Install Go + command: | + wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz + tar -C /usr/local -xzf /tmp/go.tar.gz + echo 'export PATH=/usr/local/go/bin:"$PATH"' >> "$BASH_ENV" + YAML + end + end + + context "--ci set to gitlab" do + let(:flags) { "--ext=go --ci=gitlab" } + + it "generates a .gitlab-ci.yml" do + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist + + expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML) + - wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz + - tar -C /usr/local -xzf /tmp/go.tar.gz + - export PATH=/usr/local/go/bin:$PATH + YAML + + expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML.strip) + variables: + GO_VERSION: + YAML + end + end + + context "without github.user" do + before do + # FIXME: GitHub Actions Windows Runner hang up here for some reason... + skip "Workaround for hung up" if Gem.win_platform? + + git("config --global --unset github.user") + bundle ["gem", gem_name, flags].compact.join(" ") + end + + it "includes valid module name in go.mod" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/username/#{gem_name}") + end + end + end end context "gem naming with dashed" do diff --git a/spec/bundler/quality_spec.rb b/spec/bundler/quality_spec.rb index bbd6517f21dbea..b60be9980fc145 100644 --- a/spec/bundler/quality_spec.rb +++ b/spec/bundler/quality_spec.rb @@ -20,6 +20,9 @@ def check_for_git_merge_conflicts(filename) end def check_for_tab_characters(filename) + # Because Go uses hard tabs + return if filename.end_with?(".go.tt") + failing_lines = [] each_line(filename) do |line, number| failing_lines << number + 1 if line.include?("\t")