Skip to content

Conversation

brianrourkeboll
Copy link
Contributor

@brianrourkeboll brianrourkeboll commented Sep 22, 2025

Description

Add support for the spread operator ... in record types and expressions (nominal and anonymous).

type R1      = { A : int; B : int }
type R2      = { C : int; D : int }
type R3      = { ...R1; ...R2; E : int } // { A : int; B : int; C : int; D : int; E : int }

let r1       = { A = 1; B = 2 }
let r2       = { C = 3; D = 4 }
let r3       = { ...r1; ...r2; E = 5 }   // { A = 1; B = 2; C = 3; D = 4; E = 5 }

let r1' : R1 = { ...r3; B = 99 }         // { A = 1; B = 99 }
let r2'      = {| ...r2 |}               // {| C = 3; D = 4 |}
let r3'      = { ...r1'; ...r2' }        // { A = 1; B = 99; C = 3; D = 4 }
let r3''     = {| ...r1; ...r2 |}        // {| A = 1; B = 2; C = 3; D = 4 |}
let r3'''    = {| ...r1'; ...r2' |}      // {| A = 1; B = 99; C = 3; D = 4 |}

This PR is meant to begin probing the "spread operator for objects" space (especially the set algebra and associated mechanics) while leaving room for implementing more of the scenarios outlined in fsharp/fslang-suggestions#1253 later. For example, should this prove viable, I would expect one of the next additions to be support for spreading non-records into records, i.e., mapping regular class/struct/interface properties/fields to record fields; this PR explicitly disallows that to ensure that we are free to add it later.

Checklist

Copy link
Contributor

github-actions bot commented Sep 22, 2025

❗ Release notes required

@brianrourkeboll,

Caution

No release notes found for the changed paths (see table below).

Please make sure to add an entry with an informative description of the change as well as link to this pull request, issue and language suggestion if applicable. Release notes for this repository are based on Keep A Changelog format.

The following format is recommended for this repository:

* <Informative description>. ([PR #XXXXX](https://github.com/dotnet/fsharp/pull/XXXXX))

See examples in the files, listed in the table below or in th full documentation at https://fsharp.github.io/fsharp-compiler-docs/release-notes/About.html.

If you believe that release notes are not necessary for this PR, please add NO_RELEASE_NOTES label to the pull request.

You can open this PR in browser to add release notes: open in github.dev

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/11.0.0.md No release notes found or release notes format is not correct
LanguageFeatures.fsi docs/release-notes/.Language/preview.md No release notes found or release notes format is not correct

@nojaf
Copy link
Contributor

nojaf commented Sep 22, 2025

Amazing work! I was literally asking @edgarfgp a few days ago if we had something like type R3 = { ...R1; ...R2; E : int } in F#!


// F# preview (still preview in 10.0)
LanguageFeature.FromEndSlicing, previewVersion // Unfinished features --- needs work
LanguageFeature.RecordSpreads, previewVersion
Copy link
Member

@T-Gro T-Gro Sep 22, 2025

Choose a reason for hiding this comment

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

I see two options on how to integrate all forms of spreads:

  • 1: Have a dedicate feature/spreading branch and have PRs merging incremental additions to it. This is how bigger features have been done in the past.

  • 2: Add the features to main (make us of the fact that NET10 development is close now, and within short time main will mean net11 already -> plenty of time) via dedicate feature switches. And prior to major release time, decide on the feature set to bring in.

Option 1 has advantages in terms of overall feature marketing and explain-ability to F# users.
Option 2 would give us options to dogfood selected pieces via preview SDK, and gather feedback sooner. (with the usual preview disclaimer of those bits being subject to potential change).

In this PR , Record spreads IMO form a coherent addition to the language and could be integrated standalone (= I would vote for option 2 here, i.e. integrate to main once main means net11)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd be a bit afraid that a feature branch would take a lot of effort to keep conflict-free. But yes, I understand the desire to avoid a situation like from-end slicing where the feature is never enabled but the extra code complexity sticks around (someday I will revive my slice branch that will address that particular problem, though 🙂).

Copy link
Member

Choose a reason for hiding this comment

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

I am with you on merging it to main as long as we can imagine final (e.g. at major .NET release time) user communication for that specific subset.

(sensible cuts I could imagine being separately release-able as of now basically follow the sections of the RFC Discussion: LanguageFeature.RecordSpread, ObjectIntoRecordSpread, InterfaceImplementationSpread, SpreadingPattern )

@dsyme
Copy link
Contributor

dsyme commented Sep 22, 2025

@brianrourkeboll Great to see work starting in this direction!

@brianrourkeboll
Copy link
Contributor Author

Hmm. It looks like there are some inconsistencies in the IL emitted for some of the tests I added between the net472 (left) and net10.0 (right) targets (unrelated to this feature itself):

image

It seems like it would be somewhat tough to update the IL normalization code to ignore it safely and without false positives/negatives. Do I need to make separate baselines for the two target frameworks instead? (I hope not: that would be a massive amount of duplication for basically no reason...) Any other way to handle that?

@T-Gro
Copy link
Member

T-Gro commented Oct 1, 2025

I believe the state of the art so far has indeed been to duplicate the .bsl files due to it.
This is a duck-typed attribute which is added only if the compilation unit cannot find it - maybe this fact can be hijacked somehow in order to unify netcore and net472 .bsl files?

Brainstorming:

  • Add the attribute definition to FSharp.Core built for testing purposes (maybe too complicated?)
  • Or add it there for real?
  • Add a test-helper that will add the attribute definition to executed tests

Right it is IlxGen which adds it conditionally, leading to differences in IL.
If it is added unconditionally, the baselines should not differ.

Alternative approach which I see I have used at:

let ``Struct DU compilation - have a look at IL for massive cases`` () =

Is to assert only a subset of the .bsl, not the full file (IMO this could be encoded into the framework if we want to - like a takeWhile line<>... rule for both sides of the IL comparison )

The compiler emits various types from the
System.Diagnostics.CodeAnalysis namespace for the .NET Framework target,
but those types come from the runtime for the .NET (Core) target.
Since the only IL that is material here is the field names, types, and
ordering, and since the spread logic is entirely
framework/runtime-agnostic, it is simpler to run these tests only for
the .NET (Core) target.
@brianrourkeboll
Copy link
Contributor Author

@T-Gro This was my solution for the time being: ff2a3e6. Let me know if you think the risk is too high that some kind of pertinent difference could later be introduced in the behavior of spreads between .NET Framework and .NET (Core) — the risk seems pretty low to me, though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: New
Development

Successfully merging this pull request may close these issues.

5 participants