├── .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