Summary
Accessing a C# protected base field from a derived F# member compiles fine, but the optimizer inlines the field load into a non-family method, producing illegal IL that throws System.FieldAccessException at runtime. This happens under --optimize+, which is the default for fsc (Release builds). No closure is required.
Repro
C# library cslib:
public class Base { protected string Field = "hello"; }
F#:
type Inherited() =
inherit Base()
member x.Run() = x.Field // legal: protected field is accessible in a derived member
[<EntryPoint>]
let main _ =
if (Inherited()).Run() = "hello" then 0 else 1
fsc --optimize+ -r:cslib.dll Program.fs # default for Release
Actual
Unhandled exception. System.FieldAccessException:
Attempt by method 'Program.main(System.String[])' to access field 'Base.Field' failed.
at Program.main(String[] _arg1)
| flag |
result |
--optimize- |
OK |
--optimize+ (default) |
FieldAccessException |
Reproduces under both --realsig+ and --realsig-.
Cause
The optimizer inlines Run's body into main, copying ldfld string [cslib]Base::Field into main. Run is on Inherited (has family access to Base); main is not in Base's family, so the relocated ldfld is illegal and the CLR rejects it.
The optimizer already refuses to relocate protected access — but only for method/base calls, not field loads. The free-variable flag UsesMethodLocalConstructs is set for protected TOp.ILCall (TypedTreeOps.Remapping.fs:1221) and consumed by the relocation guards (Optimizer.fs:3097/3896/3978/4171), but a protected field load never sets it (TOp.ValFieldGet/ILAsm paths). Contrast — the method form is safe under the same flags:
member x.Run() = x.Method() // protected method --optimize+ -> OK
member x.Run() = x.Field // protected field --optimize+ -> FieldAccessException
Fix direction
Make a family/protected ldfld/ldflda/stfld set UsesMethodLocalConstructs, so the four existing optimizer relocation guards pin it in-family exactly as they already do for protected method calls. Near-zero baseline churn.
Related
Same root container-placement family as #5302 (protected base access from a closure); fixing this flag is a prerequisite for any closure-based enablement there.
Summary
Accessing a C#
protectedbase field from a derived F# member compiles fine, but the optimizer inlines the field load into a non-family method, producing illegal IL that throwsSystem.FieldAccessExceptionat runtime. This happens under--optimize+, which is the default forfsc(Release builds). No closure is required.Repro
C# library
cslib:F#:
Actual
--optimize---optimize+(default)Reproduces under both
--realsig+and--realsig-.Cause
The optimizer inlines
Run's body intomain, copyingldfld string [cslib]Base::Fieldintomain.Runis onInherited(has family access toBase);mainis not inBase's family, so the relocatedldfldis illegal and the CLR rejects it.The optimizer already refuses to relocate protected access — but only for method/base calls, not field loads. The free-variable flag
UsesMethodLocalConstructsis set for protectedTOp.ILCall(TypedTreeOps.Remapping.fs:1221) and consumed by the relocation guards (Optimizer.fs:3097/3896/3978/4171), but a protected field load never sets it (TOp.ValFieldGet/ILAsmpaths). Contrast — the method form is safe under the same flags:Fix direction
Make a family/protected
ldfld/ldflda/stfldsetUsesMethodLocalConstructs, so the four existing optimizer relocation guards pin it in-family exactly as they already do for protected method calls. Near-zero baseline churn.Related
Same root container-placement family as #5302 (protected base access from a closure); fixing this flag is a prerequisite for any closure-based enablement there.