diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 5d4bd0fe6ac..46cc2aef86d 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -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 diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md index 90c1aa2faa6..b094068d7e6 100644 --- a/docs/release-notes/.Language/preview.md +++ b/docs/release-notes/.Language/preview.md @@ -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 diff --git a/src/Compiler/Checking/AccessibilityLogic.fs b/src/Compiler/Checking/AccessibilityLogic.fs index 4995095a688..19876c039b0 100644 --- a/src/Compiler/Checking/AccessibilityLogic.fs +++ b/src/Compiler/Checking/AccessibilityLogic.fs @@ -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) diff --git a/src/Compiler/Checking/AttributeChecking.fs b/src/Compiler/Checking/AttributeChecking.fs index 695eb4f3286..213e09a917d 100755 --- a/src/Compiler/Checking/AttributeChecking.fs +++ b/src/Compiler/Checking/AttributeChecking.fs @@ -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) -> @@ -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 @@ -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 diff --git a/src/Compiler/Checking/ConstraintSolver.fs b/src/Compiler/Checking/ConstraintSolver.fs index b739cede2de..1315a39163f 100644 --- a/src/Compiler/Checking/ConstraintSolver.fs +++ b/src/Compiler/Checking/ConstraintSolver.fs @@ -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 diff --git a/src/Compiler/Checking/InfoReader.fs b/src/Compiler/Checking/InfoReader.fs index 121b087f5e5..85fb98316dd 100644 --- a/src/Compiler/Checking/InfoReader.fs +++ b/src/Compiler/Checking/InfoReader.fs @@ -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" + |> 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 + declaredCtors @ [ RecdCtor(g, origTy) ] + else + declaredCtors ) static member ExcludeHiddenOfMethInfos g amap m minfos = @@ -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") + | _ -> None #if !NO_TYPEPROVIDERS | ProvidedMeth _ -> None diff --git a/src/Compiler/Checking/MethodCalls.fs b/src/Compiler/Checking/MethodCalls.fs index a9c394ee4e9..d5f7510cd66 100644 --- a/src/Compiler/Checking/MethodCalls.fs +++ b/src/Compiler/Checking/MethodCalls.fs @@ -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 @@ -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 diff --git a/src/Compiler/Checking/NameResolution.fs b/src/Compiler/Checking/NameResolution.fs index b214d3b9e0d..81fa1a5eb2a 100644 --- a/src/Compiler/Checking/NameResolution.fs +++ b/src/Compiler/Checking/NameResolution.fs @@ -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 diff --git a/src/Compiler/Checking/NicePrint.fs b/src/Compiler/Checking/NicePrint.fs index de8daffa7c5..eb55bd1dc10 100644 --- a/src/Compiler/Checking/NicePrint.fs +++ b/src/Compiler/Checking/NicePrint.fs @@ -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 diff --git a/src/Compiler/Checking/OverloadResolutionCache.fs b/src/Compiler/Checking/OverloadResolutionCache.fs index aae0a99bc2f..06a2252076b 100644 --- a/src/Compiler/Checking/OverloadResolutionCache.fs +++ b/src/Compiler/Checking/OverloadResolutionCache.fs @@ -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, _, _) -> diff --git a/src/Compiler/Checking/infos.fs b/src/Compiler/Checking/infos.fs index 1a38e10f42b..692ec248af0 100644 --- a/src/Compiler/Checking/infos.fs +++ b/src/Compiler/Checking/infos.fs @@ -674,6 +674,10 @@ type MethInfo = /// Describes a use of a pseudo-method corresponding to the default constructor for a .NET struct type | DefaultStructCtor of tcGlobals: TcGlobals * structTy: TType + /// Describes a use of the compiler-synthesized all-fields constructor of an F# record type, + /// i.e. the constructor C# sees as `new MyRecord(field1, field2, ...)`. + | RecdCtor of tcGlobals: TcGlobals * recdTy: TType + #if !NO_TYPEPROVIDERS /// Describes a use of a method backed by provided metadata | ProvidedMeth of amap: ImportMap * methodBase: Tainted * extensionMethodPriority: ExtensionMethodPriority option * m: range @@ -689,6 +693,7 @@ type MethInfo = | FSMeth(_, ty, _, _) -> ty | MethInfoWithModifiedReturnType(mi, _) -> mi.ApparentEnclosingType | DefaultStructCtor(_, ty) -> ty + | RecdCtor(_, ty) -> ty #if !NO_TYPEPROVIDERS | ProvidedMeth(amap, mi, _, m) -> ImportProvidedType amap m (mi.PApply((fun mi -> nonNull mi.DeclaringType), m)) @@ -726,6 +731,7 @@ type MethInfo = | _ -> Some (mb, staticParams) #endif | DefaultStructCtor _ -> None + | RecdCtor _ -> None /// Get the extension method priority of the method, if it has one. member x.ExtensionMemberPriorityOption = @@ -737,6 +743,7 @@ type MethInfo = #endif | MethInfoWithModifiedReturnType(mi, _) -> mi.ExtensionMemberPriorityOption | DefaultStructCtor _ -> None + | RecdCtor _ -> None /// Get the extension method priority of the method. If it is not an extension method /// then use the highest possible value since non-extension methods always take priority @@ -754,6 +761,7 @@ type MethInfo = | ProvidedMeth(_, mi, _, m) -> "ProvidedMeth: " + mi.PUntaint((fun mi -> mi.Name), m) #endif | DefaultStructCtor _ -> ".ctor" + | RecdCtor _ -> ".ctor" /// Get the method name in LogicalName form, i.e. the name as it would be stored in .NET metadata member x.LogicalName = @@ -765,6 +773,7 @@ type MethInfo = | ProvidedMeth(_, mi, _, m) -> mi.PUntaint((fun mi -> mi.Name), m) #endif | DefaultStructCtor _ -> ".ctor" + | RecdCtor _ -> ".ctor" /// Get the method name in DisplayName form member x.DisplayName = @@ -802,6 +811,7 @@ type MethInfo = | FSMeth(g, _, _, _) -> g | MethInfoWithModifiedReturnType(mi, _) -> mi.TcGlobals | DefaultStructCtor (g, _) -> g + | RecdCtor (g, _) -> g #if !NO_TYPEPROVIDERS | ProvidedMeth(amap, _, _, _) -> amap.g #endif @@ -818,6 +828,7 @@ type MethInfo = memberMethodTypars | MethInfoWithModifiedReturnType(mi, _) -> mi.FormalMethodTypars | DefaultStructCtor _ -> [] + | RecdCtor _ -> [] #if !NO_TYPEPROVIDERS | ProvidedMeth _ -> [] // There will already have been an error if there are generic parameters here. #endif @@ -834,6 +845,7 @@ type MethInfo = | FSMeth(_, _, vref, _) -> vref.XmlDoc | MethInfoWithModifiedReturnType(mi, _) -> mi.XmlDoc | DefaultStructCtor _ -> XmlDoc.Empty + | RecdCtor _ -> XmlDoc.Empty #if !NO_TYPEPROVIDERS | ProvidedMeth(_, mi, _, m)-> let lines = mi.PUntaint((fun mix -> (mix :> IProvidedCustomAttributeProvider).GetXmlDocAttributes(mi.TypeProvider.PUntaintNoFailure id)), m) @@ -856,6 +868,7 @@ type MethInfo = | FSMeth(g, _, vref, _) -> GetArgInfosOfMember x.IsCSharpStyleExtensionMember g vref |> List.map List.length | MethInfoWithModifiedReturnType(mi, _) -> mi.NumArgs | DefaultStructCtor _ -> [0] + | RecdCtor(g, ty) -> [ (tcrefOfAppTy g ty).TrueInstanceFieldsAsList.Length ] #if !NO_TYPEPROVIDERS | ProvidedMeth(_, mi, _, m) -> [mi.PApplyArray((fun mi -> mi.GetParameters()),"GetParameters", m).Length] // Why is this a list? Answer: because the method might be curried #endif @@ -878,6 +891,7 @@ type MethInfo = | FSMeth(_, _, vref, _) -> vref.IsInstanceMember || x.IsCSharpStyleExtensionMember | MethInfoWithModifiedReturnType(mi, _) -> mi.IsInstance | DefaultStructCtor _ -> false + | RecdCtor _ -> false #if !NO_TYPEPROVIDERS | ProvidedMeth(_, mi, _, m) -> mi.PUntaint((fun mi -> not mi.IsConstructor && not mi.IsStatic), m) #endif @@ -892,6 +906,7 @@ type MethInfo = | FSMeth _ -> false | MethInfoWithModifiedReturnType(mi, _) -> mi.IsProtectedAccessibility | DefaultStructCtor _ -> false + | RecdCtor _ -> false #if !NO_TYPEPROVIDERS | ProvidedMeth(_, mi, _, m) -> mi.PUntaint((fun mi -> mi.IsFamily), m) #endif @@ -902,6 +917,7 @@ type MethInfo = | FSMeth(_, _, vref, _) -> vref.IsVirtualMember | MethInfoWithModifiedReturnType(mi, _) -> mi.IsVirtual | DefaultStructCtor _ -> false + | RecdCtor _ -> false #if !NO_TYPEPROVIDERS | ProvidedMeth(_, mi, _, m) -> mi.PUntaint((fun mi -> mi.IsVirtual), m) #endif @@ -912,6 +928,7 @@ type MethInfo = | FSMeth(_g, _, vref, _) -> (vref.MemberInfo.Value.MemberFlags.MemberKind = SynMemberKind.Constructor) | MethInfoWithModifiedReturnType(mi, _) -> mi.IsConstructor | DefaultStructCtor _ -> true + | RecdCtor _ -> true #if !NO_TYPEPROVIDERS | ProvidedMeth(_, mi, _, m) -> mi.PUntaint((fun mi -> mi.IsConstructor), m) #endif @@ -925,6 +942,7 @@ type MethInfo = | _ -> false | MethInfoWithModifiedReturnType(mi, _) -> mi.IsClassConstructor | DefaultStructCtor _ -> false + | RecdCtor _ -> false #if !NO_TYPEPROVIDERS | ProvidedMeth(_, mi, _, m) -> mi.PUntaint((fun mi -> mi.IsConstructor && mi.IsStatic), m) // Note: these are never public anyway #endif @@ -935,6 +953,7 @@ type MethInfo = | FSMeth(_, _, vref, _) -> vref.MemberInfo.Value.MemberFlags.IsDispatchSlot | MethInfoWithModifiedReturnType(mi, _) -> mi.IsDispatchSlot | DefaultStructCtor _ -> false + | RecdCtor _ -> false #if !NO_TYPEPROVIDERS | ProvidedMeth _ -> x.IsVirtual // Note: follow same implementation as ILMeth #endif @@ -947,6 +966,7 @@ type MethInfo = | FSMeth(_g, _, _vref, _) -> false | MethInfoWithModifiedReturnType(mi, _) -> mi.IsFinal | DefaultStructCtor _ -> true + | RecdCtor _ -> true #if !NO_TYPEPROVIDERS | ProvidedMeth(_, mi, _, m) -> mi.PUntaint((fun mi -> mi.IsFinal), m) #endif @@ -963,6 +983,7 @@ type MethInfo = | FSMeth(g, _, vref, _) -> isInterfaceTy g minfo.ApparentEnclosingType || vref.IsDispatchSlotMember | MethInfoWithModifiedReturnType(mi, _) -> mi.IsAbstract | DefaultStructCtor _ -> false + | RecdCtor _ -> false #if !NO_TYPEPROVIDERS | ProvidedMeth(_, mi, _, m) -> mi.PUntaint((fun mi -> mi.IsAbstract), m) #endif @@ -976,7 +997,8 @@ type MethInfo = #if !NO_TYPEPROVIDERS | ProvidedMeth(_, mi, _, m) -> mi.PUntaint((fun mi -> mi.IsHideBySig), m) // REVIEW: Check this is correct #endif - | DefaultStructCtor _ -> false)) + | DefaultStructCtor _ -> false + | RecdCtor _ -> false)) /// Indicates if this is an IL method. member x.IsILMethod = @@ -992,6 +1014,7 @@ type MethInfo = | FSMeth(g, _, vref, _) -> vref.IsFSharpExplicitInterfaceImplementation g | MethInfoWithModifiedReturnType(mi, _) -> mi.IsFSharpExplicitInterfaceImplementation | DefaultStructCtor _ -> false + | RecdCtor _ -> false #if !NO_TYPEPROVIDERS | ProvidedMeth _ -> false #endif @@ -1003,6 +1026,7 @@ type MethInfo = | FSMeth(_, _, vref, _) -> vref.IsDefiniteFSharpOverrideMember | MethInfoWithModifiedReturnType(mi, _) -> mi.IsDefiniteFSharpOverride | DefaultStructCtor _ -> false + | RecdCtor _ -> false #if !NO_TYPEPROVIDERS | ProvidedMeth _ -> false #endif @@ -1137,6 +1161,7 @@ type MethInfo = | mi1, MethInfoWithModifiedReturnType(mi2, _) | MethInfoWithModifiedReturnType(mi1, _), mi2 -> MethInfo.MethInfosUseIdenticalDefinitions mi1 mi2 | DefaultStructCtor _, DefaultStructCtor _ -> tyconRefEq x1.TcGlobals x1.DeclaringTyconRef x2.DeclaringTyconRef + | RecdCtor _, RecdCtor _ -> tyconRefEq x1.TcGlobals x1.DeclaringTyconRef x2.DeclaringTyconRef #if !NO_TYPEPROVIDERS | ProvidedMeth(_, mi1, _, _), ProvidedMeth(_, mi2, _, _) -> ProvidedMethodBase.TaintedEquals (mi1, mi2) #endif @@ -1150,6 +1175,7 @@ type MethInfo = | MethInfoWithModifiedReturnType(mi,_) -> mi.ComputeHashCode() | DefaultStructCtor(_, _ty) -> 34892 // "ty" doesn't support hashing. We could use "hash (tcrefOfAppTy g ty).CompiledName" or // something but we don't have a "g" parameter here yet. But this hash need only be very approximate anyway + | RecdCtor(_, _ty) -> 34893 // Approximate, as with DefaultStructCtor above. #if !NO_TYPEPROVIDERS | ProvidedMeth(_, mi, _, _) -> ProvidedMethodInfo.TaintedGetHashCode mi #endif @@ -1164,6 +1190,7 @@ type MethInfo = | FSMeth(g, ty, vref, pri) -> FSMeth(g, instType inst ty, vref, pri) | MethInfoWithModifiedReturnType(mi, retTy) -> MethInfoWithModifiedReturnType(mi.Instantiate(amap, m, inst), retTy) | DefaultStructCtor(g, ty) -> DefaultStructCtor(g, instType inst ty) + | RecdCtor(g, ty) -> RecdCtor(g, instType inst ty) #if !NO_TYPEPROVIDERS | ProvidedMeth _ -> match inst with @@ -1183,6 +1210,7 @@ type MethInfo = retTy |> Option.map (instType inst) | MethInfoWithModifiedReturnType(_,retTy) -> Some retTy | DefaultStructCtor _ -> None + | RecdCtor _ -> None #if !NO_TYPEPROVIDERS | ProvidedMeth(amap, mi, _, m) -> GetCompiledReturnTyOfProvidedMethodInfo amap m mi @@ -1220,6 +1248,10 @@ type MethInfo = paramTypes |> List.mapSquared (fun (ParamNameAndType(_, ty)) -> instType inst ty) | MethInfoWithModifiedReturnType(mi,_) -> mi.GetParamTypes(amap,m,minst) | DefaultStructCtor _ -> [] + | RecdCtor(g, ty) -> + let tcref = tcrefOfAppTy g ty + let tinst = argsOfAppTy g ty + [ tcref.TrueInstanceFieldsAsList |> List.map (fun fspec -> actualTyOfRecdFieldForTycon tcref.Deref tinst fspec) ] #if !NO_TYPEPROVIDERS | ProvidedMeth(amap, mi, _, m) -> // A single group of tupled arguments @@ -1245,6 +1277,7 @@ type MethInfo = else [] | MethInfoWithModifiedReturnType(mi,_) -> mi.GetObjArgTypes(amap, m, minst) | DefaultStructCtor _ -> [] + | RecdCtor _ -> [] #if !NO_TYPEPROVIDERS | ProvidedMeth(amap, mi, _, m) -> if x.IsInstance then [ ImportProvidedType amap m (mi.PApply((fun mi -> nonNull mi.DeclaringType), m)) ] // find the type of the 'this' argument @@ -1299,6 +1332,9 @@ type MethInfo = | MethInfoWithModifiedReturnType(mi,_) -> mi.GetParamAttribs(amap, m) | DefaultStructCtor _ -> [[]] + | RecdCtor(g, ty) -> + [ (tcrefOfAppTy g ty).TrueInstanceFieldsAsList + |> List.map (fun _ -> ParamAttribs(false, false, false, NotOptional, NoCallerInfo, ReflectedArgInfo.None)) ] #if !NO_TYPEPROVIDERS | ProvidedMeth(amap, mi, _, _) -> @@ -1341,6 +1377,7 @@ type MethInfo = MakeSlotSig(x.LogicalName, x.ApparentEnclosingType, formalEnclosingTypars, formalMethTypars, formalParams, formalRetTy) | MethInfoWithModifiedReturnType(mi,_) -> mi.GetSlotSig(amap, m) | DefaultStructCtor _ -> error(InternalError("no slotsig for DefaultStructCtor", m)) + | RecdCtor _ -> error(InternalError("no slotsig for RecdCtor", m)) | _ -> let g = x.TcGlobals // slotsigs must contain the formal types for the arguments and return type @@ -1407,6 +1444,11 @@ type MethInfo = | MethInfoWithModifiedReturnType(_mi,_) -> failwith "unreachable" | DefaultStructCtor _ -> [[]] + | RecdCtor(g, ty) -> + let tcref = tcrefOfAppTy g ty + let tinst = argsOfAppTy g ty + [ tcref.TrueInstanceFieldsAsList + |> List.map (fun fspec -> ParamNameAndType(Some (mkSynId m fspec.LogicalName), actualTyOfRecdFieldForTycon tcref.Deref tinst fspec)) ] #if !NO_TYPEPROVIDERS | ProvidedMeth(amap, mi, _, _) -> // A single set of tupled parameters @@ -1438,6 +1480,7 @@ type MethInfo = | None -> false | MethInfoWithModifiedReturnType _ -> false | DefaultStructCtor _ -> false + | RecdCtor _ -> false #if !NO_TYPEPROVIDERS | ProvidedMeth _ -> false #endif diff --git a/src/Compiler/Checking/infos.fsi b/src/Compiler/Checking/infos.fsi index e091834e271..b7239d9b2b6 100644 --- a/src/Compiler/Checking/infos.fsi +++ b/src/Compiler/Checking/infos.fsi @@ -320,6 +320,9 @@ type MethInfo = /// Describes a use of a pseudo-method corresponding to the default constructor for a .NET struct type | DefaultStructCtor of tcGlobals: TcGlobals * structTy: TType + /// Describes a use of the compiler-synthesized all-fields constructor of an F# record type + | RecdCtor of tcGlobals: TcGlobals * recdTy: TType + #if !NO_TYPEPROVIDERS /// Describes a use of a method backed by provided metadata | ProvidedMeth of diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index dc27f32bae9..b43b8670b97 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1786,6 +1786,7 @@ featureEmptyBodiedComputationExpressions,"Support for computation expressions wi featureAllowAccessModifiersToAutoPropertiesGettersAndSetters,"Allow access modifiers to auto properties getters and setters" 3871,tcAccessModifiersNotAllowedInSRTPConstraint,"Access modifiers cannot be applied to an SRTP constraint." featureAllowObjectExpressionWithoutOverrides,"Allow object expressions without overrides" +featureRecordConstructorSyntax,"Constructing a record via its all-fields constructor" featureUseTypeSubsumptionCache,"Use type conversion cache during compilation" 3872,tcPartialActivePattern,"Multi-case partial active patterns are not supported. Consider using a single-case partial active pattern or a full active pattern." featureDontWarnOnUppercaseIdentifiersInBindingPatterns,"Don't warn on uppercase identifiers in binding patterns" diff --git a/src/Compiler/Facilities/LanguageFeatures.fs b/src/Compiler/Facilities/LanguageFeatures.fs index da4ef690311..81a34bc20a4 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fs +++ b/src/Compiler/Facilities/LanguageFeatures.fs @@ -110,6 +110,7 @@ type LanguageFeature = | PreprocessorElif | ExceptionFieldSerializationSupport | ErrorOnMissingSignatureAttribute + | RecordConstructorSyntax /// LanguageVersion management type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) = @@ -263,6 +264,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) LanguageFeature.MethodOverloadsCache, previewVersion // Performance optimization for overload resolution LanguageFeature.ImplicitDIMCoverage, languageVersion110 LanguageFeature.ErrorOnMissingSignatureAttribute, previewVersion // Opt-in: turn FS3888 from warning into error + LanguageFeature.RecordConstructorSyntax, previewVersion // Allow constructing a record via its all-fields constructor, e.g. MyRecord(a, b) ] static let defaultLanguageVersion = LanguageVersion("default") @@ -459,6 +461,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) | LanguageFeature.PreprocessorElif -> FSComp.SR.featurePreprocessorElif () | LanguageFeature.ExceptionFieldSerializationSupport -> FSComp.SR.featureExceptionFieldSerializationSupport () | LanguageFeature.ErrorOnMissingSignatureAttribute -> FSComp.SR.featureErrorOnMissingSignatureAttribute () + | LanguageFeature.RecordConstructorSyntax -> FSComp.SR.featureRecordConstructorSyntax () /// Get a version string associated with the given feature. static member GetFeatureVersionString feature = diff --git a/src/Compiler/Facilities/LanguageFeatures.fsi b/src/Compiler/Facilities/LanguageFeatures.fsi index 5ba352191af..c5fd73624e4 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fsi +++ b/src/Compiler/Facilities/LanguageFeatures.fsi @@ -101,6 +101,7 @@ type LanguageFeature = | PreprocessorElif | ExceptionFieldSerializationSupport | ErrorOnMissingSignatureAttribute + | RecordConstructorSyntax /// LanguageVersion management type LanguageVersion = diff --git a/src/Compiler/Symbols/SymbolHelpers.fs b/src/Compiler/Symbols/SymbolHelpers.fs index 742e7640293..288e3d54f6e 100644 --- a/src/Compiler/Symbols/SymbolHelpers.fs +++ b/src/Compiler/Symbols/SymbolHelpers.fs @@ -65,6 +65,7 @@ module internal SymbolHelpers = | ProvidedMeth(_, mi, _, _) -> Construct.ComputeDefinitionLocationOfProvidedItem mi #endif | DefaultStructCtor(_, AppTy g (tcref, _)) -> Some(rangeOfEntityRef preferFlag tcref) + | RecdCtor(_, AppTy g (tcref, _)) -> Some(rangeOfEntityRef preferFlag tcref) | _ -> minfo.ArbitraryValRef |> Option.map (rangeOfValRef preferFlag) let rangeOfEventInfo preferFlag (einfo: EventInfo) = @@ -813,6 +814,7 @@ module internal SymbolHelpers = | MethInfoWithModifiedReturnType(mi,_) -> getKeywordForMethInfo mi | DefaultStructCtor _ -> None + | RecdCtor _ -> None #if !NO_TYPEPROVIDERS | ProvidedMeth _ -> None #endif diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index 84e5e3cc50c..156abf55093 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -592,6 +592,11 @@ vypsat literály libovolné velikosti + + Constructing a record via its all-fields constructor + Constructing a record via its all-fields constructor + + informational messages related to reference cells informační zprávy související s referenčními buňkami diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index d6edecb4def..470a7b34a58 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -592,6 +592,11 @@ Literale beliebiger Größe auflisten + + Constructing a record via its all-fields constructor + Constructing a record via its all-fields constructor + + informational messages related to reference cells Informationsmeldungen im Zusammenhang mit Bezugszellen diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index 46573e801f1..d123670bd56 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -592,6 +592,11 @@ enumerar literales de cualquier tamaño + + Constructing a record via its all-fields constructor + Constructing a record via its all-fields constructor + + informational messages related to reference cells mensajes informativos relacionados con las celdas de referencia diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index 1acfc469b5b..39783312ec9 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -592,6 +592,11 @@ répertorier les littéraux de n’importe quelle taille + + Constructing a record via its all-fields constructor + Constructing a record via its all-fields constructor + + informational messages related to reference cells messages d’information liés aux cellules de référence diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index deaca46fad0..b4b25d544d2 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -592,6 +592,11 @@ elenca valori letterali di qualsiasi dimensione + + Constructing a record via its all-fields constructor + Constructing a record via its all-fields constructor + + informational messages related to reference cells messaggi informativi relativi alle celle di riferimento diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index 10029fab134..abd61d4c629 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -592,6 +592,11 @@ 任意のサイズのリテラルを一覧表示する + + Constructing a record via its all-fields constructor + Constructing a record via its all-fields constructor + + informational messages related to reference cells 参照セルに関連する情報メッセージ diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index 1d7a19af957..b1196eb348a 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -592,6 +592,11 @@ 모든 크기의 목록 리터럴 + + Constructing a record via its all-fields constructor + Constructing a record via its all-fields constructor + + informational messages related to reference cells 참조 셀과 관련된 정보 메시지 diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index 170e6e37718..edd3076d78c 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -592,6 +592,11 @@ wyświetlanie na liście literałów o dowolnym rozmiarze + + Constructing a record via its all-fields constructor + Constructing a record via its all-fields constructor + + informational messages related to reference cells komunikaty informacyjne związane z odwołaniami do komórek diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index 87ef255a7e8..44bf51a7421 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -592,6 +592,11 @@ literais de lista de qualquer tamanho + + Constructing a record via its all-fields constructor + Constructing a record via its all-fields constructor + + informational messages related to reference cells mensagens informativas relacionadas a células de referência diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index 57f29e98517..95e06e0c8b4 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -592,6 +592,11 @@ список литералов любого размера + + Constructing a record via its all-fields constructor + Constructing a record via its all-fields constructor + + informational messages related to reference cells информационные сообщения, связанные с ссылочными ячейками diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index 08753e47a27..b727da6b532 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -592,6 +592,11 @@ tüm boyutlardaki sabit değerleri listele + + Constructing a record via its all-fields constructor + Constructing a record via its all-fields constructor + + informational messages related to reference cells başvuru hücreleriyle ilgili bilgi mesajları diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index e117ab0ec5d..b8095b6afd9 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -592,6 +592,11 @@ 列出任何大小的文本 + + Constructing a record via its all-fields constructor + Constructing a record via its all-fields constructor + + informational messages related to reference cells 与引用单元格相关的信息性消息 diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index 31c5596a098..c2322b4bc0b 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -592,6 +592,11 @@ 列出任何大小的常值 + + Constructing a record via its all-fields constructor + Constructing a record via its all-fields constructor + + informational messages related to reference cells 與參考儲存格相關的資訊訊息 diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/Types/RecordTypes/RecordTypes.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/Types/RecordTypes/RecordTypes.fs index 3bae9db5802..be1d260f776 100644 --- a/tests/FSharp.Compiler.ComponentTests/Conformance/Types/RecordTypes/RecordTypes.fs +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/Types/RecordTypes/RecordTypes.fs @@ -616,3 +616,201 @@ module RecordTypes = |> typecheck |> shouldFail |> withSingleDiagnostic (Error 954, Line 4, Col 18, Line 4, Col 30, "This type definition involves an immediate cyclic reference through a struct field or inheritance relation") + + // Feature: allow constructing an F# record by calling its (synthesized) all-fields + // constructor positionally, e.g. MyRecord(1, "a"), as is already possible from C#. + // These tests describe the target behaviour and currently FAIL (records expose no + // F#-callable constructor; only { Field = ... } record syntax is permitted). + + [] + let ``Record can be constructed positionally via its all-fields constructor`` () = + Fsx """ +type Person = { Name : string; Age : int } +let p = Person("Isaac", 21) +if p.Name <> "Isaac" then failwith "wrong Name" +if p.Age <> 21 then failwith "wrong Age" +if p <> { Name = "Isaac"; Age = 21 } then failwith "not equal to record-syntax value" + """ + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + + [] + let ``Record positional constructor evaluates arguments left-to-right`` () = + Fsx """ +type R = { A : int; B : int } +let log = System.Collections.Generic.List() +let side n = log.Add n; n +let r = R(side 1, side 2) +if List.ofSeq log <> [1; 2] then failwith "arguments not evaluated left-to-right" +if r.A <> 1 || r.B <> 2 then failwith "wrong field values" + """ + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + + [] + let ``Record constructor supports named arguments matching field names`` () = + Fsx """ +type Person = { Name : string; Age : int } +let p = Person(Age = 21, Name = "Isaac") +if p.Name <> "Isaac" || p.Age <> 21 then failwith "wrong field values" + """ + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + + [] + let ``Generic record can be constructed positionally`` () = + Fsx """ +type Boxed<'T> = { Value : 'T; Label : string } +let b = Boxed(42, "answer") +if b.Value <> 42 || b.Label <> "answer" then failwith "wrong field values" + """ + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + + [] + let ``Struct record can be constructed positionally with all fields`` () = + Fsx """ +[] +type Point = { X : int; Y : int } +let pt = Point(3, 4) +if pt.X <> 3 || pt.Y <> 4 then failwith "wrong field values" + """ + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + + // FS-1073 scope precedence / backward compatibility: the type name is treated as a record + // constructor ONLY when there is no other binding with the same name in scope. Here a value + // binding 'Record' shadows the record type's synthesized constructor, so 'Record 0' must remain + // the function application (returning a string), NOT a record construction. Calling 'string' on + // it therefore yields the function's own result. + [] + let ``Record constructor does not shadow an in-scope value binding of the same name`` () = + Fsx """ +let Record (x: int) : string = "function" // value binding 'Record : int -> string' +type Record = { N : int } // record whose all-fields ctor would be 'Record : int -> Record' +let result : string = string (Record 0) // 'Record 0' must be the function application, not the ctor +if result <> "function" then failwith $"expected the in-scope function to be called, got '{result}'" + """ + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + + // FS-1073 accessibility gating: the synthesized constructor must be no more accessible than '{ ... }' + // construction. A record with 'private' representation can be constructed via the ctor only where the + // representation is accessible (i.e. inside the declaring module), never from outside - so F# does not + // inherit the C# behaviour where the IL constructor is public regardless of the record's accessibility. + [] + let ``Private record can be constructed via its constructor inside the declaring scope`` () = + Fsx """ +type R = private { A : int; B : int } +let r = R(1, 2) +if r.A <> 1 || r.B <> 2 then failwith "wrong field values" + """ + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + + [] + let ``Private record constructor is not accessible from outside the declaring module`` () = + FSharp """ +namespace Test + +module M = + type R = private { A : int; B : int } + +module N = + let bad = M.R(1, 2) + """ + |> withLangVersionPreview + |> typecheck + |> shouldFail + |> withSingleDiagnostic (Error 801, Line 8, Col 15, Line 8, Col 18, "This type has no accessible object constructors") + + // Only the all-fields constructor is exposed: a struct record's default (zero) initialization and a + // [] record's IL parameterless .ctor both stay unavailable from F#. + [] + let ``Struct record does not expose parameterless default initialization`` () = + Fsx """ +[] +type Point = { X : int; Y : int } +let p = Point() + """ + |> withLangVersionPreview + |> typecheck + |> shouldFail + |> withSingleDiagnostic (Error 501, Line 4, Col 9, Line 4, Col 16, "The object constructor 'Point' takes 2 argument(s) but is here given 0. The required signature is 'Point(X: int, Y: int) : Point'.") + + [] + let ``CLIMutable record does not expose its parameterless constructor`` () = + Fsx """ +[] +type R = { A : int; B : int } +let r = R() + """ + |> withLangVersionPreview + |> typecheck + |> shouldFail + |> withSingleDiagnostic (Error 501, Line 4, Col 9, Line 4, Col 12, "The object constructor 'R' takes 2 argument(s) but is here given 0. The required signature is 'R(A: int, B: int) : R'.") + + // A [] record forces field *labels* to be qualified in { } construction; + // the positional constructor has no labels, so it must work without any spurious RQA diagnostic. + [] + let ``Record constructor works on a RequireQualifiedAccess record`` () = + Fsx """ +[] +type R = { A : int; B : int } +let r = R(1, 2) +if r.A <> 1 || r.B <> 2 then failwith "wrong field values" + """ + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + + // Signature-file interplay: the constructor is not a declared member, so it is never written in a + // .fsi - it rides on the visibility of the record's representation, exactly like { } construction. + [] + let ``Record constructor is available when the signature exposes the record representation`` () = + Fsi """ +module Lib +type R = { A: int; B: int } +""" + |> withAdditionalSourceFiles [ + FsSource """ +module Lib +type R = { A: int; B: int } +""" + FsSourceWithFileName "Consumer.fs" """ +module Consumer +let r = Lib.R(1, 2) +if r.A <> 1 || r.B <> 2 then failwith "wrong field values" +""" + ] + |> withLangVersionPreview + |> compile + |> shouldSucceed + + [] + let ``Record constructor is unavailable when the signature hides the record representation`` () = + Fsi """ +module Lib +type R +""" + |> withAdditionalSourceFiles [ + FsSource """ +module Lib +type R = { A: int; B: int } +""" + FsSourceWithFileName "Consumer.fs" """ +module Consumer +let _ = Lib.R(1, 2) +""" + ] + |> withLangVersionPreview + |> compile + |> shouldFail + |> withErrorCode 1133 diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj index 1cbea95b31e..ccc88fd0bcf 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj @@ -34,6 +34,7 @@ + diff --git a/tests/FSharp.Compiler.Service.Tests/RecordConstructorTests.fs b/tests/FSharp.Compiler.Service.Tests/RecordConstructorTests.fs new file mode 100644 index 00000000000..625a49e3e23 --- /dev/null +++ b/tests/FSharp.Compiler.Service.Tests/RecordConstructorTests.fs @@ -0,0 +1,56 @@ +module FSharp.Compiler.Service.Tests.RecordConstructorTests + +open FSharp.Compiler.EditorServices +open FSharp.Compiler.Service.Tests.Common +open FSharp.Compiler.Text +open Xunit + +// IDE smoke tests for the RecordConstructorSyntax feature (FS-1073): a positional record-constructor +// call should behave like any other constructor for tooling - go-to-definition lands on the record type, +// the tooltip shows the type, and the use is linked back to the type declaration. + +let private source = """ +module M +type MyRecord = { A: int; B: int } +let r = MyRecord(1, 2) +""" + +let private tooltipToString (ToolTipText items) = + items + |> List.collect (function + | ToolTipElement.Group elements -> elements |> List.collect (fun e -> List.ofArray e.MainDescription) + | _ -> []) + |> List.map (fun t -> t.Text) + |> String.concat "" + +[] +let ``GoToDefinition on a record constructor call navigates to the record type`` () = + let _, checkResults = parseAndCheckScriptPreview("Test.fsx", source) + // 'MyRecord' constructor use is on line 4; ask for its declaration. + let location = checkResults.GetDeclarationLocation(4, 16, "let r = MyRecord(1, 2)", [ "MyRecord" ]) + match location with + | FindDeclResult.DeclFound r -> Assert.Equal(3, r.StartLine) // 'type MyRecord = ...' + | other -> failwith $"Expected the record type declaration, got {other}" + +[] +let ``Tooltip on a record constructor call mentions the record type`` () = + let tooltip = + Checker.getTooltipWithOptions [| "--langversion:preview" |] """ +module M +type MyRecord = { A: int; B: int } +let r = MyReco{caret}rd(1, 2) +""" + Assert.Contains("MyRecord", tooltipToString tooltip) + +[] +let ``A record constructor call is linked to the record type declaration`` () = + let _, checkResults = parseAndCheckScriptPreview("Test.fsx", source) + // The constructor use is on line 4 starting at column 8 ('let r = '). + let ctorUse = + checkResults.GetAllUsesOfAllSymbolsInFile() + |> Seq.find (fun u -> u.Range.StartLine = 4 && u.Range.StartColumn = 8) + // The symbol resolves back to the record type declaration on line 3. + match ctorUse.Symbol.DeclarationLocation with + | Some loc -> Assert.Equal(3, loc.StartLine) + | None -> failwith "Expected a declaration location for the record constructor symbol" + Assert.True(checkResults.GetUsesOfSymbolInFile(ctorUse.Symbol).Length >= 1) diff --git a/tests/projects/CompilerCompat/CompilerCompatApp/CompilerCompatApp.fsproj b/tests/projects/CompilerCompat/CompilerCompatApp/CompilerCompatApp.fsproj index 311c09b259b..babd856f3e6 100644 --- a/tests/projects/CompilerCompat/CompilerCompatApp/CompilerCompatApp.fsproj +++ b/tests/projects/CompilerCompat/CompilerCompatApp/CompilerCompatApp.fsproj @@ -19,6 +19,12 @@ + + + preview + $(DefineConstants);USES_PREVIEW_COMPILER + + diff --git a/tests/projects/CompilerCompat/CompilerCompatApp/Program.fs b/tests/projects/CompilerCompat/CompilerCompatApp/Program.fs index e7bd46a8ed8..7248a62a30c 100644 --- a/tests/projects/CompilerCompat/CompilerCompatApp/Program.fs +++ b/tests/projects/CompilerCompat/CompilerCompatApp/Program.fs @@ -68,8 +68,22 @@ let main _argv = printfn "ERROR: Processed result doesn't match expected" 1 else - printfn "SUCCESS: All compiler compatibility tests passed" - 0 + let viaInline = Library.makeRecordCtorPoint 7 9 +#if USES_PREVIEW_COMPILER + let viaCtor = Library.RecordCtorPoint(3, 4) +#else + let viaCtor = { Library.RecordCtorPoint.A = 3; Library.RecordCtorPoint.B = 4 } +#endif + if viaInline.A <> 7 || viaInline.B <> 9 then + printfn "ERROR: inline record constructor result mismatch" + 1 + elif viaCtor.A <> 3 || viaCtor.B <> 4 then + printfn "ERROR: record constructor result mismatch" + 1 + else + printfn "RecordCtor: inline=(%d,%d) direct=(%d,%d)" viaInline.A viaInline.B viaCtor.A viaCtor.B + printfn "SUCCESS: All compiler compatibility tests passed" + 0 with ex -> printfn "ERROR: Exception occurred: %s" ex.Message diff --git a/tests/projects/CompilerCompat/CompilerCompatLib/CompilerCompatLib.fsproj b/tests/projects/CompilerCompat/CompilerCompatLib/CompilerCompatLib.fsproj index a9442854e07..f5c46447238 100644 --- a/tests/projects/CompilerCompat/CompilerCompatLib/CompilerCompatLib.fsproj +++ b/tests/projects/CompilerCompat/CompilerCompatLib/CompilerCompatLib.fsproj @@ -28,6 +28,12 @@ + + + preview + $(DefineConstants);USES_PREVIEW_COMPILER + + diff --git a/tests/projects/CompilerCompat/CompilerCompatLib/Library.fs b/tests/projects/CompilerCompat/CompilerCompatLib/Library.fs index 625cb787b58..48f4236beba 100644 --- a/tests/projects/CompilerCompat/CompilerCompatLib/Library.fs +++ b/tests/projects/CompilerCompat/CompilerCompatLib/Library.fs @@ -55,3 +55,14 @@ module Library = [] type TypeWithLiteralAttrArg() = member _.GetValue() = LiteralAttrArg + + /// Record + inline constructor for the FS-1073 cross-compiler test. The new positional syntax is used + /// when built with a preview compiler, classic syntax otherwise; both pickle identically. + type RecordCtorPoint = { A: int; B: int } + + let inline makeRecordCtorPoint a b = +#if USES_PREVIEW_COMPILER + RecordCtorPoint(a, b) +#else + { RecordCtorPoint.A = a; RecordCtorPoint.B = b } +#endif