Skip to content

Commit 3b7d2ac

Browse files
authored
Fix FieldAccessException when optimizer relocates protected base-field access (#19963) (#19964)
* Fix FieldAccessException when optimizer relocates protected base-field access (#19963)
1 parent ee7de00 commit 3b7d2ac

13 files changed

Lines changed: 183 additions & 13 deletions

File tree

docs/release-notes/.FSharp.Compiler.Service/11.0.100.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Fix missing FS1182 ("unused binding") warning for unused `let` function bindings inside class types. ([Issue #13849](https://github.com/dotnet/fsharp/issues/13849), [PR #19805](https://github.com/dotnet/fsharp/pull/19805))
66
* Fix inner mutually-recursive `let rec ... and ...` functions under `--realsig+` not being lifted to top-level static methods (TLR), causing `FSharpFunc` closure allocations and loss of `tail.` opcodes — the large struct-mutual-recursion perf regression reported in [Issue #17607](https://github.com/dotnet/fsharp/issues/17607). ([PR #19882](https://github.com/dotnet/fsharp/pull/19882))
77
* Fix `TypeLoadException` ("Specialize tried to implicitly override a method with weaker type parameter constraints") and the related CLR crash with constrained inline calls by stripping constraints from closure-class typars in `EraseClosures.convIlxClosureDef`. ([Issue #14492](https://github.com/dotnet/fsharp/issues/14492), [Issue #19075](https://github.com/dotnet/fsharp/issues/19075), [PR #19882](https://github.com/dotnet/fsharp/pull/19882))
8+
* Fix `FieldAccessException` at runtime when the optimizer relocates a read of a `protected` (family) base-class field into a method outside the field's family (e.g. a trivial member inlined into module/startup code under `--optimize+`). Protected (family) IL field access is no longer hoisted out of its declaring family by inlining or method-splitting. ([Issue #19963](https://github.com/dotnet/fsharp/issues/19963), [PR #19964](https://github.com/dotnet/fsharp/pull/19964))
89

910
* Suppress hover/symbol resolution for wildcard `_` patterns inside `member _.…` bodies that incorrectly showed `val _: T` tooltip. ([PR #19760](https://github.com/dotnet/fsharp/pull/19760))
1011
* Deduplicate format specifier locations in computation expressions so editor tooling no longer reports duplicate entries for the same `%` specifier. ([Issue #16419](https://github.com/dotnet/fsharp/issues/16419), [PR #19791](https://github.com/dotnet/fsharp/pull/19791))

src/Compiler/AbstractIL/il.fs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3980,6 +3980,18 @@ let mkNormalLdfld fspec = I_ldfld(Aligned, Nonvolatile, fspec)
39803980

39813981
let mkNormalLdflda fspec = I_ldflda fspec
39823982

3983+
/// Matches an IL instruction that loads or stores a field, returning the referenced field spec.
3984+
[<return: Struct>]
3985+
let (|ILFieldInstr|_|) instr =
3986+
match instr with
3987+
| I_ldsfld(_, fspec)
3988+
| I_ldfld(_, _, fspec)
3989+
| I_ldsflda fspec
3990+
| I_ldflda fspec
3991+
| I_stsfld(_, fspec)
3992+
| I_stfld(_, _, fspec) -> ValueSome fspec
3993+
| _ -> ValueNone
3994+
39833995
let mkNormalLdobj dt = I_ldobj(Aligned, Nonvolatile, dt)
39843996

39853997
let mkNormalStobj dt = I_stobj(Aligned, Nonvolatile, dt)

src/Compiler/AbstractIL/il.fsi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2144,6 +2144,11 @@ val internal mkNormalStsfld: ILFieldSpec -> ILInstr
21442144
val internal mkNormalLdsfld: ILFieldSpec -> ILInstr
21452145
val internal mkNormalLdfld: ILFieldSpec -> ILInstr
21462146
val internal mkNormalLdflda: ILFieldSpec -> ILInstr
2147+
2148+
/// Matches an IL instruction that loads or stores a field, returning the referenced field spec.
2149+
[<return: Struct>]
2150+
val internal (|ILFieldInstr|_|): instr: ILInstr -> ILFieldSpec voption
2151+
21472152
val internal mkNormalLdobj: ILType -> ILInstr
21482153
val internal mkNormalStobj: ILType -> ILInstr
21492154
val internal mkLdcInt32: int32 -> ILInstr

src/Compiler/Checking/import.fs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@ let ImportILTypeRef (env: ImportMap) m (tref: ILTypeRef) =
159159
let CanImportILTypeRef (env: ImportMap) m (tref: ILTypeRef) =
160160
env.ILTypeRefToTyconRefCache.ContainsKey(tref) || CanImportILScopeRef env m tref.Scope
161161

162+
/// Imports a reference to a type definition (ILTypeRef) as a TyconRef when it can be imported.
163+
[<return: Struct>]
164+
let (|TryImportILTypeRef|_|) (env: ImportMap) m (tref: ILTypeRef) =
165+
if CanImportILTypeRef env m tref then
166+
ValueSome(ImportILTypeRef env m tref)
167+
else
168+
ValueNone
169+
162170
/// Import a type, given an AbstractIL ILTypeRef and an F# type instantiation.
163171
///
164172
/// Prefer the F# abbreviation for some built-in types, e.g. 'string' rather than

src/Compiler/Checking/import.fsi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ val internal ImportILTypeRef: ImportMap -> range -> ILTypeRef -> TyconRef
7777
/// Pre-check for ability to import a reference to a type definition, given an AbstractIL ILTypeRef, with caching
7878
val internal CanImportILTypeRef: ImportMap -> range -> ILTypeRef -> bool
7979

80+
/// Imports a reference to a type definition (ILTypeRef) as a TyconRef when it can be imported.
81+
[<return: Struct>]
82+
val internal (|TryImportILTypeRef|_|): ImportMap -> range -> ILTypeRef -> TyconRef voption
83+
8084
/// Import an IL type as an F# type.
8185
val internal ImportILType: ImportMap -> range -> TType list -> ILType -> TType
8286

src/Compiler/Optimize/Optimizer.fs

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,8 +1415,43 @@ let AbstractOptimizationInfoToEssentials =
14151415

14161416
abstractLazyModulInfo
14171417

1418+
/// True if the IL field has protected (family) accessibility.
1419+
let private isProtectedILFieldSpec cenv m (fspec: ILFieldSpec) =
1420+
match fspec.DeclaringTypeRef with
1421+
| Import.TryImportILTypeRef cenv.amap m (ILTyconRawMetadata tdef) ->
1422+
tdef.Fields.LookupByName fspec.Name
1423+
|> List.exists (fun fdef -> fdef.Access = ILMemberAccess.Family || fdef.Access = ILMemberAccess.FamilyOrAssembly)
1424+
| _ -> false
1425+
1426+
/// True if the expression loads or stores a protected (family) IL field anywhere in its body.
1427+
let private exprReferencesProtectedILField cenv expr =
1428+
let mutable found = false
1429+
1430+
let folder =
1431+
{ ExprFolder0 with
1432+
exprIntercept =
1433+
fun _recurseF noInterceptF z e ->
1434+
if not found then
1435+
match e with
1436+
| Expr.Op(TOp.ILAsm(instrs, _), _, _, m) ->
1437+
if instrs |> List.exists (function ILFieldInstr fspec -> isProtectedILFieldSpec cenv m fspec | _ -> false) then
1438+
found <- true
1439+
| _ -> ()
1440+
1441+
noInterceptF z e }
1442+
1443+
FoldExpr folder () expr |> ignore
1444+
found
1445+
1446+
/// True if the expression references constructs that are only valid within their defining method or
1447+
/// family, and so must not be relocated by inlining or method-splitting: a protected/base call
1448+
/// (UsesMethodLocalConstructs) or a protected (family) IL field access (issue #19963).
1449+
let usesMethodLocalConstructsOrProtectedField cenv (fvs: FreeVars) expr =
1450+
fvs.UsesMethodLocalConstructs
1451+
|| (fvs.ContainsILFieldAccess && exprReferencesProtectedILField cenv expr)
1452+
14181453
/// Hide information because of a "let ... in ..." or "let rec ... in ... "
1419-
let AbstractExprInfoByVars (boundVars: Val list, boundTyVars) ivalue =
1454+
let AbstractExprInfoByVars cenv (boundVars: Val list, boundTyVars) ivalue =
14201455
// Module and member bindings can be skipped when checking abstraction, since abstraction of these values has already been done when
14211456
// we hit the end of the module and called AbstractLazyModulInfoByHiding. If we don't skip these then we end up quadratically retraversing
14221457
// the inferred optimization data, i.e. at each binding all the way up a sequences of 'lets' in a module.
@@ -1447,7 +1482,7 @@ let AbstractExprInfoByVars (boundVars: Val list, boundTyVars) ivalue =
14471482
(let fvs = freeInExpr (if isNil boundTyVars then CollectLocalsWithStackGuard() else CollectTyparsAndLocals) expr
14481483
(not (isNil boundVars) && List.exists (Zset.memberOf fvs.FreeLocals) boundVars) ||
14491484
(not (isNil boundTyVars) && List.exists (Zset.memberOf fvs.FreeTyvars.FreeTypars) boundTyVars) ||
1450-
fvs.UsesMethodLocalConstructs) ->
1485+
usesMethodLocalConstructsOrProtectedField cenv fvs expr) ->
14511486

14521487
// Trimming lambda
14531488
UnknownValue
@@ -2851,7 +2886,7 @@ and OptimizeLetRec cenv env (binds, bodyExpr, m) =
28512886
let fvs = List.fold (fun acc x -> unionFreeVars acc (fst x |> freeInBindingRhs CollectLocals)) fvs0 bindsR
28522887
SplitValuesByIsUsedOrHasEffect cenv (fun () -> fvs.FreeLocals) bindsR
28532888
// Trim out any optimization info that involves escaping values
2854-
let evalueR = AbstractExprInfoByVars (vs, []) einfo.Info
2889+
let evalueR = AbstractExprInfoByVars cenv (vs, []) einfo.Info
28552890
// REVIEW: size of constructing new closures - should probably add #freevars + #recfixups here
28562891
let bodyExprR = Expr.LetRec (bindsRR, bodyExprR, m, Construct.NewFreeVarsCache())
28572892
let info = CombineValueInfos (einfo :: bindinfos) evalueR
@@ -2926,7 +2961,7 @@ and OptimizeLinearExpr cenv env expr contf =
29262961
Info = UnknownValue }
29272962
else
29282963
// On the way back up: Trim out any optimization info that involves escaping values on the way back up
2929-
let evalueR = AbstractExprInfoByVars ([bindR.Var], []) bodyInfo.Info
2964+
let evalueR = AbstractExprInfoByVars cenv ([bindR.Var], []) bodyInfo.Info
29302965

29312966
// Preserve the debug points for eliminated bindings that have debug points.
29322967
let bodyR =
@@ -3094,8 +3129,7 @@ and TryOptimizeVal cenv env (vOpt: ValRef option, shouldInline, inlineIfLambda,
30943129

30953130
| CurriedLambdaValue (_, _, _, expr, _) when shouldInline || inlineIfLambda ->
30963131
let fvs = freeInExpr CollectLocals expr
3097-
if fvs.UsesMethodLocalConstructs then
3098-
// Discarding lambda for binding because uses protected members --- TBD: Should we warn or error here
3132+
if usesMethodLocalConstructsOrProtectedField cenv fvs expr then
30993133
None
31003134
else
31013135
let exprCopy = CopyExprForInlining cenv inlineIfLambda expr m
@@ -3893,7 +3927,7 @@ and OptimizeLambdas (vspec: Val option) cenv env valReprInfo expr exprTy =
38933927
| None -> CurriedLambdaValue (lambdaId, arities, bsize, exprR, exprTy)
38943928
| Some baseVal ->
38953929
let fvs = freeInExpr CollectLocals bodyR
3896-
if fvs.UsesMethodLocalConstructs || fvs.FreeLocals.Contains baseVal then
3930+
if usesMethodLocalConstructsOrProtectedField cenv fvs bodyR || fvs.FreeLocals.Contains baseVal then
38973931
UnknownValue
38983932
else
38993933
let expr2 = mkMemberLambdas g m tps ctorThisValOpt None vsl (bodyR, bodyTy)
@@ -3975,7 +4009,7 @@ and ComputeSplitToMethodCondition flag threshold cenv env (e: Expr, einfo) =
39754009
let m = e.Range
39764010
(let fvs = freeInExpr (CollectLocalsWithStackGuard()) e
39774011
not fvs.UsesUnboundRethrow &&
3978-
not fvs.UsesMethodLocalConstructs &&
4012+
not (usesMethodLocalConstructsOrProtectedField cenv fvs e) &&
39794013
fvs.FreeLocals |> Zset.forall (fun v ->
39804014
// no direct-self-recursive references
39814015
not (env.dontSplitVars.ContainsVal v) &&
@@ -4037,7 +4071,7 @@ and OptimizeDecisionTreeTarget cenv env _m (TTarget(vs, expr, flags)) =
40374071
let env = BindInternalValsToUnknown cenv vs env
40384072
let exprR, einfo = OptimizeExpr cenv env expr
40394073
let exprR, einfo = ConsiderSplitToMethod cenv.settings.abstractBigTargets cenv.settings.bigTargetSize cenv env (exprR, einfo)
4040-
let evalueR = AbstractExprInfoByVars (vs, []) einfo.Info
4074+
let evalueR = AbstractExprInfoByVars cenv (vs, []) einfo.Info
40414075
TTarget(vs, exprR, flags),
40424076
{ TotalSize=einfo.TotalSize
40434077
FunctionSize=einfo.FunctionSize
@@ -4168,8 +4202,7 @@ and OptimizeBinding cenv isRec env (TBind(vref, expr, spBind)) =
41684202
UnknownValue
41694203
else
41704204
let fvs = freeInExpr CollectLocals body
4171-
if fvs.UsesMethodLocalConstructs then
4172-
// Discarding lambda for binding because uses protected members
4205+
if usesMethodLocalConstructsOrProtectedField cenv fvs body then
41734206
UnknownValue
41744207
elif fvs.FreeLocals.ToArray() |> Seq.fold(fun acc v -> if not acc then v.Accessibility.IsPrivate else acc) false then
41754208
// Discarding lambda for binding because uses private members

src/Compiler/TypedTree/TypedTree.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6086,6 +6086,10 @@ type FreeVars =
60866086
/// Rethrow may only occur in such locations.
60876087
UsesUnboundRethrow: bool
60886088

6089+
/// Indicates if the expression contains a direct IL field load/store — a cheap over-approximate
6090+
/// gate the optimizer refines to protected (family) fields (issue #19963). Never read by escape checks.
6091+
ContainsILFieldAccess: bool
6092+
60896093
/// The summary of locally defined tycon representations used in the expression. These may be made private by a signature
60906094
/// or marked 'internal' or 'private' and we have to check various conditions associated with that.
60916095
FreeLocalTyconReprs: FreeTycons

src/Compiler/TypedTree/TypedTree.fsi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4411,6 +4411,10 @@ type FreeVars =
44114411
/// Rethrow may only occur in such locations.
44124412
UsesUnboundRethrow: bool
44134413
4414+
/// Indicates if the expression contains a direct IL field load/store — a cheap over-approximate
4415+
/// gate the optimizer refines to protected (family) fields (issue #19963). Never read by escape checks.
4416+
ContainsILFieldAccess: bool
4417+
44144418
/// The summary of locally defined tycon representations used in the expression. These may be made private by a signature
44154419
/// or marked 'internal' or 'private' type we have to check various conditions associated with that.
44164420
FreeLocalTyconReprs: FreeTycons

src/Compiler/TypedTree/TypedTreeBasics.fs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,11 @@ let (|AbbrevOrAppTy|_|) (ty: TType) =
342342
| TType_app (tcref, tinst, _) -> ValueSome(tcref, tinst)
343343
| _ -> ValueNone
344344

345+
/// Matches a type definition reference backed by Abstract IL metadata, returning that metadata.
346+
[<return: Struct>]
347+
let (|ILTyconRawMetadata|_|) (tcref: TyconRef) =
348+
if tcref.IsILTycon then ValueSome tcref.ILTyconRawMetadata else ValueNone
349+
345350
//---------------------------------------------------------------------------
346351
// These make local/non-local references to values according to whether
347352
// the item is globally stable ("published") or not.

src/Compiler/TypedTree/TypedTreeBasics.fsi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
module internal FSharp.Compiler.TypedTreeBasics
88

99
open Internal.Utilities.Library.Extras
10+
open FSharp.Compiler.AbstractIL.IL
1011
open FSharp.Compiler.Syntax
1112
open FSharp.Compiler.Text
1213
open FSharp.Compiler.TypedTree
@@ -155,6 +156,10 @@ val stripUnitEqns: unt: Measure -> Measure
155156
[<return: Struct>]
156157
val (|AbbrevOrAppTy|_|): ty: TType -> (TyconRef * TypeInst) voption
157158

159+
/// Matches a type definition reference backed by Abstract IL metadata, returning that metadata.
160+
[<return: Struct>]
161+
val (|ILTyconRawMetadata|_|): tcref: TyconRef -> ILTypeDef voption
162+
158163
val mkLocalValRef: v: Val -> ValRef
159164

160165
val mkLocalModuleRef: v: ModuleOrNamespace -> EntityRef

0 commit comments

Comments
 (0)