Skip to content

Commit 17cb503

Browse files
Compiled ToStrings under -reflectionfree for DUs and Records (#19976)
* 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 (#19971); kept identical there so a future merge resolves cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * Add release notes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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> * Guard generated reflection-free ToString against deep-recursion overflow The augmentation-generated structural ToString recurses into fields, so a deeply nested value can exhaust the stack with an uncatchable StackOverflowException. Emit RuntimeHelpers.EnsureSufficientExecutionStack() at method entry (as C# records do in PrintMembers) so it throws a catchable InsufficientExecutionStackException instead, when the runtime provides the method. The guard is skipped for types whose every field is a flat primitive (integer/float/decimal/string/char/bool/unit/enum), which cannot recurse. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Test the reflection-free ToString deep-recursion guard A 1,000,000-deep value's generated ToString throws a catchable InsufficientExecutionStackException rather than hard-crashing the process. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * revert ToString additions to fsharp.core types * Remove stale FSharp.Core release note for the reverted Result/Choice ToString Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix code formatting in IlxGen.fs (dotnet fantomas) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * don't use quoted name * int version of reflectionfree-printing doc * doc tweaks * Link release note to the printing doc and cover anonymous records Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test backticks * Share the ToString recursion guard with anonymous records The guard lived in MakeBindingsForToStringAugmentation, which anonymous records bypass: they are synthesized too late for type augmentation and reach mkRecdToString from IlxGen instead. Deep nesting overflowed the stack rather than raising InsufficientExecutionStackException. Move it into mkToStringRecursionGuard, applied inside mkRecdToString and mkUnionToString, so every caller of the body builders gets it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add EmittedIL baselines for struct and anonymous record ToString Struct records and unions read fields off the this pointer and switch on the tag, and the anonymous record path is generated separately in IlxGen, so each gets its own baseline. The anonymous baseline omits the field reads: they name the anonymous type, whose mangled name is not stable across compilations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix empty anonymous record ToString rendering a doubled space The open/close braces carry inner spaces ("{| " and " |}"); with no fields they abut and render "{| |}". Trim the leading space when the field list is empty, matching %A's "{| |}". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * tidy comment --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5dfbf7f commit 17cb503

14 files changed

Lines changed: 912 additions & 32 deletions

File tree

‎docs/reflectionfree-printing.md‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Simple vs Reflection-based DU and Record printing
2+
3+
This document describes two modes for printing Discriminated Unions (DUs) and Records in F#: a **simple** reflection-free mode that delegates to a `string`-like operator for printing field values, and a `sprintf` mode (`sprintf "%A"`), which uses **reflection** to create output looking like F# code. In this document, the terms *simple* and *reflection* are used to distinguish the two modes.
4+
5+
Without the `--reflectionfree` flag, the compiler generates a `ToString` for DUs and Records that calls `sprintf "%A"`. With the flag, the compiler generates a `ToString` that uses the simple mode.
6+
7+
Users can choose between the two modes by 1. use of `--reflectionfree`, and by 2. calling with a `sprintf`-type caller or a `string`-type caller (e.g. the `string` operator, `ToString`, or interpolated strings).
8+
9+
If `x` is a DU or Record, then output will be simple or reflection-based as follows:
10+
| | `--reflectionfree` | no `--reflectionfree` |
11+
|---|---|---|
12+
| `string x` | simple | reflection |
13+
| `x.ToString()` | simple | reflection |
14+
| `$"{x}"` | simple | reflection |
15+
| `sprintf "%A" x` | disallowed (would be reflection) | reflection |
16+
17+
As such, the current default reflection `ToString` generation forces reflection formatting on all callers. On the other hand, generating simple `ToString` output means that the records and DUs are printed with simple or reflection formatting depending on whether the caller is of simple or reflection affinity. The `--reflectionfree` flag combines this property with a ban on `sprintf` to prevent the reflection mode from being used.
18+
19+
In addition to user-defined types, the FSharp.Core `option` type uses simple printing, while other types either have no `ToString` or use some other format.
20+
21+
## Behaviour: definitions
22+
23+
In simple printing, field values are printed with `string`-type formatting, more precisely `anyToStringShowingNull`. No line breaks are inserted.
24+
25+
- **Record**: `{ Name1 = value1; Name2 = value2 }`.
26+
- **Anonymous record**: the same, but with `{| ` and ` |}`.
27+
- **Union**: A case with no fields renders as just its name. A case with fields renders as `CaseName(value1, value2)`.
28+
29+
`[<Struct>]` records and unions, and struct anonymous records, render identically to their reference-type forms.
30+
31+
A type that supplies its own `ToString` override keeps it, with no `ToString` generated for it (either simple or reflection).
32+
33+
Reflection-mode printing is described in [plain text formatting](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/plaintext-formatting).
34+
35+
## Behavioural differences
36+
37+
### Differences in field rendering
38+
39+
The following differences between `string` and `sprintf "%A"` carry over directly into differences in field rendering between simple and reflection printing:
40+
41+
| F# value | simple (`anyToStringShowingNull`) | reflection (`sprintf "%A"`) |
42+
|---|---|---|
43+
| string field `"hi"` | `hi` | `"hi"` |
44+
| char field `'a'` | `a` | `'a'` |
45+
| float `5.0` | `5` | `5.0` |
46+
| `250uy` / `42n` / `1.5M` | `250` / `42` / `1.5` | `250uy` / `42n` / `1.5M` |
47+
| option field `None` | `null` | `None` |
48+
| array field `[\|1;2;3\|]` | `System.Int32[]` | `[\|1; 2; 3\|]` |
49+
| unit field `()` | `null` | `()` |
50+
51+
The overall differences here are:
52+
- Simple printing converts to strings, while reflection printing is more bi-directional, often generating compilable F# code.
53+
- F# types that have null representation (`unit`, `option`, and in general types with `AllowNullLiteral` or `UseNullAsTrueValue`) are printed as `null` in simple printing, while reflection printing uses a more F#-like representation.
54+
55+
### Other differences
56+
57+
These differences are in the printing of the record or DU itself rather than of its fields:
58+
59+
| F# value | simple (`string`) | reflection (`sprintf "%A"`) |
60+
|---|---|---|
61+
| `B 5` (single field) | `B(5)` | `B 5` |
62+
| `C (3, 4)` (two fields) | `C(3, 4)` | `C (3, 4)` |
63+
| record `{ X = 1; Y = 2 }` | `{ X = 1; Y = 2 }` | `{ X = 1`⏎` Y = 2 }` |
64+
| `[<StructuredFormatDisplay("Custom<{X}>")>]` | `{ X = 5 }` | `Custom<5>` |
65+
66+
The overall differences here are:
67+
- Simple printing always brackets a case's fields and never pads, while reflection printing omits brackets for a single non-tuple field and inserts a space before them otherwise.
68+
- Simple printing uses a single line (unless a field's own rendering contains breaks), while reflection printing breaks records and nested values across lines with indentation.
69+
- `StructuredFormatDisplay` is ignored in simple printing and honoured in reflection printing.
70+
71+
## Recursion and depth
72+
73+
Rendering recurses into nested records and unions. Deep nesting is guarded by `RuntimeHelpers.EnsureSufficientExecutionStack`, raising a catchable `InsufficientExecutionStackException` rather than `StackOverflowException`; cycles (which require mutation to construct) still overflow, as `option` and `list` do.

‎docs/release-notes/.FSharp.Compiler.Service/11.0.100.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
* Debug: rework for expressions stepping ([PR #19894](https://github.com/dotnet/fsharp/pull/19894))
143143
* Debug: rework conditional erasure, fix stepping over literals ([PR #19897](https://github.com/dotnet/fsharp/pull/19897))
144144
* Debug: fix if and match condition sequence points ([PR #19932](https://github.com/dotnet/fsharp/pull/19932))
145+
* Under `--reflectionfree`, discriminated unions, records and anonymous records now get a [generated `ToString`](../../reflectionfree-printing.md) (rendering each field like `Option` does) instead of falling back to the namespace-qualified type name. ([PR #19976](https://github.com/dotnet/fsharp/pull/19976))
145146
* Support common types of `NotNullIfNotNullAttribute` usage. If a method parameter is marked with `NotNullIfNotNullAttribute`, the compiler will now honor this attribute and mark the return type as non-null. ([PR #19977](https://github.com/dotnet/fsharp/pull/19977))
146147
* Checker: recover on checking language version ([PR ##19970](https://github.com/dotnet/fsharp/pull/19970))
147148
* Implied argument names for function-to-delegate coercions now fall back to the delegate's `Invoke` parameter names when the function has no recoverable names (e.g. a partial application like `System.Func<int, int>((+) 1)`), instead of synthetic `delegateArg0`, `delegateArg1`, … names. ([PR #20001](https://github.com/dotnet/fsharp/pull/20001))

‎src/Compiler/Checking/AugmentWithHashCompare.fs‎

Lines changed: 148 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

@@ -1697,3 +1703,145 @@ let MakeBindingsForUnionAugmentation g (tycon: Tycon) (vals: ValRef list) =
16971703
let isdata = mkUnionCaseTest g (thise, ucr, tinst, m)
16981704
let expr = mkLambdas g m tps [ thisv; unitv ] (isdata, g.bool_ty)
16991705
mkCompGenBind v.Deref expr)
1706+
1707+
//-------------------------------------------------------------------------
1708+
// Build reflection-free ToString functions for union and record types.
1709+
//
1710+
// Under --reflectionfree the reflective 'sprintf "%+A"' ToString is unavailable, so we build a structural
1711+
// one here (during type augmentation, so the 'string' operator calls flow through the optimizer and get
1712+
// specialised - e.g. an int field renders via a direct, allocation-free ToString rather than a boxed call).
1713+
//-------------------------------------------------------------------------
1714+
1715+
// Guard deep recursion with a catchable exception, as C# records' PrintMembers do, when the runtime provides
1716+
// it. A type whose fields are all primitive cannot nest, so it skips the guard.
1717+
let mkToStringRecursionGuard (g: TcGlobals, m: Text.range, fieldTys: TType list, body: Expr) =
1718+
let isPrimitive (ty: TType) =
1719+
isIntegerTy g ty
1720+
|| isFpTy g ty
1721+
|| isDecimalTy g ty
1722+
|| isStringTy g ty
1723+
|| typeEquiv g g.char_ty ty
1724+
|| isBoolTy g ty
1725+
|| isUnitTy g ty
1726+
|| isEnumTy g ty
1727+
1728+
if fieldTys |> List.forall isPrimitive then
1729+
body
1730+
else
1731+
match g.TryFindSysILTypeRef "System.Runtime.CompilerServices.RuntimeHelpers" with
1732+
| Some tref ->
1733+
let mspec =
1734+
mkILNonGenericStaticMethSpecInTy (mkILNonGenericBoxedTy tref, "EnsureSufficientExecutionStack", [], ILType.Void)
1735+
1736+
mkSequential m (mkAsmExpr ([ mkNormalCall mspec ], [], [], [], m)) body
1737+
| None -> body
1738+
1739+
// Render one field value as a string the way option/list do (LanguagePrimitives.anyToStringShowingNull):
1740+
// a null reference renders as "null", everything else via the 'string' operator. A value-type field can
1741+
// never be null, so it skips the box+null-guard and renders directly.
1742+
let mkFieldToString (g: TcGlobals, m: Text.range, fe: Expr) =
1743+
let fieldTy = tyOfExpr g fe
1744+
1745+
if isStructTy g fieldTy then
1746+
mkCallStringOperator g m fieldTy fe
1747+
else
1748+
let v, ve = mkCompGenLocal m "field" fieldTy
1749+
mkCompGenLet m v fe (mkNonNullCond g m g.string_ty (mkCallBox g m fieldTy ve) (mkCallStringOperator g m fieldTy ve) (mkString g m "null"))
1750+
1751+
// A record's ToString as a single line "{ F1 = v1; F2 = v2 }" (no line breaks, unlike "%+A").
1752+
// openBrace/closeBrace are "{ "/" }" for records and "{| "/" |}" for anonymous records.
1753+
let mkRecdToString (g: TcGlobals, tcref: TyconRef, tycon: Tycon, openBrace: string, closeBrace: string) =
1754+
let m = tycon.Range
1755+
let tinst, ty = mkMinimalTy g tcref
1756+
let thisv, thise = mkThisVar g m ty
1757+
1758+
let fieldParts =
1759+
tcref.AllInstanceFieldsAsList
1760+
|> List.mapi (fun i fspec ->
1761+
let fref = tcref.MakeNestedRecdFieldRef fspec
1762+
let value = mkFieldToString (g, m, mkRecdFieldGetViaExprAddr (thise, fref, tinst, m))
1763+
let nameEq = mkString g m (fspec.DisplayNameCore + " = ")
1764+
if i = 0 then [ nameEq; value ] else [ mkString g m "; "; nameEq; value ])
1765+
|> List.concat
1766+
1767+
let close =
1768+
if List.isEmpty fieldParts then
1769+
// Avoid a double space in an empty record.
1770+
closeBrace.TrimStart()
1771+
else closeBrace
1772+
let parts = mkString g m openBrace :: fieldParts @ [ mkString g m close ]
1773+
let fieldTys = tcref.AllInstanceFieldsAsList |> List.map (fun fspec -> fspec.FormalType)
1774+
thisv, mkToStringRecursionGuard (g, m, fieldTys, mkStringConcat (g, m, parts))
1775+
1776+
// A union's ToString as a match over the cases building "CaseName(f0, f1, ...)" (or just "CaseName" for a
1777+
// nullary case).
1778+
let mkUnionToString (g: TcGlobals, tcref: TyconRef, tycon: Tycon) =
1779+
let m = tycon.Range
1780+
let tinst, ty = mkMinimalTy g tcref
1781+
let thisv, thise = mkThisVar g m ty
1782+
let mbuilder = MatchBuilder(DebugPointAtBinding.NoneAtInvisible, m)
1783+
1784+
let mkResult (ucase: UnionCase) =
1785+
let cref = tcref.MakeNestedUnionCaseRef ucase
1786+
let rfields = ucase.RecdFields
1787+
1788+
if isNil rfields then
1789+
mkString g m ucase.DisplayNameCore
1790+
else
1791+
// provene is an expression proven to be of this case (the value itself for struct unions,
1792+
// otherwise a 'UnionCaseProof'), from which fields can be read.
1793+
let mkBody (provene: Expr) =
1794+
let fieldStrs =
1795+
rfields
1796+
|> List.mapi (fun j _ -> mkFieldToString (g, m, mkUnionCaseFieldGetProvenViaExprAddr (provene, cref, tinst, j, m)))
1797+
1798+
let sep = mkString g m ", "
1799+
1800+
let fieldsWithSeps =
1801+
fieldStrs |> List.mapi (fun i fe -> if i = 0 then [ fe ] else [ sep; fe ]) |> List.concat
1802+
1803+
let parts = mkString g m (ucase.DisplayNameCore + "(") :: fieldsWithSeps @ [ mkString g m ")" ]
1804+
mkStringConcat (g, m, parts)
1805+
1806+
if cref.Tycon.IsStructOrEnumTycon then
1807+
mkBody thise
1808+
else
1809+
let ucv, ucve = mkCompGenLocal m "thisCast" (mkProvenUnionCaseTy cref tinst)
1810+
mkCompGenLet m ucv (mkUnionCaseProof (thise, cref, tinst, m)) (mkBody ucve)
1811+
1812+
let cases =
1813+
tcref.UnionCasesAsList
1814+
|> List.map (fun ucase ->
1815+
let cref = tcref.MakeNestedUnionCaseRef ucase
1816+
mkCase (DecisionTreeTest.UnionCase(cref, tinst), mbuilder.AddResultTarget(mkResult ucase)))
1817+
1818+
let dtree = TDSwitch(thise, cases, None, m)
1819+
1820+
let fieldTys =
1821+
tcref.UnionCasesAsList |> List.collect (fun uc -> uc.RecdFields) |> List.map (fun rf -> rf.FormalType)
1822+
1823+
thisv, mkToStringRecursionGuard (g, m, fieldTys, mbuilder.Close(dtree, m, g.string_ty))
1824+
1825+
let TyconIsCandidateForAugmentationWithToString (g: TcGlobals, tycon: Tycon) =
1826+
g.useReflectionFreeCodeGen && (tycon.IsUnionTycon || tycon.IsRecordTycon)
1827+
1828+
let MakeValsForToStringAugmentation (g: TcGlobals, tcref: TyconRef) =
1829+
let _, ty = mkMinimalTy g tcref
1830+
let vis = tcref.Accessibility
1831+
let tps = tcref.Typars
1832+
mkValSpec g tcref ty vis (Some(mkToStringSlotSig g)) "ToString" (tps +-> (mkToStringTy (g, ty))) unitArg false
1833+
1834+
let MakeBindingsForToStringAugmentation (g: TcGlobals, tycon: Tycon, toStringVal: Val) =
1835+
let tcref = mkLocalTyconRef tycon
1836+
let m = tycon.Range
1837+
let tps = tycon.Typars
1838+
1839+
let thisv, body =
1840+
if tycon.IsUnionTycon then
1841+
mkUnionToString (g, tcref, tycon)
1842+
else
1843+
mkRecdToString (g, tcref, tycon, "{ ", " }")
1844+
1845+
let unitv, _ = mkCompGenLocal m "unitArg" g.unit_ty
1846+
let expr = mkLambdas g m tps [ thisv; unitv ] (body, g.string_ty)
1847+
[ 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, recursion guard included; 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
@@ -944,6 +944,18 @@ module AddAugmentationDeclarations =
944944
else []
945945
else []
946946

947+
// Under --reflectionfree the structural ToString is generated here (rather than in IlxGen) so the 'string'
948+
// operator calls in its body flow through the optimizer and get specialised. Like the Equals override, this
949+
// runs late so tycon.HasMember gives correct results for a user-written ToString.
950+
let AddReflectionFreeToStringBindings (cenv: cenv, env: TcEnv, tycon: Tycon) =
951+
let g = cenv.g
952+
if AugmentTypeDefinitions.TyconIsCandidateForAugmentationWithToString(g, tycon) && not (tycon.HasMember g "ToString" []) then
953+
let tcref = mkLocalTyconRef tycon
954+
let toStringVal = AugmentTypeDefinitions.MakeValsForToStringAugmentation(g, tcref)
955+
PublishValueDefn cenv env ModuleOrMemberBinding toStringVal
956+
AugmentTypeDefinitions.MakeBindingsForToStringAugmentation(g, tycon, toStringVal)
957+
else []
958+
947959
let ShouldAugmentUnion (g: TcGlobals) (tycon: Tycon) =
948960
g.langVersion.SupportsFeature LanguageFeature.UnionIsPropertiesVisible &&
949961
HasDefaultAugmentationAttribute g (mkLocalTyconRef tycon) &&
@@ -4816,8 +4828,9 @@ module TcDeclarations =
48164828
// We put the hash/compare bindings before the type definitions and the
48174829
// equality bindings after because tha is the order they've always been generated
48184830
// in, and there are code generation tests to check that.
4819-
let binds = AddAugmentationDeclarations.AddGenericHashAndComparisonBindings cenv tycon
4831+
let binds = AddAugmentationDeclarations.AddGenericHashAndComparisonBindings cenv tycon
48204832
let binds3 = AddAugmentationDeclarations.AddGenericEqualityBindings cenv envForDecls tycon
4833+
let binds5 = AddAugmentationDeclarations.AddReflectionFreeToStringBindings(cenv, envForDecls, tycon)
48214834
let binds4 =
48224835
if tycon.IsUnionTycon && AddAugmentationDeclarations.ShouldAugmentUnion g tycon then
48234836
let unionVals =
@@ -4827,7 +4840,7 @@ module TcDeclarations =
48274840
AugmentTypeDefinitions.MakeBindingsForUnionAugmentation g tycon (List.map mkLocalValRef unionVals)
48284841
else
48294842
[]
4830-
binds@binds4, binds3)
4843+
binds@binds4, binds3@binds5)
48314844

48324845
// 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
48334846
EstablishTypeDefinitionCores.TcTyconDefnCore_CheckForCyclicStructsAndInheritance cenv tycons

0 commit comments

Comments
 (0)