Filtered try/with in seq { } emits int 0 where seq<'T> is expected for the unmatched case
- Dominant language
- F#
- Stars
- 4.3k
- Forks
- 876
- Avg merge
- 4d 22h
- Merged PRs (30d)
- 144
Description
This affects **F# Compiler Services (FCS)**. We hit it in the [Fable](https://github.com/fable-compiler/Fable) compiler, which builds on FCS and consumes the typed tree to transpile F# to other languages. The mistyped node breaks Fable's statically-typed targets.
A `try/with` inside a sequence expression (`seq { }`) that uses a **filtered** handler — a type test (`with :? SomeExn ->`) or a `when` guard — is lowered to `RuntimeHelpers.EnumerateTryWith`. In `CheckSequenceExpressions.fs` the handler clauses are compiled with `FailFilter` as the match-failure action, so the unmatched fallback branch is emitted as a literal `0` of type `int` in a position where the handler must return `seq<'T>`. A normal (non-sequence) `try/with` compiles its handler with `Rethrow`, which is correctly typed. The wrong type produces invalid output on downstream consumers that rely on the typed tree.
**Repro steps**
1. Compile a sequence expression containing a `try/with` with a filtered handler:
```fsharp
seq {
try raise (System.InvalidOperationException "boom")
with :? System.ArgumentException -> yield 0
} |> Seq.toList |> ignore
```
2. Inspect the typed tree for the `RuntimeHelpers.EnumerateTryWith` handler lambda and look at the branch taken when the exception does not match the filter.
**Expected behavior**
The unmatched/fallback case of the handler should be compiled the same way as a normal `try/with` — as a **rethrow (`reraise`) of the caught exception**. That expression is bottom-typed and therefore compatible with the `seq<'T>` result type, and it is semantically correct (a non-matching exception should re-propagate).
In `CheckExpressions.fs`, `TcExprTryWith` already does this — the handler is compiled with `Rethrow`:
```fsharp
// CheckExpressions.fs:6511-6512
let v1, filterExpr = CompilePatternForMatchClauses cenv env mWithToLast mWithToLast true FailFilter None g.exn_ty g.int_ty checkedFilterClauses
let v2, handlerExpr = CompilePatternForMatchClauses cenv env mWithToLast mWithToLast true Rethrow None g.exn_ty overallTy.Commit checkedHandlerClauses
```
`CheckSequenceExpressions.fs` should use `Rethrow` for the handler in the same way.
**Actual behavior**
`CheckSequenceExpressions.fs` compiles the handler with `FailFilter` instead of `Rethrow`:
```fsharp
// CheckSequenceExpressions.fs:356-360
let v1, filterExpr =
CompilePatternForMatchClauses cenv env withRange withRange true FailFilter None g.exn_ty g.int_ty filterClauses
// correct: int
let v2, handlerExpr =
CompilePatternForMatchClauses cenv env withRange withRange true FailFilter None g.exn_ty genOuterTy handlers
// BUG: should be Rethrow
```
`FailFilter` lowers the fallback leaf to a literal `int 0`:
```fsharp
// PatternMatchCompilation.fs:1029-1035
| FailFilter -> mkInt g mMatch 0 // returns 0 (int) — wrong when result type is seq<'T>
| Rethrow -> mkReraise mMatch resultTy // bottom-typed — what's needed here
```
So the handler's unmatched fallback is `int 0` where `genOuterTy` (`seq<'T>`) is expected. The branch is unreachable at runtime (the handler only runs after the filter matches), so it is harmless on .NET, but the mistyped node is invalid for consumers of the typed tree that require correct types on every branch. In Fable this breaks compilation of the statically-typed targets.
**Suggested fix:** in `CheckSequenceExpressions.fs` (line 360), change the handler's `ActionOnFailure` from `FailFilter` to `Rethrow`, mirroring `CheckExpressions.fs:6512`.
**Known workarounds**
In the Fable compiler we post-process the `EnumerateTryWith` handler lambda and rewrite the mistyped `int 0` fallback leaf into a rethrow of the caught exception (walking through `if/then/else`, decision trees, and `let` bindings to reach the leaf).
**Related information**
* Affected component: F# Compiler Services (FCS) — `CheckSequenceExpressions.fs`; surfaced via the Fable compiler
* Operating system: macOS (Darwin), cross-platform
* .NET Runtime kind: .NET (Core), SDK 10+
* Editing Tools: N/A — reproduces via the compiler directly (F# Compiler Services)
Contributor guide
Research direction
Start in CheckSequenceExpressions.fs at the handler compilation around lines 356-360, then compare it with TcExprTryWith in CheckExpressions.fs around lines 6511-6512 and the failure actions in PatternMatchCompilation.fs. Reproduce the filtered try/with sequence and inspect its typed tree. Done means the unmatched handler branch is a rethrow rather than an int 0, while the filter still uses FailFilter.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- fsharp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100