From 3125875f3bd39ef45a61f7fb759c517c88408d9f Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Tue, 16 Jul 2024 05:31:42 -0400 Subject: [PATCH] C# collection expression support for F# lists & sets (#17359) --- .../AssemblyCheck/SkipVerifyEmbeddedPdb.txt | 2 +- docs/release-notes/.FSharp.Core/8.0.400.md | 2 +- docs/release-notes/.FSharp.Core/9.0.100.md | 2 + src/FSharp.Core/prim-types.fs | 36 +++ src/FSharp.Core/prim-types.fsi | 58 ++++- src/FSharp.Core/set.fs | 19 ++ src/FSharp.Core/set.fsi | 18 ++ tests/AheadOfTime/Trimming/check.ps1 | 4 +- ...Core.SurfaceArea.netstandard21.release.bsl | 2 + .../FSharp.Core.UnitTests.fsproj | 1 + .../Microsoft.FSharp.Collections/ListType.fs | 13 ++ .../Microsoft.FSharp.Collections/SetType.fs | 14 ++ .../Interop/CSharpCollectionExpressions.fs | 216 ++++++++++++++++++ tests/FSharp.Test.Utilities/Compiler.fs | 7 +- tests/FSharp.Test.Utilities/CompilerAssert.fs | 21 +- 15 files changed, 396 insertions(+), 19 deletions(-) create mode 100644 tests/FSharp.Core.UnitTests/Interop/CSharpCollectionExpressions.fs diff --git a/buildtools/AssemblyCheck/SkipVerifyEmbeddedPdb.txt b/buildtools/AssemblyCheck/SkipVerifyEmbeddedPdb.txt index 2961d6963f2..7af2a7f58cb 100644 --- a/buildtools/AssemblyCheck/SkipVerifyEmbeddedPdb.txt +++ b/buildtools/AssemblyCheck/SkipVerifyEmbeddedPdb.txt @@ -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 \ No newline at end of file +FSharpSuite.Tests.dll diff --git a/docs/release-notes/.FSharp.Core/8.0.400.md b/docs/release-notes/.FSharp.Core/8.0.400.md index a92b5213d8e..cd6f26bd981 100644 --- a/docs/release-notes/.FSharp.Core/8.0.400.md +++ b/docs/release-notes/.FSharp.Core/8.0.400.md @@ -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)) \ No newline at end of file +* 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)) diff --git a/docs/release-notes/.FSharp.Core/9.0.100.md b/docs/release-notes/.FSharp.Core/9.0.100.md index 7050bf1a666..eb8a8e5b052 100644 --- a/docs/release-notes/.FSharp.Core/9.0.100.md +++ b/docs/release-notes/.FSharp.Core/9.0.100.md @@ -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)) diff --git a/src/FSharp.Core/prim-types.fs b/src/FSharp.Core/prim-types.fs index d95659a14f2..2474dfcd39c 100644 --- a/src/FSharp.Core/prim-types.fs +++ b/src/FSharp.Core/prim-types.fs @@ -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 = @@ -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 + + [] + [] + type internal CollectionBuilderAttribute (builderType: Type, methodName: string) = + inherit Attribute () + member _.BuilderType = builderType + member _.MethodName = methodName + + [] + [] + type internal ScopedRefAttribute () = + inherit Attribute () + namespace Microsoft.FSharp.Collections //------------------------------------------------------------------------- @@ -4086,6 +4106,9 @@ namespace Microsoft.FSharp.Collections open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicFunctions open Microsoft.FSharp.Core.BasicInlinedOperations +#if NETSTANDARD2_1_OR_GREATER + [, "Create")>] +#endif [] [>)>] [] @@ -4111,6 +4134,19 @@ namespace Microsoft.FSharp.Collections and 'T list = List<'T> +#if NETSTANDARD2_1_OR_GREATER + and [] List = + [] + static member Create([] 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) //------------------------------------------------------------------------- diff --git a/src/FSharp.Core/prim-types.fsi b/src/FSharp.Core/prim-types.fsi index cb8c890171c..44a870853a6 100644 --- a/src/FSharp.Core/prim-types.fsi +++ b/src/FSharp.Core/prim-types.fsi @@ -1238,6 +1238,10 @@ namespace Microsoft.FSharp.Core /// ByRef and Pointer Types 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 + /// Language primitives associated with the F# language /// /// Language Primitives @@ -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 + + [] + [] + type internal CollectionBuilderAttribute = + inherit Attribute + + /// Initialize the attribute to refer to the method on the type. + /// The type of the builder to use to construct the collection. + /// The name of the method on the builder to use to construct the collection. + /// + /// must refer to a static method that accepts a single parameter of + /// type 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. + /// + new: builderType: Type * methodName: string -> CollectionBuilderAttribute + + /// Gets the type of the builder to use to construct the collection. + member BuilderType: Type + + /// Gets the name of the method on the builder to use to construct the collection. + /// This should match the metadata name of the target method. For example, this might be ".ctor" if targeting the type's constructor. + member MethodName: string + + [] + [] + 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 /// The type of immutable singly-linked lists. /// @@ -2593,6 +2631,9 @@ namespace Microsoft.FSharp.Collections /// /// /// +#if NETSTANDARD2_1_OR_GREATER + [, "Create")>] +#endif [] [] [] @@ -2646,7 +2687,7 @@ namespace Microsoft.FSharp.Collections /// /// The list with head appended to the front of tail. static member Cons: head: 'T * tail: 'T list -> 'T list - + interface IEnumerable<'T> interface IEnumerable interface IReadOnlyCollection<'T> @@ -2664,6 +2705,21 @@ namespace Microsoft.FSharp.Collections /// and 'T list = List<'T> +#if NETSTANDARD2_1_OR_GREATER + /// Contains methods for compiler use related to lists. + and [] List = + /// Creates a list with the specified items. + /// + /// The items to store in the list. + /// + /// A list containing the specified items. + [] + static member Create: [] items: System.ReadOnlySpan<'T> -> 'T list +#endif + /// An abbreviation for the CLI type type ResizeArray<'T> = System.Collections.Generic.List<'T> diff --git a/src/FSharp.Core/set.fs b/src/FSharp.Core/set.fs index b47bef257c1..4efc3fa92df 100644 --- a/src/FSharp.Core/set.fs +++ b/src/FSharp.Core/set.fs @@ -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 +[, "Create")>] +#endif [] [] [>)>] @@ -1023,6 +1026,22 @@ type Set<[] 'T when 'T: comparison>(comparer: IComparer<' .Append("; ... ]") .ToString() +#if NETSTANDARD2_1_OR_GREATER +and [] Set = + [] + static member Create([] 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 [] SetDebugView<'T when 'T: comparison>(v: Set<'T>) = [] diff --git a/src/FSharp.Core/set.fsi b/src/FSharp.Core/set.fsi index 58615cedaa9..b9ba1042945 100644 --- a/src/FSharp.Core/set.fsi +++ b/src/FSharp.Core/set.fsi @@ -13,6 +13,9 @@ open Microsoft.FSharp.Collections /// See the module for further operations on sets. /// /// All members of this class are thread-safe and may be used concurrently from multiple threads. +#if NETSTANDARD2_1_OR_GREATER +[, "Create")>] +#endif [] [] type Set<[] 'T when 'T: comparison> = @@ -233,6 +236,21 @@ type Set<[] 'T when 'T: comparison> = interface IReadOnlyCollection<'T> override Equals: obj -> bool +#if NETSTANDARD2_1_OR_GREATER +/// Contains methods for compiler use related to sets. +and [] Set = + /// Creates a set with the specified items. + /// + /// The items to store in the set. + /// + /// A set containing the specified items. + [] + static member Create: [] items: System.ReadOnlySpan<'T> -> Set<'T> +#endif + namespace Microsoft.FSharp.Collections open System diff --git a/tests/AheadOfTime/Trimming/check.ps1 b/tests/AheadOfTime/Trimming/check.ps1 index c9699c66c1b..028c39a7929 100644 --- a/tests/AheadOfTime/Trimming/check.ps1 +++ b/tests/AheadOfTime/Trimming/check.ps1 @@ -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 diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl index 92d85ccd98d..ea111e99abf 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl @@ -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]) @@ -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 diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj b/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj index 9acbcef398e..93143ca4103 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj @@ -90,6 +90,7 @@ + diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ListType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ListType.fs index 653f9b3a40b..f0eb9ff83ec 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ListType.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ListType.fs @@ -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 = + [] + 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(expected, actual) +#endif diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/SetType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/SetType.fs index ac3c213be80..f4141416d10 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/SetType.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/SetType.fs @@ -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 = + [] + 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>(expected, actual) +#endif diff --git a/tests/FSharp.Core.UnitTests/Interop/CSharpCollectionExpressions.fs b/tests/FSharp.Core.UnitTests/Interop/CSharpCollectionExpressions.fs new file mode 100644 index 00000000000..99f40530703 --- /dev/null +++ b/tests/FSharp.Core.UnitTests/Interop/CSharpCollectionExpressions.fs @@ -0,0 +1,216 @@ +module FSharp.Core.UnitTests.Interop.CSharp.CollectionExpressions + +// These tests require types available only in netstandard2.1 and up. +#if NET8_0_OR_GREATER + +open FSharp.Test +open FSharp.Test.Compiler +open Xunit + +/// Usings for the C# tests. +/// These must be prepended to each test's C# source text. +let csUsings = + """ + using System; + using System.Linq; + using Microsoft.FSharp.Collections; + + #nullable enable + """ + +/// Utility types and functions for the C# tests. +/// These must be appended to each test's C# source text. +let csUtils = + """ + public sealed record RecordClass(int X) : IComparable + { + public int CompareTo(object? obj) => obj switch + { + null => 1, + RecordClass(var otherX) => X.CompareTo(otherX), + _ => throw new ArgumentException("Invalid comparison.", nameof(obj)) + }; + } + + public readonly record struct RecordStruct(int X) : IComparable + { + public int CompareTo(object? obj) => obj switch + { + null => 1, + RecordStruct(var otherX) => X.CompareTo(otherX), + _ => throw new ArgumentException("Invalid comparison.", nameof(obj)) + }; + } + + public sealed class EqualException(string message) : Exception(message) { } + + public sealed class TrueException(string message) : Exception(message) { } + + public static class Assert + { + public static void Equal(T expected, T actual) + { + switch ((expected, actual)) + { + case (null, null): return; + case (null, not null): + case (not null, null): + case var _ when !expected.Equals(actual): throw new EqualException($"Expected '{expected}' but got '{actual}'."); + } + } + + public static void True(bool b) + { + if (!b) + { + throw new TrueException("Expected true but got false."); + } + } + } + """ + +[] +let ``FSharpList: can create using C# collection expression`` () = + CSharp $$""" + {{csUsings}} + + var expected = ListModule.OfArray([1, 2, 3]); + FSharpList actual = [1, 2, 3]; + + Assert.Equal(expected, actual); + Assert.Equal(expected.Length, actual.Length); + + for (var i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], actual[i]); + } + + Assert.True(actual is [1, 2, 3]); + + {{csUtils}} + """ + |> withCSharpLanguageVersion CSharpLanguageVersion.CSharp12 + |> withName "Test" + |> compileExeAndRun + |> shouldSucceed + +[] +let ``FSharpList: can create using C# collection expression`` () = + CSharp $$""" + {{csUsings}} + + var expected = ListModule.OfArray([new RecordClass(1), new RecordClass(2), new RecordClass(3)]); + FSharpList actual = [new RecordClass(1), new RecordClass(2), new RecordClass(3)]; + + Assert.Equal(expected, actual); + Assert.Equal(expected.Length, actual.Length); + + for (var i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], actual[i]); + } + + Assert.True(actual is [RecordClass(1), RecordClass(2), RecordClass(3)]); + + {{csUtils}} + """ + |> withCSharpLanguageVersion CSharpLanguageVersion.CSharp12 + |> withName "Test" + |> compileExeAndRun + |> shouldSucceed + +[] +let ``FSharpList: can create using C# collection expression`` () = + CSharp $$""" + {{csUsings}} + + var expected = ListModule.OfArray([new RecordStruct(1), new RecordStruct(2), new RecordStruct(3)]); + FSharpList actual = [new RecordStruct(1), new RecordStruct(2), new RecordStruct(3)]; + + Assert.Equal(expected, actual); + Assert.Equal(expected.Length, actual.Length); + + for (var i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], actual[i]); + } + + Assert.True(actual is [RecordStruct(1), RecordStruct(2), RecordStruct(3)]); + + {{csUtils}} + """ + |> withCSharpLanguageVersion CSharpLanguageVersion.CSharp12 + |> withName "Test" + |> compileExeAndRun + |> shouldSucceed + +[] +let ``FSharpSet: can create using C# collection expression`` () = + CSharp $$""" + {{csUsings}} + + var expected = SetModule.OfArray([1, 2, 3]); + FSharpSet actual = [1, 2, 3]; + + Assert.Equal(expected, actual); + Assert.Equal(expected.Count, actual.Count); + + foreach (var (e, a) in expected.Zip(actual)) + { + Assert.Equal(e, a); + } + + {{csUtils}} + """ + |> withCSharpLanguageVersion CSharpLanguageVersion.CSharp12 + |> withName "Test" + |> compileExeAndRun + |> shouldSucceed + +[] +let ``FSharpSet: can create using C# collection expression`` () = + CSharp $$""" + {{csUsings}} + + var expected = SetModule.OfArray([new RecordClass(1), new RecordClass(2), new RecordClass(3)]); + FSharpSet actual = [new RecordClass(1), new RecordClass(2), new RecordClass(3)]; + + Assert.Equal(expected, actual); + Assert.Equal(expected.Count, actual.Count); + + foreach (var (e, a) in expected.Zip(actual)) + { + Assert.Equal(e, a); + } + + {{csUtils}} + """ + |> withCSharpLanguageVersion CSharpLanguageVersion.CSharp12 + |> withName "Test" + |> compileExeAndRun + |> shouldSucceed + +[] +let ``FSharpSet: can create using C# collection expression`` () = + CSharp $$""" + {{csUsings}} + + var expected = SetModule.OfArray([new RecordStruct(1), new RecordStruct(2), new RecordStruct(3)]); + FSharpSet actual = [new RecordStruct(1), new RecordStruct(2), new RecordStruct(3)]; + + Assert.Equal(expected, actual); + Assert.Equal(expected.Count, actual.Count); + + foreach (var (e, a) in expected.Zip(actual)) + { + Assert.Equal(e, a); + } + + {{csUtils}} + """ + |> withCSharpLanguageVersion CSharpLanguageVersion.CSharp12 + |> withName "Test" + |> compileExeAndRun + |> shouldSucceed + +#endif diff --git a/tests/FSharp.Test.Utilities/Compiler.fs b/tests/FSharp.Test.Utilities/Compiler.fs index 1cbf3a3aade..a9275d15f99 100644 --- a/tests/FSharp.Test.Utilities/Compiler.fs +++ b/tests/FSharp.Test.Utilities/Compiler.fs @@ -777,12 +777,7 @@ module rec Compiler = let references = TargetFrameworkUtil.getReferences csSource.TargetFramework - let lv = - match csSource.LangVersion with - | CSharpLanguageVersion.CSharp8 -> LanguageVersion.CSharp8 - | CSharpLanguageVersion.CSharp9 -> LanguageVersion.CSharp9 - | CSharpLanguageVersion.Preview -> LanguageVersion.Preview - | _ -> LanguageVersion.Default + let lv = CSharpLanguageVersion.toLanguageVersion csSource.LangVersion let outputKind, extension = match csSource.OutputType with diff --git a/tests/FSharp.Test.Utilities/CompilerAssert.fs b/tests/FSharp.Test.Utilities/CompilerAssert.fs index a97b214acde..681f812a0ed 100644 --- a/tests/FSharp.Test.Utilities/CompilerAssert.fs +++ b/tests/FSharp.Test.Utilities/CompilerAssert.fs @@ -172,20 +172,25 @@ type CSharpLanguageVersion = | CSharp8 = 0 | CSharp9 = 1 | CSharp11 = 11 + | CSharp12 = 12 | Preview = 99 +module CSharpLanguageVersion = + /// Converts the given C# language version to a Roslyn language version value. + let toLanguageVersion lv = + match lv with + | CSharpLanguageVersion.CSharp8 -> LanguageVersion.CSharp8 + | CSharpLanguageVersion.CSharp9 -> LanguageVersion.CSharp9 + | CSharpLanguageVersion.CSharp11 -> LanguageVersion.CSharp11 + | CSharpLanguageVersion.CSharp12 -> LanguageVersion.CSharp12 + | CSharpLanguageVersion.Preview -> LanguageVersion.Preview + | _ -> LanguageVersion.Default + [] type CompilationUtil private () = static let createCSharpCompilation (source: SourceCodeFileKind, lv, tf, additionalReferences, name) = - let lv = - match lv with - | CSharpLanguageVersion.CSharp8 -> LanguageVersion.CSharp8 - | CSharpLanguageVersion.CSharp9 -> LanguageVersion.CSharp9 - | CSharpLanguageVersion.CSharp11 -> LanguageVersion.CSharp11 - | CSharpLanguageVersion.Preview -> LanguageVersion.Preview - | _ -> LanguageVersion.Default - + let lv = CSharpLanguageVersion.toLanguageVersion lv let tf = defaultArg tf TargetFramework.NetStandard20 let source = match source.GetSourceText with