Skip to content

Debug builds: SRTP-constrained inline member runs the first call's specialization for later calls with other type arguments #20826

Closed

Description

@cmeeren

Since #19548 (SDK 10.0.400), Debug builds call inline members through generated <Name>__debug@N methods instead of inlining them. When an inline member with SRTP constraints defines local closures and is called more than once with different type arguments, every call after the first runs the first call's specialization.

Repro steps

One project with Lib.fs followed by Program.fs, run with dotnet run -c Debug.

Lib.fs:

module Lib

open System

let inline create (x: ^a) : Result< ^b, 'c > =
    ((^a or ^b): (static member create: ^a -> Result< ^b, 'c >) x)

type Builder() =
    member val Registered = ResizeArray<string>()

    member this.Convert<'a, 'b>(f: Func<'a, 'b>) =
        this.Registered.Add($"{typeof<'a>.Name}->{typeof<'b>.Name}")
        this

type Builder with
    [<RequiresExplicitTypeArguments>]
    member inline this.Register<'domain, 'serialized
        when ('serialized or 'domain): (static member create: 'serialized -> Result<'domain, string>)
        and (^domain or ^serialized): (static member op_Explicit: ^domain -> ^serialized)>
        ()
        =
        let parse x =
            match create x with
            | Ok x -> x
            | Error(msg: string) -> failwith msg

        let getValue x =
            ((^domain or ^serialized): (static member op_Explicit: ^domain -> ^serialized) x)

        this.Convert<'serialized, 'domain>(parse).Convert<'domain, 'serialized>(getValue) |> ignore
        this

Program.fs:

open Lib

type A =
    | A of string
    static member create(s: string) = Ok(A s)
    static member op_Explicit(A s) : string = s

type B =
    | B of int
    static member create(i: int) = Ok(B i)
    static member op_Explicit(B i) : int = i

let b = Builder()
b.Register<A, string>() |> ignore
b.Register<B, int>() |> ignore

printfn "%s" (String.concat ", " b.Registered)

Expected behavior

String->A, A->String, Int32->B, B->Int32, as in Release or with --always-inline+.

Actual behavior

String->A, A->String, String->A, A->String: the Register<B, int> call runs the specialization for A and string.

It also happens when Lib is a separate referenced project, and with a chained call (b.Register<A, string>().Register<B, int>()). A generic inline member without SRTP constraints is unaffected.

In our application, an inline member like this registers HotChocolate type converters. In Debug, every call registered the first call's type, so the app failed at startup.

Known workarounds

<AlwaysInline>true</AlwaysInline> (--always-inline+).

Related information

  • .NET SDK 10.0.402 (F# compiler 15.2.402) and 11.0.100-rc.2.26504.105
  • Windows 11, .NET 10

Activity

  1. added this to the Backlog milestone on Oct 21, 2026
  2. added
    Area-Debugstepping, debug points, stacks and more
    Area-Compiler-SRTPbugs in SRTP inference, resolution, witness passing, code gen
    and removed on Oct 21, 2026
  3. T-Gro commented on Oct 21, 2026

    @T-Gro
    Member

    @auduchinok : Can you please have a look?

  4. auduchinok commented on Oct 21, 2026

    @auduchinok
    Member

    I'll take a look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area-Compiler-SRTPbugs in SRTP inference, resolution, witness passing, code genArea-Debugstepping, debug points, stacks and more

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions