From cbc4c203f194324625497f9bfd2f8ce029b2e9ce Mon Sep 17 00:00:00 2001 From: SamW Date: Tue, 16 Jun 2026 10:23:22 -0700 Subject: [PATCH 01/17] coerce for numeric ops --- core/integer.rbs | 39 +++++++----------- core/numeric.rbs | 5 +++ test/stdlib/Integer_test.rb | 79 +++++++++++++++++++++++++++++++++---- test/stdlib/test_helper.rb | 11 ++++++ 4 files changed, 102 insertions(+), 32 deletions(-) diff --git a/core/integer.rbs b/core/integer.rbs index c52c8ecb2..e6d8cd912 100644 --- a/core/integer.rbs +++ b/core/integer.rbs @@ -174,10 +174,8 @@ class Integer < Numeric # 10 % 3.0 # => 1.0 # 10 % Rational(3, 1) # => (1/1) # - def %: (Float) -> Float - | (Rational) -> Rational - | (Integer) -> Integer - | (Numeric) -> Numeric + def %: (Integer other) -> Integer + | [O < RBS::Ops::_Modulo[S, R], S, R] (Numeric::_Coerce[self, O, S] other) -> R # # Returns `true` if `self` is numerically equal to `other`; `false` otherwise. diff --git a/test/stdlib/Integer_test.rb b/test/stdlib/Integer_test.rb index 557a0930c..10d5d4e28 100644 --- a/test/stdlib/Integer_test.rb +++ b/test/stdlib/Integer_test.rb @@ -39,8 +39,8 @@ def test_op_mod(method: :%) 38, method, 12.0 # Notably not `Complex` as complex doesn't define `%` - assert_send_type '[O < RBS::Ops::_Subtract[S, R], S, R] (Numeric::_Coerce[38, O, S]) -> R', - 38, method, Coercable.new('fmt: %s', &:to_s) + assert_send_type '(Coercable) -> Coercable::OpReturn', + 38, method, Coercable.for_op(:%) end def test_op_and @@ -57,8 +57,8 @@ def test_op_mul assert_send_type '(Complex) -> Complex', 38, :*, 12i - assert_send_type '[O < RBS::Ops::_Times[S, R], S, R] (Numeric::_Coerce[38, O, S]) -> R', - 38, :*, Coercable.new(%w[a b], &:to_s) + assert_send_type '(Coercable) -> Coercable::OpReturn', + 38, :*, Coercable.for_op(:*) end def test_op_pow @@ -71,8 +71,8 @@ def test_op_pow assert_send_type '(Complex) -> Complex', 38, :**, 12i - assert_send_type '[O < RBS::Ops::_Power[S, R], S, R] (Numeric::_Coerce[38, O, S]) -> R', - 38, :**, Coercable.new(10i, &:i) + assert_send_type '(Coercable) -> Coercable::OpReturn', + 38, :**, Coercable.for_op(:**) end def test_op_add @@ -85,8 +85,8 @@ def test_op_add assert_send_type '(Complex) -> Complex', 38, :+, 12i - assert_send_type '[O < RBS::Ops::_Add[S, R], S, R] (Numeric::_Coerce[38, O, S]) -> R', - 38, :+, Coercable.new('foo', &:to_s) + assert_send_type '(Coercable) -> Coercable::OpReturn', + 38, :+, Coercable.for_op(:+) end def test_op_sub @@ -99,8 +99,8 @@ def test_op_sub assert_send_type '(Complex) -> Complex', 38, :-, 12i - assert_send_type '[O < RBS::Ops::_Subtract[S, R], S, R] (Numeric::_Coerce[38, O, S]) -> R', - 38, :-, Coercable.new([3], &:digits) + assert_send_type '(Coercable) -> Coercable::OpReturn', + 38, :-, Coercable.for_op(:-) end def test_op_uneg @@ -118,8 +118,8 @@ def test_op_div assert_send_type '(Complex) -> Complex', 38, :/, 12i - assert_send_type '[O < RBS::Ops::_Times[S, R], S, R] (Numeric::_Coerce[38, O, S]) -> R', - 38, :/, Coercable.new(10i, &:i) + assert_send_type '(Coercable) -> Coercable::OpReturn', + 38, :/, Coercable.for_op(:/) end def test_op_lt @@ -131,8 +131,8 @@ def test_op_lt 38, :<, 12.0 # Notably not `Complex` as complex doesn't define `<` - assert_send_type '[O < RBS::Ops::_LessThan[S, R], S, R] (Numeric::_Coerce[38, O, S]) -> R', - 38, :<, Coercable.new(Set[8, 4]){ |n| n.digits.to_set } + assert_send_type '(Coercable) -> Coercable::OpReturn', + 38, :<, Coercable.for_op(:<) end def test_op_lsh @@ -148,12 +148,20 @@ def test_op_leq 38, :<=, 12.0 # Notably not `Complex` as complex doesn't define `<=` - assert_send_type '[O < RBS::Ops::_LessThanOrEqual[S, R], S, R] (Numeric::_Coerce[38, O, S]) -> R', - 38, :<=, Coercable.new(Set[8, 4]){ |n| n.digits.to_set } + assert_send_type '(Coercable) -> Coercable::OpReturn', + 38, :<=, Coercable.for_op(:<=) end def test_op_cmp - # omit 'todo' + assert_send_type '(Integer) -> (-1 | 0 | 1)', + 38, :<=>, 12 + assert_send_type '(Rational) -> (-1 | 0 | 1)', + 38, :<=>, 12r + + with_untyped.and 12, 12r, 12.0, 12i, Coercable.new(Set[8, 4]){ |n| n.digits.to_set } do |other| + assert_send_type '(untyped) -> Integer?', + 38, :<=>, other + end end def test_op_eq(method: :==) @@ -177,8 +185,8 @@ def test_op_gt 38, :>, 12.0 # Notably not `Complex` as complex doesn't define `>` - assert_send_type '[O < RBS::Ops::_GreaterThan[S, R], S, R] (Numeric::_Coerce[38, O, S]) -> R', - 38, :>, Coercable.new(Set[8, 4]){ |n| n.digits.to_set } + assert_send_type '(Coercable) -> Coercable::OpReturn', + 38, :>, Coercable.for_op(:>) end def test_op_geq @@ -190,8 +198,8 @@ def test_op_geq 38, :>=, 12.0 # Notably not `Complex` as complex doesn't define `>` - assert_send_type '[O < RBS::Ops::_GreaterThanOrEqual[S, R], S, R] (Numeric::_Coerce[38, O, S]) -> R', - 38, :>=, Coercable.new(Set[8, 4]){ |n| n.digits.to_set } + assert_send_type '(Coercable) -> Coercable::OpReturn', + 38, :>=, Coercable.for_op(:>=) end def test_op_rsh diff --git a/test/stdlib/test_helper.rb b/test/stdlib/test_helper.rb index 248404287..228f5e82a 100644 --- a/test/stdlib/test_helper.rb +++ b/test/stdlib/test_helper.rb @@ -81,6 +81,24 @@ def with_timeout(seconds: 1, nanoseconds: 0) end class Coercable < RBS::UnitTest::Convertibles::BlankSlate + class OpReturn < ::RBS::UnitTest::Convertibles::BlankSlate + end + + class CoerceOther < ::RBS::UnitTest::Convertibles::BlankSlate + end + + class CoerceSelf < ::RBS::UnitTest::Convertibles::BlankSlate + def initialize(method = nil) + if method + ::Kernel.instance_method(:define_singleton_method).bind_call(self, method) { |other| OpReturn.new } + end + end + end + + def self.for_op(method) + new(CoerceSelf.new(method)){ |x| CoerceOther.new } + end + def initialize(self_ret, &converter) @self_ret = self_ret @converter = converter || ->(x) { x } From e3b842d46878a4783486ab42057ed12733e23d49 Mon Sep 17 00:00:00 2001 From: SamW Date: Tue, 16 Jun 2026 10:53:06 -0700 Subject: [PATCH 04/17] bitwise and shifts --- core/integer.rbs | 13 ++++++++----- test/stdlib/Integer_test.rb | 28 +++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/core/integer.rbs b/core/integer.rbs index c6a669430..6f61826ef 100644 --- a/core/integer.rbs +++ b/core/integer.rbs @@ -190,7 +190,8 @@ class Integer < Numeric # # Related: Integer#| (bitwise OR), Integer#^ (bitwise EXCLUSIVE OR). # - def &: (Integer) -> Integer + def &: (Integer other) -> Integer + | [O < RBS::Ops::_And[S, R], S, R] (Numeric::_Coerce[self, O, S] other) -> R # + # The minimum number of significant decimal digits in a double-precision + # floating point. + # + # Usually defaults to 15. + # + DIG: Integer + + # + # The difference between 1 and the smallest double-precision floating point + # number greater than 1. + # + # Usually defaults to 2.2204460492503131e-16. + # + EPSILON: Float + + # + # An expression representing positive infinity. + # + INFINITY: Float + + %a{deprecated:Does not actually exist} + Infinity: Float + + # + # The number of base digits for the `double` data type. + # + # Usually defaults to 53. + # + MANT_DIG: Integer + + # + # The largest possible integer in a double-precision floating point number. + # + # Usually defaults to 1.7976931348623157e+308. + # + MAX: Float + + # + # The largest positive exponent in a double-precision floating point where 10 + # raised to this power minus 1. + # + # Usually defaults to 308. + # + MAX_10_EXP: Integer + + # + # The largest possible exponent value in a double-precision floating point. + # + # Usually defaults to 1024. + # + MAX_EXP: Integer + + # + # The smallest positive normalized number in a double-precision floating point. + # + # Usually defaults to 2.2250738585072014e-308. + # + # If the platform supports denormalized numbers, there are numbers between zero + # and Float::MIN. 0.0.next_float returns the smallest positive + # floating point number including denormalized numbers. + # + MIN: Float + + # + # The smallest negative exponent in a double-precision floating point where 10 + # raised to this power minus 1. + # + # Usually defaults to -307. + # + MIN_10_EXP: Integer + + # + # The smallest possible exponent value in a double-precision floating point. + # + # Usually defaults to -1021. + # + MIN_EXP: Integer + + # + # An expression representing a value which is "not a number". + # + NAN: Float + + # + # The base of the floating point, or number of unique digits used to represent + # the number. + # + # Usually defaults to 2 on most systems, which would represent a base-10 + # decimal. + # + RADIX: Integer + + # Deprecated, do not use. + # + # Represents the rounding mode for floating point addition at the start time. + # + # Usually defaults to 1, rounding to the nearest number. + # + # Other modes include: + # + # -1 + # : Indeterminable + # 0 + # : Rounding towards zero + # 1 + # : Rounding to the nearest number + # 2 + # : Rounding towards positive infinity + # 3 + # : Rounding towards negative infinity + # + # + %a{deprecated} + ROUNDS: Integer + # -# The minimum number of significant decimal digits in a double-precision -# floating point. -# -# Usually defaults to 15. -# -Float::DIG: Integer - -# -# The difference between 1 and the smallest double-precision floating point -# number greater than 1. -# -# Usually defaults to 2.2204460492503131e-16. -# -Float::EPSILON: Float - -# -# An expression representing positive infinity. -# -Float::INFINITY: Float - -Float::Infinity: Float - -# -# The number of base digits for the `double` data type. -# -# Usually defaults to 53. -# -Float::MANT_DIG: Integer - -# -# The largest possible integer in a double-precision floating point number. -# -# Usually defaults to 1.7976931348623157e+308. -# -Float::MAX: Float - -# -# The largest positive exponent in a double-precision floating point where 10 -# raised to this power minus 1. -# -# Usually defaults to 308. -# -Float::MAX_10_EXP: Integer - -# -# The largest possible exponent value in a double-precision floating point. -# -# Usually defaults to 1024. -# -Float::MAX_EXP: Integer - -# -# The smallest positive normalized number in a double-precision floating point. -# -# Usually defaults to 2.2250738585072014e-308. -# -# If the platform supports denormalized numbers, there are numbers between zero -# and Float::MIN. 0.0.next_float returns the smallest positive -# floating point number including denormalized numbers. -# -Float::MIN: Float - -# -# The smallest negative exponent in a double-precision floating point where 10 -# raised to this power minus 1. -# -# Usually defaults to -307. -# -Float::MIN_10_EXP: Integer - -# -# The smallest possible exponent value in a double-precision floating point. -# -# Usually defaults to -1021. -# -Float::MIN_EXP: Integer - -# -# An expression representing a value which is "not a number". -# -Float::NAN: Float - -# -# The base of the floating point, or number of unique digits used to represent -# the number. -# -# Usually defaults to 2 on most systems, which would represent a base-10 -# decimal. -# -Float::RADIX: Integer - -# Deprecated, do not use. -# -# Represents the rounding mode for floating point addition at the start time. -# -# Usually defaults to 1, rounding to the nearest number. -# -# Other modes include: -# -# -1 -# : Indeterminable -# 0 -# : Rounding towards zero -# 1 -# : Rounding to the nearest number -# 2 -# : Rounding towards positive infinity -# 3 -# : Rounding towards negative infinity -# -# -Float::ROUNDS: Integer diff --git a/test/stdlib/Float_test.rb b/test/stdlib/Float_test.rb index 4eaf2d4e6..52f862eaf 100644 --- a/test/stdlib/Float_test.rb +++ b/test/stdlib/Float_test.rb @@ -4,6 +4,72 @@ class FloatSingletonTest < Test::Unit::TestCase include TestHelper testing 'singleton(Float)' + + def test_constant_DIG + assert_const_type 'Integer', + 'Float::DIG' + end + + def test_constant_EPSILON + assert_const_type 'Float', + 'Float::EPSILON' + end + + def test_constant_INFINITY + assert_const_type 'Float', + 'Float::INFINITY' + end + + def test_constant_MANT_DIG + assert_const_type 'Integer', + 'Float::MANT_DIG' + end + + def test_constant_MAX + assert_const_type 'Float', + 'Float::MAX' + end + + def test_constant_MAX_10_EXP + assert_const_type 'Integer', + 'Float::MAX_10_EXP' + end + + def test_constant_MAX_EXP + assert_const_type 'Integer', + 'Float::MAX_EXP' + end + + def test_constant_MIN + assert_const_type 'Float', + 'Float::MIN' + end + + def test_constant_MIN_10_EXP + assert_const_type 'Integer', + 'Float::MIN_10_EXP' + end + + def test_constant_MIN_EXP + assert_const_type 'Integer', + 'Float::MIN_EXP' + end + + def test_constant_NAN + assert_const_type 'Float', + 'Float::NAN' + end + + def test_constant_RADIX + assert_const_type 'Integer', + 'Float::RADIX' + end + + def test_constant_ROUNDS + notify "Skipping test: Float::ROUNDS does not exist" unless defined? Float::ROUNDS + assert_const_type 'Integer', + 'Float::ROUNDS' + end end class FloatInstanceTest < Test::Unit::TestCase From c1a53542311c2a7083956ab1ee6cb16ece4f756c Mon Sep 17 00:00:00 2001 From: SamW Date: Wed, 17 Jun 2026 09:57:48 -0700 Subject: [PATCH 10/17] added instance method shell --- test/stdlib/Float_test.rb | 190 +++++++++++++++++++++++++++++++++++++- 1 file changed, 189 insertions(+), 1 deletion(-) diff --git a/test/stdlib/Float_test.rb b/test/stdlib/Float_test.rb index 52f862eaf..fde2bfeef 100644 --- a/test/stdlib/Float_test.rb +++ b/test/stdlib/Float_test.rb @@ -66,7 +66,7 @@ def test_constant_RADIX end def test_constant_ROUNDS - notify "Skipping test: Float::ROUNDS does not exist" unless defined? Float::ROUNDS + return unless defined? Float::ROUNDS assert_const_type 'Integer', 'Float::ROUNDS' end @@ -76,4 +76,192 @@ class FloatInstanceTest < Test::Unit::TestCase include TestHelper testing 'Float' + + def test_op_mod + omit 'todo' + end + + def test_op_mul + omit 'todo' + end + + def test_op_pow + omit 'todo' + end + + def test_op_add + omit 'todo' + end + + def test_op_sub + omit 'todo' + end + + def test_op_uneg + omit 'todo' + end + + def test_op_div + omit 'todo' + end + + def test_op_lth + omit 'todo' + end + + def test_op_leq + omit 'todo' + end + + def test_op_cmp + omit 'todo' + end + + def test_op_eq + omit 'todo' + end + + def test_op_eqq + omit 'todo' + end + + def test_op_gth + omit 'todo' + end + + def test_op_geq + omit 'todo' + end + + def test_abs + omit 'todo' + end + + def test_angle + omit 'todo' + end + + def test_arg + omit 'todo' + end + + def test_ceil + omit 'todo' + end + + def test_coerce + omit 'todo' + end + + def test_denominator + omit 'todo' + end + + def test_divmod + omit 'todo' + end + + def test_eql? + omit 'todo' + end + + def test_fdiv + omit 'todo' + end + + def test_finite? + omit 'todo' + end + + def test_floor + omit 'todo' + end + + def test_hash + omit 'todo' + end + + def test_infinite? + omit 'todo' + end + + def test_inspect + omit 'todo' + end + + def test_magnitude + omit 'todo' + end + + def test_modulo + omit 'todo' + end + + def test_nan? + omit 'todo' + end + + def test_negative? + omit 'todo' + end + + def test_next_float + omit 'todo' + end + + def test_numerator + omit 'todo' + end + + def test_phase + omit 'todo' + end + + def test_positive? + omit 'todo' + end + + def test_prev_float + omit 'todo' + end + + def test_quo + omit 'todo' + end + + def test_rationalize + omit 'todo' + end + + def test_round + omit 'todo' + end + + def test_to_f + omit 'todo' + end + + def test_to_i + omit 'todo' + end + + def test_to_int + omit 'todo' + end + + def test_to_r + omit 'todo' + end + + def test_to_s + omit 'todo' + end + + def test_truncate + omit 'todo' + end + + def test_zero? + omit 'todo' + end end From 4e15c0e7dcfde3f728a4b594757f0045be622378 Mon Sep 17 00:00:00 2001 From: SamW Date: Wed, 17 Jun 2026 10:13:00 -0700 Subject: [PATCH 11/17] first stage of singleton methods --- core/float.rbs | 17 +++-- test/stdlib/Float_test.rb | 128 +++++++++++++++++++++++++++++--------- 2 files changed, 107 insertions(+), 38 deletions(-) diff --git a/core/float.rbs b/core/float.rbs index f3fa7f964..db219b940 100644 --- a/core/float.rbs +++ b/core/float.rbs @@ -576,7 +576,7 @@ class Float < Numeric # # Related: Float#eql? (requires `other` to be a Float). # - def ==: (untyped) -> bool + def ==: (untyped other) -> bool # # Returns `true` if `other` has the same value as `self`, `false` otherwise: @@ -591,7 +591,7 @@ class Float < Numeric # # Related: Float#eql? (requires `other` to be a Float). # - def ===: (untyped) -> bool + alias === == # # Returns the quotient from dividing `self` by `other`: @@ -795,8 +795,7 @@ class Float < Numeric # f.quo(Rational(2, 1)) # => 1.57 # f.quo(Complex(2, 0)) # => (1.57+0.0i) # - def fdiv: (Complex) -> Complex - | (Numeric) -> Float + alias fdiv quo # # Returns a string containing a representation of `self`; depending of the value @@ -964,7 +963,7 @@ class Float < Numeric # 10.0 % 4.0 # => 2.0 # 10.0 % Rational(4, 1) # => 2.0 # - def modulo: (Numeric) -> Float + alias modulo % # # Returns `self` (which is already a Float). # - def to_f: () -> Float + def to_f: () -> self # # Returns 0 if `self` is positive, Math::PI otherwise. # - def angle: ... + alias angle arg # # Returns 0 if `self` is positive, Math::PI otherwise. # - alias arg angle + def arg: () -> (0 | Float) # NB: Can also return `NaN` if self is NaN # # Returns 0 if `self` is positive, Math::PI otherwise. # - alias phase angle + alias phase arg def polar: () -> [ Float, Integer | Float ] @@ -1108,8 +1117,8 @@ class Float < Numeric # f.quo(Rational(2, 1)) # => 1.57 # f.quo(Complex(2, 0)) # => (1.57+0.0i) # - def quo: (Complex) -> Complex - | (Numeric) -> Float + def quo: (Integer | Float other) -> Float + | [S, R] (Numeric::_Coerce[self, RBS::Ops::_Divide[S, R], S] other) -> R #