From 4e8c86c406b6fdb46c104d3bec1a0342b80ce0a2 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Sun, 21 Jun 2026 20:06:45 +0100 Subject: [PATCH 01/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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 }"""]