From ec84204eb208d86e86944da030785025e4549c90 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 11 Jan 2026 10:47:28 +0100 Subject: [PATCH 1/2] [Python] Type annotation fixes --- .../Python/Fable2Python.Annotation.fs | 17 +++- .../Python/Fable2Python.Transforms.fs | 96 ++++++++++++++++--- src/Fable.Transforms/Python/Replacements.fs | 17 +++- 3 files changed, 105 insertions(+), 25 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.Annotation.fs b/src/Fable.Transforms/Python/Fable2Python.Annotation.fs index ee2c37f09..c356fe65d 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Annotation.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Annotation.fs @@ -12,12 +12,18 @@ open Fable.Transforms.Python.AST open Fable.Transforms.Python.Types open Fable.Transforms.Python.Util -/// Check if type is an inref (in-reference) or Any type. +/// Check if type is an inref wrapping a struct/value type. /// In F#, struct instance method's `this` parameter is represented as inref, /// but in Python the struct is passed directly, not wrapped in FSharpRef. -let isInRefOrAnyType (com: IPythonCompiler) = +/// For regular inref parameters (where T is a primitive or non-struct), we keep FSharpRef. +let isStructInRefType (com: IPythonCompiler) = function - | Replacements.Util.IsInRefType com _ -> true + | Replacements.Util.IsInRefType com innerType -> + match innerType with + | Fable.DeclaredType(entRef, _) -> + let ent = com.GetEntity(entRef) + ent.IsValueType + | _ -> false | Fable.Any -> true | _ -> false @@ -698,9 +704,10 @@ let makeBuiltinTypeAnnotation com ctx typ repeatedGenerics kind = match kind with | Replacements.Util.BclGuid -> stdlibModuleTypeHint com ctx "uuid" "UUID" [] repeatedGenerics | Replacements.Util.FSharpReference genArg -> - // In F#, struct instance method's `this` parameter is represented as inref, + // For struct instance methods, `this` is represented as inref in F#, // but in Python the struct is passed directly, not wrapped in FSharpRef. - if isInRefOrAnyType com typ then + // For regular byref/inref/outref parameters, we use FSharpRef. + if isStructInRefType com typ then typeAnnotation com ctx repeatedGenerics genArg else let resolved, stmts = resolveGenerics com ctx [ genArg ] repeatedGenerics diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 66e5a0c11..ea6fffa33 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -73,7 +73,15 @@ let transformImport (com: IPythonCompiler) ctx (_r: SourceLocation option) (name com.GetImportExpr(ctx, moduleName, name) |> getParts com ctx parts -let getMemberArgsAndBody (com: IPythonCompiler) ctx kind hasSpread (args: Fable.Ident list) (body: Fable.Expr) = +let getMemberArgsAndBodyCore + (com: IPythonCompiler) + ctx + kind + hasSpread + (args: Fable.Ident list) + (body: Fable.Expr) + (selfName: string) + = // printfn "getMemberArgsAndBody: %A" hasSpread let funcName, genTypeParams, args, body = match kind, args with @@ -82,9 +90,8 @@ let getMemberArgsAndBody (com: IPythonCompiler) ctx kind hasSpread (args: Fable. Set.difference (Annotation.getGenericTypeParams [ thisArg.Type ]) ctx.ScopedTypeParams let body = - // TODO: If ident is not captured maybe we can just replace it with "this" if isIdentUsed thisArg.Name body then - let thisKeyword = Fable.IdentExpr { thisArg with Name = "self" } + let thisKeyword = Fable.IdentExpr { thisArg with Name = selfName } Fable.Let(thisArg, thisKeyword, body) else @@ -106,6 +113,9 @@ let getMemberArgsAndBody (com: IPythonCompiler) ctx kind hasSpread (args: Fable. args, body, returnType +let getMemberArgsAndBody (com: IPythonCompiler) ctx kind hasSpread (args: Fable.Ident list) (body: Fable.Expr) = + getMemberArgsAndBodyCore com ctx kind hasSpread args body "self" + let getUnionCaseName (uci: Fable.UnionCase) = match uci.CompiledName with | Some cname -> cname @@ -365,6 +375,7 @@ let transformCast (com: IPythonCompiler) (ctx: Context) t e : Expression * State // Wrap ResizeArray (Python list) when cast to IEnumerable // Python lists don't implement IEnumerable_1, so they need wrapping + // Optimization: If ResizeArray was created via of_seq from IEnumerable, use the original arg | Types.ienumerableGeneric, _ when match e.Type with | Fable.Array(_, Fable.ArrayKind.ResizeArray) -> true @@ -372,7 +383,15 @@ let transformCast (com: IPythonCompiler) (ctx: Context) t e : Expression * State | _ -> false -> let listExpr, stmts = com.TransformAsExpr(ctx, e) - libCall com ctx None "util" "to_enumerable" [ listExpr ], stmts + // Check if the expression is of_seq(arg) - if so, use arg directly (already IEnumerable) + match listExpr with + | Expression.Call { + Func = Expression.Name { Id = Identifier "of_seq" } + Args = [ innerArg ] + } -> + // Skip both of_seq and to_enumerable - use original IEnumerable directly + innerArg, stmts + | _ -> libCall com ctx None "util" "to_enumerable" [ listExpr ], stmts | _ -> com.TransformAsExpr(ctx, e) | Fable.Number(Float32, _), _ -> @@ -601,7 +620,59 @@ let transformObjectExpr // A generic class nested in another generic class cannot use same type variables. (PEP-484) let ctx = { ctx with TypeParamsScope = ctx.TypeParamsScope + 1 } + // Check if any member body uses ThisValue from an outer scope (e.g., inside a constructor). + // ThisValue specifically represents `self` in a constructor/method context. + // Note: IsThisArgument identifiers are captured via default arguments (x: Any=x), + // so we only need to handle explicit ThisValue here. + let usesOuterThis = + members + |> List.exists (fun memb -> + memb.Body + |> deepExists ( + function + | Fable.Value(Fable.ThisValue _, _) -> true + | _ -> false + ) + ) + + // Only generate capture statement if outer this is actually used. + // This allows inner class methods to reference the outer instance via "_this" + // while using standard "self" for the inner instance (satisfies Pylance). + let thisCaptureStmts = + if usesOuterThis then + let anyType = stdlibModuleAnnotation com ctx "typing" "Any" [] + + [ + Statement.assign (Expression.name "_this", anyType, value = Expression.name "self") + ] + else + [] + + // Replace ThisValue in the body with an identifier reference to "_this" + // This ensures that outer self references correctly bind to the captured variable + let replaceThisValue (body: Fable.Expr) = + if usesOuterThis then + body + |> visitFromInsideOut ( + function + | Fable.Value(Fable.ThisValue typ, r) -> + Fable.IdentExpr + { + Name = "_this" + Type = typ + IsMutable = false + IsThisArgument = false + IsCompilerGenerated = true + Range = r + } + | e -> e + ) + else + body + let makeMethod prop hasSpread (fableArgs: Fable.Ident list) (fableBody: Fable.Expr) decorators = + let fableBody = replaceThisValue fableBody + let args, body, returnType = getMemberArgsAndBody com ctx (Attached(isStatic = false)) hasSpread fableArgs fableBody @@ -614,16 +685,7 @@ let transformObjectExpr com.GetIdentifier(ctx, Naming.toPythonNaming name) let self = Arg.arg "self" - - let args = - match decorators with - // Remove extra parameters from getters, i.e __unit=None - | [ Expression.Name { Id = Identifier "property" } ] -> - { args with - Args = [ self ] - Defaults = [] - } - | _ -> { args with Args = self :: args.Args } + let args = { args with Args = self :: args.Args } // Calculate type parameters for generic object expression methods let argTypes = fableArgs |> List.map _.Type @@ -666,12 +728,16 @@ let transformObjectExpr | Fable.Lambda(arg, body, _) -> [ arg ], body | _ -> memb.Args, memb.Body + // Replace ThisValue with this_ identifier for outer self references + let body = replaceThisValue body + let args', body', returnType = getMemberArgsAndBody com ctx (NonAttached memb.Name) false args body let name = com.GetIdentifier(ctx, Naming.toPythonNaming memb.Name) let self = Arg.arg "self" let args' = { args' with Args = self :: args'.Args } + let argTypes = args |> List.map _.Type let typeParams = Annotation.calculateMethodTypeParams com ctx argTypes body.Type @@ -716,7 +782,7 @@ let transformObjectExpr let stmt = Statement.classDef (name, body = classBody, bases = interfaces) - Expression.call (Expression.name name), [ stmt ] @ stmts + Expression.call (Expression.name name), thisCaptureStmts @ [ stmt ] @ stmts let transformCallArgs diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 6251895ac..5260d9ce3 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -1728,11 +1728,18 @@ let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (this | ".ctor", _, [] -> makeResizeArray (getElementType t) [] |> Some | ".ctor", _, [ ExprType(Number _) ] -> makeResizeArray (getElementType t) [] |> Some | ".ctor", _, [ ArrayOrListLiteral(vals, _) ] -> makeResizeArray (getElementType t) vals |> Some - // Use Array constructor which accepts both Iterable and IEnumerable_1 - | ".ctor", _, args -> - Helper.LibCall(com, "array", "Array", t, args, ?loc = r) - |> withTag "array" - |> Some + // When a ResizeArray is cast to IEnumerable and passed to ResizeArray constructor, + // unwrap the cast since list() can handle lists directly (avoids to_enumerable wrapper) + | ".ctor", _, [ TypeCast(innerExpr, DeclaredType(ent, _)) ] when + ent.FullName = Types.ienumerableGeneric + && match innerExpr.Type with + | Array(_, ResizeArray) -> true + | DeclaredType(entRef, _) when entRef.FullName = Types.resizeArray -> true + | _ -> false + -> + Helper.GlobalCall("list", t, [ innerExpr ], ?loc = r) |> Some + // Use resize_array.of_seq to create a list from IEnumerable_1 or any iterable + | ".ctor", _, args -> Helper.LibCall(com, "resize_array", "of_seq", t, args, ?loc = r) |> Some | "get_Item", Some ar, [ idx ] -> getExpr r t ar idx |> Some | "set_Item", Some ar, [ idx; value ] -> setExpr r ar idx value |> Some | "Add", Some ar, [ arg ] -> From 44717e6e54adcc8259fa469a41caf2211b72245a Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 11 Jan 2026 13:45:21 +0100 Subject: [PATCH 2/2] Add missing file + cleanup --- .../Python/Fable2Python.Annotation.fs | 1 - .../Python/Fable2Python.Transforms.fs | 15 ++------------- .../fable_library/resize_array.py | 6 ++++++ 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.Annotation.fs b/src/Fable.Transforms/Python/Fable2Python.Annotation.fs index c356fe65d..23cffb31d 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Annotation.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Annotation.fs @@ -24,7 +24,6 @@ let isStructInRefType (com: IPythonCompiler) = let ent = com.GetEntity(entRef) ent.IsValueType | _ -> false - | Fable.Any -> true | _ -> false let tryPyConstructor (com: IPythonCompiler) ctx ent = diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index ea6fffa33..2ffae0232 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -73,15 +73,7 @@ let transformImport (com: IPythonCompiler) ctx (_r: SourceLocation option) (name com.GetImportExpr(ctx, moduleName, name) |> getParts com ctx parts -let getMemberArgsAndBodyCore - (com: IPythonCompiler) - ctx - kind - hasSpread - (args: Fable.Ident list) - (body: Fable.Expr) - (selfName: string) - = +let getMemberArgsAndBody (com: IPythonCompiler) ctx kind hasSpread (args: Fable.Ident list) (body: Fable.Expr) = // printfn "getMemberArgsAndBody: %A" hasSpread let funcName, genTypeParams, args, body = match kind, args with @@ -91,7 +83,7 @@ let getMemberArgsAndBodyCore let body = if isIdentUsed thisArg.Name body then - let thisKeyword = Fable.IdentExpr { thisArg with Name = selfName } + let thisKeyword = Fable.IdentExpr { thisArg with Name = "self" } Fable.Let(thisArg, thisKeyword, body) else @@ -113,9 +105,6 @@ let getMemberArgsAndBodyCore args, body, returnType -let getMemberArgsAndBody (com: IPythonCompiler) ctx kind hasSpread (args: Fable.Ident list) (body: Fable.Expr) = - getMemberArgsAndBodyCore com ctx kind hasSpread args body "self" - let getUnionCaseName (uci: Fable.UnionCase) = match uci.CompiledName with | Some cname -> cname diff --git a/src/fable-library-py/fable_library/resize_array.py b/src/fable-library-py/fable_library/resize_array.py index c906c596f..9e4eef52e 100644 --- a/src/fable-library-py/fable_library/resize_array.py +++ b/src/fable-library-py/fable_library/resize_array.py @@ -9,6 +9,11 @@ from .util import to_iterable +def of_seq[T](items: Iterable[T] | IEnumerable_1[T]) -> list[T]: + """Create a ResizeArray (Python list) from an iterable or IEnumerable_1.""" + return list(to_iterable(items)) + + def exists[T](predicate: Callable[[T], bool], xs: list[T]) -> bool: """Test if a predicate is true for at least one element in a list.""" return any(predicate(x) for x in xs) @@ -146,6 +151,7 @@ def contains[T](value: T, xs: list[T], cons: Any | None = None) -> bool: "index_of", "insert_range_in_place", "iterate", + "of_seq", "remove", "remove_all_in_place", "remove_range",