From f892030e0e54d3daaad8406166bb4f25bce22259 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Tue, 16 Jun 2026 11:01:00 +0900 Subject: [PATCH 1/3] Add constant drift test for platform-invariant core classes Compares Module#constants(false) against the constants declared in the loaded RBS environment for a curated set of platform/build-invariant core classes (Float, Integer, String, ...), failing on either: * stale - declared in RBS but no longer defined at runtime, or * missing - defined at runtime but absent from the RBS signature. Classes whose constant set varies by OS or build options (Process, Socket, Errno, ...) are intentionally excluded, with a SKIP map for per-constant exceptions. --- test/stdlib/constant_drift_test.rb | 72 ++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 test/stdlib/constant_drift_test.rb diff --git a/test/stdlib/constant_drift_test.rb b/test/stdlib/constant_drift_test.rb new file mode 100644 index 000000000..53b3ecf69 --- /dev/null +++ b/test/stdlib/constant_drift_test.rb @@ -0,0 +1,72 @@ +require_relative "test_helper" + +# Guards against "constant drift" between the RBS core signatures and the actual +# runtime, in both directions: +# +# * a constant declared in RBS but no longer defined by Ruby (e.g. +# `Float::ROUNDS`, removed in Ruby 3.0), and +# * a constant defined by Ruby but missing from RBS. +# +# Only platform- and build-invariant core classes are hard-gated here. Classes +# whose constant set legitimately varies by OS or build options (`Process`, +# `Socket`, `Errno`, `Signal`, `File::Constants`, `RbConfig`, `Etc`, ...) are +# intentionally excluded: their RBS declarations cannot match any single +# platform's runtime. `Object`/`BasicObject`/`Kernel` are excluded too, since +# every top-level class shows up under `Object.constants`. +class ConstantDriftTest < Test::Unit::TestCase + # Platform/build-invariant core classes and modules whose declared constant + # set must match the runtime exactly. + HARD_GATE = [ + Float, Integer, Numeric, Rational, Complex, + Math, Comparable, + String, Symbol, + Array, Hash, Range, Struct, + NilClass, TrueClass, FalseClass + ].freeze + + # Known, intentional exceptions keyed by "::Name" => [:CONST, ...]. Use this + # for runtime constants that are `private_constant` or otherwise legitimately + # undeclared, so the gate stays green there without being weakened elsewhere. + SKIP = {}.freeze + + def env + StdlibTest::DEFAULT_ENV + end + + # Constants declared directly under `type_name` in the loaded RBS environment + # (plain constants plus nested classes/modules and their aliases), matching + # what `Module#constants(false)` returns at runtime. + def rbs_constants(type_name) + prefix = "#{type_name}::" + names = [] + [env.constant_decls, env.class_decls, env.class_alias_decls].each do |store| + store.each_key do |tn| + s = tn.to_s + next unless s.start_with?(prefix) + + rest = s.delete_prefix(prefix) + names << rest.to_sym unless rest.include?("::") + end + end + names.uniq.sort + end + + HARD_GATE.each do |klass| + define_method(:"test_no_constant_drift_#{klass.name.gsub("::", "_")}") do + type_name = "::#{klass.name}" + skip = SKIP[type_name] || [] + runtime = (klass.constants(false) - skip).sort + declared = (rbs_constants(type_name) - skip).sort + + stale = declared - runtime + missing = runtime - declared + + assert_empty stale, + "RBS declares #{type_name} constants that no longer exist at runtime: #{stale.inspect}. " \ + "Remove them from the signature (or add to ConstantDriftTest::SKIP if intentional)." + assert_empty missing, + "Runtime defines #{type_name} constants missing from RBS: #{missing.inspect}. " \ + "Add them to the signature (or add to ConstantDriftTest::SKIP if intentional)." + end + end +end From 1c275f7ec8bd452e4ca3af47ca7dde81823e6c5e Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Tue, 16 Jun 2026 11:01:09 +0900 Subject: [PATCH 2/3] Remove stale Float::ROUNDS and Float::Infinity constants Both are declared in core/float.rbs but raise NameError at runtime: * Float::ROUNDS was removed from Ruby in 3.0 (ruby/ruby#2953). * Float::Infinity was added in #1095 (which superseded #1080). Ruby does not define this constant: looking at the Ruby of that period (3.1), numeric.c defines Float::INFINITY but not Float::Infinity, and it still raises NameError today. Also add singleton constant type assertions for the remaining Float constants, so their declared types stay verified against the runtime. --- core/float.rbs | 24 ----------------- test/stdlib/Float_test.rb | 54 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/core/float.rbs b/core/float.rbs index d5d00a50a..dd4c1a7fd 100644 --- a/core/float.rbs +++ b/core/float.rbs @@ -1222,8 +1222,6 @@ Float::EPSILON: Float # Float::INFINITY: Float -Float::Infinity: Float - # # The number of base digits for the `double` data type. # @@ -1292,25 +1290,3 @@ Float::NAN: Float # 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 4981ac9e2..f6873779e 100644 --- a/test/stdlib/Float_test.rb +++ b/test/stdlib/Float_test.rb @@ -1,5 +1,59 @@ require_relative "test_helper" +class FloatSingletonTest < Test::Unit::TestCase + include TestHelper + + testing 'singleton(::Float)' + + def test_DIG + assert_const_type 'Integer', 'Float::DIG' + end + + def test_EPSILON + assert_const_type 'Float', 'Float::EPSILON' + end + + def test_INFINITY + assert_const_type 'Float', 'Float::INFINITY' + end + + def test_MANT_DIG + assert_const_type 'Integer', 'Float::MANT_DIG' + end + + def test_MAX + assert_const_type 'Float', 'Float::MAX' + end + + def test_MAX_10_EXP + assert_const_type 'Integer', 'Float::MAX_10_EXP' + end + + def test_MAX_EXP + assert_const_type 'Integer', 'Float::MAX_EXP' + end + + def test_MIN + assert_const_type 'Float', 'Float::MIN' + end + + def test_MIN_10_EXP + assert_const_type 'Integer', 'Float::MIN_10_EXP' + end + + def test_MIN_EXP + assert_const_type 'Integer', 'Float::MIN_EXP' + end + + def test_NAN + assert_const_type 'Float', 'Float::NAN' + end + + def test_RADIX + assert_const_type 'Integer', 'Float::RADIX' + end +end + class FloatTest < StdlibTest target Float From 1827122d53dbd93b88e1737da1e64302c60359d5 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Tue, 16 Jun 2026 11:22:38 +0900 Subject: [PATCH 3/3] Skip build-conditional Integer::GMP_VERSION in the drift test Integer::GMP_VERSION is defined only when Ruby is built with GMP (USE_GMP), so it is present on the Linux CI build but absent on macOS. Add it to the SKIP map so the gate stays green across platforms. --- test/stdlib/constant_drift_test.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/stdlib/constant_drift_test.rb b/test/stdlib/constant_drift_test.rb index 53b3ecf69..4fb99075a 100644 --- a/test/stdlib/constant_drift_test.rb +++ b/test/stdlib/constant_drift_test.rb @@ -25,9 +25,14 @@ class ConstantDriftTest < Test::Unit::TestCase ].freeze # Known, intentional exceptions keyed by "::Name" => [:CONST, ...]. Use this - # for runtime constants that are `private_constant` or otherwise legitimately - # undeclared, so the gate stays green there without being weakened elsewhere. - SKIP = {}.freeze + # for build- or platform-conditional constants (and `private_constant`s) that + # are legitimately undeclared, so the gate stays green across CI platforms + # without being weakened elsewhere. + SKIP = { + # Defined only when Ruby is built with GMP (USE_GMP): present on the Linux + # CI build, absent on e.g. macOS. + "::Integer" => [:GMP_VERSION] + }.freeze def env StdlibTest::DEFAULT_ENV