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
22 changes: 17 additions & 5 deletions src/Fable.Transforms/Python/Fable2Python.Annotation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -306,11 +306,16 @@ let stdlibModuleAnnotation (com: IPythonCompiler) ctx moduleName memberName args
match args with
| Expression.Name { Id = Identifier Ellipsis } :: _xs -> Expression.ellipsis
| _ ->
args
|> List.removeAt (args.Length - 1)
let argsWithoutReturn = args |> List.removeAt (args.Length - 1)

argsWithoutReturn
|> List.choose (
function
| Expression.Name { Id = Identifier "None" } when args.Length = 2 -> None
// Filter out None (unit) only when it's the sole argument.
// F# `unit -> T` means "takes no args" in Python: Callable[[], T]
// But `unit -> 'a -> T` uncurried to `(unit, 'a) -> T` must keep
// None to match the actual function signature with unit parameter.
| Expression.Name { Id = Identifier "None" } when argsWithoutReturn.Length = 1 -> None
| x -> Some x
)
|> Expression.list
Expand Down Expand Up @@ -441,7 +446,7 @@ let rec typeAnnotation
fableModuleAnnotation com ctx "option" "Option" [ Expression.none ], []
| Fable.Option(genArg, _) ->
// Must match mustWrapOption logic in Transforms.Util.fs
// Wrap when: Any, Unit, GenericParam, or nested Option
// Wrap when: Any, Unit, GenericParam, nested Option, or callable with generic params
match genArg with
| Fable.Option _
| Fable.Any
Expand All @@ -450,8 +455,15 @@ let rec typeAnnotation
// Use full Option type annotation (code will use SomeWrapper)
let resolved, stmts = resolveGenerics com ctx [ genArg ] repeatedGenerics
fableModuleAnnotation com ctx "option" "Option" resolved, stmts
| Fable.LambdaType _
| Fable.DelegateType _ when containsGenericParams genArg ->
// Callable types with generic parameters (e.g., Callable[[_A], _B])
// Must use Option[T] form because runtime wraps with SomeWrapper
let resolved, stmts = resolveGenerics com ctx [ genArg ] repeatedGenerics
fableModuleAnnotation com ctx "option" "Option" resolved, stmts
| _ ->
// For concrete types, erase to T | None (simpler, no wrapper needed)
// For concrete types (including DeclaredTypes with generics like FSharpList[T]),
// erase to T | None (simpler, no wrapper needed)
let resolved, stmts = typeAnnotation com ctx repeatedGenerics genArg
Expression.binOp (resolved, BitOr, Expression.none), stmts
| Fable.Tuple(genArgs, _) -> makeGenericTypeAnnotation com ctx "tuple" genArgs repeatedGenerics, []
Expand Down
45 changes: 38 additions & 7 deletions src/Fable.Transforms/Python/Fable2Python.Transforms.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1109,19 +1109,36 @@ let transformCall (com: IPythonCompiler) ctx range callee (callInfo: Fable.CallI
let transformCurriedApply com ctx range (TransformExpr com ctx (applied, stmts)) args =
((applied, stmts), args)
||> List.fold (fun (applied, stmts) arg ->
let args, stmts' =
let args, argStmts =
match arg with
// TODO: If arg type is unit but it's an expression with potential
// side-effects, we need to extract it and execute it before the call

// TODO: discardUnitArg may still be needed in some cases
| Fable.Value(Fable.UnitConstant, _) -> [], []
| Fable.IdentExpr ident when ident.Type = Fable.Unit -> [], []
| TransformExpr com ctx (arg, stmts') -> [ arg ], stmts'
| arg ->
let argExpr, transformStmts = com.TransformAsExpr(ctx, arg)
// When a Call or CurriedApply result is passed as an argument,
// check if we need to erase Option[T] to T | None
let argExpr =
if needsOptionEraseForBinding arg arg.Type then
wrapInOptionErase com ctx argExpr
else
argExpr

[ argExpr ], transformStmts

callFunction range applied args [], stmts @ stmts'
callFunction range applied args [], stmts @ argStmts
)

/// Extract the expected return type from a return strategy, unwrapping ResourceManager if needed
let rec private getExpectedReturnType (strategy: ReturnStrategy option) =
match strategy with
| Some(Return(Some expectedType)) -> Some expectedType
| Some(ResourceManager inner) -> getExpectedReturnType inner
| _ -> None

let transformCallAsStatements com ctx range t returnStrategy callee callInfo =
let argsLen (i: Fable.CallInfo) =
List.length i.Args
Expand All @@ -1141,17 +1158,18 @@ let transformCallAsStatements com ctx range t returnStrategy callee callInfo =
| _ ->
let expr, stmts = transformCall com ctx range callee callInfo
// Check if we need to cast Option[T] to T | None for return statements
// Also handles ResourceManager-wrapped return strategies (e.g., inside `with` blocks)
let expr =
match returnStrategy with
| Some(Return(Some expectedType)) ->
match getExpectedReturnType returnStrategy with
| Some expectedType ->
// Create a temporary Fable.Call to check if erase is needed
let callExpr = Fable.Call(callee, callInfo, t, range)

if needsOptionEraseForReturn callExpr expectedType then
wrapInOptionErase com ctx expr
else
expr
| _ -> expr
| None -> expr

stmts @ (expr |> resolveExpr ctx t returnStrategy)

Expand Down Expand Up @@ -3996,7 +4014,14 @@ let transformUnion (com: IPythonCompiler) ctx (ent: Fable.Entity) (entName: stri
// Convert to snake_case and clean to remove invalid characters like apostrophes
// Handles: "Item" -> "item", "Item1" -> "item1", "MyField" -> "my_field"
let fieldName = field.Name |> Naming.toSnakeCase |> Helpers.clean
let ta, _ = Annotation.typeAnnotation com caseCtx None field.FieldType
// Uncurry lambda types for field annotations since union case fields
// store uncurried functions at runtime (same as record fields)
let fieldType =
match field.FieldType with
| Fable.LambdaType _ -> FableTransforms.uncurryType field.FieldType
| _ -> field.FieldType

let ta, _ = Annotation.typeAnnotation com caseCtx None fieldType
let target = Expression.name (fieldName, Store)
// Use annAssign to generate: field: type (not field = type)
Statement.annAssign (target, annotation = ta, simple = true)
Expand Down Expand Up @@ -4763,6 +4788,12 @@ let rec transformDeclaration (com: IPythonCompiler) ctx (decl: Fable.Declaration
let value, stmts = transformAsExpr com ctx decl.Body
let name = com.GetIdentifier(ctx, Naming.toPythonNaming decl.Name)
let ta, _ = Annotation.typeAnnotation com ctx None decl.Body.Type
// Erase Option wrapper if needed (Call/CurriedApply returning Option[T] to T | None)
let value =
if needsOptionEraseForBinding decl.Body decl.Body.Type then
wrapInOptionErase com ctx value
else
value

stmts @ declareModuleMember com ctx info.IsPublic name (Some ta) value
else
Expand Down
30 changes: 26 additions & 4 deletions src/Fable.Transforms/Python/Fable2Python.Util.fs
Original file line number Diff line number Diff line change
Expand Up @@ -100,34 +100,56 @@ module Util =
|| hasAttribute Atts.emitIndexer atts
|| hasAttribute Atts.emitProperty atts

/// Check if a type contains any generic parameters (recursively).
/// Used to determine if Option<T> should use Option[T] annotation vs T | None.
/// Note: This is duplicated from Annotation.fs due to compilation order (Util.fs compiles first).
let private containsGenericParams (t: Fable.Type) =
FSharp2Fable.Util.getGenParamNames [ t ] |> List.isEmpty |> not

/// Check if an Option type has a concrete inner type (erases to T | None).
/// Returns true when inner type is concrete, meaning Option[T] -> T | None.
let hasConcreteOptionInner (typ: Fable.Type) =
match typ with
| Fable.Option(innerType, _) -> not (mustWrapOption innerType)
| _ -> false

/// Check if inner type of Option is a callable with generic params (needs wrapping)
let private isCallableWithGenerics (t: Fable.Type) =
match t with
| Fable.LambdaType _
| Fable.DelegateType _ -> containsGenericParams t
| _ -> false

/// Check if a call expression needs Option erase from Option[T] to T | None.
/// When target type is Option<ConcreteType> (erased to T | None), we need to erase
/// because library functions use Option[T] (wrapped form) in their signatures,
/// but actual runtime values are not wrapped for concrete types.
let private needsOptionEraseForCall (targetType: Fable.Type) =
/// Also handles function types (LambdaType) where the return type contains a concrete Option,
/// using the erase() overload for Callable[..., Option[T]] -> Callable[..., T | None].
let rec private needsOptionEraseForCall (targetType: Fable.Type) =
match targetType with
| Fable.Option(tgtInner, _) when not (mustWrapOption tgtInner) -> true
// Don't erase if inner type is callable with generic params - both annotation and runtime use wrapped form
| Fable.Option(tgtInner, _) when not (mustWrapOption tgtInner) && not (isCallableWithGenerics tgtInner) -> true
// Handle function types where return type is a concrete Option
// erase() has an overload: Callable[..., Option[T]] -> Callable[..., T | None]
| Fable.LambdaType(_, returnType) -> needsOptionEraseForCall returnType
| Fable.DelegateType(_, returnType) -> needsOptionEraseForCall returnType
| _ -> false

/// Check if binding needs Option erase from Option[T] to T | None.
let needsOptionEraseForBinding (value: Fable.Expr) (targetType: Fable.Type) =
match value with
| Fable.Call _ -> needsOptionEraseForCall targetType
| Fable.Call _
| Fable.CurriedApply _ -> needsOptionEraseForCall targetType
| _ -> false

/// Check if return expression needs Option erase from Option[T] to T | None.
/// Used when returning from a function where the expected return type is T | None
/// but the actual expression returns Option[T] (wrapped form).
let needsOptionEraseForReturn (value: Fable.Expr) (expectedReturnType: Fable.Type) =
match value with
| Fable.Call _ -> needsOptionEraseForCall expectedReturnType
| Fable.Call _
| Fable.CurriedApply _ -> needsOptionEraseForCall expectedReturnType
| _ -> false

/// Recursively check if a type contains Option (wrapped or erased).
Expand Down
Loading