Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #hash for the Big* number types #14308

Merged
merged 10 commits into from
Feb 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions spec/std/big/big_rational_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -466,12 +466,6 @@ describe BigRational do
it { br(7, -3).integer?.should be_false }
end

it "#hash" do
b = br(10, 3)
hash = b.hash
hash.should eq(b.to_f64.hash)
end

it "is a number" do
br(10, 3).is_a?(Number).should be_true
end
Expand Down
83 changes: 82 additions & 1 deletion spec/std/crystal/hasher_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -305,13 +305,14 @@ describe "Crystal::Hasher" do
Crystal::Hasher.reduce_num(Float64::MAX).should eq(0x1F00_FFFF_FFFF_FFFF_u64)
end

pending "reduces BigInt" do
it "reduces BigInt" do
Crystal::Hasher.reduce_num(0.to_big_i).should eq(0_u64)
Crystal::Hasher.reduce_num(1.to_big_i).should eq(1_u64)
Crystal::Hasher.reduce_num((-1).to_big_i).should eq(UInt64::MAX)

(1..300).each do |i|
Crystal::Hasher.reduce_num(2.to_big_i ** i).should eq(1_u64 << (i % 61))
Crystal::Hasher.reduce_num(-(2.to_big_i ** i)).should eq(&-(1_u64 << (i % 61)))
end
end

Expand All @@ -324,8 +325,88 @@ describe "Crystal::Hasher" do

(1..300).each do |i|
Crystal::Hasher.reduce_num(2.to_big_f ** i).should eq(1_u64 << (i % 61))
Crystal::Hasher.reduce_num(-(2.to_big_f ** i)).should eq(&-(1_u64 << (i % 61)))
Crystal::Hasher.reduce_num(0.5.to_big_f ** i).should eq(1_u64 << ((-i) % 61))
Crystal::Hasher.reduce_num(-(0.5.to_big_f ** i)).should eq(&-(1_u64 << ((-i) % 61)))
end
end

it "reduces BigDecimal" do
Crystal::Hasher.reduce_num(0.to_big_d).should eq(0_u64)
Crystal::Hasher.reduce_num(1.to_big_d).should eq(1_u64)
Crystal::Hasher.reduce_num((-1).to_big_d).should eq(UInt64::MAX)

# small inverse powers of 10
Crystal::Hasher.reduce_num(BigDecimal.new(1, 1)).should eq(0x1CCCCCCCCCCCCCCC_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 2)).should eq(0x0FAE147AE147AE14_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 3)).should eq(0x0E5E353F7CED9168_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 4)).should eq(0x14A305532617C1BD_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 5)).should eq(0x05438088509BF9C6_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 6)).should eq(0x06ED2674080F98FA_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 7)).should eq(0x1A4AEA3ECD9B28E5_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 8)).should eq(0x12A1176CAE291DB0_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 9)).should eq(0x01DCE8BE116A82F8_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 10)).should eq(0x1362E41301BDD9E5_u64)

# a^(p-1) === 1 (mod p)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 0x1FFFFFFFFFFFFFFE_u64)).should eq(1_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 0x1FFFFFFFFFFFFFFD_u64)).should eq(10_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 0x1FFFFFFFFFFFFFFC_u64)).should eq(100_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 0x1FFFFFFFFFFFFFFB_u64)).should eq(1000_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 0x1FFFFFFFFFFFFFFA_u64)).should eq(10000_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 0x1FFFFFFFFFFFFFF9_u64)).should eq(100000_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 0x1FFFFFFFFFFFFFF8_u64)).should eq(1000000_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 0x1FFFFFFFFFFFFFF7_u64)).should eq(10000000_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 0x1FFFFFFFFFFFFFF6_u64)).should eq(100000000_u64)
Crystal::Hasher.reduce_num(BigDecimal.new(1, 0x1FFFFFFFFFFFFFF5_u64)).should eq(1000000000_u64)

(1..300).each do |i|
Crystal::Hasher.reduce_num(2.to_big_d ** i).should eq(1_u64 << (i % 61))
Crystal::Hasher.reduce_num(-(2.to_big_d ** i)).should eq(&-(1_u64 << (i % 61)))
Crystal::Hasher.reduce_num(0.5.to_big_d ** i).should eq(1_u64 << ((-i) % 61))
Crystal::Hasher.reduce_num(-(0.5.to_big_d ** i)).should eq(&-(1_u64 << ((-i) % 61)))
end
end

it "reduces BigRational" do
Crystal::Hasher.reduce_num(0.to_big_r).should eq(0_u64)
Crystal::Hasher.reduce_num(1.to_big_r).should eq(1_u64)
Crystal::Hasher.reduce_num((-1).to_big_r).should eq(UInt64::MAX)

# inverses of small integers
Crystal::Hasher.reduce_num(BigRational.new(1, 2)).should eq(0x1000000000000000_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 3)).should eq(0x1555555555555555_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 4)).should eq(0x0800000000000000_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 5)).should eq(0x1999999999999999_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 6)).should eq(0x1AAAAAAAAAAAAAAA_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 7)).should eq(0x1B6DB6DB6DB6DB6D_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 8)).should eq(0x0400000000000000_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 9)).should eq(0x1C71C71C71C71C71_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 10)).should eq(0x1CCCCCCCCCCCCCCC_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 11)).should eq(0x1D1745D1745D1745_u64)

Crystal::Hasher.reduce_num(BigRational.new(1, 0x1000000000000000_u64)).should eq(2_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 0x1555555555555555_u64)).should eq(3_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 0x0800000000000000_u64)).should eq(4_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 0x1999999999999999_u64)).should eq(5_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 0x1AAAAAAAAAAAAAAA_u64)).should eq(6_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 0x1B6DB6DB6DB6DB6D_u64)).should eq(7_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 0x0400000000000000_u64)).should eq(8_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 0x1C71C71C71C71C71_u64)).should eq(9_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 0x1CCCCCCCCCCCCCCC_u64)).should eq(10_u64)
Crystal::Hasher.reduce_num(BigRational.new(1, 0x1D1745D1745D1745_u64)).should eq(11_u64)

(1..300).each do |i|
Crystal::Hasher.reduce_num(2.to_big_r ** i).should eq(1_u64 << (i % 61))
Crystal::Hasher.reduce_num(-(2.to_big_r ** i)).should eq(&-(1_u64 << (i % 61)))
Crystal::Hasher.reduce_num(0.5.to_big_r ** i).should eq(1_u64 << ((-i) % 61))
Crystal::Hasher.reduce_num(-(0.5.to_big_r ** i)).should eq(&-(1_u64 << ((-i) % 61)))
end

Crystal::Hasher.reduce_num(BigRational.new(1, 0x1FFF_FFFF_FFFF_FFFF_u64)).should eq(Crystal::Hasher::HASH_INF_PLUS)
Crystal::Hasher.reduce_num(BigRational.new(-1, 0x1FFF_FFFF_FFFF_FFFF_u64)).should eq(Crystal::Hasher::HASH_INF_MINUS)
Crystal::Hasher.reduce_num(BigRational.new(2, 0x1FFF_FFFF_FFFF_FFFF_u64)).should eq(Crystal::Hasher::HASH_INF_PLUS)
Crystal::Hasher.reduce_num(BigRational.new(-2, 0x1FFF_FFFF_FFFF_FFFF_u64)).should eq(Crystal::Hasher::HASH_INF_MINUS)
end
end
end
23 changes: 19 additions & 4 deletions src/big/big_decimal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -753,10 +753,6 @@ struct BigDecimal < Number
self
end

def hash(hasher)
hasher.string(to_s)
end

# Returns the *quotient* as absolutely negative if `self` and *other* have
# different signs, otherwise returns the *quotient*.
def normalize_quotient(other : BigDecimal, quotient : BigInt) : BigInt
Expand Down Expand Up @@ -879,3 +875,22 @@ class String
BigDecimal.new(self)
end
end

# :nodoc:
struct Crystal::Hasher
def self.reduce_num(value : BigDecimal)
v = reduce_num(value.value.abs)

# v = UInt64.mulmod(v, 10_u64.powmod(-scale, HASH_MODULUS), HASH_MODULUS)
# TODO: consider #7516 or similar
scale = value.scale
x = 0x1ccc_cccc_cccc_cccc_u64 # 10^-1 (mod HASH_MODULUS)
while scale > 0
v = UInt64.mulmod(v, x, HASH_MODULUS) if scale.bits_set?(1)
scale = scale.unsafe_shr(1)
x = UInt64.mulmod(x, x, HASH_MODULUS)
end

v &* value.sign
end
end
3 changes: 0 additions & 3 deletions src/big/big_float.cr
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,6 @@ struct BigFloat < Float
new(mpf)
end

# TODO: improve this
def_hash to_f64

def self.default_precision
LibGMP.mpf_get_default_prec
end
Expand Down
21 changes: 7 additions & 14 deletions src/big/big_int.cr
Original file line number Diff line number Diff line change
Expand Up @@ -488,9 +488,6 @@ struct BigInt < Int
LibGMP.sizeinbase(self, 2).to_i
end

# TODO: check hash equality for numbers >= 2**63
def_hash to_i64!

def to_s(base : Int = 10, *, precision : Int = 1, upcase : Bool = false) : String
raise ArgumentError.new("Invalid base #{base}") unless 2 <= base <= 36 || base == 62
raise ArgumentError.new("upcase must be false for base 62") if upcase && base == 62
Expand Down Expand Up @@ -985,18 +982,14 @@ end

# :nodoc:
struct Crystal::Hasher
private HASH_MODULUS_INT_P = BigInt.new((1_u64 << HASH_BITS) - 1)
private HASH_MODULUS_INT_N = -BigInt.new((1_u64 << HASH_BITS) - 1)
private HASH_MODULUS_INT_P = BigInt.new(HASH_MODULUS)

def self.reduce_num(value : BigInt)
# it should calculate `remainder(HASH_MODULUS)`
if LibGMP::UI == UInt64
v = LibGMP.tdiv_ui(value, HASH_MODULUS).to_i64
value < 0 ? -v : v
elsif value >= HASH_MODULUS_INT_P || value <= HASH_MODULUS_INT_N
value.unsafe_truncated_mod(HASH_MODULUS_INT_P).to_i64
else
value.to_i64
end
{% if LibGMP::UI == UInt64 %}
v = LibGMP.tdiv_ui(value, HASH_MODULUS)
value < 0 ? &-v : v
{% else %}
value.remainder(HASH_MODULUS_INT_P).to_u64!
{% end %}
end
end
20 changes: 6 additions & 14 deletions src/big/big_rational.cr
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,6 @@ struct BigRational < Number
BigRational.new { |mpq| LibGMP.mpq_abs(mpq, self) }
end

# TODO: improve this
def_hash to_f64

# Returns the `Float64` representing this rational.
def to_f : Float64
to_f64
Expand Down Expand Up @@ -459,18 +456,13 @@ end

# :nodoc:
struct Crystal::Hasher
private HASH_MODULUS_RAT_P = BigRational.new((1_u64 << HASH_BITS) - 1)
private HASH_MODULUS_RAT_N = -BigRational.new((1_u64 << HASH_BITS) - 1)

def self.reduce_num(value : BigRational)
rem = value
if value >= HASH_MODULUS_RAT_P || value <= HASH_MODULUS_RAT_N
num = value.numerator
denom = value.denominator
div = num.tdiv(denom)
floor = div.tdiv(HASH_MODULUS)
rem -= floor * HASH_MODULUS
inverse = BigInt.new do |mpz|
if LibGMP.invert(mpz, value.denominator, HASH_MODULUS_INT_P) == 0
# inverse doesn't exist, i.e. denominator is a multiple of HASH_MODULUS
return value >= 0 ? HASH_INF_PLUS : HASH_INF_MINUS
end
end
rem.to_big_f.hash
UInt64.mulmod(reduce_num(value.numerator.abs), inverse.to_u64!, HASH_MODULUS) &* value.sign
end
end
1 change: 1 addition & 0 deletions src/big/lib_gmp.cr
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ lib LibGMP
fun gcd_ui = __gmpz_gcd_ui(rop : MPZ*, op1 : MPZ*, op2 : UI) : UI
fun lcm = __gmpz_lcm(rop : MPZ*, op1 : MPZ*, op2 : MPZ*)
fun lcm_ui = __gmpz_lcm_ui(rop : MPZ*, op1 : MPZ*, op2 : UI)
fun invert = __gmpz_invert(rop : MPZ*, op1 : MPZ*, op2 : MPZ*) : Int
fun remove = __gmpz_remove(rop : MPZ*, op : MPZ*, f : MPZ*) : BitcntT

# # Miscellaneous Functions
Expand Down
26 changes: 26 additions & 0 deletions src/int.cr
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,32 @@ struct Int
{% end %}
end

# :nodoc:
#
# Computes (x * y) % z, but without intermediate overflows.
# Precondition: `0 <= x < z && y >= 0`
def self.mulmod(x, y, z)
result = zero
while y > 0
if y.bits_set?(1)
# result = (result + x) % z
if result >= z &- x
result &-= z &- x
else
result &+= x
end
end
# x = (x + x) % z
if x >= z &- x
x &-= z &- x
else
x = x.unsafe_shl(1)
end
y = y.unsafe_shr(1)
end
result
end

# Returns the result of shifting this number's bits *count* positions to the right.
# Also known as arithmetic right shift.
#
Expand Down
Loading