Skip to content

Commit 6a2199c

Browse files
charlesroddieclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 2c108ed commit 6a2199c

5 files changed

Lines changed: 201 additions & 149 deletions

File tree

src/Compiler/Checking/AugmentWithHashCompare.fs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ let mkGetHashCodeSlotSig (g: TcGlobals) =
8181
let mkEqualsSlotSig (g: TcGlobals) =
8282
TSlotSig("Equals", g.obj_ty_noNulls, [], [], [ [ TSlotParam(Some("obj"), g.obj_ty_withNulls, false, false, false, []) ] ], Some g.bool_ty)
8383

84+
let mkToStringSlotSig (g: TcGlobals) =
85+
TSlotSig("ToString", g.obj_ty_noNulls, [], [], [ [] ], Some g.string_ty)
86+
8487
//-------------------------------------------------------------------------
8588
// Helpers associated with code-generation of comparison/hash augmentations
8689
//-------------------------------------------------------------------------
@@ -112,6 +115,9 @@ let mkEqualsWithComparerTyExact g ty =
112115
let mkHashTy g ty =
113116
mkFunTy g (mkThisTy g ty) (mkFunTy g g.unit_ty g.int_ty)
114117

118+
let mkToStringTy (g: TcGlobals, ty: TType) =
119+
mkFunTy g (mkThisTy g ty) (mkFunTy g g.unit_ty g.string_ty)
120+
115121
let mkHashWithComparerTy g ty =
116122
mkFunTy g (mkThisTy g ty) (mkFunTy g g.IEqualityComparer_ty g.int_ty)
117123

@@ -1700,3 +1706,111 @@ let MakeBindingsForUnionAugmentation g (tycon: Tycon) (vals: ValRef list) =
17001706
let isdata = mkUnionCaseTest g (thise, ucr, tinst, m)
17011707
let expr = mkLambdas g m tps [ thisv; unitv ] (isdata, g.bool_ty)
17021708
mkCompGenBind v.Deref expr)
1709+
1710+
//-------------------------------------------------------------------------
1711+
// Build reflection-free ToString functions for union and record types.
1712+
//
1713+
// Under --reflectionfree the reflective 'sprintf "%+A"' ToString is unavailable, so we build a structural
1714+
// one here (during type augmentation, so the 'string' operator calls flow through the optimizer and get
1715+
// specialised - e.g. an int field renders via a direct, allocation-free ToString rather than a boxed call).
1716+
//-------------------------------------------------------------------------
1717+
1718+
// Render one field value as a string the way option/list do (LanguagePrimitives.anyToStringShowingNull):
1719+
// a null reference renders as "null", everything else via the 'string' operator. A value-type field can
1720+
// never be null, so it skips the box+null-guard and renders directly.
1721+
let mkFieldToString (g: TcGlobals, m: Text.range, fe: Expr) =
1722+
let fieldTy = tyOfExpr g fe
1723+
1724+
if isStructTy g fieldTy then
1725+
mkCallStringOperator g m fieldTy fe
1726+
else
1727+
let v, ve = mkCompGenLocal m "field" fieldTy
1728+
mkCompGenLet m v fe (mkNonNullCond g m g.string_ty (mkCallBox g m fieldTy ve) (mkCallStringOperator g m fieldTy ve) (mkString g m "null"))
1729+
1730+
// A record's ToString as a single line "{ F1 = v1; F2 = v2 }" (no line breaks, unlike "%+A").
1731+
// openBrace/closeBrace are "{ "/" }" for records and "{| "/" |}" for anonymous records.
1732+
let mkRecdToString (g: TcGlobals, tcref: TyconRef, tycon: Tycon, openBrace: string, closeBrace: string) =
1733+
let m = tycon.Range
1734+
let tinst, ty = mkMinimalTy g tcref
1735+
let thisv, thise = mkThisVar g m ty
1736+
1737+
let fieldParts =
1738+
tcref.AllInstanceFieldsAsList
1739+
|> List.mapi (fun i fspec ->
1740+
let fref = tcref.MakeNestedRecdFieldRef fspec
1741+
let value = mkFieldToString (g, m, mkRecdFieldGetViaExprAddr (thise, fref, tinst, m))
1742+
let nameEq = mkString g m (fspec.DisplayName + " = ")
1743+
if i = 0 then [ nameEq; value ] else [ mkString g m "; "; nameEq; value ])
1744+
|> List.concat
1745+
1746+
let parts = mkString g m openBrace :: fieldParts @ [ mkString g m closeBrace ]
1747+
thisv, mkStringConcat (g, m, parts)
1748+
1749+
// A union's ToString as a match over the cases building "CaseName(f0, f1, ...)" (or just "CaseName" for a
1750+
// nullary case).
1751+
let mkUnionToString (g: TcGlobals, tcref: TyconRef, tycon: Tycon) =
1752+
let m = tycon.Range
1753+
let tinst, ty = mkMinimalTy g tcref
1754+
let thisv, thise = mkThisVar g m ty
1755+
let mbuilder = MatchBuilder(DebugPointAtBinding.NoneAtInvisible, m)
1756+
1757+
let mkResult (ucase: UnionCase) =
1758+
let cref = tcref.MakeNestedUnionCaseRef ucase
1759+
let rfields = ucase.RecdFields
1760+
1761+
if isNil rfields then
1762+
mkString g m ucase.DisplayName
1763+
else
1764+
// provene is an expression proven to be of this case (the value itself for struct unions,
1765+
// otherwise a 'UnionCaseProof'), from which fields can be read.
1766+
let mkBody (provene: Expr) =
1767+
let fieldStrs =
1768+
rfields
1769+
|> List.mapi (fun j _ -> mkFieldToString (g, m, mkUnionCaseFieldGetProvenViaExprAddr (provene, cref, tinst, j, m)))
1770+
1771+
let sep = mkString g m ", "
1772+
1773+
let fieldsWithSeps =
1774+
fieldStrs |> List.mapi (fun i fe -> if i = 0 then [ fe ] else [ sep; fe ]) |> List.concat
1775+
1776+
let parts = mkString g m (ucase.DisplayName + "(") :: fieldsWithSeps @ [ mkString g m ")" ]
1777+
mkStringConcat (g, m, parts)
1778+
1779+
if cref.Tycon.IsStructOrEnumTycon then
1780+
mkBody thise
1781+
else
1782+
let ucv, ucve = mkCompGenLocal m "thisCast" (mkProvenUnionCaseTy cref tinst)
1783+
mkCompGenLet m ucv (mkUnionCaseProof (thise, cref, tinst, m)) (mkBody ucve)
1784+
1785+
let cases =
1786+
tcref.UnionCasesAsList
1787+
|> List.map (fun ucase ->
1788+
let cref = tcref.MakeNestedUnionCaseRef ucase
1789+
mkCase (DecisionTreeTest.UnionCase(cref, tinst), mbuilder.AddResultTarget(mkResult ucase)))
1790+
1791+
let dtree = TDSwitch(thise, cases, None, m)
1792+
thisv, mbuilder.Close(dtree, m, g.string_ty)
1793+
1794+
let TyconIsCandidateForAugmentationWithToString (g: TcGlobals, tycon: Tycon) =
1795+
g.useReflectionFreeCodeGen && (tycon.IsUnionTycon || tycon.IsRecordTycon)
1796+
1797+
let MakeValsForToStringAugmentation (g: TcGlobals, tcref: TyconRef) =
1798+
let _, ty = mkMinimalTy g tcref
1799+
let vis = tcref.Accessibility
1800+
let tps = tcref.Typars tcref.Range
1801+
mkValSpec g tcref ty vis (Some(mkToStringSlotSig g)) "ToString" (tps +-> (mkToStringTy (g, ty))) unitArg false
1802+
1803+
let MakeBindingsForToStringAugmentation (g: TcGlobals, tycon: Tycon, toStringVal: Val) =
1804+
let tcref = mkLocalTyconRef tycon
1805+
let m = tycon.Range
1806+
let tps = tycon.Typars m
1807+
1808+
let thisv, body =
1809+
if tycon.IsUnionTycon then
1810+
mkUnionToString (g, tcref, tycon)
1811+
else
1812+
mkRecdToString (g, tcref, tycon, "{ ", " }")
1813+
1814+
let unitv, _ = mkCompGenLocal m "unitArg" g.unit_ty
1815+
let expr = mkLambdas g m tps [ thisv; unitv ] (body, g.string_ty)
1816+
[ mkCompGenBind toStringVal expr ]

src/Compiler/Checking/AugmentWithHashCompare.fsi

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,15 @@ val TypeDefinitelyHasEquality: TcGlobals -> TType -> bool
5151
val MakeValsForUnionAugmentation: TcGlobals -> TyconRef -> Val list
5252

5353
val MakeBindingsForUnionAugmentation: TcGlobals -> Tycon -> ValRef list -> Binding list
54+
55+
/// Build a record's single-line reflection-free ToString body; returns the 'this' value and the body expression.
56+
val mkRecdToString: g: TcGlobals * tcref: TyconRef * tycon: Tycon * openBrace: string * closeBrace: string -> Val * Expr
57+
58+
/// Whether a reflection-free structural ToString should be generated for this type.
59+
val TyconIsCandidateForAugmentationWithToString: g: TcGlobals * tycon: Tycon -> bool
60+
61+
/// Make the ToString override slot for a reflection-free record or union.
62+
val MakeValsForToStringAugmentation: g: TcGlobals * tcref: TyconRef -> Val
63+
64+
/// Build the body binding for a reflection-free record or union ToString override.
65+
val MakeBindingsForToStringAugmentation: g: TcGlobals * tycon: Tycon * toStringVal: Val -> Binding list

src/Compiler/Checking/CheckDeclarations.fs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,18 @@ module AddAugmentationDeclarations =
910910
else []
911911
else []
912912

913+
// Under --reflectionfree the structural ToString is generated here (rather than in IlxGen) so the 'string'
914+
// operator calls in its body flow through the optimizer and get specialised. Like the Equals override, this
915+
// runs late so tycon.HasMember gives correct results for a user-written ToString.
916+
let AddReflectionFreeToStringBindings (cenv: cenv, env: TcEnv, tycon: Tycon) =
917+
let g = cenv.g
918+
if AugmentTypeDefinitions.TyconIsCandidateForAugmentationWithToString(g, tycon) && not (tycon.HasMember g "ToString" []) then
919+
let tcref = mkLocalTyconRef tycon
920+
let toStringVal = AugmentTypeDefinitions.MakeValsForToStringAugmentation(g, tcref)
921+
PublishValueDefn cenv env ModuleOrMemberBinding toStringVal
922+
AugmentTypeDefinitions.MakeBindingsForToStringAugmentation(g, tycon, toStringVal)
923+
else []
924+
913925
let ShouldAugmentUnion (g: TcGlobals) (tycon: Tycon) =
914926
g.langVersion.SupportsFeature LanguageFeature.UnionIsPropertiesVisible &&
915927
HasDefaultAugmentationAttribute g (mkLocalTyconRef tycon) &&
@@ -4728,8 +4740,9 @@ module TcDeclarations =
47284740
// We put the hash/compare bindings before the type definitions and the
47294741
// equality bindings after because tha is the order they've always been generated
47304742
// in, and there are code generation tests to check that.
4731-
let binds = AddAugmentationDeclarations.AddGenericHashAndComparisonBindings cenv tycon
4743+
let binds = AddAugmentationDeclarations.AddGenericHashAndComparisonBindings cenv tycon
47324744
let binds3 = AddAugmentationDeclarations.AddGenericEqualityBindings cenv envForDecls tycon
4745+
let binds5 = AddAugmentationDeclarations.AddReflectionFreeToStringBindings(cenv, envForDecls, tycon)
47334746
let binds4 =
47344747
if tycon.IsUnionTycon && AddAugmentationDeclarations.ShouldAugmentUnion g tycon then
47354748
let unionVals =
@@ -4739,7 +4752,7 @@ module TcDeclarations =
47394752
AugmentTypeDefinitions.MakeBindingsForUnionAugmentation g tycon (List.map mkLocalValRef unionVals)
47404753
else
47414754
[]
4742-
binds@binds4, binds3)
4755+
binds@binds4, binds3@binds5)
47434756

47444757
// 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
47454758
EstablishTypeDefinitionCores.TcTyconDefnCore_CheckForCyclicStructsAndInheritance cenv tycons

src/Compiler/CodeGen/IlxGen.fs

Lines changed: 14 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -11279,17 +11279,6 @@ and GenSprintfPrintingMethod cenv eenv methName ilThisTy m =
1127911279
| _ -> ()
1128011280
]
1128111281

11282-
/// Generate the 'ToString' method for a union type. Normally this calls 'sprintf "%+A"' (see
11283-
/// GenSprintfPrintingMethod). Under reflection-free code generation 'sprintf' is unavailable, so instead emit a
11284-
/// match over the cases that builds "CaseName(f0, f1, ...)" using the 'string' operator on each field.
11285-
/// Format one field value the same way option/list do (LanguagePrimitives.anyToStringShowingNull):
11286-
/// render null as "null", otherwise via the 'string' operator.
11287-
and GenFieldToString (cenv: cenv, m: range, fe: Expr) =
11288-
let g = cenv.g
11289-
let fieldTy = tyOfExpr g fe
11290-
let v, ve = mkCompGenLocal m "field" fieldTy
11291-
mkCompGenLet m v fe (mkNonNullCond g m g.string_ty (mkCallBox g m fieldTy ve) (mkCallStringOperator g m fieldTy ve) (mkString g m "null"))
11292-
1129311282
/// Emit a [<CompilerGenerated>] virtual ToString override whose body is the given string-typed expression.
1129411283
/// 'thisv' is the 'this' value (stored at arg 0) referenced by bodyExpr.
1129511284
and EmitToStringMethodDef (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, thisv: Val, bodyExpr: Expr) =
@@ -11308,84 +11297,18 @@ and EmitToStringMethodDef (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv,
1130811297

1130911298
[ mdef.With(customAttrs = mkILCustomAttrs [ g.CompilerGeneratedAttribute ]) ]
1131011299

11311-
/// Build the 'this' local for a generated ToString (a byref for struct types) and the type instantiation.
11312-
and GenToStringThis (cenv: cenv, tcref: TyconRef, m: range) =
11313-
let g = cenv.g
11314-
let tinst, ty = generalizeTyconRef g tcref
11315-
let thisv, thise = mkCompGenLocal m "this" (if isStructTy g ty then mkByrefTy g ty else ty)
11316-
tinst, thisv, thise
11317-
11318-
and GenUnionToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, ilThisTy: ILType, tcref: TyconRef, m: range) =
11319-
let g = cenv.g
11320-
11321-
if not g.useReflectionFreeCodeGen then
11322-
GenSprintfPrintingMethod cenv eenv "ToString" ilThisTy m
11323-
else
11324-
let tinst, thisv, thise = GenToStringThis (cenv, tcref, m)
11325-
11326-
let mbuilder = MatchBuilder(DebugPointAtBinding.NoneAtInvisible, m)
11327-
11328-
let mkResult (ucase: UnionCase) =
11329-
let cref = tcref.MakeNestedUnionCaseRef ucase
11330-
let rfields = ucase.RecdFields
11331-
11332-
if isNil rfields then
11333-
mkString g m ucase.DisplayName
11334-
else
11335-
// provene is an expression proven to be of this case (the value itself for struct unions,
11336-
// otherwise a 'UnionCaseProof'), from which fields can be read.
11337-
let mkBody (provene: Expr) =
11338-
let fieldStrs =
11339-
rfields
11340-
|> List.mapi (fun j _ -> GenFieldToString (cenv, m, mkUnionCaseFieldGetProvenViaExprAddr (provene, cref, tinst, j, m)))
11341-
11342-
let sep = mkString g m ", "
11343-
11344-
let fieldsWithSeps =
11345-
fieldStrs |> List.mapi (fun i fe -> if i = 0 then [ fe ] else [ sep; fe ]) |> List.concat
11346-
11347-
let parts = mkString g m (ucase.DisplayName + "(") :: fieldsWithSeps @ [ mkString g m ")" ]
11348-
mkStringConcat (g, m, parts)
11349-
11350-
if cref.Tycon.IsStructOrEnumTycon then
11351-
mkBody thise
11352-
else
11353-
let ucv, ucve = mkCompGenLocal m "thisCast" (mkProvenUnionCaseTy cref tinst)
11354-
mkCompGenLet m ucv (mkUnionCaseProof (thise, cref, tinst, m)) (mkBody ucve)
11355-
11356-
let cases =
11357-
tcref.UnionCasesAsList
11358-
|> List.map (fun ucase ->
11359-
let cref = tcref.MakeNestedUnionCaseRef ucase
11360-
mkCase (DecisionTreeTest.UnionCase(cref, tinst), mbuilder.AddResultTarget(mkResult ucase)))
11361-
11362-
let dtree = TDSwitch(thise, cases, None, m)
11363-
let matchExpr = mbuilder.Close(dtree, m, g.string_ty)
11364-
11365-
EmitToStringMethodDef (cenv, mgbuf, eenv, thisv, matchExpr)
11366-
11367-
/// Generate a record's ToString as a single line "{ F1 = v1; F2 = v2 }" (no line breaks, unlike "%+A"),
11368-
/// fields formatted like union fields. openBrace/closeBrace are "{ "/" }" for records and "{| "/" |}" for
11369-
/// anonymous records. Under non-reflection-free codegen, falls back to sprintf "%+A".
11300+
/// Generate an anonymous record's ToString as a single line "{| F1 = v1; F2 = v2 |}". Nominal records and
11301+
/// unions get their reflection-free ToString from the type-augmentation phase instead (so the 'string'
11302+
/// operator calls are optimized), but anonymous record types are synthesized too late for that, so they are
11303+
/// generated here. Under non-reflection-free codegen, falls back to sprintf "%+A".
1137011304
and GenRecordToStringMethod (cenv: cenv, mgbuf: AssemblyBuilder, eenv: IlxGenEnv, ilThisTy: ILType, tcref: TyconRef, m: range, openBrace: string, closeBrace: string) =
1137111305
let g = cenv.g
1137211306

1137311307
if not g.useReflectionFreeCodeGen then
1137411308
GenSprintfPrintingMethod cenv eenv "ToString" ilThisTy m
1137511309
else
11376-
let tinst, thisv, thise = GenToStringThis (cenv, tcref, m)
11377-
11378-
let fieldParts =
11379-
tcref.AllInstanceFieldsAsList
11380-
|> List.mapi (fun i fspec ->
11381-
let fref = tcref.MakeNestedRecdFieldRef fspec
11382-
let value = GenFieldToString (cenv, m, mkRecdFieldGetViaExprAddr (thise, fref, tinst, m))
11383-
let nameEq = mkString g m (fspec.DisplayName + " = ")
11384-
if i = 0 then [ nameEq; value ] else [ mkString g m "; "; nameEq; value ])
11385-
|> List.concat
11386-
11387-
let parts = mkString g m openBrace :: fieldParts @ [ mkString g m closeBrace ]
11388-
EmitToStringMethodDef (cenv, mgbuf, eenv, thisv, mkStringConcat (g, m, parts))
11310+
let thisv, body = AugmentTypeDefinitions.mkRecdToString (g, tcref, tcref.Deref, openBrace, closeBrace)
11311+
EmitToStringMethodDef (cenv, mgbuf, eenv, thisv, body)
1138911312

1139011313
and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option =
1139111314
let g = cenv.g
@@ -11970,8 +11893,10 @@ and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option
1197011893
then
1197111894
yield mkILSimpleStorageCtor (Some g.ilg.typ_Object.TypeSpec, ilThisTy, [], [], reprAccess, None, eenv.imports)
1197211895

11973-
if not (tycon.HasMember g "ToString" []) then
11974-
yield! GenRecordToStringMethod(cenv, mgbuf, eenvinner, ilThisTy, tcref, m, "{ ", " }")
11896+
// Reflection-free nominal records get their ToString from the type-augmentation phase; here we
11897+
// only emit the sprintf "%+A" ToString for the non-reflection-free case.
11898+
if not g.useReflectionFreeCodeGen && not (tycon.HasMember g "ToString" []) then
11899+
yield! GenSprintfPrintingMethod cenv eenvinner "ToString" ilThisTy m
1197511900

1197611901
| TFSharpTyconRepr r when tycon.IsFSharpDelegateTycon ->
1197711902

@@ -11994,8 +11919,10 @@ and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option
1199411919
yield! mkILDelegateMethods reprAccess g.ilg (g.iltyp_AsyncCallback, g.iltyp_IAsyncResult) (parameters, ret)
1199511920
| _ -> ()
1199611921

11997-
| TFSharpTyconRepr { fsobjmodel_kind = TFSharpUnion } when not (tycon.HasMember g "ToString" []) ->
11998-
yield! GenUnionToStringMethod(cenv, mgbuf, eenvinner, ilThisTy, tcref, m)
11922+
// Reflection-free nominal unions get their ToString from the type-augmentation phase; here we
11923+
// only emit the sprintf "%+A" ToString for the non-reflection-free case.
11924+
| TFSharpTyconRepr { fsobjmodel_kind = TFSharpUnion } when not g.useReflectionFreeCodeGen && not (tycon.HasMember g "ToString" []) ->
11925+
yield! GenSprintfPrintingMethod cenv eenvinner "ToString" ilThisTy m
1199911926
| _ -> ()
1200011927
]
1200111928

0 commit comments

Comments
 (0)