Skip to content

Fix FieldAccessException when optimizer relocates protected base-field access (#19963)#19964

Merged
T-Gro merged 6 commits into
mainfrom
fix/19963-optimizer-protected-field
Jun 23, 2026
Merged

Fix FieldAccessException when optimizer relocates protected base-field access (#19963)#19964
T-Gro merged 6 commits into
mainfrom
fix/19963-optimizer-protected-field

Conversation

@T-Gro

@T-Gro T-Gro commented Jun 17, 2026

Copy link
Copy Markdown
Member

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 relocated ldfld/stfld is then illegal IL and throws System.FieldAccessException at runtime. This happens under --optimize+, the default for Release builds, regardless of --realsig.

// C# library
public class Base { protected string Field = "hello"; }
type Inherited() =
    inherit Base()
    member x.Run() = x.Field          // legal; compiles today

[<EntryPoint>]
let main _ = if (Inherited()).Run() = "hello" then 0 else 1

--optimize- runs fine; --optimize+ inlines Run into main (which is not in Base's family) and throws:

System.FieldAccessException: Attempt by method 'Program.main(...)' to access field 'Base.Field' failed.

Cause

Protected method/base calls are already pinned in-family: they set FreeVars.UsesMethodLocalConstructs (via TOp.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+:

member x.Run() = x.Method()   // protected method  -> OK
member x.Run() = x.Field      // protected field   -> FieldAccessException

Fix

Add a FreeVars.ContainsILFieldAccess flag, set for any IL field instruction (TOp.ILAsm ldfld/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.Zero in 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 use TOp.ValFieldGet and are unaffected.

Validation

  • New regression tests for instance and static protected base fields under --optimize+ (red before, green after).
  • Full EmittedIL suite: 1307 tests, zero baseline churn.
  • Accessibility and object-expression suites unaffected.

This is also a prerequisite for enabling protected-member access from closures (#5302): the same relocation otherwise reintroduces the runtime failure there.

…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>
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/11.0.100.md

@github-actions github-actions Bot added the AI-Tooling-Check-Bypassed Tooling check: non-fork PR, not diff-analyzed label Jun 17, 2026

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/Compiler/TypedTree/TypedTreeOps.Remapping.fs
Comment thread src/Compiler/Optimize/Optimizer.fs Outdated
Comment thread src/Compiler/Optimize/Optimizer.fs Outdated
Comment thread src/Compiler/Optimize/Optimizer.fs Outdated
@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Jun 18, 2026
…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>
@T-Gro T-Gro requested a review from abonie June 18, 2026 11:08
T-Gro and others added 2 commits June 18, 2026 13:56
…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>
@T-Gro T-Gro enabled auto-merge (squash) June 22, 2026 11:23
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>
@T-Gro T-Gro merged commit 3b7d2ac into main Jun 23, 2026
50 checks passed
@github-project-automation github-project-automation Bot moved this from New to In Progress in F# Compiler and Tooling 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-reviewed PR reviewed by AI review council AI-Tooling-Check-Bypassed Tooling check: non-fork PR, not diff-analyzed

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Optimizer inlines protected base-field access into a non-family method -> FieldAccessException (--optimize+)

2 participants