diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index ed0e795be6..6a0c0db93d 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 ea8b7333a1..14f9291d7c 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 = diff --git a/tests/Js/Main/TypeTests.fs b/tests/Js/Main/TypeTests.fs index 2d6cddb4d5..3654cf8abb 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") ]