Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

String operator always returning nonNull string #17809

Merged

Conversation

T-Gro
Copy link
Member

@T-Gro T-Gro commented Sep 27, 2024

This fixes #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 T-Gro linked an issue Sep 27, 2024 that may be closed by this pull request
7 tasks
@T-Gro T-Gro changed the title [WIP to see bsl changes] String operator always returning nonNull string String operator always returning nonNull string Sep 27, 2024
@T-Gro T-Gro marked this pull request as ready for review September 27, 2024 09:39
@T-Gro T-Gro requested a review from a team as a code owner September 27, 2024 09:39
@abonie
Copy link
Member

abonie commented Sep 27, 2024

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

Copy link
Contributor

@brianrourkeboll brianrourkeboll left a comment

Choose a reason for hiding this comment

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

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

Is this accurate?

Before
// Before: both returned null.
string { new obj () with override _.ToString () = null }
string { new IFormattable with override _.ToString (_, _) = null }
After
// After: both return "".
string { new obj () with override _.ToString () = null }
string { new IFormattable with override _.ToString (_, _) = null }

This is technically a breaking change, so it seems like it should probably should have a language suggestion (e.g., compare fsharp/fslang-suggestions#891).

I'd also be curious what the perf implications are, if any, of the extra IL and branches this adds to string calls. It would be a shame to make 99.99% of code using string slower just to make the nullness info accurate in the extremely rare scenario of someone intentionally overriding ToString to return null.

@T-Gro
Copy link
Member Author

T-Gro commented Sep 30, 2024

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.

@vzarytovskii
Copy link
Member

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?

@T-Gro
Copy link
Member Author

T-Gro commented Oct 4, 2024

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.

@vzarytovskii
Copy link
Member

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

@T-Gro
Copy link
Member Author

T-Gro commented Oct 4, 2024

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

@dotnet dotnet deleted a comment from github-actions bot Oct 15, 2024
Copy link
Contributor

❗ Release notes required


✅ 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

@T-Gro T-Gro enabled auto-merge (squash) October 15, 2024 09:21
@T-Gro T-Gro added this to the October-2024 milestone Oct 15, 2024
@T-Gro T-Gro self-assigned this Oct 15, 2024
@psfinaki
Copy link
Member

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

@T-Gro
Copy link
Member Author

T-Gro commented Nov 6, 2024

Suggestion approved now: fsharp/fslang-suggestions#1386

@T-Gro
Copy link
Member Author

T-Gro commented Nov 6, 2024

/azp run

Copy link

Azure Pipelines successfully started running 2 pipeline(s).

Copy link
Contributor

@0101 0101 left a comment

Choose a reason for hiding this comment

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

I agree with @psfinaki about adding a simple test that covers this behavior.

docs/release-notes/.FSharp.Core/9.0.200.md Outdated Show resolved Hide resolved
Co-authored-by: Petr Pokorny <petrpokorny@microsoft.com>
@T-Gro T-Gro requested a review from 0101 November 7, 2024 11:22
@T-Gro T-Gro merged commit 87587f8 into main Nov 8, 2024
33 checks passed
@T-Gro T-Gro deleted the 17742-nullness-issue-string-function-signature-doesnt-hold branch November 11, 2024 10:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Nullness issue - string function signature doesn't hold
6 participants