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

Introduce Crystal::Hasher for computing a hash value #4946

Merged
merged 1 commit into from
Sep 11, 2017
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
9 changes: 6 additions & 3 deletions spec/std/big/big_int_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,12 @@ describe "BigInt" do
end

it "#hash" do
hash = 5.to_big_i.hash
hash.should eq(5)
typeof(hash).should eq(UInt64)
b1 = 5.to_big_i
b2 = 5.to_big_i
b3 = 6.to_big_i

b1.hash.should eq(b2.hash)
b1.hash.should_not eq(b3.hash)
end

it "clones" do
Expand Down
3 changes: 1 addition & 2 deletions spec/std/bool_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ describe "Bool" do
end

describe "hash" do
it { true.hash.should eq(1) }
it { false.hash.should eq(0) }
it { true.hash.should_not eq(false.hash) }
end

describe "to_s" do
Expand Down
2 changes: 1 addition & 1 deletion spec/std/enum_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ describe Enum do
end

it "has hash" do
SpecEnum::Two.hash.should eq(1.hash)
SpecEnum::Two.hash.should_not eq(SpecEnum::Three.hash)
end

it "parses" do
Expand Down
4 changes: 2 additions & 2 deletions spec/std/float_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,11 @@ describe "Float" do

describe "hash" do
it "does for Float32" do
1.2_f32.hash.should_not eq(0)
1.2_f32.hash.should eq(1.2_f32.hash)
end

it "does for Float64" do
1.2.hash.should_not eq(0)
1.2.hash.should eq(1.2.hash)
end
end

Expand Down
9 changes: 4 additions & 5 deletions spec/std/hash_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ describe "Hash" do
end

it "works with mixed types" do
{1 => :a, "a" => 1, 1.0 => "a", :a => 1.0}.values_at(1, "a", 1.0, :a).should eq({:a, 1, "a", 1.0})
{1 => :a, "a" => 1, 2.0 => "a", :a => 1.0}.values_at(1, "a", 2.0, :a).should eq({:a, 1, "a", 1.0})
end
end

Expand Down Expand Up @@ -542,14 +542,13 @@ describe "Hash" do
end

it "computes hash" do
h = { {1 => 2} => {3 => 4} }
h.hash.should_not eq(h.object_id)

h1 = { {1 => 2} => {3 => 4} }
h2 = { {1 => 2} => {3 => 4} }
h.hash.should eq(h2.hash)
h1.hash.should eq(h2.hash)

h3 = {1 => 2, 3 => 4}
h4 = {3 => 4, 1 => 2}

h3.hash.should eq(h4.hash)
end

Expand Down
2 changes: 1 addition & 1 deletion spec/std/named_tuple_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ describe "NamedTuple" do

it "computes a hash value" do
tup1 = {a: 1, b: 'a'}
tup1.hash.should_not eq(0)
tup1.hash.should eq(tup1.dup.hash)

tup2 = {b: 'a', a: 1}
tup2.hash.should eq(tup1.hash)
Expand Down
4 changes: 1 addition & 3 deletions spec/std/proc_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,5 @@ describe "Proc" do
f2.call('r').should eq(2)
end

it "#hash" do
->{ 1 }.hash.should_not eq(0)
end
typeof(->{ 1 }.hash)
end
5 changes: 3 additions & 2 deletions spec/std/struct_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ describe "Struct" do

it "does hash" do
s = StructSpec::TestClass.new(1, "hello")
s.hash.should eq(31 + "hello".hash)
s.hash.should eq(s.dup.hash)
end

it "does hash for struct wrapper (#1940)" do
StructSpec::BigIntWrapper.new(BigInt.new(0)).hash.should eq(0)
s = StructSpec::BigIntWrapper.new(BigInt.new(0))
s.hash.should eq(s.dup.hash)
end

it "does dup" do
Expand Down
4 changes: 3 additions & 1 deletion spec/std/time/span_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ describe Time::Span do
end

it "test hash code" do
Time::Span.new(77).hash.should eq(77)
t1 = Time::Span.new(77)
t2 = Time::Span.new(77)
t1.hash.should eq(t2.hash)
end

it "test subtract" do
Expand Down
5 changes: 2 additions & 3 deletions src/big/big_float.cr
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,8 @@ struct BigFloat < Float
new(mpf)
end

def hash
to_f64.hash
end
# TODO: improve this
def_hash to_f64

def self.default_precision
LibGMP.mpf_get_default_prec
Expand Down
5 changes: 2 additions & 3 deletions src/big/big_int.cr
Original file line number Diff line number Diff line change
Expand Up @@ -343,9 +343,8 @@ struct BigInt < Int
io << "_big_i"
end

def hash
to_u64
end
# TODO: improve this
def_hash to_u64

# Returns a string representation of self.
#
Expand Down
5 changes: 2 additions & 3 deletions src/big/big_rational.cr
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,8 @@ struct BigRational < Number
BigRational.new { |mpq| LibGMP.mpq_abs(mpq, self) }
end

def hash
to_f64.hash
end
# TODO: improve this
def_hash to_f64

# Returns the `Float64` representing this rational.
def to_f
Expand Down
6 changes: 3 additions & 3 deletions src/bool.cr
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ struct Bool
self != other
end

# Returns a hash value for this boolean: 0 for `false`, 1 for `true`.
def hash
self ? 1 : 0
# See `Object#hash(hasher)`
def hash(hasher)
hasher.bool(self)
end

# Returns `"true"` for `true` and `"false"` for `false`.
Expand Down
6 changes: 3 additions & 3 deletions src/char.cr
Original file line number Diff line number Diff line change
Expand Up @@ -414,9 +414,9 @@ struct Char
Unicode.upcase(self, options) { |char| yield char }
end

# Returns this char's codepoint.
def hash
ord
# See `Object#hash(hasher)`
def hash(hasher)
hasher.char(self)
end

# Returns a Char that is one codepoint bigger than this char's codepoint.
Expand Down
5 changes: 3 additions & 2 deletions src/class.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ class Class
to_s(io)
end

def hash
crystal_type_id
# See `Object#hash(hasher)`
def hash(hasher)
hasher.class(self)
end

def ==(other : Class)
Expand Down
12 changes: 3 additions & 9 deletions src/compiler/crystal/syntax/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1175,9 +1175,7 @@ module Crystal
self
end

def hash
0
end
def_hash
end

# A qualified identifier.
Expand Down Expand Up @@ -1545,9 +1543,7 @@ module Crystal
Self.new
end

def hash
0
end
def_hash
end

abstract class ControlExpression < ASTNode
Expand Down Expand Up @@ -2025,9 +2021,7 @@ module Crystal
Underscore.new
end

def hash
0
end
def_hash
end

class Splat < UnaryExpression
Expand Down
59 changes: 59 additions & 0 deletions src/crystal/hasher.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
require "crystal/system/random"

# :nodoc:
struct Crystal::Hasher
# Implementation of a Hasher to compute a fast and safe hash
# value for primitive and basic Crystal objects. All other
# hashes are computed based on these.
#
# TODO: the implementation is naive and should probably use
# another algorithm like SipHash or FNV.

@@seed = uninitialized UInt64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@seed = Random::System.rand(UInt64::MIN..UInt64::MAX) with #4894

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It breaks the compiler at all with compiler bug inside. I don't know why :)

But I'll pull request with Random::Secure.

Crystal::System::Random.random_bytes(Slice.new(pointerof(@@seed).as(UInt8*), 8))

property result : UInt64 = @@seed

def nil
self
end

def bool(value)
(value ? 1 : 0).hash(self)
end

def int(value)
@result = @result * 31 + value.to_u64
self
end

def float(value)
@result *= @result * 31 + value.to_f64.unsafe_as(UInt64)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this *= intended?

self
end

def char(value)
value.ord.hash(self)
end

def enum(value)
value.value.hash(self)
end

def symbol(value)
value.to_i.hash(self)
end

def reference(value)
@result = @result * 31 + value.object_id.to_u64
self
end

def string(value)
value.to_slice.hash(self)
end

def class(value)
value.crystal_type_id.hash(self)
end
end
6 changes: 3 additions & 3 deletions src/enum.cr
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,9 @@ struct Enum
value == other.value
end

# Returns a hash value. This is the hash of the underlying value.
def hash
value.hash
# See `Object#hash(hasher)`
def hash(hasher)
hasher.enum(self)
end

# Iterates each values in a Flags Enum.
Expand Down
13 changes: 5 additions & 8 deletions src/float.cr
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ struct Float
end
end

# See `Object#hash(hasher)`
def hash(hasher)
hasher.float(self)
end

# Writes this float to the given *io* in the given *format*.
# See also: `IO#write_bytes`.
def to_io(io : IO, format : IO::ByteFormat)
Expand Down Expand Up @@ -153,10 +158,6 @@ struct Float32
io << "_f32"
end

def hash
unsafe_as(Int32)
end

def clone
self
end
Expand Down Expand Up @@ -211,10 +212,6 @@ struct Float64
Printer.print(self, io)
end

def hash
unsafe_as(Int64)
end

def clone
self
end
Expand Down
26 changes: 15 additions & 11 deletions src/hash.cr
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require "crystal/hasher"

# A `Hash` represents a mapping of keys to values.
#
# See the [official docs](http://crystal-lang.org/docs/syntax_and_semantics/literals/hash.html) for the basics.
Expand Down Expand Up @@ -706,18 +708,20 @@ class Hash(K, V)
true
end

# See also: `Object#hash`.
#
# ```
# foo = {"foo" => "bar"}
# foo.hash # => 3247054
# ```
def hash
hash = size
# See `Object#hash(hasher)`
def hash(hasher)
# The hash value must be the same regardless of the
# order of the keys.
result = hasher.result

each do |key, value|
hash += key.hash ^ value.hash
copy = hasher
copy = key.hash(copy)
copy = value.hash(copy)
result += copy.result
end
hash

result.hash(hasher)
end

# Duplicates a `Hash`.
Expand Down Expand Up @@ -864,7 +868,7 @@ class Hash(K, V)
end

private def bucket_index(key)
key.hash.to_u32.remainder(@buckets_size).to_i
key.hash.remainder(@buckets_size).to_i
end

private def calculate_new_size(size)
Expand Down
8 changes: 3 additions & 5 deletions src/http/headers.cr
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ struct HTTP::Headers
record Key, name : String do
forward_missing_to @name

def hash
h = 0
def hash(hasher)
name.each_byte do |c|
c = normalize_byte(c)
h = 31 * h + c
hasher = normalize_byte(c).hash(hasher)
end
h
hasher
end

def ==(key2)
Expand Down
Loading