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 965957dbc0c..a4ddb66302e 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -75,6 +75,7 @@ ([PR #19724](https://github.com/dotnet/fsharp/pull/19724)) * Emit debug points at a stack-empty position ([PR #19877](https://github.com/dotnet/fsharp/pull/19877)) * Fix spurious XmlDoc warnings (unknown parameter / no documentation for parameter) under `--warnon:3390` when a get/set property documents the full parameter set across both accessors. ([Issue #13684](https://github.com/dotnet/fsharp/issues/13684), [PR #19884](https://github.com/dotnet/fsharp/pull/19884)) +* Fix FSI pretty printing to distinguish anonymous records (`{| ... |}`) from nominal records (`{ ... }`). ([Issue #6116](https://github.com/dotnet/fsharp/issues/6116), [PR #19919](https://github.com/dotnet/fsharp/pull/19919)) ### Added diff --git a/src/Compiler/Utilities/sformat.fs b/src/Compiler/Utilities/sformat.fs index 6ba5b68c462..15f7a06a121 100644 --- a/src/Compiler/Utilities/sformat.fs +++ b/src/Compiler/Utilities/sformat.fs @@ -149,6 +149,8 @@ module TaggedText = let rightBracket = tagPunctuation "]" let leftBrace = tagPunctuation "{" let rightBrace = tagPunctuation "}" + let leftBraceBar = tagPunctuation "{|" + let rightBraceBar = tagPunctuation "|}" let equals = tagOperator "=" #if COMPILER @@ -216,8 +218,6 @@ module TaggedText = // common tagged literals let lineBreak = tagLineBreak "\n" let space = tagSpace " " - let leftBraceBar = tagPunctuation "{|" - let rightBraceBar = tagPunctuation "|}" let arrow = tagPunctuation "->" let dot = tagPunctuation "." let leftAngle = tagPunctuation "<" @@ -487,11 +487,17 @@ module ReflectUtils = | Value | Reference + [] + type RecordKind = + | Nominal + | AnonReference + | AnonStruct + [] type ValueInfo = | TupleValue of TupleType * (obj * Type)[] | FunctionClosureValue of Type - | RecordValue of (string * obj * Type)[] + | RecordValue of kind: RecordKind * (string * obj * Type)[] | UnionCaseValue of string * (string * (obj * Type))[] | ExceptionValue of Type * (string * (obj * Type))[] | NullValue @@ -555,7 +561,17 @@ module ReflectUtils = elif FSharpType.IsRecord(reprty, bindingFlags) then let props = FSharpType.GetRecordFields(reprty, bindingFlags) + let kind = + if reprty.Name.StartsWith("<>f__AnonymousType", StringComparison.Ordinal) then + if reprty.IsValueType then + RecordKind.AnonStruct + else + RecordKind.AnonReference + else + RecordKind.Nominal + RecordValue( + kind, props |> Array.map (fun prop -> prop.Name, prop.GetValue(obj, null), prop.PropertyType) ) @@ -861,13 +877,18 @@ module Display = let unitL = wordL (tagPunctuation "()") - let makeRecordL nameXs = + let makeRecordL kind nameXs = let itemL (name, xL) = (wordL name ^^ wordL equals) -- xL let braceL xs = - (wordL leftBrace) ^^ xs ^^ (wordL rightBrace) + match kind with + | RecordKind.AnonReference + | RecordKind.AnonStruct -> (wordL leftBraceBar) ^^ xs ^^ (wordL rightBraceBar) + | RecordKind.Nominal -> (wordL leftBrace) ^^ xs ^^ (wordL rightBrace) - nameXs |> List.map itemL |> aboveListL |> braceL + let itemLayouts = nameXs |> List.map itemL + + braceL (aboveListL itemLayouts) let makePropertiesL nameXs = let itemL (name, v) = @@ -1200,12 +1221,17 @@ module Display = | TupleType.Value -> structL ^^ fields | TupleType.Reference -> fields - and recordValueL depthLim items = + and recordValueL depthLim kind items = let itemL (name, x, ty) = countNodes 1 tagRecordField name, nestedObjL depthLim Precedence.BracketIfTuple (x, ty) - makeRecordL (List.map itemL items) + let body = makeRecordL kind (List.map itemL items) + + match kind with + | RecordKind.AnonStruct -> structL -- body + | RecordKind.AnonReference + | RecordKind.Nominal -> body and listValueL depthLim constr recd = match constr with @@ -1471,7 +1497,7 @@ module Display = match repr with | TupleValue(tupleType, vals) -> tupleValueL depthLim prec vals tupleType - | RecordValue items -> recordValueL depthLim (Array.toList items) + | RecordValue(kind, items) -> recordValueL depthLim kind (Array.toList items) | UnionCaseValue(constr, recd) when // x is List. Note: "null" is never a valid list value. (not (isNull x)) && isListType (x.GetType()) diff --git a/tests/AheadOfTime/Trimming/Program.fs b/tests/AheadOfTime/Trimming/Program.fs index 6f5e806b099..2ab73bc9bb0 100644 --- a/tests/AheadOfTime/Trimming/Program.fs +++ b/tests/AheadOfTime/Trimming/Program.fs @@ -9093,7 +9093,7 @@ module PercentAPublicTests = let testPercentAMyAnonymousRecord () = let data = {| A = "Hello, World!"; B = 1.027m; C = 1028; D = 1.029 |} - test "test8901" (lazy (sprintf "%A" data).Replace("\n", ";")) """{ A = "Hello, World!"; B = 1.027M; C = 1028; D = 1.029 }""" + test "test8901" (lazy (sprintf "%A" data).Replace("\n", ";")) """{| A = "Hello, World!"; B = 1.027M; C = 1028; D = 1.029 |}""" let testDiscriminatedUnion () = test "test8902" (lazy (sprintf "%A" (IntNumber 10 )).Replace("\n", ";")) """IntNumber 10""" diff --git a/tests/AheadOfTime/Trimming/check.ps1 b/tests/AheadOfTime/Trimming/check.ps1 index 1695a3684f0..2abf9c9fb8a 100644 --- a/tests/AheadOfTime/Trimming/check.ps1 +++ b/tests/AheadOfTime/Trimming/check.ps1 @@ -63,10 +63,10 @@ function CheckTrim($root, $tfm, $outputfile, $expected_len, $callerLineNumber) { $allErrors = @() # Check net9.0 trimmed assemblies -$allErrors += CheckTrim -root "SelfContained_Trimming_Test" -tfm "net9.0" -outputfile "FSharp.Core.dll" -expected_len 311808 -callerLineNumber 66 +$allErrors += CheckTrim -root "SelfContained_Trimming_Test" -tfm "net9.0" -outputfile "FSharp.Core.dll" -expected_len 314880 -callerLineNumber 66 # Check net9.0 trimmed assemblies with static linked FSharpCore -$allErrors += CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net9.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 9169408 -callerLineNumber 69 +$allErrors += CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net9.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 9172992 -callerLineNumber 69 # Check net9.0 trimmed assemblies with F# metadata resources removed $allErrors += CheckTrim -root "FSharpMetadataResource_Trimming_Test" -tfm "net9.0" -outputfile "FSharpMetadataResource_Trimming_Test.dll" -expected_len 7609344 -callerLineNumber 72 diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 954c02b8aca..9890126f895 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -388,6 +388,7 @@ + diff --git a/tests/FSharp.Compiler.ComponentTests/InteractiveSession/AnonRecordPrinting.fs b/tests/FSharp.Compiler.ComponentTests/InteractiveSession/AnonRecordPrinting.fs new file mode 100644 index 00000000000..51e39dda0fc --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/InteractiveSession/AnonRecordPrinting.fs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +/// Tests for FSI pretty-printing of anonymous records (regression for dotnet/fsharp#6116). +/// Anonymous records must be printed with `{| |}` braces, distinct from nominal records which use `{ }`. +namespace InteractiveSession + +open Xunit +open FSharp.Test.Compiler + +module AnonRecordPrinting = + + // --- Anonymous records: must use {| |} --- + + // NOTE: Assertions target the VALUE portion (after `=`) only, because the + // `val it: ...` type-annotation already contains `{| ... |}` for anon + // record types regardless of the value-printing bug being fixed here. + + [] + let ``Anonymous record prints with bar braces``() = + // The layout engine may break multi-field records across lines, so the + // opening and closing bar-braces are checked separately (mirrors the + // approach used by the nominal-record test below). + Fsx """ +let r = {| Name = "Phillip"; Age = 28 |};; +() +""" + |> withOptions ["--nologo"] + |> runFsi + |> shouldSucceed + |> withStdOutContains "= {| Age = 28" + |> withStdOutContains "Name = \"Phillip\" |}" + |> ignore + + [] + let ``Anonymous record with single field prints with bar braces``() = + Fsx """ +{| X = 1 |};; +() +""" + |> withOptions ["--nologo"] + |> runFsi + |> shouldSucceed + |> withStdOutContains "= {| X = 1 |}" + |> ignore + + [] + let ``Nested anonymous record prints with bar braces at both levels``() = + Fsx """ +{| Inner = {| X = 1 |} |};; +() +""" + |> withOptions ["--nologo"] + |> runFsi + |> shouldSucceed + // Both outer and inner record values must use bar-braces. + |> withStdOutContains "= {| Inner = {| X = 1 |} |}" + |> ignore + + [] + let ``Struct anonymous record prints with struct keyword and bar braces``() = + // The layout engine may break multi-field records across lines, so the + // `struct {|` opener and the `|}` closer are checked separately. + Fsx """ +struct {| X = 1; Y = 2 |};; +() +""" + |> withOptions ["--nologo"] + |> runFsi + |> shouldSucceed + |> withStdOutContains "= struct {| X = 1" + |> withStdOutContains "Y = 2 |}" + |> ignore + + // --- Nominal records: must KEEP printing with { } (regression guard) --- + + [] + let ``Nominal record still prints with plain braces and not bar braces``() = + Fsx """ +type R = { Name: string; Age: int } +let r = { Name = "Phillip"; Age = 28 };; +() +""" + |> withOptions ["--nologo"] + |> runFsi + |> shouldSucceed + |> withStdOutContains "{ Name = \"Phillip\"" + |> withStdOutContains "Age = 28 }" + |> ignore + + // --- Other shapes must be unchanged (regression guard) --- + + [] + let ``Tuple printing unchanged``() = + Fsx """ +(1, "x");; +() +""" + |> withOptions ["--nologo"] + |> runFsi + |> shouldSucceed + |> withStdOutContains "(1, \"x\")" + |> ignore + + [] + let ``Discriminated union printing unchanged``() = + Fsx """ +type DU = A of int | B of string +A 42;; +() +""" + |> withOptions ["--nologo"] + |> runFsi + |> shouldSucceed + |> withStdOutContains "A 42" + |> ignore + + [] + let ``List printing unchanged``() = + Fsx """ +[1; 2; 3];; +() +""" + |> withOptions ["--nologo"] + |> runFsi + |> shouldSucceed + |> withStdOutContains "[1; 2; 3]" + |> ignore diff --git a/tests/fsharp/core/anon/lib.fs b/tests/fsharp/core/anon/lib.fs index 271c8266b46..80fb262b44d 100644 --- a/tests/fsharp/core/anon/lib.fs +++ b/tests/fsharp/core/anon/lib.fs @@ -52,8 +52,8 @@ module KindB1 = check "coijoiwcnkwle1" {| a = 1 |} {| a = 1 |} check "coijoiwcnkwle2" {| a = 2 |} {| a = 2 |} - check "coijoiwcnkwle3" (sprintf "%A" {| X = 10 |}) "{ X = 10 }" - check "coijoiwcnkwle4" (sprintf "%A" {| X = 10; Y = 1 |}) "{ X = 10\n Y = 1 }" + check "coijoiwcnkwle3" (sprintf "%A" {| X = 10 |}) "{| X = 10 |}" + check "coijoiwcnkwle4" (sprintf "%A" {| X = 10; Y = 1 |}) "{| X = 10\n Y = 1 |}" check "clekoiew09" (f2 {| X = {| X = 10 |} |}) 10 check "cewkew0oijew" (f2 {| X = {| X = 20 |} |}) 20 diff --git a/tests/fsharp/core/anon/test.fsx b/tests/fsharp/core/anon/test.fsx index 6b065cdd7f0..2b0b9f7427c 100644 --- a/tests/fsharp/core/anon/test.fsx +++ b/tests/fsharp/core/anon/test.fsx @@ -25,13 +25,13 @@ module Test = let testAccess = (KindB1.data1.X, KindB1.data3.X) - check "coijoiwcnkwle2" (sprintf "%A" KindB1.data1) "{ X = 1 }" + check "coijoiwcnkwle2" (sprintf "%A" KindB1.data1) "{| X = 1 |}" module Tests2 = let testAccess = (KindB2.data1.X, KindB2.data3.X, KindB2.data3.Y) - check "coijoiwcnkwle3" (sprintf "%A" KindB2.data1) "{ X = 1 }" + check "coijoiwcnkwle3" (sprintf "%A" KindB2.data1) "{| X = 1 |}" let _ = (KindB2.data1 = KindB2.data1) @@ -49,7 +49,7 @@ module CrossAssemblyTest = check "vrknvio1" (SampleAPI.SampleFunction {| A=1; B = "abc" |}) 4 // note, this is creating an instance of an anonymous record from another assembly. check "vrknvio2" (SampleAPI.SampleFunctionAcceptingList [ {| A=1; B = "abc" |}; {| A=2; B = "def" |} ]) [4; 5] // note, this is creating an instance of an anonymous record from another assembly. check "vrknvio3" (let d = SampleAPI.SampleFunctionReturningAnonRecd() in d.A + d.B.Length) 4 - check "vrknvio4" (let d = SampleAPIStruct.SampleFunctionReturningAnonRecd() in d.ToString()) ("{ A = 1\n " + """B = "abc" }""") + check "vrknvio4" (let d = SampleAPIStruct.SampleFunctionReturningAnonRecd() in d.ToString()) ("struct {| A = 1\n " + """B = "abc" |}""") tests() module CrossAssemblyTestStruct = diff --git a/tests/fsharp/core/printing/output.1000.stdout.bsl b/tests/fsharp/core/printing/output.1000.stdout.bsl index c856eef8fc6..bf2d4d45814 100644 --- a/tests/fsharp/core/printing/output.1000.stdout.bsl +++ b/tests/fsharp/core/printing/output.1000.stdout.bsl @@ -2810,7 +2810,7 @@ val it: unit = () > {"AnonRecordField2":10} val it: unit = () -> val it: {| AnonRecordField2: int |} = { AnonRecordField2 = 11 } +> val it: {| AnonRecordField2: int |} = {| AnonRecordField2 = 11 |} module FSI_0326.Project.fsproj @@ -2837,10 +2837,10 @@ val test4a: string = "{"MutableField4":15}" > type R5 = {| AnonRecordField5: int |} val test5a: string = "{"AnonRecordField5":17}" -> val test5b: R5 = { AnonRecordField5 = 17 } +> val test5b: R5 = {| AnonRecordField5 = 17 |} > val test5c: string = "{"AnonRecordField5":18}" -> val test5d: R5 = { AnonRecordField5 = 18 } +> val test5d: R5 = {| AnonRecordField5 = 18 |} > > > diff --git a/tests/fsharp/core/printing/output.200.stdout.bsl b/tests/fsharp/core/printing/output.200.stdout.bsl index f631daf9b1e..20c5d9517f7 100644 --- a/tests/fsharp/core/printing/output.200.stdout.bsl +++ b/tests/fsharp/core/printing/output.200.stdout.bsl @@ -2055,7 +2055,7 @@ val it: unit = () > {"AnonRecordField2":10} val it: unit = () -> val it: {| AnonRecordField2: int |} = { AnonRecordField2 = 11 } +> val it: {| AnonRecordField2: int |} = {| AnonRecordField2 = 11 |} module FSI_0326.Project.fsproj @@ -2082,10 +2082,10 @@ val test4a: string = "{"MutableField4":15}" > type R5 = {| AnonRecordField5: int |} val test5a: string = "{"AnonRecordField5":17}" -> val test5b: R5 = { AnonRecordField5 = 17 } +> val test5b: R5 = {| AnonRecordField5 = 17 |} > val test5c: string = "{"AnonRecordField5":18}" -> val test5d: R5 = { AnonRecordField5 = 18 } +> val test5d: R5 = {| AnonRecordField5 = 18 |} > > > diff --git a/tests/fsharp/core/printing/output.multiemit.stdout.bsl b/tests/fsharp/core/printing/output.multiemit.stdout.bsl index 61e575a929c..7b0e7a67d83 100644 --- a/tests/fsharp/core/printing/output.multiemit.stdout.bsl +++ b/tests/fsharp/core/printing/output.multiemit.stdout.bsl @@ -6357,7 +6357,7 @@ val it: unit = () > {"AnonRecordField2":10} val it: unit = () -> val it: {| AnonRecordField2: int |} = { AnonRecordField2 = 11 } +> val it: {| AnonRecordField2: int |} = {| AnonRecordField2 = 11 |} module FSI_0325.Project.fsproj @@ -6384,10 +6384,10 @@ val test4a: string = "{"MutableField4":15}" > type R5 = {| AnonRecordField5: int |} val test5a: string = "{"AnonRecordField5":17}" -> val test5b: R5 = { AnonRecordField5 = 17 } +> val test5b: R5 = {| AnonRecordField5 = 17 |} > val test5c: string = "{"AnonRecordField5":18}" -> val test5d: R5 = { AnonRecordField5 = 18 } +> val test5d: R5 = {| AnonRecordField5 = 18 |} > > > diff --git a/tests/fsharp/core/printing/output.off.stdout.bsl b/tests/fsharp/core/printing/output.off.stdout.bsl index 034110b2168..ae3ab918b37 100644 --- a/tests/fsharp/core/printing/output.off.stdout.bsl +++ b/tests/fsharp/core/printing/output.off.stdout.bsl @@ -1824,7 +1824,7 @@ val it: unit > {"AnonRecordField2":10} val it: unit = () -> val it: {| AnonRecordField2: int |} = { AnonRecordField2 = 11 } +> val it: {| AnonRecordField2: int |} = {| AnonRecordField2 = 11 |} module FSI_0326.Project.fsproj diff --git a/tests/fsharp/core/printing/output.stdout.bsl b/tests/fsharp/core/printing/output.stdout.bsl index 61e575a929c..7b0e7a67d83 100644 --- a/tests/fsharp/core/printing/output.stdout.bsl +++ b/tests/fsharp/core/printing/output.stdout.bsl @@ -6357,7 +6357,7 @@ val it: unit = () > {"AnonRecordField2":10} val it: unit = () -> val it: {| AnonRecordField2: int |} = { AnonRecordField2 = 11 } +> val it: {| AnonRecordField2: int |} = {| AnonRecordField2 = 11 |} module FSI_0325.Project.fsproj @@ -6384,10 +6384,10 @@ val test4a: string = "{"MutableField4":15}" > type R5 = {| AnonRecordField5: int |} val test5a: string = "{"AnonRecordField5":17}" -> val test5b: R5 = { AnonRecordField5 = 17 } +> val test5b: R5 = {| AnonRecordField5 = 17 |} > val test5c: string = "{"AnonRecordField5":18}" -> val test5d: R5 = { AnonRecordField5 = 18 } +> val test5d: R5 = {| AnonRecordField5 = 18 |} > > >