Skip to content
Draft
38 changes: 37 additions & 1 deletion src/Compiler/Checking/Expressions/CheckExpressions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7308,6 +7308,27 @@ and TcObjectExpr (cenv: cenv) env tpenv (objTy, realObjTy, argopt, binds, extraI
DispatchSlotChecking.CheckDispatchSlotsAreImplemented (env.DisplayEnv, cenv.infoReader, m, env.NameEnv, cenv.tcSink, isOverallTyAbstract, true, implTy, dispatchSlots, availPriorOverrides, overrideSpecs) |> ignore

// 3. create the specs of overrides

// Fix for struct object expressions: extract captured struct members to avoid byref fields
// This transformation is only applied when ALL of the following conditions are met:
// 1. The object expression derives from a base class (not just implementing an interface)
// 2. We're inside a struct instance member method (env.eFamilyType is a struct)
// 3. The object expression captures the struct's 'this' reference (baseValOpt)
// See CheckExpressionsOps.TryExtractStructMembersFromObjectExpr for implementation details
let enclosingStructTyconRefOpt =
match env.eFamilyType with
| Some tcref when tcref.IsStructOrEnumTycon -> Some tcref
| _ -> None

let capturedStructMembers, methodBodyRemap =
CheckExpressionsOps.TryExtractStructMembersFromObjectExpr
g
enclosingStructTyconRefOpt
isInterfaceTy
baseValOpt
overridesAndVirts
mWholeExpr

let allTypeImpls =
overridesAndVirts |> List.map (fun (m, implTy, _, dispatchSlotsKeyed, _, overrides) ->
let overrides' =
Expand All @@ -7331,7 +7352,14 @@ and TcObjectExpr (cenv: cenv) env tpenv (objTy, realObjTy, argopt, binds, extraI
| Some x -> x
| None -> error(Error(FSComp.SR.tcAtLeastOneOverrideIsInvalid(), mObjTy))

yield TObjExprMethod(overridden.GetSlotSig(cenv.amap, m), bindingAttribs, mtps, [thisVal] :: methodVars, bindingBody, id.idRange) ]
// Remap method body to use local copies of struct members
let bindingBody' =
if methodBodyRemap.valRemap.IsEmpty then
bindingBody
else
remapExpr g CloneAll methodBodyRemap bindingBody

yield TObjExprMethod(overridden.GetSlotSig(cenv.amap, m), bindingAttribs, mtps, [thisVal] :: methodVars, bindingBody', id.idRange) ]
(implTy, overrides'))

let objtyR, overrides' = allTypeImpls.Head
Expand All @@ -7345,6 +7373,14 @@ and TcObjectExpr (cenv: cenv) env tpenv (objTy, realObjTy, argopt, binds, extraI
// 4. Build the implementation
let expr = mkObjExpr(objtyR, baseValOpt, ctorCall, overrides', extraImpls, mWholeExpr)
let expr = mkCoerceIfNeeded g realObjTy objtyR expr

// Wrap with bindings for captured struct members
let expr =
if capturedStructMembers.IsEmpty then
expr
else
List.foldBack (fun (v, e) body -> mkInvisibleLet mWholeExpr v e body) capturedStructMembers expr

expr, tpenv

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

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

/// Extract captured struct instance members from object expressions to avoid illegal byref fields in closures.
/// When an object expression inside a struct instance member method captures struct fields, the generated
/// closure would contain a byref<Struct> field which violates CLI rules. This function extracts those struct
/// member values into local variables and rewrites the object expression methods to use the locals instead.
///
/// Returns: (capturedMemberBindings, methodBodyRemap) where:
/// - capturedMemberBindings: list of (localVar, valueExpr) pairs to prepend before the object expression
/// - methodBodyRemap: Remap to apply to object expression method bodies to use the captured locals
let TryExtractStructMembersFromObjectExpr
(g: TcGlobals.TcGlobals)
(enclosingStructTyconRefOpt: TyconRef option)
(isInterfaceTy: bool)
(baseValOpt: Val option)
overridesAndVirts
(mWholeExpr: range) : (Val * Expr) list * Remap =

// Only transform when:
// 1. Not a pure interface implementation
// 2. We're inside a struct instance member (eFamilyType is a struct)
// 3. There's a baseVal being captured
// 4. The baseVal's type matches the enclosing struct
match enclosingStructTyconRefOpt, baseValOpt with
| Some enclosingTcref, Some baseVal when not isInterfaceTy ->
match tryTcrefOfAppTy g baseVal.Type with
| ValueSome tcref when tyconRefEq g tcref enclosingTcref ->
// This is the real case: object expr inside struct method capturing 'this'
// Collect all method bodies from the object expression overrides
let allMethodBodies =
overridesAndVirts
|> List.collect (fun (_, _, _, _, _, overrides) ->
overrides |> List.map (fun (_, (_, _, _, _, bindingBody)) -> bindingBody))

// Early exit if no methods to analyze
if allMethodBodies.IsEmpty then
[], Remap.Empty
else
// Find all free variables in the method bodies
let freeVars =
allMethodBodies
|> List.fold (fun acc body ->
let bodyFreeVars = freeInExpr CollectTyparsAndLocals body
unionFreeVars acc bodyFreeVars) emptyFreeVars

// Filter to only instance members of the enclosing struct type
let structMembers =
freeVars.FreeLocals
|> Zset.elements
|> List.filter (fun (v: Val) ->
// Must be an instance member (not static)
v.IsInstanceMember &&
// Must have a declaring entity
v.HasDeclaringEntity &&
// The declaring entity must be the enclosing struct
tyconRefEq g v.DeclaringEntity enclosingTcref)

// Early exit if no struct members captured
if structMembers.IsEmpty then
[], Remap.Empty
else
// Create local variables for each captured struct member
let bindings =
structMembers
|> List.map (fun (memberVal: Val) ->
// Create a new local to hold the member's value
let localVal, _ = mkCompGenLocal mWholeExpr memberVal.DisplayName memberVal.Type
// The value expression is just a reference to the member
let valueExpr = exprForVal mWholeExpr memberVal
(memberVal, localVal, valueExpr))

// Build a remap from original member vals to new local vals
let remap =
bindings
|> List.fold (fun (remap: Remap) (origVal, localVal, _) ->
{ remap with valRemap = remap.valRemap.Add origVal (mkLocalValRef localVal) }) Remap.Empty

// Return the bindings to be added before the object expression
let bindPairs = bindings |> List.map (fun (_, localVal, valueExpr) -> (localVal, valueExpr))
bindPairs, remap
| _ -> [], Remap.Empty
| _ -> [], Remap.Empty
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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`` () =
FSharp """
type Class(test : obj) = class end

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

let s = Struct(42)
let obj = s.Test()
"""
|> compile
|> shouldSucceed

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

[<Struct>]
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()
"""
|> compile
|> shouldSucceed

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

[<Struct>]
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()
"""
|> compile
|> 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