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/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
44 changes: 35 additions & 9 deletions src/Compiler/Utilities/sformat.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 "<"
Expand Down Expand Up @@ -487,11 +487,17 @@ module ReflectUtils =
| Value
| Reference

[<RequireQualifiedAccess; StructuralComparison; StructuralEquality>]
type RecordKind =
| Nominal
| AnonReference
| AnonStruct

[<NoEquality; NoComparison>]
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
Expand Down Expand Up @@ -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)
)
Expand Down Expand Up @@ -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) =
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<T>. Note: "null" is never a valid list value.
(not (isNull x)) && isListType (x.GetType())
Expand Down
2 changes: 1 addition & 1 deletion tests/AheadOfTime/Trimming/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
4 changes: 2 additions & 2 deletions tests/AheadOfTime/Trimming/check.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@
<Compile Include="Scripting\TypeCheckOnlyTests.fs" />
<Compile Include="Scripting\PreferredUiLangTests.fs" />
<Compile Include="InteractiveSession\Misc.fs" />
<Compile Include="InteractiveSession\AnonRecordPrinting.fs" />
<Compile Include="TypeChecks\TypeRelations.fs" />
<Compile Include="TypeChecks\SeqTypeCheckTests.fs" />
<Compile Include="TypeChecks\CheckDeclarationsTests.fs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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.

[<Fact>]
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

[<Fact>]
let ``Anonymous record with single field prints with bar braces``() =
Fsx """
{| X = 1 |};;
()
"""
|> withOptions ["--nologo"]
|> runFsi
|> shouldSucceed
|> withStdOutContains "= {| X = 1 |}"
|> ignore

[<Fact>]
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

[<Fact>]
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) ---

[<Fact>]
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) ---

[<Fact>]
let ``Tuple printing unchanged``() =
Fsx """
(1, "x");;
()
"""
|> withOptions ["--nologo"]
|> runFsi
|> shouldSucceed
|> withStdOutContains "(1, \"x\")"
|> ignore

[<Fact>]
let ``Discriminated union printing unchanged``() =
Fsx """
type DU = A of int | B of string
A 42;;
()
"""
|> withOptions ["--nologo"]
|> runFsi
|> shouldSucceed
|> withStdOutContains "A 42"
|> ignore

[<Fact>]
let ``List printing unchanged``() =
Fsx """
[1; 2; 3];;
()
"""
|> withOptions ["--nologo"]
|> runFsi
|> shouldSucceed
|> withStdOutContains "[1; 2; 3]"
|> ignore
4 changes: 2 additions & 2 deletions tests/fsharp/core/anon/lib.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions tests/fsharp/core/anon/test.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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 =
Expand Down
6 changes: 3 additions & 3 deletions tests/fsharp/core/printing/output.1000.stdout.bsl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 |}

> > >
6 changes: 3 additions & 3 deletions tests/fsharp/core/printing/output.200.stdout.bsl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 |}

> > >
6 changes: 3 additions & 3 deletions tests/fsharp/core/printing/output.multiemit.stdout.bsl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 |}

> > >
2 changes: 1 addition & 1 deletion tests/fsharp/core/printing/output.off.stdout.bsl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading