From c00b32e93a4099a656138a42fd84859adfed488b Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sat, 14 Feb 2026 15:30:33 +0200 Subject: [PATCH 1/3] Add generic print and println functions to FSharp.Core Add inline `print: 'T -> unit` and `println: 'T -> unit` to ExtraTopLevelOperators. These use the existing `string` function for conversion (InvariantCulture for IFormattable, .ToString() fallback) and write to Console.Out. RFC FS-1125 --- docs/release-notes/.FSharp.Core/10.0.300.md | 2 + src/FSharp.Core/fslib-extra-pervasives.fs | 8 ++ src/FSharp.Core/fslib-extra-pervasives.fsi | 27 +++++++ ...p.Core.SurfaceArea.netstandard20.debug.bsl | 2 + ...Core.SurfaceArea.netstandard20.release.bsl | 2 + ...p.Core.SurfaceArea.netstandard21.debug.bsl | 2 + ...Core.SurfaceArea.netstandard21.release.bsl | 2 + .../FSharp.Core.UnitTests.fsproj | 1 + .../Microsoft.FSharp.Core/PrintTests.fs | 76 +++++++++++++++++++ 9 files changed, 122 insertions(+) create mode 100644 tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Core/PrintTests.fs diff --git a/docs/release-notes/.FSharp.Core/10.0.300.md b/docs/release-notes/.FSharp.Core/10.0.300.md index c6012363182..125844e2a49 100644 --- a/docs/release-notes/.FSharp.Core/10.0.300.md +++ b/docs/release-notes/.FSharp.Core/10.0.300.md @@ -2,6 +2,8 @@ ### Added +* Added generic `print` and `println` functions (`'T -> unit`) to `ExtraTopLevelOperators` for simple value printing to stdout. ([RFC FS-1125](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1125-print-printn-functions.md), [PR #19265](https://github.com/dotnet/fsharp/pull/19265)) + ### Changed * Added complexity documentation (Big-O notation) to all 462 functions across Array, List, Seq, Map, and Set collection modules. ([PR #19240](https://github.com/dotnet/fsharp/pull/19240)) diff --git a/src/FSharp.Core/fslib-extra-pervasives.fs b/src/FSharp.Core/fslib-extra-pervasives.fs index 924f057b7e1..6cce8b96969 100644 --- a/src/FSharp.Core/fslib-extra-pervasives.fs +++ b/src/FSharp.Core/fslib-extra-pervasives.fs @@ -277,6 +277,14 @@ module ExtraTopLevelOperators = let eprintfn format = Printf.eprintfn format + [] + let inline print (value: 'T) = + Console.Out.Write(string value) + + [] + let inline println (value: 'T) = + Console.Out.WriteLine(string value) + [] let async = AsyncBuilder() diff --git a/src/FSharp.Core/fslib-extra-pervasives.fsi b/src/FSharp.Core/fslib-extra-pervasives.fsi index 1e38995a2bd..9889adc163d 100644 --- a/src/FSharp.Core/fslib-extra-pervasives.fsi +++ b/src/FSharp.Core/fslib-extra-pervasives.fsi @@ -56,6 +56,33 @@ module ExtraTopLevelOperators = [] val eprintfn: format: Printf.TextWriterFormat<'T> -> 'T + /// Converts the value to a string using the string operator and writes it to the standard output. + /// + /// The value to print. + /// + /// + /// + /// print "Hello, " + /// print "World!" + /// // output: Hello, World! + /// + /// + [] + val inline print: value: 'T -> unit + + /// Converts the value to a string using the string operator and writes it to the standard output, followed by a newline. + /// + /// The value to print. + /// + /// + /// + /// println "Hello, World!" + /// // output: Hello, World! + /// + /// + [] + val inline println: value: 'T -> unit + /// Print to a string using the given format. /// /// The formatter. diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl index 5bd39b09a39..617e5087293 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl @@ -1073,6 +1073,8 @@ Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormatToStringThenFail[T,TR Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormatToString[T](Microsoft.FSharp.Core.PrintfFormat`4[T,Microsoft.FSharp.Core.Unit,System.String,System.String]) Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormatToTextWriter[T](System.IO.TextWriter, Microsoft.FSharp.Core.PrintfFormat`4[T,System.IO.TextWriter,Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]) Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormat[T](Microsoft.FSharp.Core.PrintfFormat`4[T,System.IO.TextWriter,Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]) +Microsoft.FSharp.Core.ExtraTopLevelOperators: Void PrintValue[T](T) +Microsoft.FSharp.Core.ExtraTopLevelOperators: Void PrintValueLine[T](T) Microsoft.FSharp.Core.ExtraTopLevelOperators: T SpliceExpression[T](Microsoft.FSharp.Quotations.FSharpExpr`1[T]) Microsoft.FSharp.Core.ExtraTopLevelOperators: T SpliceUntypedExpression[T](Microsoft.FSharp.Quotations.FSharpExpr) Microsoft.FSharp.Core.ExtraTopLevelOperators: T[,] CreateArray2D[a,T](System.Collections.Generic.IEnumerable`1[a]) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl index 8096acb15a1..779e1740d75 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl @@ -1073,6 +1073,8 @@ Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormatToStringThenFail[T,TR Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormatToString[T](Microsoft.FSharp.Core.PrintfFormat`4[T,Microsoft.FSharp.Core.Unit,System.String,System.String]) Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormatToTextWriter[T](System.IO.TextWriter, Microsoft.FSharp.Core.PrintfFormat`4[T,System.IO.TextWriter,Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]) Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormat[T](Microsoft.FSharp.Core.PrintfFormat`4[T,System.IO.TextWriter,Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]) +Microsoft.FSharp.Core.ExtraTopLevelOperators: Void PrintValue[T](T) +Microsoft.FSharp.Core.ExtraTopLevelOperators: Void PrintValueLine[T](T) Microsoft.FSharp.Core.ExtraTopLevelOperators: T SpliceExpression[T](Microsoft.FSharp.Quotations.FSharpExpr`1[T]) Microsoft.FSharp.Core.ExtraTopLevelOperators: T SpliceUntypedExpression[T](Microsoft.FSharp.Quotations.FSharpExpr) Microsoft.FSharp.Core.ExtraTopLevelOperators: T[,] CreateArray2D[a,T](System.Collections.Generic.IEnumerable`1[a]) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl index 95ebeea8a37..61e0d754a3f 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl @@ -1076,6 +1076,8 @@ Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormatToStringThenFail[T,TR Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormatToString[T](Microsoft.FSharp.Core.PrintfFormat`4[T,Microsoft.FSharp.Core.Unit,System.String,System.String]) Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormatToTextWriter[T](System.IO.TextWriter, Microsoft.FSharp.Core.PrintfFormat`4[T,System.IO.TextWriter,Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]) Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormat[T](Microsoft.FSharp.Core.PrintfFormat`4[T,System.IO.TextWriter,Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]) +Microsoft.FSharp.Core.ExtraTopLevelOperators: Void PrintValue[T](T) +Microsoft.FSharp.Core.ExtraTopLevelOperators: Void PrintValueLine[T](T) Microsoft.FSharp.Core.ExtraTopLevelOperators: T SpliceExpression[T](Microsoft.FSharp.Quotations.FSharpExpr`1[T]) Microsoft.FSharp.Core.ExtraTopLevelOperators: T SpliceUntypedExpression[T](Microsoft.FSharp.Quotations.FSharpExpr) Microsoft.FSharp.Core.ExtraTopLevelOperators: T[,] CreateArray2D[a,T](System.Collections.Generic.IEnumerable`1[a]) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl index c4106d360c4..551b1c2bd80 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl @@ -1076,6 +1076,8 @@ Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormatToStringThenFail[T,TR Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormatToString[T](Microsoft.FSharp.Core.PrintfFormat`4[T,Microsoft.FSharp.Core.Unit,System.String,System.String]) Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormatToTextWriter[T](System.IO.TextWriter, Microsoft.FSharp.Core.PrintfFormat`4[T,System.IO.TextWriter,Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]) Microsoft.FSharp.Core.ExtraTopLevelOperators: T PrintFormat[T](Microsoft.FSharp.Core.PrintfFormat`4[T,System.IO.TextWriter,Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]) +Microsoft.FSharp.Core.ExtraTopLevelOperators: Void PrintValue[T](T) +Microsoft.FSharp.Core.ExtraTopLevelOperators: Void PrintValueLine[T](T) Microsoft.FSharp.Core.ExtraTopLevelOperators: T SpliceExpression[T](Microsoft.FSharp.Quotations.FSharpExpr`1[T]) Microsoft.FSharp.Core.ExtraTopLevelOperators: T SpliceUntypedExpression[T](Microsoft.FSharp.Quotations.FSharpExpr) Microsoft.FSharp.Core.ExtraTopLevelOperators: T[,] CreateArray2D[a,T](System.Collections.Generic.IEnumerable`1[a]) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj b/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj index 16e45542174..b250d992e5e 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj @@ -73,6 +73,7 @@ + diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Core/PrintTests.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Core/PrintTests.fs new file mode 100644 index 00000000000..e70ce3928a9 --- /dev/null +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Core/PrintTests.fs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +// Various tests for: +// Microsoft.FSharp.Core.ExtraTopLevelOperators.print +// Microsoft.FSharp.Core.ExtraTopLevelOperators.println + +namespace FSharp.Core.UnitTests + +open System +open System.IO +open Xunit + +[] +type PrintTests() = + + let captureConsoleOut (f: unit -> unit) = + let oldOut = Console.Out + use sw = new StringWriter() + Console.SetOut(sw) + try + f () + sw.ToString() + finally + Console.SetOut(oldOut) + + [] + member _.``print writes string value``() = + let result = captureConsoleOut (fun () -> print "hello") + Assert.Equal("hello", result) + + [] + member _.``print writes integer value``() = + let result = captureConsoleOut (fun () -> print 42) + Assert.Equal("42", result) + + [] + member _.``print writes float with InvariantCulture``() = + let result = captureConsoleOut (fun () -> print 3.14) + Assert.Equal("3.14", result) + + [] + member _.``print writes bool value``() = + let result = captureConsoleOut (fun () -> print true) + Assert.Equal("True", result) + + [] + member _.``print writes Some value``() = + let result = captureConsoleOut (fun () -> print (Some 42)) + Assert.Equal("Some(42)", result) + + [] + member _.``print writes None value``() = + let result = captureConsoleOut (fun () -> print None) + Assert.Equal("", result) + + [] + member _.``print writes list value``() = + let result = captureConsoleOut (fun () -> print [1; 2; 3]) + Assert.Equal("[1; 2; 3]", result) + + [] + member _.``println writes value followed by newline``() = + let result = captureConsoleOut (fun () -> println "hello") + Assert.Equal("hello" + Environment.NewLine, result) + + [] + member _.``multiple prints concatenate``() = + let result = captureConsoleOut (fun () -> + print "Hello, " + print "World!") + Assert.Equal("Hello, World!", result) + + [] + member _.``println writes integer with newline``() = + let result = captureConsoleOut (fun () -> println 42) + Assert.Equal("42" + Environment.NewLine, result) From 064148466df5bcab399520695b5c32e4eb57e628 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sat, 14 Feb 2026 16:59:40 +0200 Subject: [PATCH 2/3] Add IL baseline tests for print and println Verify that inline specialization produces direct calls to Int32.ToString, Double.ToString with InvariantCulture, and Console.Out.Write/WriteLine for the respective types. --- .../EmittedIL/PrintFunction.fs | 70 +++++++++++++++++++ .../FSharp.Compiler.ComponentTests.fsproj | 1 + 2 files changed, 71 insertions(+) create mode 100644 tests/FSharp.Compiler.ComponentTests/EmittedIL/PrintFunction.fs diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/PrintFunction.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/PrintFunction.fs new file mode 100644 index 00000000000..f0f7581ec4e --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/PrintFunction.fs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace EmittedIL + +open Xunit +open FSharp.Test.Compiler + +module PrintFunction = + + [] + let ``print with int specializes to Int32 ToString with InvariantCulture``() = + FSharp """ +module PrintInt + +let printInt () = print 42 + """ + |> withOptimize + |> compile + |> shouldSucceed + |> verifyIL [ + """call class [netstandard]System.IO.TextWriter [netstandard]System.Console::get_Out()""" + """call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture()""" + """call instance string [netstandard]System.Int32::ToString(string,""" + """callvirt instance void [netstandard]System.IO.TextWriter::Write(string)"""] + + [] + let ``println with int specializes to Int32 ToString with InvariantCulture``() = + FSharp """ +module PrintlnInt + +let printlnInt () = println 42 + """ + |> withOptimize + |> compile + |> shouldSucceed + |> verifyIL [ + """call class [netstandard]System.IO.TextWriter [netstandard]System.Console::get_Out()""" + """call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture()""" + """call instance string [netstandard]System.Int32::ToString(string,""" + """callvirt instance void [netstandard]System.IO.TextWriter::WriteLine(string)"""] + + [] + let ``print with string writes to Console Out``() = + FSharp """ +module PrintString + +let printStr () = print "hello" + """ + |> withOptimize + |> compile + |> shouldSucceed + |> verifyIL [ + """call class [netstandard]System.IO.TextWriter [netstandard]System.Console::get_Out()""" + """callvirt instance void [netstandard]System.IO.TextWriter::Write(string)"""] + + [] + let ``println with float specializes to Double ToString with InvariantCulture``() = + FSharp """ +module PrintlnFloat + +let printlnFloat () = println 3.14 + """ + |> withOptimize + |> compile + |> shouldSucceed + |> verifyIL [ + """call class [netstandard]System.IO.TextWriter [netstandard]System.Console::get_Out()""" + """call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture()""" + """call instance string [netstandard]System.Double::ToString(string,""" + """callvirt instance void [netstandard]System.IO.TextWriter::WriteLine(string)"""] diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index f236ca6599d..c67f0b77550 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -146,6 +146,7 @@ + From 485c8dcd9619d7c789b20f6c96cf858b3b239b98 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sun, 15 Feb 2026 09:51:45 +0200 Subject: [PATCH 3/3] Add static optimizations for string, char, and bool For culture-independent types (string, char, bool), bypass the `string` operator and call the appropriate TextWriter.Write/WriteLine overload directly. Numeric types (int, float, etc.) must still go through `string` to ensure InvariantCulture formatting, since TextWriter.Write(int/float) uses the writer's FormatProvider which is CurrentCulture for Console.Out. Add IL tests verifying char and bool use their direct overloads. --- src/FSharp.Core/fslib-extra-pervasives.fs | 9 ++++++ .../EmittedIL/PrintFunction.fs | 30 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/FSharp.Core/fslib-extra-pervasives.fs b/src/FSharp.Core/fslib-extra-pervasives.fs index 6cce8b96969..ceaf56a0c45 100644 --- a/src/FSharp.Core/fslib-extra-pervasives.fs +++ b/src/FSharp.Core/fslib-extra-pervasives.fs @@ -280,10 +280,19 @@ module ExtraTopLevelOperators = [] let inline print (value: 'T) = Console.Out.Write(string value) + // Culture-independent types can bypass 'string' and use TextWriter overloads directly. + // Numeric types must go through 'string' to ensure InvariantCulture formatting, + // since TextWriter.Write(int/float/...) uses the writer's FormatProvider (CurrentCulture). + when 'T : string = Console.Out.Write((# "" value : string #)) + when 'T : char = Console.Out.Write((# "" value : char #)) + when 'T : bool = Console.Out.Write((# "" value : bool #)) [] let inline println (value: 'T) = Console.Out.WriteLine(string value) + when 'T : string = Console.Out.WriteLine((# "" value : string #)) + when 'T : char = Console.Out.WriteLine((# "" value : char #)) + when 'T : bool = Console.Out.WriteLine((# "" value : bool #)) [] let async = AsyncBuilder() diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/PrintFunction.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/PrintFunction.fs index f0f7581ec4e..9fd319c6b6e 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/PrintFunction.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/PrintFunction.fs @@ -40,7 +40,7 @@ let printlnInt () = println 42 """callvirt instance void [netstandard]System.IO.TextWriter::WriteLine(string)"""] [] - let ``print with string writes to Console Out``() = + let ``print with string calls Write directly``() = FSharp """ module PrintString @@ -53,6 +53,34 @@ let printStr () = print "hello" """call class [netstandard]System.IO.TextWriter [netstandard]System.Console::get_Out()""" """callvirt instance void [netstandard]System.IO.TextWriter::Write(string)"""] + [] + let ``print with char calls Write char overload``() = + FSharp """ +module PrintChar + +let printChar () = print 'A' + """ + |> withOptimize + |> compile + |> shouldSucceed + |> verifyIL [ + """call class [netstandard]System.IO.TextWriter [netstandard]System.Console::get_Out()""" + """callvirt instance void [netstandard]System.IO.TextWriter::Write(char)"""] + + [] + let ``print with bool calls Write bool overload``() = + FSharp """ +module PrintBool + +let printBool () = print true + """ + |> withOptimize + |> compile + |> shouldSucceed + |> verifyIL [ + """call class [netstandard]System.IO.TextWriter [netstandard]System.Console::get_Out()""" + """callvirt instance void [netstandard]System.IO.TextWriter::Write(bool)"""] + [] let ``println with float specializes to Double ToString with InvariantCulture``() = FSharp """