diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index bedc64231..4b9e7a120 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -1777,6 +1777,20 @@ let seqModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg let elementType = getElementType t makeArrayFrom elementType arg |> Some + // Native-generator impls live in `seq_native` (see Seq.fs) + | ("Map" | "Filter" | "Collect" | "Append" | "Truncate" | "Unfold" | "TryHead" | "Head" | "TryItem" | "Item" | "Choose" | "MapIndexed" | "Map2" | "MapIndexed2" | "Map3" | "TakeWhile" | "Take") as meth, + _ -> + let meth = Naming.lowerFirst meth + let args = wrapLastArgIfString i.CompiledName args + + let args = + match args with + | [] -> args + | _ -> args |> List.replaceLast (wrapDictToItems com) + + Helper.LibCall(com, "seq_native", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + |> Some + | meth, _ -> let meth = Naming.lowerFirst meth let args = wrapLastArgIfString i.CompiledName args diff --git a/src/fable-library-py/fable_library/Seq.fs b/src/fable-library-py/fable_library/Seq.fs index 03344fdea..209749119 100644 --- a/src/fable-library-py/fable_library/Seq.fs +++ b/src/fable-library-py/fable_library/Seq.fs @@ -6,6 +6,7 @@ module SeqModule open Fable.Core +open Fable.Core.PyInterop type IEnumerator<'T> = System.Collections.Generic.IEnumerator<'T> type IEnumerable<'T> = System.Collections.Generic.IEnumerable<'T> @@ -302,8 +303,8 @@ let delay (generator: unit -> seq<'T>) = let concat<'Collection, 'T when 'Collection :> seq<'T>> (sources: seq<'Collection>) : seq<'T> = mkSeq (fun () -> Enumerator.concat sources) -let unfold (generator: 'State -> ('T * 'State) option) (state: 'State) = - mkSeq (fun () -> Enumerator.unfold generator state) +[] +let unfold (generator: 'State -> ('T * 'State) option) (state: 'State) : seq<'T> = nativeOnly let empty () = delay (fun () -> Array.empty :> seq<'T>) @@ -346,7 +347,7 @@ let generateIndexed create compute dispose = // let inline generateUsing (openf: unit -> ('U :> System.IDisposable)) compute = // generate openf compute (fun (s: 'U) -> s.Dispose()) -let append (xs: seq<'T>) (ys: seq<'T>) = concat [| xs; ys |] +// `append`: no F# impl - Python's Replacements.fs redirects `Seq.append` straight to `seq_native`. let cast (xs: IEnumerable<'T>) = mkSeq (fun () -> @@ -354,18 +355,7 @@ let cast (xs: IEnumerable<'T>) = xs.GetEnumerator() |> Enumerator.cast ) -let choose (chooser: 'T -> 'U option) (xs: seq<'T>) = - generate - (fun () -> ofSeq xs) - (fun e -> - let mutable curr = None - - while (Option.isNone curr && e.MoveNext()) do - curr <- chooser e.Current - - curr - ) - (fun e -> e.Dispose()) +// `choose`: no F# impl - Python's Replacements.fs redirects `Seq.choose` straight to `seq_native`. let compareWith (comparer: 'T -> 'T -> int) (xs: seq<'T>) (ys: seq<'T>) : int = use e1 = ofSeq xs @@ -449,14 +439,8 @@ let enumerateWhile (guard: unit -> bool) (xs: seq<'T>) = 0 ) -let filter f (xs: seq<'T>) = - xs - |> choose (fun x -> - if f x then - Some x - else - None - ) +[] +let filter (predicate: 'T -> bool) (xs: seq<'T>) : seq<'T> = nativeOnly let exists predicate (xs: seq<'T>) = use e = ofSeq xs @@ -598,22 +582,7 @@ let forAll predicate xs = let forAll2 predicate xs ys = not (exists2 (fun x y -> not (predicate x y)) xs ys) -let tryHead (xs: seq<'T>) = - match xs with - | :? array<'T> as a -> Array.tryHead a - | :? list<'T> as a -> List.tryHead a - | _ -> - use e = ofSeq xs - - if e.MoveNext() then - Some(e.Current) - else - None - -let head (xs: seq<'T>) = - match tryHead xs with - | Some x -> x - | None -> invalidArg "source" SR.inputSequenceEmpty +// `tryHead`/`head`: no F# impl - Replacements.fs redirects both straight to `seq_native`. let initialize count f = unfold @@ -635,27 +604,7 @@ let isEmpty (xs: seq<'T>) = use e = ofSeq xs not (e.MoveNext()) -let tryItem index (xs: seq<'T>) = - match xs with - | :? array<'T> as a -> Array.tryItem index a - | :? list<'T> as a -> List.tryItem index a - | _ -> - use e = ofSeq xs - - let rec loop index = - if not (e.MoveNext()) then - None - elif index = 0 then - Some e.Current - else - loop (index - 1) - - loop index - -let item index (xs: seq<'T>) = - match tryItem index xs with - | Some x -> x - | None -> invalidArg "index" SR.notEnoughElements +// `tryItem`/`item`: no F# impl, same reason as `tryHead`/`head` above. let iterate action xs = fold (fun () x -> action x) () xs @@ -717,80 +666,21 @@ let length (xs: seq<'T>) = count -let map (mapping: 'T -> 'U) (xs: seq<'T>) = - generate - (fun () -> ofSeq xs) - (fun e -> - if e.MoveNext() then - Some(mapping e.Current) - else - None - ) - (fun e -> e.Dispose()) +[] +let map (mapping: 'T -> 'U) (xs: seq<'T>) : seq<'U> = nativeOnly -let mapIndexed (mapping: int -> 'T -> 'U) (xs: seq<'T>) = - generateIndexed - (fun () -> ofSeq xs) - (fun i e -> - if e.MoveNext() then - Some(mapping i e.Current) - else - None - ) - (fun e -> e.Dispose()) +[] +let mapIndexed (mapping: int -> 'T -> 'U) (xs: seq<'T>) : seq<'U> = nativeOnly let indexed (xs: seq<'T>) = xs |> mapIndexed (fun i x -> (i, x)) -let map2 (mapping: 'T1 -> 'T2 -> 'U) (xs: seq<'T1>) (ys: seq<'T2>) = - generate - (fun () -> (ofSeq xs, ofSeq ys)) - (fun (e1, e2) -> - if e1.MoveNext() && e2.MoveNext() then - Some(mapping e1.Current e2.Current) - else - None - ) - (fun (e1, e2) -> - try - e1.Dispose() - finally - e2.Dispose() - ) +[] +let map2 (mapping: 'T1 -> 'T2 -> 'U) (xs: seq<'T1>) (ys: seq<'T2>) : seq<'U> = nativeOnly -let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: seq<'T1>) (ys: seq<'T2>) = - generateIndexed - (fun () -> (ofSeq xs, ofSeq ys)) - (fun i (e1, e2) -> - if e1.MoveNext() && e2.MoveNext() then - Some(mapping i e1.Current e2.Current) - else - None - ) - (fun (e1, e2) -> - try - e1.Dispose() - finally - e2.Dispose() - ) +// `mapIndexed2`: no F# impl - Replacements.fs redirects `Seq.mapIndexed2` straight to `seq_native`. -let map3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'U) (xs: seq<'T1>) (ys: seq<'T2>) (zs: seq<'T3>) = - generate - (fun () -> (ofSeq xs, ofSeq ys, ofSeq zs)) - (fun (e1, e2, e3) -> - if e1.MoveNext() && e2.MoveNext() && e3.MoveNext() then - Some(mapping e1.Current e2.Current e3.Current) - else - None - ) - (fun (e1, e2, e3) -> - try - e1.Dispose() - finally - try - e2.Dispose() - finally - e3.Dispose() - ) +[] +let map3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'U) (xs: seq<'T1>) (ys: seq<'T2>) (zs: seq<'T3>) : seq<'U> = nativeOnly let readOnly (xs: seq<'T>) = let xs = nullArgCheck "source" xs @@ -992,49 +882,14 @@ let skipWhile predicate (xs: seq<'T>) = let tail (xs: seq<'T>) = skip 1 xs -let take count (xs: seq<'T>) = - generateIndexed - (fun () -> ofSeq xs) - (fun i e -> - if i < count then - if e.MoveNext() then - Some(e.Current) - else - invalidArg "source" SR.notEnoughElements - else - None - ) - (fun e -> e.Dispose()) - -let takeWhile predicate (xs: seq<'T>) = - generate - (fun () -> ofSeq xs) - (fun e -> - if e.MoveNext() && predicate e.Current then - Some(e.Current) - else - None - ) - (fun e -> e.Dispose()) +// `take`/`takeWhile`: no F# impl - Replacements.fs redirects both straight to `seq_native`. -let truncate count (xs: seq<'T>) = - generateIndexed - (fun () -> ofSeq xs) - (fun i e -> - if i < count && e.MoveNext() then - Some(e.Current) - else - None - ) - (fun e -> e.Dispose()) +// `truncate`/`collect`: no F# impl, same reason as `append` above. let zip (xs: seq<'T1>) (ys: seq<'T2>) = map2 (fun x y -> (x, y)) xs ys let zip3 (xs: seq<'T1>) (ys: seq<'T2>) (zs: seq<'T3>) = map3 (fun x y z -> (x, y, z)) xs ys zs -let collect<'T, 'Collection, 'U when 'Collection :> 'U seq> (mapping: 'T -> 'Collection) (xs: seq<'T>) : seq<'U> = - delay (fun () -> xs |> map mapping |> concat) - let where predicate (xs: seq<'T>) = filter predicate xs let pairwise (xs: seq<'T>) = diff --git a/src/fable-library-py/fable_library/list.pyi b/src/fable-library-py/fable_library/list.pyi index b59b01ad4..ebaaa3b36 100644 --- a/src/fable-library-py/fable_library/list.pyi +++ b/src/fable-library-py/fable_library/list.pyi @@ -1,4 +1,6 @@ from .array_ import Array +from .core import int32 +from .option import Option from .protocols import IEnumerable_1 class FSharpList[T](IEnumerable_1[T]): @@ -7,3 +9,5 @@ class FSharpList[T](IEnumerable_1[T]): def of_array[T](xs: Array[T]) -> FSharpList[T]: ... def length[T](xs: FSharpList[T]) -> int: ... +def try_head[T](xs: FSharpList[T]) -> Option[T]: ... +def try_item[T](n: int32, xs: FSharpList[T]) -> Option[T]: ... diff --git a/src/fable-library-py/fable_library/seq_native.py b/src/fable-library-py/fable_library/seq_native.py new file mode 100644 index 000000000..c43d01183 --- /dev/null +++ b/src/fable-library-py/fable_library/seq_native.py @@ -0,0 +1,238 @@ +"""Native-generator replacements for the hottest `Seq` combinators. + +`Seq.fs` otherwise builds these on `generate`/`Enumerator.generateWhileSome`: an `Option` +allocated per element plus a `MoveNext`/`Current` call per step. These use plain Python +generators instead, bound from `Seq.fs` via `[]` + `nativeOnly`. + +Two rules keep F# `seq` semantics intact: + 1. Each `NativeSeq` holds a THUNK, not a live generator, so every enumeration gets a fresh + generator - restartable, and source side effects re-run each time. + 2. Disposal calls the generator's `.close()` (runs `finally` via `GeneratorExit`), via + `util.Enumerator.Dispose`. +""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable, Iterator, Mapping +from typing import Any + +from .array_ import Array +from .array_ import try_head as array_try_head +from .array_ import try_item as array_try_item +from .bases import EnumerableBase +from .core import int32 +from .list import FSharpList +from .list import try_head as list_try_head +from .list import try_item as list_try_item +from .option import Option, some, value +from .protocols import IEnumerable_1, IEnumerator +from .types import UNIT, Unit +from .util import Enumerator, get_enumerator, to_iterator + +_SENTINEL: Any = object() + + +class NativeSeq[T](EnumerableBase[T]): + """An IEnumerable backed by a thunk that produces a fresh iterator per enumeration. + Like `util.Enumerable`, but takes a thunk instead of a plain iterable, so restartability + doesn't depend on the iterable already being re-iterable.""" + + __slots__ = ("_thunk",) + + def __init__(self, thunk: Callable[[], Iterable[T]]) -> None: + self._thunk = thunk + + def GetEnumerator(self, __unit: Unit = UNIT) -> IEnumerator[T]: + return Enumerator(iter(self._thunk())) + + def __iter__(self) -> Iterator[T]: + return iter(self._thunk()) + + +def _source_iter[T](xs: IEnumerable_1[T]) -> Iterator[T]: + """A Python iterator over `xs` matching its IEnumerable contract. + + Most sources' `__iter__` already matches `GetEnumerator`/`Current`, so iterating directly + is correct and avoids MoveNext/Current overhead. Exception: `Mapping` (`FSharpMap`) - its + `__iter__` yields keys only, diverging from the `KeyValuePair` tuples `Current` produces - + those need the explicit bridge. + """ + match xs: + case Mapping(): + return to_iterator(get_enumerator(xs)) + case Iterable(): + return iter(xs) + case _: + return to_iterator(get_enumerator(xs)) + + +def map[T, U](mapping: Callable[[T], U], xs: IEnumerable_1[T]) -> NativeSeq[U]: + return NativeSeq(lambda: (mapping(x) for x in _source_iter(xs))) + + +def filter[T](predicate: Callable[[T], bool], xs: IEnumerable_1[T]) -> NativeSeq[T]: + return NativeSeq(lambda: (x for x in _source_iter(xs) if predicate(x))) + + +def collect[T, U](mapping: Callable[[T], IEnumerable_1[U]], xs: IEnumerable_1[T]) -> NativeSeq[U]: + return NativeSeq(lambda: (y for x in _source_iter(xs) for y in _source_iter(mapping(x)))) + + +def append[T](xs: IEnumerable_1[T], ys: IEnumerable_1[T]) -> NativeSeq[T]: + def gen() -> Iterator[T]: + yield from _source_iter(xs) + yield from _source_iter(ys) + + return NativeSeq(gen) + + +def truncate[T](count: int, xs: IEnumerable_1[T]) -> NativeSeq[T]: + def gen() -> Iterator[T]: + if count <= 0: + return + + i = 0 + for x in _source_iter(xs): + yield x # yield BEFORE checking, so we never pull the (count+1)-th element + i += 1 + if i >= count: + return + + return NativeSeq(gen) + + +def unfold[T, S](generator: Callable[[S], tuple[T, S] | None], state: S) -> NativeSeq[T]: + def gen() -> Iterator[T]: + s = state + while True: + r = generator(s) + if r is None: # F# option erases to None here: the payload is a tuple, never None + return + x, s = r + yield x + + return NativeSeq(gen) + + +def choose[T, U](chooser: Callable[[T], Option[U]], xs: IEnumerable_1[T]) -> NativeSeq[U]: + def gen() -> Iterator[U]: + for x in _source_iter(xs): + r = chooser(x) + if r is not None: + yield value(r) + + return NativeSeq(gen) + + +def map_indexed[T, U](mapping: Callable[[int32, T], U], xs: IEnumerable_1[T]) -> NativeSeq[U]: + return NativeSeq(lambda: (mapping(int32(i), x) for i, x in enumerate(_source_iter(xs)))) + + +def map2[T1, T2, U](mapping: Callable[[T1, T2], U], xs: IEnumerable_1[T1], ys: IEnumerable_1[T2]) -> NativeSeq[U]: + return NativeSeq(lambda: (mapping(x, y) for x, y in zip(_source_iter(xs), _source_iter(ys)))) + + +def map_indexed2[T1, T2, U]( + mapping: Callable[[int32, T1, T2], U], xs: IEnumerable_1[T1], ys: IEnumerable_1[T2] +) -> NativeSeq[U]: + return NativeSeq( + lambda: (mapping(int32(i), x, y) for i, (x, y) in enumerate(zip(_source_iter(xs), _source_iter(ys)))) + ) + + +def map3[T1, T2, T3, U]( + mapping: Callable[[T1, T2, T3], U], xs: IEnumerable_1[T1], ys: IEnumerable_1[T2], zs: IEnumerable_1[T3] +) -> NativeSeq[U]: + return NativeSeq( + lambda: (mapping(x, y, z) for x, y, z in zip(_source_iter(xs), _source_iter(ys), _source_iter(zs))) + ) + + +def take_while[T](predicate: Callable[[T], bool], xs: IEnumerable_1[T]) -> NativeSeq[T]: + def gen() -> Iterator[T]: + for x in _source_iter(xs): + if not predicate(x): + return + yield x + + return NativeSeq(gen) + + +def take[T](count: int, xs: IEnumerable_1[T]) -> NativeSeq[T]: + def gen() -> Iterator[T]: + it = _source_iter(xs) + for _ in range(count): + v = next(it, _SENTINEL) + if v is _SENTINEL: + raise Exception("The input sequence has an insufficient number of elements. (Parameter 'source')") + yield v + + return NativeSeq(gen) + + +# Eager terminal ops: pull via _source_iter instead of the Option-per-element `generate` +# protocol the generic (non-Array/FSharpList) fallback used before. +def try_head[T](xs: IEnumerable_1[T]) -> Option[T]: + match xs: + case Array(): + return array_try_head(xs) + case FSharpList(): + return list_try_head(xs) + case _: + v = next(_source_iter(xs), _SENTINEL) + return None if v is _SENTINEL else some(v) + + +def head[T](xs: IEnumerable_1[T]) -> T: + result = try_head(xs) + if result is None: + raise Exception("The input sequence was empty. (Parameter 'source')") + return value(result) + + +def try_item[T](index: int32, xs: IEnumerable_1[T]) -> Option[T]: + match xs: + case Array(): + return array_try_item(index, xs) + case FSharpList(): + return list_try_item(index, xs) + case _: + if index < 0: + return None + + it = _source_iter(xs) + for _ in range(index): + if next(it, _SENTINEL) is _SENTINEL: + return None + + v = next(it, _SENTINEL) + return None if v is _SENTINEL else some(v) + + +def item[T](index: int32, xs: IEnumerable_1[T]) -> T: + result = try_item(index, xs) + if result is None: + raise Exception("The input sequence has an insufficient number of elements. (Parameter 'index')") + return value(result) + + +__all__ = [ + "NativeSeq", + "append", + "choose", + "collect", + "filter", + "head", + "item", + "map", + "map2", + "map3", + "map_indexed", + "map_indexed2", + "take", + "take_while", + "truncate", + "try_head", + "try_item", + "unfold", +] diff --git a/src/fable-library-py/fable_library/util.py b/src/fable-library-py/fable_library/util.py index 66542668d..d25e19b12 100644 --- a/src/fable-library-py/fable_library/util.py +++ b/src/fable-library-py/fable_library/util.py @@ -482,7 +482,10 @@ def System_Collections_IEnumerator_Reset(self) -> None: raise Exception("Python iterators cannot be reset") def Dispose(self) -> None: - return + # Runs `finally` blocks via GeneratorExit; guarded since not all iterators support `close`. + close = getattr(self.iter, "close", None) + if close is not None: + close() def __next__(self) -> T: return next(self.iter)