From 95fb87d0128421e68470d7f45acd0012a21a39f4 Mon Sep 17 00:00:00 2001 From: Davis Davalos-DeLosh Date: Wed, 2 Jul 2025 23:22:57 -0600 Subject: [PATCH 1/2] Fix generic parameter resolution in inline functions with static member constraints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes an issue where inline functions with multiple generic type parameters and static member constraints would incorrectly resolve all trait calls to the first type parameter's witness. Changes: - Add tryFindWitnessWithSourceTypes to properly select witnesses based on source type information when multiple witnesses match - Update trait call resolution to use source types for disambiguation - Fix generic argument composition in nested inline functions to preserve outer context mappings The fix ensures that each generic parameter's static member constraints are resolved to the correct witness, including in complex scenarios like nested inline function calls. Fixes the issue where code like: ```fsharp let inline test<'a, 'b when 'a: (static member M: unit -> string) and 'b: (static member M: unit -> string)> () = 'a.M(), 'b.M() ``` Would incorrectly return the same value for both calls. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/Fable.Transforms/FSharp2Fable.Util.fs | 82 ++++++++++++++++++++++- src/Fable.Transforms/FSharp2Fable.fs | 12 ++-- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index ed0e795be..6a0c0db93 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -1719,6 +1719,68 @@ module TypeHelpers = && listEquals (typeEquals false) argTypes w.ArgTypes ) + // Enhanced version that handles multiple witnesses for the same trait + // by using source type information to pick the correct one + let tryFindWitnessWithSourceTypes (ctx: Context) sourceTypes argTypes isInstance traitName = + let matchingWitnesses = + ctx.Witnesses + |> List.filter (fun w -> + w.TraitName = traitName + && w.IsInstance = isInstance + && listEquals (typeEquals false) argTypes w.ArgTypes + ) + + match matchingWitnesses with + | [] -> None + | [ single ] -> Some single + | multiple -> + // Multiple witnesses for the same trait - need to pick the right one + // based on which generic parameter is being resolved + match sourceTypes with + | [ sourceType ] -> + // First, resolve the source type in case it's a generic parameter + // that needs to be mapped to a concrete type + let resolvedSourceType = + match sourceType with + | Fable.GenericParam(name, _, _) -> + // Check if this generic parameter has been mapped to a concrete type + match Map.tryFind name ctx.GenericArgs with + | Some concreteType -> concreteType + | None -> sourceType + | _ -> sourceType + + // Now find which witness to use based on the resolved source type + match resolvedSourceType with + | Fable.GenericParam(name, isMeasure, _) -> + // For generic parameters, find their position in the original function signature + let genParamNames = ctx.GenericArgs |> Map.toList |> List.map fst + let genParamPosition = genParamNames |> List.tryFindIndex ((=) name) + + match genParamPosition with + | Some idx when idx < List.length multiple -> + // Witnesses are provided in order corresponding to generic parameters + List.tryItem idx multiple + | _ -> List.tryHead multiple + + | Fable.DeclaredType(entity, _) -> + // Find the position of this entity type in the generic arguments + let genArgPosition = + ctx.GenericArgs + |> Map.toList + |> List.tryFindIndex (fun (_, argType) -> + match argType with + | Fable.DeclaredType(e, _) -> e.FullName = entity.FullName + | _ -> false + ) + + match genArgPosition with + | Some idx when idx < List.length multiple -> + // Witnesses are provided in order corresponding to generic parameters + List.tryItem idx multiple + | _ -> List.tryHead multiple + | _ -> List.tryHead multiple + | _ -> List.tryHead multiple + module Identifiers = open Helpers open TypeHelpers @@ -2702,9 +2764,27 @@ module Util = let genArgs = List.zipSafe inExpr.GenericArgs info.GenericArgs |> Map + // For nested inline functions, we need to preserve the outer context's generic args + // and resolve any references through them + let resolveTypeWithOuterContext (typ: Fable.Type) = + match typ with + | Fable.GenericParam(name, _, _) -> + // If this generic parameter is mapped in the outer context, use that + match Map.tryFind name ctx.GenericArgs with + | Some resolvedType -> resolvedType + | None -> typ + | _ -> typ + + // Create the composed generic args by resolving through the outer context + let composedGenArgs = + genArgs + |> Map.map (fun _ typ -> resolveTypeWithOuterContext typ) + // Also preserve any generic args from the outer context that aren't overridden + |> Map.fold (fun acc k v -> Map.add k v acc) ctx.GenericArgs + let ctx = { ctx with - GenericArgs = genArgs + GenericArgs = composedGenArgs InlinePath = { ToFile = inExpr.FileName diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index ea8b7333a..14f9291d7 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -923,9 +923,10 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs return Fable.Unresolved(e, typ, r) | None -> - match tryFindWitness ctx argTypes flags.IsInstance traitName with + let sourceTypes = List.map (makeType ctx.GenericArgs) sourceTypes + + match tryFindWitnessWithSourceTypes ctx sourceTypes argTypes flags.IsInstance traitName with | None -> - let sourceTypes = List.map (makeType ctx.GenericArgs) sourceTypes return transformTraitCall com ctx r typ sourceTypes traitName flags.IsInstance argTypes argExprs | Some w -> let callInfo = makeCallInfo None argExprs argTypes @@ -2454,11 +2455,10 @@ let resolveInlineExpr (com: IFableCompiler) ctx info expr = let argExprs = argExprs |> List.map (resolveInlineExpr com ctx info) - match tryFindWitness ctx argTypes isInstance traitName with - | None -> - let sourceTypes = sourceTypes |> List.map (resolveInlineType ctx.GenericArgs) + let sourceTypes = sourceTypes |> List.map (resolveInlineType ctx.GenericArgs) - transformTraitCall com ctx r t sourceTypes traitName isInstance argTypes argExprs + match tryFindWitnessWithSourceTypes ctx sourceTypes argTypes isInstance traitName with + | None -> transformTraitCall com ctx r t sourceTypes traitName isInstance argTypes argExprs | Some w -> // As witnesses come from the context, idents may be duplicated, see #2855 let info = From 1675fb5ae57e4700b2eb3dd1514aab6dd6361313 Mon Sep 17 00:00:00 2001 From: Davis Davalos-DeLosh Date: Thu, 3 Jul 2025 15:25:40 -0600 Subject: [PATCH 2/2] Add comprehensive test suite for generic parameter resolution fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added tests covering: - Two and three parameter inline functions with static member constraints - Multiple constraints per type parameter - Reversed parameter order - Nested inline function scenarios - Various type parameter combinations All tests pass successfully, verifying the fix for issue #4093. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- tests/Js/Main/TypeTests.fs | 76 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/Js/Main/TypeTests.fs b/tests/Js/Main/TypeTests.fs index 2d6cddb4d..3654cf8ab 100644 --- a/tests/Js/Main/TypeTests.fs +++ b/tests/Js/Main/TypeTests.fs @@ -639,6 +639,50 @@ type StaticClass = static member DefaultNullParam([] x: obj) = x static member inline InlineAdd(x: int, ?y: int) = x + (defaultArg y 2) +// Test types for generic parameter static member resolution +type TestTypeA = + static member GetValue() = "A" + static member Combine(x: int, y: int) = x + y + 100 + +type TestTypeB = + static member GetValue() = "B" + static member Combine(x: int, y: int) = x + y + 200 + +type TestTypeC = + static member GetValue() = "C" + static member Combine(x: int, y: int) = x + y + 300 + +// Inline functions for testing multiple generic parameters with static member constraints +let inline getTwoValues<'a, 'b when 'a: (static member GetValue: unit -> string) + and 'b: (static member GetValue: unit -> string)> () = + 'a.GetValue(), 'b.GetValue() + +let inline getThreeValues<'a, 'b, 'c when 'a: (static member GetValue: unit -> string) + and 'b: (static member GetValue: unit -> string) + and 'c: (static member GetValue: unit -> string)> () = + 'a.GetValue(), 'b.GetValue(), 'c.GetValue() + +let inline getValuesAndCombine<'a, 'b when 'a: (static member GetValue: unit -> string) + and 'a: (static member Combine: int * int -> int) + and 'b: (static member GetValue: unit -> string) + and 'b: (static member Combine: int * int -> int)> x y = + let aVal = 'a.GetValue() + let bVal = 'b.GetValue() + let aCombined = 'a.Combine(x, y) + let bCombined = 'b.Combine(x, y) + (aVal, aCombined), (bVal, bCombined) + +let inline getReversed<'x, 'y when 'x: (static member GetValue: unit -> string) + and 'y: (static member GetValue: unit -> string)> () = + 'y.GetValue(), 'x.GetValue() + +let inline innerGet<'t when 't: (static member GetValue: unit -> string)> () = + 't.GetValue() + +let inline outerGet<'a, 'b when 'a: (static member GetValue: unit -> string) + and 'b: (static member GetValue: unit -> string)> () = + innerGet<'a>(), innerGet<'b>() + let tests = testList "Types" [ @@ -1393,4 +1437,36 @@ let tests = (upper :> IGenericMangledInterface).ReadOnlyValue |> equal "value" (upper :> IGenericMangledInterface).SetterOnlyValue <- "setter only value" (upper :> IGenericMangledInterface).Value |> equal "setter only value" + + // Test for generic type parameter static member resolution in inline functions + // https://github.com/fable-compiler/Fable/issues/4093 + testCase "Inline function with two generic parameters resolves static members correctly" <| fun () -> + let result = getTwoValues() + result |> equal ("A", "B") + + testCase "Inline function with three generic parameters resolves static members correctly" <| fun () -> + let result = getThreeValues() + result |> equal ("A", "B", "C") + + testCase "Inline function with multiple constraints per type parameter works" <| fun () -> + let result = getValuesAndCombine 10 20 + result |> equal (("A", 130), ("B", 230)) + + testCase "Inline function with reversed type parameter order works" <| fun () -> + let result = getReversed() + result |> equal ("B", "A") + + testCase "Nested inline functions resolve generic parameters correctly" <| fun () -> + let result = outerGet() + result |> equal ("A", "B") + + testCase "Different type parameter combinations work correctly" <| fun () -> + let result1 = getTwoValues() + result1 |> equal ("B", "A") + + let result2 = getTwoValues() + result2 |> equal ("C", "A") + + let result3 = getTwoValues() + result3 |> equal ("B", "C") ]