From 87c756b6c0f572922717e38b51929bf89158f30f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Mar 2026 01:33:22 +0000 Subject: [PATCH 1/5] Fix captured side-effect-free values inlined into closures in release mode In canInlineArg, the first branch allowed inlining a value when it had no side effects and was referenced at most once. However it did not check whether the ident was captured inside a closure (lambda or ObjectExpr member body). Values like an empty ResizeArray() are considered side-effect-free, but inlining them into a getter body means a new instance is created on each getter call rather than sharing the single instance. Add isIdentCaptured check to the first branch to match the guard already present in the second branch. Fixes #3779. Co-Authored-By: Claude Sonnet 4.6 --- src/Fable.Cli/CHANGELOG.md | 1 + src/Fable.Compiler/CHANGELOG.md | 1 + src/Fable.Transforms/FableTransforms.fs | 14 +++++++++++++- tests/Js/Main/TypeTests.fs | 17 +++++++++++++++++ tests/Python/TestType.fs | 18 ++++++++++++++++++ 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 8ddd8b64ea..d29351f2ca 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* [All] Fix captured side-effect-free values (e.g. empty ResizeArray) being incorrectly inlined into object expression getters in release mode, causing a new instance to be created on each getter call (fixes #3779) (by @MangelMaxime) * [Beam] Fix `|> ignore` on cross-module Emit calls generating variable bindings that shadow Emit case-clause variables (by @dbrattli) * [Beam] Fix `containsIdentRef` not checking `Call` ThisArg (by @dbrattli) * [All] Fix CLI color not resetting after error messages (fixes #3755) (by @MangelMaxime) diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index c54085d64c..4a431ce464 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* [All] Fix captured side-effect-free values (e.g. empty ResizeArray) being incorrectly inlined into object expression getters in release mode, causing a new instance to be created on each getter call (fixes #3779) (by @MangelMaxime) * [Beam] Fix `|> ignore` on cross-module Emit calls generating variable bindings that shadow Emit case-clause variables (by @dbrattli) * [Beam] Fix `containsIdentRef` not checking `Call` ThisArg (by @dbrattli) * [JS/TS] `StringEnum` now respect `CompiledValue` and `CompiledName` (by @shayanhabibi) diff --git a/src/Fable.Transforms/FableTransforms.fs b/src/Fable.Transforms/FableTransforms.fs index ea5a437069..82f59c2450 100644 --- a/src/Fable.Transforms/FableTransforms.fs +++ b/src/Fable.Transforms/FableTransforms.fs @@ -242,7 +242,19 @@ let canInlineArg (com: Compiler) identName value body = | _ -> let refCount = countReferencesUntil 2 identName body - (refCount <= 1 && not (canHaveSideEffects com value)) + // Inlining a plain identifier reference is always safe (it's just a rename), + // regardless of whether the ident is captured in a closure. + let isIdentValue = + match value with + | IdentExpr _ -> true + | _ -> false + + // Don't inline even side-effect-free values when they're captured in a closure (e.g. object expression + // getter): inlining would create a new value on each closure invocation instead of sharing one instance. + // Exception: plain ident values are always safe to inline (they don't create new objects). + (refCount <= 1 + && not (canHaveSideEffects com value) + && (isIdentValue || not (isIdentCaptured identName body))) // If it can have side effects, make sure is at least referenced once so the expression is not erased || (refCount = 1 && noSideEffectBeforeIdent identName body diff --git a/tests/Js/Main/TypeTests.fs b/tests/Js/Main/TypeTests.fs index ac68232210..65615bdfa2 100644 --- a/tests/Js/Main/TypeTests.fs +++ b/tests/Js/Main/TypeTests.fs @@ -657,6 +657,10 @@ type MangledAbstractClass5(v) = inherit MangledAbstractClass4(v + 5) override _.MyMethod(x: int) = base.MyMethod(x) + v + 7 +[] +type AbstractClassWithResizeArrayProp() = + abstract Warnings: ResizeArray with get + type ConcreteClass1() = inherit MangledAbstractClass5(2) @@ -1621,4 +1625,17 @@ let tests = top.A |> equal 0 top.B.A |> equal 0 top.B.B |> equal false + + testCase "Abstract class property backed by captured variable in object expression works" <| fun () -> + let warnings = ResizeArray() + + let reader = + { new AbstractClassWithResizeArrayProp() with + member __.Warnings = warnings + } + + reader.Warnings.Add("Warning 1") + reader.Warnings.Add("Warning 2") + + reader.Warnings.Count |> equal 2 ] diff --git a/tests/Python/TestType.fs b/tests/Python/TestType.fs index 0e817dbdbf..5b753e669d 100644 --- a/tests/Python/TestType.fs +++ b/tests/Python/TestType.fs @@ -577,6 +577,10 @@ type MangledAbstractClass5(v) = inherit MangledAbstractClass4(v + 5) override _.MyMethod(x: int) = base.MyMethod(x) + v + 7 +[] +type AbstractClassWithResizeArrayProp() = + abstract Warnings: ResizeArray with get + type ConcreteClass1() = inherit MangledAbstractClass5(2) @@ -1776,3 +1780,17 @@ let ``test Unchecked.defaultof works for fields on structs`` () = top.A |> equal 0 top.B.A |> equal 0 top.B.B |> equal false + +[] +let ``test Abstract class property backed by captured variable in object expression works`` () = + let warnings = ResizeArray() + + let reader = + { new AbstractClassWithResizeArrayProp() with + member __.Warnings = warnings + } + + reader.Warnings.Add("Warning 1") + reader.Warnings.Add("Warning 2") + + reader.Warnings.Count |> equal 2 From a2713b92d3197bc674cdcd24a42be188c2e147ed Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Tue, 24 Mar 2026 21:16:33 +0100 Subject: [PATCH 2/5] fix: try to pyright errors --- src/Fable.Transforms/Python/Fable2Python.Util.fs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.Util.fs b/src/Fable.Transforms/Python/Fable2Python.Util.fs index e95fdbcf94..fffce36062 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Util.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Util.fs @@ -269,7 +269,9 @@ module Util = match fieldInfo.FieldType with | Some typ -> hasErasedOptionReturnType typ | None -> false - | _ -> false + | _ -> + // For any other expression (e.g. CurriedApply, Call) check the expression type + hasErasedOptionReturnType argExpr.Type | _ -> false /// Wraps None values in cast(type, None) for type safety. From 0ef7a8aaa8c27f9e1d1c77dc0c93d23fbbbee6ab Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Tue, 24 Mar 2026 22:07:20 +0100 Subject: [PATCH 3/5] fix: [Python] Fix Pyright errors from generic enum-to-numeric casts When LanguagePrimitives.EnumToValue is used with a generic enum constraint (e.g. 'TEnum when 'TEnum: enum), the PR's inlining fix causes the cast result to be hoisted into a let-binding. This produced Pyright errors because constructor calls like byte(value) or int32(value) don't typecheck when the source is a generic param (object*). Fix: - In transformCast, trace through TypeCast chains to find the innermost source expression. If it's a GenericParam, emit typing.cast() instead of a constructor call. This handles double-cast patterns from EnumToValue + |> byte. - Add UInt32 case to transformCast (like existing Float32/Float64/Int32 cases) so uint32() constructor is emitted for UInt32 casts. - In Fable2Python.Util, add isCastFromGenericParam helper and use it in transformBindingAsStatements to skip the type annotation (avoiding 'TENUM not assignable to byte' annotation errors). Result: encode.py goes from 9 new errors to 0, total Pyright count back to 93. --- .../Python/Fable2Python.Transforms.fs | 41 +++++++++++++++++-- .../Python/Fable2Python.Util.fs | 22 ++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 2ec963ca0c..68a0491282 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -337,8 +337,31 @@ let optimizeTailCall (com: IPythonCompiler) (ctx: Context) range (tc: ITailCallO yield Statement.continue' (?loc = range) ] -let transformCast (com: IPythonCompiler) (ctx: Context) t e : Expression * Statement list = +let transformCast (com: IPythonCompiler) (ctx: Context) (t: Fable.Type) (e: Fable.Expr) : Expression * Statement list = // printfn "transformCast: %A" (t, e) + // When casting from a generic param to a numeric type, use typing.cast() instead of + // calling the constructor, since Pyright can't verify generic params satisfy numeric protocols. + // Trace through TypeCast chains to find the innermost source expression. + // This handles double-cast patterns like TypeCast(TypeCast(IdentExpr{TENUM}, Int32), Int32) + // that arise from EnumToValue + numeric conversion (e.g. int32(EnumToValue value)). + let rec getInnermostExpr (expr: Fable.Expr) = + match expr with + | Fable.TypeCast(inner, _) -> getInnermostExpr inner + | _ -> expr + + let isGenericParamSource = + match (getInnermostExpr e).Type with + | Fable.GenericParam _ -> true + | _ -> false + + let castFromGenericToNumeric () = + let ta, stmts = Annotation.typeAnnotation com ctx None t + // Use the innermost expression to avoid double-cast for TypeCast chain patterns + // like TypeCast(TypeCast(IdentExpr{TENUM}, Int32), Int32) → cast(int32, value) + let value, valueStmts = com.TransformAsExpr(ctx, getInnermostExpr e) + let cast = com.GetImportExpr(ctx, "typing", "cast") + Expression.call (cast, [ ta; value ]), stmts @ valueStmts + match t, e with | IEnumerableOfKeyValuePair(kvpEnt) -> // Call .items() on the dictionary and wrap with to_enumerable for IEnumerable_1 compatibility @@ -383,6 +406,10 @@ let transformCast (com: IPythonCompiler) (ctx: Context) t e : Expression * State | _ -> libCall com ctx None "util" "to_enumerable" [ listExpr ], stmts | _ -> com.TransformAsExpr(ctx, e) + // When casting from a generic type param to any numeric type, use typing.cast() instead of + // calling the constructor. Pyright can't verify generic params satisfy numeric protocols + // (e.g. TENUM: enum → byte), so a constructor call like byte(value) would fail. + | Fable.Number _, _ when isGenericParamSource -> castFromGenericToNumeric () | Fable.Number(Float32, _), _ -> let cons = libValue com ctx "core" "float32" let value, stmts = com.TransformAsExpr(ctx, e) @@ -395,6 +422,10 @@ let transformCast (com: IPythonCompiler) (ctx: Context) t e : Expression * State let cons = libValue com ctx "core" "int32" let value, stmts = com.TransformAsExpr(ctx, e) Expression.call (cons, [ value ], ?loc = None), stmts + | Fable.Number(UInt32, _), _ -> + let cons = libValue com ctx "core" "uint32" + let value, stmts = com.TransformAsExpr(ctx, e) + Expression.call (cons, [ value ], ?loc = None), stmts | _ -> com.TransformAsExpr(ctx, e) let transformCurry (com: IPythonCompiler) (ctx: Context) expr arity : Expression * Statement list = @@ -1495,12 +1526,15 @@ let transformBindingAsExpr (com: IPythonCompiler) ctx (var: Fable.Ident) (value: let transformBindingAsStatements (com: IPythonCompiler) ctx (var: Fable.Ident) (value: Fable.Expr) = let shouldTreatAsStatement = isPyStatement ctx false value let needsErase = needsOptionEraseForBinding value var.Type - // Skip type annotation to avoid Option[T] vs T | None mismatch issues with Pyright: + // Skip type annotation to avoid type mismatch issues with Pyright: // 1. When extracting from invariant containers (Array, List) with Options // 2. When assigning from a wrapped option after None check (narrowing issue) + // 3. When casting from a generic parameter to a concrete numeric type (e.g. EnumToValue) + // In this case, transformCast emits a typing.cast() call which gives Pyright the type info. let skipAnnotation = valueExtractsFromInvariantContainer value var.Type || isWrappedOptionNarrowingAssignment value + || isCastFromGenericParam value var.Type if shouldTreatAsStatement then let varName, varExpr = Expression.name var.Name, identAsExpr com ctx var @@ -1521,7 +1555,8 @@ let transformBindingAsStatements (com: IPythonCompiler) ctx (var: Fable.Ident) ( let varName = com.GetIdentifierAsExpr(ctx, Naming.toPythonNaming var.Name) if skipAnnotation then - // No type annotation - let Python infer from function return type + // No type annotation - let Python infer the type from the RHS. + // For generic-to-numeric casts, transformCast already emits typing.cast() on the RHS. let decl = varDeclaration ctx varName None expr stmts @ decl else diff --git a/src/Fable.Transforms/Python/Fable2Python.Util.fs b/src/Fable.Transforms/Python/Fable2Python.Util.fs index fffce36062..a92b0b7689 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Util.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Util.fs @@ -199,6 +199,28 @@ module Util = | _ -> false | _ -> false + /// Check if a value is a TypeCast chain from a generic parameter to a numeric type. + /// This happens with LanguagePrimitives.EnumToValue patterns like: + /// let byte<'TEnum when 'TEnum: enum> (value: 'TEnum) = EnumToValue value |> byte + /// The binding `let value_1: byte = value` would fail Pyright because TENUM is object*. + /// In this case, skip the type annotation and let Python infer from the runtime value. + let isCastFromGenericParam (value: Fable.Expr) (varType: Fable.Type) = + match varType with + | Fable.Number _ -> + // Trace through TypeCast chains to see if the source is a generic param + let rec getInnerExpr expr = + match expr with + | Fable.TypeCast(inner, _) -> getInnerExpr inner + | _ -> expr + + match getInnerExpr value with + | Fable.IdentExpr ident -> + match ident.Type with + | Fable.GenericParam _ -> true + | _ -> false + | _ -> false + | _ -> false + /// Get narrowed contexts for then/else branches based on a guard expression /// Returns (thenCtx, elseCtx) with appropriate type narrowing applied let getNarrowedContexts (ctx: Context) (guardExpr: Fable.Expr) : Context * Context = From 08a5aea1c6051ce13ea818f101fe60dc3522b520 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Wed, 25 Mar 2026 00:00:47 +0100 Subject: [PATCH 4/5] fix: try to fix TypeScript and Rust code --- src/Fable.Transforms/FableTransforms.fs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Fable.Transforms/FableTransforms.fs b/src/Fable.Transforms/FableTransforms.fs index 82f59c2450..9dcdac981f 100644 --- a/src/Fable.Transforms/FableTransforms.fs +++ b/src/Fable.Transforms/FableTransforms.fs @@ -242,19 +242,22 @@ let canInlineArg (com: Compiler) identName value body = | _ -> let refCount = countReferencesUntil 2 identName body - // Inlining a plain identifier reference is always safe (it's just a rename), - // regardless of whether the ident is captured in a closure. - let isIdentValue = + // Don't inline values that create new mutable state (e.g. ResizeArray(), mutable arrays) + // when they're captured in a closure (e.g. object expression getter): inlining would create + // a new instance on each closure invocation instead of sharing the single instance. + // Lambdas, delegates, imports, ident refs, and immutable values are safe to inline. + let createsMutableState = match value with - | IdentExpr _ -> true + | Value(NewArray(_, _, kind), _) -> + match kind with + | MutableArray + | ResizeArray -> true + | ImmutableArray -> false | _ -> false - // Don't inline even side-effect-free values when they're captured in a closure (e.g. object expression - // getter): inlining would create a new value on each closure invocation instead of sharing one instance. - // Exception: plain ident values are always safe to inline (they don't create new objects). (refCount <= 1 && not (canHaveSideEffects com value) - && (isIdentValue || not (isIdentCaptured identName body))) + && not (createsMutableState && isIdentCaptured identName body)) // If it can have side effects, make sure is at least referenced once so the expression is not erased || (refCount = 1 && noSideEffectBeforeIdent identName body From 67b4164c88d56125bedbce759af0d582b3a7edc2 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Wed, 25 Mar 2026 16:36:27 +0100 Subject: [PATCH 5/5] revert: python changes they are not needed anymore --- src/Fable.Transforms/FableTransforms.fs | 6 +-- .../Python/Fable2Python.Transforms.fs | 41 ++----------------- .../Python/Fable2Python.Util.fs | 26 +----------- 3 files changed, 7 insertions(+), 66 deletions(-) diff --git a/src/Fable.Transforms/FableTransforms.fs b/src/Fable.Transforms/FableTransforms.fs index 9dcdac981f..60189f787a 100644 --- a/src/Fable.Transforms/FableTransforms.fs +++ b/src/Fable.Transforms/FableTransforms.fs @@ -243,9 +243,9 @@ let canInlineArg (com: Compiler) identName value body = let refCount = countReferencesUntil 2 identName body // Don't inline values that create new mutable state (e.g. ResizeArray(), mutable arrays) - // when they're captured in a closure (e.g. object expression getter): inlining would create - // a new instance on each closure invocation instead of sharing the single instance. - // Lambdas, delegates, imports, ident refs, and immutable values are safe to inline. + // into closures: even though creation is side-effect-free, inlining into a closure + // called multiple times would create a new instance per call instead of sharing the + // single captured instance let createsMutableState = match value with | Value(NewArray(_, _, kind), _) -> diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 68a0491282..2ec963ca0c 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -337,31 +337,8 @@ let optimizeTailCall (com: IPythonCompiler) (ctx: Context) range (tc: ITailCallO yield Statement.continue' (?loc = range) ] -let transformCast (com: IPythonCompiler) (ctx: Context) (t: Fable.Type) (e: Fable.Expr) : Expression * Statement list = +let transformCast (com: IPythonCompiler) (ctx: Context) t e : Expression * Statement list = // printfn "transformCast: %A" (t, e) - // When casting from a generic param to a numeric type, use typing.cast() instead of - // calling the constructor, since Pyright can't verify generic params satisfy numeric protocols. - // Trace through TypeCast chains to find the innermost source expression. - // This handles double-cast patterns like TypeCast(TypeCast(IdentExpr{TENUM}, Int32), Int32) - // that arise from EnumToValue + numeric conversion (e.g. int32(EnumToValue value)). - let rec getInnermostExpr (expr: Fable.Expr) = - match expr with - | Fable.TypeCast(inner, _) -> getInnermostExpr inner - | _ -> expr - - let isGenericParamSource = - match (getInnermostExpr e).Type with - | Fable.GenericParam _ -> true - | _ -> false - - let castFromGenericToNumeric () = - let ta, stmts = Annotation.typeAnnotation com ctx None t - // Use the innermost expression to avoid double-cast for TypeCast chain patterns - // like TypeCast(TypeCast(IdentExpr{TENUM}, Int32), Int32) → cast(int32, value) - let value, valueStmts = com.TransformAsExpr(ctx, getInnermostExpr e) - let cast = com.GetImportExpr(ctx, "typing", "cast") - Expression.call (cast, [ ta; value ]), stmts @ valueStmts - match t, e with | IEnumerableOfKeyValuePair(kvpEnt) -> // Call .items() on the dictionary and wrap with to_enumerable for IEnumerable_1 compatibility @@ -406,10 +383,6 @@ let transformCast (com: IPythonCompiler) (ctx: Context) (t: Fable.Type) (e: Fabl | _ -> libCall com ctx None "util" "to_enumerable" [ listExpr ], stmts | _ -> com.TransformAsExpr(ctx, e) - // When casting from a generic type param to any numeric type, use typing.cast() instead of - // calling the constructor. Pyright can't verify generic params satisfy numeric protocols - // (e.g. TENUM: enum → byte), so a constructor call like byte(value) would fail. - | Fable.Number _, _ when isGenericParamSource -> castFromGenericToNumeric () | Fable.Number(Float32, _), _ -> let cons = libValue com ctx "core" "float32" let value, stmts = com.TransformAsExpr(ctx, e) @@ -422,10 +395,6 @@ let transformCast (com: IPythonCompiler) (ctx: Context) (t: Fable.Type) (e: Fabl let cons = libValue com ctx "core" "int32" let value, stmts = com.TransformAsExpr(ctx, e) Expression.call (cons, [ value ], ?loc = None), stmts - | Fable.Number(UInt32, _), _ -> - let cons = libValue com ctx "core" "uint32" - let value, stmts = com.TransformAsExpr(ctx, e) - Expression.call (cons, [ value ], ?loc = None), stmts | _ -> com.TransformAsExpr(ctx, e) let transformCurry (com: IPythonCompiler) (ctx: Context) expr arity : Expression * Statement list = @@ -1526,15 +1495,12 @@ let transformBindingAsExpr (com: IPythonCompiler) ctx (var: Fable.Ident) (value: let transformBindingAsStatements (com: IPythonCompiler) ctx (var: Fable.Ident) (value: Fable.Expr) = let shouldTreatAsStatement = isPyStatement ctx false value let needsErase = needsOptionEraseForBinding value var.Type - // Skip type annotation to avoid type mismatch issues with Pyright: + // Skip type annotation to avoid Option[T] vs T | None mismatch issues with Pyright: // 1. When extracting from invariant containers (Array, List) with Options // 2. When assigning from a wrapped option after None check (narrowing issue) - // 3. When casting from a generic parameter to a concrete numeric type (e.g. EnumToValue) - // In this case, transformCast emits a typing.cast() call which gives Pyright the type info. let skipAnnotation = valueExtractsFromInvariantContainer value var.Type || isWrappedOptionNarrowingAssignment value - || isCastFromGenericParam value var.Type if shouldTreatAsStatement then let varName, varExpr = Expression.name var.Name, identAsExpr com ctx var @@ -1555,8 +1521,7 @@ let transformBindingAsStatements (com: IPythonCompiler) ctx (var: Fable.Ident) ( let varName = com.GetIdentifierAsExpr(ctx, Naming.toPythonNaming var.Name) if skipAnnotation then - // No type annotation - let Python infer the type from the RHS. - // For generic-to-numeric casts, transformCast already emits typing.cast() on the RHS. + // No type annotation - let Python infer from function return type let decl = varDeclaration ctx varName None expr stmts @ decl else diff --git a/src/Fable.Transforms/Python/Fable2Python.Util.fs b/src/Fable.Transforms/Python/Fable2Python.Util.fs index a92b0b7689..e95fdbcf94 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Util.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Util.fs @@ -199,28 +199,6 @@ module Util = | _ -> false | _ -> false - /// Check if a value is a TypeCast chain from a generic parameter to a numeric type. - /// This happens with LanguagePrimitives.EnumToValue patterns like: - /// let byte<'TEnum when 'TEnum: enum> (value: 'TEnum) = EnumToValue value |> byte - /// The binding `let value_1: byte = value` would fail Pyright because TENUM is object*. - /// In this case, skip the type annotation and let Python infer from the runtime value. - let isCastFromGenericParam (value: Fable.Expr) (varType: Fable.Type) = - match varType with - | Fable.Number _ -> - // Trace through TypeCast chains to see if the source is a generic param - let rec getInnerExpr expr = - match expr with - | Fable.TypeCast(inner, _) -> getInnerExpr inner - | _ -> expr - - match getInnerExpr value with - | Fable.IdentExpr ident -> - match ident.Type with - | Fable.GenericParam _ -> true - | _ -> false - | _ -> false - | _ -> false - /// Get narrowed contexts for then/else branches based on a guard expression /// Returns (thenCtx, elseCtx) with appropriate type narrowing applied let getNarrowedContexts (ctx: Context) (guardExpr: Fable.Expr) : Context * Context = @@ -291,9 +269,7 @@ module Util = match fieldInfo.FieldType with | Some typ -> hasErasedOptionReturnType typ | None -> false - | _ -> - // For any other expression (e.g. CurriedApply, Call) check the expression type - hasErasedOptionReturnType argExpr.Type + | _ -> false | _ -> false /// Wraps None values in cast(type, None) for type safety.