dotnet / fsharp

The F# compiler, F# core library, F# language service, and F# tooling integration for Visual Studio
https://dotnet.microsoft.com/languages/fsharp
MIT License
3.89k stars 782 forks source link

Structured plain text formatting on private values ignore width specifiers #14557

Open calebho opened 1 year ago

calebho commented 1 year ago

Please provide a succinct description of the issue.

Repro steps

Provide the steps required to reproduce the problem:

Script:

type Public = { Foo: string; Bar: int }

type private Private = { Baz: string; Quux: int option }

let pub = { Foo = "hello"; Bar = 42 }
let private priv = { Baz = "world"; Quux = Some 100 }

printfn "public"
printfn $"%0A{pub}"
printfn ""
printfn "private without '+'"
printfn $"%0A{priv}"
printfn ""
printfn "private with '+'"
printfn $"%0+A{priv}

Run with: dotnet fsi Repro.fsx

Expected behavior

The documentation states,

Specifying a print width of 0 results in no print width being used. A single line of text will result, except where embedded strings in the output contain line breaks.

No fields of priv are embedded strings containing line breaks, therefore its string representation should be one line.

Actual behavior The second group of prints, i.e. "private without '+'", spans more than one line:

public
{ Foo = "hello" Bar = 42 }

private without '+'
{ Baz = "world"
  Quux = Some 100 }

private with '+'
{ Baz = "world" Quux = Some 100 }

Known workarounds

Add + to the format string.

Related information

Provide any related information (optional):

JaggerJo commented 1 year ago

After creating a failing test / repro for this bug and digging into it a bit deeper this seems to behave up to spec.

Here is a runnable sample in sharplab.

Using %A+0 works because it allows access to the private structure of the record as stated in the documentation.

Using %A0 on the other hand only has access to the public structure. Even calling FSharpType.IsRecord(...) returns false for private record types. This means we fallback to the ToString implementation of the instance type.

https://github.com/dotnet/fsharp/blob/1d819b92435c955a50b0cf5d97104e6ad431fe1f/src/Compiler/Utilities/sformat.fs#L543-L551

I think we end up calling this function with BindingFlags.Public except when a + is included in the format specifier then we use indingFlags.Public ||| BindingFlags.NonPublic instead.

https://github.com/dotnet/fsharp/blob/1d819b92435c955a50b0cf5d97104e6ad431fe1f/src/FSharp.Core/printf.fs#L939-L943