Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/Fable.Transforms/FSharp2Fable.Util.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2016,6 +2016,42 @@ module Util =

Fable.TryCatch(body, catchClause, finalizer, r)

/// Temporary fix for try/with with filtered (typed or `when`) handlers in seq expressions.
/// FCS compiles the handler of RuntimeHelpers.EnumerateTryWith with FailFilter as the
/// match-failure action (CheckSequenceExpressions.fs), so the handler's unmatched fallback
/// branch is emitted as a mistyped `0` (int) where seq<'T> is expected. The branch is
/// unreachable at runtime (Seq.enumerateTryWith only calls the handler after the filter
/// matched), but the wrong type breaks statically-typed targets (TypeScript/Dart).
/// Rewrite the mistyped leaf into a rethrow of the caught exception, which is bottom-typed
/// on all targets and semantically correct if it were ever reached.
/// TODO: Remove when FCS compiles the handler with Rethrow like normal try/with does
/// (see CheckExpressions.fs vs CheckSequenceExpressions.fs).
/// FCS issue: https://github.com/dotnet/fsharp/issues/20040
/// Fable PR: https://github.com/fable-compiler/Fable/pull/4719
let fixEnumerateTryWithHandler (memb: FSharpMemberOrFunctionOrValue) (args: Fable.Expr list) =
if memb.FullName = "Microsoft.FSharp.Core.CompilerServices.RuntimeHelpers.EnumerateTryWith" then
match args with
| [ source; filterLambda; Fable.Lambda(exIdent, handlerBody, name) ] ->
let expectedType = handlerBody.Type

let rec fixLeaf (expr: Fable.Expr) =
match expr with
| Fable.Value(Fable.NumberConstant _, r) when expr.Type <> expectedType ->
makeThrow r expectedType (Fable.IdentExpr exIdent)
| Fable.IfThenElse(guardExpr, thenExpr, elseExpr, r) ->
Fable.IfThenElse(guardExpr, fixLeaf thenExpr, fixLeaf elseExpr, r)
| Fable.DecisionTree(tree, targets) ->
let targets = targets |> List.map (fun (idents, e) -> idents, fixLeaf e)

Fable.DecisionTree(tree, targets)
| Fable.Let(ident, value, body) -> Fable.Let(ident, value, fixLeaf body)
| _ -> expr

[ source; filterLambda; Fable.Lambda(exIdent, fixLeaf handlerBody, name) ]
| _ -> args
else
args

let addGenArgsToContext (ctx: Context) (memb: FSharpMemberOrFunctionOrValue) (genArgs: Fable.Type list) =
if not (List.isEmpty genArgs) then
let genParams =
Expand Down
3 changes: 3 additions & 0 deletions src/Fable.Transforms/FSharp2Fable.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,9 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs
else
args

// Fix the mistyped fallback FCS generates for filtered try/with handlers in seq expressions
let args = fixEnumerateTryWithHandler memb args

match callee, memb with
| Some(CreateEvent(callee, event) as createEvent), _ ->
let! callee = transformExpr com ctx [] callee
Expand Down
70 changes: 70 additions & 0 deletions src/fable-library-dart/Seq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,76 @@ let enumerateWhile (guard: unit -> bool) (xs: seq<'T>) : seq<'T> =
0
)

let enumerateTryWith (source: seq<'T>) (catchFilter: exn -> int) (catchHandler: exn -> seq<'T>) : seq<'T> =
mkSeq (fun () ->
let mutable e: IEnumerator<'T> option = None
let mutable caught = false // switched to the handler sequence
let mutable started = false
let mutable finished = false
let mutable curr = None

let current () =
if not started then
Enumerator.notStarted ()
elif finished then
Enumerator.alreadyFinished ()

match curr with
| None -> Enumerator.alreadyFinished ()
| Some x -> x

let rec tryNext () =
try
let en =
match e with
| Some en -> en
| None ->
let en = source.GetEnumerator()
e <- Some en
en

if en.MoveNext() then
curr <- Some en.Current
true
else
finished <- true
false
with ex when not caught ->
if catchFilter ex <> 0 then
caught <- true

match e with
| Some en ->
(try
en.Dispose()
with _ ->
())

e <- None
| None -> ()

e <- Some((catchHandler ex).GetEnumerator())
tryNext ()
else
reraise ()

let next () =
if not started then
started <- true

if finished then
false
else
tryNext ()

let dispose () =
match e with
| Some en -> en.Dispose()
| None -> ()

Enumerator.fromFunctions current next dispose
)

let filter f (xs: seq<'T>) =
xs
|> choose (fun x ->
Expand Down
65 changes: 65 additions & 0 deletions src/fable-library-py/fable_library/Seq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,71 @@ let enumerateWhile (guard: unit -> bool) (xs: seq<'T>) =
0
)

// State for `enumerateTryWith`, threaded through `generate` as a parameter (rather than captured
// mutables) so Fable's Python codegen never emits a `nonlocal` for the swapped enumerator — a
// plain `mutable` would produce conflicting `nonlocal e` declarations across the try/except.
// A record (not a `ref` cell) is used on purpose: Fable's ref-cell -> mutable optimization could
// reintroduce the `nonlocal`, whereas a record field is always an attribute assignment.
type private EnumerateTryWithState<'T> =
{
mutable Source: IEnumerator<'T> option
mutable Caught: bool // switched to the handler sequence
}

let enumerateTryWith (source: seq<'T>) (catchFilter: exn -> int) (catchHandler: exn -> seq<'T>) : seq<'T> =
generate
(fun () ->
{
Source = None
Caught = false
}
)
(fun (state: EnumerateTryWithState<'T>) ->
let mutable result = None
let mutable go = true

while go do
try
let en =
match state.Source with
| Some en -> en
| None ->
let en = source.GetEnumerator()
state.Source <- Some en
en

if en.MoveNext() then
result <- Some en.Current
go <- false
else
// exhausted: leave result = None so `generate` finishes
go <- false
with ex ->
if (not state.Caught) && catchFilter ex <> 0 then
match state.Source with
| Some en ->
(try
en.Dispose()
with _ ->
())

state.Source <- None
| None -> ()

state.Source <- Some((catchHandler ex).GetEnumerator())
state.Caught <- true
// else: loop again, pulling from the handler sequence
else
reraise ()

result
)
(fun (state: EnumerateTryWithState<'T>) ->
match state.Source with
| Some en -> en.Dispose()
| None -> ()
)

[<Import("filter", "fable_library.seq_native")>]
let filter (predicate: 'T -> bool) (xs: seq<'T>) : seq<'T> = nativeOnly

Expand Down
52 changes: 52 additions & 0 deletions src/fable-library-rust/src/Seq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,58 @@ let enumerateWhile (guard: unit -> bool) (xs: 'T seq) =
0
)

let enumerateTryWith (source: 'T seq) (catchFilter: exn -> int) (catchHandler: exn -> 'T seq) =
mkSeq (fun () ->
let mutable e: IEnumerator<'T> option = None
let mutable caught = false // switched to the handler sequence
let mutable finished = false

let next () =
let mutable res = None
let mutable again = true

while again do
again <- false

if not finished then
try
let en =
match e with
| Some en -> en
| None ->
let en = source.GetEnumerator()
e <- Some en
en

if en.MoveNext() then
res <- Some en.Current
else
finished <- true
with ex ->
if not caught && catchFilter ex <> 0 then
caught <- true

match e with
| Some en ->
en.Dispose()
e <- None
| None -> ()

e <- Some((catchHandler ex).GetEnumerator())
again <- true // pull the next element from the handler sequence
else
reraise ()

res

let dispose () =
match e with
| Some en -> en.Dispose()
| None -> ()

Enumerable.fromFunctions next dispose
)

let exactlyOne (xs: 'T seq) =
use e = ofSeq xs

Expand Down
70 changes: 70 additions & 0 deletions src/fable-library-ts/Seq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,76 @@ let enumerateWhile (guard: unit -> bool) (xs: seq<'T>) =
0
)

let enumerateTryWith (source: seq<'T>) (catchFilter: exn -> int) (catchHandler: exn -> seq<'T>) : seq<'T> =
mkSeq (fun () ->
let mutable e: IEnumerator<'T> option = None
let mutable caught = false // switched to the handler sequence
let mutable started = false
let mutable finished = false
let mutable curr = None

let current () =
if not started then
Enumerator.notStarted ()
elif finished then
Enumerator.alreadyFinished ()

match curr with
| None -> Enumerator.alreadyFinished ()
| Some x -> x

let rec tryNext () =
try
let en =
match e with
| Some en -> en
| None ->
let en = source.GetEnumerator()
e <- Some en
en

if en.MoveNext() then
curr <- Some en.Current
true
else
finished <- true
false
with ex when not caught ->
if catchFilter ex <> 0 then
caught <- true

match e with
| Some en ->
(try
en.Dispose()
with _ ->
())

e <- None
| None -> ()

e <- Some((catchHandler ex).GetEnumerator())
tryNext ()
else
reraise ()

let next () =
if not started then
started <- true

if finished then
false
else
tryNext ()

let dispose () =
match e with
| Some en -> en.Dispose()
| None -> ()

Enumerator.fromFunctions current next dispose
)

let filter f (xs: seq<'T>) =
xs
|> choose (fun x ->
Expand Down
51 changes: 51 additions & 0 deletions tests/Beam/SeqExpressionTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,57 @@ let ``test try finally in seq expressions works`` () =
with _ -> ()
equal 1 n

[<Fact>]
let ``test try...with in list expressions works`` () =
[ try 1 with _ -> 0 ]
|> equal [1]

[<Fact>]
let ``test try...with in list expressions catches exceptions`` () =
[ try raise (exn "boom") with _ -> 42 ]
|> equal [42]

[<Fact>]
let ``test try...with in array expressions works`` () =
[| try raise (exn "boom") with _ -> 7 |]
|> equal [| 7 |]

[<Fact>]
let ``test try...with in seq expressions preserves yielded elements before the exception`` () =
let mutable caught = false
let xs =
seq {
try
yield 1
raise (exn "boom")
yield 2
with _ ->
caught <- true
yield 3
} |> Seq.toList
equal [1; 3] xs
equal true caught

// TODO: enable when Beam correctly rethrows exceptions not matched by a filtered
// try/with handler in seq expressions (see PR #4719 review discussion)
// [<Fact>]
// let ``test try...with in seq expressions rethrows unmatched exceptions`` () =
// let mutable propagated = false
// try
// seq {
// try
// yield 1
// raise (System.InvalidOperationException "boom")
// yield 2
// with :? System.ArgumentException ->
// yield 0
// }
// |> Seq.toList
// |> ignore
// with :? System.InvalidOperationException ->
// propagated <- true
// equal true propagated

[<Fact>]
let ``test array expressions work`` () =
[| for x in 1 .. 10 do yield x |]
Expand Down
Loading