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
2 changes: 1 addition & 1 deletion pyrightconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
"reportConstantRedefinition": false, // Fable preserves casing for variables starting with uppercase
"reportUnnecessaryIsInstance": false, // Not a problem in generated code (look into this later)
"reportUnnecessaryComparison": false, // Generated tests do this on purpose
"reportUnnecessaryCast": false, // Generated code for union case field need addional cast otherwise Pyrigh cannot infer the resolved typed
"reportImportCycles": true,
"reportMissingImports": true,
"reportUnnecessaryCast": true,
"reportDuplicateImport": true,
"reportOverlappingOverload": true,
"reportInconsistentConstructor": true,
Expand Down
11 changes: 6 additions & 5 deletions src/Fable.Transforms/Python/Fable2Python.Transforms.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1466,12 +1466,13 @@ let transformGet (com: IPythonCompiler) ctx range typ (fableExpr: Fable.Expr) ki
let field = uci.UnionCaseFields |> List.item i.FieldIndex
let fieldName = field.Name |> Naming.toFieldSnakeCase |> Helpers.clean
// `fableExpr` is statically typed as the union base, which doesn't declare this
// field, so tell Pyright which case class we're reading from (cast is a no-op
// at runtime). See reportUnnecessaryCast in pyrightconfig.json.
// field, so tell Pyright which case class we're reading from via the library's
// `narrow` helper (a no-op at runtime). Keeping the "cast" inside `narrow`
// leaves generated code cast-free and preserves reportUnnecessaryCast.
let caseRef = getUnionCaseRef com ctx i.Entity ent uci
let cast = com.GetImportExpr(ctx, "typing", "cast")
let castedExpr = Expression.call (cast, [ caseRef; baseExpr ])
let! finalExpr = getExpr com ctx range castedExpr (Expression.stringConstant fieldName)
let narrow = libValue com ctx "union" "narrow"
let narrowedExpr = Expression.call (narrow, [ caseRef; baseExpr ])
let! finalExpr = getExpr com ctx range narrowedExpr (Expression.stringConstant fieldName)
return finalExpr
}

Expand Down
20 changes: 19 additions & 1 deletion src/fable-library-py/fable_library/union.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,24 @@ def CompareTo(self, other: Any) -> int:
return -1 if self.tag < other.tag else 1


def narrow[T](case: type[T], value: object) -> T:
"""Narrow a union value to one of its case classes for the type checker.

F# discriminated unions compile to a base ``Union`` class plus one case
class per case (e.g. ``Tree`` -> ``Tree_Leaf | Tree_Node``). Reading a
case-specific field such as ``x.left_`` requires the value to be typed as
that case class, but after a ``match`` on ``tag`` it is still statically
typed as the union. ``narrow(Tree_Node, x)`` re-types ``x`` so the field
read resolves, playing the same role as ``typing.cast`` but scoped to
unions. Keeping it in the library (instead of emitting ``cast`` inline)
lets generated code stay ``cast``-free and preserves ``reportUnnecessaryCast``.

``case`` is only used by the type checker to bind ``T``; this is an identity
function at runtime.
"""
return value # pyright: ignore[reportReturnType]


@dataclass_transform()
def tagged_union(tag: int):
"""Decorator for union case classes.
Expand Down Expand Up @@ -135,4 +153,4 @@ def fields(self) -> Array[Any]:
return decorator


__all__ = ["Union", "tagged_union"]
__all__ = ["Union", "narrow", "tagged_union"]