├── .gitignore ├── LICENSE.md ├── README.md ├── StackXML.Benchmark ├── Program.cs ├── SimpleLoadBenchmark.cs ├── StackXML.Benchmark.csproj └── XmlEncodeBenchmark.cs ├── StackXML.Generator ├── ComputeSharp │ ├── Extensions │ │ ├── AttributeDataExtensions.cs │ │ ├── ISymbolExtensions.cs │ │ ├── ITypeSymbolExtensions.cs │ │ ├── IncrementalValueProviderExtensions.cs │ │ └── IndentedTextWriterExtensions.cs │ ├── Helpers │ │ ├── EquatableArray{T}.cs │ │ ├── HashCode.cs │ │ ├── ImmutableArrayBuilder{T}.cs │ │ ├── IndentedTextWriter.cs │ │ └── ObjectPool{T}.cs │ ├── LICENSE │ └── Models │ │ ├── HierarchyInfo.cs │ │ └── TypeInfo.cs ├── Properties │ └── launchSettings.json ├── StackXML.Generator.csproj ├── StrGenerator.cs └── XmlGenerator.cs ├── StackXML.Tests ├── InterpretBool.cs ├── SpanStrTests.cs ├── StackXML.Tests.csproj ├── StrReadWriteTests.cs ├── StructuredStr.cs ├── Xml.cs └── XmlEncodingTests.cs ├── StackXML.sln └── StackXML ├── CDataMode.cs ├── IXmlSerializable.cs ├── StackXML.csproj ├── Str ├── BaseStrFormatter.cs ├── BaseStrParser.cs ├── IStrClass.cs ├── IStrFormatter.cs ├── IStrParser.cs ├── SpanStr.cs ├── StandardStrParser.cs ├── StrAttributes.cs ├── StrClassExtensions.cs ├── StrReader.cs └── StrWriter.cs ├── XmlAttributes.cs ├── XmlReadBuffer.cs ├── XmlReadParams.cs ├── XmlWriteBuffer.cs └── XmlWriteParams.cs /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ 4 | riderModule.iml 5 | /_ReSharper.Caches/ 6 | .idea -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 zingballyhoo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StackXML 2 | Stack based zero*-allocation XML serializer and deserializer powered by C# 9 source generators. 3 | 4 | ## Why 5 | Premature optimisation :) 6 | 7 | ## Setup 8 | - Add the following to your project to reference the serializer and enable the source generator 9 | ```xml 10 | 11 | 12 | 13 | 14 | ``` 15 | - The common entrypoint for deserializing is `XmlReadBuffer.ReadStatic(ReadOnlySpan)` 16 | - The common entrypoint for serializing is `XmlWriteBuffer.SerializeStatic(IXmlSerializable)` 17 | - This method returns a string, to avoid this allocation you will need create your own instance of XmlWriteBuffer and ensure it is disposed safely like `SerializeStatic` does. The `ToSpan` method returns the char span containing the serialized text 18 | 19 | ## Features 20 | - Fully structured XML serialization and deserialization with 0 allocations, apart from the output data structure when deserializing. Serialization uses a pooled buffer from `ArrayPool.Shared` that is released when the serializer is disposed. 21 | - `XmlReadBuffer` handles deserialization 22 | - `XmlWriteBuffer` handles serialization 23 | - `XmlCls` maps a type to an element 24 | - Used for the serializer to know what the element name should be 25 | - Used by the deserializer to map to IXmlSerializable bodies with no explicit name 26 | - `XmlField` maps to attributes 27 | - `XmlBody` maps to child elements 28 | - `IXmlSerializable` (not actually an interface, see quirks) represents a type that can be read from or written to XML 29 | - Can be manually added as a base, or the source generator will add it automatically to any type that has XML attributes 30 | - Parsing delimited attributes into typed lists 31 | - `` 32 | - `[XmlField("list")] [XmlSplitStr(',')] public List m_list;` 33 | - Using StrReader and StrWriter, see below 34 | - StrReader and StrWriter classes, for reading and writing (comma usually) delimited strings with 0 allocations. 35 | - Can be used in a fully structured way by adding `StrField` attributes to fields on a `ref partial struct` (not compatible with XmlSplitStr, maybe future consideration) 36 | - Agnostic logging through [LibLog](https://github.com/damianh/LibLog) 37 | 38 | ## Quirks 39 | - Invalid data between elements is ignored 40 | - `anything here is completely missed` 41 | - Spaces between attributes is not required by the deserializer 42 | - e.g `` 43 | - XmlSerializer must be disposed otherwise the pooled buffer will be leaked. 44 | - XmlSerializer.SerializeStatic gives of an example of how this should be done in a safe way 45 | - Data types can only be classes, not structs. 46 | - All types must inherit from IXmlSerializable (either manually or added by the source generator) which is actually an abstract class and not an interface 47 | - Using structs would be possible but I don't think its worth the box 48 | - ~~Types from another assembly can't be used as a field/body. Needs fixing~~ 49 | - All elements in the data to parse must be defined in the type in one way or another, otherwise an exception will be thrown. 50 | - The deserializer relies on complete parsing and has no way of skipping elements 51 | - Comments within a primitive type body will cause the parser to crash (future consideration...) 52 | - `hi` 53 | - Null strings are currently output exactly the same as empty strings... might need changing 54 | - The source generator emits a parameterless constructor on all XML types that initializes `List` bodies to an empty list 55 | - Trying to serialize a null list currently crashes the serializer.... 56 | - When decoding XML text an extra allocation of the input string is required 57 | - WebUtility.HtmlDecode does not provide an overload taking a span, but the method taking a string turns it into a span anyway.. hmm 58 | - The decode is avoided where possible 59 | - Would be nice to be able to use [ValueStringBuilder](https://github.com/dotnet/runtime/blob/master/src/libraries/Common/src/System/Text/ValueStringBuilder.cs). See https://github.com/dotnet/runtime/issues/25587 60 | 61 | ## Performance 62 | Very simple benchmark, loading a single element and getting the string value of its attribute `attribute` 63 | ``` ini 64 | 65 | BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19045 66 | Intel Core i5-6600K CPU 3.50GHz (Skylake), 1 CPU, 4 logical and 4 physical cores 67 | .NET SDK=9.0.200 68 | [Host] : .NET 9.0.2 (9.0.225.6610), X64 RyuJIT 69 | DefaultJob : .NET 9.0.2 (9.0.225.6610), X64 RyuJIT 70 | ``` 71 | | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | 72 | |-------------- |------------:|----------:|----------:|-------:|--------:|-------:|------:|------:|----------:| 73 | | ReadBuffer | 60.16 ns | 0.791 ns | 0.740 ns | 1.00 | 0.00 | 0.0178 | - | - | 56 B | 74 | | XmlReader_ | 823.91 ns | 6.864 ns | 6.421 ns | 13.70 | 0.23 | 3.2892 | - | - | 10,336 B | 75 | | XDocument_ | 1,047.87 ns | 17.032 ns | 15.931 ns | 17.42 | 0.27 | 3.4218 | - | - | 10,760 B | 76 | | XmlDocument | 1,435.48 ns | 15.425 ns | 14.428 ns | 23.87 | 0.43 | 3.9063 | - | - | 12,248 B | 77 | | XmlSerializer | 6,398.11 ns | 88.037 ns | 82.350 ns | 106.37 | 2.14 | 4.5471 | - | - | 14,305 B | 78 | 79 | ## Example data classes 80 | ### Simple Attribute 81 | ```xml 82 | 83 | ``` 84 | ```csharp 85 | [XmlCls("test"))] 86 | public partial class Test 87 | { 88 | [XmlField("attribute")] 89 | public string m_attribute; 90 | } 91 | ``` 92 | ### Text body 93 | ```xml 94 | 95 | 96 | 97 | ``` 98 | CData can be configured by setting `cdataMode` for serializing and deserializing 99 | ```xml 100 | 101 | Hello world 102 | 103 | ``` 104 | ```csharp 105 | [XmlCls("test2"))] 106 | public partial class Test2 107 | { 108 | [XmlBody("name")] 109 | public string m_name; 110 | } 111 | ``` 112 | ### Lists 113 | ```xml 114 | 115 | 116 | 117 | 118 | 119 | 120 | ``` 121 | ```csharp 122 | [XmlCls("listItem"))] 123 | public partial class ListItem 124 | { 125 | [XmlField("name")] 126 | public string m_name; 127 | 128 | [XmlField("age")] 129 | public int m_age; // could also be byte, uint etc 130 | } 131 | 132 | [XmlCls("container")] 133 | public partial class ListContainer 134 | { 135 | [XmlBody()] 136 | public List m_items; // no explicit name, is taken from XmlCls 137 | } 138 | ``` 139 | ### Delimited attributes 140 | ```xml 141 | 142 | 143 | cool 144 | awesome 145 | fresh 146 | 147 | ``` 148 | ```csharp 149 | [XmlCls("musicTrack"))] 150 | public partial class MusicTrack 151 | { 152 | [XmlField("id")] 153 | public int m_id; 154 | 155 | [XmlBody("n")] 156 | public string m_name; 157 | 158 | [XmlField("artists"), XmlSplitStr(',')] 159 | public List m_artists; 160 | 161 | [XmlBody("tags")] 162 | public List m_tags; 163 | } 164 | ``` -------------------------------------------------------------------------------- /StackXML.Benchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BenchmarkDotNet.Running; 3 | 4 | namespace StackXML.Benchmark 5 | { 6 | public static class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | //var test = new XmlEncodeBenchmark(); 11 | //test.WriteBuffer(); 12 | //test.WriteBuffer_BestCase(); 13 | //test.WriteBuffer_WorstCase(); 14 | //test.WriteBuffer_BestCaseBaseline(); 15 | //return; 16 | 17 | var test2 = new SimpleLoadBenchmark(); 18 | var a = test2.ReadBuffer(); 19 | var b = test2.XmlDocument(); 20 | var c = test2.XmlDocument(); 21 | var d = test2.XmlSerializer(); 22 | var e = test2.XmlReader_(); 23 | if (a != b || b != c || c != d || d != e) throw new Exception(); 24 | 25 | //BenchmarkRunner.Run(); 26 | BenchmarkRunner.Run(); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /StackXML.Benchmark/SimpleLoadBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Xml; 4 | using System.Xml.Linq; 5 | using System.Xml.Serialization; 6 | using BenchmarkDotNet.Attributes; 7 | using BenchmarkDotNet.Order; 8 | 9 | namespace StackXML.Benchmark 10 | { 11 | [Orderer(SummaryOrderPolicy.FastestToSlowest)] 12 | [MemoryDiagnoser] 13 | [RPlotExporter] 14 | [BenchmarkCategory(nameof(SimpleLoadBenchmark))] 15 | public partial class SimpleLoadBenchmark 16 | { 17 | private const string s_strToDecode = ""; 18 | 19 | // XmlType and XmlAttribute from System.Xml.Serialization 20 | [XmlCls("test"), XmlType("test")] 21 | public partial class StructuredClass 22 | { 23 | [XmlField("attribute"), XmlAttribute("attribute")] public string m_attribute; 24 | } 25 | 26 | [Benchmark(Baseline=true)] 27 | public string ReadBuffer() 28 | { 29 | var parsed = XmlReadBuffer.ReadStatic(s_strToDecode); 30 | return parsed.m_attribute; 31 | } 32 | 33 | [Benchmark] 34 | public string XmlSerializer() 35 | { 36 | using var stringReader = new StringReader(s_strToDecode); 37 | using var xmlReader = XmlReader.Create(stringReader); 38 | var serializer = new XmlSerializer(typeof(StructuredClass)); 39 | var parsed = (StructuredClass)serializer.Deserialize(xmlReader); 40 | return parsed.m_attribute; 41 | } 42 | 43 | [Benchmark] 44 | public string XmlReader_() 45 | { 46 | using var stringReader = new StringReader(s_strToDecode); 47 | using var xmlReader = XmlReader.Create(stringReader); 48 | while (xmlReader.Read()) 49 | { 50 | if (!xmlReader.IsStartElement()) continue; 51 | if (xmlReader.Name != "test") continue; 52 | return xmlReader.GetAttribute("attribute"); 53 | } 54 | throw new Exception(); 55 | } 56 | 57 | [Benchmark] 58 | public string XmlDocument() 59 | { 60 | var xdoc = new XmlDocument(); 61 | xdoc.LoadXml(s_strToDecode); 62 | var node = xdoc.FirstChild; 63 | return node.Attributes["attribute"].Value; 64 | } 65 | 66 | [Benchmark] 67 | public string XDocument_() 68 | { 69 | var xdoc = XDocument.Parse(s_strToDecode); 70 | var node = xdoc.Root; 71 | return node.Attribute("attribute").Value; 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /StackXML.Benchmark/StackXML.Benchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /StackXML.Benchmark/XmlEncodeBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security; 3 | using System.Text; 4 | using System.Xml.Linq; 5 | using BenchmarkDotNet.Attributes; 6 | using BenchmarkDotNet.Order; 7 | 8 | namespace StackXML.Benchmark 9 | { 10 | [Orderer(SummaryOrderPolicy.FastestToSlowest)] 11 | [MemoryDiagnoser] 12 | public class XmlEncodeBenchmark 13 | { 14 | private const string s_strToEncode = ""; 15 | private const string s_worstCaseStr = "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"; 16 | private const string s_bestCaseStr = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 17 | 18 | private static string EncodeUsingWriteBuffer(string toEncode) 19 | { 20 | using var writeBuffer = XmlWriteBuffer.Create(); 21 | writeBuffer.EncodeText(toEncode); 22 | return writeBuffer.ToStr(); 23 | } 24 | 25 | [BenchmarkCategory("WriteBuffer"), Benchmark(Baseline = true)] 26 | public string WriteBuffer_BestCaseBaseline() 27 | { 28 | using var writeBuffer = XmlWriteBuffer.Create(); 29 | writeBuffer.PutString(s_bestCaseStr); 30 | return writeBuffer.ToStr(); 31 | } 32 | 33 | [BenchmarkCategory("WriteBuffer"), Benchmark] 34 | public string WriteBuffer() => EncodeUsingWriteBuffer(s_strToEncode); 35 | [BenchmarkCategory("WriteBuffer"), Benchmark] 36 | public string WriteBuffer_WorstCase() => EncodeUsingWriteBuffer(s_worstCaseStr); 37 | [BenchmarkCategory("WriteBuffer"), Benchmark] 38 | public string WriteBuffer_BestCase() => EncodeUsingWriteBuffer(s_bestCaseStr); 39 | 40 | [BenchmarkCategory("SecurityElement"), Benchmark] 41 | public string SecurityElement_() => SecurityElement.Escape(s_strToEncode); 42 | [BenchmarkCategory("SecurityElement"), Benchmark] 43 | public string SecurityElement_BestCase() => SecurityElement.Escape(s_bestCaseStr); 44 | [BenchmarkCategory("SecurityElement"), Benchmark] 45 | public string SecurityElement_WorstCase() => SecurityElement.Escape(s_worstCaseStr); 46 | 47 | [BenchmarkCategory("XElement"), Benchmark] 48 | public string XElement() 49 | { 50 | // ReSharper disable once PossibleNullReferenceException 51 | return new XElement("t", s_strToEncode).LastNode.ToString(); 52 | } 53 | 54 | [BenchmarkCategory("XText"), Benchmark] 55 | public string XText() 56 | { 57 | return new XText(s_strToEncode).ToString(); 58 | } 59 | 60 | [BenchmarkCategory("XmlWriter"), Benchmark] 61 | public string XmlWriter() 62 | { 63 | var settings = new System.Xml.XmlWriterSettings 64 | { 65 | ConformanceLevel = System.Xml.ConformanceLevel.Fragment 66 | }; 67 | var builder = new StringBuilder(); 68 | 69 | using var writer = System.Xml.XmlWriter.Create(builder, settings); 70 | writer.WriteString(s_strToEncode); 71 | 72 | return builder.ToString(); 73 | } 74 | 75 | [BenchmarkCategory("Westwind"), Benchmark] 76 | public string Westwind() 77 | { 78 | // ReSharper disable once PossibleNullReferenceException 79 | return XmlString(s_strToEncode, false); 80 | } 81 | 82 | // https://weblog.west-wind.com/posts/2018/Nov/30/Returning-an-XML-Encoded-String-in-NET 83 | // https://github.com/RickStrahl/Westwind.Utilities/blob/master/Westwind.Utilities/Utilities/XmlUtils.cs#L66 84 | /* 85 | MIT License 86 | =========== 87 | 88 | Copyright (c) 2012-2020 West Wind Technologies 89 | 90 | Permission is hereby granted, free of charge, to any person obtaining a copy 91 | of this software and associated documentation files (the "Software"), to deal 92 | in the Software without restriction, including without limitation the rights 93 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 94 | copies of the Software, and to permit persons to whom the Software is 95 | furnished to do so, subject to the following conditions: 96 | 97 | The above copyright notice and this permission notice shall be included in all 98 | copies or substantial portions of the Software. 99 | 100 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 101 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 102 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 103 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 104 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 105 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 106 | SOFTWARE. 107 | */ 108 | public static string XmlString(string text, bool isAttribute = false) 109 | { 110 | var sb = new StringBuilder(text.Length); 111 | 112 | foreach (var chr in text) 113 | { 114 | if (chr == '<') 115 | sb.Append("<"); 116 | else if (chr == '>') 117 | sb.Append(">"); 118 | else if (chr == '&') 119 | sb.Append("&"); 120 | 121 | // special handling for quotes 122 | else if (isAttribute && chr == '\"') 123 | sb.Append("""); 124 | else if (isAttribute && chr == '\'') 125 | sb.Append("'"); 126 | 127 | // Legal sub-chr32 characters 128 | else if (chr == '\n') 129 | sb.Append(isAttribute ? " " : "\n"); 130 | else if (chr == '\r') 131 | sb.Append(isAttribute ? " " : "\r"); 132 | else if (chr == '\t') 133 | sb.Append(isAttribute ? " " : "\t"); 134 | 135 | else 136 | { 137 | if (chr < 32) 138 | throw new InvalidOperationException("Invalid character in Xml String. Chr " + 139 | Convert.ToInt16(chr) + " is illegal."); 140 | sb.Append(chr); 141 | } 142 | } 143 | 144 | return sb.ToString(); 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /StackXML.Generator/ComputeSharp/Extensions/AttributeDataExtensions.cs: -------------------------------------------------------------------------------- 1 | // This file is ported from ComputeSharp (Sergio0694/ComputeSharp), 2 | // see LICENSE in ComputeSharp directory 3 | 4 | using System.Collections.Generic; 5 | using System.Diagnostics.CodeAnalysis; 6 | using Microsoft.CodeAnalysis; 7 | 8 | namespace ComputeSharp.SourceGeneration.Extensions; 9 | 10 | /// 11 | /// Extension methods for the type. 12 | /// 13 | internal static class AttributeDataExtensions 14 | { 15 | /// 16 | /// Tries to get the location of the input instance. 17 | /// 18 | /// The input instance to get the location for. 19 | /// The resulting location for , if a syntax reference is available. 20 | public static Location? GetLocation(this AttributeData attributeData) 21 | { 22 | if (attributeData.ApplicationSyntaxReference is { } syntaxReference) 23 | { 24 | return syntaxReference.SyntaxTree.GetLocation(syntaxReference.Span); 25 | } 26 | 27 | return null; 28 | } 29 | 30 | /// 31 | /// Tries to get a constructor argument at a given index from the input instance. 32 | /// 33 | /// The type of constructor argument to retrieve. 34 | /// The target instance to get the argument from. 35 | /// The index of the argument to try to retrieve. 36 | /// The resulting argument, if it was found. 37 | /// Whether or not an argument of type at position was found. 38 | public static bool TryGetConstructorArgument(this AttributeData attributeData, int index, [NotNullWhen(true)] out T? result) 39 | { 40 | if (attributeData.ConstructorArguments.Length > index && 41 | attributeData.ConstructorArguments[index].Value is T argument) 42 | { 43 | result = argument; 44 | 45 | return true; 46 | } 47 | 48 | result = default; 49 | 50 | return false; 51 | } 52 | 53 | /// 54 | /// Tries to get a given named argument value from an instance, if present. 55 | /// 56 | /// The type of argument to check. 57 | /// The target instance to check. 58 | /// The name of the argument to check. 59 | /// The resulting argument value, if present. 60 | /// Whether or not contains an argument named with a valid value. 61 | public static bool TryGetNamedArgument(this AttributeData attributeData, string name, out T? value) 62 | { 63 | foreach (KeyValuePair properties in attributeData.NamedArguments) 64 | { 65 | if (properties.Key == name) 66 | { 67 | value = (T?)properties.Value.Value; 68 | 69 | return true; 70 | } 71 | } 72 | 73 | value = default; 74 | 75 | return false; 76 | } 77 | } -------------------------------------------------------------------------------- /StackXML.Generator/ComputeSharp/Extensions/ISymbolExtensions.cs: -------------------------------------------------------------------------------- 1 | // This file is ported from ComputeSharp (Sergio0694/ComputeSharp), 2 | // see LICENSE in ComputeSharp directory 3 | 4 | using System; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.Threading; 7 | using Microsoft.CodeAnalysis; 8 | 9 | namespace ComputeSharp.SourceGeneration.Extensions; 10 | 11 | /// 12 | /// Extension methods for types. 13 | /// 14 | internal static class ISymbolExtensions 15 | { 16 | /// 17 | /// A custom instance with fully qualified style, without global::. 18 | /// 19 | public static readonly SymbolDisplayFormat FullyQualifiedWithoutGlobalFormat = new( 20 | globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, 21 | typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, 22 | genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, 23 | miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers); 24 | 25 | /// 26 | /// Checks whether a given symbol is accessible from its containing assembly (including eg. through nested types). 27 | /// 28 | /// The input instance. 29 | /// The instance currently in use. 30 | /// Whether is accessible from its containing assembly. 31 | public static bool IsAccessibleFromContainingAssembly(this ISymbol symbol, Compilation compilation) 32 | { 33 | // If the symbol is associated across multiple assemblies, it must be accessible 34 | if (symbol.ContainingAssembly is not IAssemblySymbol assemblySymbol) 35 | { 36 | return true; 37 | } 38 | 39 | return compilation.IsSymbolAccessibleWithin(symbol, assemblySymbol); 40 | } 41 | 42 | /// 43 | /// Checks whether a given symbol is accessible from the assembly of a given compilation (including eg. through nested types). 44 | /// 45 | /// The input instance. 46 | /// The instance currently in use. 47 | /// Whether is accessible from the assembly for . 48 | public static bool IsAccessibleFromCompilationAssembly(this ISymbol symbol, Compilation compilation) 49 | { 50 | return compilation.IsSymbolAccessibleWithin(symbol, compilation.Assembly); 51 | } 52 | 53 | /// 54 | /// Gets the fully qualified name for a given symbol. 55 | /// 56 | /// The input instance. 57 | /// Whether to include the global:: prefix. 58 | /// The fully qualified name for . 59 | public static string GetFullyQualifiedName(this ISymbol symbol, bool includeGlobal = false) 60 | { 61 | return includeGlobal 62 | ? symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) 63 | : symbol.ToDisplayString(FullyQualifiedWithoutGlobalFormat); 64 | } 65 | 66 | /// 67 | /// Gets the fully qualified name for a given symbol, including nullability annotations 68 | /// 69 | /// The input instance. 70 | /// The fully qualified name for . 71 | public static string GetFullyQualifiedNameWithNullabilityAnnotations(this ISymbol symbol) 72 | { 73 | return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier)); 74 | } 75 | 76 | /// 77 | /// Checks whether or not a given symbol has an attribute with the specified type. 78 | /// 79 | /// The input instance to check. 80 | /// The instance for the attribute type to look for. 81 | /// Whether or not has an attribute with the specified type. 82 | public static bool HasAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol) 83 | { 84 | return TryGetAttributeWithType(symbol, typeSymbol, out _); 85 | } 86 | 87 | /// 88 | /// Tries to get an attribute with the specified type. 89 | /// 90 | /// The input instance to check. 91 | /// The instance for the attribute type to look for. 92 | /// The resulting attribute, if it was found. 93 | /// Whether or not has an attribute with the specified name. 94 | public static bool TryGetAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol, [NotNullWhen(true)] out AttributeData? attributeData) 95 | { 96 | foreach (AttributeData attribute in symbol.GetAttributes()) 97 | { 98 | if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, typeSymbol)) 99 | { 100 | attributeData = attribute; 101 | 102 | return true; 103 | } 104 | } 105 | 106 | attributeData = null; 107 | 108 | return false; 109 | } 110 | 111 | /// 112 | /// Tries to get an attribute with the specified fully qualified metadata name. 113 | /// 114 | /// The input instance to check. 115 | /// The attribute name to look for. 116 | /// The resulting attribute data, if found. 117 | /// Whether or not has an attribute with the specified name. 118 | public static bool TryGetAttributeWithFullyQualifiedMetadataName(this ISymbol symbol, string name, [NotNullWhen(true)] out AttributeData? attributeData) 119 | { 120 | foreach (AttributeData attribute in symbol.GetAttributes()) 121 | { 122 | if (attribute.AttributeClass is INamedTypeSymbol attributeSymbol && 123 | attributeSymbol.HasFullyQualifiedMetadataName(name)) 124 | { 125 | attributeData = attribute; 126 | 127 | return true; 128 | } 129 | } 130 | 131 | attributeData = null; 132 | 133 | return false; 134 | } 135 | 136 | /// 137 | /// Tries to get a syntax node with a given type from an input symbol. 138 | /// 139 | /// The type of syntax node to look for. 140 | /// The input instance to get the syntax node for. 141 | /// The used to cancel the operation, if needed. 142 | /// The resulting syntax node, if found. 143 | /// Whether or not a syntax node of type was retrieved successfully. 144 | public static bool TryGetSyntaxNode(this ISymbol symbol, CancellationToken token, [NotNullWhen(true)] out T? syntaxNode) 145 | where T : SyntaxNode 146 | { 147 | // If there are no syntax references, there is nothing to do 148 | if (symbol.DeclaringSyntaxReferences is not [SyntaxReference syntaxReference, ..]) 149 | { 150 | syntaxNode = null; 151 | 152 | return false; 153 | } 154 | 155 | // Get the target node, and check that it's of the desired type 156 | T? candidateNode = syntaxReference.GetSyntax(token) as T; 157 | 158 | syntaxNode = candidateNode; 159 | 160 | return candidateNode is not null; 161 | } 162 | 163 | /// 164 | /// Gets the first symbol of a specific type among the ancestors of a given symbol. 165 | /// 166 | /// The input symbol to start the search from. 167 | /// An optional predicate to filter symbols. 168 | /// The resulting symbol, if a match was found. 169 | public static TSymbol? FirstAncestorOrSelf(this ISymbol symbol, Func? predicate = null) 170 | where TSymbol : class, ISymbol 171 | { 172 | for (ISymbol? parentSymbol = symbol; parentSymbol is not null; parentSymbol = parentSymbol.ContainingSymbol) 173 | { 174 | if (parentSymbol is TSymbol targetSymbol && (predicate?.Invoke(targetSymbol) is not false)) 175 | { 176 | return targetSymbol; 177 | } 178 | } 179 | 180 | return null; 181 | } 182 | } -------------------------------------------------------------------------------- /StackXML.Generator/ComputeSharp/Extensions/ITypeSymbolExtensions.cs: -------------------------------------------------------------------------------- 1 | // This file is ported from ComputeSharp (Sergio0694/ComputeSharp), 2 | // see LICENSE in ComputeSharp directory 3 | 4 | using System; 5 | using ComputeSharp.SourceGeneration.Helpers; 6 | using Microsoft.CodeAnalysis; 7 | 8 | namespace ComputeSharp.SourceGeneration.Extensions; 9 | 10 | /// 11 | /// Extension methods for types. 12 | /// 13 | internal static class ITypeSymbolExtensions 14 | { 15 | /// 16 | /// Gets the method of this symbol that have a particular name. 17 | /// 18 | /// The input instance to check. 19 | /// The name of the method to find. 20 | /// The target method, if present. 21 | public static IMethodSymbol? GetMethod(this ITypeSymbol symbol, string name) 22 | { 23 | foreach (ISymbol memberSymbol in symbol.GetMembers(name)) 24 | { 25 | if (memberSymbol is IMethodSymbol methodSymbol && 26 | memberSymbol.Name == name) 27 | { 28 | return methodSymbol; 29 | } 30 | } 31 | 32 | return null; 33 | } 34 | 35 | /// 36 | /// Checks whether or not a given type symbol has a specified fully qualified metadata name. 37 | /// 38 | /// The input instance to check. 39 | /// The full name to check. 40 | /// Whether has a full name equals to . 41 | public static bool HasFullyQualifiedMetadataName(this ITypeSymbol symbol, string name) 42 | { 43 | using ImmutableArrayBuilder builder = new(); 44 | 45 | symbol.AppendFullyQualifiedMetadataName(in builder); 46 | 47 | return builder.WrittenSpan.SequenceEqual(name.AsSpan()); 48 | } 49 | 50 | /// 51 | /// Checks whether or not a given inherits from a specified type. 52 | /// 53 | /// The target instance to check. 54 | /// The full name of the type to check for inheritance. 55 | /// Whether or not inherits from . 56 | public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name) 57 | { 58 | INamedTypeSymbol? baseType = typeSymbol.BaseType; 59 | 60 | while (baseType is not null) 61 | { 62 | if (baseType.HasFullyQualifiedMetadataName(name)) 63 | { 64 | return true; 65 | } 66 | 67 | baseType = baseType.BaseType; 68 | } 69 | 70 | return false; 71 | } 72 | 73 | /// 74 | /// Checks whether or not a given inherits from a specified type. 75 | /// 76 | /// The target instance to check. 77 | /// The instane to check for inheritance from. 78 | /// Whether or not inherits from . 79 | public static bool InheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol) 80 | { 81 | INamedTypeSymbol? currentBaseTypeSymbol = typeSymbol.BaseType; 82 | 83 | while (currentBaseTypeSymbol is not null) 84 | { 85 | if (SymbolEqualityComparer.Default.Equals(currentBaseTypeSymbol, baseTypeSymbol)) 86 | { 87 | return true; 88 | } 89 | 90 | currentBaseTypeSymbol = currentBaseTypeSymbol.BaseType; 91 | } 92 | 93 | return false; 94 | } 95 | 96 | /// 97 | /// Checks whether or not a given implements an interface of a specified type. 98 | /// 99 | /// The target instance to check. 100 | /// The instance to check for inheritance from. 101 | /// Whether or not has an interface of type . 102 | public static bool HasInterfaceWithType(this ITypeSymbol typeSymbol, ITypeSymbol interfaceSymbol) 103 | { 104 | foreach (INamedTypeSymbol interfaceType in typeSymbol.AllInterfaces) 105 | { 106 | if (SymbolEqualityComparer.Default.Equals(interfaceType, interfaceSymbol)) 107 | { 108 | return true; 109 | } 110 | } 111 | 112 | return false; 113 | } 114 | 115 | /// 116 | /// Gets the fully qualified metadata name for a given instance. 117 | /// 118 | /// The input instance. 119 | /// The fully qualified metadata name for . 120 | public static string GetFullyQualifiedMetadataName(this ITypeSymbol symbol) 121 | { 122 | using ImmutableArrayBuilder builder = new(); 123 | 124 | symbol.AppendFullyQualifiedMetadataName(in builder); 125 | 126 | return builder.ToString(); 127 | } 128 | 129 | /// 130 | /// Appends the fully qualified metadata name for a given symbol to a target builder. 131 | /// 132 | /// The input instance. 133 | /// The target instance. 134 | public static void AppendFullyQualifiedMetadataName(this ITypeSymbol symbol, ref readonly ImmutableArrayBuilder builder) 135 | { 136 | static void BuildFrom(ISymbol? symbol, ref readonly ImmutableArrayBuilder builder) 137 | { 138 | switch (symbol) 139 | { 140 | // Namespaces that are nested also append a leading '.' 141 | case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }: 142 | BuildFrom(symbol.ContainingNamespace, in builder); 143 | builder.Add('.'); 144 | builder.AddRange(symbol.MetadataName.AsSpan()); 145 | break; 146 | 147 | // Other namespaces (ie. the one right before global) skip the leading '.' 148 | case INamespaceSymbol { IsGlobalNamespace: false }: 149 | builder.AddRange(symbol.MetadataName.AsSpan()); 150 | break; 151 | 152 | // Types with no namespace just have their metadata name directly written 153 | case ITypeSymbol { ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true } }: 154 | builder.AddRange(symbol.MetadataName.AsSpan()); 155 | break; 156 | 157 | // Types with a containing non-global namespace also append a leading '.' 158 | case ITypeSymbol { ContainingSymbol: INamespaceSymbol namespaceSymbol }: 159 | BuildFrom(namespaceSymbol, in builder); 160 | builder.Add('.'); 161 | builder.AddRange(symbol.MetadataName.AsSpan()); 162 | break; 163 | 164 | // Nested types append a leading '+' 165 | case ITypeSymbol { ContainingSymbol: ITypeSymbol typeSymbol }: 166 | BuildFrom(typeSymbol, in builder); 167 | builder.Add('+'); 168 | builder.AddRange(symbol.MetadataName.AsSpan()); 169 | break; 170 | default: 171 | break; 172 | } 173 | } 174 | 175 | BuildFrom(symbol, in builder); 176 | } 177 | } -------------------------------------------------------------------------------- /StackXML.Generator/ComputeSharp/Extensions/IncrementalValueProviderExtensions.cs: -------------------------------------------------------------------------------- 1 | // This file is ported from ComputeSharp (Sergio0694/ComputeSharp), 2 | // see LICENSE in ComputeSharp directory 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.Immutable; 7 | using ComputeSharp.SourceGeneration.Helpers; 8 | using Microsoft.CodeAnalysis; 9 | 10 | namespace ComputeSharp.SourceGeneration.Extensions; 11 | 12 | /// 13 | /// Extension methods for . 14 | /// 15 | internal static class IncrementalValuesProviderExtensions 16 | { 17 | /// 18 | /// Groups items in a given sequence by a specified key. 19 | /// 20 | /// The type of value that this source provides access to. 21 | /// The type of resulting key elements. 22 | /// The type of resulting projected elements. 23 | /// The input instance. 24 | /// The key selection . 25 | /// The element selection . 26 | /// An with the grouped results. 27 | public static IncrementalValuesProvider<(TKey Key, EquatableArray Right)> GroupBy( 28 | this IncrementalValuesProvider source, 29 | Func keySelector, 30 | Func elementSelector) 31 | where TValues : IEquatable 32 | where TKey : IEquatable 33 | where TElement : IEquatable 34 | { 35 | return source.Collect().SelectMany((item, token) => 36 | { 37 | Dictionary.Builder> map = []; 38 | 39 | foreach (TValues value in item) 40 | { 41 | TKey key = keySelector(value); 42 | TElement element = elementSelector(value); 43 | 44 | if (!map.TryGetValue(key, out ImmutableArray.Builder builder)) 45 | { 46 | builder = ImmutableArray.CreateBuilder(); 47 | 48 | map.Add(key, builder); 49 | } 50 | 51 | builder.Add(element); 52 | } 53 | 54 | token.ThrowIfCancellationRequested(); 55 | 56 | ImmutableArray<(TKey Key, EquatableArray Elements)>.Builder result = 57 | ImmutableArray.CreateBuilder<(TKey, EquatableArray)>(); 58 | 59 | foreach (KeyValuePair.Builder> entry in map) 60 | { 61 | result.Add((entry.Key, entry.Value.ToImmutable())); 62 | } 63 | 64 | return result; 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /StackXML.Generator/ComputeSharp/Extensions/IndentedTextWriterExtensions.cs: -------------------------------------------------------------------------------- 1 | // This file is ported from ComputeSharp (Sergio0694/ComputeSharp), 2 | // see LICENSE in ComputeSharp directory 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using ComputeSharp.SourceGeneration.Helpers; 8 | 9 | namespace ComputeSharp.SourceGeneration.Extensions; 10 | 11 | /// 12 | /// Extension methods for the type. 13 | /// 14 | internal static class IndentedTextWriterExtensions 15 | { 16 | /// 17 | /// Writes the following attributes into a target writer: 18 | /// 19 | /// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] 20 | /// [global::System.Diagnostics.DebuggerNonUserCode] 21 | /// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] 22 | /// 23 | /// 24 | /// The instance to write into. 25 | /// The name of the generator. 26 | /// Whether to use fully qualified type names or not. 27 | /// Whether to also include the attribute for non-user code. 28 | public static void WriteGeneratedAttributes( 29 | this IndentedTextWriter writer, 30 | string generatorName, 31 | bool useFullyQualifiedTypeNames = true, 32 | bool includeNonUserCodeAttributes = true) 33 | { 34 | // We can use this class to get the assembly, as all files for generators are just included 35 | // via shared projects. As such, the assembly will be the same as the generator type itself. 36 | Version assemblyVersion = typeof(IndentedTextWriterExtensions).Assembly.GetName().Version; 37 | 38 | if (useFullyQualifiedTypeNames) 39 | { 40 | writer.WriteLine($$"""[global::System.CodeDom.Compiler.GeneratedCode("{{generatorName}}", "{{assemblyVersion}}")]"""); 41 | 42 | if (includeNonUserCodeAttributes) 43 | { 44 | writer.WriteLine($$"""[global::System.Diagnostics.DebuggerNonUserCode]"""); 45 | writer.WriteLine($$"""[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"""); 46 | } 47 | } 48 | else 49 | { 50 | writer.WriteLine($$"""[GeneratedCode("{{generatorName}}", "{{assemblyVersion}}")]"""); 51 | 52 | if (includeNonUserCodeAttributes) 53 | { 54 | writer.WriteLine($$"""[DebuggerNonUserCode]"""); 55 | writer.WriteLine($$"""[ExcludeFromCodeCoverage]"""); 56 | } 57 | } 58 | } 59 | 60 | /// 61 | /// Writes a sequence of using directives, sorted correctly. 62 | /// 63 | /// The instance to write into. 64 | /// The sequence of using directives to write. 65 | public static void WriteSortedUsingDirectives(this IndentedTextWriter writer, IEnumerable usingDirectives) 66 | { 67 | // Add the System directives first, in the correct order 68 | foreach (string usingDirective in usingDirectives.Where(static name => name.StartsWith("global::System", StringComparison.InvariantCulture)).OrderBy(static name => name)) 69 | { 70 | writer.WriteLine($"using {usingDirective};"); 71 | } 72 | 73 | // Add the other directives, also sorted in the correct order 74 | foreach (string usingDirective in usingDirectives.Where(static name => !name.StartsWith("global::System", StringComparison.InvariantCulture)).OrderBy(static name => name)) 75 | { 76 | writer.WriteLine($"using {usingDirective};"); 77 | } 78 | 79 | // Leave a trailing blank line if at least one using directive has been written. 80 | // This is so that any members will correctly have a leading blank line before. 81 | writer.WriteLineIf(usingDirectives.Any()); 82 | } 83 | 84 | /// 85 | /// Writes a series of members separated by one line between each of them. 86 | /// 87 | /// The type of input items to process. 88 | /// The instance to write into. 89 | /// The input items to process. 90 | /// The instance to invoke for each item. 91 | public static void WriteLineSeparatedMembers( 92 | this IndentedTextWriter writer, 93 | ReadOnlySpan items, 94 | IndentedTextWriter.Callback callback) 95 | { 96 | for (int i = 0; i < items.Length; i++) 97 | { 98 | if (i > 0) 99 | { 100 | writer.WriteLine(); 101 | } 102 | 103 | callback(items[i], writer); 104 | } 105 | } 106 | 107 | /// 108 | /// Writes a series of initialization expressions separated by a comma between each of them. 109 | /// 110 | /// The type of input items to process. 111 | /// The instance to write into. 112 | /// The input items to process. 113 | /// The instance to invoke for each item. 114 | public static void WriteInitializationExpressions( 115 | this IndentedTextWriter writer, 116 | ReadOnlySpan items, 117 | IndentedTextWriter.Callback callback) 118 | { 119 | for (int i = 0; i < items.Length; i++) 120 | { 121 | callback(items[i], writer); 122 | 123 | if (i < items.Length - 1) 124 | { 125 | writer.WriteLine(","); 126 | } 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /StackXML.Generator/ComputeSharp/Helpers/EquatableArray{T}.cs: -------------------------------------------------------------------------------- 1 | // This file is ported from ComputeSharp (Sergio0694/ComputeSharp), 2 | // see LICENSE in ComputeSharp directory 3 | 4 | // Licensed to the .NET Foundation under one or more agreements. 5 | // The .NET Foundation licenses this file to you under the MIT license. 6 | // See the LICENSE file in the project root for more information. 7 | 8 | using System; 9 | using System.Collections; 10 | using System.Collections.Generic; 11 | using System.Collections.Immutable; 12 | using System.Linq; 13 | using System.Runtime.CompilerServices; 14 | using System.Runtime.InteropServices; 15 | 16 | namespace ComputeSharp.SourceGeneration.Helpers; 17 | 18 | /// 19 | /// Extensions for . 20 | /// 21 | internal static class EquatableArray 22 | { 23 | /// 24 | /// Creates an instance from a given . 25 | /// 26 | /// The type of items in the input array. 27 | /// The input instance. 28 | /// An instance from a given . 29 | public static EquatableArray AsEquatableArray(this ImmutableArray array) 30 | where T : IEquatable 31 | { 32 | return new(array); 33 | } 34 | } 35 | 36 | /// 37 | /// An imutable, equatable array. This is equivalent to but with value equality support. 38 | /// 39 | /// The type of values in the array. 40 | /// The input to wrap. 41 | internal readonly struct EquatableArray(ImmutableArray array) : IEquatable>, IEnumerable 42 | where T : IEquatable 43 | { 44 | /// 45 | /// The underlying array. 46 | /// 47 | private readonly T[]? array = Unsafe.As, T[]?>(ref array); 48 | 49 | /// 50 | /// Gets a reference to an item at a specified position within the array. 51 | /// 52 | /// The index of the item to retrieve a reference to. 53 | /// A reference to an item at a specified position within the array. 54 | public ref readonly T this[int index] 55 | { 56 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 57 | get => ref AsImmutableArray().ItemRef(index); 58 | } 59 | 60 | /// 61 | /// Gets a value indicating whether the current array is empty. 62 | /// 63 | public bool IsEmpty 64 | { 65 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 66 | get => AsImmutableArray().IsEmpty; 67 | } 68 | 69 | /// 70 | /// Gets a value indicating whether the current array is default or empty. 71 | /// 72 | public bool IsDefaultOrEmpty 73 | { 74 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 75 | get => AsImmutableArray().IsDefaultOrEmpty; 76 | } 77 | 78 | /// 79 | /// Gets the length of the current array. 80 | /// 81 | public int Length 82 | { 83 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 84 | get => AsImmutableArray().Length; 85 | } 86 | 87 | /// 88 | public bool Equals(EquatableArray array) 89 | { 90 | return AsSpan().SequenceEqual(array.AsSpan()); 91 | } 92 | 93 | /// 94 | public override bool Equals(object? obj) 95 | { 96 | return obj is EquatableArray array && Equals(this, array); 97 | } 98 | 99 | /// 100 | public override unsafe int GetHashCode() 101 | { 102 | if (this.array is not T[] array) 103 | { 104 | return 0; 105 | } 106 | 107 | HashCode hashCode = default; 108 | 109 | if (typeof(T) == typeof(byte)) 110 | { 111 | ReadOnlySpan span = array; 112 | ref T r0 = ref MemoryMarshal.GetReference(span); 113 | ref byte r1 = ref Unsafe.As(ref r0); 114 | 115 | fixed (byte* p = &r1) 116 | { 117 | ReadOnlySpan bytes = new(p, span.Length); 118 | 119 | hashCode.AddBytes(bytes); 120 | } 121 | } 122 | else 123 | { 124 | foreach (T item in array) 125 | { 126 | hashCode.Add(item); 127 | } 128 | } 129 | 130 | return hashCode.ToHashCode(); 131 | } 132 | 133 | /// 134 | /// Gets an instance from the current . 135 | /// 136 | /// The from the current . 137 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 138 | public ImmutableArray AsImmutableArray() 139 | { 140 | return Unsafe.As>(ref Unsafe.AsRef(in this.array)); 141 | } 142 | 143 | /// 144 | /// Creates an instance from a given . 145 | /// 146 | /// The input instance. 147 | /// An instance from a given . 148 | public static EquatableArray FromImmutableArray(ImmutableArray array) 149 | { 150 | return new(array); 151 | } 152 | 153 | /// 154 | /// Returns a wrapping the current items. 155 | /// 156 | /// A wrapping the current items. 157 | public ReadOnlySpan AsSpan() 158 | { 159 | return AsImmutableArray().AsSpan(); 160 | } 161 | 162 | /// 163 | /// Copies the contents of this instance. to a mutable array. 164 | /// 165 | /// The newly instantiated array. 166 | public T[] ToArray() 167 | { 168 | return [.. AsImmutableArray()]; 169 | } 170 | 171 | /// 172 | /// Gets an value to traverse items in the current array. 173 | /// 174 | /// An value to traverse items in the current array. 175 | public ImmutableArray.Enumerator GetEnumerator() 176 | { 177 | return AsImmutableArray().GetEnumerator(); 178 | } 179 | 180 | /// 181 | IEnumerator IEnumerable.GetEnumerator() 182 | { 183 | return ((IEnumerable)AsImmutableArray()).GetEnumerator(); 184 | } 185 | 186 | /// 187 | IEnumerator IEnumerable.GetEnumerator() 188 | { 189 | return ((IEnumerable)AsImmutableArray()).GetEnumerator(); 190 | } 191 | 192 | /// 193 | /// Implicitly converts an to . 194 | /// 195 | /// An instance from a given . 196 | public static implicit operator EquatableArray(ImmutableArray array) => FromImmutableArray(array); 197 | 198 | /// 199 | /// Implicitly converts an to . 200 | /// 201 | /// An instance from a given . 202 | public static implicit operator ImmutableArray(EquatableArray array) => array.AsImmutableArray(); 203 | 204 | /// 205 | /// Checks whether two values are the same. 206 | /// 207 | /// The first value. 208 | /// The second value. 209 | /// Whether and are equal. 210 | public static bool operator ==(EquatableArray left, EquatableArray right) => left.Equals(right); 211 | 212 | /// 213 | /// Checks whether two values are not the same. 214 | /// 215 | /// The first value. 216 | /// The second value. 217 | /// Whether and are not equal. 218 | public static bool operator !=(EquatableArray left, EquatableArray right) => !left.Equals(right); 219 | } -------------------------------------------------------------------------------- /StackXML.Generator/ComputeSharp/Helpers/HashCode.cs: -------------------------------------------------------------------------------- 1 | // This file is ported from ComputeSharp (Sergio0694/ComputeSharp), 2 | // see LICENSE in ComputeSharp directory 3 | 4 | using System.Collections.Generic; 5 | using System.ComponentModel; 6 | using System.Runtime.CompilerServices; 7 | using System.Runtime.InteropServices; 8 | using System.Security.Cryptography; 9 | 10 | #pragma warning disable CS0809, IDE0009, IDE1006, IDE0048, CA1065 11 | 12 | namespace System; 13 | 14 | /// 15 | /// A polyfill type that mirrors some methods from on .7. 16 | /// 17 | internal struct HashCode 18 | { 19 | private const uint Prime1 = 2654435761U; 20 | private const uint Prime2 = 2246822519U; 21 | private const uint Prime3 = 3266489917U; 22 | private const uint Prime4 = 668265263U; 23 | private const uint Prime5 = 374761393U; 24 | 25 | private static readonly uint seed = GenerateGlobalSeed(); 26 | 27 | private uint v1, v2, v3, v4; 28 | private uint queue1, queue2, queue3; 29 | private uint length; 30 | 31 | /// 32 | /// Initializes the default seed. 33 | /// 34 | /// A random seed. 35 | private static unsafe uint GenerateGlobalSeed() 36 | { 37 | byte[] bytes = new byte[4]; 38 | 39 | RandomNumberGenerator.Create().GetBytes(bytes); 40 | 41 | return BitConverter.ToUInt32(bytes, 0); 42 | } 43 | 44 | /// 45 | /// Combines a value into a hash code. 46 | /// 47 | /// The type of the value to combine into the hash code. 48 | /// The value to combine into the hash code. 49 | /// The hash code that represents the value. 50 | public static int Combine(T1 value) 51 | { 52 | uint hc1 = (uint)(value?.GetHashCode() ?? 0); 53 | uint hash = MixEmptyState(); 54 | 55 | hash += 4; 56 | hash = QueueRound(hash, hc1); 57 | hash = MixFinal(hash); 58 | 59 | return (int)hash; 60 | } 61 | 62 | /// 63 | /// Combines two values into a hash code. 64 | /// 65 | /// The type of the first value to combine into the hash code. 66 | /// The type of the second value to combine into the hash code. 67 | /// The first value to combine into the hash code. 68 | /// The second value to combine into the hash code. 69 | /// The hash code that represents the values. 70 | public static int Combine(T1 value1, T2 value2) 71 | { 72 | uint hc1 = (uint)(value1?.GetHashCode() ?? 0); 73 | uint hc2 = (uint)(value2?.GetHashCode() ?? 0); 74 | uint hash = MixEmptyState(); 75 | 76 | hash += 8; 77 | hash = QueueRound(hash, hc1); 78 | hash = QueueRound(hash, hc2); 79 | hash = MixFinal(hash); 80 | 81 | return (int)hash; 82 | } 83 | 84 | /// 85 | /// Combines three values into a hash code. 86 | /// 87 | /// The type of the first value to combine into the hash code. 88 | /// The type of the second value to combine into the hash code. 89 | /// The type of the third value to combine into the hash code. 90 | /// The first value to combine into the hash code. 91 | /// The second value to combine into the hash code. 92 | /// The third value to combine into the hash code. 93 | /// The hash code that represents the values. 94 | public static int Combine(T1 value1, T2 value2, T3 value3) 95 | { 96 | uint hc1 = (uint)(value1?.GetHashCode() ?? 0); 97 | uint hc2 = (uint)(value2?.GetHashCode() ?? 0); 98 | uint hc3 = (uint)(value3?.GetHashCode() ?? 0); 99 | uint hash = MixEmptyState(); 100 | 101 | hash += 12; 102 | hash = QueueRound(hash, hc1); 103 | hash = QueueRound(hash, hc2); 104 | hash = QueueRound(hash, hc3); 105 | hash = MixFinal(hash); 106 | 107 | return (int)hash; 108 | } 109 | 110 | /// 111 | /// Combines four values into a hash code. 112 | /// 113 | /// The type of the first value to combine into the hash code. 114 | /// The type of the second value to combine into the hash code. 115 | /// The type of the third value to combine into the hash code. 116 | /// The type of the fourth value to combine into the hash code. 117 | /// The first value to combine into the hash code. 118 | /// The second value to combine into the hash code. 119 | /// The third value to combine into the hash code. 120 | /// The fourth value to combine into the hash code. 121 | /// The hash code that represents the values. 122 | public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4) 123 | { 124 | uint hc1 = (uint)(value1?.GetHashCode() ?? 0); 125 | uint hc2 = (uint)(value2?.GetHashCode() ?? 0); 126 | uint hc3 = (uint)(value3?.GetHashCode() ?? 0); 127 | uint hc4 = (uint)(value4?.GetHashCode() ?? 0); 128 | 129 | Initialize(out uint v1, out uint v2, out uint v3, out uint v4); 130 | 131 | v1 = Round(v1, hc1); 132 | v2 = Round(v2, hc2); 133 | v3 = Round(v3, hc3); 134 | v4 = Round(v4, hc4); 135 | 136 | uint hash = MixState(v1, v2, v3, v4); 137 | 138 | hash += 16; 139 | hash = MixFinal(hash); 140 | 141 | return (int)hash; 142 | } 143 | 144 | /// 145 | /// Combines five values into a hash code. 146 | /// 147 | /// The type of the first value to combine into the hash code. 148 | /// The type of the second value to combine into the hash code. 149 | /// The type of the third value to combine into the hash code. 150 | /// The type of the fourth value to combine into the hash code. 151 | /// The type of the fifth value to combine into the hash code. 152 | /// The first value to combine into the hash code. 153 | /// The second value to combine into the hash code. 154 | /// The third value to combine into the hash code. 155 | /// The fourth value to combine into the hash code. 156 | /// The fifth value to combine into the hash code. 157 | /// The hash code that represents the values. 158 | public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) 159 | { 160 | uint hc1 = (uint)(value1?.GetHashCode() ?? 0); 161 | uint hc2 = (uint)(value2?.GetHashCode() ?? 0); 162 | uint hc3 = (uint)(value3?.GetHashCode() ?? 0); 163 | uint hc4 = (uint)(value4?.GetHashCode() ?? 0); 164 | uint hc5 = (uint)(value5?.GetHashCode() ?? 0); 165 | 166 | Initialize(out uint v1, out uint v2, out uint v3, out uint v4); 167 | 168 | v1 = Round(v1, hc1); 169 | v2 = Round(v2, hc2); 170 | v3 = Round(v3, hc3); 171 | v4 = Round(v4, hc4); 172 | 173 | uint hash = MixState(v1, v2, v3, v4); 174 | 175 | hash += 20; 176 | hash = QueueRound(hash, hc5); 177 | hash = MixFinal(hash); 178 | 179 | return (int)hash; 180 | } 181 | 182 | /// 183 | /// Combines six values into a hash code. 184 | /// 185 | /// The type of the first value to combine into the hash code. 186 | /// The type of the second value to combine into the hash code. 187 | /// The type of the third value to combine into the hash code. 188 | /// The type of the fourth value to combine into the hash code. 189 | /// The type of the fifth value to combine into the hash code. 190 | /// The type of the sixth value to combine into the hash code. 191 | /// The first value to combine into the hash code. 192 | /// The second value to combine into the hash code. 193 | /// The third value to combine into the hash code. 194 | /// The fourth value to combine into the hash code. 195 | /// The fifth value to combine into the hash code. 196 | /// The sixth value to combine into the hash code. 197 | /// The hash code that represents the values. 198 | public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) 199 | { 200 | uint hc1 = (uint)(value1?.GetHashCode() ?? 0); 201 | uint hc2 = (uint)(value2?.GetHashCode() ?? 0); 202 | uint hc3 = (uint)(value3?.GetHashCode() ?? 0); 203 | uint hc4 = (uint)(value4?.GetHashCode() ?? 0); 204 | uint hc5 = (uint)(value5?.GetHashCode() ?? 0); 205 | uint hc6 = (uint)(value6?.GetHashCode() ?? 0); 206 | 207 | Initialize(out uint v1, out uint v2, out uint v3, out uint v4); 208 | 209 | v1 = Round(v1, hc1); 210 | v2 = Round(v2, hc2); 211 | v3 = Round(v3, hc3); 212 | v4 = Round(v4, hc4); 213 | 214 | uint hash = MixState(v1, v2, v3, v4); 215 | 216 | hash += 24; 217 | hash = QueueRound(hash, hc5); 218 | hash = QueueRound(hash, hc6); 219 | hash = MixFinal(hash); 220 | 221 | return (int)hash; 222 | } 223 | 224 | /// 225 | /// Combines seven values into a hash code. 226 | /// 227 | /// The type of the first value to combine into the hash code. 228 | /// The type of the second value to combine into the hash code. 229 | /// The type of the third value to combine into the hash code. 230 | /// The type of the fourth value to combine into the hash code. 231 | /// The type of the fifth value to combine into the hash code. 232 | /// The type of the sixth value to combine into the hash code. 233 | /// The type of the seventh value to combine into the hash code. 234 | /// The first value to combine into the hash code. 235 | /// The second value to combine into the hash code. 236 | /// The third value to combine into the hash code. 237 | /// The fourth value to combine into the hash code. 238 | /// The fifth value to combine into the hash code. 239 | /// The sixth value to combine into the hash code. 240 | /// The seventh value to combine into the hash code. 241 | /// The hash code that represents the values. 242 | public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) 243 | { 244 | uint hc1 = (uint)(value1?.GetHashCode() ?? 0); 245 | uint hc2 = (uint)(value2?.GetHashCode() ?? 0); 246 | uint hc3 = (uint)(value3?.GetHashCode() ?? 0); 247 | uint hc4 = (uint)(value4?.GetHashCode() ?? 0); 248 | uint hc5 = (uint)(value5?.GetHashCode() ?? 0); 249 | uint hc6 = (uint)(value6?.GetHashCode() ?? 0); 250 | uint hc7 = (uint)(value7?.GetHashCode() ?? 0); 251 | 252 | Initialize(out uint v1, out uint v2, out uint v3, out uint v4); 253 | 254 | v1 = Round(v1, hc1); 255 | v2 = Round(v2, hc2); 256 | v3 = Round(v3, hc3); 257 | v4 = Round(v4, hc4); 258 | 259 | uint hash = MixState(v1, v2, v3, v4); 260 | 261 | hash += 28; 262 | hash = QueueRound(hash, hc5); 263 | hash = QueueRound(hash, hc6); 264 | hash = QueueRound(hash, hc7); 265 | hash = MixFinal(hash); 266 | 267 | return (int)hash; 268 | } 269 | 270 | /// 271 | /// Combines eight values into a hash code. 272 | /// 273 | /// The type of the first value to combine into the hash code. 274 | /// The type of the second value to combine into the hash code. 275 | /// The type of the third value to combine into the hash code. 276 | /// The type of the fourth value to combine into the hash code. 277 | /// The type of the fifth value to combine into the hash code. 278 | /// The type of the sixth value to combine into the hash code. 279 | /// The type of the seventh value to combine into the hash code. 280 | /// The type of the eighth value to combine into the hash code. 281 | /// The first value to combine into the hash code. 282 | /// The second value to combine into the hash code. 283 | /// The third value to combine into the hash code. 284 | /// The fourth value to combine into the hash code. 285 | /// The fifth value to combine into the hash code. 286 | /// The sixth value to combine into the hash code. 287 | /// The seventh value to combine into the hash code. 288 | /// The eighth value to combine into the hash code. 289 | /// The hash code that represents the values. 290 | public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) 291 | { 292 | uint hc1 = (uint)(value1?.GetHashCode() ?? 0); 293 | uint hc2 = (uint)(value2?.GetHashCode() ?? 0); 294 | uint hc3 = (uint)(value3?.GetHashCode() ?? 0); 295 | uint hc4 = (uint)(value4?.GetHashCode() ?? 0); 296 | uint hc5 = (uint)(value5?.GetHashCode() ?? 0); 297 | uint hc6 = (uint)(value6?.GetHashCode() ?? 0); 298 | uint hc7 = (uint)(value7?.GetHashCode() ?? 0); 299 | uint hc8 = (uint)(value8?.GetHashCode() ?? 0); 300 | 301 | Initialize(out uint v1, out uint v2, out uint v3, out uint v4); 302 | 303 | v1 = Round(v1, hc1); 304 | v2 = Round(v2, hc2); 305 | v3 = Round(v3, hc3); 306 | v4 = Round(v4, hc4); 307 | 308 | v1 = Round(v1, hc5); 309 | v2 = Round(v2, hc6); 310 | v3 = Round(v3, hc7); 311 | v4 = Round(v4, hc8); 312 | 313 | uint hash = MixState(v1, v2, v3, v4); 314 | 315 | hash += 32; 316 | hash = MixFinal(hash); 317 | 318 | return (int)hash; 319 | } 320 | 321 | /// 322 | /// Adds a single value to the current hash. 323 | /// 324 | /// The type of the value to add into the hash code. 325 | /// The value to add into the hash code. 326 | public void Add(T value) 327 | { 328 | Add(value?.GetHashCode() ?? 0); 329 | } 330 | 331 | /// 332 | /// Adds a single value to the current hash. 333 | /// 334 | /// The type of the value to add into the hash code. 335 | /// The value to add into the hash code. 336 | /// The instance to use. 337 | public void Add(T value, IEqualityComparer? comparer) 338 | { 339 | Add(value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode())); 340 | } 341 | 342 | /// 343 | /// Adds a span of bytes to the hash code. 344 | /// 345 | /// The span. 346 | public void AddBytes(ReadOnlySpan value) 347 | { 348 | ref byte pos = ref MemoryMarshal.GetReference(value); 349 | ref byte end = ref Unsafe.Add(ref pos, value.Length); 350 | 351 | while ((nint)Unsafe.ByteOffset(ref pos, ref end) >= sizeof(int)) 352 | { 353 | Add(Unsafe.ReadUnaligned(ref pos)); 354 | pos = ref Unsafe.Add(ref pos, sizeof(int)); 355 | } 356 | 357 | while (Unsafe.IsAddressLessThan(ref pos, ref end)) 358 | { 359 | Add((int)pos); 360 | pos = ref Unsafe.Add(ref pos, 1); 361 | } 362 | } 363 | 364 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 365 | private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) 366 | { 367 | v1 = seed + Prime1 + Prime2; 368 | v2 = seed + Prime2; 369 | v3 = seed; 370 | v4 = seed - Prime1; 371 | } 372 | 373 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 374 | private static uint Round(uint hash, uint input) 375 | { 376 | return RotateLeft(hash + input * Prime2, 13) * Prime1; 377 | } 378 | 379 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 380 | private static uint QueueRound(uint hash, uint queuedValue) 381 | { 382 | return RotateLeft(hash + queuedValue * Prime3, 17) * Prime4; 383 | } 384 | 385 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 386 | private static uint MixState(uint v1, uint v2, uint v3, uint v4) 387 | { 388 | return RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); 389 | } 390 | 391 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 392 | private static uint MixEmptyState() 393 | { 394 | return seed + Prime5; 395 | } 396 | 397 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 398 | private static uint MixFinal(uint hash) 399 | { 400 | hash ^= hash >> 15; 401 | hash *= Prime2; 402 | hash ^= hash >> 13; 403 | hash *= Prime3; 404 | hash ^= hash >> 16; 405 | 406 | return hash; 407 | } 408 | 409 | private void Add(int value) 410 | { 411 | uint val = (uint)value; 412 | uint previousLength = length++; 413 | uint position = previousLength % 4; 414 | 415 | if (position == 0) 416 | { 417 | queue1 = val; 418 | } 419 | else if (position == 1) 420 | { 421 | queue2 = val; 422 | } 423 | else if (position == 2) 424 | { 425 | queue3 = val; 426 | } 427 | else 428 | { 429 | if (previousLength == 3) 430 | { 431 | Initialize(out v1, out v2, out v3, out v4); 432 | } 433 | 434 | v1 = Round(v1, queue1); 435 | v2 = Round(v2, queue2); 436 | v3 = Round(v3, queue3); 437 | v4 = Round(v4, val); 438 | } 439 | } 440 | 441 | /// 442 | /// Gets the resulting hashcode from the current instance. 443 | /// 444 | /// The resulting hashcode from the current instance. 445 | public readonly int ToHashCode() 446 | { 447 | uint length = this.length; 448 | uint position = length % 4; 449 | uint hash = length < 4 ? MixEmptyState() : MixState(v1, v2, v3, v4); 450 | 451 | hash += length * 4; 452 | 453 | if (position > 0) 454 | { 455 | hash = QueueRound(hash, queue1); 456 | 457 | if (position > 1) 458 | { 459 | hash = QueueRound(hash, queue2); 460 | 461 | if (position > 2) 462 | { 463 | hash = QueueRound(hash, queue3); 464 | } 465 | } 466 | } 467 | 468 | hash = MixFinal(hash); 469 | 470 | return (int)hash; 471 | } 472 | 473 | /// 474 | [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] 475 | [EditorBrowsable(EditorBrowsableState.Never)] 476 | public override int GetHashCode() 477 | { 478 | throw new NotSupportedException(); 479 | } 480 | 481 | /// 482 | [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] 483 | [EditorBrowsable(EditorBrowsableState.Never)] 484 | public override bool Equals(object? obj) 485 | { 486 | throw new NotSupportedException(); 487 | } 488 | 489 | /// 490 | /// Rotates the specified value left by the specified number of bits. 491 | /// Similar in behavior to the x86 instruction ROL. 492 | /// 493 | /// The value to rotate. 494 | /// The number of bits to rotate by. 495 | /// Any value outside the range [0..31] is treated as congruent mod 32. 496 | /// The rotated value. 497 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 498 | private static uint RotateLeft(uint value, int offset) 499 | { 500 | return (value << offset) | (value >> (32 - offset)); 501 | } 502 | } -------------------------------------------------------------------------------- /StackXML.Generator/ComputeSharp/Helpers/ImmutableArrayBuilder{T}.cs: -------------------------------------------------------------------------------- 1 | // This file is ported from ComputeSharp (Sergio0694/ComputeSharp), 2 | // see LICENSE in ComputeSharp directory 3 | 4 | // Licensed to the .NET Foundation under one or more agreements. 5 | // The .NET Foundation licenses this file to you under the MIT license. 6 | // See the LICENSE file in the project root for more information. 7 | 8 | using System; 9 | using System.Collections; 10 | using System.Collections.Generic; 11 | using System.Collections.Immutable; 12 | using System.Runtime.CompilerServices; 13 | 14 | namespace ComputeSharp.SourceGeneration.Helpers; 15 | 16 | /// 17 | /// A helper type to build sequences of values with pooled buffers. 18 | /// 19 | /// The type of items to create sequences for. 20 | internal struct ImmutableArrayBuilder : IDisposable 21 | { 22 | /// 23 | /// The shared instance to share objects. 24 | /// 25 | private static readonly ObjectPool SharedObjectPool = new(static () => new Writer()); 26 | 27 | /// 28 | /// The rented instance to use. 29 | /// 30 | private Writer? writer; 31 | 32 | /// 33 | /// Creates a new object. 34 | /// 35 | public ImmutableArrayBuilder() 36 | { 37 | this.writer = SharedObjectPool.Allocate(); 38 | } 39 | 40 | /// 41 | /// Gets the data written to the underlying buffer so far, as a . 42 | /// 43 | public readonly ReadOnlySpan WrittenSpan 44 | { 45 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 46 | get => this.writer!.WrittenSpan; 47 | } 48 | 49 | /// 50 | /// Gets the number of elements currently written in the current instance. 51 | /// 52 | public readonly int Count 53 | { 54 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 55 | get => this.writer!.Count; 56 | } 57 | 58 | /// 59 | /// Advances the current writer and gets a to the requested memory area. 60 | /// 61 | /// The requested size to advance by. 62 | /// A to the requested memory area. 63 | /// 64 | /// No other data should be written to the builder while the returned 65 | /// is in use, as it could invalidate the memory area wrapped by it, if resizing occurs. 66 | /// 67 | public readonly Span Advance(int requestedSize) 68 | { 69 | return this.writer!.Advance(requestedSize); 70 | } 71 | 72 | /// 73 | public readonly void Add(T item) 74 | { 75 | this.writer!.Add(item); 76 | } 77 | 78 | /// 79 | /// Adds the specified items to the end of the array. 80 | /// 81 | /// The items to add at the end of the array. 82 | public readonly void AddRange(ReadOnlySpan items) 83 | { 84 | this.writer!.AddRange(items); 85 | } 86 | 87 | /// 88 | public readonly void Clear() 89 | { 90 | this.writer!.Clear(); 91 | } 92 | 93 | /// 94 | /// Inserts an item to the builder at the specified index. 95 | /// 96 | /// The zero-based index at which should be inserted. 97 | /// The object to insert into the current instance. 98 | public readonly void Insert(int index, T item) 99 | { 100 | this.writer!.Insert(index, item); 101 | } 102 | 103 | /// 104 | /// Gets an instance for the current builder. 105 | /// 106 | /// An instance for the current builder. 107 | /// 108 | /// The builder should not be mutated while an enumerator is in use. 109 | /// 110 | public readonly IEnumerable AsEnumerable() 111 | { 112 | return this.writer!; 113 | } 114 | 115 | /// 116 | public readonly ImmutableArray ToImmutable() 117 | { 118 | T[] array = this.writer!.WrittenSpan.ToArray(); 119 | 120 | return Unsafe.As>(ref array); 121 | } 122 | 123 | /// 124 | public readonly T[] ToArray() 125 | { 126 | return this.writer!.WrittenSpan.ToArray(); 127 | } 128 | 129 | /// 130 | public override readonly string ToString() 131 | { 132 | return this.writer!.WrittenSpan.ToString(); 133 | } 134 | 135 | /// 136 | public void Dispose() 137 | { 138 | Writer? writer = this.writer; 139 | 140 | this.writer = null; 141 | 142 | if (writer is not null) 143 | { 144 | writer.Clear(); 145 | 146 | SharedObjectPool.Free(writer); 147 | } 148 | } 149 | 150 | /// 151 | /// A class handling the actual buffer writing. 152 | /// 153 | private sealed class Writer : IList, IReadOnlyList 154 | { 155 | /// 156 | /// The underlying array. 157 | /// 158 | private T[] array; 159 | 160 | /// 161 | /// The starting offset within . 162 | /// 163 | private int index; 164 | 165 | /// 166 | /// Creates a new instance with the specified parameters. 167 | /// 168 | public Writer() 169 | { 170 | if (typeof(T) == typeof(char)) 171 | { 172 | this.array = new T[1024]; 173 | } 174 | else 175 | { 176 | this.array = new T[8]; 177 | } 178 | 179 | this.index = 0; 180 | } 181 | 182 | /// 183 | public int Count => this.index; 184 | 185 | /// 186 | public ReadOnlySpan WrittenSpan 187 | { 188 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 189 | get => new(this.array, 0, this.index); 190 | } 191 | 192 | /// 193 | bool ICollection.IsReadOnly => true; 194 | 195 | /// 196 | T IReadOnlyList.this[int index] => WrittenSpan[index]; 197 | 198 | /// 199 | T IList.this[int index] 200 | { 201 | get => WrittenSpan[index]; 202 | set => throw new NotSupportedException(); 203 | } 204 | 205 | /// 206 | public Span Advance(int requestedSize) 207 | { 208 | EnsureCapacity(requestedSize); 209 | 210 | Span span = this.array.AsSpan(this.index, requestedSize); 211 | 212 | this.index += requestedSize; 213 | 214 | return span; 215 | } 216 | 217 | /// 218 | public void Add(T value) 219 | { 220 | EnsureCapacity(1); 221 | 222 | this.array[this.index++] = value; 223 | } 224 | 225 | /// 226 | public void AddRange(ReadOnlySpan items) 227 | { 228 | EnsureCapacity(items.Length); 229 | 230 | items.CopyTo(this.array.AsSpan(this.index)); 231 | 232 | this.index += items.Length; 233 | } 234 | 235 | /// 236 | public void Insert(int index, T item) 237 | { 238 | if (index < 0 || index > this.index) 239 | { 240 | ImmutableArrayBuilder.ThrowArgumentOutOfRangeExceptionForIndex(); 241 | } 242 | 243 | EnsureCapacity(1); 244 | 245 | if (index < this.index) 246 | { 247 | Array.Copy(this.array, index, this.array, index + 1, this.index - index); 248 | } 249 | 250 | this.array[index] = item; 251 | this.index++; 252 | } 253 | 254 | /// 255 | public void Clear() 256 | { 257 | if (typeof(T) != typeof(byte) && 258 | typeof(T) != typeof(char) && 259 | typeof(T) != typeof(int)) 260 | { 261 | this.array.AsSpan(0, this.index).Clear(); 262 | } 263 | 264 | this.index = 0; 265 | } 266 | 267 | /// 268 | /// Ensures that has enough free space to contain a given number of new items. 269 | /// 270 | /// The minimum number of items to ensure space for in . 271 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 272 | private void EnsureCapacity(int requestedSize) 273 | { 274 | if (requestedSize > this.array.Length - this.index) 275 | { 276 | ResizeBuffer(requestedSize); 277 | } 278 | } 279 | 280 | /// 281 | /// Resizes to ensure it can fit the specified number of new items. 282 | /// 283 | /// The minimum number of items to ensure space for in . 284 | [MethodImpl(MethodImplOptions.NoInlining)] 285 | private void ResizeBuffer(int sizeHint) 286 | { 287 | int minimumSize = this.index + sizeHint; 288 | int requestedSize = Math.Max(this.array.Length * 2, minimumSize); 289 | 290 | T[] newArray = new T[requestedSize]; 291 | 292 | Array.Copy(this.array, newArray, this.index); 293 | 294 | this.array = newArray; 295 | } 296 | 297 | /// 298 | int IList.IndexOf(T item) 299 | { 300 | return Array.IndexOf(this.array, item, 0, this.index); 301 | } 302 | 303 | /// 304 | void IList.RemoveAt(int index) 305 | { 306 | throw new NotSupportedException(); 307 | } 308 | 309 | /// 310 | bool ICollection.Contains(T item) 311 | { 312 | return Array.IndexOf(this.array, item, 0, this.index) >= 0; 313 | } 314 | 315 | /// 316 | void ICollection.CopyTo(T[] array, int arrayIndex) 317 | { 318 | Array.Copy(this.array, 0, array, arrayIndex, this.index); 319 | } 320 | 321 | /// 322 | bool ICollection.Remove(T item) 323 | { 324 | throw new NotSupportedException(); 325 | } 326 | 327 | /// 328 | IEnumerator IEnumerable.GetEnumerator() 329 | { 330 | T?[] array = this.array!; 331 | int length = this.index; 332 | 333 | for (int i = 0; i < length; i++) 334 | { 335 | yield return array[i]!; 336 | } 337 | } 338 | 339 | /// 340 | IEnumerator IEnumerable.GetEnumerator() 341 | { 342 | return ((IEnumerable)this).GetEnumerator(); 343 | } 344 | } 345 | } 346 | 347 | /// 348 | /// Private helpers for the type. 349 | /// 350 | file static class ImmutableArrayBuilder 351 | { 352 | /// 353 | /// Throws an for "index". 354 | /// 355 | public static void ThrowArgumentOutOfRangeExceptionForIndex() 356 | { 357 | throw new ArgumentOutOfRangeException("index"); 358 | } 359 | } -------------------------------------------------------------------------------- /StackXML.Generator/ComputeSharp/Helpers/IndentedTextWriter.cs: -------------------------------------------------------------------------------- 1 | // This file is ported from ComputeSharp (Sergio0694/ComputeSharp), 2 | // see LICENSE in ComputeSharp directory 3 | 4 | // Licensed to the .NET Foundation under one or more agreements. 5 | // The .NET Foundation licenses this file to you under the MIT license. 6 | // See the LICENSE file in the project root for more information. 7 | 8 | using System; 9 | using System.ComponentModel; 10 | using System.Globalization; 11 | using System.Runtime.CompilerServices; 12 | using System.Text; 13 | 14 | #pragma warning disable IDE0060, IDE0290 15 | 16 | namespace ComputeSharp.SourceGeneration.Helpers; 17 | 18 | /// 19 | /// A helper type to build sequences of values with pooled buffers. 20 | /// 21 | internal sealed class IndentedTextWriter : IDisposable 22 | { 23 | /// 24 | /// The default indentation (4 spaces). 25 | /// 26 | private const string DefaultIndentation = " "; 27 | 28 | /// 29 | /// The default new line ('\n'). 30 | /// 31 | private const char DefaultNewLine = '\n'; 32 | 33 | /// 34 | /// The instance that text will be written to. 35 | /// 36 | private ImmutableArrayBuilder builder; 37 | 38 | /// 39 | /// The current indentation level. 40 | /// 41 | private int currentIndentationLevel; 42 | 43 | /// 44 | /// The current indentation, as text. 45 | /// 46 | private string currentIndentation = ""; 47 | 48 | /// 49 | /// The cached array of available indentations, as text. 50 | /// 51 | private string[] availableIndentations; 52 | 53 | /// 54 | /// Creates a new object. 55 | /// 56 | public IndentedTextWriter() 57 | { 58 | this.builder = new ImmutableArrayBuilder(); 59 | this.currentIndentationLevel = 0; 60 | this.currentIndentation = ""; 61 | this.availableIndentations = new string[4]; 62 | this.availableIndentations[0] = ""; 63 | 64 | for (int i = 1, n = this.availableIndentations.Length; i < n; i++) 65 | { 66 | this.availableIndentations[i] = this.availableIndentations[i - 1] + DefaultIndentation; 67 | } 68 | } 69 | 70 | /// 71 | /// Advances the current writer and gets a to the requested memory area. 72 | /// 73 | /// The requested size to advance by. 74 | /// A to the requested memory area. 75 | /// 76 | /// No other data should be written to the writer while the returned 77 | /// is in use, as it could invalidate the memory area wrapped by it, if resizing occurs. 78 | /// 79 | public Span Advance(int requestedSize) 80 | { 81 | // Add the leading whitespace if needed (same as WriteRawText below) 82 | if (this.builder.Count == 0 || this.builder.WrittenSpan[^1] == DefaultNewLine) 83 | { 84 | this.builder.AddRange(this.currentIndentation.AsSpan()); 85 | } 86 | 87 | return this.builder.Advance(requestedSize); 88 | } 89 | 90 | /// 91 | /// Increases the current indentation level. 92 | /// 93 | public void IncreaseIndent() 94 | { 95 | this.currentIndentationLevel++; 96 | 97 | if (this.currentIndentationLevel == this.availableIndentations.Length) 98 | { 99 | Array.Resize(ref this.availableIndentations, this.availableIndentations.Length * 2); 100 | } 101 | 102 | // Set both the current indentation and the current position in the indentations 103 | // array to the expected indentation for the incremented level (ie. one level more). 104 | this.currentIndentation = this.availableIndentations[this.currentIndentationLevel] 105 | ??= this.availableIndentations[this.currentIndentationLevel - 1] + DefaultIndentation; 106 | } 107 | 108 | /// 109 | /// Decreases the current indentation level. 110 | /// 111 | public void DecreaseIndent() 112 | { 113 | this.currentIndentationLevel--; 114 | this.currentIndentation = this.availableIndentations[this.currentIndentationLevel]; 115 | } 116 | 117 | /// 118 | /// Writes a block to the underlying buffer. 119 | /// 120 | /// A value to close the open block with. 121 | public Block WriteBlock() 122 | { 123 | WriteLine("{"); 124 | IncreaseIndent(); 125 | 126 | return new(this); 127 | } 128 | 129 | /// 130 | /// Writes content to the underlying buffer. 131 | /// 132 | /// The content to write. 133 | /// Whether the input content is multiline. 134 | public void Write(string content, bool isMultiline = false) 135 | { 136 | Write(content.AsSpan(), isMultiline); 137 | } 138 | 139 | /// 140 | /// Writes content to the underlying buffer. 141 | /// 142 | /// The content to write. 143 | /// Whether the input content is multiline. 144 | public void Write(ReadOnlySpan content, bool isMultiline = false) 145 | { 146 | if (isMultiline) 147 | { 148 | while (content.Length > 0) 149 | { 150 | int newLineIndex = content.IndexOf(DefaultNewLine); 151 | 152 | if (newLineIndex < 0) 153 | { 154 | // There are no new lines left, so the content can be written as a single line 155 | WriteRawText(content); 156 | 157 | break; 158 | } 159 | else 160 | { 161 | ReadOnlySpan line = content[..newLineIndex]; 162 | 163 | // Write the current line (if it's empty, we can skip writing the text entirely). 164 | // This ensures that raw multiline string literals with blank lines don't have 165 | // extra whitespace at the start of those lines, which would otherwise happen. 166 | WriteIf(!line.IsEmpty, line); 167 | WriteLine(); 168 | 169 | // Move past the new line character (the result could be an empty span) 170 | content = content[(newLineIndex + 1)..]; 171 | } 172 | } 173 | } 174 | else 175 | { 176 | WriteRawText(content); 177 | } 178 | } 179 | 180 | /// 181 | /// Writes content to the underlying buffer. 182 | /// 183 | /// The interpolated string handler with content to write. 184 | public void Write([InterpolatedStringHandlerArgument("")] ref WriteInterpolatedStringHandler handler) 185 | { 186 | _ = this; 187 | } 188 | 189 | /// 190 | /// Writes content to the underlying buffer depending on an input condition. 191 | /// 192 | /// The condition to use to decide whether or not to write content. 193 | /// The content to write. 194 | /// Whether the input content is multiline. 195 | public void WriteIf(bool condition, string content, bool isMultiline = false) 196 | { 197 | if (condition) 198 | { 199 | Write(content.AsSpan(), isMultiline); 200 | } 201 | } 202 | 203 | /// 204 | /// Writes content to the underlying buffer depending on an input condition. 205 | /// 206 | /// The condition to use to decide whether or not to write content. 207 | /// The content to write. 208 | /// Whether the input content is multiline. 209 | public void WriteIf(bool condition, ReadOnlySpan content, bool isMultiline = false) 210 | { 211 | if (condition) 212 | { 213 | Write(content, isMultiline); 214 | } 215 | } 216 | 217 | /// 218 | /// Writes content to the underlying buffer depending on an input condition. 219 | /// 220 | /// The condition to use to decide whether or not to write content. 221 | /// The interpolated string handler with content to write. 222 | public void WriteIf(bool condition, [InterpolatedStringHandlerArgument("", nameof(condition))] ref WriteIfInterpolatedStringHandler handler) 223 | { 224 | _ = this; 225 | } 226 | 227 | /// 228 | /// Writes a line to the underlying buffer. 229 | /// 230 | /// Indicates whether to skip adding the line if there already is one. 231 | public void WriteLine(bool skipIfPresent = false) 232 | { 233 | if (skipIfPresent && this.builder.WrittenSpan is [.., '\n', '\n']) 234 | { 235 | return; 236 | } 237 | 238 | this.builder.Add(DefaultNewLine); 239 | } 240 | 241 | /// 242 | /// Writes content to the underlying buffer and appends a trailing new line. 243 | /// 244 | /// The content to write. 245 | /// Whether the input content is multiline. 246 | public void WriteLine(string content, bool isMultiline = false) 247 | { 248 | WriteLine(content.AsSpan(), isMultiline); 249 | } 250 | 251 | /// 252 | /// Writes content to the underlying buffer and appends a trailing new line. 253 | /// 254 | /// The content to write. 255 | /// Whether the input content is multiline. 256 | public void WriteLine(ReadOnlySpan content, bool isMultiline = false) 257 | { 258 | Write(content, isMultiline); 259 | WriteLine(); 260 | } 261 | 262 | /// 263 | /// Writes content to the underlying buffer and appends a trailing new line. 264 | /// 265 | /// The interpolated string handler with content to write. 266 | public void WriteLine([InterpolatedStringHandlerArgument("")] ref WriteInterpolatedStringHandler handler) 267 | { 268 | WriteLine(); 269 | } 270 | 271 | /// 272 | /// Writes a line to the underlying buffer depending on an input condition. 273 | /// 274 | /// The condition to use to decide whether or not to write content. 275 | /// Indicates whether to skip adding the line if there already is one. 276 | public void WriteLineIf(bool condition, bool skipIfPresent = false) 277 | { 278 | if (condition) 279 | { 280 | WriteLine(skipIfPresent); 281 | } 282 | } 283 | 284 | /// 285 | /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition. 286 | /// 287 | /// The condition to use to decide whether or not to write content. 288 | /// The content to write. 289 | /// Whether the input content is multiline. 290 | public void WriteLineIf(bool condition, string content, bool isMultiline = false) 291 | { 292 | if (condition) 293 | { 294 | WriteLine(content.AsSpan(), isMultiline); 295 | } 296 | } 297 | 298 | /// 299 | /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition. 300 | /// 301 | /// The condition to use to decide whether or not to write content. 302 | /// The content to write. 303 | /// Whether the input content is multiline. 304 | public void WriteLineIf(bool condition, ReadOnlySpan content, bool isMultiline = false) 305 | { 306 | if (condition) 307 | { 308 | Write(content, isMultiline); 309 | WriteLine(); 310 | } 311 | } 312 | 313 | /// 314 | /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition. 315 | /// 316 | /// The condition to use to decide whether or not to write content. 317 | /// The interpolated string handler with content to write. 318 | public void WriteLineIf(bool condition, [InterpolatedStringHandlerArgument("", nameof(condition))] ref WriteIfInterpolatedStringHandler handler) 319 | { 320 | if (condition) 321 | { 322 | WriteLine(); 323 | } 324 | } 325 | 326 | /// 327 | public override string ToString() 328 | { 329 | return this.builder.WrittenSpan.Trim().ToString(); 330 | } 331 | 332 | /// 333 | public void Dispose() 334 | { 335 | this.builder.Dispose(); 336 | } 337 | 338 | /// 339 | /// Writes raw text to the underlying buffer, adding leading indentation if needed. 340 | /// 341 | /// The raw text to write. 342 | private void WriteRawText(ReadOnlySpan content) 343 | { 344 | if (this.builder.Count == 0 || this.builder.WrittenSpan[^1] == DefaultNewLine) 345 | { 346 | this.builder.AddRange(this.currentIndentation.AsSpan()); 347 | } 348 | 349 | this.builder.AddRange(content); 350 | } 351 | 352 | /// 353 | /// A delegate representing a callback to write data into an instance. 354 | /// 355 | /// The type of data to use. 356 | /// The input data to use to write into . 357 | /// The instance to write into. 358 | public delegate void Callback(T value, IndentedTextWriter writer); 359 | 360 | /// 361 | /// Represents an indented block that needs to be closed. 362 | /// 363 | /// The input instance to wrap. 364 | public struct Block(IndentedTextWriter writer) : IDisposable 365 | { 366 | /// 367 | /// The instance to write to. 368 | /// 369 | private IndentedTextWriter? writer = writer; 370 | 371 | /// 372 | public void Dispose() 373 | { 374 | IndentedTextWriter? writer = this.writer; 375 | 376 | this.writer = null; 377 | 378 | if (writer is not null) 379 | { 380 | writer.DecreaseIndent(); 381 | writer.WriteLine("}"); 382 | } 383 | } 384 | } 385 | 386 | /// 387 | /// Provides a handler used by the language compiler to append interpolated strings into instances. 388 | /// 389 | [EditorBrowsable(EditorBrowsableState.Never)] 390 | [InterpolatedStringHandler] 391 | public readonly ref struct WriteInterpolatedStringHandler 392 | { 393 | /// The associated to which to append. 394 | private readonly IndentedTextWriter writer; 395 | 396 | /// Creates a handler used to append an interpolated string into a . 397 | /// The number of constant characters outside of interpolation expressions in the interpolated string. 398 | /// The number of interpolation expressions in the interpolated string. 399 | /// The associated to which to append. 400 | /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. 401 | public WriteInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextWriter writer) 402 | { 403 | this.writer = writer; 404 | } 405 | 406 | /// Writes the specified string to the handler. 407 | /// The string to write. 408 | public void AppendLiteral(string value) 409 | { 410 | this.writer.Write(value); 411 | } 412 | 413 | /// Writes the specified value to the handler. 414 | /// The value to write. 415 | public void AppendFormatted(string? value) 416 | { 417 | AppendFormatted(value); 418 | } 419 | 420 | /// Writes the specified character span to the handler. 421 | /// The span to write. 422 | public void AppendFormatted(ReadOnlySpan value) 423 | { 424 | this.writer.Write(value); 425 | } 426 | 427 | /// Writes the specified value to the handler. 428 | /// The value to write. 429 | /// The type of the value to write. 430 | public void AppendFormatted(T value) 431 | { 432 | if (value is not null) 433 | { 434 | this.writer.Write(value.ToString()); 435 | } 436 | } 437 | 438 | /// Writes the specified value to the handler. 439 | /// The value to write. 440 | /// The format string. 441 | /// The type of the value to write. 442 | public void AppendFormatted(T value, string? format) 443 | { 444 | if (value is IFormattable) 445 | { 446 | this.writer.Write(((IFormattable)value).ToString(format, CultureInfo.InvariantCulture)); 447 | } 448 | else if (value is not null) 449 | { 450 | this.writer.Write(value.ToString()); 451 | } 452 | } 453 | } 454 | 455 | /// 456 | /// Provides a handler used by the language compiler to conditionally append interpolated strings into instances. 457 | /// 458 | [EditorBrowsable(EditorBrowsableState.Never)] 459 | [InterpolatedStringHandler] 460 | public readonly ref struct WriteIfInterpolatedStringHandler 461 | { 462 | /// The associated to use. 463 | private readonly WriteInterpolatedStringHandler handler; 464 | 465 | /// Creates a handler used to append an interpolated string into a . 466 | /// The number of constant characters outside of interpolation expressions in the interpolated string. 467 | /// The number of interpolation expressions in the interpolated string. 468 | /// The associated to which to append. 469 | /// The condition to use to decide whether or not to write content. 470 | /// A value indicating whether formatting should proceed. 471 | /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. 472 | public WriteIfInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextWriter writer, bool condition, out bool shouldAppend) 473 | { 474 | if (condition) 475 | { 476 | this.handler = new WriteInterpolatedStringHandler(literalLength, formattedCount, writer); 477 | 478 | shouldAppend = true; 479 | } 480 | else 481 | { 482 | this.handler = default; 483 | 484 | shouldAppend = false; 485 | } 486 | } 487 | 488 | /// 489 | public void AppendLiteral(string value) 490 | { 491 | this.handler.AppendLiteral(value); 492 | } 493 | 494 | /// 495 | public void AppendFormatted(string? value) 496 | { 497 | this.handler.AppendFormatted(value); 498 | } 499 | 500 | /// 501 | public void AppendFormatted(ReadOnlySpan value) 502 | { 503 | this.handler.AppendFormatted(value); 504 | } 505 | 506 | /// 507 | public void AppendFormatted(T value) 508 | { 509 | this.handler.AppendFormatted(value); 510 | } 511 | 512 | /// 513 | public void AppendFormatted(T value, string? format) 514 | { 515 | this.handler.AppendFormatted(value, format); 516 | } 517 | } 518 | } -------------------------------------------------------------------------------- /StackXML.Generator/ComputeSharp/Helpers/ObjectPool{T}.cs: -------------------------------------------------------------------------------- 1 | // This file is ported from ComputeSharp (Sergio0694/ComputeSharp), 2 | // see LICENSE in ComputeSharp directory 3 | 4 | // Licensed to the .NET Foundation under one or more agreements. 5 | // The .NET Foundation licenses this file to you under the MIT license. 6 | // See the LICENSE file in the project root for more information. 7 | 8 | // Ported from Roslyn, see: https://github.com/dotnet/roslyn/blob/main/src/Dependencies/PooledObjects/ObjectPool%601.cs. 9 | 10 | using System; 11 | using System.Runtime.CompilerServices; 12 | using System.Threading; 13 | 14 | #pragma warning disable RS1035 15 | 16 | namespace ComputeSharp.SourceGeneration.Helpers; 17 | 18 | /// 19 | /// 20 | /// Generic implementation of object pooling pattern with predefined pool size limit. The main purpose 21 | /// is that limited number of frequently used objects can be kept in the pool for further recycling. 22 | /// 23 | /// 24 | /// Notes: 25 | /// 26 | /// 27 | /// It is not the goal to keep all returned objects. Pool is not meant for storage. If there 28 | /// is no space in the pool, extra returned objects will be dropped. 29 | /// 30 | /// 31 | /// It is implied that if object was obtained from a pool, the caller will return it back in 32 | /// a relatively short time. Keeping checked out objects for long durations is ok, but 33 | /// reduces usefulness of pooling. Just new up your own. 34 | /// 35 | /// 36 | /// 37 | /// 38 | /// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice. 39 | /// Rationale: if there is no intent for reusing the object, do not use pool - just use "new". 40 | /// 41 | /// 42 | /// The type of objects to pool. 43 | /// The input factory to produce items. 44 | /// 45 | /// The factory is stored for the lifetime of the pool. We will call this only when pool needs to 46 | /// expand. compared to "new T()", Func gives more flexibility to implementers and faster than "new T()". 47 | /// 48 | /// The pool size to use. 49 | internal sealed class ObjectPool(Func factory, int size) 50 | where T : class 51 | { 52 | /// 53 | /// The array of cached items. 54 | /// 55 | private readonly Element[] items = new Element[size - 1]; 56 | 57 | /// 58 | /// Storage for the pool objects. The first item is stored in a dedicated field 59 | /// because we expect to be able to satisfy most requests from it. 60 | /// 61 | private T? firstItem; 62 | 63 | /// 64 | /// Creates a new instance with the specified parameters. 65 | /// 66 | /// The input factory to produce items. 67 | public ObjectPool(Func factory) 68 | : this(factory, Environment.ProcessorCount * 2) 69 | { 70 | } 71 | 72 | /// 73 | /// Produces a instance. 74 | /// 75 | /// The returned item to use. 76 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 77 | public T Allocate() 78 | { 79 | T? item = this.firstItem; 80 | 81 | if (item is null || item != Interlocked.CompareExchange(ref this.firstItem, null, item)) 82 | { 83 | item = AllocateSlow(); 84 | } 85 | 86 | return item; 87 | } 88 | 89 | /// 90 | /// Returns a given instance to the pool. 91 | /// 92 | /// The instance to return. 93 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 94 | public void Free(T obj) 95 | { 96 | if (this.firstItem is null) 97 | { 98 | this.firstItem = obj; 99 | } 100 | else 101 | { 102 | FreeSlow(obj); 103 | } 104 | } 105 | 106 | /// 107 | /// Allocates a new item. 108 | /// 109 | /// The returned item to use. 110 | [MethodImpl(MethodImplOptions.NoInlining)] 111 | private T AllocateSlow() 112 | { 113 | foreach (ref Element element in this.items.AsSpan()) 114 | { 115 | T? instance = element.Value; 116 | 117 | if (instance is not null) 118 | { 119 | if (instance == Interlocked.CompareExchange(ref element.Value, null, instance)) 120 | { 121 | return instance; 122 | } 123 | } 124 | } 125 | 126 | return factory(); 127 | } 128 | 129 | /// 130 | /// Frees a given item. 131 | /// 132 | /// The item to return to the pool. 133 | [MethodImpl(MethodImplOptions.NoInlining)] 134 | private void FreeSlow(T obj) 135 | { 136 | foreach (ref Element element in this.items.AsSpan()) 137 | { 138 | if (element.Value is null) 139 | { 140 | element.Value = obj; 141 | 142 | break; 143 | } 144 | } 145 | } 146 | 147 | /// 148 | /// A container for a produced item (using a wrapper to avoid covariance checks). 149 | /// 150 | private struct Element 151 | { 152 | /// 153 | /// The value held at the current element. 154 | /// 155 | internal T? Value; 156 | } 157 | } -------------------------------------------------------------------------------- /StackXML.Generator/ComputeSharp/LICENSE: -------------------------------------------------------------------------------- 1 | SHA: 2e62cc358c3babdc50257e23c60569e148166e31 2 | 3 | MIT License 4 | 5 | Copyright (c) 2024 Sergio Pedri 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /StackXML.Generator/ComputeSharp/Models/HierarchyInfo.cs: -------------------------------------------------------------------------------- 1 | // This file is ported from ComputeSharp (Sergio0694/ComputeSharp), 2 | // see LICENSE in ComputeSharp directory 3 | 4 | using System; 5 | using ComputeSharp.SourceGeneration.Extensions; 6 | using ComputeSharp.SourceGeneration.Helpers; 7 | using Microsoft.CodeAnalysis; 8 | using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; 9 | 10 | namespace ComputeSharp.SourceGeneration.Models; 11 | 12 | /// 13 | /// A model describing the hierarchy info for a specific type. 14 | /// 15 | /// The fully qualified metadata name for the current type. 16 | /// Gets the namespace for the current type. 17 | /// Gets the sequence of type definitions containing the current type. 18 | internal sealed partial record HierarchyInfo(string FullyQualifiedMetadataName, string Namespace, EquatableArray Hierarchy) 19 | { 20 | /// 21 | /// Creates a new instance from a given . 22 | /// 23 | /// The input instance to gather info for. 24 | /// A instance describing . 25 | public static HierarchyInfo From(INamedTypeSymbol typeSymbol) 26 | { 27 | using ImmutableArrayBuilder hierarchy = new(); 28 | 29 | for (INamedTypeSymbol? parent = typeSymbol; 30 | parent is not null; 31 | parent = parent.ContainingType) 32 | { 33 | hierarchy.Add(new TypeInfo( 34 | parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), 35 | parent.TypeKind, 36 | parent.IsRecord)); 37 | } 38 | 39 | return new( 40 | typeSymbol.GetFullyQualifiedMetadataName(), 41 | typeSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)), 42 | hierarchy.ToImmutable()); 43 | } 44 | 45 | /// 46 | /// Writes syntax for the current hierarchy into a target writer. 47 | /// 48 | /// The type of state to pass to callbacks. 49 | /// The input state to pass to callbacks. 50 | /// The target instance to write text to. 51 | /// A list of base types to add to the generated type, if any. 52 | /// The callbacks to use to write members into the declared type. 53 | public void WriteSyntax( 54 | T state, 55 | IndentedTextWriter writer, 56 | ReadOnlySpan baseTypes, 57 | ReadOnlySpan> memberCallbacks) 58 | { 59 | // Write the generated file header 60 | writer.WriteLine("// "); 61 | writer.WriteLine("#pragma warning disable"); 62 | writer.WriteLine(); 63 | 64 | // Declare the namespace, if needed 65 | if (Namespace.Length > 0) 66 | { 67 | writer.WriteLine($"namespace {Namespace}"); 68 | writer.WriteLine("{"); 69 | writer.IncreaseIndent(); 70 | } 71 | 72 | // Declare all the opening types until the inner-most one 73 | for (int i = Hierarchy.Length - 1; i >= 0; i--) 74 | { 75 | writer.WriteLine($$"""/// """); 76 | writer.Write($$"""partial {{Hierarchy[i].GetTypeKeyword()}} {{Hierarchy[i].QualifiedName}}"""); 77 | 78 | // Add any base types, if needed 79 | if (i == 0 && !baseTypes.IsEmpty) 80 | { 81 | writer.Write(" : "); 82 | writer.WriteInitializationExpressions(baseTypes, static (item, writer) => writer.Write(item)); 83 | writer.WriteLine(); 84 | } 85 | else 86 | { 87 | writer.WriteLine(); 88 | } 89 | 90 | writer.WriteLine($$"""{"""); 91 | writer.IncreaseIndent(); 92 | } 93 | 94 | // Generate all nested members 95 | writer.WriteLineSeparatedMembers(memberCallbacks, (callback, writer) => callback(state, writer)); 96 | 97 | // Close all scopes and reduce the indentation 98 | for (int i = 0; i < Hierarchy.Length; i++) 99 | { 100 | writer.DecreaseIndent(); 101 | writer.WriteLine("}"); 102 | } 103 | 104 | // Close the namespace scope as well, if needed 105 | if (Namespace.Length > 0) 106 | { 107 | writer.DecreaseIndent(); 108 | writer.WriteLine("}"); 109 | } 110 | } 111 | 112 | /// 113 | /// Gets the fully qualified type name for the current instance. 114 | /// 115 | /// The fully qualified type name for the current instance. 116 | public string GetFullyQualifiedTypeName() 117 | { 118 | using ImmutableArrayBuilder fullyQualifiedTypeName = new(); 119 | 120 | fullyQualifiedTypeName.AddRange("global::".AsSpan()); 121 | 122 | if (Namespace.Length > 0) 123 | { 124 | fullyQualifiedTypeName.AddRange(Namespace.AsSpan()); 125 | fullyQualifiedTypeName.Add('.'); 126 | } 127 | 128 | fullyQualifiedTypeName.AddRange(Hierarchy[^1].QualifiedName.AsSpan()); 129 | 130 | for (int i = Hierarchy.Length - 2; i >= 0; i--) 131 | { 132 | fullyQualifiedTypeName.Add('.'); 133 | fullyQualifiedTypeName.AddRange(Hierarchy[i].QualifiedName.AsSpan()); 134 | } 135 | 136 | return fullyQualifiedTypeName.ToString(); 137 | } 138 | } -------------------------------------------------------------------------------- /StackXML.Generator/ComputeSharp/Models/TypeInfo.cs: -------------------------------------------------------------------------------- 1 | // This file is ported from ComputeSharp (Sergio0694/ComputeSharp), 2 | // see LICENSE in ComputeSharp directory 3 | 4 | using Microsoft.CodeAnalysis; 5 | 6 | namespace ComputeSharp.SourceGeneration.Models; 7 | 8 | /// 9 | /// A model describing a type info in a type hierarchy. 10 | /// 11 | /// The qualified name for the type. 12 | /// The type of the type in the hierarchy. 13 | /// Whether the type is a record type. 14 | internal sealed record TypeInfo(string QualifiedName, TypeKind Kind, bool IsRecord) 15 | { 16 | /// 17 | /// Gets the keyword for the current type kind. 18 | /// 19 | /// The keyword for the current type kind. 20 | public string GetTypeKeyword() 21 | { 22 | return Kind switch 23 | { 24 | TypeKind.Struct when IsRecord => "record struct", 25 | TypeKind.Struct => "struct", 26 | TypeKind.Interface => "interface", 27 | TypeKind.Class when IsRecord => "record", 28 | _ => "class" 29 | }; 30 | } 31 | } -------------------------------------------------------------------------------- /StackXML.Generator/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "DebugRoslynSourceGenerator - Benchmark": { 5 | "commandName": "DebugRoslynComponent", 6 | "targetProject": "../StackXML.Benchmark/StackXML.Benchmark.csproj" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /StackXML.Generator/StackXML.Generator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | latest 6 | true 7 | enable 8 | 9 | false 10 | true 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /StackXML.Generator/StrGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Threading; 4 | using ComputeSharp.SourceGeneration.Extensions; 5 | using ComputeSharp.SourceGeneration.Helpers; 6 | using ComputeSharp.SourceGeneration.Models; 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.CSharp.Syntax; 9 | 10 | namespace StackXML.Generator 11 | { 12 | [Generator] 13 | public class StrGenerator : IIncrementalGenerator 14 | { 15 | private record ClassGenInfo 16 | { 17 | public required HierarchyInfo m_hierarchy; 18 | public EquatableArray m_fields; 19 | } 20 | 21 | private record FieldGenInfo 22 | { 23 | public readonly HierarchyInfo m_ownerHierarchy; 24 | public readonly int? m_group; 25 | public readonly string? m_defaultValue; 26 | 27 | public readonly string m_fieldName; 28 | public readonly string m_typeShortName; 29 | public readonly string m_typeQualifiedName; 30 | public readonly bool m_isNullable; 31 | public readonly bool m_isStrBody; 32 | 33 | public FieldGenInfo(IFieldSymbol fieldSymbol, VariableDeclaratorSyntax variableDeclaratorSyntax) 34 | { 35 | m_ownerHierarchy = HierarchyInfo.From(fieldSymbol.ContainingType); 36 | m_defaultValue = variableDeclaratorSyntax.Initializer?.Value.ToString(); 37 | 38 | m_fieldName = fieldSymbol.Name; 39 | 40 | var type = (INamedTypeSymbol)fieldSymbol.Type; 41 | m_isNullable = ExtractNullable(ref type); 42 | 43 | m_typeShortName = type.Name; 44 | m_typeQualifiedName = type.GetFullyQualifiedName(); 45 | 46 | if (fieldSymbol.TryGetAttributeWithFullyQualifiedMetadataName("StackXML.Str.StrOptionalAttribute", out var optionalAttribute)) 47 | { 48 | m_group = (int)optionalAttribute.ConstructorArguments[0].Value!; 49 | } 50 | 51 | foreach (var member in type.GetMembers()) 52 | { 53 | if (member is not IFieldSymbol childFieldSymbol) 54 | { 55 | continue; 56 | } 57 | 58 | if (!childFieldSymbol.TryGetAttributeWithFullyQualifiedMetadataName("StackXML.Str.StrFieldAttribute", out _)) 59 | { 60 | continue; 61 | } 62 | 63 | m_isStrBody = true; 64 | break; 65 | } 66 | } 67 | } 68 | 69 | public void Initialize(IncrementalGeneratorInitializationContext context) 70 | { 71 | var fieldDeclarations = context.SyntaxProvider.ForAttributeWithMetadataName( 72 | "StackXML.Str.StrFieldAttribute", 73 | (syntaxNode, _) => syntaxNode is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { Parent: TypeDeclarationSyntax, AttributeLists.Count: > 0 } } }, 74 | TransformField); 75 | 76 | // group by containing type 77 | var typeDeclarations = fieldDeclarations.GroupBy(static x => x.m_ownerHierarchy, static x => x).Select((x, token) => new ClassGenInfo 78 | { 79 | m_hierarchy = x.Key, 80 | m_fields = x.Right 81 | }); 82 | 83 | context.RegisterSourceOutput(typeDeclarations, Process); 84 | } 85 | 86 | private static FieldGenInfo TransformField(GeneratorAttributeSyntaxContext context, CancellationToken token) 87 | { 88 | return new FieldGenInfo((IFieldSymbol)context.TargetSymbol, (VariableDeclaratorSyntax)context.TargetNode); 89 | } 90 | 91 | private static void Process(SourceProductionContext productionContext, ClassGenInfo classGenInfo) 92 | { 93 | using var w = new IndentedTextWriter(); 94 | 95 | w.WriteLine("using StackXML;"); 96 | w.WriteLine("using StackXML.Str;"); 97 | w.WriteLine(); 98 | 99 | classGenInfo.m_hierarchy.WriteSyntax(classGenInfo, w, ["IStrClass"], [ProcessClass]); 100 | productionContext.AddSource($"{classGenInfo.m_hierarchy.FullyQualifiedMetadataName}.cs", w.ToString()); 101 | } 102 | 103 | private static void ProcessClass(ClassGenInfo classGenInfo, IndentedTextWriter writer) 104 | { 105 | WriteDeserializeMethod(classGenInfo, writer); 106 | writer.WriteLine(); 107 | WriteSerializeMethod(classGenInfo, writer); 108 | } 109 | 110 | private static void WriteDeserializeMethod(ClassGenInfo classGenInfo, IndentedTextWriter writer) 111 | { 112 | writer.WriteLine("public void Deserialize(ref StrReader reader)"); 113 | writer.WriteLine("{"); 114 | writer.IncreaseIndent(); 115 | 116 | HashSet groupsStarted = new HashSet(); 117 | int? currentGroup = null; 118 | foreach (var field in classGenInfo.m_fields) 119 | { 120 | if (currentGroup != field.m_group) 121 | { 122 | if (currentGroup != null) 123 | { 124 | writer.DecreaseIndent(); 125 | writer.WriteLine("}"); 126 | } 127 | 128 | if (field.m_group != null) 129 | { 130 | const string c_conditionName = "read"; 131 | 132 | if (groupsStarted.Add(field.m_group.Value)) 133 | { 134 | writer.WriteLine($"var {c_conditionName}{field.m_group.Value} = reader.HasRemaining();"); 135 | } 136 | 137 | writer.WriteLine($"if ({c_conditionName}{field.m_group.Value})"); 138 | writer.WriteLine("{"); 139 | writer.IncreaseIndent(); 140 | } 141 | 142 | currentGroup = field.m_group; 143 | } 144 | 145 | if (field.m_isStrBody) 146 | { 147 | writer.WriteLine($"{field.m_fieldName} = new {field.m_typeQualifiedName}();"); 148 | writer.WriteLine($"{field.m_fieldName}.Deserialize(ref reader);"); 149 | } else 150 | { 151 | var reader = GetReaderForType(field.m_typeShortName, field.m_typeQualifiedName); 152 | writer.WriteLine($"{field.m_fieldName} = {reader};"); 153 | } 154 | } 155 | 156 | if (currentGroup != null) 157 | { 158 | writer.DecreaseIndent(); 159 | writer.WriteLine("}"); 160 | } 161 | 162 | writer.DecreaseIndent(); 163 | writer.WriteLine("}"); 164 | } 165 | 166 | private static void WriteSerializeMethod(ClassGenInfo classGenInfo, IndentedTextWriter writer) 167 | { 168 | writer.WriteLine("public void Serialize(ref StrWriter writer)"); 169 | writer.WriteLine("{"); 170 | writer.IncreaseIndent(); 171 | { 172 | HashSet allGroups = new HashSet(); 173 | foreach (var field in classGenInfo.m_fields) 174 | { 175 | if (field.m_group != null) allGroups.Add(field.m_group.Value); 176 | } 177 | 178 | const string c_conditionName = "doGroup"; 179 | 180 | HashSet setupGroups = new HashSet(); 181 | List groupConditions = new List(); 182 | foreach (var field in classGenInfo.m_fields) 183 | { 184 | if (field.m_group != null && setupGroups.Add(field.m_group.Value)) 185 | { 186 | List boolOrs = new List(); 187 | foreach (var existingGroup in allGroups) 188 | { 189 | if (existingGroup <= field.m_group) continue; 190 | boolOrs.Add($"{c_conditionName}{existingGroup}"); 191 | } 192 | 193 | if (field.m_defaultValue != null) 194 | { 195 | boolOrs.Add($"{field.m_fieldName} != {field.m_defaultValue}"); 196 | } else 197 | { 198 | boolOrs.Add($"{field.m_fieldName} != default"); 199 | } 200 | groupConditions.Add($"bool {c_conditionName}{field.m_group} = {string.Join(" || ", boolOrs)};"); 201 | } 202 | } 203 | 204 | groupConditions.Reverse(); 205 | foreach (var condition in groupConditions) 206 | { 207 | writer.WriteLine(condition); 208 | } 209 | 210 | int? currentGroup = null; 211 | foreach (var field in classGenInfo.m_fields) 212 | { 213 | if (currentGroup != field.m_group) 214 | { 215 | if (currentGroup != null) 216 | { 217 | writer.DecreaseIndent(); 218 | writer.WriteLine("}"); 219 | } 220 | if (field.m_group != null) 221 | { 222 | writer.WriteLine($"if ({c_conditionName}{field.m_group.Value})"); 223 | writer.WriteLine("{"); 224 | writer.IncreaseIndent(); 225 | } 226 | currentGroup = field.m_group; 227 | } 228 | 229 | var toWrite = field.m_fieldName; 230 | if (field.m_isNullable) 231 | { 232 | toWrite += ".Value"; 233 | } 234 | if (field.m_isStrBody) 235 | { 236 | writer.WriteLine($"{toWrite}.Serialize(ref writer);"); 237 | } else 238 | { 239 | var writerFunc = GetWriterForType(field.m_fieldName, toWrite); 240 | writer.WriteLine($"{writerFunc};"); 241 | } 242 | } 243 | if (currentGroup != null) 244 | { 245 | writer.DecreaseIndent(); 246 | writer.WriteLine("}"); 247 | } 248 | } 249 | writer.DecreaseIndent(); 250 | writer.WriteLine("}"); 251 | } 252 | 253 | private static bool ExtractNullable(ref INamedTypeSymbol type) 254 | { 255 | if (type.Name != "Nullable") return false; 256 | type = (INamedTypeSymbol)type.TypeArguments[0]; 257 | return true; 258 | } 259 | 260 | public static string GetWriterForType(string type, string toWrite) 261 | { 262 | var result = type switch 263 | { 264 | _ => $"writer.Put({toWrite})" 265 | }; 266 | return result; 267 | } 268 | 269 | public static string GetReaderForType(string shortName, string qualifiedName) 270 | { 271 | var result = shortName switch 272 | { 273 | "String" => "reader.GetString().ToString()", 274 | "ReadOnlySpan" => "reader.GetString()", // todo: ReadOnlySpan only... 275 | "SpanStr" => "reader.GetSpanString()", 276 | _ => $"reader.Get<{qualifiedName}>()" 277 | }; 278 | return result; 279 | } 280 | } 281 | } -------------------------------------------------------------------------------- /StackXML.Tests/InterpretBool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using StackXML.Str; 4 | using Xunit; 5 | 6 | namespace StackXML.Tests 7 | { 8 | public static class InterpretBool 9 | { 10 | [Theory] 11 | [InlineData("TRUE", true)] 12 | [InlineData("True", true)] 13 | [InlineData("truE", true)] 14 | [InlineData("FALSE", false)] 15 | [InlineData("False", false)] 16 | [InlineData("falsE", false)] 17 | [InlineData("0", false)] 18 | [InlineData("1", true)] 19 | [InlineData("yes", true)] 20 | [InlineData("no", false)] 21 | public static void Interpret(string str, bool expected) 22 | { 23 | var actual = StandardStrParser.InterpretBool(str.AsSpan()); 24 | Assert.Equal(expected, actual); 25 | } 26 | 27 | [Fact] 28 | public static void InterpretError() 29 | { 30 | Assert.Throws(() => StandardStrParser.InterpretBool("yep")); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /StackXML.Tests/SpanStrTests.cs: -------------------------------------------------------------------------------- 1 | using StackXML.Str; 2 | using Xunit; 3 | 4 | namespace StackXML.Tests 5 | { 6 | public class SpanStrTests 7 | { 8 | [Theory] 9 | [InlineData("a", 'a', true)] 10 | [InlineData("b", 'a', false)] 11 | [InlineData("bbbbbbbbbbbbbbbbbbbbbbbbba", 'a', true)] 12 | [InlineData("bbbbbbbbbbbbbabbbbbbbbbbbb", 'a', true)] 13 | public void Contains(string input, char c, bool expected) 14 | { 15 | var spanStr = new SpanStr(input); 16 | var result = spanStr.Contains(c); 17 | Assert.Equal(expected, result); 18 | } 19 | 20 | [Theory] 21 | [InlineData("aa", "aa", true)] 22 | [InlineData("aa", "aab", false)] 23 | [InlineData("bb", "aa", false)] 24 | [InlineData("a", "a", true)] 25 | [InlineData("..,aabb1234", "..,aabb1234", true)] 26 | public void Equal(string input1, string input2, bool expected) 27 | { 28 | SpanStr spanStr1 = new SpanStr(input1); 29 | Assert.Equal(spanStr1 == input2, expected); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /StackXML.Tests/StackXML.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | True 7 | 8 | 9 | 10 | True 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /StackXML.Tests/StrReadWriteTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using StackXML.Str; 4 | using Xunit; 5 | 6 | namespace StackXML.Tests 7 | { 8 | public static class StrReadWriteTests 9 | { 10 | private const string c_target = "hello world 5.6 255"; 11 | 12 | [Fact] 13 | public static void Write() 14 | { 15 | using var writer = new StrWriter(' '); 16 | writer.PutString("hello"); 17 | writer.PutString("world"); 18 | writer.Put(5.6); 19 | writer.Put(255); 20 | 21 | var built = writer.ToString(); 22 | 23 | Assert.Equal(c_target, built); 24 | } 25 | 26 | [Fact] 27 | public static void Read() 28 | { 29 | var reader = new StrReader(c_target.AsSpan(), ' ', StandardStrParser.s_instance); 30 | var hello = reader.GetString(); 31 | var world = reader.GetString(); 32 | var fivePointSix = reader.Get(); 33 | var ff = reader.Get(); 34 | 35 | Assert.Equal("hello", hello.ToString()); 36 | Assert.Equal("world", world.ToString()); 37 | Assert.Equal(5.6, fivePointSix); 38 | Assert.Equal(255, ff); 39 | } 40 | 41 | [Fact] 42 | public static void TestReadToEnd() 43 | { 44 | var actual = new[] 45 | { 46 | "1", "2", "3", "4", "5" 47 | }; 48 | 49 | var input = "1,2,3,4,5"; 50 | var reader = new StrReader(input.AsSpan(), ',', StandardStrParser.s_instance); 51 | 52 | var readToEnd = reader.ReadToEnd().ToArray(); 53 | 54 | Assert.Equal(actual, readToEnd); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /StackXML.Tests/StructuredStr.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using StackXML.Str; 3 | using Xunit; 4 | 5 | namespace StackXML.Tests 6 | { 7 | public ref partial struct GeneratedClass 8 | { 9 | [StrField] public int m_int; 10 | [StrField] public double m_double; 11 | [StrField] public string m_string; 12 | [StrField] public SpanStr m_spanStr; 13 | } 14 | 15 | public static class StructuredStr 16 | { 17 | [Fact] 18 | public static void RoundTrip() 19 | { 20 | const char separator = '/'; 21 | 22 | var input = new GeneratedClass 23 | { 24 | m_int = int.MaxValue, 25 | m_double = 3.14, 26 | m_string = "hello world", 27 | m_spanStr = new SpanStr("span string") 28 | }; 29 | 30 | var writer = new StrWriter(separator); 31 | input.Serialize(ref writer); 32 | 33 | var builtString = writer.ToString(); 34 | bool exceptionThrown = false; 35 | try 36 | { 37 | writer.PutRaw('\0'); 38 | Assert.True(false); // lol 39 | } catch (ObjectDisposedException) 40 | { 41 | exceptionThrown = true; 42 | // good 43 | } 44 | Assert.True(exceptionThrown); 45 | var reader = new StrReader(builtString.AsSpan(), separator, StandardStrParser.s_instance); 46 | 47 | var output = new GeneratedClass(); 48 | output.Deserialize(ref reader); 49 | 50 | Assert.Equal(input.m_int, output.m_int); 51 | Assert.Equal(input.m_double, output.m_double); 52 | Assert.Equal(input.m_string, output.m_string); 53 | Assert.Equal(input.m_spanStr.ToString(), output.m_spanStr.ToString()); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /StackXML.Tests/Xml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using Xunit; 5 | 6 | namespace StackXML.Tests 7 | { 8 | [XmlCls("emptyClass")] 9 | public partial class EmptyClass 10 | { 11 | } 12 | 13 | [XmlCls(c_longName)] 14 | public partial class VeryLongName 15 | { 16 | public const string c_longName = 17 | "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; 18 | // length = 1025 (buffer len + 1) 19 | } 20 | 21 | [XmlCls("a")] 22 | public partial class StackSlam 23 | { 24 | [XmlBody("a")] public List m_child; 25 | } 26 | 27 | [XmlCls("attrs")] 28 | public ref partial struct WithAttributes 29 | { 30 | [XmlField("int")] public int m_int; 31 | [XmlField("uint")] public uint m_uint; 32 | [XmlField("double")] public double m_double; 33 | [XmlField("bool")] public bool m_bool; 34 | [XmlField("byte")] public byte m_byte; 35 | [XmlField("string")] public ReadOnlySpan m_string; 36 | } 37 | 38 | [XmlCls("stringbodies")] 39 | public partial class StringBodies 40 | { 41 | [XmlBody("a")] public string m_a; 42 | [XmlBody("b")] public string m_b; 43 | [XmlBody("null")] public string m_null; 44 | [XmlBody("empty")] public string m_empty; 45 | } 46 | 47 | [XmlCls("stringbody")] 48 | public partial class StringBody 49 | { 50 | [XmlBody] public string m_fullBody; 51 | } 52 | 53 | [XmlCls("stringbodyarray")] 54 | public partial class FullBodyArray 55 | { 56 | [XmlBody] public List m_bodies; 57 | } 58 | 59 | public class TestCrashException : Exception 60 | { 61 | } 62 | 63 | [XmlCls("abort")] 64 | public partial class AbortClassBase 65 | { 66 | [XmlBody("abortIfPresent")] public string m_abortIfPresent; 67 | [XmlBody("crashIfPresent")] public string m_crashIfPresent; 68 | } 69 | 70 | public class AbortClass : AbortClassBase 71 | { 72 | public override bool ParseSubBody(ref XmlReadBuffer buffer, ReadOnlySpan name, ReadOnlySpan bodySpan, ReadOnlySpan innerBodySpan, 73 | ref int end, ref int endInner) 74 | { 75 | switch (name) 76 | { 77 | case "abortIfPresent": 78 | { 79 | buffer.m_abort = true; 80 | return false; 81 | } 82 | case "crashIfPresent": 83 | { 84 | throw new TestCrashException(); 85 | } 86 | default: 87 | { 88 | return base.ParseSubBody(ref buffer, name, bodySpan, innerBodySpan, ref end, ref endInner); 89 | } 90 | } 91 | } 92 | } 93 | 94 | public static class Xml 95 | { 96 | 97 | [Fact] 98 | public static void SerializeNull() 99 | { 100 | Assert.Throws(() => XmlWriteBuffer.SerializeStatic(null)); 101 | } 102 | 103 | [Fact] 104 | public static void SerializeEmpty() 105 | { 106 | var empty = new EmptyClass(); 107 | var result = XmlWriteBuffer.SerializeStatic(empty); 108 | Assert.Equal("", result); 109 | } 110 | 111 | [Fact] 112 | public static void SerializeLongName() 113 | { 114 | var longName = new VeryLongName(); 115 | var result = XmlWriteBuffer.SerializeStatic(longName); 116 | Assert.Equal($"<{VeryLongName.c_longName}/>", result); 117 | } 118 | 119 | [Fact] 120 | public static void SerializeAttributes() 121 | { 122 | var truth = new WithAttributes 123 | { 124 | m_int = -1, 125 | m_uint = uint.MaxValue, 126 | m_double = 3.14, 127 | m_string = "david and tim<", 128 | m_bool = true, 129 | m_byte = 128 130 | }; 131 | 132 | const string expected = 133 | ""; 134 | const string expectedCompatible = 135 | ""; 136 | const string expectedWithDecl = 137 | ""; 138 | const string expectedWithComment = 139 | ""; 140 | 141 | var result = XmlWriteBuffer.SerializeStatic(truth); 142 | Assert.Equal(expected, result); 143 | 144 | AssertEqualWithAttrs(expected, truth); 145 | AssertEqualWithAttrs(expectedCompatible, truth); 146 | AssertEqualWithAttrs(expectedWithDecl, truth); 147 | AssertEqualWithAttrs(expectedWithComment, truth); 148 | } 149 | 150 | private static void AssertEqualWithAttrs(string serialiezd, WithAttributes truth) 151 | { 152 | var deserialized = XmlReadBuffer.ReadStatic(serialiezd); 153 | Assert.Equal(truth.m_int, deserialized.m_int); 154 | Assert.Equal(truth.m_uint, deserialized.m_uint); 155 | Assert.Equal(truth.m_double, deserialized.m_double); 156 | Assert.Equal(truth.m_string.ToString(), deserialized.m_string.ToString()); 157 | } 158 | 159 | [Theory] 160 | [InlineData(CDataMode.Off)] 161 | [InlineData(CDataMode.On)] 162 | [InlineData(CDataMode.OnEncoded)] 163 | public static void SerializeStringBodies(CDataMode cdataMode) 164 | { 165 | var truth = new StringBodies 166 | { 167 | m_a = "blah1<>&&", 168 | m_b = "blah2", 169 | m_null = null, 170 | m_empty = string.Empty 171 | }; 172 | var result = XmlWriteBuffer.SerializeStatic(truth, cdataMode); 173 | 174 | var deserialized = XmlReadBuffer.ReadStatic(result, cdataMode); 175 | Assert.Equal(truth.m_a, deserialized.m_a); 176 | Assert.Equal(truth.m_b, deserialized.m_b); 177 | //Assert.Equal(truth.m_null, deserialized.m_null); // todo: do we want to avoid writing nulls?? currently empty string 178 | Assert.Equal(truth.m_empty, deserialized.m_empty); 179 | } 180 | 181 | [Theory] 182 | [InlineData(CDataMode.Off)] 183 | [InlineData(CDataMode.On)] 184 | [InlineData(CDataMode.OnEncoded)] 185 | public static void SerializeStringBody(CDataMode cdataMode) 186 | { 187 | var truth = new StringBody() 188 | { 189 | m_fullBody = "asdjhasjkdhakjsdhjkahsdjhkasdhasd<>&&" 190 | }; 191 | var result = XmlWriteBuffer.SerializeStatic(truth, cdataMode); 192 | 193 | var deserialized = XmlReadBuffer.ReadStatic(result, cdataMode); 194 | Assert.Equal(truth.m_fullBody, deserialized.m_fullBody); 195 | } 196 | 197 | [Theory] 198 | [InlineData(CDataMode.Off)] 199 | [InlineData(CDataMode.On)] 200 | [InlineData(CDataMode.OnEncoded)] 201 | public static void SerializeStringBodyArray(CDataMode cdataMode) 202 | { 203 | var truthArray = new FullBodyArray 204 | { 205 | m_bodies = 206 | [ 207 | // doesn't matter what the inner type is 208 | // i'm just reusing 209 | new StringBody() 210 | { 211 | m_fullBody = "first" 212 | }, 213 | new StringBody 214 | { 215 | m_fullBody = "second" 216 | } 217 | ] 218 | }; 219 | 220 | var result = XmlWriteBuffer.SerializeStatic(truthArray, cdataMode); 221 | 222 | var deserialized = XmlReadBuffer.ReadStatic(result, cdataMode); 223 | Assert.Equal(truthArray.m_bodies[0].m_fullBody, deserialized.m_bodies[0].m_fullBody); 224 | Assert.Equal(truthArray.m_bodies[1].m_fullBody, deserialized.m_bodies[1].m_fullBody); 225 | } 226 | 227 | [Fact] 228 | public static void HandleUnknownBodies() 229 | { 230 | const string input = ""; 231 | Assert.Throws(() => XmlReadBuffer.ReadStatic(input)); 232 | Assert.Throws(() => XmlReadBuffer.ReadStatic(input)); 233 | Assert.Throws(() => XmlReadBuffer.ReadStatic(input)); 234 | Assert.Throws(() => XmlReadBuffer.ReadStatic(input)); 235 | Assert.Throws(() => XmlReadBuffer.ReadStatic(input)); 236 | } 237 | 238 | [Fact] 239 | public static void HandleUnknownAttributes() 240 | { 241 | const string input = ""; 242 | XmlReadBuffer.ReadStatic(input); // ...nothing happens 243 | } 244 | 245 | [Fact] 246 | public static void SlamStackSerialize() 247 | { 248 | var head = BuildStackSlammer(1000); 249 | var serialized = XmlWriteBuffer.SerializeStatic(head); 250 | } 251 | 252 | private static int GetDefaultMaxDepth() => new XmlReadParams().m_maxDepth; 253 | 254 | [Fact] 255 | public static void SlamStackDeserialize() 256 | { 257 | var head = BuildStackSlammer(GetDefaultMaxDepth()-1); 258 | var serialized = XmlWriteBuffer.SerializeStatic(head); 259 | var deserialized = XmlReadBuffer.ReadStatic(serialized); 260 | } 261 | 262 | [Fact] 263 | public static void SlamStackDeserializeError() 264 | { 265 | var head = BuildStackSlammer(GetDefaultMaxDepth()); 266 | var serialized = XmlWriteBuffer.SerializeStatic(head); 267 | Assert.Throws(() => XmlReadBuffer.ReadStatic(serialized)); 268 | } 269 | 270 | private static StackSlam BuildStackSlammer(int count) 271 | { 272 | var current = new StackSlam(); 273 | var head = current; 274 | for (int i = 0; i < count-1; i++) 275 | { 276 | var next = new StackSlam(); 277 | current.m_child = new List 278 | { 279 | next 280 | }; 281 | current = next; 282 | } 283 | return head; 284 | } 285 | 286 | [Fact] 287 | public static void TestAbortNoCrash() 288 | { 289 | var str = "gonna do this firstyep"; 290 | var parsed = XmlReadBuffer.ReadStatic(str); // should succeed 291 | } 292 | 293 | [Fact] 294 | public static void TestAbortCrash() 295 | { 296 | var str = "yep"; 297 | Assert.Throws(() => XmlReadBuffer.ReadStatic(str)); // should crash 298 | } 299 | } 300 | } -------------------------------------------------------------------------------- /StackXML.Tests/XmlEncodingTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace StackXML.Tests 4 | { 5 | public class XmlEncodingTests 6 | { 7 | // thx to https://www.liquid-technologies.com/XML/EscapingData.aspx 8 | 9 | [Theory] 10 | [InlineData("", "<hello>")] 11 | [InlineData("if (age < 5)", "if (age < 5)")] 12 | [InlineData("if (age > 5)", "if (age > 5)")] 13 | [InlineData("if (age > 3 && age < 8)", "if (age > 3 && age < 8)")] 14 | [InlineData("She said \"You're right\"", "She said \"You're right\"")] 15 | public void Encode(string input, string expected) 16 | { 17 | var encoded = EncodeStr(input, false); 18 | Assert.Equal(expected, encoded); 19 | } 20 | 21 | [Theory] 22 | [InlineData("He said \"OK\"", "He said "OK"")] 23 | [InlineData("She said \"You're right\"", "She said "You're right"")] 24 | [InlineData("Smith&Sons", "Smith&Sons")] 25 | [InlineData("a>b", "a>b")] 26 | [InlineData("aUse CData sections for text. Text in the CData block will not be encoded 6 | On, 7 | /// Use encoded text sections 8 | Off, 9 | /// Use CData sections containing encoded text. Against normal XML spec 10 | OnEncoded 11 | } 12 | } -------------------------------------------------------------------------------- /StackXML/IXmlSerializable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StackXML 4 | { 5 | /// 6 | /// Interface for types that can be read from and written to XML using and 7 | /// 8 | public interface IXmlSerializable 9 | { 10 | /// Gets the name of the node to be written 11 | /// Name of the node to be written 12 | ReadOnlySpan GetNodeName(); 13 | 14 | bool ParseFullBody(ref XmlReadBuffer buffer, ReadOnlySpan bodySpan, ref int end); 15 | 16 | bool ParseSubBody(ref XmlReadBuffer buffer, ReadOnlySpan name, 17 | ReadOnlySpan bodySpan, ReadOnlySpan innerBodySpan, 18 | ref int end, ref int endInner); 19 | 20 | bool ParseAttribute(ref XmlReadBuffer buffer, ReadOnlySpan name, ReadOnlySpan value); 21 | 22 | void SerializeBody(ref XmlWriteBuffer buffer); 23 | 24 | void SerializeAttributes(ref XmlWriteBuffer buffer); 25 | } 26 | } -------------------------------------------------------------------------------- /StackXML/StackXML.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | LIBLOG_EXCLUDE_CODE_COVERAGE 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /StackXML/Str/BaseStrFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace StackXML.Str 5 | { 6 | public class BaseStrFormatter : IStrFormatter 7 | { 8 | public static BaseStrFormatter s_instance = new BaseStrFormatter(); 9 | 10 | public virtual bool TryFormat(Span dest, T value, out int charsWritten) where T : ISpanFormattable 11 | { 12 | return value.TryFormat(dest, out charsWritten, "", CultureInfo.InvariantCulture); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /StackXML/Str/BaseStrParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace StackXML.Str 5 | { 6 | public class BaseStrParser : IStrParser 7 | { 8 | public static BaseStrParser s_instance = new BaseStrParser(); 9 | 10 | public virtual T Parse(ReadOnlySpan span) where T : ISpanParsable 11 | { 12 | return T.Parse(span, CultureInfo.InvariantCulture); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /StackXML/Str/IStrClass.cs: -------------------------------------------------------------------------------- 1 | namespace StackXML.Str 2 | { 3 | public interface IStrClass 4 | { 5 | void Serialize(ref StrWriter writer); 6 | void Deserialize(ref StrReader reader); 7 | } 8 | } -------------------------------------------------------------------------------- /StackXML/Str/IStrFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StackXML.Str 4 | { 5 | public interface IStrFormatter 6 | { 7 | bool TryFormat(Span dest, T value, out int charsWritten) where T : ISpanFormattable; 8 | } 9 | } -------------------------------------------------------------------------------- /StackXML/Str/IStrParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StackXML.Str 4 | { 5 | public interface IStrParser 6 | { 7 | T Parse(ReadOnlySpan span) where T : ISpanParsable; 8 | } 9 | } -------------------------------------------------------------------------------- /StackXML/Str/SpanStr.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | 4 | namespace StackXML.Str 5 | { 6 | public readonly ref struct SpanStr 7 | { 8 | private readonly ReadOnlySpan m_data; 9 | private readonly string m_str; 10 | 11 | public int Length => m_str != null ? m_str.Length : m_data.Length; 12 | 13 | public SpanStr(ReadOnlySpan data) 14 | { 15 | m_data = data; 16 | m_str = null; 17 | } 18 | 19 | public SpanStr(string str) 20 | { 21 | m_str = str; 22 | m_data = default; 23 | } 24 | 25 | public bool Contains(char c) 26 | { 27 | return ((ReadOnlySpan)this).IndexOf(c) != -1; 28 | } 29 | 30 | public static bool operator ==(SpanStr left, SpanStr right) 31 | { 32 | return ((ReadOnlySpan)left).SequenceEqual(right); // turn both into spans 33 | } 34 | public static bool operator !=(SpanStr left, SpanStr right) => !(left == right); 35 | 36 | public static bool operator ==(SpanStr left, string right) 37 | { 38 | return ((ReadOnlySpan)left).SequenceEqual(right); 39 | } 40 | public static bool operator !=(SpanStr left, string right) => !(left == right); 41 | 42 | public readonly char this[int index] => ((ReadOnlySpan)this)[index]; 43 | 44 | public static implicit operator ReadOnlySpan(SpanStr str) 45 | { 46 | if (str.m_str != null) return str.m_str.AsSpan(); 47 | return str.m_data; 48 | } 49 | 50 | public static explicit operator string(SpanStr str) // explicit to prevent accidental allocs 51 | { 52 | return str.ToString(); 53 | } 54 | 55 | public override string ToString() 56 | { 57 | if (m_str != null) return m_str; 58 | return m_data.ToString(); 59 | } 60 | 61 | /// 62 | /// This method is not supported as spans cannot be boxed. To compare two spans, use operator==. 63 | /// 64 | /// Always thrown by this method. 65 | /// 66 | /// 67 | [Obsolete("Equals() on ReadOnlySpan will always throw an exception. Use == instead.")] 68 | [EditorBrowsable(EditorBrowsableState.Never)] 69 | public override bool Equals(object obj) => throw new NotSupportedException(); 70 | 71 | /// 72 | /// This method is not supported as spans cannot be boxed. 73 | /// 74 | /// Always thrown by this method. 75 | /// 76 | /// 77 | [Obsolete("GetHashCode() on ReadOnlySpan will always throw an exception.")] 78 | [EditorBrowsable(EditorBrowsableState.Never)] 79 | public override int GetHashCode() => throw new NotSupportedException(); 80 | } 81 | } -------------------------------------------------------------------------------- /StackXML/Str/StandardStrParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace StackXML.Str 5 | { 6 | public class StandardStrParser : BaseStrParser 7 | { 8 | public static StandardStrParser s_instance = new StandardStrParser(); 9 | 10 | public override T Parse(ReadOnlySpan span) 11 | { 12 | if (span.Length == 0 && typeof(T).IsPrimitive) return default; // todo: I had to handle this... 13 | 14 | if (typeof(T) == typeof(bool)) 15 | { 16 | return (T)(object)InterpretBool(span); 17 | } 18 | return base.Parse(span); 19 | } 20 | 21 | public static bool InterpretBool(ReadOnlySpan val) 22 | { 23 | if (val is "0") return false; 24 | if (val is "1") return true; 25 | 26 | if (val.Equals("no", StringComparison.InvariantCultureIgnoreCase)) return false; 27 | if (val.Equals("yes", StringComparison.InvariantCultureIgnoreCase)) return true; 28 | 29 | if (val.Equals("false", StringComparison.InvariantCultureIgnoreCase)) return false; 30 | if (val.Equals("true", StringComparison.InvariantCultureIgnoreCase)) return true; 31 | 32 | throw new InvalidDataException($"unknown boolean \"{val.ToString()}\""); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /StackXML/Str/StrAttributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StackXML.Str 4 | { 5 | [AttributeUsage(AttributeTargets.Field)] 6 | public class StrFieldAttribute : Attribute 7 | { 8 | } 9 | 10 | [AttributeUsage(AttributeTargets.Field)] 11 | public class StrOptionalAttribute : Attribute 12 | { 13 | private readonly int m_group; 14 | 15 | public StrOptionalAttribute(int group = 0) 16 | { 17 | m_group = group; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /StackXML/Str/StrClassExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StackXML.Str 4 | { 5 | public static class StrClassExtensions 6 | { 7 | public static string AsString(this T obj, char separator, IStrFormatter? formatter=null) where T : IStrClass, allows ref struct 8 | { 9 | var writer = new StrWriter(separator, formatter); 10 | try 11 | { 12 | obj.Serialize(ref writer); 13 | return writer.AsSpan().ToString(); 14 | } finally 15 | { 16 | writer.Dispose(); 17 | } 18 | } 19 | 20 | public static bool TryFormat(this T obj, Span destination, out int charsWritten, char separator, IStrFormatter? formatter=null) where T : IStrClass, allows ref struct 21 | { 22 | var writer = new StrWriter(separator, formatter); 23 | try 24 | { 25 | obj.Serialize(ref writer); 26 | var finishedSpan = writer.AsSpan(); 27 | charsWritten = finishedSpan.Length; 28 | return finishedSpan.TryCopyTo(destination); 29 | } finally 30 | { 31 | writer.Dispose(); 32 | } 33 | } 34 | 35 | public static void FullyDeserialize(this T obj, ref StrReader reader) where T : class, IStrClass 36 | { 37 | obj.Deserialize(ref reader); 38 | if (reader.HasRemaining()) 39 | { 40 | throw new Exception("DeserializeFinal: had trailing data"); 41 | } 42 | } 43 | 44 | public static void FullyDeserialize(ref this T obj, ref StrReader reader) where T : struct, IStrClass, allows ref struct 45 | { 46 | obj.Deserialize(ref reader); 47 | if (reader.HasRemaining()) 48 | { 49 | throw new Exception("DeserializeFinal: had trailing data"); 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /StackXML/Str/StrReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace StackXML.Str 5 | { 6 | public ref struct StrReader 7 | { 8 | private readonly ReadOnlySpan m_str; 9 | private readonly IStrParser m_parser; 10 | private MemoryExtensions.SpanSplitEnumerator m_enumerator; 11 | 12 | private bool m_moved; 13 | private bool m_moveSuccess; 14 | 15 | public StrReader(ReadOnlySpan str, char separator, IStrParser? parser=null) 16 | { 17 | m_str = str; 18 | m_parser = parser ?? BaseStrParser.s_instance; 19 | m_enumerator = str.Split(separator); 20 | } 21 | 22 | public ReadOnlySpan GetString() 23 | { 24 | if (!TryMove()) return default; 25 | return m_str[ConsumeRange()]; 26 | } 27 | 28 | public SpanStr GetSpanString() 29 | { 30 | if (!TryMove()) return default; 31 | return new SpanStr(m_str[ConsumeRange()]); 32 | } 33 | 34 | public T Get() where T : ISpanParsable 35 | { 36 | return m_parser.Parse(GetString()); 37 | } 38 | 39 | public IReadOnlyList ReadToEnd() 40 | { 41 | List lst = new List(); 42 | while (HasRemaining()) 43 | { 44 | var str = GetString(); 45 | lst.Add(str.ToString()); 46 | } 47 | return lst; 48 | } 49 | 50 | private Range ConsumeRange() 51 | { 52 | var result = m_enumerator.Current; 53 | m_moved = false; 54 | return result; 55 | } 56 | 57 | private bool TryMove() 58 | { 59 | if (m_moved) return m_moveSuccess; 60 | 61 | m_moveSuccess = m_enumerator.MoveNext(); 62 | m_moved = true; 63 | return m_moveSuccess; 64 | } 65 | 66 | public bool HasRemaining() 67 | { 68 | return TryMove(); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /StackXML/Str/StrWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CommunityToolkit.HighPerformance.Buffers; 3 | 4 | namespace StackXML.Str 5 | { 6 | public ref struct StrWriter 7 | { 8 | private ArrayPoolBufferWriter m_writer; 9 | private readonly IStrFormatter m_formatter; 10 | 11 | public readonly char m_separator; 12 | public bool m_separatorAtEnd; 13 | 14 | private bool m_isFirst; 15 | 16 | public ReadOnlySpan m_builtSpan => m_writer.WrittenSpan; 17 | 18 | public StrWriter(char separator, IStrFormatter? formatter=null) 19 | { 20 | m_writer = new ArrayPoolBufferWriter(256); 21 | m_formatter = formatter ?? BaseStrFormatter.s_instance; 22 | 23 | m_separator = separator; 24 | m_separatorAtEnd = false; 25 | 26 | m_isFirst = true; 27 | } 28 | 29 | private void Resize() 30 | { 31 | m_writer.GetSpan(m_writer.Capacity*2); 32 | } 33 | 34 | private void AssertWriteable() 35 | { 36 | if (m_writer == null) throw new ObjectDisposedException("StrWriter"); 37 | } 38 | 39 | private void PutSeparator() 40 | { 41 | if (m_isFirst) m_isFirst = false; 42 | else PutRaw(m_separator); 43 | } 44 | 45 | public void PutString(ReadOnlySpan str) 46 | { 47 | PutSeparator(); 48 | PutRaw(str); 49 | } 50 | 51 | public void Put(ReadOnlySpan str) 52 | { 53 | PutString(str); 54 | } 55 | 56 | public void Put(T value) where T : ISpanFormattable 57 | { 58 | PutSeparator(); 59 | int charsWritten; 60 | while (!m_formatter.TryFormat(m_writer.GetSpan(), value, out charsWritten)) 61 | { 62 | Resize(); 63 | } 64 | m_writer.Advance(charsWritten); 65 | } 66 | 67 | public void PutRaw(char c) 68 | { 69 | AssertWriteable(); 70 | m_writer.GetSpan(1)[0] = c; 71 | m_writer.Advance(1); 72 | } 73 | 74 | public void PutRaw(ReadOnlySpan str) 75 | { 76 | AssertWriteable(); 77 | str.CopyTo(m_writer.GetSpan(str.Length)); 78 | m_writer.Advance(str.Length); 79 | } 80 | 81 | public void Finish(bool terminate) 82 | { 83 | if (m_separatorAtEnd) PutRaw(m_separator); 84 | if (terminate) PutRaw('\0'); 85 | } 86 | 87 | public ReadOnlySpan AsSpan(bool terminate=false) 88 | { 89 | AssertWriteable(); 90 | 91 | Finish(terminate); 92 | return m_builtSpan; 93 | } 94 | 95 | public override string ToString() 96 | { 97 | AssertWriteable(); 98 | 99 | Finish(false); 100 | var str = m_builtSpan.ToString(); 101 | Dispose(); 102 | return str; 103 | } 104 | 105 | 106 | public void Dispose() 107 | { 108 | m_writer?.Dispose(); 109 | m_writer = null!; 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /StackXML/XmlAttributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StackXML 4 | { 5 | 6 | [AttributeUsage(AttributeTargets.Field)] 7 | public class XmlFieldAttribute : Attribute 8 | { 9 | public readonly string m_name; 10 | 11 | public XmlFieldAttribute(string name) 12 | { 13 | m_name = name; 14 | } 15 | } 16 | 17 | [AttributeUsage(AttributeTargets.Field)] 18 | public class XmlBodyAttribute : Attribute 19 | { 20 | public readonly string? m_name; 21 | 22 | public XmlBodyAttribute(string? name=null) 23 | { 24 | m_name = name; 25 | } 26 | } 27 | 28 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] 29 | public class XmlClsAttribute : Attribute 30 | { 31 | public readonly string? m_name; 32 | 33 | public XmlClsAttribute(string? name) 34 | { 35 | m_name = name; 36 | } 37 | } 38 | 39 | [AttributeUsage(AttributeTargets.Field)] 40 | public class XmlSplitStrAttribute : Attribute 41 | { 42 | public readonly char m_splitOn; 43 | 44 | public XmlSplitStrAttribute(char splitOn=',') 45 | { 46 | m_splitOn = splitOn; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /StackXML/XmlReadBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using StackXML.Logging; 5 | 6 | namespace StackXML 7 | { 8 | /// Stack based XML deserializer 9 | public ref struct XmlReadBuffer 10 | { 11 | private const string c_commentStart = ""; 13 | private const string c_declarationEnd = "?>"; 14 | public const string c_cdataStart = ""; 16 | 17 | public XmlReadParams m_params; 18 | 19 | /// Abort parsing immediately 20 | public bool m_abort; 21 | 22 | /// Current depth of calls to 23 | public int m_depth; 24 | 25 | private static readonly ILog s_logger = LogProvider.GetLogger(nameof(XmlReadBuffer)); 26 | 27 | public XmlReadBuffer() 28 | { 29 | m_params = new XmlReadParams(); 30 | } 31 | 32 | /// 33 | /// Parses XML node attributes 34 | /// 35 | /// Text span 36 | /// Index in which is at the end of the node declaration 37 | /// Starting position within 38 | /// Object to receive parsed data 39 | /// Unable to parse data 40 | /// Position within which is at the end of the attribute list 41 | private int DeserializeAttributes(ReadOnlySpan currSpan, int closeBraceIdx, int position, ref T obj) where T: IXmlSerializable, allows ref struct 42 | { 43 | while (position < closeBraceIdx) 44 | { 45 | var spaceSpan = currSpan.Slice(position, closeBraceIdx-position); 46 | if (spaceSpan[0] == ' ') 47 | { 48 | position++; 49 | continue; 50 | } 51 | 52 | var eqIdx = spaceSpan.IndexOf('='); 53 | if (eqIdx == -1) 54 | { 55 | break; 56 | } 57 | var attributeName = spaceSpan.Slice(0, eqIdx); 58 | 59 | var quoteType = spaceSpan[eqIdx + 1]; 60 | if (quoteType != '\'' && quoteType != '\"') throw new InvalidDataException($"invalid quote char {quoteType}"); 61 | 62 | var attributeValueSpan = spaceSpan.Slice(eqIdx + 2); 63 | var quoteEndIdx = attributeValueSpan.IndexOf(quoteType); 64 | if (quoteEndIdx == -1) throw new InvalidDataException("unable to find pair end quote"); 65 | 66 | var attributeValue = attributeValueSpan.Slice(0, quoteEndIdx); 67 | var attributeValueDecoded = DecodeText(attributeValue); 68 | 69 | var assigned = obj.ParseAttribute(ref this, attributeName, attributeValueDecoded); 70 | if (m_abort) return -1; 71 | if (!assigned) 72 | { 73 | s_logger.Warn("[XmlReadBuffer]: unhandled attribute {AttributeName} on {ClassName}. \"{Value}\"", 74 | attributeName.ToString(), obj.GetNodeName().ToString(), attributeValue.ToString()); 75 | } 76 | 77 | position += attributeName.Length + attributeValue.Length + 2 + 1; // ='' -- 3 chars 78 | } 79 | return position; 80 | } 81 | 82 | /// Parse an XML node and children into structured class 83 | /// Text to parse 84 | /// Object to receive parsed data 85 | /// Position within that the node ends at 86 | /// Unable to parse data 87 | /// Internal error 88 | private int ReadInto(ReadOnlySpan span, ref T obj) where T: IXmlSerializable, allows ref struct 89 | { 90 | m_depth++; 91 | if (m_depth >= m_params.m_maxDepth) 92 | { 93 | throw new Exception($"maximum depth {m_params.m_maxDepth} reached"); 94 | } 95 | var primary = true; 96 | for (var i = 0; i < span.Length;) 97 | { 98 | var currSpan = span.Slice(i); 99 | 100 | if (currSpan[0] != '<') 101 | { 102 | var idxOfAngleBracket = currSpan.IndexOf('<'); 103 | if (idxOfAngleBracket == -1) break; 104 | i += idxOfAngleBracket; 105 | continue; 106 | } 107 | 108 | if (currSpan.Length > 1) 109 | { 110 | // no need to check length here.. name has to be at least 1 char lol 111 | if (currSpan[1] == '/') 112 | { 113 | // current block has ended 114 | m_depth--; 115 | return i+2; // todo: hmm. this make caller responsible for aligning again 116 | } 117 | if (currSpan[1] == '?') 118 | { 119 | // skip xml declaration 120 | // e.g 121 | 122 | var declarationEnd = currSpan.IndexOf(c_declarationEnd); 123 | if (declarationEnd == -1) throw new InvalidDataException("where is declaration end"); 124 | i += declarationEnd+c_declarationEnd.Length; 125 | continue; 126 | } 127 | if (currSpan.StartsWith(c_commentStart)) 128 | { 129 | var commentEnd = currSpan.IndexOf(c_commentEnd); 130 | if (commentEnd == -1) throw new InvalidDataException("where is comment end"); 131 | i += commentEnd+c_commentEnd.Length; 132 | continue; 133 | } 134 | if (currSpan[1] == '!') 135 | { 136 | throw new Exception("xml data type definitions are not supported"); 137 | } 138 | } 139 | 140 | var closeBraceIdx = currSpan.IndexOf('>'); 141 | var spaceIdx = currSpan.IndexOf(' '); 142 | if (spaceIdx > closeBraceIdx) spaceIdx = -1; // we are looking for a space in the node declaration 143 | var nameEndIdx = Math.Min(closeBraceIdx, spaceIdx); 144 | if (nameEndIdx == -1) nameEndIdx = closeBraceIdx; // todo min of 1 and -1 is -1 145 | if (nameEndIdx == -1) throw new InvalidDataException("unable to find end of node name"); 146 | 147 | var noBody = false; 148 | if (currSpan[nameEndIdx - 1] == '/') 149 | { 150 | // 151 | noBody = true; 152 | nameEndIdx -= 1; 153 | } 154 | var nodeName = currSpan.Slice(1, nameEndIdx - 1); 155 | 156 | const int unassignedIdx = int.MinValue; 157 | 158 | if (primary) 159 | { 160 | // read actual node 161 | 162 | int afterAttrs; 163 | 164 | if (spaceIdx != -1) 165 | { 166 | afterAttrs = spaceIdx+1; // skip space 167 | afterAttrs = DeserializeAttributes(currSpan, closeBraceIdx, afterAttrs, ref obj); 168 | if (m_abort) 169 | { 170 | m_depth--; 171 | return -1; 172 | } 173 | } else 174 | { 175 | afterAttrs = closeBraceIdx; 176 | } 177 | 178 | var afterAttrsChar = currSpan[afterAttrs]; 179 | 180 | if (noBody || afterAttrsChar == '/') 181 | { 182 | // no body 183 | m_depth--; 184 | return i + closeBraceIdx + 1; 185 | } 186 | 187 | primary = false; 188 | 189 | if (afterAttrsChar != '>') 190 | throw new InvalidDataException( 191 | "char after attributes should have been the end of the node, but it isn't"); 192 | 193 | var bodySpan = currSpan.Slice(closeBraceIdx+1); 194 | 195 | var endIdx = unassignedIdx; 196 | 197 | var handled = obj.ParseFullBody(ref this, bodySpan, ref endIdx); 198 | if (m_abort) 199 | { 200 | m_depth--; 201 | return -1; 202 | } 203 | 204 | if (handled) 205 | { 206 | if (endIdx == unassignedIdx) throw new Exception("endIdx should be set if returning true from ParseFullBody"); 207 | 208 | var fullSpanIdx = afterAttrs + 1 + endIdx; 209 | 210 | // should be 211 | if (currSpan[fullSpanIdx] != '<' || 212 | currSpan[fullSpanIdx + 1] != '/' || 213 | !currSpan.Slice(fullSpanIdx + 2, nodeName.Length).SequenceEqual(nodeName) || 214 | currSpan[fullSpanIdx + 2 + nodeName.Length] != '>') 215 | { 216 | 217 | throw new InvalidDataException("Unexpected data after handling full body"); 218 | } 219 | return i + fullSpanIdx + 2 + nodeName.Length; 220 | } else 221 | { 222 | i += closeBraceIdx+1; 223 | continue; 224 | } 225 | } else 226 | { 227 | // read child nodes 228 | 229 | // todo: i would like to use nullable but the language doesn't like it (can't "out int" into "ref int?") 230 | var endIdx = unassignedIdx; 231 | var endInnerIdx = unassignedIdx; 232 | 233 | var innerBodySpan = currSpan.Slice(closeBraceIdx+1); 234 | var parsedSub = obj.ParseSubBody(ref this, nodeName, 235 | currSpan, innerBodySpan, 236 | ref endIdx, ref endInnerIdx); 237 | if (m_abort) 238 | { 239 | m_depth--; 240 | return -1; 241 | } 242 | if (parsedSub) 243 | { 244 | if (endIdx != unassignedIdx) 245 | { 246 | i += endIdx; 247 | continue; 248 | } else if (endInnerIdx != unassignedIdx) 249 | { 250 | // (3 + nodeName.Length) = 251 | i += closeBraceIdx + 1 + endInnerIdx + (3 + nodeName.Length); 252 | continue; 253 | } else 254 | { 255 | throw new Exception("one of endIdx or endInnerIdx should be set if returning true from ParseSubBody"); 256 | } 257 | } else 258 | { 259 | throw new InvalidDataException($"Unknown sub body {nodeName.ToString()} on {obj.GetNodeName().ToString()}"); 260 | } 261 | } 262 | 263 | #pragma warning disable 162 264 | throw new Exception("bottom of parser loop should be unreachable"); 265 | #pragma warning restore 162 266 | } 267 | m_depth--; 268 | return span.Length; 269 | } 270 | 271 | private ReadOnlySpan DeserializeElementRawInnerText(ReadOnlySpan span, out int endEndIdx) 272 | { 273 | endEndIdx = span.IndexOf('<'); // find start of next node 274 | if (endEndIdx == -1) throw new InvalidDataException("unable to find end of text"); 275 | var textSlice = span.Slice(0, endEndIdx); 276 | return DecodeText(textSlice); 277 | } 278 | 279 | /// Decode XML encoded text 280 | /// 281 | /// Decoded text 282 | private ReadOnlySpan DecodeText(ReadOnlySpan input) 283 | { 284 | var andIndex = input.IndexOf('&'); 285 | if (andIndex == -1) 286 | { 287 | // no need to decode :) 288 | return input; 289 | } 290 | return WebUtility.HtmlDecode(input.ToString()); // todo: allocates input as string, gross 291 | } 292 | 293 | /// 294 | /// Deserialize XML element inner text. Switches between CDATA and raw text on 295 | /// 296 | /// Span at the beginning of the element's inner text 297 | /// The index of the end of the text within 298 | /// Deserialized inner text data 299 | /// The bounds of the text could not be determined 300 | public ReadOnlySpan DeserializeCDATA(ReadOnlySpan span, out int endEndIdx) 301 | { 302 | if (m_params.m_cdataMode == CDataMode.Off) 303 | { 304 | return DeserializeElementRawInnerText(span, out endEndIdx); 305 | } 306 | // todo: CDATA cannot contain the string "]]>" anywhere in the XML document. 307 | 308 | if (!span.StartsWith(c_cdataStart)) throw new InvalidDataException("invalid cdata start"); 309 | 310 | var endIdx = span.IndexOf(c_cdataEnd); 311 | if (endIdx == -1) throw new InvalidDataException("unable to find end of cdata"); 312 | 313 | endEndIdx = c_cdataEnd.Length + endIdx; 314 | 315 | var stringData = span.Slice(c_cdataStart.Length, endIdx - c_cdataStart.Length); 316 | if (m_params.m_cdataMode == CDataMode.OnEncoded) 317 | { 318 | return DecodeText(stringData); 319 | } 320 | return stringData; 321 | } 322 | 323 | /// 324 | /// Create a new instance of and parse into it 325 | /// 326 | /// Text to parse 327 | /// Index into that is at the end of the node 328 | /// Type to parse 329 | /// The created instance 330 | public T Read(ReadOnlySpan span, out int end) where T: IXmlSerializable, new(), allows ref struct 331 | { 332 | var t = new T(); 333 | end = ReadInto(span, ref t); 334 | return t; 335 | } 336 | 337 | public void ReadInto(ReadOnlySpan span, ref T obj, out int end) where T: IXmlSerializable, allows ref struct 338 | { 339 | end = ReadInto(span, ref obj); 340 | } 341 | 342 | /// 343 | /// The same as but without the `end` out parameter 344 | /// 345 | /// Text to parse 346 | /// Type to parse 347 | /// The created instance 348 | public T Read(ReadOnlySpan span) where T: IXmlSerializable, new(), allows ref struct 349 | { 350 | return Read(span, out _); 351 | } 352 | 353 | public static T ReadStatic(ReadOnlySpan span, XmlReadParams? para=null) where T: IXmlSerializable, new(), allows ref struct 354 | { 355 | var reader = new XmlReadBuffer 356 | { 357 | m_params = para ?? new XmlReadParams() 358 | }; 359 | return reader.Read(span); 360 | } 361 | 362 | /// 363 | /// Parse into a new instance without manually creating a XmlReadBuffer 364 | /// 365 | /// Text to parse 366 | /// 367 | /// Type to parse 368 | /// The created instance 369 | public static T ReadStatic(ReadOnlySpan span, CDataMode cdataMode) where T: IXmlSerializable, new(), allows ref struct 370 | { 371 | return ReadStatic(span, new XmlReadParams 372 | { 373 | m_cdataMode = cdataMode 374 | }); 375 | } 376 | 377 | public static void ReadIntoStatic(ReadOnlySpan span, ref T obj, XmlReadParams? para=null) where T: IXmlSerializable, allows ref struct 378 | { 379 | var reader = new XmlReadBuffer 380 | { 381 | m_params = para ?? new XmlReadParams() 382 | }; 383 | reader.ReadInto(span, ref obj); 384 | } 385 | } 386 | } -------------------------------------------------------------------------------- /StackXML/XmlReadParams.cs: -------------------------------------------------------------------------------- 1 | using StackXML.Str; 2 | 3 | namespace StackXML 4 | { 5 | public struct XmlReadParams 6 | { 7 | /// Type of text blocks to deserialize 8 | public CDataMode m_cdataMode = CDataMode.On; 9 | 10 | /// 11 | /// Maximum object that can be reached before an exception will be thrown to protect the application 12 | /// 13 | public int m_maxDepth = 50; 14 | 15 | public IStrParser m_stringParser = StandardStrParser.s_instance; 16 | 17 | public XmlReadParams() 18 | { 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /StackXML/XmlWriteBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Diagnostics; 4 | using System.Runtime.CompilerServices; 5 | using CommunityToolkit.HighPerformance.Buffers; 6 | 7 | namespace StackXML 8 | { 9 | /// Stack based XML serializer 10 | public ref struct XmlWriteBuffer 11 | { 12 | private readonly ArrayPoolBufferWriter m_writer; 13 | public XmlWriteParams m_params; 14 | 15 | /// Whether or not a node head is currently open (> hasn't been written) 16 | private bool m_pendingNodeHeadClose; 17 | 18 | /// 19 | /// Create a new XmlWriteBuffer 20 | /// 21 | /// XmlWriteBuffer instance 22 | public static XmlWriteBuffer Create() 23 | { 24 | return new XmlWriteBuffer(); 25 | } 26 | 27 | /// 28 | /// Actual XmlWriteBuffer constructor 29 | /// 30 | public XmlWriteBuffer() 31 | { 32 | m_writer = new ArrayPoolBufferWriter(); 33 | m_params = new XmlWriteParams(); 34 | m_pendingNodeHeadClose = false; 35 | } 36 | 37 | /// Resize internal char buffer 38 | private void Resize() 39 | { 40 | m_writer.GetSpan(m_writer.Capacity*2); 41 | } 42 | 43 | /// Record of a node that is currently being written into the buffer 44 | public readonly ref struct NodeRecord 45 | { 46 | public readonly ReadOnlySpan m_name; 47 | 48 | public NodeRecord(ReadOnlySpan name) 49 | { 50 | m_name = name; 51 | } 52 | } 53 | 54 | /// 55 | /// Puts a ">" character to signify the end of the current node head ("<name>") if it hasn't been already done 56 | /// 57 | private void CloseNodeHeadForBodyIfOpen() 58 | { 59 | if (!m_pendingNodeHeadClose) return; 60 | PutChar('>'); 61 | m_pendingNodeHeadClose = false; 62 | } 63 | 64 | /// Start an XML node 65 | /// Name of the node 66 | /// Record describing the node 67 | public NodeRecord StartNodeHead(ReadOnlySpan name) 68 | { 69 | CloseNodeHeadForBodyIfOpen(); 70 | 71 | PutChar('<'); 72 | PutString(name); 73 | m_pendingNodeHeadClose = true; 74 | return new NodeRecord(name); 75 | } 76 | 77 | /// End an XML node 78 | /// Record describing the open node 79 | public void EndNode(ref NodeRecord record) 80 | { 81 | if (!m_pendingNodeHeadClose) 82 | { 83 | PutString("'); 86 | } else 87 | { 88 | PutString("/>"); 89 | m_pendingNodeHeadClose = false; 90 | } 91 | } 92 | 93 | /// Escape and put text into the buffer 94 | /// The raw text to write 95 | public void PutCData(ReadOnlySpan text) 96 | { 97 | CloseNodeHeadForBodyIfOpen(); 98 | if (m_params.m_cdataMode == CDataMode.Off) 99 | { 100 | EncodeText(text); 101 | return; 102 | } 103 | 104 | PutString(XmlReadBuffer.c_cdataStart); 105 | if (m_params.m_cdataMode == CDataMode.OnEncoded) EncodeText(text); 106 | else PutString(text); // CDataMode.On 107 | PutString(XmlReadBuffer.c_cdataEnd); 108 | } 109 | 110 | public void PutAttribute(ReadOnlySpan name, ReadOnlySpan value) 111 | { 112 | StartAttrCommon(name); 113 | EncodeText(value, true); 114 | EndAttrCommon(); 115 | } 116 | 117 | public void PutAttribute(ReadOnlySpan name, T value) where T : ISpanFormattable 118 | { 119 | StartAttrCommon(name); 120 | Put(value); 121 | EndAttrCommon(); 122 | } 123 | 124 | public void PutAttribute(ReadOnlySpan name, bool value) 125 | { 126 | // todo: why is bool not ISpanFormattable? 127 | PutAttribute(name, value ? '1' : '0'); 128 | } 129 | 130 | /// Write the starting characters for an attribute (" name=''") 131 | /// Name of the attribute 132 | private void StartAttrCommon(ReadOnlySpan name) 133 | { 134 | Debug.Assert(m_pendingNodeHeadClose); 135 | PutChar(' '); 136 | PutString(name); 137 | PutString("='"); 138 | } 139 | 140 | /// End an attribute 141 | [MethodImpl(MethodImplOptions.AggressiveInlining)] // don't bother calling this 142 | private void EndAttrCommon() 143 | { 144 | PutChar('\''); 145 | } 146 | 147 | public void Put(T value) where T : ISpanFormattable 148 | { 149 | int charsWritten; 150 | while (!m_params.m_stringFormatter.TryFormat(m_writer.GetSpan(), value, out charsWritten)) 151 | { 152 | Resize(); 153 | } 154 | m_writer.Advance(charsWritten); 155 | } 156 | 157 | /// Put a raw into the buffer 158 | /// The string to write 159 | public void PutString(string str) 160 | { 161 | if (string.IsNullOrEmpty(str)) return; 162 | PutString(str.AsSpan()); 163 | } 164 | 165 | /// Put a raw into the buffer 166 | /// The span of text to write 167 | public void PutString(ReadOnlySpan str) 168 | { 169 | if (str.Length == 0) return; 170 | 171 | while (!str.TryCopyTo(m_writer.GetSpan())) 172 | { 173 | Resize(); 174 | } 175 | m_writer.Advance(str.Length); 176 | } 177 | 178 | /// Put a raw into the buffer 179 | /// The character to write 180 | public void PutChar(char c) 181 | { 182 | // todo: use same resize strategy as elsewhere? 183 | m_writer.GetSpan(1)[0] = c; 184 | m_writer.Advance(1); 185 | } 186 | 187 | public void PutObject(in T value) where T : IXmlSerializable, allows ref struct 188 | { 189 | var node = StartNodeHead(value.GetNodeName()); 190 | value.SerializeAttributes(ref this); 191 | value.SerializeBody(ref this); 192 | EndNode(ref node); 193 | } 194 | 195 | /// Allocate and return serialized XML data as a 196 | /// String of serialized XML 197 | public string ToStr() 198 | { 199 | return ToSpan().ToString(); 200 | } 201 | 202 | /// 203 | /// Get of used portion of the internal buffer containing serialized XML data 204 | /// 205 | /// Serialized XML data 206 | public ReadOnlySpan ToSpan() 207 | { 208 | return m_writer.WrittenSpan; 209 | } 210 | 211 | /// Release internal buffer 212 | public void Dispose() 213 | { 214 | m_writer.Dispose(); 215 | } 216 | 217 | /// 218 | /// Serialize a baseclass of to XML text 219 | /// 220 | /// The object to serialize 221 | /// Should text be written as CDATA 222 | /// Serialized XML 223 | public static string SerializeStatic(in T obj, CDataMode cdataMode=CDataMode.On) where T : IXmlSerializable, allows ref struct 224 | { 225 | if (obj == null) throw new ArgumentNullException(nameof(obj)); 226 | var writer = Create(); 227 | writer.m_params.m_cdataMode = cdataMode; 228 | try 229 | { 230 | writer.PutObject(obj); 231 | var str = writer.ToStr(); 232 | return str; 233 | } finally 234 | { 235 | writer.Dispose(); 236 | } 237 | } 238 | 239 | 240 | private static readonly SearchValues s_escapeChars = SearchValues.Create( 241 | [ 242 | '<', '>', '&' 243 | ]); 244 | 245 | private static readonly SearchValues s_escapeCharsAttribute = SearchValues.Create( 246 | [ 247 | '<', '>', '&', '\'', '\"', '\n', '\r', '\t' 248 | ]); 249 | 250 | /// Encode unescaped text into the buffer 251 | /// Unescaped text 252 | /// True if text is for an attribute, false for an element 253 | public void EncodeText(ReadOnlySpan input, bool attribute=false) 254 | { 255 | var escapeChars = attribute ? s_escapeCharsAttribute : s_escapeChars; 256 | 257 | ReadOnlySpan currentInput = input; 258 | while (true) 259 | { 260 | int escapeCharIdx = currentInput.IndexOfAny(escapeChars); 261 | if (escapeCharIdx == -1) 262 | { 263 | PutString(currentInput); 264 | return; 265 | } 266 | 267 | PutString(currentInput.Slice(0, escapeCharIdx)); 268 | 269 | var charToEncode = currentInput[escapeCharIdx]; 270 | PutString(charToEncode switch 271 | { 272 | '<' => "<", 273 | '>' => ">", 274 | '&' => "&", 275 | '\'' => "'", 276 | '\"' => """, 277 | '\n' => " ", 278 | '\r' => " ", 279 | '\t' => " ", 280 | _ => throw new Exception($"unknown escape char \"{charToEncode}\". how did we get here") 281 | }); 282 | currentInput = currentInput.Slice(escapeCharIdx + 1); 283 | } 284 | } 285 | } 286 | } -------------------------------------------------------------------------------- /StackXML/XmlWriteParams.cs: -------------------------------------------------------------------------------- 1 | using StackXML.Str; 2 | 3 | namespace StackXML 4 | { 5 | public struct XmlWriteParams 6 | { 7 | /// Type of text blocks to serialize 8 | public CDataMode m_cdataMode = CDataMode.On; 9 | 10 | public IStrFormatter m_stringFormatter = BaseStrFormatter.s_instance; 11 | 12 | public XmlWriteParams() 13 | { 14 | } 15 | } 16 | } --------------------------------------------------------------------------------