From b13fee1b760119a79d038e43fb05d88e800fc7e5 Mon Sep 17 00:00:00 2001 From: Enioluwa Segun Date: Wed, 30 Nov 2022 12:18:08 -0500 Subject: [PATCH] [c#] Add support for System.Collections.Immutable collections Co-authored-by: Christopher Warrington --- CHANGELOG.md | 5 + compiler/src/Language/Bond/Codegen/Cs/Util.hs | 8 +- compiler/tests/TestMain.hs | 10 ++ .../immutable_collections_types.cs | 56 ++++++++ .../generated/immutable_collections_types.cs | 56 ++++++++ .../tests/schema/immutable_collections.bond | 18 +++ cs/cs.sln | 21 +++ cs/nuget/bond.csharp.test.csproj | 4 + cs/src/core/Comparer.cs | 11 +- .../core/expressions/DeserializerTransform.cs | 122 +++++++++++++++--- cs/test/core/Core.csproj | 5 + cs/test/core/ImmutableCollections.bond | 18 +++ cs/test/core/SerializationTests.cs | 6 + cs/test/core/UnitTest.bond | 1 + cs/test/coreNS10/CoreNS10.csproj | 5 + doc/src/bond_cs.md | 28 ++++ .../cs/core/immutable_collections/Program.cs | 56 ++++++++ .../immutable_collections.csproj | 23 ++++ .../cs/core/immutable_collections/schema.bond | 18 +++ 19 files changed, 448 insertions(+), 23 deletions(-) create mode 100644 compiler/tests/generated/collection-interfaces/immutable_collections_types.cs create mode 100644 compiler/tests/generated/immutable_collections_types.cs create mode 100644 compiler/tests/schema/immutable_collections.bond create mode 100644 cs/test/core/ImmutableCollections.bond create mode 100644 examples/cs/core/immutable_collections/Program.cs create mode 100644 examples/cs/core/immutable_collections/immutable_collections.csproj create mode 100644 examples/cs/core/immutable_collections/schema.bond diff --git a/CHANGELOG.md b/CHANGELOG.md index fb702b690c..9ddacccead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,11 @@ different versioning scheme, following the Haskell community's code will be generated for them. See [issue \#1131, Bond-over-gRPC will be deprecated February 2022](https://github.com/microsoft/bond/issues/1131), for the full announcement. +* Added codegen and deserialization support for container type aliases to + use + [System.Collections.Immutable](https://learn.microsoft.com/dotnet/api/system.collections.immutable) + collections. (Pull request + [\#1161](https://github.com/microsoft/bond/pull/1161)) ## 10.0: 2022-03-07 ## diff --git a/compiler/src/Language/Bond/Codegen/Cs/Util.hs b/compiler/src/Language/Bond/Codegen/Cs/Util.hs index 84f654330f..0c730267c1 100644 --- a/compiler/src/Language/Bond/Codegen/Cs/Util.hs +++ b/compiler/src/Language/Bond/Codegen/Cs/Util.hs @@ -16,7 +16,8 @@ module Language.Bond.Codegen.Cs.Util import Data.Int (Int64) import Data.Monoid import Prelude -import Data.Text.Lazy (Text) +import Data.Text.Lazy (Text, isPrefixOf) +import Data.Text.Lazy.Builder (toLazyText) import Text.Shakespeare.Text import Paths_bond (version) import Data.Version (showVersion) @@ -112,15 +113,20 @@ paramConstraints = newlineBeginSep 2 constraint constraint (TypeParam _ Nothing) = mempty constraint (TypeParam name (Just Value)) = [lt|where #{name} : struct|] +isImmutableCollection :: MappingContext -> Type -> Bool +isImmutableCollection cs t = [lt|System.Collections.Immutable.Immutable|] `isPrefixOf` toLazyText (getInstanceTypeName cs t) + -- Initial value for C# field/property or Nothing if C# implicit default is OK defaultValue :: MappingContext -> Field -> Maybe Text defaultValue cs Field {fieldDefault = Nothing, ..} = implicitDefault fieldType where newInstance t = Just [lt|new #{getInstanceTypeName cs t}()|] + staticEmptyField t = Just [lt|#{getInstanceTypeName cs t}.Empty|] implicitDefault (BT_Bonded t) = Just [lt|global::Bond.Bonded<#{getTypeName cs t}>.Empty|] implicitDefault t@(BT_TypeParam _) = Just [lt|global::Bond.GenericFactory.Create<#{getInstanceTypeName cs t}>()|] implicitDefault t@BT_Blob = newInstance t implicitDefault t@(BT_UserDefined a@Alias {..} args) + | isImmutableCollection cs t = staticEmptyField t | customAliasMapping cs a = newInstance t | otherwise = implicitDefault $ resolveAlias a args implicitDefault t diff --git a/compiler/tests/TestMain.hs b/compiler/tests/TestMain.hs index ca60f46b8b..e20358d897 100644 --- a/compiler/tests/TestMain.hs +++ b/compiler/tests/TestMain.hs @@ -144,6 +144,16 @@ tests = testGroup "Compiler tests" , verifyCsCodegen "inheritance" , verifyCsCodegen "aliases" , verifyCsCodegen "complex_inheritance" + , verifyCodegen + [ "c#" + , "--using=ImmutableArray=System.Collections.Immutable.ImmutableArray<{0}>" + , "--using=ImmutableList=System.Collections.Immutable.ImmutableList<{0}>" + , "--using=ImmutableHashSet=System.Collections.Immutable.ImmutableHashSet<{0}>" + , "--using=ImmutableSortedSet=System.Collections.Immutable.ImmutableSortedSet<{0}>" + , "--using=ImmutableDictionary=System.Collections.Immutable.ImmutableDictionary<{0},{1}>" + , "--using=ImmutableSortedDictionary=System.Collections.Immutable.ImmutableSortedDictionary<{0},{1}>" + ] + "immutable_collections" , verifyCodegenVariation [ "c#" , "--preview-constructor-parameters" diff --git a/compiler/tests/generated/collection-interfaces/immutable_collections_types.cs b/compiler/tests/generated/collection-interfaces/immutable_collections_types.cs new file mode 100644 index 0000000000..db9d74dc04 --- /dev/null +++ b/compiler/tests/generated/collection-interfaces/immutable_collections_types.cs @@ -0,0 +1,56 @@ + + +// suppress "Missing XML comment for publicly visible type or member" +#pragma warning disable 1591 + + +#region ReSharper warnings +// ReSharper disable PartialTypeWithSinglePart +// ReSharper disable RedundantNameQualifier +// ReSharper disable InconsistentNaming +// ReSharper disable CheckNamespace +// ReSharper disable UnusedParameter.Local +// ReSharper disable RedundantUsingDirective +#endregion + +namespace tests +{ + using System.Collections.Generic; + + [global::Bond.Schema] + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public partial class ImmutableCollectionsHolder + { + [global::Bond.Id(0)] + public System.Collections.Immutable.ImmutableArray ImmutableArrayString { get; set; } + + [global::Bond.Id(1)] + public System.Collections.Immutable.ImmutableList ImmutableListString { get; set; } + + [global::Bond.Id(2)] + public System.Collections.Immutable.ImmutableHashSet ImmutableHashSetString { get; set; } + + [global::Bond.Id(3)] + public System.Collections.Immutable.ImmutableSortedSet ImmutableSortedSetInt { get; set; } + + [global::Bond.Id(4)] + public System.Collections.Immutable.ImmutableDictionary ImmutableDictionaryStringMap { get; set; } + + [global::Bond.Id(5)] + public System.Collections.Immutable.ImmutableSortedDictionary ImmutableSortedDictionaryStringMap { get; set; } + + public ImmutableCollectionsHolder() + : this("tests.ImmutableCollectionsHolder", "ImmutableCollectionsHolder") + {} + + protected ImmutableCollectionsHolder(string fullName, string name) + { + ImmutableArrayString = System.Collections.Immutable.ImmutableArray.Empty; + ImmutableListString = System.Collections.Immutable.ImmutableList.Empty; + ImmutableHashSetString = System.Collections.Immutable.ImmutableHashSet.Empty; + ImmutableSortedSetInt = System.Collections.Immutable.ImmutableSortedSet.Empty; + ImmutableDictionaryStringMap = System.Collections.Immutable.ImmutableDictionary.Empty; + ImmutableSortedDictionaryStringMap = System.Collections.Immutable.ImmutableSortedDictionary.Empty; + } + } +} // tests diff --git a/compiler/tests/generated/immutable_collections_types.cs b/compiler/tests/generated/immutable_collections_types.cs new file mode 100644 index 0000000000..db9d74dc04 --- /dev/null +++ b/compiler/tests/generated/immutable_collections_types.cs @@ -0,0 +1,56 @@ + + +// suppress "Missing XML comment for publicly visible type or member" +#pragma warning disable 1591 + + +#region ReSharper warnings +// ReSharper disable PartialTypeWithSinglePart +// ReSharper disable RedundantNameQualifier +// ReSharper disable InconsistentNaming +// ReSharper disable CheckNamespace +// ReSharper disable UnusedParameter.Local +// ReSharper disable RedundantUsingDirective +#endregion + +namespace tests +{ + using System.Collections.Generic; + + [global::Bond.Schema] + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public partial class ImmutableCollectionsHolder + { + [global::Bond.Id(0)] + public System.Collections.Immutable.ImmutableArray ImmutableArrayString { get; set; } + + [global::Bond.Id(1)] + public System.Collections.Immutable.ImmutableList ImmutableListString { get; set; } + + [global::Bond.Id(2)] + public System.Collections.Immutable.ImmutableHashSet ImmutableHashSetString { get; set; } + + [global::Bond.Id(3)] + public System.Collections.Immutable.ImmutableSortedSet ImmutableSortedSetInt { get; set; } + + [global::Bond.Id(4)] + public System.Collections.Immutable.ImmutableDictionary ImmutableDictionaryStringMap { get; set; } + + [global::Bond.Id(5)] + public System.Collections.Immutable.ImmutableSortedDictionary ImmutableSortedDictionaryStringMap { get; set; } + + public ImmutableCollectionsHolder() + : this("tests.ImmutableCollectionsHolder", "ImmutableCollectionsHolder") + {} + + protected ImmutableCollectionsHolder(string fullName, string name) + { + ImmutableArrayString = System.Collections.Immutable.ImmutableArray.Empty; + ImmutableListString = System.Collections.Immutable.ImmutableList.Empty; + ImmutableHashSetString = System.Collections.Immutable.ImmutableHashSet.Empty; + ImmutableSortedSetInt = System.Collections.Immutable.ImmutableSortedSet.Empty; + ImmutableDictionaryStringMap = System.Collections.Immutable.ImmutableDictionary.Empty; + ImmutableSortedDictionaryStringMap = System.Collections.Immutable.ImmutableSortedDictionary.Empty; + } + } +} // tests diff --git a/compiler/tests/schema/immutable_collections.bond b/compiler/tests/schema/immutable_collections.bond new file mode 100644 index 0000000000..1ce842b29c --- /dev/null +++ b/compiler/tests/schema/immutable_collections.bond @@ -0,0 +1,18 @@ +namespace tests + +using ImmutableArray = list; +using ImmutableList = list; +using ImmutableHashSet = set; +using ImmutableSortedSet = set; +using ImmutableDictionary = map; +using ImmutableSortedDictionary = map; + +struct ImmutableCollectionsHolder +{ + 0: ImmutableArray ImmutableArrayString; + 1: ImmutableList ImmutableListString; + 2: ImmutableHashSet ImmutableHashSetString; + 3: ImmutableSortedSet ImmutableSortedSetInt; + 4: ImmutableDictionary ImmutableDictionaryStringMap; + 5: ImmutableSortedDictionary ImmutableSortedDictionaryStringMap; +} diff --git a/cs/cs.sln b/cs/cs.sln index 7c1465af38..a35bd53fdc 100644 --- a/cs/cs.sln +++ b/cs/cs.sln @@ -247,6 +247,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "default-ignore-output", "te {21E175D5-BBDD-4B63-8FB7-38899BF2F9D1} = {21E175D5-BBDD-4B63-8FB7-38899BF2F9D1} EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "immutable_collections", "..\examples\cs\core\immutable_collections\immutable_collections.csproj", "{9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -881,6 +883,24 @@ Global {98BF6B10-F821-4402-8923-5F7B726BBFC8}.Release|Mixed Platforms.Build.0 = Release|Any CPU {98BF6B10-F821-4402-8923-5F7B726BBFC8}.Release|Win32.ActiveCfg = Release|Any CPU {98BF6B10-F821-4402-8923-5F7B726BBFC8}.Release|Win32.Build.0 = Release|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Debug|Win32.ActiveCfg = Debug|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Debug|Win32.Build.0 = Debug|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Fields|Any CPU.ActiveCfg = Release|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Fields|Any CPU.Build.0 = Release|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Fields|Mixed Platforms.ActiveCfg = Release|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Fields|Mixed Platforms.Build.0 = Release|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Fields|Win32.ActiveCfg = Release|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Fields|Win32.Build.0 = Release|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Release|Any CPU.Build.0 = Release|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Release|Win32.ActiveCfg = Release|Any CPU + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6}.Release|Win32.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -920,6 +940,7 @@ Global {4FACD9A1-B8AC-4D76-B4B5-B71607EA8F8C} = {621A2166-EEE0-4A27-88AA-5BE5AC996452} {3805B86E-2BE3-4FFA-A11C-B4981069EC88} = {4268A1D3-AF40-4120-B021-D95A0F754221} {98BF6B10-F821-4402-8923-5F7B726BBFC8} = {8CF1F5CD-548F-4323-869E-AA5903C88B6A} + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6} = {621A2166-EEE0-4A27-88AA-5BE5AC996452} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6EB58560-CA9C-4F6C-B916-CCA6C7FE2A2B} diff --git a/cs/nuget/bond.csharp.test.csproj b/cs/nuget/bond.csharp.test.csproj index 9dc0522006..29faf5329e 100644 --- a/cs/nuget/bond.csharp.test.csproj +++ b/cs/nuget/bond.csharp.test.csproj @@ -15,6 +15,7 @@ + @@ -39,5 +40,8 @@ $(BondOptions) --using="DateTime=System.DateTime" + + $(BondOptions) --using="ImmutableArray=System.Collections.Immutable.ImmutableArray<{0}>" --using="ImmutableList=System.Collections.Immutable.ImmutableList<{0}>" --using="ImmutableHashSet=System.Collections.Immutable.ImmutableHashSet<{0}>" --using="ImmutableSortedSet=System.Collections.Immutable.ImmutableSortedSet<{0}>" --using="ImmutableDictionary=System.Collections.Immutable.ImmutableDictionary<{0},{1}>" --using="ImmutableSortedDictionary=System.Collections.Immutable.ImmutableSortedDictionary<{0},{1}>" + diff --git a/cs/src/core/Comparer.cs b/cs/src/core/Comparer.cs index 1c47a96878..fa1df200eb 100644 --- a/cs/src/core/Comparer.cs +++ b/cs/src/core/Comparer.cs @@ -59,7 +59,16 @@ static Expression ObjectsEqual(Expression left, Expression right) return Expression.Call(null, comparerEqual.MakeGenericMethod(type), left, right); if (type.IsBondContainer()) - return EnumerablesEqual(left, right); + { + if (type.IsValueType()) + { + return StructsEqual(left, right); + } + else + { + return EnumerablesEqual(left, right); + } + } if (type.IsGenericType() && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) return KeyValuePairEqual(left, right); diff --git a/cs/src/core/expressions/DeserializerTransform.cs b/cs/src/core/expressions/DeserializerTransform.cs index 690ef08bf7..371f0b4694 100644 --- a/cs/src/core/expressions/DeserializerTransform.cs +++ b/cs/src/core/expressions/DeserializerTransform.cs @@ -83,6 +83,25 @@ internal class DeserializerTransform static readonly MethodInfo bufferBlockCopy = Reflection.MethodInfoOf((byte[] a) => Buffer.BlockCopy(a, default(int), a, default(int), default(int))); + // Immutable collection types are represented/identified as strings + // to avoid depending on the System.Collections.Immutable assembly + // or NuGet package + static readonly HashSet immutableListSetTypeNames = new HashSet + { + "System.Collections.Immutable.ImmutableArray`1", + "System.Collections.Immutable.ImmutableList`1", + "System.Collections.Immutable.ImmutableHashSet`1", + "System.Collections.Immutable.ImmutableSortedSet`1", + }; + + static readonly HashSet immutableMapTypeNames = new HashSet + { + "System.Collections.Immutable.ImmutableDictionary`2", + "System.Collections.Immutable.ImmutableSortedDictionary`2", + }; + + static readonly HashSet immutableCollectionTypeNames = new HashSet(immutableListSetTypeNames.Concat(immutableMapTypeNames)); + public DeserializerTransform( Expression> deferredDeserialize, Factory factory, @@ -408,20 +427,44 @@ Expression Container(IParser parser, Expression container, Type schemaType, bool } } - var add = container.Type.GetMethod(typeof(ICollection<>), "Add", item.Type); + // For System.Collections.Immutable lists/sets, use the builders to construct them, since ICollection.Add() + // is not supported for them. + var containerGenericTypeDef = container.Type.GetGenericTypeDefinition(); + if (immutableListSetTypeNames.Contains(containerGenericTypeDef.FullName)) + { + var builderType = container.Type.GetTypeInfo().GetDeclaredNestedType("Builder").MakeGenericType(item.Type); + var builder = Expression.Variable(builderType, container + "_builder"); + var builderAdd = builderType.GetMethod(typeof(ICollection<>), "Add", item.Type); + + addItem = Expression.Block( + Value(valueParser, item, elementType, itemSchemaType, initialize: true), + Expression.Call(builder, builderAdd, item)); + + var toBuilderMethod = container.Type.FindMethod("ToBuilder"); + var toImmutableMethod = builderType.FindMethod("ToImmutable"); + var constructBuilder = Expression.Assign(builder, Expression.Call(container, toBuilderMethod)); + var reconstructImmutable = Expression.Assign(container, Expression.Call(builder, toImmutableMethod)); + + parameters = new[] { item, builder }; + beforeLoop = Expression.Block(beforeLoop, constructBuilder); + afterLoop = Expression.Block(afterLoop, reconstructImmutable); + } + else + { + var add = container.Type.GetMethod(typeof(ICollection<>), "Add", item.Type); - addItem = Expression.Block( - Value(valueParser, item, elementType, itemSchemaType, initialize: true), - Expression.Call(container, add, item)); + addItem = Expression.Block( + Value(valueParser, item, elementType, itemSchemaType, initialize: true), + Expression.Call(container, add, item)); - parameters = new[] { item }; + parameters = new[] { item }; + } } return Expression.Block( parameters, beforeLoop, - ControlExpression.While(next, - addItem), + ControlExpression.While(next, addItem), afterLoop); }); } @@ -451,19 +494,48 @@ Expression Map(IParser parser, Expression map, Type schemaType, bool initialize) Expression.Assign(map, newContainer(map.Type, schemaType, cappedCount))); } - var add = map.Type.GetDeclaredProperty(typeof(IDictionary<,>), "Item", value.Type); - - Expression addItem = Expression.Block( - Value(keyParser, key, keyType, itemSchemaType.Key, initialize: true), - nextValue, - Value(valueParser, value, valueType, itemSchemaType.Value, initialize: true), - Expression.Assign(Expression.Property(map, add, new Expression[] { key }), value)); - - return Expression.Block( - new [] { key, value }, - init, - ControlExpression.While(nextKey, - addItem)); + // For System.Collections.Immutable maps, use the builders to construct them, since + // the setter IDictionary.Item[] is not supported for them. + var mapGenericTypeDef = map.Type.GetGenericTypeDefinition(); + if (immutableMapTypeNames.Contains(mapGenericTypeDef.FullName)) + { + var builderType = map.Type.GetTypeInfo().GetDeclaredNestedType("Builder").MakeGenericType(itemSchemaType.Key, itemSchemaType.Value); + var builder = Expression.Variable(builderType, map + "_builder"); + var builderAdd = builderType.GetDeclaredProperty(typeof(IDictionary<,>), "Item", value.Type); + + var addItem = Expression.Block( + Value(keyParser, key, keyType, itemSchemaType.Key, initialize: true), + nextValue, + Value(valueParser, value, valueType, itemSchemaType.Value, initialize: true), + Expression.Assign(Expression.Property(builder, builderAdd, new Expression[] { key }), value)); + + var toBuilderMethod = map.Type.FindMethod("ToBuilder"); + var toImmutableMethod = builderType.FindMethod("ToImmutable"); + var constructBuilder = Expression.Assign(builder, Expression.Call(map, toBuilderMethod)); + var reconstructImmutable = Expression.Assign(map, Expression.Call(builder, toImmutableMethod)); + + return Expression.Block( + new[] { key, value, builder }, + init, + constructBuilder, + ControlExpression.While(nextKey, addItem), + reconstructImmutable); + } + else + { + var add = map.Type.GetDeclaredProperty(typeof(IDictionary<,>), "Item", value.Type); + + var addItem = Expression.Block( + Value(keyParser, key, keyType, itemSchemaType.Key, initialize: true), + nextValue, + Value(valueParser, value, valueType, itemSchemaType.Value, initialize: true), + Expression.Assign(Expression.Property(map, add, new Expression[] { key }), value)); + + return Expression.Block( + new[] { key, value }, + init, + ControlExpression.While(nextKey, addItem)); + } }); } @@ -558,7 +630,15 @@ static Expression New(Type type, Type schemaType, params Expression[] arguments) } else if (schemaType.IsGenericType()) { - schemaType = schemaType.GetGenericTypeDefinition().MakeGenericType(type.GetTypeInfo().GenericTypeArguments); + // All System.Collections.Immutable collections have a static field ImmutableX.Empty + // which is the simplest constructor. + var schemaGenericTypeDef = schemaType.GetGenericTypeDefinition(); + if (immutableCollectionTypeNames.Contains(schemaGenericTypeDef.FullName)) + { + return Expression.Field(null, schemaType.GetTypeInfo().GetDeclaredField("Empty")); + } + + schemaType = schemaGenericTypeDef.MakeGenericType(type.GetTypeInfo().GenericTypeArguments); } else if (schemaType.IsArray) { diff --git a/cs/test/core/Core.csproj b/cs/test/core/Core.csproj index c99be1ae49..b4c5e33bd9 100644 --- a/cs/test/core/Core.csproj +++ b/cs/test/core/Core.csproj @@ -41,6 +41,9 @@ $(BondOptions) --using="DateTime=System.DateTime" + + $(BondOptions) --using="ImmutableArray=System.Collections.Immutable.ImmutableArray<{0}>" --using="ImmutableList=System.Collections.Immutable.ImmutableList<{0}>" --using="ImmutableHashSet=System.Collections.Immutable.ImmutableHashSet<{0}>" --using="ImmutableSortedSet=System.Collections.Immutable.ImmutableSortedSet<{0}>" --using="ImmutableDictionary=System.Collections.Immutable.ImmutableDictionary<{0},{1}>" --using="ImmutableSortedDictionary=System.Collections.Immutable.ImmutableSortedDictionary<{0},{1}>" + @@ -49,12 +52,14 @@ + + diff --git a/cs/test/core/ImmutableCollections.bond b/cs/test/core/ImmutableCollections.bond new file mode 100644 index 0000000000..332418c693 --- /dev/null +++ b/cs/test/core/ImmutableCollections.bond @@ -0,0 +1,18 @@ +namespace UnitTest.ImmutableCollections + +using ImmutableArray = list; +using ImmutableList = list; +using ImmutableHashSet = set; +using ImmutableSortedSet = set; +using ImmutableDictionary = map; +using ImmutableSortedDictionary = map; + +struct ImmutableCollectionsHolder +{ + 0: ImmutableArray ImmutableArrayString; + 1: ImmutableList ImmutableListString; + 2: ImmutableHashSet ImmutableHashSetString; + 3: ImmutableSortedSet ImmutableSortedSetInt; + 4: ImmutableDictionary ImmutableDictionaryStringMap; + 5: ImmutableSortedDictionary ImmutableSortedDictionaryStringMap; +} diff --git a/cs/test/core/SerializationTests.cs b/cs/test/core/SerializationTests.cs index 7cae57a8b8..6301d6937e 100644 --- a/cs/test/core/SerializationTests.cs +++ b/cs/test/core/SerializationTests.cs @@ -439,6 +439,12 @@ public void TypeFromFileWithSpaces() Assert.IsNotNull(new EnsureSpacesInPathsWork()); } + [Test] + public void ImmutableCollections() + { + TestSerialization(); + } + void TestTypePromotion() { TestFieldSerialization(); diff --git a/cs/test/core/UnitTest.bond b/cs/test/core/UnitTest.bond index d985e1eae6..0bf4a047b3 100644 --- a/cs/test/core/UnitTest.bond +++ b/cs/test/core/UnitTest.bond @@ -2,6 +2,7 @@ import "bond/core/bond.bond" import "Aliases.bond" // Uses mixed slashes to test gbc can deal with that import "dir1\dir2/Bond File With Spaces.bond" +import "ImmutableCollections.bond" namespace UnitTest diff --git a/cs/test/coreNS10/CoreNS10.csproj b/cs/test/coreNS10/CoreNS10.csproj index 1a362d6a13..b30697a19d 100644 --- a/cs/test/coreNS10/CoreNS10.csproj +++ b/cs/test/coreNS10/CoreNS10.csproj @@ -44,6 +44,9 @@ $(BondOptions) --using="DateTime=System.DateTime" + + $(BondOptions) --using="ImmutableArray=System.Collections.Immutable.ImmutableArray<{0}>" --using="ImmutableList=System.Collections.Immutable.ImmutableList<{0}>" --using="ImmutableHashSet=System.Collections.Immutable.ImmutableHashSet<{0}>" --using="ImmutableSortedSet=System.Collections.Immutable.ImmutableSortedSet<{0}>" --using="ImmutableDictionary=System.Collections.Immutable.ImmutableDictionary<{0},{1}>" --using="ImmutableSortedDictionary=System.Collections.Immutable.ImmutableSortedDictionary<{0},{1}>" + @@ -54,12 +57,14 @@ + + diff --git a/doc/src/bond_cs.md b/doc/src/bond_cs.md index 3c7a3a01e8..1e1377e035 100644 --- a/doc/src/bond_cs.md +++ b/doc/src/bond_cs.md @@ -1177,6 +1177,34 @@ type aliases don't require a user defined converter. - `examples/cs/core/container_alias` +System.Collection.Immutable support +----------------------------------- + +Bond provides special support for using the +[System.Collections.Immutable](https://learn.microsoft.com/dotnet/api/system.collections.immutable) +collections as container type aliases. The following aliases are supported: + +| Underlying Bond type | Supported System.Collections.Immutable container | +|----------------------|-----------------------------------------------------------------------------------------| +| `vector` | `ImmutableArray`, `ImmutableList` | +| `list` | `ImmutableArray`, `ImmutableHashSet`, `ImmutableList`, `ImmutableSortedSet` | +| `set` | `ImmutableHashSet`, `ImmutableSortedSet` | +| `map` | `ImmutableDictionary`, `ImmutableSortedDictionary` | + +During code generation, immutable collection fields are handled specially. +Since they do not have parameterless constructors, the Bond compiler will +instead use the static `Empty` field is used as the default value, e.g. +[ImmutableList\.Empty](https://learn.microsoft.com/dotnet/api/system.collections.immutable.immutablelist-1.empty). + +When deserializing immutable collections, Bond will use the inner `Builder` +classes to efficiently reconstruct the collection, e.g. +[ImmutableList\.Builder](https://learn.microsoft.com/dotnet/api/system.collections.immutable.immutablelist-1.builder). + +See the below project for examples on using immutable collections as +container aliases: + +- `examples/cs/core/immutable_collections_alias` + Converter --------- diff --git a/examples/cs/core/immutable_collections/Program.cs b/examples/cs/core/immutable_collections/Program.cs new file mode 100644 index 0000000000..6f55bedfbb --- /dev/null +++ b/examples/cs/core/immutable_collections/Program.cs @@ -0,0 +1,56 @@ +namespace Examples +{ + using Bond; + using Bond.IO.Unsafe; + using Bond.Protocols; + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + + static class Program + { + static void Main(string[] args) + { + var random = new Random(); + var ints = new int[100]; + for (int i = 0; i < ints.Length; i++) + { + ints[i] = random.Next(); + } + + var strings = new string[100]; + for (int i = 0; i < strings.Length; i++) + { + strings[i] = $"string{i}"; + } + + var stringToStringMap = strings.Select((str) => new KeyValuePair(str, str)); + var intToStringMap = strings.Select((str, idx) => new KeyValuePair(idx, str)); + + var src = new ImmutableCollectionsHolder() + { + ImmutableArrayOfStrings = ImmutableArray.CreateRange(strings), + ImmutableHashSetOfStrings = ImmutableHashSet.CreateRange(strings), + ImmutableListOfStrings = ImmutableList.CreateRange(strings), + ImmutableSortedIntToStringDictionary = ImmutableSortedDictionary.CreateRange(intToStringMap), + ImmutableSortedSetOfInts = ImmutableSortedSet.CreateRange(ints), + ImmutableStringToStringDictionary = ImmutableDictionary.CreateRange(stringToStringMap), + }; + + var outputBuffer = new OutputBuffer(); + var writer = new CompactBinaryWriter(outputBuffer); + Serialize.To(writer, src); + + var inputBuffer = new InputBuffer(outputBuffer.Data); + var reader = new CompactBinaryReader(inputBuffer); + var dst = Deserialize.From(reader); + ThrowIfFalse(Comparer.Equal(src, dst)); + } + + static void ThrowIfFalse(bool b) + { + if (!b) throw new Exception("Assertion failed"); + } + } +} diff --git a/examples/cs/core/immutable_collections/immutable_collections.csproj b/examples/cs/core/immutable_collections/immutable_collections.csproj new file mode 100644 index 0000000000..67b5449c71 --- /dev/null +++ b/examples/cs/core/immutable_collections/immutable_collections.csproj @@ -0,0 +1,23 @@ + + + + {9CC2754E-501D-4DB3-8EAA-8B91025E5DF6} + Exe + immutable_collections + immutable_collections + net45 + --using="ImmutableArray=System.Collections.Immutable.ImmutableArray<{0}>" --using="ImmutableList=System.Collections.Immutable.ImmutableList<{0}>" --using="ImmutableHashSet=System.Collections.Immutable.ImmutableHashSet<{0}>" --using="ImmutableSortedSet=System.Collections.Immutable.ImmutableSortedSet<{0}>" --using="ImmutableDictionary=System.Collections.Immutable.ImmutableDictionary<{0},{1}>" --using="ImmutableSortedDictionary=System.Collections.Immutable.ImmutableSortedDictionary<{0},{1}>" + + + + + + + + + + + + + + diff --git a/examples/cs/core/immutable_collections/schema.bond b/examples/cs/core/immutable_collections/schema.bond new file mode 100644 index 0000000000..1fd1e3beb7 --- /dev/null +++ b/examples/cs/core/immutable_collections/schema.bond @@ -0,0 +1,18 @@ +namespace Examples + +using ImmutableArray = list; +using ImmutableList = list; +using ImmutableHashSet = set; +using ImmutableSortedSet = set; +using ImmutableDictionary = map; +using ImmutableSortedDictionary = map; + +struct ImmutableCollectionsHolder +{ + 0: ImmutableArray ImmutableArrayOfStrings; + 1: ImmutableList ImmutableListOfStrings; + 2: ImmutableHashSet ImmutableHashSetOfStrings; + 3: ImmutableSortedSet ImmutableSortedSetOfInts; + 4: ImmutableDictionary ImmutableStringToStringDictionary; + 5: ImmutableSortedDictionary ImmutableSortedIntToStringDictionary; +}