From e31c2569079d09cf63d6822558b053b8b8cc9937 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 19 Nov 2025 18:17:43 -0500 Subject: [PATCH 1/5] ZJIT: Inline GuardNotFrozen into LIR No sense calling a C function. --- zjit/src/codegen.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 4c865dcd8a1b34..944f9504a25fb9 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -654,10 +654,20 @@ fn gen_guard_block_param_proxy(jit: &JITState, asm: &mut Assembler, level: u32, } 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)); + let side_exit = side_exit(jit, state, GuardNotFrozen); + let recv = asm.load(recv); + // Side-exit if recv is false + assert_eq!(Qfalse.as_i64(), 0); + asm.test(recv, recv); + asm.jz(side_exit.clone()); + // Side-exit if recv is immediate + asm.test(recv, (RUBY_IMMEDIATE_MASK as u64).into()); + asm.jnz(side_exit.clone()); + // It's a heap object, so check the frozen flag + let flags = asm.load(Opnd::mem(64, recv, RUBY_OFFSET_RBASIC_FLAGS)); + asm.test(flags, (RUBY_FL_FREEZE as u64).into()); + // Side-exit if frozen + asm.jnz(side_exit.clone()); recv } From 6e2906f60da20d6cd057aa8ad4b84f8c988406d9 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 21 Nov 2025 13:43:52 -0500 Subject: [PATCH 2/5] ZJIT: Don't make GuardNotFrozen consider immediates --- zjit/src/codegen.rs | 10 +--------- zjit/src/cruby_methods.rs | 2 ++ zjit/src/hir.rs | 8 ++++++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 944f9504a25fb9..3300219ccda07a 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -654,20 +654,12 @@ fn gen_guard_block_param_proxy(jit: &JITState, asm: &mut Assembler, level: u32, } fn gen_guard_not_frozen(jit: &JITState, asm: &mut Assembler, recv: Opnd, state: &FrameState) -> Opnd { - let side_exit = side_exit(jit, state, GuardNotFrozen); let recv = asm.load(recv); - // Side-exit if recv is false - assert_eq!(Qfalse.as_i64(), 0); - asm.test(recv, recv); - asm.jz(side_exit.clone()); - // Side-exit if recv is immediate - asm.test(recv, (RUBY_IMMEDIATE_MASK as u64).into()); - asm.jnz(side_exit.clone()); // It's a heap object, so check the frozen flag let flags = asm.load(Opnd::mem(64, recv, RUBY_OFFSET_RBASIC_FLAGS)); asm.test(flags, (RUBY_FL_FREEZE as u64).into()); // Side-exit if frozen - asm.jnz(side_exit.clone()); + asm.jnz(side_exit(jit, state, GuardNotFrozen)); recv } diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 6d09b5e5a7995f..23c05e6d58fb54 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -330,6 +330,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; }; + // We know that all Array are HeapObject, so no need to insert a GuardType(HeapObject). let arr = fun.push_insn(block, hir::Insn::GuardNotFrozen { recv, state }); Some(fun.push_insn(block, hir::Insn::ArrayPop { array: arr, state })) } @@ -391,6 +392,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 }); + // We know that all String are HeapObject, so no need to insert a GuardType(HeapObject). 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. diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 3a69dd6610e514..d6ccf9160e7eeb 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -903,7 +903,8 @@ pub enum Insn { /// Side-exit if the block param has been modified or the block handler for the frame /// 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. + /// Side-exit if val is frozen. Does *not* check if the val is an immediate; assumes that it is + /// a heap object. 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 }, @@ -2553,6 +2554,7 @@ impl Function { // // No need for a GuardShape. if let OptimizedMethodType::StructAset = opt_type { + // We know that all Struct are HeapObject, so no need to insert a GuardType(HeapObject). recv = self.push_insn(block, Insn::GuardNotFrozen { recv, state }); } @@ -4150,7 +4152,6 @@ impl Function { | Insn::IsNil { val } | Insn::IsMethodCfunc { val, .. } | Insn::GuardShape { val, .. } - | Insn::GuardNotFrozen { recv: val, .. } | Insn::SetGlobal { val, .. } | Insn::SetLocal { val, .. } | Insn::SetClassVar { val, .. } @@ -4169,6 +4170,9 @@ impl Function { | Insn::DefinedIvar { self_val: val, .. } => { self.assert_subtype(insn_id, val, types::BasicObject) } + Insn::GuardNotFrozen { recv, .. } => { + self.assert_subtype(insn_id, recv, types::HeapBasicObject) + } // Instructions with 2 Ruby object operands Insn::SetIvar { self_val: left, val: right, .. } | Insn::NewRange { low: left, high: right, .. } From 2289961b485b1cbf7b1012693722c16a6cdb4cda Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 21 Nov 2025 19:30:04 +0100 Subject: [PATCH 3/5] [ruby/rubygems] Undeprecate Gem::Version#<=> against strings This pattern is extremely common across the ecosystem, I don't think it's reasonable to deprecate it. I understand the performance argument, but perhaps the dependency resolution algorithm can use another method that is private API and only works with two `Version` instance. https://github.com/ruby/rubygems/commit/024b4b547a --- lib/rubygems/version.rb | 3 --- test/rubygems/test_gem_version.rb | 6 +----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index a88a4a49ee2b94..90fe1b3c24324a 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -344,9 +344,6 @@ def approximate_recommendation def <=>(other) if String === other - unless Gem::Deprecate.skip - warn "comparing version objects with strings is deprecated and will be removed", uplevel: 1 - end return unless self.class.correct?(other) return self <=> self.class.new(other) end diff --git a/test/rubygems/test_gem_version.rb b/test/rubygems/test_gem_version.rb index ce38a59113d2d2..3987c620d0a74d 100644 --- a/test/rubygems/test_gem_version.rb +++ b/test/rubygems/test_gem_version.rb @@ -160,11 +160,7 @@ def test_spaceship [-1, "1.9.3.1"], [nil, "whatever"], ].each do |cmp, string_ver| - actual_stdout, actual_stderr = capture_output do - assert_equal(cmp, v("1.9.3") <=> string_ver) - end - assert_empty actual_stdout - assert_match(/comparing version objects with strings is deprecated and will be removed/, actual_stderr) + assert_equal(cmp, v("1.9.3") <=> string_ver) end end From b47b37c12cf30b06ec5afc365f3739b0744b3f4c Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 21 Nov 2025 12:46:03 +0000 Subject: [PATCH 4/5] [DOC] Harmonize #* methods --- complex.c | 9 +++++---- numeric.c | 10 ++++++---- rational.c | 16 +++++++++------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/complex.c b/complex.c index 54c8cd0d6e8ad5..d69b0a65817497 100644 --- a/complex.c +++ b/complex.c @@ -913,15 +913,16 @@ comp_mul(VALUE areal, VALUE aimag, VALUE breal, VALUE bimag, VALUE *real, VALUE /* * call-seq: - * complex * numeric -> new_complex + * self * other -> numeric * - * Returns the product of +self+ and +numeric+: + * Returns the numeric product of +self+ and +other+: * + * Complex.rect(9, 8) * 4 # => (36+32i) + * Complex.rect(20, 9) * 9.8 # => (196.0+88.2i) * Complex.rect(2, 3) * Complex.rect(2, 3) # => (-5+12i) * Complex.rect(900) * Complex.rect(1) # => (900+0i) * Complex.rect(-2, 9) * Complex.rect(-9, 2) # => (0-85i) - * Complex.rect(9, 8) * 4 # => (36+32i) - * Complex.rect(20, 9) * 9.8 # => (196.0+88.2i) + * Complex.rect(9, 8) * Rational(2, 3) # => ((6/1)+(16/3)*i) * */ VALUE diff --git a/numeric.c b/numeric.c index 0d5f3f26f617e3..a9e5fa78663bc2 100644 --- a/numeric.c +++ b/numeric.c @@ -1195,13 +1195,14 @@ rb_float_minus(VALUE x, VALUE y) * call-seq: * self * other -> numeric * - * Returns a new \Float which is the product of +self+ and +other+: + * Returns the numeric product of +self+ and +other+: * * f = 3.14 * f * 2 # => 6.28 * f * 2.0 # => 6.28 * f * Rational(1, 2) # => 1.57 * f * Complex(2, 0) # => (6.28+0.0i) + * */ VALUE @@ -4098,16 +4099,17 @@ fix_mul(VALUE x, VALUE y) /* * call-seq: - * self * numeric -> numeric_result + * self * other -> numeric * - * Performs multiplication: + * Returns the numeric product of +self+ and +other+: * * 4 * 2 # => 8 - * 4 * -2 # => -8 * -4 * 2 # => -8 + * 4 * -2 # => -8 * 4 * 2.0 # => 8.0 * 4 * Rational(1, 3) # => (4/3) * 4 * Complex(2, 0) # => (8+0i) + * */ VALUE diff --git a/rational.c b/rational.c index 28f116580fb945..77b1b175d08185 100644 --- a/rational.c +++ b/rational.c @@ -853,15 +853,17 @@ f_muldiv(VALUE self, VALUE anum, VALUE aden, VALUE bnum, VALUE bden, int k) /* * call-seq: - * rat * numeric -> numeric + * self * other -> numeric * - * Performs multiplication. + * Returns the numeric product of +self+ and +other+: + * + * Rational(9, 8) * 4 #=> (9/2) + * Rational(20, 9) * 9.8 #=> 21.77777777777778 + * Rational(9, 8) * Complex(1, 2) # => ((9/8)+(9/4)*i) + * Rational(2, 3) * Rational(2, 3) #=> (4/9) + * Rational(900) * Rational(1) #=> (900/1) + * Rational(-2, 9) * Rational(-9, 2) #=> (1/1) * - * Rational(2, 3) * Rational(2, 3) #=> (4/9) - * Rational(900) * Rational(1) #=> (900/1) - * Rational(-2, 9) * Rational(-9, 2) #=> (1/1) - * Rational(9, 8) * 4 #=> (9/2) - * Rational(20, 9) * 9.8 #=> 21.77777777777778 */ VALUE rb_rational_mul(VALUE self, VALUE other) From 7789aaca3736fdac2c00b141b12f127168c485cd Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 21 Nov 2025 16:44:55 +0000 Subject: [PATCH 5/5] [DOC] Tweaks for Integer#** --- numeric.c | 53 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/numeric.c b/numeric.c index a9e5fa78663bc2..a2c91d87a57db3 100644 --- a/numeric.c +++ b/numeric.c @@ -4592,17 +4592,48 @@ fix_pow(VALUE x, VALUE y) /* * call-seq: - * self ** numeric -> numeric_result - * - * Raises +self+ to the power of +numeric+: - * - * 2 ** 3 # => 8 - * 2 ** -3 # => (1/8) - * -2 ** 3 # => -8 - * -2 ** -3 # => (-1/8) - * 2 ** 3.3 # => 9.849155306759329 - * 2 ** Rational(3, 1) # => (8/1) - * 2 ** Complex(3, 0) # => (8+0i) + * self ** exponent -> numeric + * + * Returns the value of base +self+ raised to the power +exponent+; + * see {Exponentiation}[https://en.wikipedia.org/wiki/Exponentiation]: + * + * # Result for non-negative Integer exponent is Integer. + * 2 ** 0 # => 1 + * 2 ** 1 # => 2 + * 2 ** 2 # => 4 + * 2 ** 3 # => 8 + * -2 ** 3 # => -8 + * # Result for negative Integer exponent is Rational, not Float. + * 2 ** -3 # => (1/8) + * -2 ** -3 # => (-1/8) + * + * # Result for Float exponent is Float. + * 2 ** 0.0 # => 1.0 + * 2 ** 1.0 # => 2.0 + * 2 ** 2.0 # => 4.0 + * 2 ** 3.0 # => 8.0 + * -2 ** 3.0 # => -8.0 + * 2 ** -3.0 # => 0.125 + * -2 ** -3.0 # => -0.125 + * + * # Result for non-negative Complex exponent is Complex with Integer parts. + * 2 ** Complex(0, 0) # => (1+0i) + * 2 ** Complex(1, 0) # => (2+0i) + * 2 ** Complex(2, 0) # => (4+0i) + * 2 ** Complex(3, 0) # => (8+0i) + * -2 ** Complex(3, 0) # => (-8+0i) + * # Result for negative Complex exponent is Complex with Rational parts. + * 2 ** Complex(-3, 0) # => ((1/8)+(0/1)*i) + * -2 ** Complex(-3, 0) # => ((-1/8)+(0/1)*i) + * + * # Result for Rational exponent is Rational. + * 2 ** Rational(0, 1) # => (1/1) + * 2 ** Rational(1, 1) # => (2/1) + * 2 ** Rational(2, 1) # => (4/1) + * 2 ** Rational(3, 1) # => (8/1) + * -2 ** Rational(3, 1) # => (-8/1) + * 2 ** Rational(-3, 1) # => (1/8) + * -2 ** Rational(-3, 1) # => (-1/8) * */ VALUE