Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
874e489
Allow constructing F# records via their all-fields constructor (FS-1073)
charlesroddie Jun 19, 2026
d3e1804
Rename MethInfo.RecdAllFieldsCtor to RecdCtor
charlesroddie Jun 19, 2026
402baa2
Add release notes for record constructors (FS-1073)
charlesroddie Jun 19, 2026
8c3997e
Test that struct/CLIMutable records expose no parameterless constructor
charlesroddie Jun 22, 2026
f56706b
Clarify wording: struct = default initialization, CLIMutable = parame…
charlesroddie Jun 22, 2026
83db34d
Fix GoToDefinition for record constructor calls + IDE smoke tests
charlesroddie Jun 22, 2026
249c623
Test record constructor on a RequireQualifiedAccess record
charlesroddie Jun 22, 2026
06a1bf6
Add CompilerCompat cross-compiler tests for record constructors (FS-1…
charlesroddie Jun 22, 2026
0797866
Test record constructor signature-file interplay
charlesroddie Jun 22, 2026
8093757
CompilerCompat: generalize define to USES_PREVIEW_COMPILER, drop comm…
charlesroddie Jun 25, 2026
23e39ee
Test quoted representation of record constructor matches { } syntax
charlesroddie Jun 29, 2026
1a9a043
Merge remote-tracking branch 'upstream/main' into feature/record-posi…
charlesroddie Jul 2, 2026
a5ab09f
Fix NicePrint RecdCtor arm after merge: pass extTypeDisplay
charlesroddie Jul 2, 2026
5b23ed5
Report FS3350 for record constructor use on an unsupported langversion
charlesroddie Jul 24, 2026
512974f
Merge branch 'main' into feature/record-positional-ctor
charlesroddie Jul 24, 2026
d6e061c
Merge remote-tracking branch 'upstream/main' into feature/record-posi…
charlesroddie Jul 29, 2026
0b301e3
Merge remote-tracking branch 'upstream/main' into feature/record-posi…
charlesroddie Jul 30, 2026
e6eba27
Merge remote-tracking branch 'upstream/main' into feature/record-posi…
charlesroddie Aug 3, 2026
bbac2ff
Gate record constructor surfacing and exclude it from signature output
charlesroddie Aug 4, 2026
c320c76
Merge remote-tracking branch 'upstream/main' into feature/record-posi…
charlesroddie Aug 4, 2026
5978867
Code-review cleanups: List.singleton, filter formatting, Console.Writ…
charlesroddie Aug 4, 2026
b48d956
Code-review cleanups: match ValueNone explicitly, tighten comments
charlesroddie Aug 5, 2026
16b7108
Use a wildcard instead of rebinding the matched location in a test
charlesroddie Aug 6, 2026
96d3593
Merge branch 'main' into feature/record-positional-ctor
T-Gro Aug 6, 2026
5aecbd3
Merge branch 'main' into feature/record-positional-ctor
charlesroddie Aug 7, 2026
6bdbfe4
Strip trailing whitespace flagged by Fantomas in LanguageFeatures.fs
charlesroddie Aug 7, 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.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
* Debug: rework for expressions stepping ([PR #19894](https://github.com/dotnet/fsharp/pull/19894))
* Debug: rework conditional erasure, fix stepping over literals ([PR #19897](https://github.com/dotnet/fsharp/pull/19897))
* Debug: fix if and match condition sequence points ([PR #19932](https://github.com/dotnet/fsharp/pull/19932))
* Surface the synthesized all-fields constructor of F# record types to F# code under the `RecordConstructorSyntax` preview feature, via a new `MethInfo.RecdCtor` case. ([PR #19974](https://github.com/dotnet/fsharp/pull/19974))

### Changed

Expand Down
1 change: 1 addition & 0 deletions docs/release-notes/.Language/preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,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))
* Added `ErrorOnMissingSignatureAttribute` preview language feature: makes FS3888 (compiler-semantic attribute on the `.fs` but not on the `.fsi`) an error instead of a warning. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880))
* Allow constructing a record via its all-fields constructor, e.g. `MyRecord(a, b)`, with positional or named arguments (`RecordConstructorSyntax` preview feature). Accessibility matches `{ ... }` construction. ([Suggestion #722](https://github.com/fsharp/fslang-suggestions/issues/722), [RFC FS-1073](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1073-record-constructors.md), [PR #19974](https://github.com/dotnet/fsharp/pull/19974))

### Fixed

Expand Down
7 changes: 7 additions & 0 deletions src/Compiler/Checking/AccessibilityLogic.fs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,13 @@ let rec IsTypeAndMethInfoAccessible amap m accessDomainTy ad = function
| FSMeth (_, _, vref, _) -> IsValAccessible ad vref
| MethInfoWithModifiedReturnType(mi,_) -> IsTypeAndMethInfoAccessible amap m accessDomainTy ad mi
| DefaultStructCtor(g, ty) -> IsTypeAccessible g amap m ad ty
| RecdCtor(g, ty) ->
// The synthesized all-fields constructor must be no more accessible than constructing the record
// with '{ ... }' syntax: require the type, its representation and every field to be accessible.
// This stops F# inheriting the C# behaviour where the IL constructor is public regardless of the
// record's 'private'/'internal' representation.
IsTypeAccessible g amap m ad ty &&
((tcrefOfAppTy g ty).TrueInstanceFieldsAsRefList |> List.forall (IsRecdFieldAccessible amap m ad))
#if !NO_TYPEPROVIDERS
| ProvidedMeth(amap, tpmb, _, m) as etmi ->
let access = tpmb.PUntaint((fun mi -> ComputeILAccess mi.IsPublic mi.IsFamily mi.IsFamilyOrAssembly mi.IsFamilyAndAssembly), m)
Expand Down
3 changes: 3 additions & 0 deletions src/Compiler/Checking/AttributeChecking.fs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ let rec GetAttribInfosOfMethod amap m minfo =
| FSMeth (g, _, vref, _) -> vref.Attribs |> AttribInfosOfFS g
| MethInfoWithModifiedReturnType(mi,_) -> GetAttribInfosOfMethod amap m mi
| DefaultStructCtor _ -> []
| RecdCtor _ -> []
#if !NO_TYPEPROVIDERS
// TODO: provided attributes
| ProvidedMeth (_, _mi, _, _m) ->
Expand Down Expand Up @@ -191,6 +192,7 @@ let rec BindMethInfoAttributes m minfo f1 f2 f3 =
| FSMeth (_, _, vref, _) -> f2 vref.Attribs
| MethInfoWithModifiedReturnType(mi,_) -> BindMethInfoAttributes m mi f1 f2 f3
| DefaultStructCtor _ -> f2 []
| RecdCtor _ -> f2 []
#if !NO_TYPEPROVIDERS
| ProvidedMeth (_, mi, _, _) -> f3 (mi.PApply((fun st -> (st :> IProvidedCustomAttributeProvider)), m))
#endif
Expand Down Expand Up @@ -246,6 +248,7 @@ let rec MethInfoHasWellKnownAttribute g (m: range) (ilFlag: WellKnownILAttribute
| ILMeth(_, ilMethInfo, _) -> ilMethInfo.RawMetadata.HasWellKnownAttribute(g, ilFlag)
| FSMeth(_, _, vref, _) -> ValHasWellKnownAttribute g valFlag vref.Deref
| DefaultStructCtor _ -> false
| RecdCtor _ -> false
| MethInfoWithModifiedReturnType(mi, _) -> MethInfoHasWellKnownAttribute g m ilFlag valFlag attribSpec mi
#if !NO_TYPEPROVIDERS
| ProvidedMeth _ -> MethInfoHasAttribute g m attribSpec minfo
Expand Down
5 changes: 4 additions & 1 deletion src/Compiler/Checking/ConstraintSolver.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2185,9 +2185,12 @@ and MemberConstraintSolutionOfMethInfo css m minfo minst staticTyOpt =

| MethInfoWithModifiedReturnType(mi,_) -> MemberConstraintSolutionOfMethInfo css m mi minst staticTyOpt

| MethInfo.DefaultStructCtor _ ->
| MethInfo.DefaultStructCtor _ ->
error(InternalError("the default struct constructor was the unexpected solution to a trait constraint", m))

| MethInfo.RecdCtor _ ->
error(InternalError("the record all-fields constructor was the unexpected solution to a trait constraint", m))

#if !NO_TYPEPROVIDERS
| ProvidedMeth(amap, mi, _, m) ->
let g = amap.g
Expand Down
29 changes: 21 additions & 8 deletions src/Compiler/Checking/InfoReader.fs
Original file line number Diff line number Diff line change
Expand Up @@ -953,14 +953,22 @@ type InfoReader(g: TcGlobals, amap: ImportMap) as this =
else
match tryTcrefOfAppTy g metadataTy with
| ValueNone -> []
| ValueSome tcref ->
tcref.MembersOfFSharpTyconByName
|> NameMultiMap.find ".ctor"
|> List.choose(fun vref ->
match vref.MemberInfo with
| Some membInfo when (membInfo.MemberFlags.MemberKind = SynMemberKind.Constructor) -> Some vref
| _ -> None)
|> List.map (fun x -> FSMeth(g, origTy, x, None))
| ValueSome tcref ->
let declaredCtors =
tcref.MembersOfFSharpTyconByName
|> NameMultiMap.find ".ctor"
Comment thread
T-Gro marked this conversation as resolved.
|> List.choose(fun vref ->
match vref.MemberInfo with
| Some membInfo when (membInfo.MemberFlags.MemberKind = SynMemberKind.Constructor) -> Some vref
| _ -> None)
|> List.map (fun x -> FSMeth(g, origTy, x, None))
// An F# record exposes no *declared* constructor, but its synthesized all-fields constructor is
// callable from C# as 'new MyRecord(f1, f2, ...)'. Under the RecordConstructorSyntax feature we
// surface that same constructor to F# too (it elaborates to a record allocation - see BuildMethodCall).
if g.langVersion.SupportsFeature LanguageFeature.RecordConstructorSyntax && tcref.IsRecordTycon then
Comment thread
T-Gro marked this conversation as resolved.
declaredCtors @ [ RecdCtor(g, origTy) ]
else
declaredCtors
)

static member ExcludeHiddenOfMethInfos g amap m minfos =
Expand Down Expand Up @@ -1264,6 +1272,11 @@ let rec GetXmlDocSigOfMethInfo (infoReader: InfoReader) m (minfo: MethInfo) =
| ValueSome tcref ->
Some(None, $"M:{tcref.CompiledRepresentationForNamedType.FullName}.#ctor")
| _ -> None
| RecdCtor(g, ty) ->
match tryTcrefOfAppTy g ty with
| ValueSome tcref ->
Some(None, $"M:{tcref.CompiledRepresentationForNamedType.FullName}.#ctor")
Comment thread
T-Gro marked this conversation as resolved.
| _ -> None

#if !NO_TYPEPROVIDERS
| ProvidedMeth _ -> None
Expand Down
17 changes: 14 additions & 3 deletions src/Compiler/Checking/MethodCalls.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1120,9 +1120,14 @@ let rec MakeMethInfoCall (amap: ImportMap) m (minfo: MethInfo) minst args static

| MethInfoWithModifiedReturnType(mi,_) -> MakeMethInfoCall amap m mi minst args staticTyOpt

| DefaultStructCtor(_, ty) ->
| DefaultStructCtor(_, ty) ->
mkDefault (m, ty)

| RecdCtor(g, ty) ->
let tcref = tcrefOfAppTy g ty
let tinst = argsOfAppTy g ty
mkRecordExpr g (RecdExpr, tcref, tinst, tcref.TrueInstanceFieldsAsRefList, args, m)

#if !NO_TYPEPROVIDERS
| ProvidedMeth(amap, mi, _, m) ->
let isProp = false // not necessarily correct, but this is only used post-creflect where this flag is irrelevant
Expand Down Expand Up @@ -1264,9 +1269,15 @@ let rec BuildMethodCall tcVal g amap isMutable m isProp minfo valUseFlags minst
else
warning(Error(FSComp.SR.tcDefaultStructConstructorCall(), m))
else
if not (TypeHasDefaultValue g m ty) then
if not (TypeHasDefaultValue g m ty) then
errorR(Error(FSComp.SR.tcDefaultStructConstructorCall(), m))
mkDefault (m, ty), ty)
mkDefault (m, ty), ty

// Build a record allocation from a call to the synthesized all-fields constructor of an F# record.
| RecdCtor (g, ty) ->
let tcref = tcrefOfAppTy g ty
let tinst = argsOfAppTy g ty
mkRecordExpr g (RecdExpr, tcref, tinst, tcref.TrueInstanceFieldsAsRefList, allArgs, m), ty)

let ILFieldStaticChecks g amap infoReader ad m (finfo : ILFieldInfo) =
CheckILFieldInfoAccessible g amap m ad finfo
Expand Down
4 changes: 3 additions & 1 deletion src/Compiler/Checking/NameResolution.fs
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,9 @@ let rec TrySelectExtensionMethInfoOfILExtMem m amap apparentTy (actualParent, mi
| ProvidedMeth(amap,providedMeth,_,m) ->
ProvidedMeth(amap, providedMeth, Some pri,m) |> Some
#endif
| DefaultStructCtor _ ->
| DefaultStructCtor _ ->
None
| RecdCtor _ ->
None

/// Select from a list of extension methods
Expand Down
7 changes: 5 additions & 2 deletions src/Compiler/Checking/NicePrint.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1785,10 +1785,13 @@ module InfoMemberPrinting =
let amap = infoReader.amap

match methInfo with
| DefaultStructCtor _ ->
let prettyTyparInst, _ = PrettyTypes.PrettifyInst amap.g typarInst
| DefaultStructCtor _ ->
let prettyTyparInst, _ = PrettyTypes.PrettifyInst amap.g typarInst
let resL = PrintTypes.layoutTyconRef denv methInfo.ApparentEnclosingTyconRef ^^ wordL punctuationUnit
prettyTyparInst, resL
| RecdCtor _ ->
let prettyTyparInst, _ = PrettyTypes.PrettifyInst amap.g typarInst
prettyTyparInst, layoutMethInfoCSharpStyle amap m denv methInfo methInfo.FormalMethodInst
| FSMeth(_, _, vref, _) ->
let prettyTyparInst, resL = PrintTastMemberOrVals.prettyLayoutOfValOrMember { denv with showMemberContainers=true } infoReader typarInst vref
prettyTyparInst, resL
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Checking/OverloadResolutionCache.fs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ let rec computeMethInfoHash (minfo: MethInfo) : int =
| FSMeth(_, _, vref, _) -> HashingPrimitives.combineHash (hash vref.Stamp) (hash vref.LogicalName)
| ILMeth(_, ilMethInfo, _) -> HashingPrimitives.combineHash (hash ilMethInfo.ILName) (hash ilMethInfo.DeclaringTyconRef.Stamp)
| DefaultStructCtor(_, _) -> hash "DefaultStructCtor"
| RecdCtor(_, _) -> hash "RecdCtor"
| MethInfoWithModifiedReturnType(original, _) -> computeMethInfoHash original
#if !NO_TYPEPROVIDERS
| ProvidedMeth(_, mb, _, _) ->
Expand Down
Loading
Loading