Skip to content

Record spreads: { ...expr }, { ...ty } - #18927

Open
brianrourkeboll wants to merge 2 commits into
dotnet:mainfrom
brianrourkeboll:record-spreads
Open

Record spreads: { ...expr }, { ...ty }#18927
brianrourkeboll wants to merge 2 commits into
dotnet:mainfrom
brianrourkeboll:record-spreads

Conversation

@brianrourkeboll

@brianrourkeboll brianrourkeboll commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

Description

Adds language support for record-to-record type and expression spreads.

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 r4 : R1 = {  ...r3;  B = 99 }        // {  A = 1; B = 99 }
let r5      = {| ...r2 |}                // {| C = 3; D = 4 |}
let r6      = {  ...r4; ...r5 }          // {  A = 1; B = 99; C = 3; D = 4 }
let r7      = {| ...r1; ...r2 |}         // {| A = 1; B = 2; C = 3; D = 4 |}
let r8      = {| ...r4; ...r5 |}         // {| A = 1; B = 99; C = 3; D = 4 |}

This PR implements an independently useful record-to-record subset of the broader "spreads for F#" language suggestion: fsharp/fslang-suggestions#1253.

Checklist

  • Release notes entry updated
  • Fix any regressions
  • Enough™ tests
    • Parsing and error recovery
    • Set algebra
    • Accessibility
    • Mutability
    • Generics
    • Recursion
    • Effects
    • Allowed and disallowed sources
    • Conversions/coercions
    • Emitted IL
      • Attribute shadowing
      • Most of the rest of the above
  • Good (enough) error message when ... used in places that don't yet support it (e.g., seq { ...xs })
  • RFC: [RFC FS-1151] Record spreads fsharp/fslang-design#805
  • Clean up/refactor

Feature overview

I hope that the tests can serve as a reasonable overview of the feature and its behavior. If you see any glaring omissions, or if the tests are unclear or incomplete, please let me know. (I see that I still need to add tests for coercions/conversions…)

Parsing & error recovery

module Parsing =
[<Fact>]
let ``{...} → error`` () =
let src =
"""
type R1 = { A : int; B : int }
type R2 = { ... }
let r1 : R1 = { ... }
let r2 = {| ... |}
let r1' : R1 = { r1 with ... }
let r2' = {| r1 with ... |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3900, Line 3, Col 29, Line 3, Col 32, "Missing spread source type after '...'."
Error 3899, Line 4, Col 33, Line 4, Col 36, "Missing spread source expression after '...'."
Error 3899, Line 5, Col 29, Line 5, Col 32, "Missing spread source expression after '...'."
Error 3899, Line 6, Col 42, Line 6, Col 45, "Missing spread source expression after '...'."
Error 3899, Line 7, Col 38, Line 7, Col 41, "Missing spread source expression after '...'."
]
[<Fact>]
let ``{ ...r with } → error`` () =
let src =
"""
type R = { A : int; B : int }
let r1 = { A = 1; B = 2 }
let r2 = { ...r1 with A = 3 }
let r3 = {| ...r1 with A = 3 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3903, Line 4, Col 28, Line 4, Col 31, "Spreading is not supported in this position. Use one of the forms { ...expr1; A = expr2 } or { expr1 with A = expr2 } instead."
Error 3903, Line 5, Col 29, Line 5, Col 32, "Spreading is not supported in this position. Use one of the forms { ...expr1; A = expr2 } or { expr1 with A = expr2 } instead."
]
[<Fact>]
let ``seq {...} → error`` () =
let src =
"""
let xs = [1..10]
let _ = seq { ... }
let _ = seq { ...xs }
let _ = seq { ...xs; ...xs }
let _ = seq { ...xs; 1 }
let _ = seq { 1; ...xs }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3899, Line 3, Col 31, Line 3, Col 34, "Missing spread source expression after '...'."
// This is because the sequence expression body is being parsed as a record.
// If we add support for spreads in sequence expressions, we will need to update record parsing.
Error 10, Line 6, Col 38, Line 6, Col 39, "Unexpected integer literal in expression. Expected '}' or other token."
Error 604, Line 6, Col 29, Line 6, Col 30, "Unmatched '{'"
Error 3902, Line 7, Col 34, Line 7, Col 37, "Spreading is not supported in this construct."
]
[<Fact>]
let ``custom {...} → error`` () =
let src =
"""
type Custom () =
member _.Zero () = []
member _.Yield x = [x]
member _.YieldFrom xs = xs
member _.Combine (xs, ys) = xs @ ys
member _.Delay f = f ()
let custom = Custom ()
let xs = [1..10]
let _ = custom { ... }
let _ = custom { ...xs }
let _ = custom { ...xs; ...xs }
let _ = custom { ...xs; 1 }
let _ = custom { 1; ...xs }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3899, Line 12, Col 34, Line 12, Col 37, "Missing spread source expression after '...'."
// This is because the computation body is being parsed as a record.
// If we add support for spreads in custom computation expressions, we will need to update record parsing.
Error 10, Line 15, Col 41, Line 15, Col 42, "Unexpected integer literal in expression. Expected '}' or other token."
Error 604, Line 15, Col 32, Line 15, Col 33, "Unmatched '{'"
Error 3902, Line 16, Col 37, Line 16, Col 40, "Spreading is not supported in this construct."
]
[<Fact>]
let ``[ ... ] → error`` () =
let src =
"""
let xs = [1..10]
let _ = [ ... ]
let _ = [ ...xs ]
let _ = [ ...xs; ...xs ]
let _ = [ ...xs; 1 ]
let _ = [ 1; ...xs ]
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3902, Line 3, Col 27, Line 3, Col 30, "Spreading is not supported in this construct."
Error 3902, Line 4, Col 27, Line 4, Col 30, "Spreading is not supported in this construct."
Error 3902, Line 5, Col 27, Line 5, Col 30, "Spreading is not supported in this construct."
Error 3902, Line 5, Col 34, Line 5, Col 37, "Spreading is not supported in this construct."
Error 3902, Line 6, Col 27, Line 6, Col 30, "Spreading is not supported in this construct."
Error 3902, Line 7, Col 30, Line 7, Col 33, "Spreading is not supported in this construct."
]
[<Fact>]
let ``[| ... |] → error`` () =
let src =
"""
let xs = [1..10]
let _ = [| ... |]
let _ = [| ...xs |]
let _ = [| ...xs; ...xs |]
let _ = [| ...xs; 1 |]
let _ = [| 1; ...xs |]
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3902, Line 3, Col 28, Line 3, Col 31, "Spreading is not supported in this construct."
Error 3902, Line 4, Col 28, Line 4, Col 31, "Spreading is not supported in this construct."
Error 3902, Line 5, Col 28, Line 5, Col 31, "Spreading is not supported in this construct."
Error 3902, Line 5, Col 35, Line 5, Col 38, "Spreading is not supported in this construct."
Error 3902, Line 6, Col 28, Line 6, Col 31, "Spreading is not supported in this construct."
Error 3902, Line 7, Col 31, Line 7, Col 34, "Spreading is not supported in this construct."
]
// Spreads in anonymous record _types_ are not currently supported.
// This does differ from nominal record type definitions,
// but the added complexity to suport them here does not seem worthwhile.
[<Fact>]
let ``Spread in anonymous record type → error`` () =
let src =
"""
type NominalRecordTy = { A : int }
type AnonymousRecordTy = {| A : int |}
type Alias1 = {| ...NominalRecordTy |}
type Alias2 = {| ...AnonymousRecordTy |}
let f (x : {| ...NominalRecordTy |}) = ()
let g (x : {| ...AnonymousRecordTy |}) = ()
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3244, Line 5, Col 31, Line 5, Col 55, "Invalid anonymous record type"
Error 3244, Line 6, Col 31, Line 6, Col 57, "Invalid anonymous record type"
Error 3244, Line 8, Col 28, Line 8, Col 52, "Invalid anonymous record type"
Error 3244, Line 9, Col 28, Line 9, Col 54, "Invalid anonymous record type"
]
[<Fact>]
let ``new () = { ... } → error`` () =
let src =
"""
type R = { X : int }
let r = { X = 1 }
type C =
val X : int
new () = { ...r }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3902, Line 6, Col 32, Line 6, Col 35, "Spreading is not supported in this construct."
]

Record type spreads: set algebra

module Algebra =
/// No overlap, spread ⊕ field.
[<Fact>]
let ``{...{A,B},C} = {A,B}{C} = {A,B,C}`` () =
let src =
"""
type R1 = { A : int; B : int }
type R2 = { ...R1; C : int }
let _ : R2 = { A = 1; B = 2; C = 3 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// No overlap, spread from anonymous record ⊕ field.
[<Fact>]
let ``{...{|A,B|},C} = {A,B}{C} = {A,B,C}`` () =
let src =
"""
type R2 = { ...{| A : int; B : int |}; C : int }
let _ : R2 = { A = 1; B = 2; C = 3 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// No overlap, field ⊕ spread.
[<Fact>]
let ``{A,...{B,C}} = {A} ⊕ {B,C} = {A,B,C}`` () =
let src =
"""
type R1 = { B : int; C : int }
type R2 = { A : int; ...R1 }
let _ : R2 = { A = 1; B = 2; C = 3 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// No overlap, spread ⊕ spread.
[<Fact>]
let ``{...{A,B},...{C,D}} = {A,B} ⊕ {C,D} = {A,B,C,D}`` () =
let src =
"""
type R1 = { A : int; B : int }
type R2 = { C : int; D : int }
type R3 = { ...R1; ...R2 }
let _ : R3 = { A = 1; B = 2; C = 3; D = 4 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// Rightward explicit duplicate field shadows field from spread.
[<Fact>]
let ``{...{A₀,B},A₁} = {A₀,B} ⊕ {A₁} = {A₁,B,C}`` () =
let src =
"""
type R1 = { A : int; B : int }
type R2 = { ...R1; A : string }
let _ : R2 = { A = "1"; B = 2 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// Rightward spread field shadows leftward spread field.
[<Fact>]
let ``{...{A₀,B},...{A₁}} = {A₀,B} ⊕ {A₁} = {A₁,B,C}`` () =
let src =
"""
type R1 = { A : int; B : int }
type R2 = { A : string }
type R3 = { ...R1; ...R2 }
type R4 = { ...R2; ...R1 }
let _ : R3 = { A = "1"; B = 2 }
let _ : R4 = { A = 1; B = 2 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// Rightward spread field shadows leftward explicit field with warning.
[<Fact>]
let ``{A₀,...{A₁,B}} = {A₀} ⊕ {A₁,B} = {A₁_warn,B,C}`` () =
let src =
"""
type R1 = { A : int; B : int }
type R2 = { A : string; ...R1 }
let _ : R2 = { A = 1; B = 2 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withSingleDiagnostic (Warning 3897, Line 3, Col 45, Line 3, Col 50, "Spread field 'A: int' from type 'R1' shadows an explicitly declared field with the same name.")
/// Explicit duplicate fields remain disallowed.
[<Fact>]
let ``{A₀,...{A₁,B},A₂} = {A₀} ⊕ {A₁,B} ⊕ {A₂} = {A₁_warn,B,A₂_error}`` () =
let src =
"""
type R1 = { A : int; B : int }
type R2 = { A : string; ...R1; A : float }
let _ : R2 = { A = 1; B = 2 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Warning 3897, Line 3, Col 45, Line 3, Col 50, "Spread field 'A: int' from type 'R1' shadows an explicitly declared field with the same name."
Error 37, Line 3, Col 52, Line 3, Col 53, "Duplicate definition of field 'A'"
]
[<Fact>]
let ``No dupes allowed, multiple`` () =
let src =
"""
type R1 = { A : int; B : string }
type R2 = { A : decimal }
type R3 = { ...R2; A : string; ...R1; A : float }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Warning 3897, Line 4, Col 52, Line 4, Col 57, "Spread field 'A: int' from type 'R1' shadows an explicitly declared field with the same name."
Error 37, Line 4, Col 59, Line 4, Col 60, "Duplicate definition of field 'A'"
]

Record type spreads: accessibility

module Accessibility =
/// Fields should have the accessibility of the target type.
/// A spread from less to more accessible is valid as long as the less accessible
/// fields are accessible at the point of the spread.
[<Fact>]
let ``Accessibility comes from target`` () =
let src =
"""
open System.Reflection
module A =
type R = internal { A : int; B : int }
module B =
type T = { ...A.R }
let (|PropName|) (prop : PropertyInfo) = prop.Name
match typeof<B.T>.GetProperties() with
| [|PropName "A"; PropName "B"|] -> ()
| unexpected -> failwith $"Expected B.T to have public properties \"A\" and \"B\" but got %A{unexpected}."
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> compileExeAndRun
|> shouldSucceed

Record type spreads: mutability

module Mutability =
[<Fact>]
let ``Mutability is brought over`` () =
let src =
"""
type R1 = { A : int; mutable B : string }
type R2 = { ...R1 }
let r2 : R2 = { A = 1; B = "3" }
r2.B <- "99"
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed

Record type spreads: generic type parameters

module GenericTypeParameters =
[<Fact>]
let ``Single type parameter, inferred at usage`` () =
let src =
"""
type R1<'a> = { A : 'a; B : string }
type R2<'a> = { X : 'a; Y : string }
type R3<'a> = { ...R1<'a>; ...R2<'a> }
let _ : R3<_> = { A = 3; B = "lol"; X = 4; Y = "haha" }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``Single type parameter, inconsistent instantiation disallowed`` () =
let src =
"""
type R1<'a> = { A : 'a }
type R2<'a> = { B : 'a }
type R3<'a> = { ...R1<'a>; ...R2<'a> }
let _ : R3<int> = { A = 3; B = "lol" }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 1, Line 6, Col 52, Line 6, Col 57, "This expression was expected to have type
'int'
but here has type
'string' "
]
[<Fact>]
let ``Single type parameter, annotated at usage`` () =
let src =
"""
type R1<'a> = { A : 'a; B : string }
type R2<'a> = { X : 'a; Y : string }
type R3<'a> = { ...R1<'a>; ...R2<'a> }
let _ : R3<int> = { A = 3; B = "lol"; X = 4; Y = "haha" }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``Multiple type parameters`` () =
let src =
"""
type R1<'a> = { A : 'a; B : string }
type R2<'a> = { X : 'a; Y : string }
type R3<'a, 'b> = { ...R1<'a>; ...R2<'b> }
let _ : R3<_, _> = { A = 3; B = "lol"; X = 3.14; Y = "haha" }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``'a → 'a list`` () =
let src =
"""
type R1<'a> = { A : 'a }
type R2<'a> = { ...R1<'a list> }
let _ : R2<int> = { A = [3] }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``Single type parameter, not in scope, not allowed`` () =
let src =
"""
type R1<'a> = { A : 'a; B : string }
type R2<'a> = { X : 'a; Y : string }
type R3<'a> = { ...R1<'a>; ...R2<'b> }
type R4 = { ...R1<'a>; ...R2<'b> }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 39, Line 4, Col 54, Line 4, Col 56, "The type parameter 'b is not defined."
Error 39, Line 5, Col 39, Line 5, Col 41, "The type parameter 'a is not defined."
Error 39, Line 5, Col 50, Line 5, Col 52, "The type parameter 'b is not defined."
]
/// Akin to:
///
/// type R1<[<Measure>] 'a> = { A : int<'a> }
/// type R2<'a> = { X : R1<'a> }
[<Fact>]
let ``Measure attribute on source, required on spread destination`` () =
let src =
"""
type R1<[<Measure>] 'a> = { A : int<'a> }
type R2<'a> = { ...R1<'a> }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withSingleDiagnostic (Error 702, Line 3, Col 43, Line 3, Col 45, "Expected unit-of-measure parameter, not type parameter. Explicit unit-of-measure parameters must be marked with the [<Measure>] attribute.")
[<Fact>]
let ``Measure attribute on source, measure on spread destination, OK`` () =
let src =
"""
type R1<[<Measure>] 'a> = { A : int<'a> }
type R2<[<Measure>] 'b> = { ...R1<'b> }
type [<Measure>] m
type R3 = { ...R1<m> }
let _ : R1<m> = { A = 3<m> }
let _ : R2<m> = { A = 3<m> }
let _ : R3 = { A = 3<m> }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// Akin to:
///
/// type R1<'a when 'a : comparison> = { A : 'a }
/// type R2<'a> = { X : R1<'a> }
[<Fact>]
let ``Constraint on source, required on spread destination`` () =
let src =
"""
type R1<'a when 'a : comparison> = { A : 'a }
type R2<'a> = { ...R1<'a> }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withSingleDiagnostic (Error 1, Line 3, Col 40, Line 3, Col 46, "A type parameter is missing a constraint 'when 'a: comparison'")
[<Fact>]
let ``Constraint on source, required on spread destination, error if not compatible at usage`` () =
let src =
"""
type R1<'a when 'a : comparison> = { A : 'a list }
type R2<'a when 'a : comparison> = { ...R1<'a> }
let _ : R2<_> = { A = [obj ()] }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withSingleDiagnostic (Error 193, Line 5, Col 44, Line 5, Col 50, "The type 'obj' does not support the 'comparison' constraint. For example, it does not support the 'System.IComparable' interface")
[<Fact>]
let ``Constraint on source, constraint on spread destination, compatible at usage, OK`` () =
let src =
"""
type R1<'a when 'a : comparison> = { A : 'a }
type R2<'a when 'a : comparison> = { ...R1<'a> }
type R3<'a when 'a : comparison> = { ...R1<'a list> }
let _ : R1<int> = { A = 3 }
let _ : R2<int> = { A = 3 }
let _ : R3<int> = { A = [3] }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed

Record type spreads: non-record source (not allowed)

module NonRecordSource =
[<Fact>]
let ``{...class} → error`` () =
let src =
"""
type C () =
member _.A = 1
member _.B = 2
type R = { ...C }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withSingleDiagnostic (Error 3891, Line 6, Col 32, Line 6, Col 36, "The source type of a spread into a record type definition must itself be a nominal or anonymous record type.")
[<Fact>]
let ``{...abstract_class} → error`` () =
let src =
"""
[<AbstractClass>]
type C () =
abstract A : int
default _.A = 1
abstract B : int
default _.B = 2
type R = { ...C }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withSingleDiagnostic (Error 3891, Line 9, Col 32, Line 9, Col 36, "The source type of a spread into a record type definition must itself be a nominal or anonymous record type.")
[<Fact>]
let ``{...struct} → error`` () =
let src =
"""
[<Struct>]
type S =
member _.A = 1
member _.B = 2
type R = { ...S }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withSingleDiagnostic (Error 3891, Line 7, Col 32, Line 7, Col 36, "The source type of a spread into a record type definition must itself be a nominal or anonymous record type.")
[<Fact>]
let ``{...interface} → error`` () =
let src =
"""
type IFace =
abstract A : int
abstract B : int
type R = { ...IFace }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withSingleDiagnostic (Error 3891, Line 6, Col 32, Line 6, Col 40, "The source type of a spread into a record type definition must itself be a nominal or anonymous record type.")
[<Fact>]
let ``{...int} → error`` () =
let src =
"""
type R = { ...int }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withSingleDiagnostic (Error 3891, Line 2, Col 32, Line 2, Col 38, "The source type of a spread into a record type definition must itself be a nominal or anonymous record type.")
[<Fact>]
let ``{...(int -> int)} → error`` () =
let src =
"""
type R = { ...(int -> int) }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withSingleDiagnostic (Error 3891, Line 2, Col 32, Line 2, Col 47, "The source type of a spread into a record type definition must itself be a nominal or anonymous record type.")

Record type spreads: members other than record fields

module MembersOtherThanRecordFields =
[<Fact>]
let ``All members other than record fields are ignored`` () =
let src =
"""
open FSharp.Reflection
type R1 =
{ A : int
B : int }
member this.Lol = this.A + this.B
member _.Ha () = ()
static member X = "3"
static member val Y = 42
static member Q () = ()
[<AutoOpen>]
module R1Extensions =
type R1 with
member this.Lolol = this.Lol + this.Lol
type R2 = { ...R1; C : string }
match
FSharpType.GetRecordFields typeof<R2>
|> Array.map _.Name
with
| [|"A"; "B"; "C"|] -> ()
| unexpected -> failwith $"Expected R2 to have fields [|\"A\"; \"B\"|] but found %A{unexpected}."
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> compileExeAndRun
|> shouldSucceed

Record type spreads: recursion (cycles not allowed)

module Recursion =
[<Fact>]
let ``Simple mutually recursive type spreads → one error each`` () =
let src =
"""
module M
type A = { ...B }
and B = { ...A }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> compile
|> shouldFail
|> withDiagnostics [
Error 3901, Line 4, Col 26, Line 4, Col 27, "This type definition involves a cyclic reference through a spread."
Error 3901, Line 5, Col 26, Line 5, Col 27, "This type definition involves a cyclic reference through a spread."
]
[<Fact>]
let ``Mutually recursive type spreads → error`` () =
let src =
"""
type R = { A : int; ...S; B : int }
and S = { C : int; ...R; D : int }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3901, Line 2, Col 26, Line 2, Col 27, "This type definition involves a cyclic reference through a spread."
Error 3901, Line 3, Col 26, Line 3, Col 27, "This type definition involves a cyclic reference through a spread."
Warning 3897, Line 2, Col 41, Line 2, Col 45, "Spread field 'A: int' from type 'S' shadows an explicitly declared field with the same name."
]
[<Fact>]
let ``Mutually recursive type spreads with some indirection → error`` () =
let src =
"""
type R = { A : int; ...S }
and S = { B : int; ...T }
and T = { C : int; ...U }
and U = { D : int; ...R }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3901, Line 2, Col 26, Line 2, Col 27, "This type definition involves a cyclic reference through a spread."
Error 3901, Line 3, Col 26, Line 3, Col 27, "This type definition involves a cyclic reference through a spread."
Error 3901, Line 4, Col 26, Line 4, Col 27, "This type definition involves a cyclic reference through a spread."
Error 3901, Line 5, Col 26, Line 5, Col 27, "This type definition involves a cyclic reference through a spread."
Warning 3897, Line 2, Col 41, Line 2, Col 45, "Spread field 'A: int' from type 'S' shadows an explicitly declared field with the same name."
]
[<Fact>]
let ``Mutually recursive type spreads in recursive module → error`` () =
let src =
"""
module rec M
type R = { A : int; ...S; B : int }
type S = { C : int; ...R; D : int }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3901, Line 4, Col 26, Line 4, Col 27, "This type definition involves a cyclic reference through a spread."
Error 3901, Line 5, Col 26, Line 5, Col 27, "This type definition involves a cyclic reference through a spread."
Warning 3897, Line 4, Col 41, Line 4, Col 45, "Spread field 'A: int' from type 'S' shadows an explicitly declared field with the same name."
]
[<Fact>]
let ``Complex mutually recursive type spreads → error`` () =
let src =
"""
module rec M
[<AutoOpen>]
module N =
type R = { A : int; ...O.S }
module O =
type S = { B : int; ...T }
type T = { C : int; ...U }
[<AutoOpen>]
module P =
[<AutoOpen>]
module Q =
type U = { D : int; ...R }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3901, Line 6, Col 30, Line 6, Col 31, "This type definition involves a cyclic reference through a spread."
Error 3901, Line 9, Col 34, Line 9, Col 35, "This type definition involves a cyclic reference through a spread."
Error 3901, Line 11, Col 26, Line 11, Col 27, "This type definition involves a cyclic reference through a spread."
Error 3901, Line 17, Col 34, Line 17, Col 35, "This type definition involves a cyclic reference through a spread."
Warning 3897, Line 6, Col 45, Line 6, Col 51, "Spread field 'A: int' from type 'O.S' shadows an explicitly declared field with the same name."
]
[<Fact>]
let ``Mutually recursive type defns with spreads, no cycles → success`` () =
let src =
"""
module M =
type R = { α : int }
and S = { β : int }
and T = { γ : int }
and U = { δ : int }
type R = { A : int; ...M.S }
and S = { B : int; ...M.T }
and T = { C : int; ...M.U }
and U = { D : int; ...M.R }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``Mutually recursive type defns with spreads, reverse order → success`` () =
let src =
"""
module M
type R = { ...S }
and S = { α : int; β : int; }
let r : R = { α = 1; β = 2 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``Mutually recursive type defns with spreads, reverse order, transitive → success`` () =
let src =
"""
module M
type R = { ...S }
and S = { ...T }
and T = { α : int; β : int; }
let r : R = { α = 1; β = 2 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``Mutually recursive generic type defns with spreads, reverse order, transitive → success`` () =
let src =
"""
module M
type R<'T> = { ...S<'T> }
and S<'T> = { ...T<'T> }
and T<'T> = { α : 'T; β : 'T; }
let r : R<int> = { α = 1; β = 2 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``Mutually recursive type defns with spreads, reverse order, more complicated → success`` () =
let src =
"""
module M
type R = { α : int; ...S; δ : int }
and S = { β : int; γ : int }
let r : R = { α = 1; β = 2; γ = 3; δ = 4 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``Mutually recursive type defns with spreads, errors → not duplicated`` () =
let src =
"""
module M
type R = { α : int; ...S; δ : int; δ : int }
and S = { α : int; β : int; γ : int }
let r : R = { α = 1; β = 2; γ = 3; δ = 4 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 37, Line 4, Col 56, Line 4, Col 57, "Duplicate definition of field 'δ'"
Warning 3897, Line 4, Col 41, Line 4, Col 45, "Spread field 'α: int' from type 'S' shadows an explicitly declared field with the same name."
]

Record type spreads: nullability

module Nullability =
[<Fact>]
let ``Can't spread from a nullable type`` () =
let src =
"""
type R1 = { A : int }
type R2 = { ...(R1 | null) }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> withCheckNulls
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3892, Line 3, Col 33, Line 3, Col 47, "The source type of a spread into a record type definition cannot be nullable."
]

Record type spreads: allowed (but not required) in signature files

module Signatures =
[<Fact>]
let ``Can use spreads in signatures`` () =
let src =
"""
type R1 = { A : int }
type R2 = { ...R1; B : int }
type R3 = {| A : int |}
type R4 = { ...R1; B : int }
"""
Fsi src
|> withLangVersion SupportedLangVersion
|> withCheckNulls
|> typecheck
|> shouldSucceed

Record type spreads: structness depends on target type

module Structness =
[<Fact>]
let ``Structness depends only on the target type`` () =
let src =
"""
type [<Struct>] R1 = { A : int }
type R2 = { ...R1 }
type R3 = { A : int }
type [<Struct>] R4 = { ...R3 }
if typeof<R2>.IsValueType then
failwith "R2 should not be a struct type because it is not explicitly annotated as such."
if not typeof<R4>.IsValueType then
failwith "R4 should be a struct type because it is explicitly annotated as such."
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> compileExeAndRun
|> shouldSucceed

Anonymous record expression spreads: set algebra

module Algebra =
/// No overlap, spread ⊕ field.
[<Fact>]
let ``{...{A,B},C} = {A,B} ⊕ {C} = {A,B,C}`` () =
let src =
"""
let r1 = {| A = 1; B = 2 |}
let r2 : {| A : int ; B : int; C : int |} = {| ...r1; C = 3 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// No overlap, field ⊕ spread.
[<Fact>]
let ``{A,...{B,C}} = {A} ⊕ {B,C} = {A,B,C}`` () =
let src =
"""
let r1 = {| A = 1; B = 2 |}
let r2 : {| A : int ; B : int; C : int |} = {| C = 3; ...r1 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// No overlap, spread ⊕ spread.
[<Fact>]
let ``{...{A,B},...{C,D}} = {A,B} ⊕ {C,D} = {A,B,C,D}`` () =
let src =
"""
let r1 = {| A = 1 ; B = 2 |}
let r2 = {| C = 3; D = 4 |}
let r3 : {| A : int ; B : int; C : int; D : int |} = {| ...r1; ...r2 |}
let r4 : {| A : int ; B : int; C : int; D : int |} = {| ...r2; ...r3 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// Rightward explicit duplicate field shadows field from spread.
[<Fact>]
let ``{...{A₀,B},A₁} = {A₀,B} ⊕ {A₁} = {A₁,B,C}`` () =
let src =
"""
let r1 = {| A = 1; B = 2 |}
let r2 : {| A : string; B : int |} = {| ...r1; A = "A" |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// Rightward spread field shadows leftward spread field.
[<Fact>]
let ``{...{A₀,B},...{A₁}} = {A₀,B} ⊕ {A₁} = {A₁,B,C}`` () =
let src =
"""
let r1 = {| A = 1; B = 2 |}
let r2 = {| A = "A" |}
let r3 : {| A : string; B : int |} = {| ...r1; ...r2 |}
let r4 : {| A : int; B : int |} = {| ...r2; ...r1 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// Rightward spread field shadows leftward explicit field with warning.
[<Fact>]
let ``{A₀,...{A₁,B}} = {A₀} ⊕ {A₁,B} = {A₁_warn,B,C}`` () =
let src =
"""
let r1 = {| A = 1; B = 2 |}
let r2 : {| A : int; B : int |} = {| A = "A"; ...r1 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Warning 3898, Line 4, Col 67, Line 4, Col 72, "Spread field 'A: int' shadows an explicitly declared field with the same name."
]
/// Explicit duplicate fields remain disallowed.
[<Fact>]
let ``{A₀,...{A₁,B},A₂} = {A₀} ⊕ {A₁,B} ⊕ {A₂} = {A₁_warn,B,A₂_error}`` () =
let src =
"""
let r1 = {| A = 1; B = 2 |}
let r2 = {| A = "A"; ...r1; A = 3.14 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Warning 3898, Line 4, Col 42, Line 4, Col 47, "Spread field 'A: int' shadows an explicitly declared field with the same name."
Error 3522, Line 4, Col 49, Line 4, Col 57, "The field 'A' appears multiple times in this record expression."
]
[<Fact>]
let ``No dupes allowed, multiple`` () =
let src =
"""
let r1 = {| A = 1; B = "B" |}
let r2 = {| A = 3m |}
let r3 = {| ...r2; A = "A"; ...r1; A = 3.14 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Warning 3898, Line 5, Col 49, Line 5, Col 54, "Spread field 'A: int' shadows an explicitly declared field with the same name."
Error 3522, Line 5, Col 56, Line 5, Col 64, "The field 'A' appears multiple times in this record expression."
]
[<Fact>]
let ``{...{A,B,C}}:{B} = {A,B,C} ∩ {B} = {B}`` () =
let src =
"""
let src = {| A = 1; B = "B"; C = 3m |}
let typedTarget : {| B : string |} = {| ...src |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``{...{}} = ∅ ⊕ ∅ = ∅`` () =
let src =
"""
module M
let r = {| ...{||} |}
if r <> {||} then failwith $"Expected {{||}} but got %A{r}."
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> compileExeAndRun
|> shouldSucceed

Anonymous record expression spreads: accessibility

module Accessibility =
/// Fields should have the accessibility of the target type.
/// A spread from less to more accessible is valid as long as the less accessible
/// fields are accessible at the point of the spread.
[<Fact>]
let ``Accessibility comes from target`` () =
let src =
"""
let private r1 = {| A = 1; B = "B" |}
let public r2 : {| A : int; B : string |} = {| ...r1 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed

Anonymous record expression spreads: mutability

module Mutability =
[<Fact>]
let ``Mutability is _not_ brought over`` () =
let src =
"""
type R1 = { A : int; mutable B : string }
let r1 = { A = 1; B = "B" }
let r2 = {| ...r1 |}
r2.B <- "99"
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 799, Line 6, Col 24, Line 6, Col 25, "Invalid assignment"
]

Anonymous record expression spreads: generic type parameters

module GenericTypeParameters =
[<Fact>]
let ``Single type parameter`` () =
let src =
"""
let f (x : 'a) =
let r1 : {| A : 'a; B : string |} = {| A = x; B = "B" |}
let r2 : {| X : 'a; Y : string |} = {| X = x; Y = "Y" |}
let r3 : {| A : 'a; B : string; X : 'a; Y : string |} = {| ...r1; ...r2 |}
r3
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``Multiple type parameters`` () =
let src =
"""
let r1 (x : 'a) = {| A = x; B = "B" |}
let r2 (x : 'a) = {| X = x; Y = "Y" |}
let r3 (x : 'a) (y : 'b) : {| A : 'a; B : string; X : 'b; Y : string |} = {| ...r1 x; ...r2 y |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``Measure attribute on source, present on spread destination`` () =
let src =
"""
let r1 (r2 : {| A : int<'m> |}) : {| A : int<'m> |} = {| ...r2 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``Constraints kept`` () =
let src =
"""
let r1<'a when 'a : comparison> (r2 : {| A : 'a |}) : unit -> {| A : 'a |} = fun () -> {| ...r2 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed

Anonymous record expression spreads: non-record-source (not currently allowed)

module NonRecordSource =
[<Fact>]
let ``{...class} → error`` () =
let src =
"""
type C () =
member _.A = 1
member _.B = 2
let r = {| ...C () |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3895, Line 6, Col 35, Line 6, Col 39, "The source expression of a spread into an anonymous record expression must have a nominal or anonymous record type."
]
[<Fact>]
let ``{...abstract_class} → error`` () =
let src =
"""
[<AbstractClass>]
type C () =
abstract A : int
abstract B : int
let r =
{|
...
{ new C () with
member _.A = 1
member _.B = 2 }
|}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3895, Line 10, Col 33, Line 12, Col 53, "The source expression of a spread into an anonymous record expression must have a nominal or anonymous record type."
]
[<Fact>]
let ``{...struct} → error`` () =
let src =
"""
[<Struct>]
type S =
member _.A = 1
member _.B = 2
let r = {| ...S () |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3895, Line 7, Col 35, Line 7, Col 39, "The source expression of a spread into an anonymous record expression must have a nominal or anonymous record type."
]
[<Fact>]
let ``{...interface} → error`` () =
let src =
"""
type IFace =
abstract A : int
abstract B : int
let r =
{|
...
{ new IFace with
member _.A = 1
member _.B = 2 }
|}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3895, Line 9, Col 33, Line 11, Col 53, "The source expression of a spread into an anonymous record expression must have a nominal or anonymous record type."
]
[<Fact>]
let ``{...int} → error`` () =
let src =
"""
let r = {| ...0 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3895, Line 2, Col 35, Line 2, Col 36, "The source expression of a spread into an anonymous record expression must have a nominal or anonymous record type."
]
[<Fact>]
let ``{...(int -> int)} → error`` () =
let src =
"""
let r = {| ...(fun x -> x + 1) |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3895, Line 2, Col 35, Line 2, Col 51, "The source expression of a spread into an anonymous record expression must have a nominal or anonymous record type."
]
[<Fact>]
let ``{...int list} → error`` () =
let src =
"""
let r = {| ...[1..10] |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3895, Line 2, Col 35, Line 2, Col 42, "The source expression of a spread into an anonymous record expression must have a nominal or anonymous record type."
]

Anonymous record expression spreads: non-field members on record sources are ignored

module MembersOtherThanRecordFields =
[<Fact>]
let ``Instance properties that are not record fields are ignored`` () =
let src =
"""
type R1 =
{ A : int
B : string }
member this.Lol = string this.A + this.B
type R2 = { ...R1; C : string }
let r1 = { A = 3; B = "3"; C = "asdf" }
let r2 : {| A : int; B : string; C : string |} = {| ...r1 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``All members other than record fields are ignored`` () =
let src =
"""
type R1 =
{ A : int
B : int }
member this.Lol = this.A + this.B
member _.Ha () = ()
static member X = "3"
static member val Y = 42
static member Q () = ()
[<AutoOpen>]
module R1Extensions =
type R1 with
member this.Lolol = this.Lol + this.Lol
type R2 = { ...R1; C : string }
let r2 : R2 = { A = 3; B = 3; C = "asdf" }
let r3 = {| ...r2 |}
let typeofR3 = r3.GetType ()
if typeofR3 <> typeof<{| A : int; B : int; C : string |}> then
failwith $"Expected r3 to have type {{| A : int; B : int; C : string |}} but got {typeofR3.Name}."
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> compileExeAndRun
|> shouldSucceed

Anonymous record expression spreads: effects

module Effects =
[<Fact>]
let ``Effects in spread sources are evaluated exactly once per spread, even if all fields are shadowed`` () =
let src =
"""
let effects = ResizeArray ()
let f () = effects.Add "f"; {| A = 0; B = 1 |}
let g () = effects.Add "g"; {| A = 2; B = 3 |}
let h () = effects.Add "h"; {| A = 99 |}
let r = {| ...g (); ...f (); ...g (); ...h (); A = 100 |}
let expected = {| A = 100; B = 3 |}
if r <> expected then failwith $"Expected %A{expected} but got %A{r}."
match List.ofSeq effects with
| ["g"; "f"; "g"; "h"] -> ()
| unexpected -> failwith $"Expected [\"g\"; \"f\"; \"g\"; \"h\"] but got %A{unexpected}."
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> compileExeAndRun
|> shouldSucceed

Anonymous record expression spreads: conversions and coercions

module Conversions =
[<Fact>]
let ``Coercions work as though they were field assignments`` () =
let src =
"""
let r1 = {| A = 3; B = 4 |}
let r2 : {| A : obj; B : obj |} = {| ...r1 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``Implicit conversions work as though they were field assignments`` () =
let src =
"""
[<NoEquality; NoComparison; DefaultAugmentation(false)>]
type T =
| T of int
static member op_Implicit (T t) = U t
and [<NoEquality; NoComparison; DefaultAugmentation(false)>] U =
| U of int
#nowarn 3391
let r1 : {| A : T |} = {| A = T 3 |}
let r2 : {| A : U |} = {| A = T 3 |}
let r2' : {| A : U |} = {| ...r1 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed

Anonymous record expression spreads: nullability

module Nullability =
[<Fact>]
let ``Can't spread from a nullable value`` () =
let src =
"""
let r1 : {| A : int |} | null = null
let r2 = {| ...r1 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> withCheckNulls
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3260, Line 2, Col 30, Line 2, Col 50, "The type '{| A: int |}' does not support a nullness qualification."
Error 43, Line 2, Col 53, Line 2, Col 57, "The type '{| A: int |}' does not have 'null' as a proper value"
]

Anonymous record expression spreads: inference

module Inference =
[<Fact>]
let ``Unknown source type → error`` () =
let src =
"""
let f x = {| x with B = 2; C = 3 |}
let g x = {| ...x; B = 2; C = 3 |}
let h x : {| A : int; B : int; C : int |} = {| ...x; B = 2; C = 3 |}
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> compile
|> shouldFail
|> withDiagnostics [
Error 3245, Line 2, Col 34, Line 2, Col 35, "The input to a copy-and-update expression that creates an anonymous record must be either an anonymous record or a record"
Error 3895, Line 3, Col 37, Line 3, Col 38, "The source expression of a spread into an anonymous record expression must have a nominal or anonymous record type."
Error 3895, Line 4, Col 71, Line 4, Col 72, "The source expression of a spread into an anonymous record expression must have a nominal or anonymous record type."
Error 1, Line 4, Col 65, Line 4, Col 89, "This anonymous record is missing field 'A'."
]

Anonymous record expression spreads: structness

module Structness =
[<Fact>]
let ``Various structness combinations work`` () =
let src =
"""
type RefNominalRecd = { A : int }
type [<Struct>] StructNominalRecd = { A : int }
let refAnonRecd = {| A = 1 |}
let structAnonRecd = struct {| A = 1 |}
let refNominalRecd : RefNominalRecd = { A = 1 }
let structNominalRecd : StructNominalRecd = { A = 1 }
let ``ref anon src, no explicit target, stays ref`` = {| ...refAnonRecd; B = 2 |}
let ``ref anon src, explicit struct target, becomes struct`` = struct {| ...refAnonRecd; B = 2 |}
let ``ref anon src, inferred struct target, becomes struct`` : struct {| A : int; B : int |} = {| ...refAnonRecd; B = 2 |}
let ``struct anon src, no explicit target, stays struct`` = {| ...structAnonRecd; B = 2 |}
let ``struct anon src, explicit struct target, stays struct`` = struct {| ...structAnonRecd; B = 2 |}
let ``struct anon src, inferred struct target, stays struct`` : struct {| A : int; B : int |} = {| ...structAnonRecd; B = 2 |}
let ``ref nominal src, no explicit target, stays ref`` = {| ...refAnonRecd; B = 2 |}
let ``ref nominal src, explicit struct target, becomes struct`` = struct {| ...refAnonRecd; B = 2 |}
let ``ref nominal src, inferred struct target, becomes struct`` : struct {| A : int; B : int |} = {| ...refAnonRecd; B = 2 |}
let ``struct nominal src, no explicit target, stays struct`` = {| ...structAnonRecd; B = 2 |}
let ``struct nominal src, explicit struct target, stays struct`` = struct {| ...structAnonRecd; B = 2 |}
let ``struct nominal src, inferred struct target, stays struct`` : struct {| A : int; B : int |} = {| ...structAnonRecd; B = 2 |}
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> compileExeAndRun
|> shouldSucceed

Anonymous record expression spreads: nested updates

module NestedUpdates =
[<Fact>]
let ``Nested update with no preceding spread: error`` () =
let src =
"""
let orig () = {| Nested = {| A = "value1"; B = "value1" |} |}
let _ = {| Nested.A = "value2"; Nested.B = "value2"; ...orig () |}
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> compile
|> shouldFail
|> withDiagnostics [
Error 39, Line 4, Col 32, Line 4, Col 38, "The namespace or module 'Nested' is not defined."
]
[<Fact>]
let ``Nested update with no matching preceding spread: error`` () =
let src =
"""
let orig () = {| Nested = {| A = "value1"; B = "value1" |} |}
let _ = {| ...orig (); Other.A = "value2" |}
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> compile
|> shouldFail
|> withDiagnostics [
Error 39, Line 4, Col 44, Line 4, Col 49, "The namespace or module 'Other' is not defined."
]
[<Fact>]
let ``Nested updates apply to last matching spread: anynonymous to anynonymous`` () =
let src =
"""
let orig1 () = {| Nested = {| A = "value1"; B = "value1" |}; Other = {| A = "value2"; B = "value2" |} |}
let orig2 () = {| Nested = {| A = "value3"; B = "value3" |} |}
let actual = {| ...orig1 (); Nested.B = "value4"; ...orig2 (); Other.B = "value5" |}
let expected = {| Nested = {| A = "value3"; B = "value3" |}; Other = {| A = "value2"; B = "value5" |} |}
if actual <> expected then
failwith $"Expected %A{expected} but got %A{actual}."
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> ignoreWarnings
|> compileExeAndRun
|> shouldSucceed
|> withDiagnostics [
Warning 3898, Line 5, Col 71, Line 5, Col 82, "Spread field 'Nested: {| A: string; B: string |}' shadows an explicitly declared field with the same name."
]
[<Fact>]
let ``Nested updates apply to last matching spread: nominal to anonymous`` () =
let src =
"""
type NestedRecord = { A : string; B : string }
type OuterRecord1 = { Nested : NestedRecord; Other : NestedRecord }
type OuterRecord2 = { Nested : NestedRecord }
let orig1 () = { Nested = { A = "value1"; B = "value1" }; Other = { A = "value2"; B = "value2" } }
let orig2 () = { Nested = { A = "value3"; B = "value3" } }
let actual = {| ...orig1 (); Nested.B = "value4"; ...orig2 (); Other.B = "value5" |}
let expected = {| Nested = { A = "value3"; B = "value3" }; Other = { A = "value2"; B = "value5" } |}
if actual <> expected then
failwith $"Expected %A{expected} but got %A{actual}."
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> ignoreWarnings
|> compileExeAndRun
|> shouldSucceed
|> withDiagnostics [
Warning 3898, Line 9, Col 71, Line 9, Col 82, "Spread field 'Nested: NestedRecord' shadows an explicitly declared field with the same name."
]
[<Fact>]
let ``We assume any qualified field assignment following any spreads to be nested updates`` () =
let src =
"""
let r1 = {| A = {| B = 1; C = {| D = 2 |} |} |}
let r2 = {| ...r1; A.C.D = 3 |}
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> ignoreWarnings
|> compile
|> shouldSucceed
[<Fact>]
let ``Ambiguity in qualified field assignment following spreads doesn't matter when the target type is an anonymous record`` () =
let src =
"""
module A =
type C = { D : int }
let r1 = {| A = {| B = 1; C = {| D = 2 |} |} |}
let r2 = {| ...r1; A.C.D = 3 |}
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> ignoreWarnings
|> compile
|> shouldSucceed

Nominal record expression spreads: set algebra

module Algebra =
/// No overlap, spread ⊕ field.
[<Fact>]
let ``{...{A,B},C} = {A,B} ⊕ {C} = {A,B,C}`` () =
let src =
"""
type R1 = { A : int; B : int }
type R2 = { A : int; B : int; C : int }
let r1 = { A = 1; B = 2 }
let r2 = { ...r1; C = 3 }
let r1' = {| A = 1; B = 2 |}
let r2' = { ...r1; C = 3 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// No overlap, field ⊕ spread.
[<Fact>]
let ``{A,...{B,C}} = {A} ⊕ {B,C} = {A,B,C}`` () =
let src =
"""
type R1 = { B : int; C : int }
type R2 = { A : int; B : int; C : int }
let r1 = { B = 1; C = 2 }
let r2 = { A = 3; ...r1 }
let r1' = {| B = 1; C = 2 |}
let r2' = { A = 3; ...r1 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// No overlap, spread ⊕ spread.
[<Fact>]
let ``{...{A,B},...{C,D}} = {A,B} ⊕ {C,D} = {A,B,C,D}`` () =
let src =
"""
type R1 = { A : int; B : int }
type R2 = { C : int; D : int }
type R3 = { A : int; B : int; C : int; D : int }
let r1 = { A = 1; B = 2 }
let r2 = { C = 3; D = 4 }
let r3 = { ...r1; ...r2 }
let r3' = { ...r2; ...r3 }
let r1' = {| A = 1; B = 2 |}
let r2' = {| C = 3; D = 4 |}
let r3'' = { ...r1; ...r2 }
let r3''' = { ...r2; ...r3 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
/// Rightward explicit duplicate field shadows field from spread.
[<Fact>]
let ``{...{A₀,B},A₁} = {A₀,B} ⊕ {A₁} = {A₁,B,C}`` () =
let src =
"""
module M
type R1 = { A : int; B : int }
let r1 = { A = 1; B = 2 }
let r1' = { ...r1; A = 99 }
if r1'.A <> 99 then failwith $"Expected r1'.A = 99 but got %A{r1'.A}."
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> compileExeAndRun
|> shouldSucceed
/// Rightward spread field shadows leftward spread field.
[<Fact>]
let ``{...{A₀,B},...{A₁}} = {A₀,B} ⊕ {A₁} = {A₁,B,C}`` () =
let src =
"""
module M
type R1 = { A : int; B : int }
let r1 = { A = 1; B = 2 }
let r1' = { ...r1; ...{| A = 99 |} }
if r1'.A <> 99 then failwith $"Expected r1'.A = 99 but got %A{r1'.A}."
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> compileExeAndRun
|> shouldSucceed
/// Rightward spread field shadows leftward explicit field with warning.
[<Fact>]
let ``{A₀,...{A₁,B}} = {A₀} ⊕ {A₁,B} = {A₁_warn,B,C}`` () =
let src =
"""
type R1 = { A : int; B : int }
let r1 = { A = 1; B = 2 }
let r1' = { A = 0; ...r1 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withSingleDiagnostic (Warning 3898, Line 5, Col 40, Line 5, Col 45, "Spread field 'A: int' shadows an explicitly declared field with the same name.")
/// Explicit duplicate fields remain disallowed.
[<Fact>]
let ``{A₀,...{A₁,B},A₂} = {A₀} ⊕ {A₁,B} ⊕ {A₂} = {A₁_warn,B,A₂_error}`` () =
let src =
"""
type R1 = { A : int; B : int }
let r1 = { A = 1; B = 2; A = 3; ...{| A = 4 |}; A = 5 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 668, Line 4, Col 46, Line 4, Col 51, "The field 'A' appears multiple times in this record expression or pattern"
Warning 3898, Line 4, Col 53, Line 4, Col 67, "Spread field 'A: int' shadows an explicitly declared field with the same name."
Error 668, Line 4, Col 69, Line 4, Col 74, "The field 'A' appears multiple times in this record expression or pattern"
]
/// Extra fields are ignored.
[<Fact>]
let ``{...{A,B,C}}:{B} = {A,B,C} ∩ {B} = {B}`` () =
let src =
"""
type R1 = { A : int; B : int; C : int }
type R2 = { B : int }
let r1 = { A = 1; B = 2; C = 3 }
let r2 : R2 = { ...r1 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed

Nominal record expression spreads: accessibility

module Accessibility =
/// Fields should have the accessibility of the target type.
/// A spread from less to more accessible is valid as long as the less accessible
/// fields are accessible at the point of the spread.
[<Fact>]
let ``Accessibility comes from target`` () =
let src =
"""
type private R1 = { A : int; B : string }
type public R2 = { ...R1 }
let private r1 = { A = 1; B = "2" }
let public r2 : R2 = { ...r1 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed

Nominal record expression spreads: non-record source (not currently allowed)

module NonRecordSource =
[<Fact>]
let ``{...class} → error`` () =
let src =
"""
type C () =
member _.A = 1
member _.B = 2
type R = { A : int }
let r : R = { ...C () }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3893, Line 8, Col 35, Line 8, Col 42, "The source expression of a spread into a nominal record expression must have a nominal or anonymous record type."
Error 764, Line 8, Col 33, Line 8, Col 44, "No assignment given for field 'A' of type 'Test.R'"
]
[<Fact>]
let ``{...abstract_class} → error`` () =
let src =
"""
[<AbstractClass>]
type C () =
abstract A : int
abstract B : int
type R = { A : int }
let r : R =
{
...
{ new C () with
member _.A = 1
member _.B = 2 }
}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3893, Line 11, Col 29, Line 14, Col 53, "The source expression of a spread into a nominal record expression must have a nominal or anonymous record type."
Error 764, Line 10, Col 25, Line 15, Col 26, "No assignment given for field 'A' of type 'Test.R'"
]
[<Fact>]
let ``{...struct} → error`` () =
let src =
"""
[<Struct>]
type S =
member _.A = 1
member _.B = 2
type R = { A : int }
let r : R = { ...S () }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3893, Line 9, Col 35, Line 9, Col 42, "The source expression of a spread into a nominal record expression must have a nominal or anonymous record type."
Error 764, Line 9, Col 33, Line 9, Col 44, "No assignment given for field 'A' of type 'Test.R'"
]
[<Fact>]
let ``{...interface} → error`` () =
let src =
"""
type IFace =
abstract A : int
abstract B : int
type R = { A : int }
let r : R =
{
...
{ new IFace with
member _.A = 1
member _.B = 2 }
}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3893, Line 10, Col 29, Line 13, Col 53, "The source expression of a spread into a nominal record expression must have a nominal or anonymous record type."
Error 764, Line 9, Col 25, Line 14, Col 26, "No assignment given for field 'A' of type 'Test.R'"
]
[<Fact>]
let ``{...int} → error`` () =
let src =
"""
type R = { A : int }
let r : R = { ...int }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3893, Line 4, Col 35, Line 4, Col 41, "The source expression of a spread into a nominal record expression must have a nominal or anonymous record type."
Error 764, Line 4, Col 33, Line 4, Col 43, "No assignment given for field 'A' of type 'Test.R'"
]
[<Fact>]
let ``{...(int -> int)} → error`` () =
let src =
"""
type R = { A : int }
let r = { ...(fun x -> x + 1) }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withSingleDiagnostic (Error 3893, Line 4, Col 31, Line 4, Col 50, "The source expression of a spread into a nominal record expression must have a nominal or anonymous record type.")

Nominal record expression spreads: non-field members on record sources are ignored

module MembersOtherThanRecordFields =
[<Fact>]
let ``Instance properties that are not record fields are ignored`` () =
let src =
"""
type R1 =
{ A : int
B : string }
member this.Lol = string this.A + this.B
type R2 = { ...R1; C : string }
let _ : R2 = { A = 3; B = "3"; C = "asdf" }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``All members other than record fields are ignored`` () =
let src =
"""
type R1 =
{ A : int
B : int }
member this.Lol = this.A + this.B
member _.Ha () = ()
static member X = "3"
static member val Y = 42
static member Q () = ()
type R2 = { ...R1; C : string }
let r2 : R2 = { A = 3; B = 3; C = "asdf" }
ignore r2.Lol // Should not exist.
r2.Ha () // Should not exist.
ignore R2.Y // Should not exist.
R2.Q () // Should not exist.
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 39, Line 14, Col 31, Line 14, Col 34, "The type 'R2' does not define a field, constructor, or member named 'Lol'."
Error 39, Line 15, Col 24, Line 15, Col 26, "The type 'R2' does not define a field, constructor, or member named 'Ha'."
Error 39, Line 16, Col 31, Line 16, Col 32, "The type 'R2' does not define a field, constructor, or member named 'Y'."
Error 39, Line 17, Col 24, Line 17, Col 25, "The type 'R2' does not define a field, constructor, or member named 'Q'."
]

Nominal record expression spreads: effects

module Effects =
[<Fact>]
let ``Effects in spread sources are evaluated exactly once per spread, even if all fields are shadowed`` () =
let src =
"""
type R = { A : int; B : int }
let effects = ResizeArray ()
let f () = effects.Add "f"; { A = 0; B = 1 }
let g () = effects.Add "g"; { A = 2; B = 3 }
let h () = effects.Add "h"; {| A = 99 |}
let r = { ...g (); ...f (); ...g (); ...h (); A = 100 }
let expected = { A = 100; B = 3 }
if r <> expected then failwith $"Expected %A{expected} but got %A{r}."
match List.ofSeq effects with
| ["g"; "f"; "g"; "h"] -> ()
| unexpected -> failwith $"Expected [\"g\"; \"f\"; \"g\"; \"h\"] but got %A{unexpected}."
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> compileExeAndRun
|> shouldSucceed

Nominal record expression spreads: conversions and coercions

module Conversions =
[<Fact>]
let ``Coercions work as though they were field assignments`` () =
let src =
"""
type R1 = { A : int; B : string }
[<NoComparison; NoEquality>]
type R2 = { A : obj; B : obj }
let r1 = { A = 3; B = "4" }
let r2 : R2 = { ...r1 }
let r1' = {| A = 3; B = "4" |}
let r3 : R2 = { ...r1' }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> typecheck
|> shouldSucceed
[<Fact>]
let ``Implicit conversions work as though they were field assignments`` () =
let src =
"""
type T =
| T of int
static member op_Implicit (T t) = U t
and U =
| U of int
type R1 = { A : T }
type R2 = { A : U }
let r1 : R1 = { A = T 3 }
let r2 : R2 = { A = T 3 }
let r3 : R2 = { ...r1 }
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> ignoreWarnings
|> typecheck
|> shouldSucceed
|> withDiagnostics [
Warning 3391, Line 13, Col 41, Line 13, Col 44, """This expression uses the implicit conversion 'static member T.op_Implicit: T -> U' to convert type 'T' to type 'U'. See https://aka.ms/fsharp-implicit-convs. This warning may be disabled using '#nowarn "3391"."""
Warning 3391, Line 14, Col 35, Line 14, Col 44, """This expression uses the implicit conversion 'static member T.op_Implicit: T -> U' to convert type 'T' to type 'U'. See https://aka.ms/fsharp-implicit-convs. This warning may be disabled using '#nowarn "3391"."""
]

Nominal record expression spreads: nullability

module Nullability =
[<Fact>]
let ``Can't spread from a nullable value`` () =
let src =
"""
type R = { A : int}
let r1 : R | null = null
let r2 : R = { ...r1 }
let r2' : {| A : int |} = {| ...r1 |}
"""
FSharp src
|> withLangVersion SupportedLangVersion
|> withCheckNulls
|> typecheck
|> shouldFail
|> withDiagnostics [
Error 3894, Line 4, Col 36, Line 4, Col 41, "The source expression of a spread into a nominal record expression cannot be nullable."
Error 764, Line 4, Col 34, Line 4, Col 43, "No assignment given for field 'A' of type 'Test.R'"
Error 3896, Line 5, Col 50, Line 5, Col 55, "The source expression of a spread into an anonymous record expression cannot be nullable."
Error 1, Line 5, Col 47, Line 5, Col 58, "This anonymous record is missing field 'A'."
]

Nominal record expression spreads: target type inference

module Inference =
[<Fact>]
let ``No target type specified, no additional fields, target type inferred to be same as spread source type`` () =
let src =
"""
type R1 = { A : int; B : int }
type R2 = { A : int; B : int; C : int }
let r1 = { A = 1; B = 2 }
let anon1 = {| A = 1; B = 2 |}
let r1InferredFromR1 = { ...r1 }
let r1InferredFromAnon = { ...anon1 }
let r2 = { A = 1; B = 2; C = 3 }
let anon2 = {| A = 1; B = 2; C = 3 |}
let r2InferredFromR2 = { ...r2 }
let r2InferredFromAnon = { ...anon2 }
let ``type of r1InferredFromR1`` = r1InferredFromR1.GetType ()
if ``type of r1InferredFromR1`` <> typeof<R1> then
failwith $"Expected r1InferredFromR1 to have type R1 but got {``type of r1InferredFromR1``.Name}."
let ``type of r1InferredFromAnon`` = r1InferredFromAnon.GetType ()
if ``type of r1InferredFromAnon`` <> typeof<R1> then
failwith $"Expected r1InferredFromAnon to have type R1 but got {``type of r1InferredFromAnon``.Name}."
let ``type of r2InferredFromR2`` = r2InferredFromR2.GetType ()
if ``type of r2InferredFromR2`` <> typeof<R2> then
failwith $"Expected r2InferredFromR2 to have type R2 but got {``type of r2InferredFromR2``.Name}."
let ``type of r2InferredFromAnon`` = r2InferredFromAnon.GetType ()
if ``type of r2InferredFromAnon`` <> typeof<R2> then
failwith $"Expected r2InferredFromAnon to have type R2 but got {``type of r2InferredFromAnon``.Name}."
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> compileExeAndRun
|> shouldSucceed
[<Fact>]
let ``Unknown source type, nominal record type in scope error`` () =
let src =
"""
type R = { A : int; B : int; C : int }
let f x = { x with B = 2; C = 3 } // No error; x is inferred to have type R, because source and target type must be the same.
let g x = { ...x; B = 2; C = 3 } // Error; we do not force the source to have the same type as the target.
let h x : R = { ...x; B = 2; C = 3 } // Error; we do not force the source to have the same type as the target.
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> compile
|> shouldFail
|> withDiagnostics [
Error 3893, Line 5, Col 33, Line 5, Col 37, "The source expression of a spread into a nominal record expression must have a nominal or anonymous record type."
Error 764, Line 5, Col 31, Line 5, Col 53, "No assignment given for field 'A' of type 'Test.R'"
Error 3893, Line 6, Col 37, Line 6, Col 41, "The source expression of a spread into a nominal record expression must have a nominal or anonymous record type."
Error 764, Line 6, Col 35, Line 6, Col 57, "No assignment given for field 'A' of type 'Test.R'"
]

Nominal record expression spreads: structness

module Structness =
[<Fact>]
let ``Various structness combinations work`` () =
let src =
"""
type RefNominalRecd = { A : int; B : int }
type [<Struct>] StructNominalRecd = { A : int; B : int }
let refAnonRecd = {| A = 1; B = 2 |}
let structAnonRecd = struct {| A = 1; B = 2 |}
let refNominalRecd : RefNominalRecd = { A = 1; B = 2 }
let structNominalRecd : StructNominalRecd = { A = 1; B = 2 }
let ``ref nominal src, ref nominal dst`` : RefNominalRecd = { ...refNominalRecd; B = 3 }
let ``ref nominal src, struct nominal dst`` : StructNominalRecd = { ...refNominalRecd; B = 3 }
let ``struct nominal src, ref nominal dst`` : RefNominalRecd = { ...structNominalRecd; B = 3 }
let ``struct nominal src, struct nominal dst`` : StructNominalRecd = { ...structNominalRecd; B = 3 }
let ``ref anon src, ref nominal dst`` : RefNominalRecd = { ...refAnonRecd; B = 3 }
let ``ref anon src, struct nominal dst`` : StructNominalRecd = { ...refAnonRecd; B = 3 }
let ``struct anon src, ref nominal dst`` : RefNominalRecd = { ...structAnonRecd; B = 3 }
let ``struct anon src, struct nominal dst`` : StructNominalRecd = { ...structAnonRecd; B = 3 }
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> compileExeAndRun
|> shouldSucceed

Nominal record expression spreads: nested updates

module NestedUpdates =
[<Fact>]
let ``Nested update with no preceding spread: error`` () =
let src =
"""
type NestedRecord = { A : string; B : string }
type OuterRecord = { Nested : NestedRecord }
let orig () = { Nested = { A = "value1"; B = "value1" } }
let _ = { Nested.A = "value2"; Nested.B = "value2"; ...orig () }
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> compile
|> shouldFail
|> withDiagnostics [
Error 39, Line 7, Col 31, Line 7, Col 37, "The namespace or module 'Nested' is not defined."
Error 39, Line 7, Col 52, Line 7, Col 58, "The namespace or module 'Nested' is not defined."
]
[<Fact>]
let ``Nested update with no matching preceding spread: error`` () =
let src =
"""
type NestedRecord = { A : string; B : string }
type OuterRecord = { Nested : NestedRecord }
let orig () = { Nested = { A = "value1"; B = "value1" } }
let _ = { ...orig (); Other.A = "value2" }
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> compile
|> shouldFail
|> withDiagnostics [
Error 39, Line 7, Col 43, Line 7, Col 48, "The namespace or module 'Other' is not defined."
]
[<Fact>]
let ``Nested updates apply to last matching spread: nominal to nominal`` () =
let src =
"""
type NestedRecord = { A : string; B : string }
type OuterRecord1 = { Nested : NestedRecord; Other : NestedRecord }
type OuterRecord2 = { Nested : NestedRecord }
let orig1 () = { Nested = { A = "value1"; B = "value1" }; Other = { A = "value2"; B = "value2" } }
let orig2 () = { Nested = { A = "value3"; B = "value3" } }
let actual = { ...orig1 (); Nested.B = "value4"; ...orig2 (); Other.B = "value5" }
let expected = { Nested = { A = "value3"; B = "value3" }; Other = { A = "value2"; B = "value5" } }
if actual <> expected then
failwith $"Expected %A{expected} but got %A{actual}."
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> ignoreWarnings
|> compileExeAndRun
|> shouldSucceed
|> withDiagnostics [
Warning 3898, Line 9, Col 70, Line 9, Col 81, "Spread field 'Nested: NestedRecord' shadows an explicitly declared field with the same name."
]
[<Fact>]
let ``Nested updates apply to last matching spread: anonymous to nominal`` () =
let src =
"""
type NestedRecord = { A : string; B : string }
type OuterRecord = { Nested : NestedRecord; Other : NestedRecord }
let orig1 () = {| Nested = { A = "value1"; B = "value1" }; Other = { A = "value2"; B = "value2" } |}
let orig2 () = {| Nested = { A = "value3"; B = "value3" } |}
let actual = { ...orig1 (); Nested.B = "value4"; ...orig2 (); Other.B = "value5" }
let expected = { Nested = { A = "value3"; B = "value3" }; Other = { A = "value2"; B = "value5" } }
if actual <> expected then
failwith $"Expected %A{expected} but got %A{actual}."
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> ignoreWarnings
|> compileExeAndRun
|> shouldSucceed
|> withDiagnostics [
Warning 3898, Line 8, Col 70, Line 8, Col 81, "Spread field 'Nested: NestedRecord' shadows an explicitly declared field with the same name."
]
[<Fact>]
let ``We assume any qualified field assignment following any spreads to be nested updates`` () =
let src =
"""
type Inner = { D : int }
type Middle = { B : int; C : Inner }
type Outer = { A : Middle }
let r1 = { A = { B = 1; C = { D = 2 } } }
let r2 = { ...r1; A.C.D = 3 }
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> ignoreWarnings
|> compile
|> shouldSucceed
[<Fact>]
let ``Ambiguity in qualified field assignment following spreads in inferred expression leads to error because it would affect target type`` () =
let src =
"""
type Inner = { D : int }
type Middle = { B : int; C : Inner }
type Outer = { A : Middle }
module A =
type C = { D : int }
let r1 = { A = { B = 1; C = { D = 2 } } }
let r2 = { ...r1; A.C.D = 3 }
"""
Fsx src
|> withLangVersion SupportedLangVersion
|> ignoreWarnings
|> compile
|> shouldFail
|> withDiagnostics [
Error 656, Line 9, Col 30, Line 9, Col 50, "This record contains fields from inconsistent types"
]

@github-actions

github-actions Bot commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

❗ Release notes required

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


✅ Found changes and release notes in following paths:

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
LanguageFeatures.fsi docs/release-notes/.Language/preview.md

@nojaf

nojaf commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

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

Comment thread src/Compiler/Facilities/LanguageFeatures.fs
@dsyme

dsyme commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

@brianrourkeboll Great to see work starting in this direction!

Comment thread src/Compiler/Checking/Expressions/CheckExpressions.fs Outdated
Comment thread src/Compiler/Checking/NameResolution.fs Outdated
@brianrourkeboll

Copy link
Copy Markdown
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

T-Gro commented Oct 1, 2025

Copy link
Copy Markdown
Member

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 )

@brianrourkeboll

Copy link
Copy Markdown
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.

@T-Gro

T-Gro commented Nov 10, 2025

Copy link
Copy Markdown
Member

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

This is fine - we would only need a split if there is anything hinting a different decision path, such as ilimport, dependency on attributes, or ilxgen. Right now there is neither of that.

We might later want an overall end to end scenario just as a smoke test for the desktop compiler, which is what is running in Visual Studio - more as a proof of overall stability.
(right now the combination of compiler tfm and target tfm is coupled when it comes to ComponentTests)

@brianrourkeboll brianrourkeboll left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ha, these can go back.

Comment thread tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/Unmanaged.fs Outdated
Comment thread tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/Unmanaged.fs Outdated
Comment thread src/Compiler/Checking/CheckDeclarations.fs Outdated
Comment thread src/Compiler/SyntaxTree/SyntaxTree.fs
Comment thread src/Compiler/SyntaxTree/SyntaxTree.fs Outdated
Comment thread src/Compiler/SyntaxTree/SyntaxTreeOps.fs
Comment thread src/Compiler/SyntaxTree/SyntaxTreeOps.fs Outdated
Comment thread src/Compiler/pars.fsy Outdated
Comment thread src/Compiler/Checking/CheckDeclarations.fs Outdated
Comment thread src/Compiler/Checking/Expressions/CheckExpressions.fs Outdated
@brianrourkeboll
brianrourkeboll force-pushed the record-spreads branch 4 times, most recently from 86ff984 to 4817822 Compare June 13, 2026 17:29
@brianrourkeboll
brianrourkeboll force-pushed the record-spreads branch 6 times, most recently from 04e303a to 2f57763 Compare July 10, 2026 20:38
@brianrourkeboll

Copy link
Copy Markdown
Contributor Author

@T-Gro

Those are failing tests on the service layer (https://gist.github.com/T-Gro/5e2df1271a16db22b714aaad403c31f0), can be applied using:

gh gist view 5e2df1271a16db22b714aaad403c31f0 -f spread_service_tests.diff | git apply

In this test, I don't think I understand why the two synthesized fields should have distinct declaration locations. The current declaration location for both is ...R1. Is that not correct? Are you saying R2.A and R2.B should have the declaration locations of R1.A and R1.B instead?

[<Fact>]
let ``spread - synthesized fields have distinct DeclarationLocations`` () =
    let _, checkResults =
        getParseAndCheckResultsPreview """
type R1 = { A : int; B : int }
type R2 = { ...R1; C : int }
"""
    let r2 = findSymbolByName "R2" checkResults :?> FSharpEntity
    let r2A = r2.FSharpFields |> Seq.find (fun f -> f.Name = "A")
    let r2B = r2.FSharpFields |> Seq.find (fun f -> f.Name = "B")
    if getRangeCoords r2A.DeclarationLocation = getRangeCoords r2B.DeclarationLocation then
        failwith $"Expected spread-synthesized fields A and B to have distinct DeclarationLocations, but both are %A{getRangeCoords r2A.DeclarationLocation}."

@brianrourkeboll
brianrourkeboll force-pushed the record-spreads branch 3 times, most recently from 95fcfda to 63d606a Compare July 24, 2026 10:48
@brianrourkeboll
brianrourkeboll marked this pull request as ready for review July 24, 2026 10:49
@brianrourkeboll
brianrourkeboll requested a review from a team as a code owner July 24, 2026 10:49
@brianrourkeboll brianrourkeboll changed the title Spread operator { ...expr }, { ...ty } for records Record spreads: { ...expr }, { ...ty } Jul 24, 2026
Comment thread src/Compiler/FSComp.txt Outdated
@brianrourkeboll
brianrourkeboll force-pushed the record-spreads branch 4 times, most recently from f344df1 to 1bc322b Compare July 28, 2026 12:10
@brianrourkeboll

Copy link
Copy Markdown
Contributor Author

@dsyme, @auduchinok: this and the RFC are ready for review, if you are interested in weighing in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

6 participants