From 744ed174fa14911840027bab2a1d2fa3349d9454 Mon Sep 17 00:00:00 2001 From: Justin Mimbs Date: Mon, 3 Feb 2025 21:56:31 -0500 Subject: [PATCH 1/2] Include hash in enumbatteries --- Project.toml | 2 +- src/StructHelpers.jl | 64 +++++++++++++++++++++++++++++--------------- test/runtests.jl | 17 +++++++----- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/Project.toml b/Project.toml index 43ecb21..4b39b5f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "StructHelpers" uuid = "4093c41a-2008-41fd-82b8-e3f9d02b504f" authors = ["Jan Weidner and contributors"] -version = "1.2.0" +version = "1.3.0" [deps] ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" diff --git a/src/StructHelpers.jl b/src/StructHelpers.jl index a566902..7a88a15 100644 --- a/src/StructHelpers.jl +++ b/src/StructHelpers.jl @@ -27,7 +27,7 @@ of `obj` or handle them in a special way. """ function hash_eq_as(obj) # it would be better to just use getproperties - # but this would cause hashed to change, which we want to + # but this would cause hashed to change, which we want to # keep backwards compatible for now. # # TODO: Change to getproperties once we want to make a hash breaking change @@ -50,10 +50,10 @@ end isequal(getproperties(o1), getproperties(o2)) end -function start_hash(o, h, typesalt::Nothing) +function start_hash(o, h, typesalt::Nothing) Base.hash(typeof(o), h) end -function start_hash(o, h, typesalt) +function start_hash(o, h, typesalt) Base.hash(typesalt, h) end @@ -166,15 +166,10 @@ macro batteries(T, kw...) Got: $nt """) end - if val isa Bool - - elseif pname == :typesalt - typesalt = val - if !(typesalt isa Union{Nothing,Integer}) - error("""`typesalt` must be literally `nothing` or an unsigned integer. Got: - typesalt = $(repr(typesalt))::$(typeof(typesalt)) - """) - end + if pname == :typesalt + check_typesalt(val) + elseif val isa Bool + # pass else error(""" Bad keyword argument value: @@ -197,7 +192,7 @@ macro batteries(T, kw...) push!(ret.args, :(import StructTypes as $ST)) end if nt.hash - def = :(function Base.hash(o::$T, h::UInt) + def = :(function Base.hash(o::$T, h::UInt) h = ($start_hash)(o, h, $(nt.typesalt)) proxy = ($hash_eq_as)(o) Base.hash(proxy, h) @@ -213,7 +208,7 @@ macro batteries(T, kw...) push!(ret.args, def) end if nt.isequal - def = :(function Base.isequal(o1::$T, o2::$T) + def = :(function Base.isequal(o1::$T, o2::$T) isequal($hash_eq_as(o1), $hash_eq_as(o2)) end ) @@ -255,6 +250,17 @@ function def_has_batteries(T) ) end +function check_typesalt(typesalt) + if !(typesalt isa Union{Nothing,Integer}) + error( + """ + `typesalt` must be literally `nothing` or an unsigned integer. Got: + typesalt = $(repr(typesalt))::$(typeof(typesalt)) + """ + ) + end +end + function error_parse_macro_kw(kw; comment=nothing) msg = """ Expected a keyword argument of the form name = value. @@ -351,15 +357,19 @@ function def_symbol_or_enum_from_string_body(f,T) end const ENUM_BATTERIES_DEFAULTS = ( - string_conversion=false, - symbol_conversion=false, - selfconstructor=BATTERIES_DEFAULTS.selfconstructor, + string_conversion = false, + symbol_conversion = false, + selfconstructor = BATTERIES_DEFAULTS.selfconstructor, + hash = BATTERIES_DEFAULTS.hash, + typesalt = BATTERIES_DEFAULTS.typesalt, ) const ENUM_BATTERIES_DOCSTRINGS = ( - string_conversion="Add `convert(MyEnum, ::String)`, `MyEnum(::String)`, `convert(String, ::MyEnum)` and `String(::MyEnum)`", - symbol_conversion="Add `convert(MyEnum, ::Symbol)`, `MyEnum(::Symbol)`, `convert(Symbol, ::MyEnum)` and `Symbol(::MyEnum)`", - selfconstructor=BATTERIES_DOCSTRINGS.selfconstructor, + string_conversion = "Add `convert(MyEnum, ::String)`, `MyEnum(::String)`, `convert(String, ::MyEnum)` and `String(::MyEnum)`", + symbol_conversion = "Add `convert(MyEnum, ::Symbol)`, `MyEnum(::Symbol)`, `convert(Symbol, ::MyEnum)` and `Symbol(::MyEnum)`", + selfconstructor = BATTERIES_DOCSTRINGS.selfconstructor, + hash = BATTERIES_DOCSTRINGS.hash, + typesalt = BATTERIES_DOCSTRINGS.typesalt, ) if (keys(ENUM_BATTERIES_DEFAULTS) != keys(ENUM_BATTERIES_DOCSTRINGS)) @@ -410,8 +420,10 @@ macro enumbatteries(T, kw...) Got: $nt """) end - if val isa Bool - + if pname == :typesalt + check_typesalt(val) + elseif val isa Bool + # pass else error(""" Bad keyword argument value: @@ -446,6 +458,14 @@ macro enumbatteries(T, kw...) def = def_selfconstructor(T) push!(ret.args, def) end + if nt.hash + def = :(function Base.hash(o::$T, h::UInt) + h = ($start_hash)(o, h, $(nt.typesalt)) + Base.hash(UInt(o), h) + end + ) + push!(ret.args, def) + end push!(ret.args, def_has_batteries(T)) return esc(ret) end diff --git a/test/runtests.jl b/test/runtests.jl index 9165ef6..4705ffe 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -92,7 +92,7 @@ struct SNoIsEqual; a; end @test Empty1() !== Empty2() @test Empty1() != Empty2() @test hash(Empty1()) != hash(Empty2()) - + if VERSION >= v"1.8" @test_throws "Bad keyword argument value:" @macroexpand @batteries SErrors kwconstructor="true" @test_throws "Unsupported keyword" @macroexpand @batteries SErrors kwconstructor=true nonsense=true @@ -102,7 +102,7 @@ struct SNoIsEqual; a; end @test_throws Exception @macroexpand @batteries SErrors kwconstructor=true nonsense=true @test_throws Exception @macroexpand @batteries SErrors nonsense end - + @testset "typesalt" begin @test hash(Salt1()) === hash(Salt1b()) @@ -131,8 +131,8 @@ end @enum Color Red Blue Green @enumbatteries Color string_conversion = true symbol_conversion = true selfconstructor = false -@enum Shape Circle Square -@enumbatteries Shape symbol_conversion =true +@enum Shape Circle = 7 Square = 8 +@enumbatteries Shape symbol_conversion = true typesalt = 0x0578044908fb9846 @testset "@enumbatteries" begin @test SH.has_batteries(Color) @@ -169,6 +169,9 @@ end @test_throws Exception convert(String, Circle) @test_throws Exception Shape("Circle") @test_throws Exception convert(Shape, "Circle") + + @test hash(Circle) == hash(7, hash(0x0578044908fb9846)) + @test hash(Square) == hash(8, hash(0x0578044908fb9846)) end struct Bad end @@ -177,7 +180,7 @@ struct Bad end @macroexpand @batteries Bad typesalt = 0xb6a4b9eeeb03b58b if VERSION >= v"1.7" @test_throws "`typesalt` must be literally `nothing` or an unsigned integer." @macroexpand @batteries Bad typesalt = "ouch" - @test_throws "Unsupported keyword." @macroexpand @batteries Bad does_not_exist = true + @test_throws "Unsupported keyword." @macroexpand @batteries Bad does_not_exist = true @test_throws "Bad keyword argument value" @macroexpand @batteries Bad hash=:nonsense @test_throws "Bad keyword argument value" @macroexpand @batteries Bad StructTypes=:nonsense end @@ -192,7 +195,7 @@ struct HashEqAs <: AbstractHashEqAs hash_eq_as payload end -SH.@batteries HashEqAs +SH.@batteries HashEqAs struct HashEqAsTS1 <: AbstractHashEqAs hash_eq_as payload @@ -250,7 +253,7 @@ Base.:(==)(::HashEqErr, ::HashEqErr) = error() @test SH.structural_hash(S(2,NaN), UInt(0)) != SH.structural_hash(S(2,NaN), UInt(1)) end -struct WithStructTypes +struct WithStructTypes x y end From b20c9ceb9329d45e2b01b6d206c089e753013674 Mon Sep 17 00:00:00 2001 From: Justin Mimbs Date: Mon, 3 Feb 2025 22:39:57 -0500 Subject: [PATCH 2/2] Make enum hash off by default for backward compat --- src/StructHelpers.jl | 4 ++-- test/runtests.jl | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/StructHelpers.jl b/src/StructHelpers.jl index 7a88a15..553c286 100644 --- a/src/StructHelpers.jl +++ b/src/StructHelpers.jl @@ -360,7 +360,7 @@ const ENUM_BATTERIES_DEFAULTS = ( string_conversion = false, symbol_conversion = false, selfconstructor = BATTERIES_DEFAULTS.selfconstructor, - hash = BATTERIES_DEFAULTS.hash, + hash = false, typesalt = BATTERIES_DEFAULTS.typesalt, ) @@ -433,7 +433,7 @@ macro enumbatteries(T, kw...) """) end end - nt = merge(ENUM_BATTERIES_DEFAULTS, nt) + nt = merge(ENUM_BATTERIES_DEFAULTS, (; hash = haskey(nt, :typesalt)), nt) TT = Base.eval(__module__, T)::Type ret = quote end diff --git a/test/runtests.jl b/test/runtests.jl index 4705ffe..a7df8a7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -134,6 +134,9 @@ end @enum Shape Circle = 7 Square = 8 @enumbatteries Shape symbol_conversion = true typesalt = 0x0578044908fb9846 +@enum Size Small Medium Large +@enumbatteries Size hash = true + @testset "@enumbatteries" begin @test SH.has_batteries(Color) @test !SH.has_batteries(EnumNoBatteries) @@ -169,9 +172,20 @@ end @test_throws Exception convert(String, Circle) @test_throws Exception Shape("Circle") @test_throws Exception convert(Shape, "Circle") +end +@testset "@enumbatteries hash" begin + # hash with typesalt @test hash(Circle) == hash(7, hash(0x0578044908fb9846)) @test hash(Square) == hash(8, hash(0x0578044908fb9846)) + + # hash = true + @test hash(Small) == hash(0, hash(Size)) + @test hash(Medium) == hash(1, hash(Size)) + @test hash(Large) == hash(2, hash(Size)) + + # no hash by default + @test hash(Red) != hash(0, hash(Color)) end struct Bad end