From 4e8c86c406b6fdb46c104d3bec1a0342b80ce0a2 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Sun, 21 Jun 2026 20:06:45 +0100 Subject: [PATCH 01/29] Add a compiler intrinsic for the 'string' operator Adds string_operator_info / mkCallStringOperator so generated code can call Operators.string. These lines are duplicated by the interpolated-string PR (dotnet/fsharp#19971); kept identical there so a future merge resolves cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Compiler/TypedTree/TcGlobals.fs | 2 ++ src/Compiler/TypedTree/TcGlobals.fsi | 2 ++ src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs | 3 +++ src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi | 3 +++ 4 files changed, 10 insertions(+) diff --git a/src/Compiler/TypedTree/TcGlobals.fs b/src/Compiler/TypedTree/TcGlobals.fs index 6dfbd42f98c..b2eb4cd0f74 100644 --- a/src/Compiler/TypedTree/TcGlobals.fs +++ b/src/Compiler/TypedTree/TcGlobals.fs @@ -792,6 +792,7 @@ type TcGlobals( let v_byte_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "byte" , None , Some "ToByte", [vara], ([[varaTy]], v_byte_ty)) let v_sbyte_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "sbyte" , None , Some "ToSByte", [vara], ([[varaTy]], v_sbyte_ty)) + let v_string_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "string", None, Some "ToString", [vara], ([[varaTy]], v_string_ty)) let v_int16_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "int16" , None , Some "ToInt16", [vara], ([[varaTy]], v_int16_ty)) let v_uint16_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "uint16" , None , Some "ToUInt16", [vara], ([[varaTy]], v_uint16_ty)) let v_int32_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "int32" , None , Some "ToInt32", [vara], ([[varaTy]], v_int32_ty)) @@ -1594,6 +1595,7 @@ type TcGlobals( member _.byte_operator_info = v_byte_operator_info member _.sbyte_operator_info = v_sbyte_operator_info + member _.string_operator_info = v_string_operator_info member _.int16_operator_info = v_int16_operator_info member _.uint16_operator_info = v_uint16_operator_info member _.int32_operator_info = v_int32_operator_info diff --git a/src/Compiler/TypedTree/TcGlobals.fsi b/src/Compiler/TypedTree/TcGlobals.fsi index e27bc1605a2..7c00bea8f1b 100644 --- a/src/Compiler/TypedTree/TcGlobals.fsi +++ b/src/Compiler/TypedTree/TcGlobals.fsi @@ -939,6 +939,8 @@ type internal TcGlobals = member sbyte_operator_info: IntrinsicValRef + member string_operator_info: IntrinsicValRef + member sbyte_tcr: TypedTree.EntityRef member sbyte_ty: TypedTree.TType diff --git a/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs b/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs index bdebaf40ff5..42ec600088e 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs @@ -1366,6 +1366,9 @@ module internal Makers = let mkCallNewFormat (g: TcGlobals) m aty bty cty dty ety formatStringExpr = mkApps g (typedExprForIntrinsic g m g.new_format_info, [ [ aty; bty; cty; dty; ety ] ], [ formatStringExpr ], m) + let mkCallStringOperator (g: TcGlobals) m argTy e = + mkApps g (typedExprForIntrinsic g m g.string_operator_info, [ [ argTy ] ], [ e ], m) + let tryMkCallBuiltInWitness (g: TcGlobals) traitInfo argExprs m = let info, tinst = g.MakeBuiltInWitnessInfo traitInfo let vref = ValRefForIntrinsic info diff --git a/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi b/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi index ad90c5c818c..2b6f296ac6d 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi +++ b/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi @@ -208,6 +208,9 @@ module internal Makers = val mkCallNewFormat: TcGlobals -> range -> TType -> TType -> TType -> TType -> TType -> formatStringExpr: Expr -> Expr + /// Build a call to the 'string' operator (Operators.ToString) at the given argument type. + val mkCallStringOperator: TcGlobals -> range -> argTy: TType -> Expr -> Expr + val mkCallGetGenericComparer: TcGlobals -> range -> Expr val mkCallGetGenericEREqualityComparer: TcGlobals -> range -> Expr From 1d5769cfd6728ad71f2839835cac0be099830866 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Sun, 21 Jun 2026 16:26:43 +0100 Subject: [PATCH 02/29] Generate a match-based ToString for unions under --reflectionfree Under --reflectionfree the union ToString previously emitted nothing, so DUs fell back to Object.ToString() (the namespace-qualified type name). Instead generate a match over the cases that builds "CaseName(f0, f1, ...)" using the 'string' operator on each field, via a TypedTree expression fed to CodeGenMethodForExpr. This recurses naturally into nested unions and is reflection-free. The default (sprintf "%+A") path is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Compiler/CodeGen/IlxGen.fs | 72 ++++++++++++++++++- .../CompilerOptions/fsc/reflectionfree.fs | 11 ++- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index 94b9096fb1e..efd3c3e6e1e 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -11279,6 +11279,76 @@ and GenPrintingMethod cenv eenv methName ilThisTy m = | _ -> () ] +/// Generate the 'ToString' method for a union type. Normally this calls 'sprintf "%+A"' (see +/// GenPrintingMethod). Under reflection-free code generation 'sprintf' is unavailable, so instead emit a +/// match over the cases that builds "CaseName(f0, f1, ...)" using the 'string' operator on each field. +and GenUnionToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, ilThisTy: ILType, tcref: TyconRef, m: range) = + let g = cenv.g + + if not g.useReflectionFreeCodeGen then + GenPrintingMethod cenv eenv "ToString" ilThisTy m + else + let tinst, thisv, thise = + let tinst, ty = generalizeTyconRef g tcref + let thisv, thise = mkCompGenLocal m "this" (if isStructTy g ty then mkByrefTy g ty else ty) + tinst, thisv, thise + + let mbuilder = MatchBuilder(DebugPointAtBinding.NoneAtInvisible, m) + + let mkResult (ucase: UnionCase) = + let cref = tcref.MakeNestedUnionCaseRef ucase + let rfields = ucase.RecdFields + + if isNil rfields then + mkString g m ucase.DisplayName + else + // provene is an expression that has been proven to be of this case (the value itself for + // struct unions, otherwise a 'UnionCaseProof'), from which fields can be read. + let mkBody (provene: Expr) = + let fieldStrs = + rfields + |> List.mapi (fun j _ -> + let fe = mkUnionCaseFieldGetProvenViaExprAddr (provene, cref, tinst, j, m) + mkCallStringOperator g m (tyOfExpr g fe) fe) + + let sep = mkString g m ", " + + let fieldsWithSeps = + fieldStrs |> List.mapi (fun i fe -> if i = 0 then [ fe ] else [ sep; fe ]) |> List.concat + + let parts = mkString g m (ucase.DisplayName + "(") :: fieldsWithSeps @ [ mkString g m ")" ] + mkStaticCall_String_Concat_Array g m (mkArray (g.string_ty, parts, m)) + + if cref.Tycon.IsStructOrEnumTycon then + mkBody thise + else + let ucv, ucve = mkCompGenLocal m "thisCast" (mkProvenUnionCaseTy cref tinst) + mkCompGenLet m ucv (mkUnionCaseProof (thise, cref, tinst, m)) (mkBody ucve) + + let cases = + tcref.UnionCasesAsList + |> List.map (fun ucase -> + let cref = tcref.MakeNestedUnionCaseRef ucase + mkCase (DecisionTreeTest.UnionCase(cref, tinst), mbuilder.AddResultTarget(mkResult ucase))) + + let dtree = TDSwitch(thise, cases, None, m) + let matchExpr = mbuilder.Close(dtree, m, g.string_ty) + + let eenvForMeth = AddStorageForLocalVals g [ (thisv, Arg 0) ] eenv + let ilMethodBody = CodeGenMethodForExpr cenv mgbuf ([], "ToString", eenvForMeth, 0, Some thisv, matchExpr, Return) + + let mdef = + mkILNonGenericVirtualInstanceMethod ( + "ToString", + ILMemberAccess.Public, + [], + mkILReturn g.ilg.typ_String, + MethodBody.IL(InterruptibleLazy.FromValue ilMethodBody) + ) + + let mdef = mdef.With(customAttrs = mkILCustomAttrs [ g.CompilerGeneratedAttribute ]) + [ mdef ] + and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option = let g = cenv.g let tcref = mkLocalTyconRef tycon @@ -11887,7 +11957,7 @@ and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option | _ -> () | TFSharpTyconRepr { fsobjmodel_kind = TFSharpUnion } when not (tycon.HasMember g "ToString" []) -> - yield! GenToStringMethod cenv eenv ilThisTy m + yield! GenUnionToStringMethod(cenv, mgbuf, eenv, ilThisTy, tcref, m) | _ -> () ] diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs index 65b96d7d9c8..f07e2742566 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs @@ -35,15 +35,22 @@ let someCode = """ [] -let ``Records and DUs don't have generated ToString`` () = +let ``Records and classes don't have generated ToString`` () = someCode |> withOptions [ "--reflectionfree" ] |> compileExeAndRun |> shouldSucceed |> withStdOutContains "Thing says: Test+MyRecord" - |> withStdOutContains "Thing says: Test+MyUnion+B" |> withStdOutContains "Thing says: Test+MyClass" +[] +let ``Unions have a generated ToString that matches on the case`` () = + someCode + |> withOptions [ "--reflectionfree" ] + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "Thing says: B(foo)" + [] let ``No debug display attribute`` () = someCode From bfb39395066e5ec71a8690e1ad3dc6c08ba0c37c Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Sun, 21 Jun 2026 16:39:09 +0100 Subject: [PATCH 03/29] Extract mkStringConcat helper for arity-dispatched String.Concat The "concatenate a list of string exprs, picking the cheapest String.Concat overload by arity" pattern was duplicated in CheckExpressions (interpolation lowering) and the optimizer, and our new union ToString used the array overload unconditionally. Extract mkStringConcat into TypedTreeOps.ExprOps and route all three through it. This also lets single-field union cases emit Concat3 instead of allocating a string[] (IlxGen runs after the optimizer, so nothing else would collapse that array form). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Compiler/CodeGen/IlxGen.fs | 2 +- src/Compiler/Optimize/Optimizer.fs | 14 +------------- src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs | 11 +++++++++++ src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi | 4 ++++ 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index efd3c3e6e1e..dd1f84b013d 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -11317,7 +11317,7 @@ and GenUnionToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, fieldStrs |> List.mapi (fun i fe -> if i = 0 then [ fe ] else [ sep; fe ]) |> List.concat let parts = mkString g m (ucase.DisplayName + "(") :: fieldsWithSeps @ [ mkString g m ")" ] - mkStaticCall_String_Concat_Array g m (mkArray (g.string_ty, parts, m)) + mkStringConcat (g, m, parts) if cref.Tycon.IsStructOrEnumTycon then mkBody thise diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index 3a9856d7912..db0c925f544 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -2524,19 +2524,7 @@ and MakeOptimizedSystemStringConcatCall cenv env m args = let args = optimizeArgs args [] - let expr = - match args with - | [ arg ] -> - arg - | [ arg1; arg2 ] -> - mkStaticCall_String_Concat2 g m arg1 arg2 - | [ arg1; arg2; arg3 ] -> - mkStaticCall_String_Concat3 g m arg1 arg2 arg3 - | [ arg1; arg2; arg3; arg4 ] -> - mkStaticCall_String_Concat4 g m arg1 arg2 arg3 arg4 - | args -> - let arg = mkArray (g.string_ty, args, m) - mkStaticCall_String_Concat_Array g m arg + let expr = mkStringConcat (g, m, args) match expr with | Expr.Op(TOp.ILCall(_, _, _, _, _, _, _, ilMethRef, _, _, _) as op, tyargs, args, m) diff --git a/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs b/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs index 42ec600088e..a87c12325a1 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs @@ -1573,6 +1573,17 @@ module internal Makers = m ) + /// Concatenate string-valued expressions, choosing the cheapest String.Concat overload by arity. + /// An empty list yields "" and a singleton yields itself. + let mkStringConcat (g: TcGlobals, m: range, exprs: Expr list) = + match exprs with + | [] -> mkString g m "" + | [ arg ] -> arg + | [ arg1; arg2 ] -> mkStaticCall_String_Concat2 g m arg1 arg2 + | [ arg1; arg2; arg3 ] -> mkStaticCall_String_Concat3 g m arg1 arg2 arg3 + | [ arg1; arg2; arg3; arg4 ] -> mkStaticCall_String_Concat4 g m arg1 arg2 arg3 arg4 + | _ -> mkStaticCall_String_Concat_Array g m (mkArray (g.string_ty, exprs, m)) + // Quotations can't contain any IL. // As a result, we aim to get rid of all IL generation in the typechecker and pattern match // compiler, or else train the quotation generator to understand the generated IL. diff --git a/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi b/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi index 2b6f296ac6d..70379648e63 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi +++ b/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi @@ -449,6 +449,10 @@ module internal Makers = val mkStaticCall_String_Concat_Array: TcGlobals -> range -> Expr -> Expr + /// Concatenate string-valued expressions, choosing the cheapest String.Concat overload by arity. + /// An empty list yields "" and a singleton yields itself. + val mkStringConcat: TcGlobals * range * Expr list -> Expr + val mkDecr: TcGlobals -> range -> Expr -> Expr val mkIncr: TcGlobals -> range -> Expr -> Expr From e5710fa949cbb91b25ac1b651466346d15396150 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Sun, 21 Jun 2026 17:18:36 +0100 Subject: [PATCH 04/29] Fix generated union ToString for generic unions The match-based ToString body is a TypedTree expression codegen'd via CodeGenMethodForExpr, but it was built with `eenv`, which lacks the tycon's type parameters. For generic unions this produced wrong IL: the wrong case branch (always the null-as-true-value case) or a NullReferenceException for single-case unions. Use `eenvinner` (the per-tycon environment) so the generic method body resolves its type parameters. The old sprintf path was unaffected because it emits raw IL off the pre-built ilThisTy. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Compiler/CodeGen/IlxGen.fs | 2 +- .../CompilerOptions/fsc/reflectionfree.fs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index dd1f84b013d..ba3c08b3d6e 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -11957,7 +11957,7 @@ and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option | _ -> () | TFSharpTyconRepr { fsobjmodel_kind = TFSharpUnion } when not (tycon.HasMember g "ToString" []) -> - yield! GenUnionToStringMethod(cenv, mgbuf, eenv, ilThisTy, tcref, m) + yield! GenUnionToStringMethod(cenv, mgbuf, eenvinner, ilThisTy, tcref, m) | _ -> () ] diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs index f07e2742566..74b0e2ecbf8 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs @@ -51,6 +51,30 @@ let ``Unions have a generated ToString that matches on the case`` () = |> shouldSucceed |> withStdOutContains "Thing says: B(foo)" +[] +let ``Generic unions get a correct generated ToString`` () = + FSharp """ +module Test +type Box<'T> = Box of 'T | Empty // single nullary case -> UseNullAsTrueValue representation +type Single<'T> = Just of 'T + +[] +let main _ = + Box 42 |> string |> printfn "%s" + Box (Box 7) |> string |> printfn "%s" // nested generic + (Empty: Box) |> string |> printfn "%s" + Just 5 |> string |> printfn "%s" // single-case generic union + 0 + """ + |> asExe + |> withOptions [ "--reflectionfree" ] + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "Box(42)" + |> withStdOutContains "Box(Box(7))" + |> withStdOutContains "Empty" + |> withStdOutContains "Just(5)" + [] let ``No debug display attribute`` () = someCode From d5712c820992865e53c48c7413cfc9c90cf8722a Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Sun, 21 Jun 2026 18:00:35 +0100 Subject: [PATCH 05/29] Render union ToString fields like option (null -> "null") To make a generated union ToString consistent with how option/list format their contents (LanguagePrimitives.anyToStringShowingNull), format each field as: if (box field) is non-null then 'string field' else "null". Previously a null field rendered as "" (the 'string' operator's null behaviour). Generated inline rather than calling anyToStringShowingNull, which is internal to FSharp.Core and so not callable from user-compiled code. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Compiler/CodeGen/IlxGen.fs | 11 ++++++++++- .../CompilerOptions/fsc/reflectionfree.fs | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index ba3c08b3d6e..80573cc89eb 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -11305,11 +11305,20 @@ and GenUnionToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, // provene is an expression that has been proven to be of this case (the value itself for // struct unions, otherwise a 'UnionCaseProof'), from which fields can be read. let mkBody (provene: Expr) = + // Format each field the same way option/list do (LanguagePrimitives.anyToStringShowingNull): + // render null as "null", otherwise via the 'string' operator. let fieldStrs = rfields |> List.mapi (fun j _ -> let fe = mkUnionCaseFieldGetProvenViaExprAddr (provene, cref, tinst, j, m) - mkCallStringOperator g m (tyOfExpr g fe) fe) + let fieldTy = tyOfExpr g fe + let v, ve = mkCompGenLocal m "field" fieldTy + + mkCompGenLet + m + v + fe + (mkNonNullCond g m g.string_ty (mkCallBox g m fieldTy ve) (mkCallStringOperator g m fieldTy ve) (mkString g m "null"))) let sep = mkString g m ", " diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs index 74b0e2ecbf8..37509a7ca28 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs @@ -75,6 +75,25 @@ let main _ = |> withStdOutContains "Empty" |> withStdOutContains "Just(5)" +[] +let ``Generated ToString renders a null field as "null" like option does`` () = + FSharp """ +module Test +type W = W of string + +[] +let main _ = + W null |> string |> printfn "%s" // null field -> "null" + (Some (null: string)).ToString() |> printfn "%s" // option renders it the same way + 0 + """ + |> asExe + |> withOptions [ "--reflectionfree" ] + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "W(null)" + |> withStdOutContains "Some(null)" + [] let ``No debug display attribute`` () = someCode From 26f0e53d271c061b60a186a521c11baa935e14c8 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Sun, 21 Jun 2026 18:19:05 +0100 Subject: [PATCH 06/29] Tidy reflection-free union ToString tests Normalize union declarations to a leading '|', use System.Console.WriteLine instead of printfn (the printf machinery is what these changes move away from), and make the null-field test compare the union's rendering directly against option's rather than asserting a fixed string. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../CompilerOptions/fsc/reflectionfree.fs | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs index 37509a7ca28..64717a77a74 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs @@ -55,15 +55,17 @@ let ``Unions have a generated ToString that matches on the case`` () = let ``Generic unions get a correct generated ToString`` () = FSharp """ module Test -type Box<'T> = Box of 'T | Empty // single nullary case -> UseNullAsTrueValue representation -type Single<'T> = Just of 'T +type Box<'T> = + | Box of 'T + | Empty +type Single<'T> = | Just of 'T [] let main _ = - Box 42 |> string |> printfn "%s" - Box (Box 7) |> string |> printfn "%s" // nested generic - (Empty: Box) |> string |> printfn "%s" - Just 5 |> string |> printfn "%s" // single-case generic union + Box 42 |> string |> System.Console.WriteLine + Box (Box 7) |> string |> System.Console.WriteLine // nested generic + (Empty: Box) |> string |> System.Console.WriteLine + Just 5 |> string |> System.Console.WriteLine // single-case generic union 0 """ |> asExe @@ -76,23 +78,28 @@ let main _ = |> withStdOutContains "Just(5)" [] -let ``Generated ToString renders a null field as "null" like option does`` () = +let ``Generated ToString renders a field the same way option does`` () = FSharp """ module Test -type W = W of string +type Wrapper = | Wrap of string [] let main _ = - W null |> string |> printfn "%s" // null field -> "null" - (Some (null: string)).ToString() |> printfn "%s" // option renders it the same way + let value: string = null + // A union field should render its content the same way option does. Compare the two directly rather + // than asserting a fixed rendering. "Wrap" and "Some" are both 4 chars, so dropping them leaves the + // field rendering to compare. + let fromUnion = (Wrap value |> string).Substring 4 + let fromOption = ((Some value).ToString()).Substring 4 + if fromUnion = fromOption then System.Console.WriteLine "fields-render-alike" + else System.Console.WriteLine("DIFFER: " + fromUnion + " vs " + fromOption) 0 """ |> asExe |> withOptions [ "--reflectionfree" ] |> compileExeAndRun |> shouldSucceed - |> withStdOutContains "W(null)" - |> withStdOutContains "Some(null)" + |> withStdOutContains "fields-render-alike" [] let ``No debug display attribute`` () = From 87e687027d6e75267bbdd5cfa27d7241574784eb Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Sun, 21 Jun 2026 18:19:14 +0100 Subject: [PATCH 07/29] Add reflection-free ToString to Result and Choice Result and Choice had no ToString override, so they fell back to the compiler-generated sprintf "%+A" one, which uses reflection. Give them hand-written overrides mirroring option/list (String.Concat + anyToStringShowingNull), e.g. Ok 5 -> "Ok(5)", Choice1Of2 7 -> "Choice1Of2(7)". This is reflection-free / AOT-friendly and consistent with option's "Some(x)" rendering. Note: this changes the observable ToString of Result/Choice from the "%A"-style "Ok 5" to "Ok(5)". Co-Authored-By: Claude Opus 4.8 (1M context) --- src/FSharp.Core/prim-types.fs | 76 +++++++++++++++---- .../Microsoft.FSharp.Core/ResultTests.fs | 7 ++ 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/src/FSharp.Core/prim-types.fs b/src/FSharp.Core/prim-types.fs index 036ba49ce48..663623b8966 100644 --- a/src/FSharp.Core/prim-types.fs +++ b/src/FSharp.Core/prim-types.fs @@ -3709,34 +3709,60 @@ namespace Microsoft.FSharp.Core [] [] - type Choice<'T1,'T2> = - | Choice1Of2 of 'T1 + type Choice<'T1,'T2> = + | Choice1Of2 of 'T1 | Choice2Of2 of 'T2 - + + override x.ToString() = + match x with + | Choice1Of2 v -> String.Concat("Choice1Of2(", anyToStringShowingNull v, ")") + | Choice2Of2 v -> String.Concat("Choice2Of2(", anyToStringShowingNull v, ")") + [] [] type Choice<'T1,'T2,'T3> = - | Choice1Of3 of 'T1 + | Choice1Of3 of 'T1 | Choice2Of3 of 'T2 | Choice3Of3 of 'T3 - + + override x.ToString() = + match x with + | Choice1Of3 v -> String.Concat("Choice1Of3(", anyToStringShowingNull v, ")") + | Choice2Of3 v -> String.Concat("Choice2Of3(", anyToStringShowingNull v, ")") + | Choice3Of3 v -> String.Concat("Choice3Of3(", anyToStringShowingNull v, ")") + [] [] type Choice<'T1,'T2,'T3,'T4> = - | Choice1Of4 of 'T1 + | Choice1Of4 of 'T1 | Choice2Of4 of 'T2 | Choice3Of4 of 'T3 | Choice4Of4 of 'T4 - + + override x.ToString() = + match x with + | Choice1Of4 v -> String.Concat("Choice1Of4(", anyToStringShowingNull v, ")") + | Choice2Of4 v -> String.Concat("Choice2Of4(", anyToStringShowingNull v, ")") + | Choice3Of4 v -> String.Concat("Choice3Of4(", anyToStringShowingNull v, ")") + | Choice4Of4 v -> String.Concat("Choice4Of4(", anyToStringShowingNull v, ")") + [] [] type Choice<'T1,'T2,'T3,'T4,'T5> = - | Choice1Of5 of 'T1 + | Choice1Of5 of 'T1 | Choice2Of5 of 'T2 | Choice3Of5 of 'T3 | Choice4Of5 of 'T4 | Choice5Of5 of 'T5 - + + override x.ToString() = + match x with + | Choice1Of5 v -> String.Concat("Choice1Of5(", anyToStringShowingNull v, ")") + | Choice2Of5 v -> String.Concat("Choice2Of5(", anyToStringShowingNull v, ")") + | Choice3Of5 v -> String.Concat("Choice3Of5(", anyToStringShowingNull v, ")") + | Choice4Of5 v -> String.Concat("Choice4Of5(", anyToStringShowingNull v, ")") + | Choice5Of5 v -> String.Concat("Choice5Of5(", anyToStringShowingNull v, ")") + [] [] type Choice<'T1,'T2,'T3,'T4,'T5,'T6> = @@ -3746,7 +3772,16 @@ namespace Microsoft.FSharp.Core | Choice4Of6 of 'T4 | Choice5Of6 of 'T5 | Choice6Of6 of 'T6 - + + override x.ToString() = + match x with + | Choice1Of6 v -> String.Concat("Choice1Of6(", anyToStringShowingNull v, ")") + | Choice2Of6 v -> String.Concat("Choice2Of6(", anyToStringShowingNull v, ")") + | Choice3Of6 v -> String.Concat("Choice3Of6(", anyToStringShowingNull v, ")") + | Choice4Of6 v -> String.Concat("Choice4Of6(", anyToStringShowingNull v, ")") + | Choice5Of6 v -> String.Concat("Choice5Of6(", anyToStringShowingNull v, ")") + | Choice6Of6 v -> String.Concat("Choice6Of6(", anyToStringShowingNull v, ")") + [] [] type Choice<'T1,'T2,'T3,'T4,'T5,'T6,'T7> = @@ -3757,7 +3792,17 @@ namespace Microsoft.FSharp.Core | Choice5Of7 of 'T5 | Choice6Of7 of 'T6 | Choice7Of7 of 'T7 - + + override x.ToString() = + match x with + | Choice1Of7 v -> String.Concat("Choice1Of7(", anyToStringShowingNull v, ")") + | Choice2Of7 v -> String.Concat("Choice2Of7(", anyToStringShowingNull v, ")") + | Choice3Of7 v -> String.Concat("Choice3Of7(", anyToStringShowingNull v, ")") + | Choice4Of7 v -> String.Concat("Choice4Of7(", anyToStringShowingNull v, ")") + | Choice5Of7 v -> String.Concat("Choice5Of7(", anyToStringShowingNull v, ")") + | Choice6Of7 v -> String.Concat("Choice6Of7(", anyToStringShowingNull v, ")") + | Choice7Of7 v -> String.Concat("Choice7Of7(", anyToStringShowingNull v, ")") + [] exception MatchFailureException of string * int * int with override x.Message = SR.GetString(SR.matchCasesIncomplete) @@ -4054,10 +4099,15 @@ namespace Microsoft.FSharp.Core [] [] [] - type Result<'T,'TError> = - | Ok of ResultValue:'T + type Result<'T,'TError> = + | Ok of ResultValue:'T | Error of ErrorValue:'TError + override x.ToString() = + match x with + | Ok v -> String.Concat("Ok(", anyToStringShowingNull v, ")") + | Error e -> String.Concat("Error(", anyToStringShowingNull e, ")") + [] [] [] diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Core/ResultTests.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Core/ResultTests.fs index e2b08e867d7..d85888f5bea 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Core/ResultTests.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Core/ResultTests.fs @@ -175,3 +175,10 @@ type ResultTests() = member this.ToValueOption() = Assert.AreEqual(Result.toValueOption (Error 42), ValueNone) Assert.AreEqual(Result.toValueOption (Ok 42), ValueSome 42) + + [] + member _.ToStringRendersCaseAndValue() = + // Reflection-free ToString, consistent with option's "Some(x)" style; null renders as "null". + Assert.AreEqual("Ok(5)", (Ok 5).ToString()) + Assert.AreEqual("Error(bad)", (Error "bad").ToString()) + Assert.AreEqual("Ok(null)", (Ok (null: string)).ToString()) From 956a4b0c983c25a557a764785b99a7ee306feb2a Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Sun, 21 Jun 2026 18:38:39 +0100 Subject: [PATCH 08/29] Generate a single-line ToString for records under --reflectionfree Records previously fell back to Object.ToString() (the namespace-qualified type name) under --reflectionfree. Generate "{ F1 = v1; F2 = v2 }" on a single line (no line breaks, unlike sprintf "%+A"), with fields formatted like union fields (null -> "null", otherwise via 'string'). Factor the shared field formatter and ToString-method emission out of the union path. The default (sprintf "%+A") path is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Compiler/CodeGen/IlxGen.fs | 90 ++++++++++++------- .../CompilerOptions/fsc/reflectionfree.fs | 23 ++++- 2 files changed, 80 insertions(+), 33 deletions(-) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index 80573cc89eb..cdd1a7545d4 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -11282,16 +11282,46 @@ and GenPrintingMethod cenv eenv methName ilThisTy m = /// Generate the 'ToString' method for a union type. Normally this calls 'sprintf "%+A"' (see /// GenPrintingMethod). Under reflection-free code generation 'sprintf' is unavailable, so instead emit a /// match over the cases that builds "CaseName(f0, f1, ...)" using the 'string' operator on each field. +/// Format one field value the same way option/list do (LanguagePrimitives.anyToStringShowingNull): +/// render null as "null", otherwise via the 'string' operator. +and GenFieldToString (cenv: cenv, m: range, fe: Expr) = + let g = cenv.g + let fieldTy = tyOfExpr g fe + let v, ve = mkCompGenLocal m "field" fieldTy + mkCompGenLet m v fe (mkNonNullCond g m g.string_ty (mkCallBox g m fieldTy ve) (mkCallStringOperator g m fieldTy ve) (mkString g m "null")) + +/// Emit a [] virtual ToString override whose body is the given string-typed expression. +/// 'thisv' is the 'this' value (stored at arg 0) referenced by bodyExpr. +and GenToStringMethodFromExpr (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, thisv: Val, bodyExpr: Expr) = + let g = cenv.g + let eenvForMeth = AddStorageForLocalVals g [ (thisv, Arg 0) ] eenv + let ilMethodBody = CodeGenMethodForExpr cenv mgbuf ([], "ToString", eenvForMeth, 0, Some thisv, bodyExpr, Return) + + let mdef = + mkILNonGenericVirtualInstanceMethod ( + "ToString", + ILMemberAccess.Public, + [], + mkILReturn g.ilg.typ_String, + MethodBody.IL(InterruptibleLazy.FromValue ilMethodBody) + ) + + [ mdef.With(customAttrs = mkILCustomAttrs [ g.CompilerGeneratedAttribute ]) ] + +/// Build the 'this' local for a generated ToString (a byref for struct types) and the type instantiation. +and GenToStringThis (cenv: cenv, tcref: TyconRef, m: range) = + let g = cenv.g + let tinst, ty = generalizeTyconRef g tcref + let thisv, thise = mkCompGenLocal m "this" (if isStructTy g ty then mkByrefTy g ty else ty) + tinst, thisv, thise + and GenUnionToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, ilThisTy: ILType, tcref: TyconRef, m: range) = let g = cenv.g if not g.useReflectionFreeCodeGen then GenPrintingMethod cenv eenv "ToString" ilThisTy m else - let tinst, thisv, thise = - let tinst, ty = generalizeTyconRef g tcref - let thisv, thise = mkCompGenLocal m "this" (if isStructTy g ty then mkByrefTy g ty else ty) - tinst, thisv, thise + let tinst, thisv, thise = GenToStringThis (cenv, tcref, m) let mbuilder = MatchBuilder(DebugPointAtBinding.NoneAtInvisible, m) @@ -11302,23 +11332,12 @@ and GenUnionToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, if isNil rfields then mkString g m ucase.DisplayName else - // provene is an expression that has been proven to be of this case (the value itself for - // struct unions, otherwise a 'UnionCaseProof'), from which fields can be read. + // provene is an expression proven to be of this case (the value itself for struct unions, + // otherwise a 'UnionCaseProof'), from which fields can be read. let mkBody (provene: Expr) = - // Format each field the same way option/list do (LanguagePrimitives.anyToStringShowingNull): - // render null as "null", otherwise via the 'string' operator. let fieldStrs = rfields - |> List.mapi (fun j _ -> - let fe = mkUnionCaseFieldGetProvenViaExprAddr (provene, cref, tinst, j, m) - let fieldTy = tyOfExpr g fe - let v, ve = mkCompGenLocal m "field" fieldTy - - mkCompGenLet - m - v - fe - (mkNonNullCond g m g.string_ty (mkCallBox g m fieldTy ve) (mkCallStringOperator g m fieldTy ve) (mkString g m "null"))) + |> List.mapi (fun j _ -> GenFieldToString (cenv, m, mkUnionCaseFieldGetProvenViaExprAddr (provene, cref, tinst, j, m))) let sep = mkString g m ", " @@ -11343,20 +11362,29 @@ and GenUnionToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, let dtree = TDSwitch(thise, cases, None, m) let matchExpr = mbuilder.Close(dtree, m, g.string_ty) - let eenvForMeth = AddStorageForLocalVals g [ (thisv, Arg 0) ] eenv - let ilMethodBody = CodeGenMethodForExpr cenv mgbuf ([], "ToString", eenvForMeth, 0, Some thisv, matchExpr, Return) + GenToStringMethodFromExpr (cenv, mgbuf, eenv, thisv, matchExpr) - let mdef = - mkILNonGenericVirtualInstanceMethod ( - "ToString", - ILMemberAccess.Public, - [], - mkILReturn g.ilg.typ_String, - MethodBody.IL(InterruptibleLazy.FromValue ilMethodBody) - ) +/// Generate a record's ToString as a single line "{ F1 = v1; F2 = v2 }" (no line breaks, unlike "%+A"), +/// fields formatted like union fields. Under non-reflection-free codegen, falls back to sprintf "%+A". +and GenRecordToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, ilThisTy: ILType, tcref: TyconRef, m: range) = + let g = cenv.g + + if not g.useReflectionFreeCodeGen then + GenPrintingMethod cenv eenv "ToString" ilThisTy m + else + let tinst, thisv, thise = GenToStringThis (cenv, tcref, m) + + let fieldParts = + tcref.AllInstanceFieldsAsList + |> List.mapi (fun i fspec -> + let fref = tcref.MakeNestedRecdFieldRef fspec + let value = GenFieldToString (cenv, m, mkRecdFieldGetViaExprAddr (thise, fref, tinst, m)) + let nameEq = mkString g m (fspec.DisplayName + " = ") + if i = 0 then [ nameEq; value ] else [ mkString g m "; "; nameEq; value ]) + |> List.concat - let mdef = mdef.With(customAttrs = mkILCustomAttrs [ g.CompilerGeneratedAttribute ]) - [ mdef ] + let parts = mkString g m "{ " :: fieldParts @ [ mkString g m " }" ] + GenToStringMethodFromExpr (cenv, mgbuf, eenv, thisv, mkStringConcat (g, m, parts)) and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option = let g = cenv.g @@ -11942,7 +11970,7 @@ and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option yield mkILSimpleStorageCtor (Some g.ilg.typ_Object.TypeSpec, ilThisTy, [], [], reprAccess, None, eenv.imports) if not (tycon.HasMember g "ToString" []) then - yield! GenToStringMethod cenv eenv ilThisTy m + yield! GenRecordToStringMethod(cenv, mgbuf, eenvinner, ilThisTy, tcref, m) | TFSharpTyconRepr r when tycon.IsFSharpDelegateTycon -> diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs index 64717a77a74..a4251720499 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs @@ -35,14 +35,33 @@ let someCode = """ [] -let ``Records and classes don't have generated ToString`` () = +let ``Classes don't have a generated ToString`` () = someCode |> withOptions [ "--reflectionfree" ] |> compileExeAndRun |> shouldSucceed - |> withStdOutContains "Thing says: Test+MyRecord" |> withStdOutContains "Thing says: Test+MyClass" +[] +let ``Records get a generated single-line ToString`` () = + FSharp """ +module Test +type Point = { X: int; Y: int } +type Nested = { P: Point; S: string } + +[] +let main _ = + { X = 1; Y = 2 } |> string |> System.Console.WriteLine + { P = { X = 1; Y = 2 }; S = null } |> string |> System.Console.WriteLine // nested record + null field + 0 + """ + |> asExe + |> withOptions [ "--reflectionfree" ] + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "{ X = 1; Y = 2 }" + |> withStdOutContains "{ P = { X = 1; Y = 2 }; S = null }" + [] let ``Unions have a generated ToString that matches on the case`` () = someCode From a0211661f1450ca65b26869ef8b8f0a40f12d6ce Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Sun, 21 Jun 2026 20:12:21 +0100 Subject: [PATCH 09/29] Update FSharp.Core surface-area baselines for Result/Choice ToString Result and Choice`2..7 now declare an explicit ToString() override, so they appear in the public surface area. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../FSharp.Core.SurfaceArea.netstandard20.debug.bsl | 9 ++++++++- .../FSharp.Core.SurfaceArea.netstandard20.release.bsl | 9 ++++++++- .../FSharp.Core.SurfaceArea.netstandard21.debug.bsl | 9 ++++++++- .../FSharp.Core.SurfaceArea.netstandard21.release.bsl | 7 +++++++ 4 files changed, 31 insertions(+), 3 deletions(-) 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 5b6cc0bce4e..0e1b7eea5c5 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl @@ -1106,6 +1106,7 @@ Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice` Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2+Tags[T1,T2] Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice1Of2(T1) Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice2Of2(T2) +Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 Item Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`3+Choice2Of3[T1,T2,T3]: T2 Item @@ -1139,6 +1140,7 @@ Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoi Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice1Of3(T1) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice2Of3(T2) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice3Of3(T3) +Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 Item Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`4+Choice2Of4[T1,T2,T3,T4]: T2 Item @@ -1179,6 +1181,7 @@ Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpC Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice2Of4(T2) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice3Of4(T3) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice4Of4(T4) +Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 Item Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`5+Choice2Of5[T1,T2,T3,T4,T5]: T2 Item @@ -1226,6 +1229,7 @@ Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSha Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice3Of5(T3) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice4Of5(T4) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice5Of5(T5) +Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 Item Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`6+Choice2Of6[T1,T2,T3,T4,T5,T6]: T2 Item @@ -1280,6 +1284,7 @@ Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.F Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice4Of6(T4) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice5Of6(T5) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice6Of6(T6) +Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 Item Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`7+Choice2Of7[T1,T2,T3,T4,T5,T6,T7]: T2 Item @@ -1341,6 +1346,7 @@ Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Cor Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice5Of7(T5) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice6Of7(T6) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice7Of7(T7) +Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: System.String ToString() Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] FromConverter(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] op_Implicit(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: System.Converter`2[T,TResult] ToConverter(Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]) @@ -1420,6 +1426,7 @@ Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Int32 get_Tag() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2+Tags[T,TError] Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewError(TError) Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewOk(T) +Microsoft.FSharp.Core.FSharpResult`2[T,TError]: System.String ToString() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T ResultValue Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T get_ResultValue() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: TError ErrorValue @@ -2667,4 +2674,4 @@ Microsoft.FSharp.Reflection.UnionCaseInfo: System.String Name Microsoft.FSharp.Reflection.UnionCaseInfo: System.String ToString() Microsoft.FSharp.Reflection.UnionCaseInfo: System.String get_Name() Microsoft.FSharp.Reflection.UnionCaseInfo: System.Type DeclaringType -Microsoft.FSharp.Reflection.UnionCaseInfo: System.Type get_DeclaringType() \ No newline at end of file +Microsoft.FSharp.Reflection.UnionCaseInfo: System.Type get_DeclaringType() 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 217d4b7c837..8ddee8cb2d2 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl @@ -603,8 +603,8 @@ Microsoft.FSharp.Collections.SetModule: Microsoft.FSharp.Collections.FSharpSet`1 Microsoft.FSharp.Collections.SetModule: Microsoft.FSharp.Collections.FSharpSet`1[T] UnionMany[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Collections.FSharpSet`1[T]]) Microsoft.FSharp.Collections.SetModule: Microsoft.FSharp.Collections.FSharpSet`1[T] Union[T](Microsoft.FSharp.Collections.FSharpSet`1[T], Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: System.Collections.Generic.IEnumerable`1[T] ToSeq[T](Microsoft.FSharp.Collections.FSharpSet`1[T]) -Microsoft.FSharp.Collections.SetModule: System.Tuple`2[Microsoft.FSharp.Collections.FSharpSet`1[T],Microsoft.FSharp.Collections.FSharpSet`1[T]] Partition[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: System.Tuple`2[Microsoft.FSharp.Collections.FSharpSet`1[T1],Microsoft.FSharp.Collections.FSharpSet`1[T2]] PartitionWith[T,T1,T2](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]], Microsoft.FSharp.Collections.FSharpSet`1[T]) +Microsoft.FSharp.Collections.SetModule: System.Tuple`2[Microsoft.FSharp.Collections.FSharpSet`1[T],Microsoft.FSharp.Collections.FSharpSet`1[T]] Partition[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: T MaxElement[T](Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: T MinElement[T](Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: TState FoldBack[T,TState](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpFunc`2[TState,TState]], Microsoft.FSharp.Collections.FSharpSet`1[T], TState) @@ -1106,6 +1106,7 @@ Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice` Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2+Tags[T1,T2] Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice1Of2(T1) Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice2Of2(T2) +Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 Item Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`3+Choice2Of3[T1,T2,T3]: T2 Item @@ -1139,6 +1140,7 @@ Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoi Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice1Of3(T1) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice2Of3(T2) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice3Of3(T3) +Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 Item Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`4+Choice2Of4[T1,T2,T3,T4]: T2 Item @@ -1179,6 +1181,7 @@ Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpC Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice2Of4(T2) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice3Of4(T3) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice4Of4(T4) +Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 Item Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`5+Choice2Of5[T1,T2,T3,T4,T5]: T2 Item @@ -1226,6 +1229,7 @@ Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSha Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice3Of5(T3) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice4Of5(T4) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice5Of5(T5) +Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 Item Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`6+Choice2Of6[T1,T2,T3,T4,T5,T6]: T2 Item @@ -1280,6 +1284,7 @@ Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.F Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice4Of6(T4) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice5Of6(T5) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice6Of6(T6) +Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 Item Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`7+Choice2Of7[T1,T2,T3,T4,T5,T6,T7]: T2 Item @@ -1341,6 +1346,7 @@ Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Cor Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice5Of7(T5) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice6Of7(T6) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice7Of7(T7) +Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: System.String ToString() Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] FromConverter(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] op_Implicit(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: System.Converter`2[T,TResult] ToConverter(Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]) @@ -1420,6 +1426,7 @@ Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Int32 get_Tag() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2+Tags[T,TError] Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewError(TError) Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewOk(T) +Microsoft.FSharp.Core.FSharpResult`2[T,TError]: System.String ToString() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T ResultValue Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T get_ResultValue() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: TError ErrorValue 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 43defdb622e..e69cd5734de 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl @@ -1109,6 +1109,7 @@ Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice` Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2+Tags[T1,T2] Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice1Of2(T1) Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice2Of2(T2) +Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 Item Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`3+Choice2Of3[T1,T2,T3]: T2 Item @@ -1142,6 +1143,7 @@ Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoi Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice1Of3(T1) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice2Of3(T2) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice3Of3(T3) +Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 Item Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`4+Choice2Of4[T1,T2,T3,T4]: T2 Item @@ -1182,6 +1184,7 @@ Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpC Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice2Of4(T2) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice3Of4(T3) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice4Of4(T4) +Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 Item Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`5+Choice2Of5[T1,T2,T3,T4,T5]: T2 Item @@ -1229,6 +1232,7 @@ Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSha Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice3Of5(T3) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice4Of5(T4) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice5Of5(T5) +Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 Item Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`6+Choice2Of6[T1,T2,T3,T4,T5,T6]: T2 Item @@ -1283,6 +1287,7 @@ Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.F Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice4Of6(T4) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice5Of6(T5) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice6Of6(T6) +Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 Item Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`7+Choice2Of7[T1,T2,T3,T4,T5,T6,T7]: T2 Item @@ -1344,6 +1349,7 @@ Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Cor Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice5Of7(T5) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice6Of7(T6) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice7Of7(T7) +Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: System.String ToString() Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] FromConverter(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] op_Implicit(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: System.Converter`2[T,TResult] ToConverter(Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]) @@ -1423,6 +1429,7 @@ Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Int32 get_Tag() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2+Tags[T,TError] Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewError(TError) Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewOk(T) +Microsoft.FSharp.Core.FSharpResult`2[T,TError]: System.String ToString() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T ResultValue Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T get_ResultValue() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: TError ErrorValue @@ -2670,4 +2677,4 @@ Microsoft.FSharp.Reflection.UnionCaseInfo: System.String Name Microsoft.FSharp.Reflection.UnionCaseInfo: System.String ToString() Microsoft.FSharp.Reflection.UnionCaseInfo: System.String get_Name() Microsoft.FSharp.Reflection.UnionCaseInfo: System.Type DeclaringType -Microsoft.FSharp.Reflection.UnionCaseInfo: System.Type get_DeclaringType() \ No newline at end of file +Microsoft.FSharp.Reflection.UnionCaseInfo: System.Type get_DeclaringType() 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 ed913ea04d3..ef1e3ea6b82 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl @@ -1109,6 +1109,7 @@ Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice` Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2+Tags[T1,T2] Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice1Of2(T1) Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice2Of2(T2) +Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 Item Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`3+Choice2Of3[T1,T2,T3]: T2 Item @@ -1142,6 +1143,7 @@ Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoi Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice1Of3(T1) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice2Of3(T2) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice3Of3(T3) +Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 Item Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`4+Choice2Of4[T1,T2,T3,T4]: T2 Item @@ -1182,6 +1184,7 @@ Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpC Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice2Of4(T2) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice3Of4(T3) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice4Of4(T4) +Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 Item Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`5+Choice2Of5[T1,T2,T3,T4,T5]: T2 Item @@ -1229,6 +1232,7 @@ Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSha Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice3Of5(T3) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice4Of5(T4) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice5Of5(T5) +Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 Item Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`6+Choice2Of6[T1,T2,T3,T4,T5,T6]: T2 Item @@ -1283,6 +1287,7 @@ Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.F Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice4Of6(T4) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice5Of6(T5) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice6Of6(T6) +Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 Item Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`7+Choice2Of7[T1,T2,T3,T4,T5,T6,T7]: T2 Item @@ -1344,6 +1349,7 @@ Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Cor Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice5Of7(T5) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice6Of7(T6) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice7Of7(T7) +Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: System.String ToString() Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] FromConverter(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] op_Implicit(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: System.Converter`2[T,TResult] ToConverter(Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]) @@ -1423,6 +1429,7 @@ Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Int32 get_Tag() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2+Tags[T,TError] Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewError(TError) Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewOk(T) +Microsoft.FSharp.Core.FSharpResult`2[T,TError]: System.String ToString() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T ResultValue Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T get_ResultValue() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: TError ErrorValue From 8eef294069f216f4ced91cf248fd9b800a5e6d5a Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Sun, 21 Jun 2026 20:26:12 +0100 Subject: [PATCH 10/29] Add release notes Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + docs/release-notes/.FSharp.Core/10.0.300.md | 1 + 2 files changed, 2 insertions(+) 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 dd63aa988bd..78d6c792af0 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -96,6 +96,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)) +* Under `--reflectionfree`, discriminated unions and records now get a generated `ToString` (rendering each field like `Option` does) instead of falling back to the namespace-qualified type name. ([PR #19976](https://github.com/dotnet/fsharp/pull/19976)) ### Changed diff --git a/docs/release-notes/.FSharp.Core/10.0.300.md b/docs/release-notes/.FSharp.Core/10.0.300.md index 6904303cc37..2d28428507b 100644 --- a/docs/release-notes/.FSharp.Core/10.0.300.md +++ b/docs/release-notes/.FSharp.Core/10.0.300.md @@ -18,5 +18,6 @@ ### 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)) +* `Result` and `Choice` now have a reflection-free `ToString` consistent with `Option`'s `Some(x)` style (e.g. `Ok 0` renders as `"Ok(0)"` instead of `"Ok 0"`). ([PR #19976](https://github.com/dotnet/fsharp/pull/19976)) ### Breaking Changes From 3754def28f75fbaf5dae2a6811b1757cb61f4ee3 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Sun, 21 Jun 2026 23:29:16 +0100 Subject: [PATCH 11/29] Generate a single-line ToString for anonymous records under --reflectionfree Drive anonymous-record ToString through the synthetic record tycon (already built for equality/comparison) rather than sprintf "%A", so under --reflectionfree it renders "{| Name = value; ... |}" on a single line. GenRecordToStringMethod now takes open/close brace strings ("{ "/" }" for records, "{| "/" |}" for anonymous records). The default (non-reflection-free) codegen path is unchanged and still falls back to sprintf "%+A". Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Compiler/CodeGen/IlxGen.fs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index cdd1a7545d4..6856d25acc7 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -2222,7 +2222,6 @@ type AnonTypeGenerationTable() = mkLdfldMethodDef ("get_" + propName, ILMemberAccess.Public, false, ilTy, fldName, fldTy, ILAttributes.Empty, attrs) |> g.AddMethodGeneratedAttributes - yield! genToStringMethod ilTy ] let ilBaseTy = (if isStruct then g.iltyp_ValueType else g.ilg.typ_Object) @@ -2325,6 +2324,10 @@ type AnonTypeGenerationTable() = Some(mkLocalValRef augmentation.EqualsExactWithComparer) ) + // Generate ToString through the synthetic record tycon (renders "{| Name = value; ... |}" under + // --reflectionfree, otherwise sprintf "%+A"). Done here, not in ilMethods above, because it needs the tycon. + let ilToStringMethodDefs = genToStringMethod (ilTy, tycon) + // Build the ILTypeDef. We don't rely on the normal record generation process because we want very specific field names let ilTypeDefAttribs = @@ -2347,7 +2350,7 @@ type AnonTypeGenerationTable() = ilGenericParams, ilBaseTy, ilInterfaceTys, - mkILMethods (ilCtorDef :: ilMethods), + mkILMethods (ilCtorDef :: ilMethods @ ilToStringMethodDefs), ilFieldDefs, emptyILTypeDefs, ilProperties, @@ -3803,7 +3806,7 @@ and GenAllocRecd cenv cgbuf eenv ctorInfo (tcref, argTys, args, m) sequel = and GenAllocAnonRecd cenv cgbuf eenv (anonInfo: AnonRecdTypeInfo, tyargs, args, m) sequel = let anonCtor, _anonMethods, anonType = - cgbuf.mgbuf.LookupAnonType((fun ilThisTy -> GenToStringMethod cenv eenv ilThisTy m), anonInfo) + cgbuf.mgbuf.LookupAnonType((fun (ilThisTy, tycon) -> GenRecordToStringMethod(cenv, cgbuf.mgbuf, EnvForTycon tycon eenv, ilThisTy, mkLocalTyconRef tycon, m, "{| ", " |}")), anonInfo) let boxity = anonType.Boxity GenExprs cenv cgbuf eenv args @@ -3817,7 +3820,7 @@ and GenAllocAnonRecd cenv cgbuf eenv (anonInfo: AnonRecdTypeInfo, tyargs, args, and GenGetAnonRecdField cenv cgbuf eenv (anonInfo: AnonRecdTypeInfo, e, tyargs, n, m) sequel = let _anonCtor, anonMethods, anonType = - cgbuf.mgbuf.LookupAnonType((fun ilThisTy -> GenToStringMethod cenv eenv ilThisTy m), anonInfo) + cgbuf.mgbuf.LookupAnonType((fun (ilThisTy, tycon) -> GenRecordToStringMethod(cenv, cgbuf.mgbuf, EnvForTycon tycon eenv, ilThisTy, mkLocalTyconRef tycon, m, "{| ", " |}")), anonInfo) let boxity = anonType.Boxity let ilTypeArgs = GenTypeArgs cenv m eenv.tyenv tyargs @@ -10842,7 +10845,7 @@ and GenImplFile cenv (mgbuf: AssemblyBuilder) mainInfoOpt eenv (implFile: Checke // Generate all the anonymous record types mentioned anywhere in this module for anonInfo in anonRecdTypes.Values do - mgbuf.GenerateAnonType((fun ilThisTy -> GenToStringMethod cenv eenv ilThisTy m), anonInfo) + mgbuf.GenerateAnonType((fun (ilThisTy, tycon) -> GenRecordToStringMethod(cenv, mgbuf, EnvForTycon tycon eenv, ilThisTy, mkLocalTyconRef tycon, m, "{| ", " |}")), anonInfo) let withQName (loc: CompileLocation) = { loc with @@ -11210,9 +11213,6 @@ and GenAbstractBinding cenv eenv tref (vref: ValRef) = else [], [], [] -and GenToStringMethod cenv eenv ilThisTy m = - GenPrintingMethod cenv eenv "ToString" ilThisTy m - /// Generate a ToString/get_Message method that calls 'sprintf "%A"' and GenPrintingMethod cenv eenv methName ilThisTy m = let g = cenv.g @@ -11365,8 +11365,9 @@ and GenUnionToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, GenToStringMethodFromExpr (cenv, mgbuf, eenv, thisv, matchExpr) /// Generate a record's ToString as a single line "{ F1 = v1; F2 = v2 }" (no line breaks, unlike "%+A"), -/// fields formatted like union fields. Under non-reflection-free codegen, falls back to sprintf "%+A". -and GenRecordToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, ilThisTy: ILType, tcref: TyconRef, m: range) = +/// fields formatted like union fields. openBrace/closeBrace are "{ "/" }" for records and "{| "/" |}" for +/// anonymous records. Under non-reflection-free codegen, falls back to sprintf "%+A". +and GenRecordToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, ilThisTy: ILType, tcref: TyconRef, m: range, openBrace: string, closeBrace: string) = let g = cenv.g if not g.useReflectionFreeCodeGen then @@ -11383,7 +11384,7 @@ and GenRecordToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv if i = 0 then [ nameEq; value ] else [ mkString g m "; "; nameEq; value ]) |> List.concat - let parts = mkString g m "{ " :: fieldParts @ [ mkString g m " }" ] + let parts = mkString g m openBrace :: fieldParts @ [ mkString g m closeBrace ] GenToStringMethodFromExpr (cenv, mgbuf, eenv, thisv, mkStringConcat (g, m, parts)) and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option = @@ -11970,7 +11971,7 @@ and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option yield mkILSimpleStorageCtor (Some g.ilg.typ_Object.TypeSpec, ilThisTy, [], [], reprAccess, None, eenv.imports) if not (tycon.HasMember g "ToString" []) then - yield! GenRecordToStringMethod(cenv, mgbuf, eenvinner, ilThisTy, tcref, m) + yield! GenRecordToStringMethod(cenv, mgbuf, eenvinner, ilThisTy, tcref, m, "{ ", " }") | TFSharpTyconRepr r when tycon.IsFSharpDelegateTycon -> From a5f33caba50777b1c236bf7d59df5a29c96fab0d Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Mon, 22 Jun 2026 18:52:10 +0100 Subject: [PATCH 12/29] Test that a hand-written ToString override is kept under --reflectionfree Addresses review feedback: generation is gated on `not (HasMember "ToString")`, so a user-defined ToString on a union or record wins over the generated one. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../CompilerOptions/fsc/reflectionfree.fs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs index a4251720499..92a260af31c 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs @@ -120,6 +120,31 @@ let main _ = |> shouldSucceed |> withStdOutContains "fields-render-alike" +[] +let ``A hand-written ToString override is kept, not replaced by the generated one`` () = + FSharp """ +module Test +type MyDU = + | A of int + override _.ToString() = "custom-du" + +type MyRecord = + { X: int } + override _.ToString() = "custom-record" + +[] +let main _ = + A 1 |> string |> System.Console.WriteLine + { X = 1 } |> string |> System.Console.WriteLine + 0 + """ + |> asExe + |> withOptions [ "--reflectionfree" ] + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "custom-du" + |> withStdOutContains "custom-record" + [] let ``No debug display attribute`` () = someCode From 7ed0d8d7bee0fc36bfc0d711d5b3ae2a6e722c06 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Mon, 22 Jun 2026 19:09:00 +0100 Subject: [PATCH 13/29] Rename ToString generators for clarity Addresses review feedback: distinguish the reflective sprintf path from the structural one. GenPrintingMethod -> GenSprintfPrintingMethod (the sprintf "%+A" ToString/get_Message), GenToStringMethodFromExpr -> EmitToStringMethodDef. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Compiler/CodeGen/IlxGen.fs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index 6856d25acc7..f4aba4823db 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -11214,7 +11214,7 @@ and GenAbstractBinding cenv eenv tref (vref: ValRef) = [], [], [] /// Generate a ToString/get_Message method that calls 'sprintf "%A"' -and GenPrintingMethod cenv eenv methName ilThisTy m = +and GenSprintfPrintingMethod cenv eenv methName ilThisTy m = let g = cenv.g [ @@ -11280,7 +11280,7 @@ and GenPrintingMethod cenv eenv methName ilThisTy m = ] /// Generate the 'ToString' method for a union type. Normally this calls 'sprintf "%+A"' (see -/// GenPrintingMethod). Under reflection-free code generation 'sprintf' is unavailable, so instead emit a +/// GenSprintfPrintingMethod). Under reflection-free code generation 'sprintf' is unavailable, so instead emit a /// match over the cases that builds "CaseName(f0, f1, ...)" using the 'string' operator on each field. /// Format one field value the same way option/list do (LanguagePrimitives.anyToStringShowingNull): /// render null as "null", otherwise via the 'string' operator. @@ -11292,7 +11292,7 @@ and GenFieldToString (cenv: cenv, m: range, fe: Expr) = /// Emit a [] virtual ToString override whose body is the given string-typed expression. /// 'thisv' is the 'this' value (stored at arg 0) referenced by bodyExpr. -and GenToStringMethodFromExpr (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, thisv: Val, bodyExpr: Expr) = +and EmitToStringMethodDef (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, thisv: Val, bodyExpr: Expr) = let g = cenv.g let eenvForMeth = AddStorageForLocalVals g [ (thisv, Arg 0) ] eenv let ilMethodBody = CodeGenMethodForExpr cenv mgbuf ([], "ToString", eenvForMeth, 0, Some thisv, bodyExpr, Return) @@ -11319,7 +11319,7 @@ and GenUnionToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, let g = cenv.g if not g.useReflectionFreeCodeGen then - GenPrintingMethod cenv eenv "ToString" ilThisTy m + GenSprintfPrintingMethod cenv eenv "ToString" ilThisTy m else let tinst, thisv, thise = GenToStringThis (cenv, tcref, m) @@ -11362,7 +11362,7 @@ and GenUnionToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, let dtree = TDSwitch(thise, cases, None, m) let matchExpr = mbuilder.Close(dtree, m, g.string_ty) - GenToStringMethodFromExpr (cenv, mgbuf, eenv, thisv, matchExpr) + EmitToStringMethodDef (cenv, mgbuf, eenv, thisv, matchExpr) /// Generate a record's ToString as a single line "{ F1 = v1; F2 = v2 }" (no line breaks, unlike "%+A"), /// fields formatted like union fields. openBrace/closeBrace are "{ "/" }" for records and "{| "/" |}" for @@ -11371,7 +11371,7 @@ and GenRecordToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv let g = cenv.g if not g.useReflectionFreeCodeGen then - GenPrintingMethod cenv eenv "ToString" ilThisTy m + GenSprintfPrintingMethod cenv eenv "ToString" ilThisTy m else let tinst, thisv, thise = GenToStringThis (cenv, tcref, m) @@ -11385,7 +11385,7 @@ and GenRecordToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv |> List.concat let parts = mkString g m openBrace :: fieldParts @ [ mkString g m closeBrace ] - GenToStringMethodFromExpr (cenv, mgbuf, eenv, thisv, mkStringConcat (g, m, parts)) + EmitToStringMethodDef (cenv, mgbuf, eenv, thisv, mkStringConcat (g, m, parts)) and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option = let g = cenv.g @@ -12619,7 +12619,7 @@ and GenExnDef cenv mgbuf eenv m (exnc: Tycon) : ILTypeRef option = && not (exnc.HasMember g "Message" []) && not (fspecs |> List.exists (fun rf -> rf.DisplayNameCore = "Message")) then - yield! GenPrintingMethod cenv eenv "get_Message" ilThisTy m + yield! GenSprintfPrintingMethod cenv eenv "get_Message" ilThisTy m ] let interfaces = From dd3d75c47e0eb5f57d62e18fc21fca7e8f82e20f Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Mon, 22 Jun 2026 19:14:24 +0100 Subject: [PATCH 14/29] Restore tabular layout for string_operator_info in TcGlobals Addresses review feedback: keep the column-aligned layout of the surrounding intrinsic table. Also makes these two lines byte-identical to the same intrinsic added by #19971, so a future merge resolves cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Compiler/TypedTree/TcGlobals.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Compiler/TypedTree/TcGlobals.fs b/src/Compiler/TypedTree/TcGlobals.fs index b2eb4cd0f74..78f1dd9c450 100644 --- a/src/Compiler/TypedTree/TcGlobals.fs +++ b/src/Compiler/TypedTree/TcGlobals.fs @@ -792,7 +792,7 @@ type TcGlobals( let v_byte_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "byte" , None , Some "ToByte", [vara], ([[varaTy]], v_byte_ty)) let v_sbyte_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "sbyte" , None , Some "ToSByte", [vara], ([[varaTy]], v_sbyte_ty)) - let v_string_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "string", None, Some "ToString", [vara], ([[varaTy]], v_string_ty)) + let v_string_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "string" , None , Some "ToString", [vara], ([[varaTy]], v_string_ty)) let v_int16_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "int16" , None , Some "ToInt16", [vara], ([[varaTy]], v_int16_ty)) let v_uint16_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "uint16" , None , Some "ToUInt16", [vara], ([[varaTy]], v_uint16_ty)) let v_int32_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "int32" , None , Some "ToInt32", [vara], ([[varaTy]], v_int32_ty)) @@ -1595,7 +1595,7 @@ type TcGlobals( member _.byte_operator_info = v_byte_operator_info member _.sbyte_operator_info = v_sbyte_operator_info - member _.string_operator_info = v_string_operator_info + member _.string_operator_info = v_string_operator_info member _.int16_operator_info = v_int16_operator_info member _.uint16_operator_info = v_uint16_operator_info member _.int32_operator_info = v_int32_operator_info From 1a5065e7e1d3267afb7b81297d30a2a0639267b0 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Tue, 23 Jun 2026 07:46:41 +0100 Subject: [PATCH 15/29] Add reflection-free ToString tests for field shapes, structs, anon records and recursion Covers DU field shapes (multiple fields vs a single tuple field), explicit vs unnamed field names rendering identically, struct unions/records, anonymous and struct anonymous records, and finite recursive/nesting types. Co-Authored-By: Claude Opus 4.8 --- .../CompilerOptions/fsc/reflectionfree.fs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs index 92a260af31c..a4696227b7e 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs @@ -145,6 +145,106 @@ let main _ = |> withStdOutContains "custom-du" |> withStdOutContains "custom-record" +[] +let ``Union field shapes: multiple fields versus a single tuple field`` () = + FSharp """ +module Test +type TwoFields = | Two of int * int +type OneTupleField = | OneTup of (int * int) +type NamedFields = | Named of x: int * y: int + +[] +let main _ = + Two (1, 2) |> string |> System.Console.WriteLine + OneTup (1, 2) |> string |> System.Console.WriteLine // a single tuple field keeps its own parens + Named (1, 2) |> string |> System.Console.WriteLine // named fields render positionally, names are not shown + 0 + """ + |> asExe + |> withOptions [ "--reflectionfree" ] + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "Two(1, 2)" + |> withStdOutContains "OneTup((1, 2))" + |> withStdOutContains "Named(1, 2)" + +[] +let ``Explicit field names do not change the rendering`` () = + FSharp """ +module Test +type Labelled = | WithNames of first: int * second: string +type Plain = | WithoutNames of int * string + +[] +let main _ = + WithNames (1, "a") |> string |> System.Console.WriteLine + WithoutNames (1, "a") |> string |> System.Console.WriteLine // unnamed fields render the same way as named ones + 0 + """ + |> asExe + |> withOptions [ "--reflectionfree" ] + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "WithNames(1, a)" + |> withStdOutContains "WithoutNames(1, a)" + +[] +let ``Struct unions and struct records get a generated ToString`` () = + FSharp """ +module Test +[] type StructUnion = | SA of a: int +[] type StructRecord = { SX: int; SY: int } + +[] +let main _ = + SA 7 |> string |> System.Console.WriteLine + { SX = 1; SY = 2 } |> string |> System.Console.WriteLine + 0 + """ + |> asExe + |> withOptions [ "--reflectionfree" ] + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "SA(7)" + |> withStdOutContains "{ SX = 1; SY = 2 }" + +[] +let ``Anonymous records get a generated single-line ToString`` () = + FSharp """ +module Test +[] +let main _ = + {| A = 1; B = "hi" |} |> string |> System.Console.WriteLine + (struct {| A = 1; B = "hi" |}) |> string |> System.Console.WriteLine // a struct anonymous record renders identically + 0 + """ + |> asExe + |> withOptions [ "--reflectionfree" ] + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "{| A = 1; B = hi |}" + +[] +let ``Recursively defined types render when the data is finite`` () = + FSharp """ +module Test +type Tree = | Leaf | Node of Tree * int * Tree +type TreeNode = { Value: int; Parent: TreeNode option } // an upward-only parent pointer stays finite + +[] +let main _ = + Node (Node (Leaf, 1, Leaf), 2, Leaf) |> string |> System.Console.WriteLine + let root = { Value = 0; Parent = None } + { Value = 1; Parent = Some root } |> string |> System.Console.WriteLine + 0 + """ + |> asExe + |> withOptions [ "--reflectionfree" ] + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "Node(Node(Leaf, 1, Leaf), 2, Leaf)" + |> withStdOutContains "{ Value = 1; Parent = Some({ Value = 0; Parent = null }) }" + [] let ``No debug display attribute`` () = someCode From 2c108ed32486b7c22419b2323a8a44014a3e2f34 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Tue, 23 Jun 2026 08:08:22 +0100 Subject: [PATCH 16/29] Add EmittedIL tests for reflection-free record and union ToString Locks in the IL emitted under --reflectionfree: each field is boxed and rendered through Operators.ToString with a null guard, and the parts are joined with String.Concat (array form for the record, 3-arg form for the single-field union case). Nullary union cases return the bare case name. Co-Authored-By: Claude Opus 4.8 --- .../EmittedIL/ReflectionFreeToString.fs | 132 ++++++++++++++++++ .../FSharp.Compiler.ComponentTests.fsproj | 1 + 2 files changed, 133 insertions(+) create mode 100644 tests/FSharp.Compiler.ComponentTests/EmittedIL/ReflectionFreeToString.fs diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/ReflectionFreeToString.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/ReflectionFreeToString.fs new file mode 100644 index 00000000000..fb27a9a73d2 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/ReflectionFreeToString.fs @@ -0,0 +1,132 @@ +// 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 ``ReflectionFreeToString`` = + + // Under --reflectionfree the reflective sprintf "%+A" ToString is replaced by a structurally + // generated one. These tests lock in the emitted IL: a match/field-read that boxes each field, + // renders it through Operators.ToString (the `string` operator) with a null guard, and joins the + // parts with String.Concat. No PrintfFormat is constructed. + + [] + let ``Record ToString is generated structurally without printf`` () = + FSharp """ +module ReflectionFreeToString +type Point = { X: int; Y: int } + """ + |> withOptions [ "--reflectionfree" ] + |> compile + |> shouldSucceed + |> verifyIL [""" +.method public strict virtual instance string ToString() cil managed +{ +.custom instance void [runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) + +.maxstack 6 +.locals init (int32 V_0) +IL_0000: ldc.i4.7 +IL_0001: newarr [runtime]System.String +IL_0006: dup +IL_0007: ldc.i4.0 +IL_0008: ldstr "{ " +IL_000d: stelem [runtime]System.String +IL_0012: dup +IL_0013: ldc.i4.1 +IL_0014: ldstr "X = " +IL_0019: stelem [runtime]System.String +IL_001e: dup +IL_001f: ldc.i4.2 +IL_0020: ldarg.0 +IL_0021: ldfld int32 ReflectionFreeToString/Point::X@ +IL_0026: stloc.0 +IL_0027: ldloc.0 +IL_0028: call object [FSharp.Core]Microsoft.FSharp.Core.Operators::Box(!!0) +IL_002d: brfalse.s IL_0037 + +IL_002f: ldloc.0 +IL_0030: call string [FSharp.Core]Microsoft.FSharp.Core.Operators::ToString(!!0) +IL_0035: br.s IL_003c + +IL_0037: ldstr "null" +IL_003c: stelem [runtime]System.String +IL_0041: dup +IL_0042: ldc.i4.3 +IL_0043: ldstr "; " +IL_0048: stelem [runtime]System.String +IL_004d: dup +IL_004e: ldc.i4.4 +IL_004f: ldstr "Y = " +IL_0054: stelem [runtime]System.String +IL_0059: dup +IL_005a: ldc.i4.5 +IL_005b: ldarg.0 +IL_005c: ldfld int32 ReflectionFreeToString/Point::Y@ +IL_0061: stloc.0 +IL_0062: ldloc.0 +IL_0063: call object [FSharp.Core]Microsoft.FSharp.Core.Operators::Box(!!0) +IL_0068: brfalse.s IL_0072 + +IL_006a: ldloc.0 +IL_006b: call string [FSharp.Core]Microsoft.FSharp.Core.Operators::ToString(!!0) +IL_0070: br.s IL_0077 + +IL_0072: ldstr "null" +IL_0077: stelem [runtime]System.String +IL_007c: dup +IL_007d: ldc.i4.6 +IL_007e: ldstr " }" +IL_0083: stelem [runtime]System.String +IL_0088: call string [runtime]System.String::Concat(string[]) +IL_008d: ret +}"""] + + [] + let ``Union ToString is generated structurally without printf`` () = + FSharp """ +module ReflectionFreeToString +type Color = | Red | Custom of int + """ + |> withOptions [ "--reflectionfree" ] + |> compile + |> shouldSucceed + |> verifyIL [""" +.method public strict virtual instance string ToString() cil managed +{ +.custom instance void [runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) + +.maxstack 5 +.locals init (class ReflectionFreeToString/Color/Custom V_0, + int32 V_1) +IL_0000: ldarg.0 +IL_0001: isinst ReflectionFreeToString/Color/_Red +IL_0006: brfalse.s IL_000e + +IL_0008: ldstr "Red" +IL_000d: ret + +IL_000e: ldarg.0 +IL_000f: castclass ReflectionFreeToString/Color/Custom +IL_0014: stloc.0 +IL_0015: ldstr "Custom(" +IL_001a: ldloc.0 +IL_001b: ldfld int32 ReflectionFreeToString/Color/Custom::item +IL_0020: stloc.1 +IL_0021: ldloc.1 +IL_0022: call object [FSharp.Core]Microsoft.FSharp.Core.Operators::Box(!!0) +IL_0027: brfalse.s IL_0031 + +IL_0029: ldloc.1 +IL_002a: call string [FSharp.Core]Microsoft.FSharp.Core.Operators::ToString(!!0) +IL_002f: br.s IL_0036 + +IL_0031: ldstr "null" +IL_0036: ldstr ")" +IL_003b: call string [runtime]System.String::Concat(string, +string, +string) +IL_0040: ret +}"""] diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 3fc031a525f..b4097b32bf9 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -247,6 +247,7 @@ + From 6a2199cbd904b98f11e0661dded31e5cca35a723 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Tue, 23 Jun 2026 09:34:01 +0100 Subject: [PATCH 17/29] Generate reflection-free ToString in the augmentation phase The structural ToString for --reflectionfree records and unions was built in IlxGen, after the optimizer, so its per-field 'string' operator calls were never inlined: each value-type field was boxed and rendered through the generic Operators.ToString, behind a null guard that is dead for a value type. Move the generation into the type-augmentation phase (alongside Equals/GetHashCode/CompareTo) so the body flows through the optimizer. The 'string' operator is now specialised - a value-type field renders via a direct, allocation-free invariant-culture ToString with no boxing and no null guard (reference fields keep the guard so null still renders as "null"). The shared body builders live in AugmentTypeDefinitions; anonymous record types are synthesized too late for augmentation, so they keep generating in IlxGen but reuse the same builder. Output is unchanged; the EmittedIL baselines are updated to the leaner IL. Co-Authored-By: Claude Opus 4.8 --- .../Checking/AugmentWithHashCompare.fs | 114 ++++++++++++++++++ .../Checking/AugmentWithHashCompare.fsi | 12 ++ src/Compiler/Checking/CheckDeclarations.fs | 17 ++- src/Compiler/CodeGen/IlxGen.fs | 101 +++------------- .../EmittedIL/ReflectionFreeToString.fs | 106 +++++++--------- 5 files changed, 201 insertions(+), 149 deletions(-) diff --git a/src/Compiler/Checking/AugmentWithHashCompare.fs b/src/Compiler/Checking/AugmentWithHashCompare.fs index 0d3c12b7599..6521d29b694 100644 --- a/src/Compiler/Checking/AugmentWithHashCompare.fs +++ b/src/Compiler/Checking/AugmentWithHashCompare.fs @@ -81,6 +81,9 @@ let mkGetHashCodeSlotSig (g: TcGlobals) = let mkEqualsSlotSig (g: TcGlobals) = TSlotSig("Equals", g.obj_ty_noNulls, [], [], [ [ TSlotParam(Some("obj"), g.obj_ty_withNulls, false, false, false, []) ] ], Some g.bool_ty) +let mkToStringSlotSig (g: TcGlobals) = + TSlotSig("ToString", g.obj_ty_noNulls, [], [], [ [] ], Some g.string_ty) + //------------------------------------------------------------------------- // Helpers associated with code-generation of comparison/hash augmentations //------------------------------------------------------------------------- @@ -112,6 +115,9 @@ let mkEqualsWithComparerTyExact g ty = let mkHashTy g ty = mkFunTy g (mkThisTy g ty) (mkFunTy g g.unit_ty g.int_ty) +let mkToStringTy (g: TcGlobals, ty: TType) = + mkFunTy g (mkThisTy g ty) (mkFunTy g g.unit_ty g.string_ty) + let mkHashWithComparerTy g ty = mkFunTy g (mkThisTy g ty) (mkFunTy g g.IEqualityComparer_ty g.int_ty) @@ -1700,3 +1706,111 @@ let MakeBindingsForUnionAugmentation g (tycon: Tycon) (vals: ValRef list) = let isdata = mkUnionCaseTest g (thise, ucr, tinst, m) let expr = mkLambdas g m tps [ thisv; unitv ] (isdata, g.bool_ty) mkCompGenBind v.Deref expr) + +//------------------------------------------------------------------------- +// Build reflection-free ToString functions for union and record types. +// +// Under --reflectionfree the reflective 'sprintf "%+A"' ToString is unavailable, so we build a structural +// one here (during type augmentation, so the 'string' operator calls flow through the optimizer and get +// specialised - e.g. an int field renders via a direct, allocation-free ToString rather than a boxed call). +//------------------------------------------------------------------------- + +// Render one field value as a string the way option/list do (LanguagePrimitives.anyToStringShowingNull): +// a null reference renders as "null", everything else via the 'string' operator. A value-type field can +// never be null, so it skips the box+null-guard and renders directly. +let mkFieldToString (g: TcGlobals, m: Text.range, fe: Expr) = + let fieldTy = tyOfExpr g fe + + if isStructTy g fieldTy then + mkCallStringOperator g m fieldTy fe + else + let v, ve = mkCompGenLocal m "field" fieldTy + mkCompGenLet m v fe (mkNonNullCond g m g.string_ty (mkCallBox g m fieldTy ve) (mkCallStringOperator g m fieldTy ve) (mkString g m "null")) + +// A record's ToString as a single line "{ F1 = v1; F2 = v2 }" (no line breaks, unlike "%+A"). +// openBrace/closeBrace are "{ "/" }" for records and "{| "/" |}" for anonymous records. +let mkRecdToString (g: TcGlobals, tcref: TyconRef, tycon: Tycon, openBrace: string, closeBrace: string) = + let m = tycon.Range + let tinst, ty = mkMinimalTy g tcref + let thisv, thise = mkThisVar g m ty + + let fieldParts = + tcref.AllInstanceFieldsAsList + |> List.mapi (fun i fspec -> + let fref = tcref.MakeNestedRecdFieldRef fspec + let value = mkFieldToString (g, m, mkRecdFieldGetViaExprAddr (thise, fref, tinst, m)) + let nameEq = mkString g m (fspec.DisplayName + " = ") + if i = 0 then [ nameEq; value ] else [ mkString g m "; "; nameEq; value ]) + |> List.concat + + let parts = mkString g m openBrace :: fieldParts @ [ mkString g m closeBrace ] + thisv, mkStringConcat (g, m, parts) + +// A union's ToString as a match over the cases building "CaseName(f0, f1, ...)" (or just "CaseName" for a +// nullary case). +let mkUnionToString (g: TcGlobals, tcref: TyconRef, tycon: Tycon) = + let m = tycon.Range + let tinst, ty = mkMinimalTy g tcref + let thisv, thise = mkThisVar g m ty + let mbuilder = MatchBuilder(DebugPointAtBinding.NoneAtInvisible, m) + + let mkResult (ucase: UnionCase) = + let cref = tcref.MakeNestedUnionCaseRef ucase + let rfields = ucase.RecdFields + + if isNil rfields then + mkString g m ucase.DisplayName + else + // provene is an expression proven to be of this case (the value itself for struct unions, + // otherwise a 'UnionCaseProof'), from which fields can be read. + let mkBody (provene: Expr) = + let fieldStrs = + rfields + |> List.mapi (fun j _ -> mkFieldToString (g, m, mkUnionCaseFieldGetProvenViaExprAddr (provene, cref, tinst, j, m))) + + let sep = mkString g m ", " + + let fieldsWithSeps = + fieldStrs |> List.mapi (fun i fe -> if i = 0 then [ fe ] else [ sep; fe ]) |> List.concat + + let parts = mkString g m (ucase.DisplayName + "(") :: fieldsWithSeps @ [ mkString g m ")" ] + mkStringConcat (g, m, parts) + + if cref.Tycon.IsStructOrEnumTycon then + mkBody thise + else + let ucv, ucve = mkCompGenLocal m "thisCast" (mkProvenUnionCaseTy cref tinst) + mkCompGenLet m ucv (mkUnionCaseProof (thise, cref, tinst, m)) (mkBody ucve) + + let cases = + tcref.UnionCasesAsList + |> List.map (fun ucase -> + let cref = tcref.MakeNestedUnionCaseRef ucase + mkCase (DecisionTreeTest.UnionCase(cref, tinst), mbuilder.AddResultTarget(mkResult ucase))) + + let dtree = TDSwitch(thise, cases, None, m) + thisv, mbuilder.Close(dtree, m, g.string_ty) + +let TyconIsCandidateForAugmentationWithToString (g: TcGlobals, tycon: Tycon) = + g.useReflectionFreeCodeGen && (tycon.IsUnionTycon || tycon.IsRecordTycon) + +let MakeValsForToStringAugmentation (g: TcGlobals, tcref: TyconRef) = + let _, ty = mkMinimalTy g tcref + let vis = tcref.Accessibility + let tps = tcref.Typars tcref.Range + mkValSpec g tcref ty vis (Some(mkToStringSlotSig g)) "ToString" (tps +-> (mkToStringTy (g, ty))) unitArg false + +let MakeBindingsForToStringAugmentation (g: TcGlobals, tycon: Tycon, toStringVal: Val) = + let tcref = mkLocalTyconRef tycon + let m = tycon.Range + let tps = tycon.Typars m + + let thisv, body = + if tycon.IsUnionTycon then + mkUnionToString (g, tcref, tycon) + else + mkRecdToString (g, tcref, tycon, "{ ", " }") + + let unitv, _ = mkCompGenLocal m "unitArg" g.unit_ty + let expr = mkLambdas g m tps [ thisv; unitv ] (body, g.string_ty) + [ mkCompGenBind toStringVal expr ] diff --git a/src/Compiler/Checking/AugmentWithHashCompare.fsi b/src/Compiler/Checking/AugmentWithHashCompare.fsi index b57e25f32cc..13899af4d36 100644 --- a/src/Compiler/Checking/AugmentWithHashCompare.fsi +++ b/src/Compiler/Checking/AugmentWithHashCompare.fsi @@ -51,3 +51,15 @@ val TypeDefinitelyHasEquality: TcGlobals -> TType -> bool val MakeValsForUnionAugmentation: TcGlobals -> TyconRef -> Val list val MakeBindingsForUnionAugmentation: TcGlobals -> Tycon -> ValRef list -> Binding list + +/// Build a record's single-line reflection-free ToString body; returns the 'this' value and the body expression. +val mkRecdToString: g: TcGlobals * tcref: TyconRef * tycon: Tycon * openBrace: string * closeBrace: string -> Val * Expr + +/// Whether a reflection-free structural ToString should be generated for this type. +val TyconIsCandidateForAugmentationWithToString: g: TcGlobals * tycon: Tycon -> bool + +/// Make the ToString override slot for a reflection-free record or union. +val MakeValsForToStringAugmentation: g: TcGlobals * tcref: TyconRef -> Val + +/// Build the body binding for a reflection-free record or union ToString override. +val MakeBindingsForToStringAugmentation: g: TcGlobals * tycon: Tycon * toStringVal: Val -> Binding list diff --git a/src/Compiler/Checking/CheckDeclarations.fs b/src/Compiler/Checking/CheckDeclarations.fs index aa8eb266d07..ee9c5858090 100644 --- a/src/Compiler/Checking/CheckDeclarations.fs +++ b/src/Compiler/Checking/CheckDeclarations.fs @@ -910,6 +910,18 @@ module AddAugmentationDeclarations = else [] else [] + // Under --reflectionfree the structural ToString is generated here (rather than in IlxGen) so the 'string' + // operator calls in its body flow through the optimizer and get specialised. Like the Equals override, this + // runs late so tycon.HasMember gives correct results for a user-written ToString. + let AddReflectionFreeToStringBindings (cenv: cenv, env: TcEnv, tycon: Tycon) = + let g = cenv.g + if AugmentTypeDefinitions.TyconIsCandidateForAugmentationWithToString(g, tycon) && not (tycon.HasMember g "ToString" []) then + let tcref = mkLocalTyconRef tycon + let toStringVal = AugmentTypeDefinitions.MakeValsForToStringAugmentation(g, tcref) + PublishValueDefn cenv env ModuleOrMemberBinding toStringVal + AugmentTypeDefinitions.MakeBindingsForToStringAugmentation(g, tycon, toStringVal) + else [] + let ShouldAugmentUnion (g: TcGlobals) (tycon: Tycon) = g.langVersion.SupportsFeature LanguageFeature.UnionIsPropertiesVisible && HasDefaultAugmentationAttribute g (mkLocalTyconRef tycon) && @@ -4728,8 +4740,9 @@ module TcDeclarations = // We put the hash/compare bindings before the type definitions and the // equality bindings after because tha is the order they've always been generated // in, and there are code generation tests to check that. - let binds = AddAugmentationDeclarations.AddGenericHashAndComparisonBindings cenv tycon + let binds = AddAugmentationDeclarations.AddGenericHashAndComparisonBindings cenv tycon let binds3 = AddAugmentationDeclarations.AddGenericEqualityBindings cenv envForDecls tycon + let binds5 = AddAugmentationDeclarations.AddReflectionFreeToStringBindings(cenv, envForDecls, tycon) let binds4 = if tycon.IsUnionTycon && AddAugmentationDeclarations.ShouldAugmentUnion g tycon then let unionVals = @@ -4739,7 +4752,7 @@ module TcDeclarations = AugmentTypeDefinitions.MakeBindingsForUnionAugmentation g tycon (List.map mkLocalValRef unionVals) else [] - binds@binds4, binds3) + binds@binds4, binds3@binds5) // Check for cyclic structs and inheritance all over again, since we may have added some fields to the struct when generating the implicit construction syntax EstablishTypeDefinitionCores.TcTyconDefnCore_CheckForCyclicStructsAndInheritance cenv tycons diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index f4aba4823db..6e756e92b9f 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -11279,17 +11279,6 @@ and GenSprintfPrintingMethod cenv eenv methName ilThisTy m = | _ -> () ] -/// Generate the 'ToString' method for a union type. Normally this calls 'sprintf "%+A"' (see -/// GenSprintfPrintingMethod). Under reflection-free code generation 'sprintf' is unavailable, so instead emit a -/// match over the cases that builds "CaseName(f0, f1, ...)" using the 'string' operator on each field. -/// Format one field value the same way option/list do (LanguagePrimitives.anyToStringShowingNull): -/// render null as "null", otherwise via the 'string' operator. -and GenFieldToString (cenv: cenv, m: range, fe: Expr) = - let g = cenv.g - let fieldTy = tyOfExpr g fe - let v, ve = mkCompGenLocal m "field" fieldTy - mkCompGenLet m v fe (mkNonNullCond g m g.string_ty (mkCallBox g m fieldTy ve) (mkCallStringOperator g m fieldTy ve) (mkString g m "null")) - /// Emit a [] virtual ToString override whose body is the given string-typed expression. /// 'thisv' is the 'this' value (stored at arg 0) referenced by bodyExpr. and EmitToStringMethodDef (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, thisv: Val, bodyExpr: Expr) = @@ -11308,84 +11297,18 @@ and EmitToStringMethodDef (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, [ mdef.With(customAttrs = mkILCustomAttrs [ g.CompilerGeneratedAttribute ]) ] -/// Build the 'this' local for a generated ToString (a byref for struct types) and the type instantiation. -and GenToStringThis (cenv: cenv, tcref: TyconRef, m: range) = - let g = cenv.g - let tinst, ty = generalizeTyconRef g tcref - let thisv, thise = mkCompGenLocal m "this" (if isStructTy g ty then mkByrefTy g ty else ty) - tinst, thisv, thise - -and GenUnionToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, ilThisTy: ILType, tcref: TyconRef, m: range) = - let g = cenv.g - - if not g.useReflectionFreeCodeGen then - GenSprintfPrintingMethod cenv eenv "ToString" ilThisTy m - else - let tinst, thisv, thise = GenToStringThis (cenv, tcref, m) - - let mbuilder = MatchBuilder(DebugPointAtBinding.NoneAtInvisible, m) - - let mkResult (ucase: UnionCase) = - let cref = tcref.MakeNestedUnionCaseRef ucase - let rfields = ucase.RecdFields - - if isNil rfields then - mkString g m ucase.DisplayName - else - // provene is an expression proven to be of this case (the value itself for struct unions, - // otherwise a 'UnionCaseProof'), from which fields can be read. - let mkBody (provene: Expr) = - let fieldStrs = - rfields - |> List.mapi (fun j _ -> GenFieldToString (cenv, m, mkUnionCaseFieldGetProvenViaExprAddr (provene, cref, tinst, j, m))) - - let sep = mkString g m ", " - - let fieldsWithSeps = - fieldStrs |> List.mapi (fun i fe -> if i = 0 then [ fe ] else [ sep; fe ]) |> List.concat - - let parts = mkString g m (ucase.DisplayName + "(") :: fieldsWithSeps @ [ mkString g m ")" ] - mkStringConcat (g, m, parts) - - if cref.Tycon.IsStructOrEnumTycon then - mkBody thise - else - let ucv, ucve = mkCompGenLocal m "thisCast" (mkProvenUnionCaseTy cref tinst) - mkCompGenLet m ucv (mkUnionCaseProof (thise, cref, tinst, m)) (mkBody ucve) - - let cases = - tcref.UnionCasesAsList - |> List.map (fun ucase -> - let cref = tcref.MakeNestedUnionCaseRef ucase - mkCase (DecisionTreeTest.UnionCase(cref, tinst), mbuilder.AddResultTarget(mkResult ucase))) - - let dtree = TDSwitch(thise, cases, None, m) - let matchExpr = mbuilder.Close(dtree, m, g.string_ty) - - EmitToStringMethodDef (cenv, mgbuf, eenv, thisv, matchExpr) - -/// Generate a record's ToString as a single line "{ F1 = v1; F2 = v2 }" (no line breaks, unlike "%+A"), -/// fields formatted like union fields. openBrace/closeBrace are "{ "/" }" for records and "{| "/" |}" for -/// anonymous records. Under non-reflection-free codegen, falls back to sprintf "%+A". +/// Generate an anonymous record's ToString as a single line "{| F1 = v1; F2 = v2 |}". Nominal records and +/// unions get their reflection-free ToString from the type-augmentation phase instead (so the 'string' +/// operator calls are optimized), but anonymous record types are synthesized too late for that, so they are +/// generated here. Under non-reflection-free codegen, falls back to sprintf "%+A". and GenRecordToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, ilThisTy: ILType, tcref: TyconRef, m: range, openBrace: string, closeBrace: string) = let g = cenv.g if not g.useReflectionFreeCodeGen then GenSprintfPrintingMethod cenv eenv "ToString" ilThisTy m else - let tinst, thisv, thise = GenToStringThis (cenv, tcref, m) - - let fieldParts = - tcref.AllInstanceFieldsAsList - |> List.mapi (fun i fspec -> - let fref = tcref.MakeNestedRecdFieldRef fspec - let value = GenFieldToString (cenv, m, mkRecdFieldGetViaExprAddr (thise, fref, tinst, m)) - let nameEq = mkString g m (fspec.DisplayName + " = ") - if i = 0 then [ nameEq; value ] else [ mkString g m "; "; nameEq; value ]) - |> List.concat - - let parts = mkString g m openBrace :: fieldParts @ [ mkString g m closeBrace ] - EmitToStringMethodDef (cenv, mgbuf, eenv, thisv, mkStringConcat (g, m, parts)) + let thisv, body = AugmentTypeDefinitions.mkRecdToString (g, tcref, tcref.Deref, openBrace, closeBrace) + EmitToStringMethodDef (cenv, mgbuf, eenv, thisv, body) and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option = let g = cenv.g @@ -11970,8 +11893,10 @@ and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option then yield mkILSimpleStorageCtor (Some g.ilg.typ_Object.TypeSpec, ilThisTy, [], [], reprAccess, None, eenv.imports) - if not (tycon.HasMember g "ToString" []) then - yield! GenRecordToStringMethod(cenv, mgbuf, eenvinner, ilThisTy, tcref, m, "{ ", " }") + // Reflection-free nominal records get their ToString from the type-augmentation phase; here we + // only emit the sprintf "%+A" ToString for the non-reflection-free case. + if not g.useReflectionFreeCodeGen && not (tycon.HasMember g "ToString" []) then + yield! GenSprintfPrintingMethod cenv eenvinner "ToString" ilThisTy m | TFSharpTyconRepr r when tycon.IsFSharpDelegateTycon -> @@ -11994,8 +11919,10 @@ and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option yield! mkILDelegateMethods reprAccess g.ilg (g.iltyp_AsyncCallback, g.iltyp_IAsyncResult) (parameters, ret) | _ -> () - | TFSharpTyconRepr { fsobjmodel_kind = TFSharpUnion } when not (tycon.HasMember g "ToString" []) -> - yield! GenUnionToStringMethod(cenv, mgbuf, eenvinner, ilThisTy, tcref, m) + // Reflection-free nominal unions get their ToString from the type-augmentation phase; here we + // only emit the sprintf "%+A" ToString for the non-reflection-free case. + | TFSharpTyconRepr { fsobjmodel_kind = TFSharpUnion } when not g.useReflectionFreeCodeGen && not (tycon.HasMember g "ToString" []) -> + yield! GenSprintfPrintingMethod cenv eenvinner "ToString" ilThisTy m | _ -> () ] diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/ReflectionFreeToString.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/ReflectionFreeToString.fs index fb27a9a73d2..c14608f9989 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/ReflectionFreeToString.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/ReflectionFreeToString.fs @@ -7,10 +7,8 @@ open FSharp.Test.Compiler module ``ReflectionFreeToString`` = - // Under --reflectionfree the reflective sprintf "%+A" ToString is replaced by a structurally - // generated one. These tests lock in the emitted IL: a match/field-read that boxes each field, - // renders it through Operators.ToString (the `string` operator) with a null guard, and joins the - // parts with String.Concat. No PrintfFormat is constructed. + // Under --reflectionfree, records and unions get a structural ToString (fields joined with String.Concat, + // value-type fields rendered via a direct allocation-free ToString, no PrintfFormat) instead of sprintf "%+A". [] let ``Record ToString is generated structurally without printf`` () = @@ -22,11 +20,11 @@ type Point = { X: int; Y: int } |> compile |> shouldSucceed |> verifyIL [""" -.method public strict virtual instance string ToString() cil managed +.method public hidebysig virtual final instance string ToString() cil managed { .custom instance void [runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) -.maxstack 6 +.maxstack 8 .locals init (int32 V_0) IL_0000: ldc.i4.7 IL_0001: newarr [runtime]System.String @@ -43,45 +41,37 @@ IL_001f: ldc.i4.2 IL_0020: ldarg.0 IL_0021: ldfld int32 ReflectionFreeToString/Point::X@ IL_0026: stloc.0 -IL_0027: ldloc.0 -IL_0028: call object [FSharp.Core]Microsoft.FSharp.Core.Operators::Box(!!0) -IL_002d: brfalse.s IL_0037 - -IL_002f: ldloc.0 -IL_0030: call string [FSharp.Core]Microsoft.FSharp.Core.Operators::ToString(!!0) -IL_0035: br.s IL_003c - -IL_0037: ldstr "null" -IL_003c: stelem [runtime]System.String -IL_0041: dup -IL_0042: ldc.i4.3 -IL_0043: ldstr "; " -IL_0048: stelem [runtime]System.String -IL_004d: dup -IL_004e: ldc.i4.4 -IL_004f: ldstr "Y = " -IL_0054: stelem [runtime]System.String -IL_0059: dup -IL_005a: ldc.i4.5 -IL_005b: ldarg.0 -IL_005c: ldfld int32 ReflectionFreeToString/Point::Y@ -IL_0061: stloc.0 -IL_0062: ldloc.0 -IL_0063: call object [FSharp.Core]Microsoft.FSharp.Core.Operators::Box(!!0) -IL_0068: brfalse.s IL_0072 - -IL_006a: ldloc.0 -IL_006b: call string [FSharp.Core]Microsoft.FSharp.Core.Operators::ToString(!!0) -IL_0070: br.s IL_0077 - -IL_0072: ldstr "null" -IL_0077: stelem [runtime]System.String -IL_007c: dup -IL_007d: ldc.i4.6 -IL_007e: ldstr " }" -IL_0083: stelem [runtime]System.String -IL_0088: call string [runtime]System.String::Concat(string[]) -IL_008d: ret +IL_0027: ldloca.s V_0 +IL_0029: ldnull +IL_002a: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture() +IL_002f: call instance string [netstandard]System.Int32::ToString(string, +class [netstandard]System.IFormatProvider) +IL_0034: stelem [runtime]System.String +IL_0039: dup +IL_003a: ldc.i4.3 +IL_003b: ldstr "; " +IL_0040: stelem [runtime]System.String +IL_0045: dup +IL_0046: ldc.i4.4 +IL_0047: ldstr "Y = " +IL_004c: stelem [runtime]System.String +IL_0051: dup +IL_0052: ldc.i4.5 +IL_0053: ldarg.0 +IL_0054: ldfld int32 ReflectionFreeToString/Point::Y@ +IL_0059: stloc.0 +IL_005a: ldloca.s V_0 +IL_005c: ldnull +IL_005d: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture() +IL_0062: call instance string [netstandard]System.Int32::ToString(string, +class [netstandard]System.IFormatProvider) +IL_0067: stelem [runtime]System.String +IL_006c: dup +IL_006d: ldc.i4.6 +IL_006e: ldstr " }" +IL_0073: stelem [runtime]System.String +IL_0078: call string [runtime]System.String::Concat(string[]) +IL_007d: ret }"""] [] @@ -94,13 +84,13 @@ type Color = | Red | Custom of int |> compile |> shouldSucceed |> verifyIL [""" -.method public strict virtual instance string ToString() cil managed +.method public hidebysig virtual final instance string ToString() cil managed { .custom instance void [runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) -.maxstack 5 +.maxstack 6 .locals init (class ReflectionFreeToString/Color/Custom V_0, - int32 V_1) +int32 V_1) IL_0000: ldarg.0 IL_0001: isinst ReflectionFreeToString/Color/_Red IL_0006: brfalse.s IL_000e @@ -115,18 +105,14 @@ IL_0015: ldstr "Custom(" IL_001a: ldloc.0 IL_001b: ldfld int32 ReflectionFreeToString/Color/Custom::item IL_0020: stloc.1 -IL_0021: ldloc.1 -IL_0022: call object [FSharp.Core]Microsoft.FSharp.Core.Operators::Box(!!0) -IL_0027: brfalse.s IL_0031 - -IL_0029: ldloc.1 -IL_002a: call string [FSharp.Core]Microsoft.FSharp.Core.Operators::ToString(!!0) -IL_002f: br.s IL_0036 - -IL_0031: ldstr "null" -IL_0036: ldstr ")" -IL_003b: call string [runtime]System.String::Concat(string, +IL_0021: ldloca.s V_1 +IL_0023: ldnull +IL_0024: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture() +IL_0029: call instance string [netstandard]System.Int32::ToString(string, +class [netstandard]System.IFormatProvider) +IL_002e: ldstr ")" +IL_0033: call string [runtime]System.String::Concat(string, string, string) -IL_0040: ret +IL_0038: ret }"""] From 474b9f94c51e887fea72356f7fe84de949f0c81a Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Sat, 27 Jun 2026 11:11:14 +0100 Subject: [PATCH 18/29] Guard generated reflection-free ToString against deep-recursion overflow The augmentation-generated structural ToString recurses into fields, so a deeply nested value can exhaust the stack with an uncatchable StackOverflowException. Emit RuntimeHelpers.EnsureSufficientExecutionStack() at method entry (as C# records do in PrintMembers) so it throws a catchable InsufficientExecutionStackException instead, when the runtime provides the method. The guard is skipped for types whose every field is a flat primitive (integer/float/decimal/string/char/bool/unit/enum), which cannot recurse. Co-Authored-By: Claude Opus 4.8 --- .../Checking/AugmentWithHashCompare.fs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/Compiler/Checking/AugmentWithHashCompare.fs b/src/Compiler/Checking/AugmentWithHashCompare.fs index 6521d29b694..30a7a077c0d 100644 --- a/src/Compiler/Checking/AugmentWithHashCompare.fs +++ b/src/Compiler/Checking/AugmentWithHashCompare.fs @@ -1811,6 +1811,37 @@ let MakeBindingsForToStringAugmentation (g: TcGlobals, tycon: Tycon, toStringVal else mkRecdToString (g, tcref, tycon, "{ ", " }") + // Only a non-flat field can make ToString recurse. + let mightRecurse = + let isFlat (ty: TType) = + isIntegerTy g ty + || isFpTy g ty + || isDecimalTy g ty + || isStringTy g ty + || typeEquiv g g.char_ty ty + || isBoolTy g ty + || isUnitTy g ty + || isEnumTy g ty + + let fieldTys = + if tycon.IsUnionTycon then + tycon.UnionCasesAsList |> List.collect (fun uc -> uc.RecdFields) |> List.map (fun rf -> rf.FormalType) + else + tycon.AllInstanceFieldsAsList |> List.map (fun rf -> rf.FormalType) + + fieldTys |> List.exists (fun ty -> not (isFlat ty)) + + // Guard deep recursion with a catchable exception, as C# records' PrintMembers do, when the runtime provides it. + let body = + if mightRecurse then + match g.TryFindSysILTypeRef "System.Runtime.CompilerServices.RuntimeHelpers" with + | Some tref -> + let mspec = mkILNonGenericStaticMethSpecInTy (mkILNonGenericBoxedTy tref, "EnsureSufficientExecutionStack", [], ILType.Void) + mkSequential m (mkAsmExpr ([ mkNormalCall mspec ], [], [], [], m)) body + | None -> body + else + body + let unitv, _ = mkCompGenLocal m "unitArg" g.unit_ty let expr = mkLambdas g m tps [ thisv; unitv ] (body, g.string_ty) [ mkCompGenBind toStringVal expr ] From e130e50324b61a1ee49f5ea167c1dbd17c887dac Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Sat, 27 Jun 2026 11:17:45 +0100 Subject: [PATCH 19/29] Test the reflection-free ToString deep-recursion guard A 1,000,000-deep value's generated ToString throws a catchable InsufficientExecutionStackException rather than hard-crashing the process. Co-Authored-By: Claude Opus 4.8 --- .../Checking/AugmentWithHashCompare.fs | 5 ++-- .../CompilerOptions/fsc/reflectionfree.fs | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Compiler/Checking/AugmentWithHashCompare.fs b/src/Compiler/Checking/AugmentWithHashCompare.fs index 30a7a077c0d..ef09eca035e 100644 --- a/src/Compiler/Checking/AugmentWithHashCompare.fs +++ b/src/Compiler/Checking/AugmentWithHashCompare.fs @@ -1811,9 +1811,8 @@ let MakeBindingsForToStringAugmentation (g: TcGlobals, tycon: Tycon, toStringVal else mkRecdToString (g, tcref, tycon, "{ ", " }") - // Only a non-flat field can make ToString recurse. let mightRecurse = - let isFlat (ty: TType) = + let isPrimitive (ty: TType) = isIntegerTy g ty || isFpTy g ty || isDecimalTy g ty @@ -1829,7 +1828,7 @@ let MakeBindingsForToStringAugmentation (g: TcGlobals, tycon: Tycon, toStringVal else tycon.AllInstanceFieldsAsList |> List.map (fun rf -> rf.FormalType) - fieldTys |> List.exists (fun ty -> not (isFlat ty)) + fieldTys |> List.exists (isPrimitive >> not) // Guard deep recursion with a catchable exception, as C# records' PrintMembers do, when the runtime provides it. let body = diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs index a4696227b7e..e7171a3ad65 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs @@ -245,6 +245,29 @@ let main _ = |> withStdOutContains "Node(Node(Leaf, 1, Leaf), 2, Leaf)" |> withStdOutContains "{ Value = 1; Parent = Some({ Value = 0; Parent = null }) }" +[] +let ``Deeply nested data fails the generated ToString with a catchable exception, not a hard overflow`` () = + FSharp """ +module Test +type Chain = | End | Link of int * Chain + +[] +let main _ = + let mutable c = End + for i in 1 .. 1_000_000 do c <- Link(i, c) + try + c.ToString() |> ignore + System.Console.WriteLine "rendered" + with :? System.InsufficientExecutionStackException -> + System.Console.WriteLine "caught" + 0 + """ + |> asExe + |> withOptions [ "--reflectionfree" ] + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "caught" + [] let ``No debug display attribute`` () = someCode From 6e276b1dc58a1e31991e0d52634df7ec7c5ecac4 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Mon, 29 Jun 2026 14:52:47 +0100 Subject: [PATCH 20/29] revert ToString additions to fsharp.core types --- src/FSharp.Core/prim-types.fs | 76 ++++--------------- ...p.Core.SurfaceArea.netstandard20.debug.bsl | 9 +-- ...Core.SurfaceArea.netstandard20.release.bsl | 9 +-- ...p.Core.SurfaceArea.netstandard21.debug.bsl | 9 +-- ...Core.SurfaceArea.netstandard21.release.bsl | 7 -- .../Microsoft.FSharp.Core/ResultTests.fs | 7 -- 6 files changed, 16 insertions(+), 101 deletions(-) diff --git a/src/FSharp.Core/prim-types.fs b/src/FSharp.Core/prim-types.fs index 663623b8966..036ba49ce48 100644 --- a/src/FSharp.Core/prim-types.fs +++ b/src/FSharp.Core/prim-types.fs @@ -3709,60 +3709,34 @@ namespace Microsoft.FSharp.Core [] [] - type Choice<'T1,'T2> = - | Choice1Of2 of 'T1 + type Choice<'T1,'T2> = + | Choice1Of2 of 'T1 | Choice2Of2 of 'T2 - - override x.ToString() = - match x with - | Choice1Of2 v -> String.Concat("Choice1Of2(", anyToStringShowingNull v, ")") - | Choice2Of2 v -> String.Concat("Choice2Of2(", anyToStringShowingNull v, ")") - + [] [] type Choice<'T1,'T2,'T3> = - | Choice1Of3 of 'T1 + | Choice1Of3 of 'T1 | Choice2Of3 of 'T2 | Choice3Of3 of 'T3 - - override x.ToString() = - match x with - | Choice1Of3 v -> String.Concat("Choice1Of3(", anyToStringShowingNull v, ")") - | Choice2Of3 v -> String.Concat("Choice2Of3(", anyToStringShowingNull v, ")") - | Choice3Of3 v -> String.Concat("Choice3Of3(", anyToStringShowingNull v, ")") - + [] [] type Choice<'T1,'T2,'T3,'T4> = - | Choice1Of4 of 'T1 + | Choice1Of4 of 'T1 | Choice2Of4 of 'T2 | Choice3Of4 of 'T3 | Choice4Of4 of 'T4 - - override x.ToString() = - match x with - | Choice1Of4 v -> String.Concat("Choice1Of4(", anyToStringShowingNull v, ")") - | Choice2Of4 v -> String.Concat("Choice2Of4(", anyToStringShowingNull v, ")") - | Choice3Of4 v -> String.Concat("Choice3Of4(", anyToStringShowingNull v, ")") - | Choice4Of4 v -> String.Concat("Choice4Of4(", anyToStringShowingNull v, ")") - + [] [] type Choice<'T1,'T2,'T3,'T4,'T5> = - | Choice1Of5 of 'T1 + | Choice1Of5 of 'T1 | Choice2Of5 of 'T2 | Choice3Of5 of 'T3 | Choice4Of5 of 'T4 | Choice5Of5 of 'T5 - - override x.ToString() = - match x with - | Choice1Of5 v -> String.Concat("Choice1Of5(", anyToStringShowingNull v, ")") - | Choice2Of5 v -> String.Concat("Choice2Of5(", anyToStringShowingNull v, ")") - | Choice3Of5 v -> String.Concat("Choice3Of5(", anyToStringShowingNull v, ")") - | Choice4Of5 v -> String.Concat("Choice4Of5(", anyToStringShowingNull v, ")") - | Choice5Of5 v -> String.Concat("Choice5Of5(", anyToStringShowingNull v, ")") - + [] [] type Choice<'T1,'T2,'T3,'T4,'T5,'T6> = @@ -3772,16 +3746,7 @@ namespace Microsoft.FSharp.Core | Choice4Of6 of 'T4 | Choice5Of6 of 'T5 | Choice6Of6 of 'T6 - - override x.ToString() = - match x with - | Choice1Of6 v -> String.Concat("Choice1Of6(", anyToStringShowingNull v, ")") - | Choice2Of6 v -> String.Concat("Choice2Of6(", anyToStringShowingNull v, ")") - | Choice3Of6 v -> String.Concat("Choice3Of6(", anyToStringShowingNull v, ")") - | Choice4Of6 v -> String.Concat("Choice4Of6(", anyToStringShowingNull v, ")") - | Choice5Of6 v -> String.Concat("Choice5Of6(", anyToStringShowingNull v, ")") - | Choice6Of6 v -> String.Concat("Choice6Of6(", anyToStringShowingNull v, ")") - + [] [] type Choice<'T1,'T2,'T3,'T4,'T5,'T6,'T7> = @@ -3792,17 +3757,7 @@ namespace Microsoft.FSharp.Core | Choice5Of7 of 'T5 | Choice6Of7 of 'T6 | Choice7Of7 of 'T7 - - override x.ToString() = - match x with - | Choice1Of7 v -> String.Concat("Choice1Of7(", anyToStringShowingNull v, ")") - | Choice2Of7 v -> String.Concat("Choice2Of7(", anyToStringShowingNull v, ")") - | Choice3Of7 v -> String.Concat("Choice3Of7(", anyToStringShowingNull v, ")") - | Choice4Of7 v -> String.Concat("Choice4Of7(", anyToStringShowingNull v, ")") - | Choice5Of7 v -> String.Concat("Choice5Of7(", anyToStringShowingNull v, ")") - | Choice6Of7 v -> String.Concat("Choice6Of7(", anyToStringShowingNull v, ")") - | Choice7Of7 v -> String.Concat("Choice7Of7(", anyToStringShowingNull v, ")") - + [] exception MatchFailureException of string * int * int with override x.Message = SR.GetString(SR.matchCasesIncomplete) @@ -4099,15 +4054,10 @@ namespace Microsoft.FSharp.Core [] [] [] - type Result<'T,'TError> = - | Ok of ResultValue:'T + type Result<'T,'TError> = + | Ok of ResultValue:'T | Error of ErrorValue:'TError - override x.ToString() = - match x with - | Ok v -> String.Concat("Ok(", anyToStringShowingNull v, ")") - | Error e -> String.Concat("Error(", anyToStringShowingNull e, ")") - [] [] [] 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 0e1b7eea5c5..5b6cc0bce4e 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl @@ -1106,7 +1106,6 @@ Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice` Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2+Tags[T1,T2] Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice1Of2(T1) Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice2Of2(T2) -Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 Item Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`3+Choice2Of3[T1,T2,T3]: T2 Item @@ -1140,7 +1139,6 @@ Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoi Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice1Of3(T1) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice2Of3(T2) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice3Of3(T3) -Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 Item Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`4+Choice2Of4[T1,T2,T3,T4]: T2 Item @@ -1181,7 +1179,6 @@ Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpC Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice2Of4(T2) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice3Of4(T3) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice4Of4(T4) -Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 Item Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`5+Choice2Of5[T1,T2,T3,T4,T5]: T2 Item @@ -1229,7 +1226,6 @@ Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSha Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice3Of5(T3) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice4Of5(T4) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice5Of5(T5) -Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 Item Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`6+Choice2Of6[T1,T2,T3,T4,T5,T6]: T2 Item @@ -1284,7 +1280,6 @@ Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.F Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice4Of6(T4) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice5Of6(T5) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice6Of6(T6) -Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 Item Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`7+Choice2Of7[T1,T2,T3,T4,T5,T6,T7]: T2 Item @@ -1346,7 +1341,6 @@ Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Cor Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice5Of7(T5) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice6Of7(T6) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice7Of7(T7) -Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: System.String ToString() Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] FromConverter(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] op_Implicit(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: System.Converter`2[T,TResult] ToConverter(Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]) @@ -1426,7 +1420,6 @@ Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Int32 get_Tag() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2+Tags[T,TError] Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewError(TError) Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewOk(T) -Microsoft.FSharp.Core.FSharpResult`2[T,TError]: System.String ToString() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T ResultValue Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T get_ResultValue() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: TError ErrorValue @@ -2674,4 +2667,4 @@ Microsoft.FSharp.Reflection.UnionCaseInfo: System.String Name Microsoft.FSharp.Reflection.UnionCaseInfo: System.String ToString() Microsoft.FSharp.Reflection.UnionCaseInfo: System.String get_Name() Microsoft.FSharp.Reflection.UnionCaseInfo: System.Type DeclaringType -Microsoft.FSharp.Reflection.UnionCaseInfo: System.Type get_DeclaringType() +Microsoft.FSharp.Reflection.UnionCaseInfo: System.Type get_DeclaringType() \ No newline at end of file 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 8ddee8cb2d2..217d4b7c837 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl @@ -603,8 +603,8 @@ Microsoft.FSharp.Collections.SetModule: Microsoft.FSharp.Collections.FSharpSet`1 Microsoft.FSharp.Collections.SetModule: Microsoft.FSharp.Collections.FSharpSet`1[T] UnionMany[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Collections.FSharpSet`1[T]]) Microsoft.FSharp.Collections.SetModule: Microsoft.FSharp.Collections.FSharpSet`1[T] Union[T](Microsoft.FSharp.Collections.FSharpSet`1[T], Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: System.Collections.Generic.IEnumerable`1[T] ToSeq[T](Microsoft.FSharp.Collections.FSharpSet`1[T]) -Microsoft.FSharp.Collections.SetModule: System.Tuple`2[Microsoft.FSharp.Collections.FSharpSet`1[T1],Microsoft.FSharp.Collections.FSharpSet`1[T2]] PartitionWith[T,T1,T2](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]], Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: System.Tuple`2[Microsoft.FSharp.Collections.FSharpSet`1[T],Microsoft.FSharp.Collections.FSharpSet`1[T]] Partition[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], Microsoft.FSharp.Collections.FSharpSet`1[T]) +Microsoft.FSharp.Collections.SetModule: System.Tuple`2[Microsoft.FSharp.Collections.FSharpSet`1[T1],Microsoft.FSharp.Collections.FSharpSet`1[T2]] PartitionWith[T,T1,T2](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]], Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: T MaxElement[T](Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: T MinElement[T](Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: TState FoldBack[T,TState](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpFunc`2[TState,TState]], Microsoft.FSharp.Collections.FSharpSet`1[T], TState) @@ -1106,7 +1106,6 @@ Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice` Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2+Tags[T1,T2] Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice1Of2(T1) Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice2Of2(T2) -Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 Item Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`3+Choice2Of3[T1,T2,T3]: T2 Item @@ -1140,7 +1139,6 @@ Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoi Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice1Of3(T1) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice2Of3(T2) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice3Of3(T3) -Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 Item Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`4+Choice2Of4[T1,T2,T3,T4]: T2 Item @@ -1181,7 +1179,6 @@ Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpC Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice2Of4(T2) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice3Of4(T3) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice4Of4(T4) -Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 Item Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`5+Choice2Of5[T1,T2,T3,T4,T5]: T2 Item @@ -1229,7 +1226,6 @@ Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSha Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice3Of5(T3) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice4Of5(T4) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice5Of5(T5) -Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 Item Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`6+Choice2Of6[T1,T2,T3,T4,T5,T6]: T2 Item @@ -1284,7 +1280,6 @@ Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.F Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice4Of6(T4) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice5Of6(T5) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice6Of6(T6) -Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 Item Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`7+Choice2Of7[T1,T2,T3,T4,T5,T6,T7]: T2 Item @@ -1346,7 +1341,6 @@ Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Cor Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice5Of7(T5) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice6Of7(T6) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice7Of7(T7) -Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: System.String ToString() Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] FromConverter(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] op_Implicit(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: System.Converter`2[T,TResult] ToConverter(Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]) @@ -1426,7 +1420,6 @@ Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Int32 get_Tag() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2+Tags[T,TError] Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewError(TError) Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewOk(T) -Microsoft.FSharp.Core.FSharpResult`2[T,TError]: System.String ToString() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T ResultValue Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T get_ResultValue() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: TError ErrorValue 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 e69cd5734de..43defdb622e 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl @@ -1109,7 +1109,6 @@ Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice` Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2+Tags[T1,T2] Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice1Of2(T1) Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice2Of2(T2) -Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 Item Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`3+Choice2Of3[T1,T2,T3]: T2 Item @@ -1143,7 +1142,6 @@ Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoi Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice1Of3(T1) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice2Of3(T2) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice3Of3(T3) -Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 Item Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`4+Choice2Of4[T1,T2,T3,T4]: T2 Item @@ -1184,7 +1182,6 @@ Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpC Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice2Of4(T2) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice3Of4(T3) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice4Of4(T4) -Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 Item Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`5+Choice2Of5[T1,T2,T3,T4,T5]: T2 Item @@ -1232,7 +1229,6 @@ Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSha Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice3Of5(T3) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice4Of5(T4) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice5Of5(T5) -Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 Item Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`6+Choice2Of6[T1,T2,T3,T4,T5,T6]: T2 Item @@ -1287,7 +1283,6 @@ Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.F Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice4Of6(T4) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice5Of6(T5) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice6Of6(T6) -Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 Item Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`7+Choice2Of7[T1,T2,T3,T4,T5,T6,T7]: T2 Item @@ -1349,7 +1344,6 @@ Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Cor Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice5Of7(T5) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice6Of7(T6) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice7Of7(T7) -Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: System.String ToString() Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] FromConverter(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] op_Implicit(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: System.Converter`2[T,TResult] ToConverter(Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]) @@ -1429,7 +1423,6 @@ Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Int32 get_Tag() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2+Tags[T,TError] Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewError(TError) Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewOk(T) -Microsoft.FSharp.Core.FSharpResult`2[T,TError]: System.String ToString() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T ResultValue Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T get_ResultValue() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: TError ErrorValue @@ -2677,4 +2670,4 @@ Microsoft.FSharp.Reflection.UnionCaseInfo: System.String Name Microsoft.FSharp.Reflection.UnionCaseInfo: System.String ToString() Microsoft.FSharp.Reflection.UnionCaseInfo: System.String get_Name() Microsoft.FSharp.Reflection.UnionCaseInfo: System.Type DeclaringType -Microsoft.FSharp.Reflection.UnionCaseInfo: System.Type get_DeclaringType() +Microsoft.FSharp.Reflection.UnionCaseInfo: System.Type get_DeclaringType() \ No newline at end of file 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 ef1e3ea6b82..ed913ea04d3 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl @@ -1109,7 +1109,6 @@ Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice` Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2+Tags[T1,T2] Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice1Of2(T1) Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: Microsoft.FSharp.Core.FSharpChoice`2[T1,T2] NewChoice2Of2(T2) -Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 Item Microsoft.FSharp.Core.FSharpChoice`3+Choice1Of3[T1,T2,T3]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`3+Choice2Of3[T1,T2,T3]: T2 Item @@ -1143,7 +1142,6 @@ Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoi Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice1Of3(T1) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice2Of3(T2) Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3] NewChoice3Of3(T3) -Microsoft.FSharp.Core.FSharpChoice`3[T1,T2,T3]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 Item Microsoft.FSharp.Core.FSharpChoice`4+Choice1Of4[T1,T2,T3,T4]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`4+Choice2Of4[T1,T2,T3,T4]: T2 Item @@ -1184,7 +1182,6 @@ Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpC Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice2Of4(T2) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice3Of4(T3) Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4] NewChoice4Of4(T4) -Microsoft.FSharp.Core.FSharpChoice`4[T1,T2,T3,T4]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 Item Microsoft.FSharp.Core.FSharpChoice`5+Choice1Of5[T1,T2,T3,T4,T5]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`5+Choice2Of5[T1,T2,T3,T4,T5]: T2 Item @@ -1232,7 +1229,6 @@ Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSha Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice3Of5(T3) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice4Of5(T4) Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5] NewChoice5Of5(T5) -Microsoft.FSharp.Core.FSharpChoice`5[T1,T2,T3,T4,T5]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 Item Microsoft.FSharp.Core.FSharpChoice`6+Choice1Of6[T1,T2,T3,T4,T5,T6]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`6+Choice2Of6[T1,T2,T3,T4,T5,T6]: T2 Item @@ -1287,7 +1283,6 @@ Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.F Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice4Of6(T4) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice5Of6(T5) Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6] NewChoice6Of6(T6) -Microsoft.FSharp.Core.FSharpChoice`6[T1,T2,T3,T4,T5,T6]: System.String ToString() Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 Item Microsoft.FSharp.Core.FSharpChoice`7+Choice1Of7[T1,T2,T3,T4,T5,T6,T7]: T1 get_Item() Microsoft.FSharp.Core.FSharpChoice`7+Choice2Of7[T1,T2,T3,T4,T5,T6,T7]: T2 Item @@ -1349,7 +1344,6 @@ Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Cor Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice5Of7(T5) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice6Of7(T6) Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7] NewChoice7Of7(T7) -Microsoft.FSharp.Core.FSharpChoice`7[T1,T2,T3,T4,T5,T6,T7]: System.String ToString() Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] FromConverter(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] op_Implicit(System.Converter`2[T,TResult]) Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]: System.Converter`2[T,TResult] ToConverter(Microsoft.FSharp.Core.FSharpFunc`2[T,TResult]) @@ -1429,7 +1423,6 @@ Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Int32 get_Tag() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2+Tags[T,TError] Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewError(TError) Microsoft.FSharp.Core.FSharpResult`2[T,TError]: Microsoft.FSharp.Core.FSharpResult`2[T,TError] NewOk(T) -Microsoft.FSharp.Core.FSharpResult`2[T,TError]: System.String ToString() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T ResultValue Microsoft.FSharp.Core.FSharpResult`2[T,TError]: T get_ResultValue() Microsoft.FSharp.Core.FSharpResult`2[T,TError]: TError ErrorValue diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Core/ResultTests.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Core/ResultTests.fs index d85888f5bea..e2b08e867d7 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Core/ResultTests.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Core/ResultTests.fs @@ -175,10 +175,3 @@ type ResultTests() = member this.ToValueOption() = Assert.AreEqual(Result.toValueOption (Error 42), ValueNone) Assert.AreEqual(Result.toValueOption (Ok 42), ValueSome 42) - - [] - member _.ToStringRendersCaseAndValue() = - // Reflection-free ToString, consistent with option's "Some(x)" style; null renders as "null". - Assert.AreEqual("Ok(5)", (Ok 5).ToString()) - Assert.AreEqual("Error(bad)", (Error "bad").ToString()) - Assert.AreEqual("Ok(null)", (Ok (null: string)).ToString()) From e3c1a34e64516694c9a7fae20b9284146a09ff00 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Mon, 29 Jun 2026 14:58:32 +0100 Subject: [PATCH 21/29] Remove stale FSharp.Core release note for the reverted Result/Choice ToString Co-Authored-By: Claude Opus 4.8 --- docs/release-notes/.FSharp.Core/10.0.300.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Core/10.0.300.md b/docs/release-notes/.FSharp.Core/10.0.300.md index 2d28428507b..6904303cc37 100644 --- a/docs/release-notes/.FSharp.Core/10.0.300.md +++ b/docs/release-notes/.FSharp.Core/10.0.300.md @@ -18,6 +18,5 @@ ### 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)) -* `Result` and `Choice` now have a reflection-free `ToString` consistent with `Option`'s `Some(x)` style (e.g. `Ok 0` renders as `"Ok(0)"` instead of `"Ok 0"`). ([PR #19976](https://github.com/dotnet/fsharp/pull/19976)) ### Breaking Changes From 817e9de925bbffe466c6b1056a5f9cd9a0fbefec Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Thu, 2 Jul 2026 12:25:15 +0100 Subject: [PATCH 22/29] Fix code formatting in IlxGen.fs (dotnet fantomas) Co-Authored-By: Claude Opus 4.8 --- src/Compiler/CodeGen/IlxGen.fs | 35 ++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index 77733614099..e5fe8737084 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -3889,7 +3889,11 @@ and GenAllocRecd cenv cgbuf eenv ctorInfo (tcref, argTys, args, m) sequel = and GenAllocAnonRecd cenv cgbuf eenv (anonInfo: AnonRecdTypeInfo, tyargs, args, m) sequel = let anonCtor, _anonMethods, anonType = - cgbuf.mgbuf.LookupAnonType((fun (ilThisTy, tycon) -> GenRecordToStringMethod(cenv, cgbuf.mgbuf, EnvForTycon tycon eenv, ilThisTy, mkLocalTyconRef tycon, m, "{| ", " |}")), anonInfo) + cgbuf.mgbuf.LookupAnonType( + (fun (ilThisTy, tycon) -> + GenRecordToStringMethod(cenv, cgbuf.mgbuf, EnvForTycon tycon eenv, ilThisTy, mkLocalTyconRef tycon, m, "{| ", " |}")), + anonInfo + ) let boxity = anonType.Boxity GenExprs cenv cgbuf eenv args @@ -3903,7 +3907,11 @@ and GenAllocAnonRecd cenv cgbuf eenv (anonInfo: AnonRecdTypeInfo, tyargs, args, and GenGetAnonRecdField cenv cgbuf eenv (anonInfo: AnonRecdTypeInfo, e, tyargs, n, m) sequel = let _anonCtor, anonMethods, anonType = - cgbuf.mgbuf.LookupAnonType((fun (ilThisTy, tycon) -> GenRecordToStringMethod(cenv, cgbuf.mgbuf, EnvForTycon tycon eenv, ilThisTy, mkLocalTyconRef tycon, m, "{| ", " |}")), anonInfo) + cgbuf.mgbuf.LookupAnonType( + (fun (ilThisTy, tycon) -> + GenRecordToStringMethod(cenv, cgbuf.mgbuf, EnvForTycon tycon eenv, ilThisTy, mkLocalTyconRef tycon, m, "{| ", " |}")), + anonInfo + ) let boxity = anonType.Boxity let ilTypeArgs = GenTypeArgs cenv m eenv.tyenv tyargs @@ -10957,7 +10965,11 @@ and GenImplFile cenv (mgbuf: AssemblyBuilder) mainInfoOpt eenv (implFile: Checke // Generate all the anonymous record types mentioned anywhere in this module for anonInfo in anonRecdTypes.Values do - mgbuf.GenerateAnonType((fun (ilThisTy, tycon) -> GenRecordToStringMethod(cenv, mgbuf, EnvForTycon tycon eenv, ilThisTy, mkLocalTyconRef tycon, m, "{| ", " |}")), anonInfo) + mgbuf.GenerateAnonType( + (fun (ilThisTy, tycon) -> + GenRecordToStringMethod(cenv, mgbuf, EnvForTycon tycon eenv, ilThisTy, mkLocalTyconRef tycon, m, "{| ", " |}")), + anonInfo + ) let withQName (loc: CompileLocation) = { loc with @@ -11396,7 +11408,9 @@ and GenSprintfPrintingMethod cenv eenv methName ilThisTy m = and EmitToStringMethodDef (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, thisv: Val, bodyExpr: Expr) = let g = cenv.g let eenvForMeth = AddStorageForLocalVals g [ (thisv, Arg 0) ] eenv - let ilMethodBody = CodeGenMethodForExpr cenv mgbuf ([], "ToString", eenvForMeth, 0, Some thisv, bodyExpr, Return) + + let ilMethodBody = + CodeGenMethodForExpr cenv mgbuf ([], "ToString", eenvForMeth, 0, Some thisv, bodyExpr, Return) let mdef = mkILNonGenericVirtualInstanceMethod ( @@ -11413,14 +11427,17 @@ and EmitToStringMethodDef (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, /// unions get their reflection-free ToString from the type-augmentation phase instead (so the 'string' /// operator calls are optimized), but anonymous record types are synthesized too late for that, so they are /// generated here. Under non-reflection-free codegen, falls back to sprintf "%+A". -and GenRecordToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, ilThisTy: ILType, tcref: TyconRef, m: range, openBrace: string, closeBrace: string) = +and GenRecordToStringMethod + (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, ilThisTy: ILType, tcref: TyconRef, m: range, openBrace: string, closeBrace: string) = let g = cenv.g if not g.useReflectionFreeCodeGen then GenSprintfPrintingMethod cenv eenv "ToString" ilThisTy m else - let thisv, body = AugmentTypeDefinitions.mkRecdToString (g, tcref, tcref.Deref, openBrace, closeBrace) - EmitToStringMethodDef (cenv, mgbuf, eenv, thisv, body) + let thisv, body = + AugmentTypeDefinitions.mkRecdToString (g, tcref, tcref.Deref, openBrace, closeBrace) + + EmitToStringMethodDef(cenv, mgbuf, eenv, thisv, body) and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option = let g = cenv.g @@ -12033,7 +12050,9 @@ and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option // Reflection-free nominal unions get their ToString from the type-augmentation phase; here we // only emit the sprintf "%+A" ToString for the non-reflection-free case. - | TFSharpTyconRepr { fsobjmodel_kind = TFSharpUnion } when not g.useReflectionFreeCodeGen && not (tycon.HasMember g "ToString" []) -> + | TFSharpTyconRepr { fsobjmodel_kind = TFSharpUnion } when + not g.useReflectionFreeCodeGen && not (tycon.HasMember g "ToString" []) + -> yield! GenSprintfPrintingMethod cenv eenvinner "ToString" ilThisTy m | _ -> () ] From 0616237e063fef35b07561ebd3588fd0f5e1b4c1 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Mon, 20 Jul 2026 16:27:38 +0100 Subject: [PATCH 23/29] don't use quoted name --- src/Compiler/Checking/AugmentWithHashCompare.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Compiler/Checking/AugmentWithHashCompare.fs b/src/Compiler/Checking/AugmentWithHashCompare.fs index 13797279de0..d19d08f181b 100644 --- a/src/Compiler/Checking/AugmentWithHashCompare.fs +++ b/src/Compiler/Checking/AugmentWithHashCompare.fs @@ -1736,7 +1736,7 @@ let mkRecdToString (g: TcGlobals, tcref: TyconRef, tycon: Tycon, openBrace: stri |> List.mapi (fun i fspec -> let fref = tcref.MakeNestedRecdFieldRef fspec let value = mkFieldToString (g, m, mkRecdFieldGetViaExprAddr (thise, fref, tinst, m)) - let nameEq = mkString g m (fspec.DisplayName + " = ") + let nameEq = mkString g m (fspec.DisplayNameCore + " = ") if i = 0 then [ nameEq; value ] else [ mkString g m "; "; nameEq; value ]) |> List.concat @@ -1756,7 +1756,7 @@ let mkUnionToString (g: TcGlobals, tcref: TyconRef, tycon: Tycon) = let rfields = ucase.RecdFields if isNil rfields then - mkString g m ucase.DisplayName + mkString g m ucase.DisplayNameCore else // provene is an expression proven to be of this case (the value itself for struct unions, // otherwise a 'UnionCaseProof'), from which fields can be read. @@ -1770,7 +1770,7 @@ let mkUnionToString (g: TcGlobals, tcref: TyconRef, tycon: Tycon) = let fieldsWithSeps = fieldStrs |> List.mapi (fun i fe -> if i = 0 then [ fe ] else [ sep; fe ]) |> List.concat - let parts = mkString g m (ucase.DisplayName + "(") :: fieldsWithSeps @ [ mkString g m ")" ] + let parts = mkString g m (ucase.DisplayNameCore + "(") :: fieldsWithSeps @ [ mkString g m ")" ] mkStringConcat (g, m, parts) if cref.Tycon.IsStructOrEnumTycon then From 3f710ee4d0607c83e61e41d4dd14469bebb7f136 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Mon, 20 Jul 2026 16:27:46 +0100 Subject: [PATCH 24/29] int version of reflectionfree-printing doc --- docs/reflectionfree-printing.md | 69 +++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/reflectionfree-printing.md diff --git a/docs/reflectionfree-printing.md b/docs/reflectionfree-printing.md new file mode 100644 index 00000000000..5ddd6ef4ac8 --- /dev/null +++ b/docs/reflectionfree-printing.md @@ -0,0 +1,69 @@ +# Simple vs Reflection-based DU and Record printing + +This document describes two modes for printing Discriminated Unions (DUs) and Records in F#: a **simple** reflection-free mode that delegates to a `string`-like operator for printing field values, and a `sprintf` mode (`sprintf "%+A"`), which uses **reflection** to create output looking like F# code. In this document, the terms *simple* and *reflection* are used to distinguish the two modes. + +Without the `--reflectionfree` flag, the compiler generates a `ToString` for DUs and Records that calls `sprintf "%+A"`. With the flag, the compiler generates a `ToString` that uses the simple mode. + +Users can choose between the two modes by 1. use of `--reflectionfree`, and by 2. calling with a `sprintf`-type caller or a `string`-type caller (e.g. the `string` operator, `ToString`, or interpolated strings). + +If `x` is a DU or Record, then output will be simple or reflection-based as follows: +| | `--reflectionfree` | no `--reflectionfree` | +|---|---|---| +| `string x` | simple | reflection | +| `x.ToString()` | simple | reflection | +| `$"{x}"` | simple | reflection | +| `sprintf "%A" x` | disallowed (would be reflection) | reflection | + +As such, the current default reflection `ToString` generation forces reflection formatting on all callers. On the other hand, generating simple `ToString` output means that the records and DUs are printed with simple or reflection formtting depending on whether the caller is of simple or reflection affinity. The `--reflectionfree` flag combines this property with a ban on `sprintf` to prevent the reflection mode from being used. + +In addition to user-defined types, the FSharp.Core `option` type uses simple printing, while other types either have no `ToString` or use some other format. + +## Behavior: definitions + +In simple printing, field values are printed with `string`-type formatting, more precisely `anyToStringShowingNull`. No line breaks are inserted. + +- **Record**: `{ Name1 = value1; Name2 = value2 }`. +- **Anonymous record**: the same, but with `{| ` and ` |}`. +- **Union**: A case with no fields renders as just its name. A case with fields renders as `CaseName(value1, value2)`. + +`[]` records and unions, and struct anonymous records, render identically to their reference-type forms. + +A type that supplies its own `ToString` override keeps it, with no `ToString` generated for it (either simple or reflection). + +Reflection-mode printing is described here: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/plaintext-formatting . + +## Behavioural Differences + +### Differences in field rendering + +The following differences between `string` and `sprintf "%+A"` carry over directly into differences in field rendering between simple and reflection record and DU printing: + +| F# value | simple (`string`) | reflection (`sprintf "%A"`) | +|---|---|---| +| char field | `'a'` | `a` | +| float `5.0` | `5` | `5.0` | +| `250uy` / `42n` / `1.5M` | `250` / `42` / `1.5` | `250uy` / `42n` / `1.5M` | +| option field `None` | `null` | `None` | +| option field `Some 2` | `Some(2)` | `Some 2` | +| voption field `ValueSome 2` | `2` | `ValueSome 2` | +| array field `[\|1;2;3\|]` | `System.Int32[]` | `[\|1; 2; 3\|]` | +| unit field `()` | `null` | `()` | + +The overall differences here are: +- Simple printing converts to strings, while reflection printing is more bi-directional, often generating compilable F# code. +- F# types that specifically allow nulls (`unit`, `option`, and in general types with `UseNullAsTrueValue`) are printed as `null` in simple printing, while reflection printing uses a more F#-like representation. + +### Other differences + +- `StructuredFormatDisplay` is ignored in simple printing. +- A DU case with a single field sometimes renders without brackets in reflection printing, but always has brackets in simple printing. +- Reflection printing often uses indents and line breaks in reflection printing, but has no line breaks in simple printing (unless field printing involves line breaks). + +| F# value | simple (`string`) | reflection (`sprintf "%+A"`) | +|---|---|---| +| Single simple field DU | `B 5` | `B(5)` | +| `[")>]` | `{ X = 5 }` (attribute ignored) | `Custom<5>` | + +## Recursion and depth + +Rendering recurses into nested records and unions. Deep nesting is guarded by `RuntimeHelpers.EnsureSufficientExecutionStack`, raising a catchable `InsufficientExecutionStackException` rather than `StackOverflowException`; cycles (which require mutation to construct) still overflow, as `Option` and `list` do. \ No newline at end of file From 432b137eeafd5bf5dda86a82db9cd1504ec19b48 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Mon, 20 Jul 2026 16:36:09 +0100 Subject: [PATCH 25/29] doc tweaks --- docs/reflectionfree-printing.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/reflectionfree-printing.md b/docs/reflectionfree-printing.md index 5ddd6ef4ac8..09c9fe695d6 100644 --- a/docs/reflectionfree-printing.md +++ b/docs/reflectionfree-printing.md @@ -40,12 +40,11 @@ The following differences between `string` and `sprintf "%+A"` carry over direct | F# value | simple (`string`) | reflection (`sprintf "%A"`) | |---|---|---| -| char field | `'a'` | `a` | +| string field `"hi"` | `hi` | `"hi"` | +| char field `'a'` | `a` | `'a'` | | float `5.0` | `5` | `5.0` | | `250uy` / `42n` / `1.5M` | `250` / `42` / `1.5` | `250uy` / `42n` / `1.5M` | | option field `None` | `null` | `None` | -| option field `Some 2` | `Some(2)` | `Some 2` | -| voption field `ValueSome 2` | `2` | `ValueSome 2` | | array field `[\|1;2;3\|]` | `System.Int32[]` | `[\|1; 2; 3\|]` | | unit field `()` | `null` | `()` | @@ -55,14 +54,19 @@ The overall differences here are: ### Other differences -- `StructuredFormatDisplay` is ignored in simple printing. -- A DU case with a single field sometimes renders without brackets in reflection printing, but always has brackets in simple printing. -- Reflection printing often uses indents and line breaks in reflection printing, but has no line breaks in simple printing (unless field printing involves line breaks). +These differences are in the printing of the record or DU itself rather than of its fields: | F# value | simple (`string`) | reflection (`sprintf "%+A"`) | |---|---|---| -| Single simple field DU | `B 5` | `B(5)` | -| `[")>]` | `{ X = 5 }` (attribute ignored) | `Custom<5>` | +| `B 5` (single field) | `B(5)` | `B 5` | +| `C (3, "hi")` (two fields) | `C(3, hi)` | `C (3, "hi")` | +| record `{ X = 1; Y = 2 }` | `{ X = 1; Y = 2 }` | `{ X = 1`⏎` Y = 2 }` | +| `[")>]` | `{ X = 5 }` | `Custom<5>` | + +The overall differences here are: +- Simple printing always brackets a case's fields and never pads, while reflection printing omits brackets for a single non-tuple field and inserts a space before them otherwise. +- Simple printing uses a single line (unless a field's own rendering contains breaks), while reflection printing breaks records and nested values across lines with indentation. +- `StructuredFormatDisplay` is ignored in simple printing and honoured in reflection printing. ## Recursion and depth From 1d6f4427752b1ce7b2988a7a0929963cdeef1ef1 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Mon, 20 Jul 2026 17:02:54 +0100 Subject: [PATCH 26/29] Link release note to the printing doc and cover anonymous records Co-Authored-By: Claude Opus 4.8 --- docs/reflectionfree-printing.md | 24 +++++++++---------- .../.FSharp.Compiler.Service/11.0.100.md | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/reflectionfree-printing.md b/docs/reflectionfree-printing.md index 09c9fe695d6..68e3091faf6 100644 --- a/docs/reflectionfree-printing.md +++ b/docs/reflectionfree-printing.md @@ -1,8 +1,8 @@ # Simple vs Reflection-based DU and Record printing -This document describes two modes for printing Discriminated Unions (DUs) and Records in F#: a **simple** reflection-free mode that delegates to a `string`-like operator for printing field values, and a `sprintf` mode (`sprintf "%+A"`), which uses **reflection** to create output looking like F# code. In this document, the terms *simple* and *reflection* are used to distinguish the two modes. +This document describes two modes for printing Discriminated Unions (DUs) and Records in F#: a **simple** reflection-free mode that delegates to a `string`-like operator for printing field values, and a `sprintf` mode (`sprintf "%A"`), which uses **reflection** to create output looking like F# code. In this document, the terms *simple* and *reflection* are used to distinguish the two modes. -Without the `--reflectionfree` flag, the compiler generates a `ToString` for DUs and Records that calls `sprintf "%+A"`. With the flag, the compiler generates a `ToString` that uses the simple mode. +Without the `--reflectionfree` flag, the compiler generates a `ToString` for DUs and Records that calls `sprintf "%A"`. With the flag, the compiler generates a `ToString` that uses the simple mode. Users can choose between the two modes by 1. use of `--reflectionfree`, and by 2. calling with a `sprintf`-type caller or a `string`-type caller (e.g. the `string` operator, `ToString`, or interpolated strings). @@ -14,11 +14,11 @@ If `x` is a DU or Record, then output will be simple or reflection-based as foll | `$"{x}"` | simple | reflection | | `sprintf "%A" x` | disallowed (would be reflection) | reflection | -As such, the current default reflection `ToString` generation forces reflection formatting on all callers. On the other hand, generating simple `ToString` output means that the records and DUs are printed with simple or reflection formtting depending on whether the caller is of simple or reflection affinity. The `--reflectionfree` flag combines this property with a ban on `sprintf` to prevent the reflection mode from being used. +As such, the current default reflection `ToString` generation forces reflection formatting on all callers. On the other hand, generating simple `ToString` output means that the records and DUs are printed with simple or reflection formatting depending on whether the caller is of simple or reflection affinity. The `--reflectionfree` flag combines this property with a ban on `sprintf` to prevent the reflection mode from being used. In addition to user-defined types, the FSharp.Core `option` type uses simple printing, while other types either have no `ToString` or use some other format. -## Behavior: definitions +## Behaviour: definitions In simple printing, field values are printed with `string`-type formatting, more precisely `anyToStringShowingNull`. No line breaks are inserted. @@ -30,15 +30,15 @@ In simple printing, field values are printed with `string`-type formatting, more A type that supplies its own `ToString` override keeps it, with no `ToString` generated for it (either simple or reflection). -Reflection-mode printing is described here: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/plaintext-formatting . +Reflection-mode printing is described in [plain text formatting](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/plaintext-formatting). -## Behavioural Differences +## Behavioural differences ### Differences in field rendering -The following differences between `string` and `sprintf "%+A"` carry over directly into differences in field rendering between simple and reflection record and DU printing: +The following differences between `string` and `sprintf "%A"` carry over directly into differences in field rendering between simple and reflection printing: -| F# value | simple (`string`) | reflection (`sprintf "%A"`) | +| F# value | simple (`anyToStringShowingNull`) | reflection (`sprintf "%A"`) | |---|---|---| | string field `"hi"` | `hi` | `"hi"` | | char field `'a'` | `a` | `'a'` | @@ -50,16 +50,16 @@ The following differences between `string` and `sprintf "%+A"` carry over direct The overall differences here are: - Simple printing converts to strings, while reflection printing is more bi-directional, often generating compilable F# code. -- F# types that specifically allow nulls (`unit`, `option`, and in general types with `UseNullAsTrueValue`) are printed as `null` in simple printing, while reflection printing uses a more F#-like representation. +- F# types that have null representation (`unit`, `option`, and in general types with `AllowNullLiteral` or `UseNullAsTrueValue`) are printed as `null` in simple printing, while reflection printing uses a more F#-like representation. ### Other differences These differences are in the printing of the record or DU itself rather than of its fields: -| F# value | simple (`string`) | reflection (`sprintf "%+A"`) | +| F# value | simple (`string`) | reflection (`sprintf "%A"`) | |---|---|---| | `B 5` (single field) | `B(5)` | `B 5` | -| `C (3, "hi")` (two fields) | `C(3, hi)` | `C (3, "hi")` | +| `C (3, 4)` (two fields) | `C(3, 4)` | `C (3, 4)` | | record `{ X = 1; Y = 2 }` | `{ X = 1; Y = 2 }` | `{ X = 1`⏎` Y = 2 }` | | `[")>]` | `{ X = 5 }` | `Custom<5>` | @@ -70,4 +70,4 @@ The overall differences here are: ## Recursion and depth -Rendering recurses into nested records and unions. Deep nesting is guarded by `RuntimeHelpers.EnsureSufficientExecutionStack`, raising a catchable `InsufficientExecutionStackException` rather than `StackOverflowException`; cycles (which require mutation to construct) still overflow, as `Option` and `list` do. \ No newline at end of file +Rendering recurses into nested records and unions. Deep nesting is guarded by `RuntimeHelpers.EnsureSufficientExecutionStack`, raising a catchable `InsufficientExecutionStackException` rather than `StackOverflowException`; cycles (which require mutation to construct) still overflow, as `option` and `list` do. \ No newline at end of file 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 50306ca1e84..5f7d2162044 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -118,7 +118,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)) -* Under `--reflectionfree`, discriminated unions and records now get a generated `ToString` (rendering each field like `Option` does) instead of falling back to the namespace-qualified type name. ([PR #19976](https://github.com/dotnet/fsharp/pull/19976)) +* Under `--reflectionfree`, discriminated unions, records and anonymous records now get a [generated `ToString`](../../reflectionfree-printing.md) (rendering each field like `Option` does) instead of falling back to the namespace-qualified type name. ([PR #19976](https://github.com/dotnet/fsharp/pull/19976)) * Checker: recover on checking language version ([PR ##19970](https://github.com/dotnet/fsharp/pull/19970)) * Implied argument names for function-to-delegate coercions now fall back to the delegate's `Invoke` parameter names when the function has no recoverable names (e.g. a partial application like `System.Func((+) 1)`), instead of synthetic `delegateArg0`, `delegateArg1`, … names. ([PR #20001](https://github.com/dotnet/fsharp/pull/20001)) From 4cae86aa026ff43a2ab0b9752f2dce2cdcb7b536 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Mon, 20 Jul 2026 17:33:17 +0100 Subject: [PATCH 27/29] test backticks --- .../CompilerOptions/fsc/reflectionfree.fs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs index e7171a3ad65..6f367ad9086 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/reflectionfree.fs @@ -188,6 +188,26 @@ let main _ = |> withStdOutContains "WithNames(1, a)" |> withStdOutContains "WithoutNames(1, a)" +[] +let ``Backtick-quoted names render without their backticks`` () = + FSharp """ +module Test +type Quoted = | ``My Case`` of int +type QuotedField = { ``My Field``: int } + +[] +let main _ = + ``My Case`` 5 |> string |> System.Console.WriteLine + { ``My Field`` = 5 } |> string |> System.Console.WriteLine + 0 + """ + |> asExe + |> withOptions [ "--reflectionfree" ] + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "My Case(5)" + |> withStdOutContains "{ My Field = 5 }" + [] let ``Struct unions and struct records get a generated ToString`` () = FSharp """ @@ -268,6 +288,28 @@ let main _ = |> shouldSucceed |> withStdOutContains "caught" +[] +let ``Deeply nested anonymous records fail the generated ToString with a catchable exception, not a hard overflow`` () = + FSharp """ +module Test + +[] +let main _ = + let mutable o: obj = box 0 + for _ in 1 .. 1_000_000 do o <- box {| Next = o |} + try + o.ToString() |> ignore + System.Console.WriteLine "rendered" + with :? System.InsufficientExecutionStackException -> + System.Console.WriteLine "caught" + 0 + """ + |> asExe + |> withOptions [ "--reflectionfree" ] + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "caught" + [] let ``No debug display attribute`` () = someCode From 46da7a28a020bfe57aa9bf62ce3d6793e6d5a3a3 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Mon, 20 Jul 2026 17:47:48 +0100 Subject: [PATCH 28/29] Share the ToString recursion guard with anonymous records The guard lived in MakeBindingsForToStringAugmentation, which anonymous records bypass: they are synthesized too late for type augmentation and reach mkRecdToString from IlxGen instead. Deep nesting overflowed the stack rather than raising InsufficientExecutionStackException. Move it into mkToStringRecursionGuard, applied inside mkRecdToString and mkUnionToString, so every caller of the body builders gets it. Co-Authored-By: Claude Opus 4.8 --- .../Checking/AugmentWithHashCompare.fs | 63 +++++++++---------- .../Checking/AugmentWithHashCompare.fsi | 2 +- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/Compiler/Checking/AugmentWithHashCompare.fs b/src/Compiler/Checking/AugmentWithHashCompare.fs index d19d08f181b..da3e50a346a 100644 --- a/src/Compiler/Checking/AugmentWithHashCompare.fs +++ b/src/Compiler/Checking/AugmentWithHashCompare.fs @@ -1712,6 +1712,30 @@ let MakeBindingsForUnionAugmentation g (tycon: Tycon) (vals: ValRef list) = // specialised - e.g. an int field renders via a direct, allocation-free ToString rather than a boxed call). //------------------------------------------------------------------------- +// Guard deep recursion with a catchable exception, as C# records' PrintMembers do, when the runtime provides +// it. A type whose fields are all primitive cannot nest, so it skips the guard. +let mkToStringRecursionGuard (g: TcGlobals, m: Text.range, fieldTys: TType list, body: Expr) = + let isPrimitive (ty: TType) = + isIntegerTy g ty + || isFpTy g ty + || isDecimalTy g ty + || isStringTy g ty + || typeEquiv g g.char_ty ty + || isBoolTy g ty + || isUnitTy g ty + || isEnumTy g ty + + if fieldTys |> List.forall isPrimitive then + body + else + match g.TryFindSysILTypeRef "System.Runtime.CompilerServices.RuntimeHelpers" with + | Some tref -> + let mspec = + mkILNonGenericStaticMethSpecInTy (mkILNonGenericBoxedTy tref, "EnsureSufficientExecutionStack", [], ILType.Void) + + mkSequential m (mkAsmExpr ([ mkNormalCall mspec ], [], [], [], m)) body + | None -> body + // Render one field value as a string the way option/list do (LanguagePrimitives.anyToStringShowingNull): // a null reference renders as "null", everything else via the 'string' operator. A value-type field can // never be null, so it skips the box+null-guard and renders directly. @@ -1741,7 +1765,8 @@ let mkRecdToString (g: TcGlobals, tcref: TyconRef, tycon: Tycon, openBrace: stri |> List.concat let parts = mkString g m openBrace :: fieldParts @ [ mkString g m closeBrace ] - thisv, mkStringConcat (g, m, parts) + let fieldTys = tcref.AllInstanceFieldsAsList |> List.map (fun fspec -> fspec.FormalType) + thisv, mkToStringRecursionGuard (g, m, fieldTys, mkStringConcat (g, m, parts)) // A union's ToString as a match over the cases building "CaseName(f0, f1, ...)" (or just "CaseName" for a // nullary case). @@ -1786,7 +1811,11 @@ let mkUnionToString (g: TcGlobals, tcref: TyconRef, tycon: Tycon) = mkCase (DecisionTreeTest.UnionCase(cref, tinst), mbuilder.AddResultTarget(mkResult ucase))) let dtree = TDSwitch(thise, cases, None, m) - thisv, mbuilder.Close(dtree, m, g.string_ty) + + let fieldTys = + tcref.UnionCasesAsList |> List.collect (fun uc -> uc.RecdFields) |> List.map (fun rf -> rf.FormalType) + + thisv, mkToStringRecursionGuard (g, m, fieldTys, mbuilder.Close(dtree, m, g.string_ty)) let TyconIsCandidateForAugmentationWithToString (g: TcGlobals, tycon: Tycon) = g.useReflectionFreeCodeGen && (tycon.IsUnionTycon || tycon.IsRecordTycon) @@ -1808,36 +1837,6 @@ let MakeBindingsForToStringAugmentation (g: TcGlobals, tycon: Tycon, toStringVal else mkRecdToString (g, tcref, tycon, "{ ", " }") - let mightRecurse = - let isPrimitive (ty: TType) = - isIntegerTy g ty - || isFpTy g ty - || isDecimalTy g ty - || isStringTy g ty - || typeEquiv g g.char_ty ty - || isBoolTy g ty - || isUnitTy g ty - || isEnumTy g ty - - let fieldTys = - if tycon.IsUnionTycon then - tycon.UnionCasesAsList |> List.collect (fun uc -> uc.RecdFields) |> List.map (fun rf -> rf.FormalType) - else - tycon.AllInstanceFieldsAsList |> List.map (fun rf -> rf.FormalType) - - fieldTys |> List.exists (isPrimitive >> not) - - // Guard deep recursion with a catchable exception, as C# records' PrintMembers do, when the runtime provides it. - let body = - if mightRecurse then - match g.TryFindSysILTypeRef "System.Runtime.CompilerServices.RuntimeHelpers" with - | Some tref -> - let mspec = mkILNonGenericStaticMethSpecInTy (mkILNonGenericBoxedTy tref, "EnsureSufficientExecutionStack", [], ILType.Void) - mkSequential m (mkAsmExpr ([ mkNormalCall mspec ], [], [], [], m)) body - | None -> body - else - body - let unitv, _ = mkCompGenLocal m "unitArg" g.unit_ty let expr = mkLambdas g m tps [ thisv; unitv ] (body, g.string_ty) [ mkCompGenBind toStringVal expr ] diff --git a/src/Compiler/Checking/AugmentWithHashCompare.fsi b/src/Compiler/Checking/AugmentWithHashCompare.fsi index 13899af4d36..424026f1330 100644 --- a/src/Compiler/Checking/AugmentWithHashCompare.fsi +++ b/src/Compiler/Checking/AugmentWithHashCompare.fsi @@ -52,7 +52,7 @@ val MakeValsForUnionAugmentation: TcGlobals -> TyconRef -> Val list val MakeBindingsForUnionAugmentation: TcGlobals -> Tycon -> ValRef list -> Binding list -/// Build a record's single-line reflection-free ToString body; returns the 'this' value and the body expression. +/// Build a record's single-line reflection-free ToString body, recursion guard included; returns the 'this' value and the body expression. val mkRecdToString: g: TcGlobals * tcref: TyconRef * tycon: Tycon * openBrace: string * closeBrace: string -> Val * Expr /// Whether a reflection-free structural ToString should be generated for this type. From 275b1887f2127b2e5aea6012e8c04c75f7fcf79b Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Mon, 20 Jul 2026 17:50:28 +0100 Subject: [PATCH 29/29] Add EmittedIL baselines for struct and anonymous record ToString Struct records and unions read fields off the this pointer and switch on the tag, and the anonymous record path is generated separately in IlxGen, so each gets its own baseline. The anonymous baseline omits the field reads: they name the anonymous type, whose mangled name is not stable across compilations. Co-Authored-By: Claude Opus 4.8 --- .../EmittedIL/ReflectionFreeToString.fs | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/ReflectionFreeToString.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/ReflectionFreeToString.fs index c14608f9989..e85db29e560 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/ReflectionFreeToString.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/ReflectionFreeToString.fs @@ -115,4 +115,170 @@ IL_0033: call string [runtime]System.String::Concat(string, string, string) IL_0038: ret +}"""] + + [] + let ``Struct record ToString reads its fields directly off the this pointer`` () = + FSharp """ +module ReflectionFreeToString +[] type SPoint = { SX: int; SY: int } + """ + |> withOptions [ "--reflectionfree" ] + |> compile + |> shouldSucceed + |> verifyIL [""" +.method public hidebysig virtual final instance string ToString() cil managed +{ +.custom instance void [runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) + +.maxstack 8 +.locals init (int32 V_0) +IL_0000: ldc.i4.7 +IL_0001: newarr [runtime]System.String +IL_0006: dup +IL_0007: ldc.i4.0 +IL_0008: ldstr "{ " +IL_000d: stelem [runtime]System.String +IL_0012: dup +IL_0013: ldc.i4.1 +IL_0014: ldstr "SX = " +IL_0019: stelem [runtime]System.String +IL_001e: dup +IL_001f: ldc.i4.2 +IL_0020: ldarg.0 +IL_0021: ldfld int32 ReflectionFreeToString/SPoint::SX@ +IL_0026: stloc.0 +IL_0027: ldloca.s V_0 +IL_0029: ldnull +IL_002a: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture() +IL_002f: call instance string [netstandard]System.Int32::ToString(string, +class [netstandard]System.IFormatProvider) +IL_0034: stelem [runtime]System.String +IL_0039: dup +IL_003a: ldc.i4.3 +IL_003b: ldstr "; " +IL_0040: stelem [runtime]System.String +IL_0045: dup +IL_0046: ldc.i4.4 +IL_0047: ldstr "SY = " +IL_004c: stelem [runtime]System.String +IL_0051: dup +IL_0052: ldc.i4.5 +IL_0053: ldarg.0 +IL_0054: ldfld int32 ReflectionFreeToString/SPoint::SY@ +IL_0059: stloc.0 +IL_005a: ldloca.s V_0 +IL_005c: ldnull +IL_005d: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture() +IL_0062: call instance string [netstandard]System.Int32::ToString(string, +class [netstandard]System.IFormatProvider) +IL_0067: stelem [runtime]System.String +IL_006c: dup +IL_006d: ldc.i4.6 +IL_006e: ldstr " }" +IL_0073: stelem [runtime]System.String +IL_0078: call string [runtime]System.String::Concat(string[]) +IL_007d: ret +}"""] + + [] + let ``Struct union ToString switches on the tag rather than the case type`` () = + FSharp """ +module ReflectionFreeToString +[] type SColor = | SRed | SCustom of item: int + """ + |> withOptions [ "--reflectionfree" ] + |> compile + |> shouldSucceed + |> verifyIL [""" +.method public hidebysig virtual final instance string ToString() cil managed +{ +.custom instance void [runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) + +.maxstack 6 +.locals init (int32 V_0) +IL_0000: ldarg.0 +IL_0001: call instance int32 ReflectionFreeToString/SColor::get_Tag() +IL_0006: ldc.i4.0 +IL_0007: bne.un.s IL_000f + +IL_0009: ldstr "SRed" +IL_000e: ret + +IL_000f: ldstr "SCustom(" +IL_0014: ldarg.0 +IL_0015: ldfld int32 ReflectionFreeToString/SColor::_item +IL_001a: stloc.0 +IL_001b: ldloca.s V_0 +IL_001d: ldnull +IL_001e: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture() +IL_0023: call instance string [netstandard]System.Int32::ToString(string, +class [netstandard]System.IFormatProvider) +IL_0028: ldstr ")" +IL_002d: call string [runtime]System.String::Concat(string, +string, +string) +IL_0032: ret +}"""] + + // An anonymous record's fields are type parameters, so each renders through the generic box+null guard and + // the recursion guard is always emitted. The field reads are left out of the baseline: they name the + // anonymous type, whose mangled name is not stable. + [] + let ``Anonymous record ToString is generated with a recursion guard`` () = + FSharp """ +module ReflectionFreeToString +let anon (o: obj) = {| A = 1; N = o |} + """ + |> withOptions [ "--reflectionfree" ] + |> compile + |> shouldSucceed + |> verifyIL [""" +.method public strict virtual instance string ToString() cil managed +{ +.custom instance void [runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) + +.maxstack 6 +.locals init (!'j__TPar' V_0, +!'j__TPar' V_1) +IL_0000: call void [runtime]System.Runtime.CompilerServices.RuntimeHelpers::EnsureSufficientExecutionStack() +IL_0005: ldc.i4.7 +IL_0006: newarr [runtime]System.String +IL_000b: dup +IL_000c: ldc.i4.0 +IL_000d: ldstr "{| " +IL_0012: stelem [runtime]System.String +IL_0017: dup +IL_0018: ldc.i4.1 +IL_0019: ldstr "A = " +IL_001e: stelem [runtime]System.String +IL_0023: dup +IL_0024: ldc.i4.2 +IL_0025: ldarg.0""" + """ +IL_002d: call object [FSharp.Core]Microsoft.FSharp.Core.Operators::Boxj__TPar'>(!!0) +IL_0032: brfalse.s IL_003c + +IL_0034: ldloc.0 +IL_0035: call string [FSharp.Core]Microsoft.FSharp.Core.Operators::ToStringj__TPar'>(!!0) +IL_003a: br.s IL_0041 + +IL_003c: ldstr "null" +IL_0041: stelem [runtime]System.String +IL_0046: dup +IL_0047: ldc.i4.3 +IL_0048: ldstr "; " +IL_004d: stelem [runtime]System.String +IL_0052: dup +IL_0053: ldc.i4.4 +IL_0054: ldstr "N = " +IL_0059: stelem [runtime]System.String +IL_005e: dup +IL_005f: ldc.i4.5 +IL_0060: ldarg.0""" + """ +IL_0083: ldstr " |}" +IL_0088: stelem [runtime]System.String +IL_008d: call string [runtime]System.String::Concat(string[]) +IL_0092: ret }"""]