Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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/.Language/preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* Warn (FS3884) when a function or delegate value is used as an interpolated string argument, since it will be formatted via `ToString` rather than being applied. ([PR #19289](https://github.com/dotnet/fsharp/pull/19289))
* Added `MethodOverloadsCache` language feature (preview) that caches overload resolution results for repeated method calls, significantly improving compilation performance. ([PR #19072](https://github.com/dotnet/fsharp/pull/19072))
* Support for .NET IL fields in SRTP member constraints (preview). Inline functions using SRTP `(^T: (member FieldName: FieldType) x)` now resolve against .NET class/struct fields, not just properties and methods. ([Language suggestion #1323](https://github.com/fsharp/fslang-suggestions/issues/1323))

### Fixed

Expand Down
122 changes: 100 additions & 22 deletions src/Compiler/Checking/ConstraintSolver.fs
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ type TraitConstraintSolution =
| TTraitSolved of minfo: MethInfo * minst: TypeInst * staticTyOpt: TType option
| TTraitSolvedRecdProp of fieldInfo: RecdFieldInfo * isSetProp: bool
| TTraitSolvedAnonRecdProp of anonRecdTypeInfo: AnonRecdTypeInfo * typeInst: TypeInst * index: int
| TTraitSolvedField of ty: TType * fieldInfo: ILFieldInfo * isSetField: bool

let BakedInTraitConstraintNames =
[ "op_Division" ; "op_Multiply"; "op_Addition"
Expand Down Expand Up @@ -2006,9 +2007,48 @@ and SolveMemberConstraint (csenv: ConstraintSolverEnv) ignoreUnresolvedOverload
else
None

let fieldSearch =
if g.langVersion.SupportsFeature LanguageFeature.SupportILFieldsInSRTP then
let isGet = nm.StartsWithOrdinal("get_")
let isSet = nm.StartsWithOrdinal("set_")

if not isRigid && ((argTys.IsEmpty && isGet) || isSet) then
Comment on lines +2014 to +2015
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The IL-field setter path can resolve even when the member-constraint shape isn’t a valid setter. fieldSearch accepts any set_ name regardless of argTys length, and later only checks the argument type when argTys is exactly [argTy] (otherwise it silently skips). Consider requiring argTys.Length = 1 for set_ (and argTys.IsEmpty for get_) before returning a field solution, so malformed signatures don’t bind to fields.

Suggested change
if not isRigid && ((argTys.IsEmpty && isGet) || isSet) then
let isValidGetter = isGet && argTys.IsEmpty
let isValidSetter = isSet && (argTys.Length = 1)
if not isRigid && (isValidGetter || isValidSetter) then

Copilot uses AI. Check for mistakes.
let fieldNm = nm[4..]

let fields =
[| for ty in supportTys do
let item =
TryFindIntrinsicNamedItemOfType
csenv.InfoReader
(fieldNm, AccessibleFromEverywhere, false)
FindMemberFlag.IgnoreOverrides
m
ty

match item with
| Some(ILFieldItem [ ilfinfo ]) when
ilfinfo.IsStatic = (not memFlags.IsInstance)
&& (isGet || not ilfinfo.IsInitOnly)
&& IsILFieldInfoAccessible g amap m AccessibleFromEverywhere ilfinfo
&& ilfinfo.LiteralValue.IsNone
&& not ilfinfo.IsSpecialName
->
yield (ilfinfo, isSet)
| _ -> () |]

match fields with
| [| (ilfinfo, isSet) |] ->
let ty = ilfinfo.FieldType(amap, m)
Some(ty, ilfinfo, isSet)
| _ -> None
else
None
else
None

// Now check if there are no feasible solutions at all
match minfos, recdPropSearch, anonRecdPropSearch with
| [], None, None when MemberConstraintIsReadyForStrongResolution csenv traitInfo ->
match minfos, recdPropSearch, anonRecdPropSearch, fieldSearch with
| [], None, None, None when MemberConstraintIsReadyForStrongResolution csenv traitInfo ->
if supportTys |> List.exists (isFunTy g) then
return! ErrorD (ConstraintSolverError(FSComp.SR.csExpectTypeWithOperatorButGivenFunction(ConvertValLogicalNameToDisplayNameCore nm), m, m2))
elif supportTys |> List.exists (isAnyTupleTy g) then
Expand Down Expand Up @@ -2069,36 +2109,69 @@ and SolveMemberConstraint (csenv: ConstraintSolverEnv) ignoreUnresolvedOverload
(fun (a, _) -> Option.isSome a)
(fun trace -> ResolveOverloading csenv (WithTrace trace) nm ndeep (Some traitInfo) CallerArgs.Empty AccessibleFromEverywhere calledMethGroup false (Some (MustEqual retTy)))

match anonRecdPropSearch, recdPropSearch, methOverloadResult with
| Some (anonInfo, tinst, i), None, None ->
// OK, the constraint is solved by a record property. Assert that the return types match.
match anonRecdPropSearch, recdPropSearch, fieldSearch, methOverloadResult with
| _, _, _, Some(calledMeth: CalledMeth<_>) ->
// Method/property has highest priority — wins even if a field also matched.
let minfo = calledMeth.Method

do! errors
let isInstance = minfo.IsInstance

if isInstance <> memFlags.IsInstance then
return!
if isInstance then
ErrorD(
ConstraintSolverError(
FSComp.SR.csMethodFoundButIsNotStatic (
(NicePrint.minimalStringOfType denv minfo.ApparentEnclosingType),
(ConvertValLogicalNameToDisplayNameCore nm),
nm
),
m,
m2
)
)
else
ErrorD(
ConstraintSolverError(
FSComp.SR.csMethodFoundButIsStatic (
(NicePrint.minimalStringOfType denv minfo.ApparentEnclosingType),
(ConvertValLogicalNameToDisplayNameCore nm),
nm
),
m,
m2
)
)
else
do! CheckMethInfoAttributes g m None minfo
return TTraitSolved(minfo, calledMeth.CalledTyArgs, calledMeth.OptionalStaticType)

| Some(anonInfo, tinst, i), _, _, None ->
// Anonymous record property — wins over record properties and fields.
let rty2 = List.item i tinst
do! SolveTypeEqualsTypeKeepAbbrevs csenv ndeep m2 trace retTy rty2
return TTraitSolvedAnonRecdProp(anonInfo, tinst, i)

| None, Some (rfinfo, isSetProp), None ->
// OK, the constraint is solved by a record property. Assert that the return types match.
| None, Some(rfinfo, isSetProp), _, None ->
// Record property — wins over fields.
let rty2 = if isSetProp then g.unit_ty else rfinfo.FieldType
do! SolveTypeEqualsTypeKeepAbbrevs csenv ndeep m2 trace retTy rty2
return TTraitSolvedRecdProp(rfinfo, isSetProp)

| None, None, Some (calledMeth: CalledMeth<_>) ->
// OK, the constraint is solved.
let minfo = calledMeth.Method
| None, None, Some(ty, ilfinfo, isSet), None ->
// IL field — lowest priority, only when nothing else matched.
let rty2 = if isSet then g.unit_ty else ty
do! SolveTypeEqualsTypeKeepAbbrevs csenv ndeep m2 trace retTy rty2

do! errors
let isInstance = minfo.IsInstance
if isInstance <> memFlags.IsInstance then
return!
if isInstance then
ErrorD(ConstraintSolverError(FSComp.SR.csMethodFoundButIsNotStatic((NicePrint.minimalStringOfType denv minfo.ApparentEnclosingType), (ConvertValLogicalNameToDisplayNameCore nm), nm), m, m2 ))
else
ErrorD(ConstraintSolverError(FSComp.SR.csMethodFoundButIsStatic((NicePrint.minimalStringOfType denv minfo.ApparentEnclosingType), (ConvertValLogicalNameToDisplayNameCore nm), nm), m, m2 ))
else
do! CheckMethInfoAttributes g m None minfo
return TTraitSolved (minfo, calledMeth.CalledTyArgs, calledMeth.OptionalStaticType)
if isSet then
match argTys with
| [ argTy ] -> do! SolveTypeEqualsTypeKeepAbbrevs csenv ndeep m2 trace argTy ty
| _ -> ()

| _ ->
return TTraitSolvedField(ty, ilfinfo, isSet)

| _ ->
do! AddUnsolvedMemberConstraint csenv ndeep m2 trace permitWeakResolution ignoreUnresolvedOverload traitInfo errors
return TTraitUnsolved
}
Expand Down Expand Up @@ -2167,6 +2240,11 @@ and RecordMemberConstraintSolution css m trace traitInfo traitConstraintSln =
TransactMemberConstraintSolution traitInfo trace sln
ResultD true

| TTraitSolvedField (ty, ilfinfo, isSet) ->
let sln = ILFieldSln(ty, ilfinfo.TypeInst, ilfinfo.ILFieldRef, ilfinfo.IsStatic, isSet)
TransactMemberConstraintSolution traitInfo trace sln
ResultD true

/// Convert a MethInfo into the data we save in the TAST
and MemberConstraintSolutionOfMethInfo css m minfo minst staticTyOpt =
#if !NO_TYPEPROVIDERS
Expand Down
53 changes: 41 additions & 12 deletions src/Compiler/Checking/MethodCalls.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2164,7 +2164,7 @@ let GenWitnessExpr amap g m (traitInfo: TraitConstraintInfo) argExprs =

let sln =
match traitInfo.Solution with
| None -> Choice5Of5()
| None -> Choice6Of6()
| Some sln ->

// Given the solution information, reconstruct the MethInfo for the solution
Expand All @@ -2179,25 +2179,54 @@ let GenWitnessExpr amap g m (traitInfo: TraitConstraintInfo) argExprs =
| Some ilActualTypeRef ->
let actualTyconRef = ImportILTypeRef amap m ilActualTypeRef
MethInfo.CreateILExtensionMeth(amap, m, origTy, actualTyconRef, None, mdef)
Choice1Of5 (ilMethInfo, minst, staticTyOpt)
Choice1Of6 (ilMethInfo, minst, staticTyOpt)

| FSMethSln(ty, vref, minst, staticTyOpt) ->
Choice1Of5 (FSMeth(g, ty, vref, None), minst, staticTyOpt)
Choice1Of6 (FSMeth(g, ty, vref, None), minst, staticTyOpt)

| FSRecdFieldSln(tinst, rfref, isSetProp) ->
Choice2Of5 (tinst, rfref, isSetProp)
Choice2Of6 (tinst, rfref, isSetProp)

| FSAnonRecdFieldSln(anonInfo, tinst, i) ->
Choice3Of5 (anonInfo, tinst, i)
Choice3Of6 (anonInfo, tinst, i)

| ClosedExprSln expr ->
Choice4Of5 expr
Choice4Of6 expr

| ILFieldSln(ty, tinst, ilfref, isStatic, isSet) ->
Choice5Of6 (ty, tinst, ilfref, isStatic, isSet)

| BuiltInSln ->
Choice5Of5 ()
Choice6Of6 ()

match sln with
| Choice1Of5(minfo, methArgTys, staticTyOpt) ->
| Choice5Of6(ty, tinst, ilfref, isStatic, isSet) ->
let declaringTyconRef = ImportILTypeRef amap m ilfref.DeclaringTypeRef
let isStruct = isStructTy g (generalizedTyconRef g declaringTyconRef)
let boxity = if isStruct then ILBoxity.AsValue else ILBoxity.AsObject
let fspec = mkILFieldSpec (ilfref, mkILNamedTy boxity ilfref.DeclaringTypeRef [])

match isStatic, isSet with
| false, false ->
// Instance getter: ldfld
Some(Expr.Op(TOp.ILAsm([ mkNormalLdfld fspec ], [ ty ]), tinst, argExprs, m))
| false, true ->
// Instance setter: stfld (handle struct address-taking)
if isStruct && not (isByrefTy g (tyOfExpr g argExprs[0])) then
let wrap, h', _readonly, _writeonly =
mkExprAddrOfExpr g true false PossiblyMutates argExprs[0] None m
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

In the ILFieldSln instance-setter case, mkExprAddrOfExpr is called with PossiblyMutates. For a field setter this is a definite mutation, and using PossiblyMutates can allow taking a readonly/inref address of an immutable struct value and then emitting stfld anyway (since the returned readonly flag is ignored). Consider switching to DefinitelyMutates and/or honoring the readonly result by forcing a defensive copy / rejecting the mutation, to align with the normal IL-field set path.

Suggested change
mkExprAddrOfExpr g true false PossiblyMutates argExprs[0] None m
mkExprAddrOfExpr g true false DefinitelyMutates argExprs[0] None m

Copilot uses AI. Check for mistakes.

Some(wrap (Expr.Op(TOp.ILAsm([ mkNormalStfld fspec ], []), tinst, [ h'; argExprs[1] ], m)))
else
Some(Expr.Op(TOp.ILAsm([ mkNormalStfld fspec ], []), tinst, argExprs, m))
| true, false ->
// Static getter: ldsfld
Some(Expr.Op(TOp.ILAsm([ mkNormalLdsfld fspec ], [ ty ]), tinst, argExprs, m))
| true, true ->
// Static setter: stsfld
Some(Expr.Op(TOp.ILAsm([ mkNormalStsfld fspec ], []), tinst, argExprs, m))

| Choice1Of6(minfo, methArgTys, staticTyOpt) ->
let argExprs =
// FIX for #421894 - typechecker assumes that coercion can be applied for the trait
// calls arguments but codegen doesn't emit coercion operations
Expand Down Expand Up @@ -2241,7 +2270,7 @@ let GenWitnessExpr amap g m (traitInfo: TraitConstraintInfo) argExprs =
else
Some (MakeMethInfoCall amap m minfo methArgTys argExprs staticTyOpt)

| Choice2Of5 (tinst, rfref, isSet) ->
| Choice2Of6 (tinst, rfref, isSet) ->
match isSet, rfref.RecdField.IsStatic, argExprs.Length with
// static setter
| true, true, 1 ->
Expand Down Expand Up @@ -2271,17 +2300,17 @@ let GenWitnessExpr amap g m (traitInfo: TraitConstraintInfo) argExprs =

| _ -> None

| Choice3Of5 (anonInfo, tinst, i) ->
| Choice3Of6 (anonInfo, tinst, i) ->
let tupInfo = anonInfo.TupInfo
if evalTupInfoIsStruct tupInfo && isByrefTy g (tyOfExpr g argExprs[0]) then
Some (mkAnonRecdFieldGetViaExprAddr (anonInfo, argExprs[0], tinst, i, m))
else
Some (mkAnonRecdFieldGet g (anonInfo, argExprs[0], tinst, i, m))

| Choice4Of5 expr ->
| Choice4Of6 expr ->
Some (MakeApplicationAndBetaReduce g (expr, tyOfExpr g expr, [], argExprs, m))

| Choice5Of5 () ->
| Choice6Of6 () ->
match traitInfo.Solution with
| None -> None // the trait has been generalized
| Some _->
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/FSComp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1809,6 +1809,7 @@ featureWarnWhenFunctionValueUsedAsInterpolatedStringArg,"Warn when a function va
featureMethodOverloadsCache,"Support for caching method overload resolution results for improved compilation performance."
featureImplicitDIMCoverage,"Implicit dispatch slot coverage for default interface member implementations"
featurePreprocessorElif,"#elif preprocessor directive"
featureSupportILFieldsInSRTP,"Support for .NET fields in SRTP member constraints"
3880,optsLangVersionOutOfSupport,"Language version '%s' is out of support. The last .NET SDK supporting it is available at https://dotnet.microsoft.com/en-us/download/dotnet/%s"
3881,optsUnrecognizedLanguageFeature,"Unrecognized language feature name: '%s'. Use a valid feature name such as 'NameOf' or 'StringInterpolation'."
3882,lexHashElifMustBeFirst,"#elif directive must appear as the first non-whitespace character on a line"
Expand Down
3 changes: 3 additions & 0 deletions src/Compiler/FSStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1155,4 +1155,7 @@
<data name="NoConstructorsAvailableForType" xml:space="preserve">
<value>No constructors are available for the type '{0}'</value>
</data>
<data name="featureSupportILFieldsInSRTP" xml:space="preserve">
<value>Support for .NET fields in SRTP member constraints</value>
</data>
Comment on lines +1158 to +1160
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

featureSupportILFieldsInSRTP is added to FSStrings.resx, but all other language-feature descriptions appear to live in FSComp.txt (and are accessed via FSComp.SR.* in LanguageFeatures.fs). Since there are no other feature* entries in FSStrings.resx, this looks unused and may create extra localization churn. Consider removing it from FSStrings.resx (and the corresponding FSStrings.*.xlf entries) if the canonical resource is FSComp.txt.

Suggested change
<data name="featureSupportILFieldsInSRTP" xml:space="preserve">
<value>Support for .NET fields in SRTP member constraints</value>
</data>

Copilot uses AI. Check for mistakes.
</root>
3 changes: 3 additions & 0 deletions src/Compiler/Facilities/LanguageFeatures.fs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ type LanguageFeature =
| MethodOverloadsCache
| ImplicitDIMCoverage
| PreprocessorElif
| SupportILFieldsInSRTP

/// LanguageVersion management
type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) =
Expand Down Expand Up @@ -258,6 +259,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array)
// F# preview (still preview in 10.0)
LanguageFeature.FromEndSlicing, previewVersion // Unfinished features --- needs work
LanguageFeature.MethodOverloadsCache, previewVersion // Performance optimization for overload resolution
LanguageFeature.SupportILFieldsInSRTP, previewVersion
LanguageFeature.ImplicitDIMCoverage, languageVersion110
]

Expand Down Expand Up @@ -453,6 +455,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array)
| LanguageFeature.MethodOverloadsCache -> FSComp.SR.featureMethodOverloadsCache ()
| LanguageFeature.ImplicitDIMCoverage -> FSComp.SR.featureImplicitDIMCoverage ()
| LanguageFeature.PreprocessorElif -> FSComp.SR.featurePreprocessorElif ()
| LanguageFeature.SupportILFieldsInSRTP -> FSComp.SR.featureSupportILFieldsInSRTP ()

/// Get a version string associated with the given feature.
static member GetFeatureVersionString feature =
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Facilities/LanguageFeatures.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ type LanguageFeature =
| MethodOverloadsCache
| ImplicitDIMCoverage
| PreprocessorElif
| SupportILFieldsInSRTP

/// LanguageVersion management
type LanguageVersion =
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Symbols/SymbolHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,7 @@ module internal SymbolHelpers =
| Some (TraitConstraintSln.FSRecdFieldSln _)
| Some (TraitConstraintSln.FSAnonRecdFieldSln _)
| Some (TraitConstraintSln.ClosedExprSln _)
| Some (TraitConstraintSln.ILFieldSln _)
| Some TraitConstraintSln.BuiltInSln
| None ->
GetXmlCommentForItemAux None infoReader m item
Expand Down
10 changes: 10 additions & 0 deletions src/Compiler/TypedTree/TypedTree.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2663,6 +2663,16 @@ type TraitConstraintSln =
/// Indicates a trait is solved by a 'fake' instance of an operator, like '+' on integers
| BuiltInSln

/// ILFieldSln(ty, tinst, ilfref, isStatic, isSet)
///
/// Indicates a trait is solved by a .NET IL field.
/// ty -- the F# type of the field
/// tinst -- the type instantiation of the declaring type
/// ilfref -- IL field reference (declaring type, name, IL type)
/// isStatic -- whether the field is static
/// isSet -- whether this is a set operation
| ILFieldSln of ty: TType * tinst: TypeInst * ilfref: ILFieldRef * isStatic: bool * isSet: bool

// %+A formatting is used, so this is not needed
//[<DebuggerBrowsable(DebuggerBrowsableState.Never)>]
//member x.DebugText = x.ToString()
Expand Down
Loading
Loading