diff --git a/src/Fable.Transforms/Python/Fable2Python.Annotation.fs b/src/Fable.Transforms/Python/Fable2Python.Annotation.fs index 23cffb31d..d1baf0d60 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Annotation.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Annotation.fs @@ -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 @@ -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 @@ -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, [] diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 2ffae0232..13f32ffe8 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -1109,7 +1109,7 @@ 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 @@ -1117,11 +1117,28 @@ let transformCurriedApply com ctx range (TransformExpr com ctx (applied, stmts)) // 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 @@ -1141,9 +1158,10 @@ 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) @@ -1151,7 +1169,7 @@ let transformCallAsStatements com ctx range t returnStrategy callee callInfo = wrapInOptionErase com ctx expr else expr - | _ -> expr + | None -> expr stmts @ (expr |> resolveExpr ctx t returnStrategy) @@ -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) @@ -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 diff --git a/src/Fable.Transforms/Python/Fable2Python.Util.fs b/src/Fable.Transforms/Python/Fable2Python.Util.fs index 203140de9..9f0ee0ee1 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Util.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Util.fs @@ -100,6 +100,12 @@ module Util = || hasAttribute Atts.emitIndexer atts || hasAttribute Atts.emitProperty atts + /// Check if a type contains any generic parameters (recursively). + /// Used to determine if Option 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) = @@ -107,19 +113,34 @@ module Util = | 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 (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. @@ -127,7 +148,8 @@ module Util = /// 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).