diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index eeb2c4dfe6..997d42d43d 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -558,7 +558,10 @@ let identityHash com r (arg: Expr) = | Char | String | Builtin BclGuid -> "stringHash" - | Number((Decimal | BigInt | Int64 | UInt64), _) -> "safeHash" + // Every numeric width reaches the same `GetHashCode`, so routing the wider + // ones through `safeHash` only added a frame and its protocol check. This + // now matches `structuralHash`, which has always sent all of them to + // `numberHash`. | Number _ | Builtin BclTimeSpan -> "numberHash" | List _ -> "safeHash" diff --git a/src/fable-library-py/src/native_array.rs b/src/fable-library-py/src/native_array.rs index 09d8810b30..0a09b0c52b 100644 --- a/src/fable-library-py/src/native_array.rs +++ b/src/fable-library-py/src/native_array.rs @@ -167,9 +167,11 @@ impl Clone for NativeArray { // Helper macro for filling storage // -// The wrapper type is tried first so a same-type value costs no conversion dunder; -// the primitive fallback accepts a plain Python int/float, which is how Int32 and -// Float64 values are represented. +// The wrapper type is tried first, which hits for the widths that are still pyo3 +// wrappers. Int32 and Float64 are plain Python ints and floats, so for those two +// the wrapper arm always fails and the primitive fallback is the normal path — +// which costs nothing measurable, because a failed `extract` is a Rust-level +// `Err` rather than a constructed Python exception. macro_rules! fill_typed_vec { ($vec:expr, $value:expr, $target_index:expr, $count:expr, $type:ty, $prim:ty) => {{ let typed_value: $prim = match $value.extract::<$type>() { diff --git a/tests/Python/TestComparison.fs b/tests/Python/TestComparison.fs index fbcdeb0d94..d8374f46be 100644 --- a/tests/Python/TestComparison.fs +++ b/tests/Python/TestComparison.fs @@ -656,3 +656,44 @@ let ``CompareTo with primitives works`` () = (1.).CompareTo(1.) |> equal 0 (1.m).CompareTo(1.m) |> equal 0 ("1").CompareTo("1") |> equal 0 + +[] +let ``test GetHashCode with wide numeric types works`` () = + // These route through identity hashing, which used to take a different path + // than structural hashing for the widths below. + (5L).GetHashCode() |> equal ((5L).GetHashCode()) + (5UL).GetHashCode() |> equal ((5UL).GetHashCode()) + (5M).GetHashCode() |> equal ((5M).GetHashCode()) + (5I).GetHashCode() |> equal ((5I).GetHashCode()) + (5L).GetHashCode() = (6L).GetHashCode() |> equal false + (5UL).GetHashCode() = (6UL).GetHashCode() |> equal false + (5M).GetHashCode() = (6M).GetHashCode() |> equal false + (5I).GetHashCode() = (6I).GetHashCode() |> equal false + +[] +let ``test hashing wide numeric types agrees with GetHashCode`` () = + hash 5L |> equal ((5L).GetHashCode()) + hash 5UL |> equal ((5UL).GetHashCode()) + hash 5M |> equal ((5M).GetHashCode()) + hash 5I |> equal ((5I).GetHashCode()) + +[] +let ``test wide numeric types work as hash keys`` () = + // Added one at a time rather than from a sequence: constructing a HashSet from + // an IEnumerable trips a known Pyright gap, the one that keeps test_hash_set.py + // in the CI exclude list. + let longs = System.Collections.Generic.HashSet() + longs.Add 1L |> ignore + longs.Add 2L |> ignore + longs.Add 2L |> equal false + longs.Contains 2L |> equal true + longs.Contains 9L |> equal false + let ulongs = System.Collections.Generic.HashSet() + ulongs.Add 2UL |> ignore + ulongs.Contains 2UL |> equal true + let decimals = System.Collections.Generic.HashSet() + decimals.Add 2M |> ignore + decimals.Contains 2M |> equal true + let bigs = System.Collections.Generic.HashSet() + bigs.Add 2I |> ignore + bigs.Contains 2I |> equal true