From ea08f63e52800b653fa7b435e349697b4248b901 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 29 Jul 2026 17:53:41 +0200 Subject: [PATCH] perf(python): back int64 arrays with typed storage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `TypedArrayCompatible` excluded Int64 and UInt64 with a comment inherited from the JavaScript target — "don't use typed array for int64 until we remove our int64 polyfill and use JS BigInt". That constraint has never applied to Python. The Rust core has backed these with `Vec`/`Vec` all along and `Int64ArrayCons`/`UInt64ArrayCons` are exported from `array_.py`; nothing had ever emitted them. So `Array.map` over an `int64[]` passed `None` as the cons and fell back to boxed `Generic` storage, keeping one wrapper object per element. Only array literals and `Seq.toArray` specialized, because those go through the typed constructor rather than a library call. For int64 this is a win on every axis, since the values are still pyo3 wrappers and the boxing that specialization avoids is real (1000 elements): storage a == b sum(a) Int64 0.09 us 17.6 us Generic 8.53 us 33.6 us Equality is 95x because `Vec == Vec` compares entirely in Rust where `Generic` walks element-by-element through Python. `TestArray.fs` had no int64 coverage at all, which is why this went unnoticed. The new tests assert value round-tripping through the storage, the boundary values that would expose a botched i64/u64 extraction, and structural equality. Co-Authored-By: Claude Opus 5 (1M context) --- src/Fable.Transforms/Python/Replacements.fs | 10 +++---- tests/Python/TestArray.fs | 33 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 7383638963..eeb2c4dfe6 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -30,15 +30,13 @@ let (|TypedArrayCompatible|_|) (com: Compiler) (arrayKind: ArrayKind) t = | UInt32 -> ValueSome "UInt32ArrayCons" | Float32 -> ValueSome "Float32ArrayCons" | Float64 -> ValueSome "Float64ArrayCons" - // Don't use typed array for int64 until we remove our int64 polyfill - // and use JS BigInt to represent int64 - // | Int64 -> Some "BigInt64ArrayCons" - // | UInt64 -> Some "BigUint64ArrayCons" + // Unlike JavaScript, which has no typed array for int64 while it polyfills the + // type, Python backs these with `Vec`/`Vec` in the Rust core. + | Int64 -> ValueSome "Int64ArrayCons" + | UInt64 -> ValueSome "UInt64ArrayCons" | Int128 | UInt128 | Float16 - | Int64 - | UInt64 | BigInt | Decimal | NativeInt diff --git a/tests/Python/TestArray.fs b/tests/Python/TestArray.fs index 5ea767a0f5..260343908c 100644 --- a/tests/Python/TestArray.fs +++ b/tests/Python/TestArray.fs @@ -1461,3 +1461,36 @@ let ``test Array.randomSampleBy works`` () = Array.distinct sample |> Array.length |> equal 3 throwsAnyError (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|] |> ignore) throwsAnyError (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|] |> ignore) + +[] +let ``test Int64 arrays roundtrip through their backing storage`` () = + let xs = [| 1L; -2L; 3L |] + Array.map (fun x -> x * 2L) xs |> equal [| 2L; -4L; 6L |] + Array.filter (fun x -> x > 0L) xs |> equal [| 1L; 3L |] + Array.append xs xs |> equal [| 1L; -2L; 3L; 1L; -2L; 3L |] + Array.rev xs |> equal [| 3L; -2L; 1L |] + Array.sort xs |> equal [| -2L; 1L; 3L |] + Array.sum xs |> equal 2L + Array.contains -2L xs |> equal true + Array.item 1 xs |> equal -2L + Array.map id xs = xs |> equal true + +[] +let ``test Int64 array values are not truncated`` () = + let xs = [| System.Int64.MinValue; 0L; System.Int64.MaxValue |] + Array.max xs |> equal System.Int64.MaxValue + Array.min xs |> equal System.Int64.MinValue + Array.sort xs |> equal [| System.Int64.MinValue; 0L; System.Int64.MaxValue |] + Array.rev (Array.rev xs) = xs |> equal true + Array.init 3 int64 |> equal [| 0L; 1L; 2L |] + Array.zeroCreate 2 |> equal [| 0L; 0L |] + +[] +let ``test UInt64 arrays roundtrip through their backing storage`` () = + let xs = [| 1UL; 2UL; System.UInt64.MaxValue |] + Array.map id xs |> equal xs + Array.filter (fun x -> x > 1UL) xs |> equal [| 2UL; System.UInt64.MaxValue |] + Array.max xs |> equal System.UInt64.MaxValue + Array.sort xs |> equal [| 1UL; 2UL; System.UInt64.MaxValue |] + Array.contains System.UInt64.MaxValue xs |> equal true + Array.zeroCreate 2 |> equal [| 0UL; 0UL |]