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.
Closures generated for an inner
let recinside a method defined in atype C with member ...augmentation are emitted as a sibling ofCinsidethe module class, not as a nested type of
C. Under--realsig+, a sourceprivatemember ofCbecomes ILprivate(type-scoped), so the siblingclosure cannot reach it and the CLR raises
MethodAccessExceptionat firstinvocation. Under
--realsig-the same private compiles to ILassembly,which is reachable from the sibling, so the bug is invisible.
Repro
IL shape (
--realsig+, shortened)Moving
Run()from the augmentation into the primary type body emitsh@8<T>asclass nested ... Holder1/h@8(nested insideHolder`) and theprogram 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-classplacement uses the module's
clocrather than the augmented type'sclocin
IlxGen.fs. Tangentially related to #19882 — that PR's TLR private-refguard prevents the inner-rec from being lifted to a module static under
--realsig+, so the fallback path (this closure) is what trips here.