From 5d5b4d4589dccc8c4326dc8c619e124b9792198f Mon Sep 17 00:00:00 2001 From: Marc Vertes <mvertes@free.fr> Date: Thu, 13 Mar 2025 12:19:42 +0100 Subject: [PATCH 1/4] fix(gnovm): allow multiple NaN keys in maps The behavior of maps with NaN no matches Go. Fixes #3867. --- gnovm/pkg/gnolang/values.go | 22 ++++++++++++++++++++++ gnovm/tests/files/map30.gno | 15 +++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 gnovm/tests/files/map30.gno diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index d60c074316b..196145b87bd 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "fmt" "math/big" + "math/rand/v2" "reflect" "strconv" "strings" @@ -1458,6 +1459,23 @@ func (tv *TypedValue) AssertNonNegative(msg string) { } } +// uvnan is the unsigned value of NaN. It can be contructed by applying the following: +// { var nan = math.NaN(); var uvnan = *(*uint64)(unsafe.Pointer(&nan)) } +const uvnan = 0x7FF8000000000001 + +// randNaN returns an uint64 representation of NaN with a random payload. +func randNaN() uint64 { + return rand.Uint64()&^uvnan | uvnan +} + +// IsNaN reports wether tv is an IEEE 754 "not a number" value. +func (tv *TypedValue) IsNaN() bool { + if tv.HasKind(Float64Kind) { + return uvnan == tv.GetFloat64()&uvnan + } + return false +} + func (tv *TypedValue) ComputeMapKey(store Store, omitType bool) MapKey { // Special case when nil: has no separator. if tv.T == nil { @@ -1476,6 +1494,10 @@ func (tv *TypedValue) ComputeMapKey(store Store, omitType bool) MapKey { } switch bt := baseOf(tv.T).(type) { case PrimitiveType: + if tv.IsNaN() { + // if NaN, generate a random payload to ensure key uniqueness. + tv.SetFloat64(randNaN()) + } pbz := tv.PrimitiveBytes() bz = append(bz, pbz...) case *PointerType: diff --git a/gnovm/tests/files/map30.gno b/gnovm/tests/files/map30.gno new file mode 100644 index 00000000000..4800334912e --- /dev/null +++ b/gnovm/tests/files/map30.gno @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "math" +) + +var NaN = math.NaN() + +func main() { + fmt.Println(map[float64]int{NaN: 1, NaN: 2}) +} + +// Output: +// map[NaN:1 NaN:2] From 32ba53580918733396adec7acb42a1fdba826001 Mon Sep 17 00:00:00 2001 From: Marc Vertes <mvertes@free.fr> Date: Thu, 13 Mar 2025 15:06:38 +0100 Subject: [PATCH 2/4] fix lint --- gnovm/pkg/gnolang/values.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index 196145b87bd..23587c3a9dc 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -1459,12 +1459,13 @@ func (tv *TypedValue) AssertNonNegative(msg string) { } } -// uvnan is the unsigned value of NaN. It can be contructed by applying the following: +// uvnan is the unsigned value of NaN. It can be obtained by applying the following: // { var nan = math.NaN(); var uvnan = *(*uint64)(unsafe.Pointer(&nan)) } const uvnan = 0x7FF8000000000001 -// randNaN returns an uint64 representation of NaN with a random payload. +// randNaN returns an uint64 representation of NaN with a random payload of 53 bits. func randNaN() uint64 { + //nolint: gosec return rand.Uint64()&^uvnan | uvnan } From f0f5f424bb96471a9123825d4a08120f8cb2c75b Mon Sep 17 00:00:00 2001 From: Marc Vertes <mvertes@free.fr> Date: Thu, 13 Mar 2025 15:08:04 +0100 Subject: [PATCH 3/4] fix lint --- gnovm/pkg/gnolang/values.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index 23587c3a9dc..89a824322ef 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -1465,8 +1465,7 @@ const uvnan = 0x7FF8000000000001 // randNaN returns an uint64 representation of NaN with a random payload of 53 bits. func randNaN() uint64 { - //nolint: gosec - return rand.Uint64()&^uvnan | uvnan + return rand.Uint64()&^uvnan | uvnan //nolint:gosec } // IsNaN reports wether tv is an IEEE 754 "not a number" value. From c9a4b344ce9b46483bc973494a6ea21762d24549 Mon Sep 17 00:00:00 2001 From: Marc Vertes <mvertes@free.fr> Date: Tue, 18 Mar 2025 09:06:46 +0100 Subject: [PATCH 4/4] fix NaN for float32. Ensure that +0.0 = -0.0 --- gnovm/pkg/gnolang/values.go | 45 ++++++++++++++++++++++++------------- gnovm/tests/files/map31.gno | 15 +++++++++++++ 2 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 gnovm/tests/files/map31.gno diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index 89a824322ef..0268b877154 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -3,6 +3,7 @@ package gnolang import ( "encoding/binary" "fmt" + "math" "math/big" "math/rand/v2" "reflect" @@ -1459,21 +1460,16 @@ func (tv *TypedValue) AssertNonNegative(msg string) { } } -// uvnan is the unsigned value of NaN. It can be obtained by applying the following: -// { var nan = math.NaN(); var uvnan = *(*uint64)(unsafe.Pointer(&nan)) } -const uvnan = 0x7FF8000000000001 - -// randNaN returns an uint64 representation of NaN with a random payload of 53 bits. -func randNaN() uint64 { - return rand.Uint64()&^uvnan | uvnan //nolint:gosec +// randNaN64 returns an uint64 representation of NaN with a random payload. +func randNaN64() uint64 { + const uvnan64 = 0x7FF8000000000001 // math.Float64bits(math.NaN()) + return rand.Uint64()&^uvnan64 | uvnan64 //nolint:gosec } -// IsNaN reports wether tv is an IEEE 754 "not a number" value. -func (tv *TypedValue) IsNaN() bool { - if tv.HasKind(Float64Kind) { - return uvnan == tv.GetFloat64()&uvnan - } - return false +// randNaN32 returns an uint32 representation of NaN with a random payload. +func randNaN32() uint32 { + const uvnan32 = 0x7FC00000 // math.Float32bits(float32(math.NaN())) + return rand.Uint32()&^uvnan32 | uvnan32 //nolint:gosec } func (tv *TypedValue) ComputeMapKey(store Store, omitType bool) MapKey { @@ -1494,9 +1490,26 @@ func (tv *TypedValue) ComputeMapKey(store Store, omitType bool) MapKey { } switch bt := baseOf(tv.T).(type) { case PrimitiveType: - if tv.IsNaN() { - // if NaN, generate a random payload to ensure key uniqueness. - tv.SetFloat64(randNaN()) + if tv.HasKind(Float64Kind) { + f := math.Float64frombits(tv.GetFloat64()) + if f != f { + // If NaN, shuffle its value, thus allowing several NaN per map + // and ensuring that a value cannot be retrieved by NaN. + tv.SetFloat64(randNaN64()) + } else if f == 0.0 { + // If zero, clear the sign, so +0.0 and -0.0 match the same key. + tv.SetFloat64(math.Float64bits(+0.0)) + } + } else if tv.HasKind(Float32Kind) { + f := math.Float32frombits(tv.GetFloat32()) + if f != f { + // If NaN, shuffle its value, thus allowing several NaN per map + // and ensuring that a value cannot be retrieved by NaN. + tv.SetFloat32(randNaN32()) + } else if f == 0.0 { + // If zero, clear the sign, so +0.0 and -0.0 match the same key. + tv.SetFloat32(math.Float32bits(+0.0)) + } } pbz := tv.PrimitiveBytes() bz = append(bz, pbz...) diff --git a/gnovm/tests/files/map31.gno b/gnovm/tests/files/map31.gno new file mode 100644 index 00000000000..30b8798ca5b --- /dev/null +++ b/gnovm/tests/files/map31.gno @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "math" +) + +var NaN = float32(math.NaN()) + +func main() { + fmt.Println(map[float32]int{NaN: 1, NaN: 2}) +} + +// Output: +// map[NaN:1 NaN:2]