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
18 changes: 12 additions & 6 deletions src/Fable.Transforms/Python/Fable2Python.Annotation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +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<StructType>,
/// but in Python the struct is passed directly, not wrapped in FSharpRef.
let isInRefOrAnyType (com: IPythonCompiler) =
/// For regular inref<T> parameters (where T is a primitive or non-struct), we keep FSharpRef.
let isStructInRefType (com: IPythonCompiler) =
function
| Replacements.Util.IsInRefType com _ -> true
| Fable.Any -> true
| Replacements.Util.IsInRefType com innerType ->
match innerType with
| Fable.DeclaredType(entRef, _) ->
let ent = com.GetEntity(entRef)
ent.IsValueType
| _ -> false
| _ -> false

let tryPyConstructor (com: IPythonCompiler) ctx ent =
Expand Down Expand Up @@ -698,9 +703,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<StructType>,
// For struct instance methods, `this` is represented as inref<StructType> 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
Expand Down
81 changes: 68 additions & 13 deletions src/Fable.Transforms/Python/Fable2Python.Transforms.fs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ 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" }

Expand Down Expand Up @@ -365,14 +364,23 @@ 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
| Fable.DeclaredType(entRef, _) when entRef.FullName = Types.resizeArray -> true
| _ -> 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, _), _ ->
Expand Down Expand Up @@ -601,7 +609,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

Expand All @@ -614,16 +674,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
Expand Down Expand Up @@ -666,12 +717,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

Expand Down Expand Up @@ -716,7 +771,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
Expand Down
17 changes: 12 additions & 5 deletions src/Fable.Transforms/Python/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ] ->
Expand Down
6 changes: 6 additions & 0 deletions src/fable-library-py/fable_library/resize_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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",
Expand Down
Loading