From 2de0ac5f893faa7a166282a49c03513f93244cef Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Fri, 12 Jun 2026 19:28:09 +0200 Subject: [PATCH] fix(python): emit wildcard default for union or-patterns (#4649) When an or-pattern over union cases shares its target with the default branch (e.g. `BestCase | MidCase -> true | WorstCase -> false`), the Python match generator dropped the wildcard `case _:` because the default's target index was already used by an explicit case. The union tag that was only reachable via the default then matched nothing and the function returned None. Always emit the wildcard default and instead drop the redundant explicit cases that route to the default's target, since the wildcard covers them. Co-Authored-By: Claude Fable 5 --- .../Python/Fable2Python.Transforms.fs | 33 ++++++++----------- tests/Python/TestPatternMatch.fs | 18 ++++++++++ 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 668d6800d..88da9750f 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -1994,19 +1994,23 @@ let private transformSwitchPatternAsMatch else let ctx = { ctx with DecisionTargets = targets } - // Group cases by target index to create or-patterns + // Group cases by target index to create or-patterns. Drop cases routing to the + // default's target since the always-emitted wildcard already covers them (#4649). let groupedCases = convertedCases |> List.groupBy snd - |> List.map (fun (targetIndex, patterns) -> - let patterns = patterns |> List.map fst + |> List.choose (fun (targetIndex, patterns) -> + if targetIndex = defaultIndex then + None + else + let patterns = patterns |> List.map fst - let pattern = - match patterns with - | [ single ] -> single - | multiple -> MatchOr multiple + let pattern = + match patterns with + | [ single ] -> single + | multiple -> MatchOr multiple - (pattern, targetIndex) + Some(pattern, targetIndex) ) // Build match cases @@ -2018,18 +2022,9 @@ let private transformSwitchPatternAsMatch MatchCase.matchCase (pattern, body) ) - // Build default case (wildcard) + // Build default case (wildcard) and always append it last let defaultCase = buildDefaultMatchCase com ctx returnStrategy targets defaultIndex - - // Check if the default case is already covered by the grouped cases - let defaultAlreadyCovered = - groupedCases |> List.exists (fun (_, idx) -> idx = defaultIndex) - - let allCases = - if defaultAlreadyCovered then - matchCases - else - matchCases @ [ defaultCase ] + let allCases = matchCases @ [ defaultCase ] // Transform the evaluation expression let subject, stmts = com.TransformAsExpr(ctx, evalExpr) diff --git a/tests/Python/TestPatternMatch.fs b/tests/Python/TestPatternMatch.fs index dd852c25c..932d0eb98 100644 --- a/tests/Python/TestPatternMatch.fs +++ b/tests/Python/TestPatternMatch.fs @@ -135,6 +135,18 @@ let colorMatch3 c = | Blue -> "blue" | _ -> "other" +type Case = + | BestCase + | MidCase + | WorstCase + +// Or-pattern over union cases where the grouped cases share the same target +// as the default branch. See https://github.com/fable-compiler/Fable/issues/4649 +let atLeastMid c = + match c with + | BestCase | MidCase -> true + | WorstCase -> false + [] let ``test union match with field extraction`` () = unionMatch (Circle 5.0) |> equal "Circle 5" @@ -159,6 +171,12 @@ let ``test simple union match with 3 cases`` () = colorMatch3 Blue |> equal "blue" colorMatch3 Green |> equal "other" +[] +let ``test or-pattern over union cases sharing default target`` () = + atLeastMid BestCase |> equal true + atLeastMid MidCase |> equal true + atLeastMid WorstCase |> equal false + // ---------------------------------------------------------------------------- // 5. Guard Expressions (when clauses) // ----------------------------------------------------------------------------