diff --git a/pyrightconfig.ci.json b/pyrightconfig.ci.json index 6d9720c74..7ac204ff4 100644 --- a/pyrightconfig.ci.json +++ b/pyrightconfig.ci.json @@ -5,7 +5,6 @@ "**/node_modules/**", "temp/fable-library-py/fable_library/list.py", "temp/tests/Python/test_applicative.py", - "temp/tests/Python/test_map.py", "temp/tests/Python/test_misc.py", "temp/tests/Python/test_type.py", "temp/tests/Python/fable_modules/thoth_json_python/encode.py" diff --git a/src/Fable.Build/Test/Python.fs b/src/Fable.Build/Test/Python.fs index 74a202009..83f40737d 100644 --- a/src/Fable.Build/Test/Python.fs +++ b/src/Fable.Build/Test/Python.fs @@ -67,7 +67,10 @@ let handle (args: string list) = else // Test against .NET - Command.Run("dotnet", "test -c Release", workingDirectory = sourceDir) + if compileOnly then + printfn "Skipping .NET test execution (--compile-only specified)" + else + Command.Run("dotnet", "test -c Release", workingDirectory = sourceDir) // Test against Python Command.Fable(fableArgs, workingDirectory = buildDir) @@ -80,7 +83,7 @@ let handle (args: string list) = // Run pytest if compileOnly then - printfn "Skipping test execution (--compile-only specified)" + printfn "Skipping Python test execution (--compile-only specified)" else Command.Run("uv", $"run pytest {buildDir} -x") diff --git a/src/Fable.Transforms/FableTransforms.fs b/src/Fable.Transforms/FableTransforms.fs index f4fb69285..0504a5757 100644 --- a/src/Fable.Transforms/FableTransforms.fs +++ b/src/Fable.Transforms/FableTransforms.fs @@ -228,10 +228,17 @@ let noSideEffectBeforeIdent identName expr = findIdentOrSideEffect expr && not sideEffect -let canInlineArg com identName value body = +let canInlineArg (com: Compiler) identName value body = match value with | Value((Null _ | UnitConstant | TypeInfo _ | BoolConstant _ | NumberConstant _ | CharConstant _), _) -> true - | Value(StringConstant s, _) -> s.Length < 100 + | Value(StringConstant s, _) -> + match com.Options.Language with + | Python -> + // Only inline short strings if they're referenced at most once, + // to avoid duplicating the literal in generated code (which can cause + // issues like property access on string literals in Python) + s.Length < 100 && countReferencesUntil 2 identName body <= 1 + | _ -> s.Length < 100 | _ -> let refCount = countReferencesUntil 2 identName body diff --git a/src/Fable.Transforms/FableTransforms.fsi b/src/Fable.Transforms/FableTransforms.fsi index 33b7051bf..cdedc2b27 100644 --- a/src/Fable.Transforms/FableTransforms.fsi +++ b/src/Fable.Transforms/FableTransforms.fsi @@ -5,6 +5,7 @@ open Fable.AST.Fable val isIdentCaptured: identName: string -> expr: Expr -> bool val isTailRecursive: identName: string -> expr: Expr -> bool * bool +val countReferencesUntil: limit: int -> identName: string -> body: Expr -> int val replaceValues: replacements: Map -> expr: Expr -> Expr val uncurryType: typ: Type -> Type diff --git a/src/Fable.Transforms/Python/Fable2Python.Annotation.fs b/src/Fable.Transforms/Python/Fable2Python.Annotation.fs index c50e73b0c..e01b3fdd9 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Annotation.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Annotation.fs @@ -543,7 +543,7 @@ let makeEntityTypeAnnotation com ctx (entRef: Fable.EntityRef) genArgs repeatedG | Types.iobservableGeneric, _ -> let resolved, stmts = resolveGenerics com ctx genArgs repeatedGenerics fableModuleAnnotation com ctx "observable" "IObservable" resolved, stmts - | Types.idictionary, _ -> stdlibModuleTypeHint com ctx "collections.abc" "MutableMapping" genArgs repeatedGenerics + | Types.idictionary, _ -> stdlibModuleTypeHint com ctx "collections.abc" "Mapping" genArgs repeatedGenerics | Types.ievent2, _ -> // IEvent<'Delegate, 'Args> - only use Args (second param) since Delegate is phantom in Python let argsType = genArgs |> List.tryItem 1 |> Option.defaultValue Fable.Any @@ -559,7 +559,7 @@ let makeEntityTypeAnnotation com ctx (entRef: Fable.EntityRef) genArgs repeatedG | "Fable.Core.Py.Set`1", _ -> let resolved, stmts = resolveGenerics com ctx genArgs repeatedGenerics fableModuleAnnotation com ctx "protocols" "ISet_1" resolved, stmts - | "Fable.Core.Py.Map`2", _ -> + | "Py.Mapping.IMapping`2", _ -> let resolved, stmts = resolveGenerics com ctx genArgs repeatedGenerics fableModuleAnnotation com ctx "protocols" "IMap" resolved, stmts | "Fable.Core.Py.Callable", _ -> @@ -581,7 +581,16 @@ let makeEntityTypeAnnotation com ctx (entRef: Fable.EntityRef) genArgs repeatedG let isErased = ent.Attributes |> Seq.exists (fun att -> att.Entity.FullName = Atts.erase) - if ent.IsInterface && not isErased then + // Check for [] attribute - use the global name directly as the type annotation + match com, ent.Attributes with + | FSharp2Fable.Util.GlobalAtt(Some customName) -> + // Use the custom global name (e.g., "list" for []) + makeGenericTypeAnnotation com ctx customName genArgs repeatedGenerics, [] + | FSharp2Fable.Util.GlobalAtt None -> + // Use the entity's display name + let name = Helpers.removeNamespace ent.FullName + makeGenericTypeAnnotation com ctx name genArgs repeatedGenerics, [] + | _ when ent.IsInterface && not isErased -> let name = Helpers.removeNamespace ent.FullName // If the interface is imported then it's erased and we need to add the actual imports @@ -597,10 +606,10 @@ let makeEntityTypeAnnotation com ctx (entRef: Fable.EntityRef) genArgs repeatedG | _ -> () makeGenericTypeAnnotation com ctx name genArgs repeatedGenerics, [] - elif isErased then + | _ when isErased -> // Erased types should use Any for type annotations stdlibModuleTypeHint com ctx "typing" "Any" [] repeatedGenerics - else + | _ -> match tryPyConstructor com ctx ent with | Some(entRef, stmts) -> match entRef with diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 4491d0da8..d4af3bebc 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -69,16 +69,7 @@ let getMemberArgsAndBody (com: IPythonCompiler) ctx kind hasSpread (args: Fable. let args, body, returnType, _typeParams = Annotation.transformFunctionWithAnnotations com ctx funcName args body - let args = - let len = args.Args.Length - - if not hasSpread || len = 0 then - args - else - { args with - VarArg = Some { args.Args[len - 1] with Annotation = None } - Args = args.Args[.. len - 2] - } + let args = adjustArgsForSpread hasSpread args args, body, returnType @@ -740,6 +731,16 @@ let transformCallArgs let hasSpread = paramsInfo |> Option.map (fun i -> i.HasSpread) |> Option.defaultValue false + // Helper to transform an arg and wrap with widen() if needed + let transformArgWithWiden (sigType: Fable.Type option) (argExpr: Fable.Expr) = + let expr, stmts = com.TransformAsExpr(ctx, argExpr) + + if needsOptionWidenForArg sigType argExpr then + let widen = com.TransformImport(ctx, "widen", getLibPath com "option") + Expression.call (widen, [ expr ]), stmts + else + expr, stmts + let args, stmts' = match args with | [] -> [], [] @@ -759,7 +760,14 @@ let transformCallArgs let expr, stmts' = com.TransformAsExpr(ctx, last) rest @ [ Expression.starred expr ], stmts @ stmts' - | args -> List.map (fun e -> com.TransformAsExpr(ctx, e)) args |> Helpers.unzipArgs + | args -> + // Transform args with widen() where needed based on signature types + args + |> List.mapi (fun i e -> + let sigType = List.tryItem i callInfo.SignatureArgTypes + transformArgWithWiden sigType e + ) + |> Helpers.unzipArgs match objArg with | None -> args, [], stmts @ stmts' @@ -1515,7 +1523,12 @@ let getDecisionTargetAndBoundValues (com: IPythonCompiler) (ctx: Context) target let bindings, replacements = (([], Map.empty), identsAndValues) ||> List.fold (fun (bindings, replacements) (ident, expr) -> - if canHaveSideEffects com expr then + // Only inline if the expression has no side effects AND is referenced at most once. + // If referenced multiple times, we should bind to a variable to avoid duplicating + // the expression (which can cause issues like accessing properties on literals). + let refCount = FableTransforms.countReferencesUntil 2 ident.Name target + + if canHaveSideEffects com expr || refCount > 1 then (ident, expr) :: bindings, replacements else bindings, Map.add ident.Name expr replacements @@ -4129,23 +4142,42 @@ let transformInterface (com: IPythonCompiler) ctx (classEnt: Fable.Entity) (_cla // Make protocol method parameters positional-only (using /) to avoid // parameter name mismatch errors when subclasses use different names // (e.g., value_1 instead of value due to closure captures) + let allParams = + memb.CurriedParameterGroups + |> Seq.indexed + |> Seq.collect (fun (n, parameterGroup) -> + parameterGroup |> Seq.indexed |> Seq.map (fun (m, pg) -> (n + m, pg)) + ) + |> Seq.toList + + // Split regular params from vararg param using shared helper + let regularParams, varArgParam = splitVarArg memb.HasSpread allParams + let posOnlyArgs = [ if memb.IsInstance then Arg.arg "self" - for n, parameterGroup in memb.CurriedParameterGroups |> Seq.indexed do - for m, pg in parameterGroup |> Seq.indexed do - // Uncurry function types to match class implementations - // F# interface uses curried types (LambdaType) but class methods - // use uncurried types (DelegateType) at runtime - let paramType = FableTransforms.uncurryType pg.Type - let ta, _ = Annotation.typeAnnotation com ctx None paramType + for idx, pg in regularParams do + // Uncurry function types to match class implementations + // F# interface uses curried types (LambdaType) but class methods + // use uncurried types (DelegateType) at runtime + let paramType = FableTransforms.uncurryType pg.Type + let ta, _ = Annotation.typeAnnotation com ctx None paramType - Arg.arg (pg.Name |> Option.defaultValue $"__arg%d{n + m}", annotation = ta) + Arg.arg (pg.Name |> Option.defaultValue $"__arg%d{idx}", annotation = ta) ] - Arguments.arguments (posonlyargs = posOnlyArgs) + // For vararg parameter, extract element type from array type for annotation + let vararg = + varArgParam + |> Option.map (fun (_idx, pg) -> + let elementType = getVarArgElementType pg.Type + let ta, _ = Annotation.typeAnnotation com ctx None elementType + Arg.arg (pg.Name |> Option.defaultValue "rest", annotation = ta) + ) + + Arguments.arguments (posonlyargs = posOnlyArgs, ?vararg = vararg) // Also uncurry return type for consistency with class implementations let uncurriedReturnType = FableTransforms.uncurryType memb.ReturnParameter.Type diff --git a/src/Fable.Transforms/Python/Fable2Python.Util.fs b/src/Fable.Transforms/Python/Fable2Python.Util.fs index f73044c80..79233903f 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Util.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Util.fs @@ -39,6 +39,36 @@ module Util = | [] -> [ Statement.Pass ] | _ -> stmts + /// Extract the element type from an array type for vararg annotations. + /// For Array, returns T. For other types, returns Any as fallback. + let getVarArgElementType (paramType: Fable.Type) : Fable.Type = + match paramType with + | Fable.Array(elemType, _) -> elemType + | _ -> Fable.Any // Fallback if not array type + + /// Splits a list of items into regular items and an optional vararg item. + /// When hasSpread is true and items is non-empty, the last item becomes the vararg. + let splitVarArg<'T> (hasSpread: bool) (items: 'T list) : 'T list * 'T option = + if hasSpread && not items.IsEmpty then + let regular = items |> List.take (items.Length - 1) + let vararg = items |> List.last + regular, Some vararg + else + items, None + + /// Adjusts Arguments to move the last arg to vararg when hasSpread is true. + /// Removes the annotation from vararg since Python infers it from *args. + let adjustArgsForSpread (hasSpread: bool) (args: Arguments) : Arguments = + let len = args.Args.Length + + if not hasSpread || len = 0 then + args + else + { args with + VarArg = Some { args.Args[len - 1] with Annotation = None } + Args = args.Args[.. len - 2] + } + let hasAttribute fullName (atts: Fable.Attribute seq) = atts |> Seq.exists (fun att -> att.Entity.FullName = fullName) @@ -79,6 +109,55 @@ module Util = | Fable.Call _ -> needsOptionEraseForCall expectedReturnType | _ -> false + /// Recursively check if a type contains Option with generic parameter that requires wrapping. + /// This checks return types of lambdas for Option[GenericParam]. + let rec private hasWrappedOptionInReturnType (typ: Fable.Type) = + match typ with + | Fable.LambdaType(_, returnType) -> + // Check if return type is Option[GenericParam] or recurse into nested lambdas + match returnType with + | Fable.Option(inner, _) -> mustWrapOption inner + | Fable.LambdaType _ -> hasWrappedOptionInReturnType returnType + | _ -> false + | _ -> false + + /// Check if a type would have its Option return type erased (T | None instead of Option[T]). + /// Returns true when the return type is Option[ConcreteType] which gets erased. + let rec private hasErasedOptionReturnType (typ: Fable.Type) = + match typ with + | Fable.LambdaType(_, returnType) -> + match returnType with + | Fable.Option(inner, _) -> not (mustWrapOption inner) // Erased when NOT wrapped + | Fable.LambdaType _ -> hasErasedOptionReturnType returnType + | _ -> false + | _ -> false + + /// Check if a callback argument needs widen() to convert erased Option callback + /// to wrapped Option form for type checker compatibility. + /// Returns true when: + /// - Expected type is Callable with Option[GenericParam] in return position + /// - Actual arg is a lambda/function that would have erased Option return type + let needsOptionWidenForArg (expectedType: Fable.Type option) (argExpr: Fable.Expr) = + match expectedType with + | Some sigType when hasWrappedOptionInReturnType sigType -> + // Check if argument is a callable with ERASED Option return type + // If arg already has wrapped Option return, don't apply widen() + match argExpr with + | Fable.Lambda _ + | Fable.Delegate _ -> + // Check if the lambda's return type would be erased + hasErasedOptionReturnType argExpr.Type + | Fable.IdentExpr ident -> + // Check if the identifier's type has erased Option return + hasErasedOptionReturnType ident.Type + | Fable.Get(_, Fable.FieldGet fieldInfo, _, _) -> + // Field access to a function + match fieldInfo.FieldType with + | Some typ -> hasErasedOptionReturnType typ + | None -> false + | _ -> false + | _ -> false + /// Wraps None values in cast(type, None) for type safety. /// Skips if type annotation is also None (unit type). let wrapNoneInCast (com: IPythonCompiler) ctx (value: Expression) (typeAnnotation: Expression) : Expression = diff --git a/src/Fable.Transforms/Python/PythonPrinter.fs b/src/Fable.Transforms/Python/PythonPrinter.fs index 39b4eae45..d4270cf90 100644 --- a/src/Fable.Transforms/Python/PythonPrinter.fs +++ b/src/Fable.Transforms/Python/PythonPrinter.fs @@ -143,11 +143,13 @@ module PrinterExtensions = if i < args.Length - 1 then printer.Print(", ") - match arguments.Args, arguments.VarArg with - | [], Some vararg -> + match arguments.PosOnlyArgs, arguments.Args, arguments.VarArg with + | [], [], Some vararg -> + // No positional args at all, just print *vararg printer.Print("*") printer.Print(vararg) - | _, Some vararg -> + | _, _, Some vararg -> + // Has positional-only or regular args, need comma before *vararg printer.Print(", *") printer.Print(vararg) | _ -> () diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 7e6168a35..e04c1165f 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2590,12 +2590,21 @@ let dictionaries (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp | "get_Item", _ -> Helper.LibCall(com, "map_util", "getItemFromDict", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some - | ReplaceName [ "set_Item", "set" - "get_Keys", "keys" - "get_Values", "values" - "ContainsKey", "has" - "Clear", "clear" ] methName, - Some c -> Helper.InstanceCall(c, methName, t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "get_Keys", Some c -> + // Wrap .keys() with to_enumerable since KeysView doesn't implement IEnumerable_1 + let keysCall = + Helper.InstanceCall(c, "keys", t, args, i.SignatureArgTypes, ?loc = r) + + Helper.LibCall(com, "util", "to_enumerable", t, [ keysCall ], ?loc = r) |> Some + | "get_Values", Some c -> + // Wrap .values() with to_enumerable since ValuesView doesn't implement IEnumerable_1 + let valuesCall = + Helper.InstanceCall(c, "values", t, args, i.SignatureArgTypes, ?loc = r) + + Helper.LibCall(com, "util", "to_enumerable", t, [ valuesCall ], ?loc = r) + |> Some + | ReplaceName [ "set_Item", "set"; "ContainsKey", "has"; "Clear", "clear" ] methName, Some c -> + Helper.InstanceCall(c, methName, t, args, i.SignatureArgTypes, ?loc = r) |> Some | _ -> None let hashSets (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = diff --git a/src/fable-library-py/fable_library/core/array.pyi b/src/fable-library-py/fable_library/core/array.pyi index 41fc97adb..26037725d 100644 --- a/src/fable-library-py/fable_library/core/array.pyi +++ b/src/fable-library-py/fable_library/core/array.pyi @@ -9,6 +9,7 @@ from __future__ import annotations from collections.abc import Callable, Iterable, Iterator, MutableSequence from typing import Any, ClassVar, Literal, SupportsInt, overload +from fable_library.option import Option from fable_library.protocols import ( IComparer_1, IEnumerable_1, @@ -75,7 +76,7 @@ class FSharpArray[T](MutableSequence[T]): def append(self, array2: FSharpArray[T], cons: FSharpCons[T] | None = None) -> FSharpArray[T]: ... # type: ignore[override] def average(self, averager: IGenericAverager[T]) -> T: ... def average_by[U](self, projection: Callable[[T], U], averager: IGenericAverager[U]) -> U: ... - def choose[U](self, chooser: Callable[[T], U | None], cons: FSharpCons[U] | None = None) -> FSharpArray[U]: ... + def choose[U](self, chooser: Callable[[T], Option[U]], cons: FSharpCons[U] | None = None) -> FSharpArray[U]: ... def chunk_by_size(self, size: SupportsInt) -> list[FSharpArray[T]]: ... def collect[U](self, mapping: Callable[[T], FSharpArray[U]], cons: Any | None = None) -> FSharpArray[U]: ... def compare_to(self, comparer: Callable[[T, T], SupportsInt], other: FSharpArray[T]) -> Int32: ... @@ -172,7 +173,7 @@ class FSharpArray[T](MutableSequence[T]): self, f: Callable[[T], bool], cons: FSharpCons[T] | None = None ) -> tuple[FSharpArray[T], FSharpArray[T]]: ... def permute(self, f: Callable[[Int32], SupportsInt], array: FSharpArray[T]) -> FSharpArray[T]: ... - def pick[U](self, chooser: Callable[[T], U | None]) -> U: ... + def pick[U](self, chooser: Callable[[T], Option[U]]) -> U: ... def reduce(self, folder: Callable[[Any, Any], Any], state: Any) -> Any: ... def reduce_back(self, reduction: Callable[[Any, Any], Any], state: Any) -> Any: ... def remove_all_in_place(self, predicate: Callable[[T], bool]) -> SupportsInt: ... @@ -209,7 +210,7 @@ class FSharpArray[T](MutableSequence[T]): def try_head(self) -> T | None: ... def try_item(self, index: SupportsInt) -> T | None: ... def try_last(self) -> T | None: ... - def try_pick[U](self, chooser: Callable[[T], U | None]) -> U | None: ... + def try_pick[U](self, chooser: Callable[[T], Option[U]]) -> U | None: ... def unzip[U](self: FSharpArray[tuple[T, U]]) -> tuple[FSharpArray[T], FSharpArray[U]]: ... def update_at(self, index: SupportsInt, value: T, cons: Any | None = None) -> FSharpArray[T]: ... def windowed(self, window_size: SupportsInt) -> FSharpArray[FSharpArray[T]]: ... @@ -224,7 +225,7 @@ def append[T](array1: Elements[T], array2: Elements[T], cons: FSharpCons[T] | No def average[T](array: Elements[T], averager: IGenericAverager[T]) -> T: ... def average_by[T, U](projection: Callable[[T], U], array: Elements[T], averager: IGenericAverager[U]) -> U: ... def choose[T, U]( - chooser: Callable[[T], U | None], array: Elements[T], cons: FSharpCons[U] | None = None + chooser: Callable[[T], Option[U]], array: Elements[T], cons: FSharpCons[U] | None = None ) -> FSharpArray[U]: ... def chunk_by_size[T](chunk_size: SupportsInt, array: Elements[T]) -> FSharpArray[FSharpArray[T]]: ... def collect[T, U]( @@ -365,7 +366,7 @@ def partition[T]( f: Callable[[T], bool], array: Elements[T], cons: FSharpCons[T] | None = None ) -> tuple[FSharpArray[T], FSharpArray[T]]: ... def permute[T](f: Callable[[Int32], SupportsInt], array: Elements[T]) -> FSharpArray[T]: ... -def pick[T, U](chooser: Callable[[T], U | None], array: Elements[T]) -> U: ... +def pick[T, U](chooser: Callable[[T], Option[U]], array: Elements[T]) -> U: ... def reduce[T](reduction: Callable[[T, T], T], array: Elements[T]) -> T: ... def reduce_back[T](reduction: Callable[[T, T], T], array: Elements[T]) -> T: ... def remove_all_in_place[T](array: FSharpArray[T], predicate: Callable[[T], bool]) -> SupportsInt: ... @@ -415,7 +416,7 @@ def try_find_index_back[T](predicate: Callable[[T], bool], array: Elements[T]) - def try_head[T](array: Elements[T]) -> T | None: ... def try_item[T](index: SupportsInt, array: Elements[T]) -> T | None: ... def try_last[T](array: Elements[T]) -> T | None: ... -def try_pick[T, U](chooser: Callable[[T], U | None], array: Elements[T]) -> U | None: ... +def try_pick[T, U](chooser: Callable[[T], Option[U]], array: Elements[T]) -> U | None: ... def unzip[T, U](array: Elements[tuple[T, U]]) -> tuple[FSharpArray[T], FSharpArray[U]]: ... def update_at[T]( index: SupportsInt, value: T, array: Elements[T], cons: FSharpCons[T] | None = None diff --git a/src/fable-library-py/fable_library/observable.py b/src/fable-library-py/fable_library/observable.py index a4f002083..0488c534e 100644 --- a/src/fable-library-py/fable_library/observable.py +++ b/src/fable-library-py/fable_library/observable.py @@ -7,7 +7,7 @@ Choice_tryValueIfChoice2Of2, FSharpChoice_2, ) -from .option import value +from .option import Option, value from .protocols import IDisposable from .util import UNIT, Disposable @@ -89,10 +89,10 @@ def protect[T]( fail(e) -def choose[T, U](chooser: Callable[[T], U | None], source: IObservable[T]) -> IObservable[U]: +def choose[T, U](chooser: Callable[[T], Option[U]], source: IObservable[T]) -> IObservable[U]: def subscribe(observer: IObserver[U]): def on_next(t: T) -> None: - def success(u: U | None) -> None: + def success(u: Option[U]) -> None: if u is not None: observer.OnNext(value(u)) diff --git a/src/fable-library-py/fable_library/option.py b/src/fable-library-py/fable_library/option.py index fc266d833..cda0cc527 100644 --- a/src/fable-library-py/fable_library/option.py +++ b/src/fable-library-py/fable_library/option.py @@ -1,21 +1,46 @@ from __future__ import annotations +from collections.abc import Callable +from typing import Any, overload + from .core import option type Option[T] = option.SomeWrapper[T] | T | None -def erase[T](opt: Option[T]) -> T | None: - """Erase Option wrapper type for type checker. +@overload +def erase[T](__fn: Callable[..., Option[T]], /) -> Callable[..., T | None]: ... + + +@overload +def erase[T](__value: Option[T], /) -> T | None: ... + + +def erase(__value_or_fn: Any, /) -> Any: + """Erase Option[T] to T | None for the type checker. + + Works on both values and functions. Identity at runtime. + Used when compiler knows Option is non-nested. + """ + return __value_or_fn + + +@overload +def widen[T](__fn: Callable[..., T | None], /) -> Callable[..., Option[T]]: ... + + +@overload +def widen[T](__value: T | None, /) -> Option[T]: ... + + +def widen(__value_or_fn: Any, /) -> Any: + """Widen T | None to Option[T] for the type checker. - Converts Option[T] (SomeWrapper[T] | T | None) to T | None. This is - an identity function at runtime - zero overhead. Used by the - compiler when it knows it can safely cross generic → concrete - boundaries. + Works on both values and functions. Identity at runtime. + Inverse of erase(). """ - # Use type: ignore instead of cast to avoid additional runtime overhead - return opt # type: ignore[return-value] + return __value_or_fn # Re-export the functions from core.option @@ -59,4 +84,5 @@ def erase[T](opt: Option[T]) -> T | None: "to_array", "to_nullable", "value", + "widen", ]