Skip to content

Commit

Permalink
C# collection expression support for F# lists & sets (#17359)
Browse files Browse the repository at this point in the history
  • Loading branch information
brianrourkeboll authored Jul 16, 2024
1 parent 26c053f commit 3125875
Show file tree
Hide file tree
Showing 15 changed files with 396 additions and 19 deletions.
2 changes: 1 addition & 1 deletion buildtools/AssemblyCheck/SkipVerifyEmbeddedPdb.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ FSharp.Test.Utilities.dll
FSharp.Compiler.Private.Scripting.UnitTests.dll
FSharp.Compiler.Service.Tests.dll
FSharp.Core.UnitTests.dll
FSharpSuite.Tests.dll
FSharpSuite.Tests.dll
2 changes: 1 addition & 1 deletion docs/release-notes/.FSharp.Core/8.0.400.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@

### Breaking Changes

* Fixed argument exception throwing inconsistency - accessing an out-of-bounds collection index will now throw `ArgumentOutOfRangeException` instead of `ArgumentException` ([#17328](https://github.com/dotnet/fsharp/pull/17328))
* Fixed argument exception throwing inconsistency - accessing an out-of-bounds collection index will now throw `ArgumentOutOfRangeException` instead of `ArgumentException` ([#17328](https://github.com/dotnet/fsharp/pull/17328))
2 changes: 2 additions & 0 deletions docs/release-notes/.FSharp.Core/9.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Added

* Enable C# collection expression support for F# lists & sets. ([Language suggestion #1355](https://github.com/fsharp/fslang-suggestions/issues/1355), [RFC FS-1145 (PR#776)](https://github.com/fsharp/fslang-design/pull/776), [PR #17359](https://github.com/dotnet/fsharp/pull/17359))

### Changed
* Change compiler default setting realsig+ when building assemblies ([Issue #17384](https://github.com/dotnet/fsharp/issues/17384), [PR #17378](https://github.com/dotnet/fsharp/pull/17385))
* Change compiler default setting for compressedMetadata ([Issue #17379](https://github.com/dotnet/fsharp/issues/17379), [PR #17383](https://github.com/dotnet/fsharp/pull/17383))
Expand Down
36 changes: 36 additions & 0 deletions src/FSharp.Core/prim-types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,9 @@ namespace Microsoft.FSharp.Core

open BasicInlinedOperations

// This exists solely so that it can be used in the CollectionBuilderAttribute on List<'T> in prim-types.fsi.
module internal TypeOfUtils =
let inline typeof<'T> = typeof<'T>

module TupleUtils =

Expand Down Expand Up @@ -4069,6 +4072,23 @@ namespace Microsoft.FSharp.Core

and 'T voption = ValueOption<'T>

// These attributes only exist in .NET 8 and up.
namespace System.Runtime.CompilerServices
open System
open Microsoft.FSharp.Core

[<Sealed>]
[<AttributeUsage(AttributeTargets.Class ||| AttributeTargets.Struct ||| AttributeTargets.Interface, Inherited = false)>]
type internal CollectionBuilderAttribute (builderType: Type, methodName: string) =
inherit Attribute ()
member _.BuilderType = builderType
member _.MethodName = methodName

[<Sealed>]
[<AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)>]
type internal ScopedRefAttribute () =
inherit Attribute ()

namespace Microsoft.FSharp.Collections

//-------------------------------------------------------------------------
Expand All @@ -4086,6 +4106,9 @@ namespace Microsoft.FSharp.Collections
open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicFunctions
open Microsoft.FSharp.Core.BasicInlinedOperations

#if NETSTANDARD2_1_OR_GREATER
[<System.Runtime.CompilerServices.CollectionBuilder(typeof<List>, "Create")>]
#endif
[<DefaultAugmentation(false)>]
[<DebuggerTypeProxyAttribute(typedefof<ListDebugView<_>>)>]
[<DebuggerDisplay("{DebugDisplay,nq}")>]
Expand All @@ -4111,6 +4134,19 @@ namespace Microsoft.FSharp.Collections

and 'T list = List<'T>

#if NETSTANDARD2_1_OR_GREATER
and [<CompilerMessage("This type is for compiler use and should not be used directly", 1204, IsHidden=true);
Sealed;
AbstractClass;
CompiledName("FSharpList")>] List =
[<CompilerMessage("This method is for compiler use and should not be used directly", 1204, IsHidden=true)>]
static member Create([<System.Runtime.CompilerServices.ScopedRef>] items: System.ReadOnlySpan<'T>) =
let mutable list : 'T list = []
for i = items.Length - 1 downto 0 do
list <- items[i] :: list
list
#endif

//-------------------------------------------------------------------------
// List (debug view)
//-------------------------------------------------------------------------
Expand Down
58 changes: 57 additions & 1 deletion src/FSharp.Core/prim-types.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -1238,6 +1238,10 @@ namespace Microsoft.FSharp.Core
/// <category>ByRef and Pointer Types</category>
type outref<'T> = byref<'T, ByRefKinds.Out>

// This exists solely so that it can be used in the CollectionBuilderAttribute on List<'T> below.
module internal TypeOfUtils =
val inline typeof<'T>: Type

/// <summary>Language primitives associated with the F# language</summary>
///
/// <category index="9">Language Primitives</category>
Expand Down Expand Up @@ -2578,12 +2582,46 @@ namespace Microsoft.FSharp.Core
/// Represents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
| Error of ErrorValue:'TError

// These attributes only exist in .NET 8 and up.
namespace System.Runtime.CompilerServices
open System
open Microsoft.FSharp.Core

[<Sealed>]
[<AttributeUsage(AttributeTargets.Class ||| AttributeTargets.Struct ||| AttributeTargets.Interface, Inherited = false)>]
type internal CollectionBuilderAttribute =
inherit Attribute

/// <summary>Initialize the attribute to refer to the <paramref name="methodName"/> method on the <paramref name="builderType"/> type.</summary>
/// <param name="builderType">The type of the builder to use to construct the collection.</param>
/// <param name="methodName">The name of the method on the builder to use to construct the collection.</param>
/// <remarks>
/// <paramref name="methodName"/> must refer to a static method that accepts a single parameter of
/// type <see cref="T:System.ReadOnlySpan`1"/> and returns an instance of the collection being built containing
/// a copy of the data from that span. In future releases of .NET, additional patterns may be supported.
/// </remarks>
new: builderType: Type * methodName: string -> CollectionBuilderAttribute

/// <summary>Gets the type of the builder to use to construct the collection.</summary>
member BuilderType: Type

/// <summary>Gets the name of the method on the builder to use to construct the collection.</summary>
/// <remarks>This should match the metadata name of the target method. For example, this might be ".ctor" if targeting the type's constructor.</remarks>
member MethodName: string

[<Sealed>]
[<AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)>]
type internal ScopedRefAttribute =
inherit Attribute
new: unit -> ScopedRefAttribute

namespace Microsoft.FSharp.Collections

open System
open System.Collections
open System.Collections.Generic
open Microsoft.FSharp.Core
open Microsoft.FSharp.Core.TypeOfUtils

/// <summary>The type of immutable singly-linked lists.</summary>
///
Expand All @@ -2593,6 +2631,9 @@ namespace Microsoft.FSharp.Collections
/// </remarks>
///
/// <exclude />
#if NETSTANDARD2_1_OR_GREATER
[<System.Runtime.CompilerServices.CollectionBuilder(typeof<List>, "Create")>]
#endif
[<DefaultAugmentation(false)>]
[<StructuralEquality; StructuralComparison>]
[<CompiledName("FSharpList`1")>]
Expand Down Expand Up @@ -2646,7 +2687,7 @@ namespace Microsoft.FSharp.Collections
///
/// <returns>The list with head appended to the front of tail.</returns>
static member Cons: head: 'T * tail: 'T list -> 'T list

interface IEnumerable<'T>
interface IEnumerable
interface IReadOnlyCollection<'T>
Expand All @@ -2664,6 +2705,21 @@ namespace Microsoft.FSharp.Collections
/// </remarks>
and 'T list = List<'T>

#if NETSTANDARD2_1_OR_GREATER
/// <summary>Contains methods for compiler use related to lists.</summary>
and [<CompilerMessage("This type is for compiler use and should not be used directly", 1204, IsHidden=true);
Sealed;
AbstractClass;
CompiledName("FSharpList")>] List =
/// <summary>Creates a list with the specified items.</summary>
///
/// <param name="items">The items to store in the list.</param>
///
/// <returns>A list containing the specified items.</returns>
[<CompilerMessage("This method is for compiler use and should not be used directly", 1204, IsHidden=true)>]
static member Create: [<System.Runtime.CompilerServices.ScopedRef>] items: System.ReadOnlySpan<'T> -> 'T list
#endif

/// <summary>An abbreviation for the CLI type <see cref="T:System.Collections.Generic.List`1"/></summary>
type ResizeArray<'T> = System.Collections.Generic.List<'T>

Expand Down
19 changes: 19 additions & 0 deletions src/FSharp.Core/set.fs
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,9 @@ module internal SetTree =
let ofArray comparer l =
Array.fold (fun acc k -> add comparer k acc) empty l

#if NETSTANDARD2_1_OR_GREATER
[<System.Runtime.CompilerServices.CollectionBuilder(typeof<Set>, "Create")>]
#endif
[<Sealed>]
[<CompiledName("FSharpSet`1")>]
[<DebuggerTypeProxy(typedefof<SetDebugView<_>>)>]
Expand Down Expand Up @@ -1023,6 +1026,22 @@ type Set<[<EqualityConditionalOn>] 'T when 'T: comparison>(comparer: IComparer<'
.Append("; ... ]")
.ToString()

#if NETSTANDARD2_1_OR_GREATER
and [<CompilerMessage("This type is for compiler use and should not be used directly", 1204, IsHidden = true);
Sealed;
AbstractClass;
CompiledName("FSharpSet")>] Set =
[<CompilerMessage("This method is for compiler use and should not be used directly", 1204, IsHidden = true)>]
static member Create([<System.Runtime.CompilerServices.ScopedRef>] items: System.ReadOnlySpan<'T>) =
let comparer = LanguagePrimitives.FastGenericComparer<'T>
let mutable acc = SetTree.empty

for item in items do
acc <- SetTree.add comparer item acc

Set(comparer, acc)
#endif

and [<Sealed>] SetDebugView<'T when 'T: comparison>(v: Set<'T>) =

[<DebuggerBrowsable(DebuggerBrowsableState.RootHidden)>]
Expand Down
18 changes: 18 additions & 0 deletions src/FSharp.Core/set.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ open Microsoft.FSharp.Collections
/// <remarks>See the <see cref="T:Microsoft.FSharp.Collections.SetModule"/> module for further operations on sets.
///
/// All members of this class are thread-safe and may be used concurrently from multiple threads.</remarks>
#if NETSTANDARD2_1_OR_GREATER
[<System.Runtime.CompilerServices.CollectionBuilder(typeof<Set>, "Create")>]
#endif
[<Sealed>]
[<CompiledName("FSharpSet`1")>]
type Set<[<EqualityConditionalOn>] 'T when 'T: comparison> =
Expand Down Expand Up @@ -233,6 +236,21 @@ type Set<[<EqualityConditionalOn>] 'T when 'T: comparison> =
interface IReadOnlyCollection<'T>
override Equals: obj -> bool

#if NETSTANDARD2_1_OR_GREATER
/// <summary>Contains methods for compiler use related to sets.</summary>
and [<CompilerMessage("This type is for compiler use and should not be used directly", 1204, IsHidden = true);
Sealed;
AbstractClass;
CompiledName("FSharpSet")>] Set =
/// <summary>Creates a set with the specified items.</summary>
///
/// <param name="items">The items to store in the set.</param>
///
/// <returns>A set containing the specified items.</returns>
[<CompilerMessage("This method is for compiler use and should not be used directly", 1204, IsHidden = true)>]
static member Create: [<System.Runtime.CompilerServices.ScopedRef>] items: System.ReadOnlySpan<'T> -> Set<'T>
#endif

namespace Microsoft.FSharp.Collections

open System
Expand Down
4 changes: 2 additions & 2 deletions tests/AheadOfTime/Trimming/check.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function CheckTrim($root, $tfm, $outputfile, $expected_len) {
# error NETSDK1124: Trimming assemblies requires .NET Core 3.0 or higher.

# Check net7.0 trimmed assemblies
CheckTrim -root "SelfContained_Trimming_Test" -tfm "net8.0" -outputfile "FSharp.Core.dll" -expected_len 284672
CheckTrim -root "SelfContained_Trimming_Test" -tfm "net8.0" -outputfile "FSharp.Core.dll" -expected_len 285184

# Check net8.0 trimmed assemblies
CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net8.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 8818176
CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net8.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 8818688
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ Microsoft.FSharp.Collections.ComparisonIdentity: System.Collections.Generic.ICom
Microsoft.FSharp.Collections.ComparisonIdentity: System.Collections.Generic.IComparer`1[T] NonStructural$W[T](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean]], Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean]])
Microsoft.FSharp.Collections.ComparisonIdentity: System.Collections.Generic.IComparer`1[T] NonStructural[T]()
Microsoft.FSharp.Collections.ComparisonIdentity: System.Collections.Generic.IComparer`1[T] Structural[T]()
Microsoft.FSharp.Collections.FSharpList: Microsoft.FSharp.Collections.FSharpList`1[T] Create[T](System.ReadOnlySpan`1[T])
Microsoft.FSharp.Collections.FSharpList`1+Tags[T]: Int32 Cons
Microsoft.FSharp.Collections.FSharpList`1+Tags[T]: Int32 Empty
Microsoft.FSharp.Collections.FSharpList`1[T]: Boolean Equals(Microsoft.FSharp.Collections.FSharpList`1[T])
Expand Down Expand Up @@ -274,6 +275,7 @@ Microsoft.FSharp.Collections.FSharpMap`2[TKey,TValue]: System.String ToString()
Microsoft.FSharp.Collections.FSharpMap`2[TKey,TValue]: TValue Item [TKey]
Microsoft.FSharp.Collections.FSharpMap`2[TKey,TValue]: TValue get_Item(TKey)
Microsoft.FSharp.Collections.FSharpMap`2[TKey,TValue]: Void .ctor(System.Collections.Generic.IEnumerable`1[System.Tuple`2[TKey,TValue]])
Microsoft.FSharp.Collections.FSharpSet: Microsoft.FSharp.Collections.FSharpSet`1[T] Create[T](System.ReadOnlySpan`1[T])
Microsoft.FSharp.Collections.FSharpSet`1[T]: Boolean Contains(T)
Microsoft.FSharp.Collections.FSharpSet`1[T]: Boolean Equals(System.Object)
Microsoft.FSharp.Collections.FSharpSet`1[T]: Boolean IsEmpty
Expand Down
1 change: 1 addition & 0 deletions tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
<Compile Include="FSharp.Core\Microsoft.FSharp.Control\EventModule.fs" />
<Compile Include="FSharp.Core\Microsoft.FSharp.Reflection\FSharpReflection.fs" />
<Compile Include="FSharp.Core\Microsoft.FSharp.Quotations\FSharpQuotations.fs" />
<Compile Include="Interop\CSharpCollectionExpressions.fs" />
<Compile Include="StructTuples.fs" />
<Compile Include="SurfaceArea.fs" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,3 +347,16 @@ type ListType() =
Assert.AreEqual(lst.[-3..(-4)], ([]: int list))
Assert.AreEqual(lst.[-4..(-3)], ([]: int list))

#if NET8_0_OR_GREATER

#nowarn "1204" // FS1204: This type/method is for compiler use and should not be used directly.

/// Tests for methods on the static, non-generic List type.
module FSharpList =
[<Fact>]
let ``List.Create creates a list from a ReadOnlySpan`` () =
let expected = [1..10]
let span = ReadOnlySpan [|1..10|]
let actual = List.Create span
Assert.Equal<int list>(expected, actual)
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -331,3 +331,17 @@ type SetType() =
Assert.AreEqual(sec.MaximumElement, 7)
Assert.AreEqual(Set.maxElement fir, 6)
Assert.AreEqual(Set.maxElement sec, 7)

#if NET8_0_OR_GREATER

#nowarn "1204" // FS1204: This type/method is for compiler use and should not be used directly.

/// Tests for methods on the static, non-generic Set type.
module FSharpSet =
[<Fact>]
let ``Set.Create creates a set from a ReadOnlySpan`` () =
let expected = set [1..10]
let span = ReadOnlySpan [|1..10|]
let actual = Set.Create span
Assert.Equal<Set<int>>(expected, actual)
#endif
Loading

0 comments on commit 3125875

Please sign in to comment.