From 5845581b3209d56bfc64b4ad84603323becaa312 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 04:39:44 +0000 Subject: [PATCH 1/3] test(stdlib): add coverage for untested builtins - abs, chr/ord, len, map, type conversions, isinstance, bytes Adds 15 new [] tests for builtins that were bound in Builtins.fs but had no test coverage: - abs (int and float) - chr / ord (and round-trip) - len (list and string) - map (single and two-iterable overloads) - str / int / float type conversions - isinstance / type - bytes (from byte array and from string with encoding) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/TestBuiltins.fs | 97 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/test/TestBuiltins.fs b/test/TestBuiltins.fs index 5af0fc8..15c4d40 100644 --- a/test/TestBuiltins.fs +++ b/test/TestBuiltins.fs @@ -120,3 +120,100 @@ let ``test pyNone is None`` () = builtins.bool pyNone |> equal false // None has type NoneType, so isinstance(None, type(None)) holds builtins.isinstance (pyNone, builtins.``type`` pyNone) |> equal true + +[] +let ``test abs with int works`` () = + builtins.abs -5 |> equal 5 + builtins.abs 0 |> equal 0 + builtins.abs 42 |> equal 42 + +[] +let ``test abs with float works`` () = + builtins.abs -3.14 |> equal 3.14 + builtins.abs 0.0 |> equal 0.0 + builtins.abs 2.72 |> equal 2.72 + +[] +let ``test chr and ord round-trip works`` () = + builtins.chr 65 |> equal 'A' + builtins.chr 97 |> equal 'a' + builtins.ord 'A' |> equal 65 + builtins.ord 'a' |> equal 97 + +[] +let ``test chr ord round-trip preserves value`` () = + let code = 9731 // snowman ☃ + builtins.ord (builtins.chr code) |> equal code + +[] +let ``test len with list works`` () = + builtins.len ([ 1; 2; 3 ] |> box) |> equal 3 + builtins.len ([] |> box) |> equal 0 + +[] +let ``test len with string works`` () = + builtins.len ("hello" |> box) |> equal 5 + builtins.len ("" |> box) |> equal 0 + +[] +let ``test map with single iterable works`` () = + builtins.map ((fun x -> x * 2), [ 1; 2; 3 ]) + |> Seq.toList + |> equal [ 2; 4; 6 ] + +[] +let ``test map with two iterables works`` () = + builtins.map ((fun (a, b) -> a + b), [ 1; 2; 3 ], [ 10; 20; 30 ]) + |> Seq.toList + |> equal [ 11; 22; 33 ] + +[] +let ``test str conversion works`` () = + builtins.str 42 |> equal "42" + builtins.str true |> equal "True" + +[] +let ``test int conversion works`` () = + builtins.int "42" |> equal 42 + builtins.int 3.9 |> equal 3 + // Python's int() truncates toward zero, not floor + builtins.int -3.9 |> equal -3 + +[] +let ``test float conversion works`` () = + builtins.float "3.14" |> equal 3.14 + builtins.float 42 |> equal 42.0 + +[] +let ``test isinstance works`` () = + let pyIntVal: obj = emitPyExpr () "42" + let pyStrVal: obj = emitPyExpr () "'hello'" + builtins.isinstance (pyStrVal, pyStr) |> equal true + builtins.isinstance (pyIntVal, pyInt) |> equal true + +[] +let ``test isinstance returns false for wrong type`` () = + let pyIntVal: obj = emitPyExpr () "42" + builtins.isinstance (pyIntVal, pyStr) |> equal false + +[] +let ``test type returns type object`` () = + let pyStrVal: obj = emitPyExpr () "'hello'" + let pyIntVal: obj = emitPyExpr () "42" + let t = builtins.``type`` pyStrVal + builtins.isinstance (pyStrVal, t) |> equal true + builtins.isinstance (pyIntVal, t) |> equal false + +[] +let ``test bytes from byte array works`` () = + let b = builtins.bytes [| 72uy; 101uy; 108uy; 108uy; 111uy |] + builtins.len b |> equal 5 + b.[0] |> equal 72uy + b.[4] |> equal 111uy + +[] +let ``test bytes from string with encoding works`` () = + let b = builtins.bytes ("ABC", "utf-8") + builtins.len b |> equal 3 + b.[0] |> equal 65uy + b.[2] |> equal 67uy From 214a0465872c049236396d71f96b4772c8ef4c67 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 3 May 2026 12:03:31 +0200 Subject: [PATCH 2/3] test(stdlib): fix len/map cases that hit binding limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `len` on an F# `int list` fails because FSharpList has no `__len__`; convert via `builtins.list` instead. Drop the two-iterable `map` case — the binding's `'T1 * 'T2 -> 'T3` signature compiles to a tuple-input function but Python's `map` calls `f(a, b)` positionally. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/TestBuiltins.fs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/test/TestBuiltins.fs b/test/TestBuiltins.fs index 15c4d40..e362081 100644 --- a/test/TestBuiltins.fs +++ b/test/TestBuiltins.fs @@ -147,8 +147,9 @@ let ``test chr ord round-trip preserves value`` () = [] let ``test len with list works`` () = - builtins.len ([ 1; 2; 3 ] |> box) |> equal 3 - builtins.len ([] |> box) |> equal 0 + // F# `int list` compiles to FSharpList (no __len__); convert via builtins.list. + builtins.len (builtins.list (seq { 1..3 })) |> equal 3 + builtins.len (builtins.list Seq.empty) |> equal 0 [] let ``test len with string works`` () = @@ -161,12 +162,6 @@ let ``test map with single iterable works`` () = |> Seq.toList |> equal [ 2; 4; 6 ] -[] -let ``test map with two iterables works`` () = - builtins.map ((fun (a, b) -> a + b), [ 1; 2; 3 ], [ 10; 20; 30 ]) - |> Seq.toList - |> equal [ 11; 22; 33 ] - [] let ``test str conversion works`` () = builtins.str 42 |> equal "42" From 2040e42385a036791355b893f65b8d8291bd0b7c Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 3 May 2026 12:06:03 +0200 Subject: [PATCH 3/3] fix(stdlib): builtins.map multi-iterable overloads use System.Func The 2- and 3-iterable overloads typed the function as `'T1 * 'T2 -> 'T3` (tuple-input), which Fable compiles to a Python function expecting a single tuple arg. But Python's `map(f, it1, it2)` calls `f(a, b)` positionally, so calls failed with "takes 1 positional argument but 2 were given". Switching to `System.Func<...>` produces a Python function with the correct positional arity. Restores the two-iterable test and adds a three-iterable test. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/stdlib/Builtins.fs | 5 +++-- test/TestBuiltins.fs | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/stdlib/Builtins.fs b/src/stdlib/Builtins.fs index dbe193a..194b275 100644 --- a/src/stdlib/Builtins.fs +++ b/src/stdlib/Builtins.fs @@ -186,11 +186,12 @@ type IExports = /// Make an iterator that computes the function using arguments from each /// of the iterables. Stops when the shortest iterable is exhausted. - abstract map: ('T1 * 'T2 -> 'T3) * IEnumerable<'T1> * IEnumerable<'T2> -> IEnumerable<'T3> + abstract map: System.Func<'T1, 'T2, 'T3> * IEnumerable<'T1> * IEnumerable<'T2> -> IEnumerable<'T3> /// Make an iterator that computes the function using arguments from each /// of the iterables. Stops when the shortest iterable is exhausted. - abstract map: ('T1 * 'T2 * 'T3 -> 'T4) * IEnumerable<'T1> * IEnumerable<'T2> * IEnumerable<'T3> -> IEnumerable<'T4> + abstract map: + System.Func<'T1, 'T2, 'T3, 'T4> * IEnumerable<'T1> * IEnumerable<'T2> * IEnumerable<'T3> -> IEnumerable<'T4> /// Return the Unicode code point for a one-character string. abstract ord: char -> int diff --git a/test/TestBuiltins.fs b/test/TestBuiltins.fs index e362081..d2c4a69 100644 --- a/test/TestBuiltins.fs +++ b/test/TestBuiltins.fs @@ -162,6 +162,18 @@ let ``test map with single iterable works`` () = |> Seq.toList |> equal [ 2; 4; 6 ] +[] +let ``test map with two iterables works`` () = + builtins.map ((fun a b -> a + b), [ 1; 2; 3 ], [ 10; 20; 30 ]) + |> Seq.toList + |> equal [ 11; 22; 33 ] + +[] +let ``test map with three iterables works`` () = + builtins.map ((fun a b c -> a + b + c), [ 1; 2; 3 ], [ 10; 20; 30 ], [ 100; 200; 300 ]) + |> Seq.toList + |> equal [ 111; 222; 333 ] + [] let ``test str conversion works`` () = builtins.str 42 |> equal "42"