You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 17cb503
Browse filesBrowse the repository at this point in the historyBrowse 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>
# 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.
-**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:
| 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:
| 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.
* 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))
145
146
* 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))
146
147
* Checker: recover on checking language version ([PR ##19970](https://github.com/dotnet/fsharp/pull/19970))
147
148
* 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))
if tycon.IsUnionTycon && AddAugmentationDeclarations.ShouldAugmentUnion g tycon then
4823
4836
letunionVals=
@@ -4827,7 +4840,7 @@ module TcDeclarations =
4827
4840
AugmentTypeDefinitions.MakeBindingsForUnionAugmentation g tycon (List.map mkLocalValRef unionVals)
4828
4841
else
4829
4842
[]
4830
-
binds@binds4, binds3)
4843
+
binds@binds4, binds3@binds5)
4831
4844
4832
4845
// 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
0 commit comments