Skip to content

MethodAccessException for type-private member call from inner-rec in a type C with member ... augmentation under --realsig+ #19933

@T-Gro

Description

@T-Gro

Closures generated for an inner let rec inside a method defined in a
type C with member ... augmentation are emitted as a sibling of C inside
the module class, not as a nested type of C. Under --realsig+, a source
private member of C becomes IL private (type-scoped), so the sibling
closure cannot reach it and the CLR raises MethodAccessException at first
invocation. Under --realsig- the same private compiles to IL assembly,
which is reachable from the sibling, so the bug is invisible.

Repro

module M
type Holder<'T>() =
    static let mutable backing = 0
    static member Init v = backing <- v
    static member private SecretMethod() = backing + 1

type Holder<'T> with
    member _.Run() =
        let rec h n = if n = 0 then Holder<'T>.SecretMethod() else h (n - 1)
        h 5

[<EntryPoint>]
let main _ =
    Holder<int>.Init 41
    if Holder<int>().Run() = 42 then 0 else 1
fsc --realsig+ --optimize+ -r:FSharp.Core.dll repro.fs
dotnet repro.dll
Unhandled exception. System.MethodAccessException:
  Attempt by method 'M+h@8<T>.Invoke(Int32)' to access method
  'M+Holder`1<T>.SecretMethod()' failed.

IL shape (--realsig+, shortened)

.class public sealed M                            // module class
  .class nested public Holder`1<T>
    .method private static int32 SecretMethod()   // IL private (type-scoped)
    .method public  instance int32 Run()

  .class nested assembly h@8<T>                   // SIBLING of Holder, not nested in it
    .method public virtual instance int32 Invoke(int32 n)
      call int32 class M/Holder`1<!T>::SecretMethod()  // crosses IL private boundary

Moving Run() from the augmentation into the primary type body emits
h@8<T> as class nested ... Holder1/h@8(nested insideHolder`) and the
program runs cleanly.

Reproduces on (all --realsig+; --realsig- clean)

net8.0 (sdk 8.0.422), net9.0 (sdk 9.0.301, 9.0.315),
net10.0 (sdk 10.0.101, 10.0.109, 10.0.204, 10.0.300).

Notes

Not specific to TLR. Independent of --optimize. The closure-class
placement uses the module's cloc rather than the augmented type's cloc
in IlxGen.fs. Tangentially related to #19882 — that PR's TLR private-ref
guard prevents the inner-rec from being lifted to a module static under
--realsig+, so the fallback path (this closure) is what trips here.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    New

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions