Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Core/10.0.300.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
132 changes: 120 additions & 12 deletions src/Compiler/CodeGen/IlxGen.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 =
Expand All @@ -2347,7 +2350,7 @@ type AnonTypeGenerationTable() =
ilGenericParams,
ilBaseTy,
ilInterfaceTys,
mkILMethods (ilCtorDef :: ilMethods),
mkILMethods (ilCtorDef :: ilMethods @ ilToStringMethodDefs),
ilFieldDefs,
emptyILTypeDefs,
ilProperties,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure to cover anons and struct anons in testing.


let withQName (loc: CompileLocation) =
{ loc with
Expand Down Expand Up @@ -11210,11 +11213,8 @@ 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 =
and GenSprintfPrintingMethod cenv eenv methName ilThisTy m =
let g = cenv.g

[
Expand Down Expand Up @@ -11279,6 +11279,114 @@ and GenPrintingMethod 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 [<CompilerGenerated>] 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) =
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",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there was a string override present, written by hand?

[<NoComparison;NoEquality>]
type MyDU = A of int
    with override x.ToString() = "A"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, and a test is added in a5f33ca

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
GenSprintfPrintingMethod cenv eenv "ToString" ilThisTy m
else
let tinst, thisv, thise = GenToStringThis (cenv, tcref, m)
Comment on lines +11321 to +11324

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that IlxGen has two different generators for ToString, I would appreciate a more distinct naming.
Fine if it becomes longer, better to avoid confusion.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed in 7ed0d8d :

  • GenPrintingMethod → GenSprintfPrintingMethod (the reflective sprintf "%+A" path, used for both ToString and exception get_Message)
  • GenToStringMethodFromExpr → EmitToStringMethodDef.


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".
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))

and GenTypeDef cenv mgbuf lazyInitInfo eenv m (tycon: Tycon) : ILTypeRef option =
let g = cenv.g
let tcref = mkLocalTyconRef tycon
Expand Down Expand Up @@ -11863,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! GenToStringMethod cenv eenv ilThisTy m
yield! GenRecordToStringMethod(cenv, mgbuf, eenvinner, ilThisTy, tcref, m, "{ ", " }")

| TFSharpTyconRepr r when tycon.IsFSharpDelegateTycon ->

Expand All @@ -11887,7 +11995,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, eenvinner, ilThisTy, tcref, m)
| _ -> ()
]

Expand Down Expand Up @@ -12511,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 =
Expand Down
14 changes: 1 addition & 13 deletions src/Compiler/Optimize/Optimizer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions src/Compiler/TypedTree/TcGlobals.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/Compiler/TypedTree/TcGlobals.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1570,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.
Expand Down
7 changes: 7 additions & 0 deletions src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -446,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
Expand Down
Loading
Loading