Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6a4733e
Initial plan
Copilot Nov 11, 2025
b8e8f28
Fix object expressions in struct types generating invalid IL with byr…
Copilot Nov 11, 2025
0616707
Add IsInstanceMember check to only transform instance members of structs
Copilot Nov 11, 2025
de89a91
Add interface check to prevent transformation for pure interface impl…
Copilot Nov 11, 2025
086f23f
Refactor struct object expression fix to CheckExpressionsOps with cle…
Copilot Nov 11, 2025
de69a3c
Fix type inference issue by removing explicit generic type annotation
Copilot Nov 11, 2025
7b92bd5
Temporarily disable struct member extraction transformation
Copilot Dec 31, 2025
e76bb66
Implement proper context detection using env.eFamilyType
Copilot Dec 31, 2025
3d761aa
Fix TcGlobals type reference to use fully qualified name
Copilot Dec 31, 2025
dda20a5
Merge branch 'main' into copilot/fix-invalid-il-byref-fields
T-Gro Jan 2, 2026
8f033f7
Apply patch from /run fantomas
actions-user Jan 2, 2026
068c876
Fix test code and remove incorrect baseValOpt check
Copilot Jan 2, 2026
a4bd40a
Remove unused baseValOpt parameter
Copilot Jan 2, 2026
67ee4fd
Apply patch from /run fantomas
actions-user Jan 2, 2026
9e01e21
Remove IsInstanceMember filter to capture all struct values
Copilot Jan 2, 2026
192f5cc
Fix context detection by analyzing free variables instead of env.eFam…
Copilot Jan 2, 2026
1660987
Broaden filter to include variables without DeclaringEntity
Copilot Jan 2, 2026
782c5db
Add ctorCall remapping and clean up debug output
Copilot Jan 2, 2026
51a0aa0
Fix syntax error - remove incomplete debug code
Copilot Jan 2, 2026
9150cff
Fix false positive byref errors by adding proper context guards
Copilot Jan 5, 2026
44092f4
Fix IsParameter error - use IsMemberOrModuleBinding check instead
Copilot Jan 5, 2026
e75f57b
Fix filter to capture struct constructor parameters correctly
Copilot Jan 5, 2026
80b0fb7
Fix enclosingStructTyconRefOpt detection - capture BEFORE EnterFamily…
Copilot Jan 6, 2026
689feb2
Simplify filter to capture struct constructor parameters correctly
Copilot Jan 6, 2026
7623be8
Remove interface-only guard - interfaces also need transformation
Copilot Jan 6, 2026
ba60ec5
Remove debug logging - all tests passing
Copilot Jan 6, 2026
ab9e0f5
Remove unused isInterfaceTy parameter
Copilot Jan 6, 2026
d1cc465
Fix false positive in static members - only transform instance member…
Copilot Jan 7, 2026
e985923
Revert object expression transformation to fix seqcore.fs regression
Copilot Jan 7, 2026
dd6a0c1
REVERT: Remove all object expression transformation code
Copilot Jan 7, 2026
fc8bfdc
Add Hypothesis 11: Confirm tests properly detect the bug
Copilot Jan 7, 2026
27509e5
fix
T-Gro Jan 16, 2026
06b1cee
Move the fix
T-Gro Jan 19, 2026
15097a0
Merge branch 'main' into copilot/fix-invalid-il-byref-fields
T-Gro Jan 19, 2026
1959e5a
Apply patch from /run fantomas
actions-user Jan 19, 2026
e0be5dc
Merge branch 'main' into copilot/fix-invalid-il-byref-fields
T-Gro Jan 19, 2026
1c5a092
Add release notes for PR #19070
Copilot Jan 19, 2026
7061219
Delete HYPOTHESIS.md
T-Gro Jan 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* Type relations cache: handle unsolved type variables ([Issue #19037](https://github.com/dotnet/fsharp/issues/19037)) ([PR #19040](https://github.com/dotnet/fsharp/pull/19040))
* Fix insertion context for modules with multiline attributes. ([Issue #18671](https://github.com/dotnet/fsharp/issues/18671))
* Fix `--typecheck-only` for scripts stopping after processing `#load`-ed script ([PR #19048](https://github.com/dotnet/fsharp/pull/19048))
* Fix object expressions in struct types generating invalid IL with byref fields causing TypeLoadException at runtime. ([Issue #19068](https://github.com/dotnet/fsharp/issues/19068), [PR #19070](https://github.com/dotnet/fsharp/pull/19070))

### Added

Expand Down
23 changes: 21 additions & 2 deletions src/Compiler/Checking/Expressions/CheckExpressions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7236,6 +7236,13 @@ and TcObjectExpr (cenv: cenv) env tpenv (objTy, realObjTy, argopt, binds, extraI
// Add the object type to the ungeneralizable items
let env = {env with eUngeneralizableItems = addFreeItemOfTy objTy env.eUngeneralizableItems }

// Save the enclosing struct context BEFORE EnterFamilyRegion overwrites env.eFamilyType.
// This is used later to detect struct instance captures that would generate illegal byref fields.
let enclosingStructTyconRefOpt =
match env.eFamilyType with
| Some tcref when tcref.IsStructOrEnumTycon -> Some tcref
| _ -> None

// Object expression members can access protected members of the implemented type
let env = EnterFamilyRegion tcref env
let ad = env.AccessRights
Expand Down Expand Up @@ -7344,8 +7351,20 @@ and TcObjectExpr (cenv: cenv) env tpenv (objTy, realObjTy, argopt, binds, extraI
errorR (Error(FSComp.SR.tcInvalidObjectExpressionSyntaxForm (), mWholeExpr))

// 4. Build the implementation
let expr = mkObjExpr(objtyR, baseValOpt, ctorCall, overrides', extraImpls, mWholeExpr)
let expr = mkCoerceIfNeeded g realObjTy objtyR expr
// Check for struct instance captures that would generate illegal byref fields.
// See AnalyzeObjExprStructCaptures and TransformObjExprForStructByrefCaptures for details.
let shouldTransform, structCaptures, _ =
AnalyzeObjExprStructCaptures enclosingStructTyconRefOpt ctorCall overrides' extraImpls

let expr =
if not shouldTransform then
// No transformation needed - build the object expression directly
let expr = mkObjExpr(objtyR, baseValOpt, ctorCall, overrides', extraImpls, mWholeExpr)
mkCoerceIfNeeded g realObjTy objtyR expr
else
// Transform to avoid byref captures
TransformObjExprForStructByrefCaptures g mWholeExpr structCaptures objtyR baseValOpt ctorCall overrides' extraImpls realObjTy

expr, tpenv

//-------------------------------------------------------------------------
Expand Down
126 changes: 126 additions & 0 deletions src/Compiler/Checking/Expressions/CheckExpressionsOps.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

module internal FSharp.Compiler.CheckExpressionsOps

open Internal.Utilities.Collections
open Internal.Utilities.Library
open Internal.Utilities.Library.Extras
open FSharp.Compiler.CheckBasics
Expand Down Expand Up @@ -389,3 +390,128 @@ let inline mkOptionalParamTyBasedOnAttribute (g: TcGlobals.TcGlobals) tyarg attr
mkValueOptionTy g tyarg
else
mkOptionTy g tyarg

//-------------------------------------------------------------------------
// Struct byref capture fix for object expressions
//-------------------------------------------------------------------------

/// When a struct instance method creates an object expression that captures constructor
/// parameters, those captures go through 'this' which is a byref. This would create
/// illegal byref fields in the closure class. This function detects such captures and
/// extracts them to local bindings before the object expression.
///
/// Returns: (shouldTransform, structCaptures, methodParamStamps)
let AnalyzeObjExprStructCaptures
(enclosingStructTyconRefOpt: TyconRef option)
(ctorCall: Expr)
(overrides: ObjExprMethod list)
(extraImpls: (TType * ObjExprMethod list) list)
: bool * Val list * Set<Stamp> =

// Collect free variables from an expression
let collectFreeVars expr =
(freeInExpr CollectLocals expr).FreeLocals |> Zset.elements

// Collect all method parameters (bound variables) from object expression methods
// These should NOT be treated as struct instance captures
let methodParams =
[
for TObjExprMethod(_, _, _, paramGroups, _, _) in overrides do
for paramGroup in paramGroups do
for v in paramGroup do
yield v
for (_, methods) in extraImpls do
for TObjExprMethod(_, _, _, paramGroups, _, _) in methods do
for paramGroup in paramGroups do
for v in paramGroup do
yield v
]
|> List.map (fun v -> v.Stamp)
|> Set.ofList

let allFreeVars =
[
yield! collectFreeVars ctorCall
for TObjExprMethod(_, _, _, _, body, _) in overrides do
yield! collectFreeVars body
for (_, methods) in extraImpls do
for TObjExprMethod(_, _, _, _, body, _) in methods do
yield! collectFreeVars body
]
|> List.distinctBy (fun v -> v.Stamp)

// Filter to struct instance captures:
// - We're in a struct context (enclosingStructTyconRefOpt is Some)
// - The value is NOT a method parameter of the object expression
// - The value is NOT a module binding
// - The value is NOT a member or module binding (excludes property getters, etc.)
// - The value is NOT a constructor
let structCaptures =
match enclosingStructTyconRefOpt with
| None -> []
| Some _ ->
allFreeVars
|> List.filter (fun v ->
not v.IsModuleBinding
&& not v.IsMemberOrModuleBinding
&& not (Set.contains v.Stamp methodParams)
&& v.LogicalName <> ".ctor")

let shouldTransform = not (List.isEmpty structCaptures)
(shouldTransform, structCaptures, methodParams)

/// Transform an object expression to avoid byref captures from struct instance state.
/// Creates local bindings for captured values and remaps references in the object expression.
let TransformObjExprForStructByrefCaptures
(g: TcGlobals.TcGlobals)
(mWholeExpr: Text.range)
(structCaptures: Val list)
(objtyR: TType)
(baseValOpt: Val option)
(ctorCall: Expr)
(overrides: ObjExprMethod list)
(extraImpls: (TType * ObjExprMethod list) list)
(realObjTy: TType)
: Expr =

// Create local bindings for each captured value to avoid byref captures
let localBindings =
structCaptures
|> List.map (fun v ->
let local, _localExpr =
mkCompGenLocal mWholeExpr (v.LogicalName + "$captured") v.Type

let readExpr = exprForVal mWholeExpr v
(v, local, readExpr))

// Build remap: original val -> local val
let remap =
localBindings
|> List.fold
(fun (r: Remap) (orig, local, _) ->
{ r with
valRemap = r.valRemap.Add orig (mkLocalValRef local)
})
Remap.Empty

// Helper to remap an object expression method
let remapMethod (TObjExprMethod(slotSig, attrs, mtps, paramGroups, body, range)) =
TObjExprMethod(slotSig, attrs, mtps, paramGroups, remapExpr g CloneAll remap body, range)

// Remap all parts of the object expression
let ctorCall' = remapExpr g CloneAll remap ctorCall
let overrides' = overrides |> List.map remapMethod

let extraImpls' =
extraImpls |> List.map (fun (ty, ms) -> (ty, ms |> List.map remapMethod))

// Build the object expression with remapped references
let objExpr =
mkObjExpr (objtyR, baseValOpt, ctorCall', overrides', extraImpls', mWholeExpr)

let objExpr = mkCoerceIfNeeded g realObjTy objtyR objExpr

// Wrap with let bindings: let x$captured = x in ...
localBindings
|> List.foldBack (fun (_, local, valueExpr) body -> mkLet DebugPointAtBinding.NoneAtInvisible mWholeExpr local valueExpr body)
<| objExpr
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
namespace FSharp.Compiler.ComponentTests.Conformance.Expressions

open Xunit
open FSharp.Test.Compiler

module StructObjectExpression =

[<Fact>]
let ``Object expression in struct should not generate byref field - simple case`` () =
Fsx """
type Class(test : obj) = class end

[<Struct; NoComparison>]
type Struct(test : obj) =
member _.Test() = {
new Class(test) with
member _.ToString() = ""
}

let s = Struct(42)
let obj = s.Test()
"""
|> withOptions [ "--nowarn:52" ] // Suppress struct copy warning
|> compileExeAndRun
|> shouldSucceed

[<Fact>]
let ``Object expression in struct with multiple fields`` () =
Fsx """
type Base(x: int, y: string) = class end

[<Struct; NoComparison>]
type MyStruct(x: int, y: string) =
member _.CreateObj() = {
new Base(x, y) with
member _.ToString() = y + string x
}

let s = MyStruct(42, "test")
let obj = s.CreateObj()
"""
|> withOptions [ "--nowarn:52" ]
|> compileExeAndRun
|> shouldSucceed

[<Fact>]
let ``Object expression in struct referencing field in override method`` () =
Fsx """
type IFoo =
abstract member DoSomething : unit -> int

[<Struct; NoComparison>]
type MyStruct(value: int) =
member _.CreateFoo() = {
new IFoo with
member _.DoSomething() = value * 2
}

let s = MyStruct(21)
let foo = s.CreateFoo()
let result = foo.DoSomething()
"""
|> withOptions [ "--nowarn:52" ]
|> compileExeAndRun
|> shouldSucceed

// Regression tests - these must continue to work

[<Fact>]
let ``Static member in struct with object expression should compile - StructBox regression`` () =
// This is the StructBox.Comparer pattern from FSharp.Core/seqcore.fs
// Static members don't have 'this' so should NOT be transformed
Fsx """
open System.Collections.Generic

[<Struct; NoComparison; NoEquality>]
type StructBox<'T when 'T: equality>(value: 'T) =
member x.Value = value

static member Comparer =
let gcomparer = HashIdentity.Structural<'T>
{ new IEqualityComparer<StructBox<'T>> with
member _.GetHashCode(v) = gcomparer.GetHashCode(v.Value)
member _.Equals(v1, v2) = gcomparer.Equals(v1.Value, v2.Value) }

let comparer = StructBox<int>.Comparer
let box1 = StructBox(42)
let box2 = StructBox(42)
let result = comparer.Equals(box1, box2)
if not result then failwith "Expected equal"
"""
|> compileExeAndRun
|> shouldSucceed

[<Fact>]
let ``Module level object expression with struct parameter should compile`` () =
// Module-level functions don't have instance context
Fsx """
[<Struct>]
type MyStruct(value: int) =
member x.Value = value

let createComparer () =
{ new System.Object() with
member _.ToString() = "comparer" }

let c = createComparer()
if c.ToString() <> "comparer" then failwith "Failed"
"""
|> compileExeAndRun
|> shouldSucceed

[<Fact>]
let ``Object expression in struct not capturing anything should compile`` () =
// Object expression that doesn't reference any struct state
Fsx """
[<Struct; NoComparison>]
type MyStruct(value: int) =
member _.CreateObj() = {
new System.Object() with
member _.ToString() = "constant"
}

let s = MyStruct(42)
let obj = s.CreateObj()
if obj.ToString() <> "constant" then failwith "Failed"
"""
|> withOptions [ "--nowarn:52" ]
|> compileExeAndRun
|> shouldSucceed

[<Fact>]
let ``Object expression in struct with method parameters should not confuse params with captures`` () =
// Method parameters are not instance captures, should not trigger transformation
Fsx """
[<Struct; NoComparison>]
type MyStruct(value: int) =
member _.Transform(multiplier: int) = {
new System.Object() with
member _.ToString() = string (value * multiplier)
}

let s = MyStruct(21)
let obj = s.Transform(2)
if obj.ToString() <> "42" then failwith "Expected 42"
"""
|> withOptions [ "--nowarn:52" ]
|> compileExeAndRun
|> shouldSucceed
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
<Compile Include="Conformance\Expressions\BindingExpressions\BindingExpressions.fs" />
<Compile Include="Conformance\Expressions\ComputationExpressions\ComputationExpressions.fs" />
<Compile Include="Conformance\Expressions\ObjectExpressions\ObjectExpressions.fs" />
<Compile Include="Conformance\Expressions\ObjectExpressions\StructObjectExpression.fs" />
<Compile Include="Conformance\Expressions\ControlFlowExpressions\PatternMatching\PatternMatching.fs" />
<Compile Include="Conformance\Expressions\ControlFlowExpressions\SequenceIteration\SequenceIteration.fs" />
<Compile Include="Conformance\Expressions\ControlFlowExpressions\Type-relatedExpressions\Type-relatedExpressions.fs" />
Expand Down
Loading