fsharp icon indicating copy to clipboard operation
fsharp copied to clipboard

String operator always returning nonNull string

Open T-Gro opened this issue 1 year ago • 8 comments

This fixes https://github.com/dotnet/fsharp/issues/17742 .

F#-originating code has always been against implicit nulls. Even when null literal was passed into the string function, the null was still replaced with an empty string.

What was missing was checking for object types overriding ToString and returning null out of their implementation - this has not been historically checked, very likely due to this pattern not being heavily used and possibly overlooked in the implementation of the string operator.

This PR fixes it by making sure that all code paths of the statically optimized string call will not return an empty string.

The alternative has been evaluated and changing the signature to let inline string x : string | null = ... would flood all legitimate usages of this standard function and enforce unnecessary null checks (under the assumption that legitimate F# code does not return null from .ToString, which is even a new warning now)

T-Gro avatar Sep 27 '24 09:09 T-Gro

This PR fixes it by making sure that all code paths of the statically optimized string call will not return an empty string.

This was supposed to say "will now return an empty string" (and "all code paths" by implication meant "all code paths that returned null before"), right? Otherwise I don't get it

abonie avatar Sep 27 '24 10:09 abonie

Just to be clear — this is an actual change in behavior, not just a change in nullness inference, right?

Yes, this bugfix (or handling a forgotten scenario) is a breaking change (I guess technically, every bugfix is). Will have to be documented as such.

I'd also be curious what the perf implications are

We first have to make it correct. I assume there is limited potential in detecting certain types whose ToString is guaranteed to by not null at JIT time (at generic instantiation), even though even the F# types could have been returning null as well.

T-Gro avatar Sep 30 '24 08:09 T-Gro

Just to be clear — this is an actual change in behavior, not just a change in nullness inference, right?

Yes, this bugfix (or handling a forgotten scenario) is a breaking change (I guess technically, every bugfix is). Will have to be documented as such.

Do we know if this is described in any rfc or spec?

I'd also be curious what the perf implications are

We first have to make it correct.

I assume there is limited potential in detecting certain types whose ToString is guaranteed to by not null at JIT time (at generic instantiation), even though even the F# types could have been returning null as well.

What the rough IL we will be generating here?

vzarytovskii avatar Oct 03 '24 20:10 vzarytovskii

I did not find any mention of the behavior for the string function, a promise about exactly returning .ToString() is not there.

The IL will look like this:

        IL_0001: callvirt instance string [System.Runtime]System.Object::ToString()
        IL_0006: stloc.0
        IL_0007: ldloc.0
        IL_0008: brtrue.s IL_0010

        IL_000a: ldstr ""
        IL_000f: ret

        IL_0010: ldloc.0
        IL_0011: ret

For all normally-behaving objects, branch prediction will be stable for the brtrue.s check.

T-Gro avatar Oct 04 '24 09:10 T-Gro

I did not find any mention of the behavior for the string function, a promise about exactly returning .ToString() is not there.

The IL will look like this:


        IL_0001: callvirt instance string [System.Runtime]System.Object::ToString()

        IL_0006: stloc.0

        IL_0007: ldloc.0

        IL_0008: brtrue.s IL_0010



        IL_000a: ldstr ""

        IL_000f: ret



        IL_0010: ldloc.0

        IL_0011: ret

For all normally-behaving objects, branch prediction will be stable for the brtrue.s check.

Yeah, I would think so as well. Will be curious to hear from @EgorBo as well

vzarytovskii avatar Oct 04 '24 11:10 vzarytovskii

C# does it like this for o.ToString() ?? "" :

        IL_0001: callvirt instance string [System.Runtime]System.Object::ToString()
        IL_0006: dup
        IL_0007: brtrue.s IL_000f

        IL_0009: pop
        IL_000a: ldstr ""

        IL_000f: ret

Which uses less instructions for the happy path (ToString result not being null).

T-Gro avatar Oct 04 '24 11:10 T-Gro

:heavy_exclamation_mark: Release notes required


:white_check_mark: Found changes and release notes in following paths:

Change path Release notes path Description
src/FSharp.Core docs/release-notes/.FSharp.Core/9.0.200.md

github-actions[bot] avatar Oct 15 '24 09:10 github-actions[bot]

The fix is clear, but I think it would be nice to add some IL baselines for this behavior.

psfinaki avatar Oct 15 '24 10:10 psfinaki

Suggestion approved now: https://github.com/fsharp/fslang-suggestions/issues/1386

T-Gro avatar Nov 06 '24 19:11 T-Gro

/azp run

T-Gro avatar Nov 06 '24 19:11 T-Gro

Azure Pipelines successfully started running 2 pipeline(s).

azure-pipelines[bot] avatar Nov 06 '24 19:11 azure-pipelines[bot]