Fix FieldAccessException when optimizer relocates protected base-field access (#19963)#19964
Merged
Conversation
…d access (#19963) The optimizer inlines/splits a member that reads 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+), copying the ldfld/stfld there and producing illegal IL that throws FieldAccessException at runtime. Protected *method*/base calls were already pinned via FreeVars.UsesMethodLocalConstructs (set for TOp.ILCall isProtected) and the optimizer's relocation guards. Field loads were invisible to that machinery. Add a FreeVars.ContainsILFieldAccess flag set for any IL field instruction (TOp.ILAsm ldfld/stfld/...) and OR it into the five optimizer relocation guards. It is deliberately NOT read by the FS0405 escape check, so it can never reject otherwise-valid code; field accessibility is unavailable where free vars are summarised, so the flag is conservative over all IL fields (F# fields use TOp.ValFieldGet and are unaffected). Measured zero EmittedIL baseline churn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
|
T-Gro
commented
Jun 18, 2026
T-Gro
left a comment
Member
Author
There was a problem hiding this comment.
AI code review (expert-reviewer). The committed design fixes the crash but has an over-broad relocation guard that will block inlining of public IL field accesses - a regression that manifests as FS1118 when bootstrapping FSharp.Core.
…ly) fields only The #19963 fix flagged every IL field access as non-relocatable, so the optimizer refused to inline `inline` values reading public IL fields - e.g. FSharp.Core's `inline GetStringSlice` (which reads String.Empty) - failing the FSharp.Core bootstrap with FS1118 across all build legs. Refine the five optimizer relocation guards to pin only protected/family IL field access: keep the cheap FreeVars.ContainsILFieldAccess gate, then resolve accessibility via ImportILTypeRef/ILFieldDef.Access in the optimizer (where the import map is available) through a single usesMethodLocalConstructsOrProtectedField helper. Public IL field access is freely optimized again, matching the existing protected-method treatment. Adds an (|ILFieldInstr|_|) active pattern and a positive regression test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ents isProtectedILFieldSpec is now a flat pattern match over two reusable active patterns instead of nested if/extract: (|TryImportILTypeRef|_|) in import.fs (CanImportILTypeRef + ImportILTypeRef) and (|ILTyconRawMetadata|_|) in TypedTreeBasics.fs (IsILTycon + ILTyconRawMetadata, a widespread idiom). Also collapse the repeated #19963 rationale to a single place. No behaviour change (build 0/0, 3/3 tests, repros unchanged). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Collapse the repeated regression rationale to 1-2 lines per test and share the single BaseClass C# lib across the two protected-field tests instead of redefining it. Test-only; 3/3 still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
No code change. The prior run's FSharpPlus leg failed with 'job was abandoned / lost contact with the agent' (AzDO infra flake) while the build was succeeding (0 errors). Empty commit to request a fresh run. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
abonie
approved these changes
Jun 23, 2026
This was referenced Jun 23, 2026
T-Gro
added a commit
that referenced
this pull request
Jun 25, 2026
Resolved conflicts: - tests/AheadOfTime/Trimming/check.ps1: kept main's expected size (will re-measure after build) - 28 EmittedIL .bsl baselines: took PR side; all baselines to be regenerated against the merged compiler IlxGen.fs / Optimizer.fs auto-merged; verified main's #19955 (augmentation-member closure nesting) and #19964 (optimizer protected-field relocation) both present and composing with the determinism codegen changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #19963.
Problem
Reading a
protected(family) base-class field from a derived F# member compiles fine, but the optimizer can relocate that field load — by inlining or method/lambda splitting — into a method outside the field's family. The relocatedldfld/stfldis then illegal IL and throwsSystem.FieldAccessExceptionat runtime. This happens under--optimize+, the default for Release builds, regardless of--realsig.--optimize-runs fine;--optimize+inlinesRunintomain(which is not inBase's family) and throws:Cause
Protected method/base calls are already pinned in-family: they set
FreeVars.UsesMethodLocalConstructs(viaTOp.ILCall isProtected), which the optimizer's relocation guards honour. Protected field loads were invisible to that machinery, so the optimizer freely relocated them. Contrast under--optimize+:Fix
Add a
FreeVars.ContainsILFieldAccessflag, set for any IL field instruction (TOp.ILAsmldfld/ldflda/stfld/ldsfld/ldsflda/stsfld), and OR it into the five optimizer relocation guards so such code is not inlined or split out of its declaring family.The flag is deliberately not read by the FS0405 escape check, so it can never reject otherwise-valid code (e.g. capturing a public field like
IntPtr.Zeroin a closure). Field accessibility is not available where free variables are summarised, so the flag is conservative over all IL field access; F#-defined fields useTOp.ValFieldGetand are unaffected.Validation
--optimize+(red before, green after).EmittedILsuite: 1307 tests, zero baseline churn.This is also a prerequisite for enabling protected-member access from closures (#5302): the same relocation otherwise reintroduces the runtime failure there.