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]