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
3 changes: 3 additions & 0 deletions src/Fable.AST/Fable.fs
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,9 @@ type Witness =
IsInstance: bool
FileName: string
Expr: Expr
/// The name of the generic parameter this witness satisfies (e.g. "'a" from `'a: (static member M: unit -> string)`).
/// Used to disambiguate when multiple witnesses match the same trait name and arg types.
GenericParamName: string option
}

member this.ArgTypes =
Expand Down
1 change: 1 addition & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [Python] Fix `nonlocal`/`global` declarations generated inside `match/case` bodies causing `SyntaxError` (by @dbrattli)
* [Python] Fix exception variable captured in deferred closures causing `NameError` (PEP 3110 scoping) (by @dbrattli)
* [JS/TS] Support format specifiers and single hole in JSX string templates (by @MangelMaxime)
* [All] Fix generic parameter resolution in inline functions with static member constraints (by @Programmerino and @MangelMaxime)

## 5.0.0-rc.2 - 2026-03-03

Expand Down
1 change: 1 addition & 0 deletions src/Fable.Compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [Python] Fix `nonlocal`/`global` declarations generated inside `match/case` bodies causing `SyntaxError` (by @dbrattli)
* [Python] Fix exception variable captured in deferred closures causing `NameError` (PEP 3110 scoping) (by @dbrattli)
* [JS/TS] Support format specifiers and single hole in JSX string templates (by @MangelMaxime)
* [All] Fix generic parameter resolution in inline functions with static member constraints (by @Programmerino and @MangelMaxime)

## 5.0.0-rc.2 - 2026-03-03

Expand Down
42 changes: 42 additions & 0 deletions src/Fable.Transforms/FSharp2Fable.Util.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1728,6 +1728,48 @@ 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 match by trait name and arg types.
// Use the source type to derive the original generic parameter name, then find
// the witness tagged with that name.
//
// Two cases:
// 1. sourceTypes contains a raw GenericParam (e.g. when compiling an inline function
// whose body is not yet specialized): extract the param name directly.
// 2. sourceTypes contains a resolved DeclaredType (e.g. 'b was resolved to TestTypeB
// via ctx.GenericArgs before being passed here): reverse-lookup in ctx.GenericArgs
// to recover the original param name ("b").
let genParamName =
match sourceTypes with
| [ Fable.GenericParam(name, _, _) ] -> Some name
| [ resolvedType ] ->
// Reverse-lookup: find the generic param name whose resolved type matches
ctx.GenericArgs
|> Map.tryFindKey (fun _paramName paramType -> typeEquals false paramType resolvedType)
| _ -> None

multiple
|> List.tryFind (fun w ->
match genParamName, w.GenericParamName with
| Some gpName, Some wGpName -> gpName = wGpName
| _ -> false
)
|> Option.orElse (List.tryHead multiple)

module Identifiers =
open Helpers
open TypeHelpers
Expand Down
63 changes: 48 additions & 15 deletions src/Fable.Transforms/FSharp2Fable.fs
Original file line number Diff line number Diff line change
Expand Up @@ -927,9 +927,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
Expand Down Expand Up @@ -980,23 +981,55 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs
match witnesses with
| [] -> return ctx
| witnesses ->
// Build a map from entity full name -> generic param name.
// Each membGenArg corresponds to a GenericParameter at the same index.
// When a concrete type (e.g. TestTypeA) satisfies a constraint for 'a,
// membGenArgs contains TestTypeA's FSharpType at the position of 'a in
// memb.GenericParameters. This lets us look up the param name from the
// declaring entity of the called member inside the witness body,
// without relying on witness ordering.
let entityFullNameToParamName =
(Map.empty, Seq.zip memb.GenericParameters membGenArgs)
||> Seq.fold (fun map (gp, fsType) ->
if fsType.HasTypeDefinition then
// Protect against primitive types (e.g. int) throwing when accessing FullName
match fsType.TypeDefinition.TryFullName with
| Some fullName -> Map.add fullName gp.Name map
| None -> map
else
map
)

let witnesses =
witnesses
|> List.choose (
function
// Index is not reliable, just append witnesses from parent call
| FSharpExprPatterns.WitnessArg _idx -> None
|> List.choose (fun w ->
match w with
// WitnessArg entries are pass-throughs from an outer scope;
// skip them so they are inherited from ctx.Witnesses as-is.
// | FSharpExprPatterns.WitnessArg _idx -> None
| NestedLambda(args, body) ->
match body with
| FSharpExprPatterns.Call(callee, memb, _, _, _args) ->
Some(memb.CompiledName, Option.isSome callee, args, body)
| FSharpExprPatterns.Call(callee, calledMemb, _, _, _args) ->
let genParamName =
calledMemb.DeclaringEntity
|> Option.bind (fun ent ->
Map.tryFind ent.FullName entityFullNameToParamName
)

Some(
calledMemb.CompiledName,
Option.isSome callee,
args,
body,
genParamName
)
| FSharpExprPatterns.AnonRecordGet(_, calleeType, fieldIndex) ->
let fieldName =
calleeType.AnonRecordTypeDetails.SortedFieldNames[fieldIndex]

Some("get_" + fieldName, true, args, body)
Some("get_" + fieldName, true, args, body, None)
| FSharpExprPatterns.FSharpFieldGet(_, _, field) ->
Some("get_" + field.Name, true, args, body)
Some("get_" + field.Name, true, args, body, None)
| _ -> None
| _ -> None
)
Expand All @@ -1005,7 +1038,7 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs
// so a witness may need other witnesses to be resolved
return!
(ctx, List.rev witnesses)
||> trampolineListFold (fun ctx (traitName, isInstance, args, body) ->
||> trampolineListFold (fun ctx (traitName, isInstance, args, body, genParamName) ->
trampoline {
let ctx, args = makeFunctionArgs com ctx args

Expand All @@ -1017,6 +1050,7 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs
IsInstance = isInstance
FileName = com.CurrentFile
Expr = Fable.Delegate(args, body, None, Fable.Tags.empty)
GenericParamName = genParamName
}

return { ctx with Witnesses = w :: ctx.Witnesses }
Expand Down Expand Up @@ -2460,11 +2494,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 =
Expand Down
76 changes: 76 additions & 0 deletions tests/Js/Main/TypeTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,50 @@ type Model = unit
let update (model: Model) =
model, ()

// 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" [

Expand Down Expand Up @@ -1394,4 +1438,36 @@ let tests =
(upper :> IGenericMangledInterface<string>).ReadOnlyValue |> equal "value"
(upper :> IGenericMangledInterface<string>).SetterOnlyValue <- "setter only value"
(upper :> IGenericMangledInterface<string>).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<TestTypeA, TestTypeB>()
result |> equal ("A", "B")

testCase "Inline function with three generic parameters resolves static members correctly" <| fun () ->
let result = getThreeValues<TestTypeA, TestTypeB, TestTypeC>()
result |> equal ("A", "B", "C")

testCase "Inline function with multiple constraints per type parameter works" <| fun () ->
let result = getValuesAndCombine<TestTypeA, TestTypeB> 10 20
result |> equal (("A", 130), ("B", 230))

testCase "Inline function with reversed type parameter order works" <| fun () ->
let result = getReversed<TestTypeA, TestTypeB>()
result |> equal ("B", "A")

testCase "Nested inline functions resolve generic parameters correctly" <| fun () ->
let result = outerGet<TestTypeA, TestTypeB>()
result |> equal ("A", "B")

testCase "Different type parameter combinations work correctly" <| fun () ->
let result1 = getTwoValues<TestTypeB, TestTypeA>()
result1 |> equal ("B", "A")

let result2 = getTwoValues<TestTypeC, TestTypeA>()
result2 |> equal ("C", "A")

let result3 = getTwoValues<TestTypeB, TestTypeC>()
result3 |> equal ("B", "C")
]
Loading