diff --git a/BinaryWizard.Sample/SampleEntity.cs b/BinaryWizard.Sample/SampleEntity.cs index f8e3f16..dcd9ea5 100644 --- a/BinaryWizard.Sample/SampleEntity.cs +++ b/BinaryWizard.Sample/SampleEntity.cs @@ -18,17 +18,18 @@ namespace BinaryWizard.Sample; [BinarySerializable] public partial class SampleEntity { - public int VectorCount; + public int IntegersCount; - [BinaryArray(Size = 2)] public SampleVector[] ConstantSizeVectors; - - [BinaryArray(SizeMember = nameof(VectorCount))] - public SampleVector[] DynamicSizeVectors; + // [BinaryArray(Size = 2)] public int[] ConstantInts; + // [BinaryArray(Size = 2)] public SampleVector[] ConstantSizeVectors; + // + [BinaryArray(SizeMember = nameof(IntegersCount))] + public int[] DynamicSizeVectors; } -[BinarySerializable] -public partial class SampleVector { - public int X; - public int Y; - public int Z; -} \ No newline at end of file +// [BinarySerializable] +// public partial class SampleVector { +// public int X; +// public int Y; +// public int Z; +// } \ No newline at end of file diff --git a/BinaryWizard.Tests/Samples/Arrays.cs b/BinaryWizard.Tests/Samples/Arrays.cs new file mode 100644 index 0000000..cd14b50 --- /dev/null +++ b/BinaryWizard.Tests/Samples/Arrays.cs @@ -0,0 +1,10 @@ +namespace BinaryWizard.Tests.Samples; + +[BinarySerializable] +public partial struct Arrays { + [BinaryArray(Size = 16)] public int[] NumberArray; + public int OtherNumbersSize; + + [BinaryArray(SizeMember = nameof(OtherNumbersSize))] + public int[] OtherNumbers; +} \ No newline at end of file diff --git a/BinaryWizard.Tests/Samples/Entity.cs b/BinaryWizard.Tests/Samples/Entity.cs index add2809..f0bb797 100644 --- a/BinaryWizard.Tests/Samples/Entity.cs +++ b/BinaryWizard.Tests/Samples/Entity.cs @@ -1,8 +1,8 @@ -namespace BinaryWizard.Tests.Samples; - -[BinarySerializable] -public partial struct Entity { - public int Id; - public string Name; - public Vector3 Position; -} \ No newline at end of file +// namespace BinaryWizard.Tests.Samples; +// +// [BinarySerializable] +// public partial struct Entity { +// public int Id; +// public string Name; +// public Vector3 Position; +// } \ No newline at end of file diff --git a/BinaryWizard.Tests/SerializerTests.cs b/BinaryWizard.Tests/SerializerTests.cs index dcbfe7d..422a8fa 100644 --- a/BinaryWizard.Tests/SerializerTests.cs +++ b/BinaryWizard.Tests/SerializerTests.cs @@ -34,31 +34,63 @@ public void Vector3_CorrectlySerialized() { using var reader = new BinaryReader(stream); var actual = Vector3.FromBinary(reader); - var expected = new Vector3 { X = 1, Y = 2, Z = 3 }; + var expected = new Vector3 { + X = 1, Y = 2, Z = 3, + }; Assert.Equal(expected, actual); } [Fact] - public void Entity_CorrectlySerialized() { + public void Arrays_CorrectlySerialized() { using var stream = new MemoryStream(); using (var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true)) { - writer.Write(69); // Id - writer.Write("glomdom"); // Name - - // Position: Vector3 - writer.Write(1); // X - writer.Write(2); // Y - writer.Write(3); // Z + for (var i = 0; i < 16; i++) { + writer.Write(i); + } + + writer.Write(4); + + for (var i = 0; i < 4; i++) { + writer.Write(i); + } } stream.Position = 0; using var reader = new BinaryReader(stream); + var actual = Arrays.FromBinary(reader); + var expected = new Arrays { + NumberArray = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + OtherNumbersSize = 4, + OtherNumbers = [0, 1, 2, 3], + }; - var actual = Entity.FromBinary(reader); - var expected = new Entity { Id = 69, Name = "glomdom", Position = new Vector3 { X = 1, Y = 2, Z = 3 } }; - - Assert.Equal(expected, actual); + Assert.Equal(expected.NumberArray, actual.NumberArray); + Assert.Equal(expected.OtherNumbersSize, actual.OtherNumbersSize); + Assert.Equal(expected.OtherNumbers, actual.OtherNumbers); } + + // [Fact] + // public void Entity_CorrectlySerialized() { + // using var stream = new MemoryStream(); + // using (var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true)) { + // writer.Write(69); // Id + // writer.Write("glomdom"); // Name + // + // // Position: Vector3 + // writer.Write(1); // X + // writer.Write(2); // Y + // writer.Write(3); // Z + // } + // + // stream.Position = 0; + // + // using var reader = new BinaryReader(stream); + // + // var actual = Entity.FromBinary(reader); + // var expected = new Entity { Id = 69, Name = "glomdom", Position = new Vector3 { X = 1, Y = 2, Z = 3 } }; + // + // Assert.Equal(expected, actual); + // } } \ No newline at end of file diff --git a/BinaryWizard/AnalyzerReleases.Unshipped.md b/BinaryWizard/AnalyzerReleases.Unshipped.md index 8c4585e..b543e76 100644 --- a/BinaryWizard/AnalyzerReleases.Unshipped.md +++ b/BinaryWizard/AnalyzerReleases.Unshipped.md @@ -1,8 +1,8 @@ ### New Rules Rule ID | Category | Severity | Notes ---------|----------|----------|---------- -BW0001 | Usage | Error | Object is not marked with `[BinarySerializable]` inside marked object. -BW0002 | Usage | Error | Array type does not have a `[BinaryArray]` attribute. -BW0003 | Usage | Error | Array marked with `[BinaryArray]` is missing argument defining size. -BW0004 | Usage | Error | Array marked with `[BinaryArray]` has conflicting size arguments. \ No newline at end of file +--------|----------|----------|------------- +BW0001 | Usage | Error | BinaryWizard +BW0002 | Usage | Error | BinaryWizard +BW0003 | Usage | Error | BinaryWizard +BW0004 | Usage | Error | BinaryWizard \ No newline at end of file diff --git a/BinaryWizard/BinaryWizard.csproj b/BinaryWizard/BinaryWizard.csproj index cc8b80c..5dace7e 100644 --- a/BinaryWizard/BinaryWizard.csproj +++ b/BinaryWizard/BinaryWizard.csproj @@ -4,7 +4,7 @@ netstandard2.0 false enable - latest + 13 true true diff --git a/BinaryWizard/Context.cs b/BinaryWizard/Context.cs new file mode 100644 index 0000000..f3c248e --- /dev/null +++ b/BinaryWizard/Context.cs @@ -0,0 +1,5 @@ +namespace BinaryWizard; + +public sealed record Context { + public int Offset { get; set; } +} \ No newline at end of file diff --git a/BinaryWizard/DebugUtilities.cs b/BinaryWizard/DebugUtilities.cs new file mode 100644 index 0000000..5e2fb87 --- /dev/null +++ b/BinaryWizard/DebugUtilities.cs @@ -0,0 +1,27 @@ +/* + * Copyright 2025 glomdom + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Diagnostics; +using BinaryWizard.Model; + +namespace BinaryWizard; + +public static class DebugUtilities { + [Conditional("DEBUG")] + public static void CreatedFieldDef(FieldDef def) { + Debug.WriteLine($"Created field definition {def.Name} of {def.TypeModel.Type} ({def.ByteSize} bytes) (dynamic? {def.IsDynamic})"); + } +} \ No newline at end of file diff --git a/BinaryWizard/Generator.cs b/BinaryWizard/Generator.cs index c5415fc..4762eec 100644 --- a/BinaryWizard/Generator.cs +++ b/BinaryWizard/Generator.cs @@ -17,22 +17,23 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Text; +using BinaryWizard.Model; +using BinaryWizard.Segmenting; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -using System.Diagnostics.CodeAnalysis; namespace BinaryWizard; [Generator] public class Generator : IIncrementalGenerator { - private SourceProductionContext _spc; - private readonly SyntaxTrivia _autogeneratedComment = SyntaxFactory.Comment("// "); public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -59,21 +60,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { (s, _) => s is ClassDeclarationSyntax or StructDeclarationSyntax, (ctx, _) => GetTypeDeclarationForSourceGen(ctx)) .Where(t => t.reportAttributeFound) - .Select((t, _) => t.Item1); + .Select((t, _) => t.syntax); var sources = context.CompilationProvider.Combine(provider.Collect()); context.RegisterSourceOutput( sources, - ((spc, t) => { - _spc = spc; - - GenerateCode(t.Left, t.Right); - }) + (spc, t) => { GenerateCode(spc, t.Left, t.Right); } ); } - private static (TypeDeclarationSyntax, bool reportAttributeFound) GetTypeDeclarationForSourceGen(GeneratorSyntaxContext context) { + private static (TypeDeclarationSyntax syntax, bool reportAttributeFound) GetTypeDeclarationForSourceGen(GeneratorSyntaxContext context) { var typeDeclaration = (TypeDeclarationSyntax)context.Node; foreach (var attributeSyntax in typeDeclaration.AttributeLists.SelectMany(list => list.Attributes)) { @@ -86,15 +83,22 @@ private static (TypeDeclarationSyntax, bool reportAttributeFound) GetTypeDeclara return (typeDeclaration, false); } - private void GenerateCode(Compilation compilation, ImmutableArray classDeclarations) { + private void GenerateCode(SourceProductionContext spc, Compilation compilation, ImmutableArray classDeclarations) { + Debug.WriteLine("Starting code generation"); + foreach (var declarationSyntax in classDeclarations) { + var context = new Context(); + var semanticModel = compilation.GetSemanticModel(declarationSyntax.SyntaxTree); if (ModelExtensions.GetDeclaredSymbol(semanticModel, declarationSyntax) is not INamedTypeSymbol classSymbol) continue; var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); var declarationName = declarationSyntax.Identifier.Text; - var method = CreateReadMethod(semanticModel, classSymbol); + var segmentManager = new SegmentManager(); + var method = CreateReadMethod(spc, context, segmentManager, semanticModel, classSymbol); + + Debug.WriteLine($"Created read method for {classSymbol.Name}"); TypeDeclarationSyntax declaration = declarationSyntax is ClassDeclarationSyntax ? SyntaxFactory.ClassDeclaration(declarationName) @@ -108,6 +112,11 @@ private void GenerateCode(Compilation compilation, ImmutableArray BuildReadStatements(SemanticModel semantics, INamedTypeSymbol classSymbol, string outputName) { + private IEnumerable BuildReadStatements(SourceProductionContext spc, + Context ctx, + SegmentManager segmentManager, + SemanticModel semantics, + INamedTypeSymbol classSymbol, + string outputName + ) { var fields = classSymbol.GetMembers().OfType(); foreach (var field in fields) { var fieldType = field.Type; if (IsPrimitiveLike(fieldType)) { - var method = GetReadMethodNameForPrimitive(fieldType); + var fieldDef = new FieldDef(field.Name, fieldType) { + ByteSize = GetByteSizeForPrimitive(fieldType), + }; - yield return SyntaxFactory.ParseStatement($"{outputName}.{field.Name} = reader.{method}();"); - } else { - if (IsArrayLike(fieldType, out var arrSymbol)) { - if (arrSymbol.Rank != 1) throw new NotSupportedException("Arrays which have more than 1 dimension are not supported."); + segmentManager.AddField(fieldDef); + DebugUtilities.CreatedFieldDef(fieldDef); + } else if (IsArrayLike(fieldType, out var arrSymbol)) { + if (arrSymbol.Rank != 1) throw new NotSupportedException("Arrays which have more than 1 dimension are not supported."); - var binaryArrayAttr = field.GetAttributes().FirstOrDefault(a => a.AttributeClass?.Name == "BinaryArrayAttribute"); - if (binaryArrayAttr is null) { - ReportArrayIsMissingAttribute(field); + var binaryArrayAttr = field.GetAttributes().FirstOrDefault(a => a.AttributeClass?.Name == "BinaryArrayAttribute"); + if (binaryArrayAttr is null) { + ReportArrayIsMissingAttribute(spc, field); - yield break; - } + yield break; + } - if (!AreAnyNamedArgsProvided(binaryArrayAttr, "Size", "SizeMember")) { - ReportArrayIsMissingSizeArgument(field); + if (!AreAnyNamedArgsProvided(binaryArrayAttr, "Size", "SizeMember")) { + ReportArrayIsMissingSizeArgument(spc, field); - yield break; - } + yield break; + } - if (AreAllNamedArgsProvided(binaryArrayAttr, "Size", "SizeMember")) { - ReportArrayHasConflictingSizeArguments(field); + if (AreAllNamedArgsProvided(binaryArrayAttr, "Size", "SizeMember")) { + ReportArrayHasConflictingSizeArguments(spc, field); - yield break; - } + yield break; + } - if (TryGetNamedArg(binaryArrayAttr, "Size", out var arrSize)) { - var statements = GetReadStatementsForArrayWithSize(semantics, arrSymbol, outputName, field.Name, (int)arrSize.Value!); + var fieldDef = new FieldDef(field.Name, arrSymbol.ElementType) { + TypeModel = { + InnerType = arrSymbol.ElementType, + InnerTypeByteSize = GetByteSizeForPrimitive(arrSymbol.ElementType), + }, + }; + + if (TryGetNamedArg(binaryArrayAttr, "Size", out var arrSize)) { + var arrSizeValue = (int)arrSize.Value!; + fieldDef.ByteSize = arrSizeValue * GetByteSizeForPrimitive(arrSymbol.ElementType); + fieldDef.TypeModel.FixedArraySize = arrSizeValue; + + DebugUtilities.CreatedFieldDef(fieldDef); + segmentManager.AddField(fieldDef); + } else if (TryGetNamedArg(binaryArrayAttr, "SizeMember", out var sizeMember)) { + var arrSizeRef = (string)sizeMember.Value!; + fieldDef.ByteSize = -1; + + DebugUtilities.CreatedFieldDef(fieldDef); + segmentManager.AddField(fieldDef, arrSizeRef); + } + } else if (HasBinarySerializableAttribute(fieldType)) { + // TODO: maybe implement segmenting for nested reading? unsure of how to implement yet - foreach (var statement in statements) { - yield return statement; - } - } + yield return SyntaxFactory.ParseStatement($"{outputName}.{field.Name} = {fieldType.Name}.FromBinary(reader);"); - if (TryGetNamedArg(binaryArrayAttr, "SizeMember", out var sizeMember)) { - var statements = GetReadStatementsForArrayWithMember(semantics, arrSymbol, outputName, field.Name, (string)sizeMember.Value!); + Debug.WriteLine("Built statements for nested object"); + } else { + ReportUnmarkedSerializableForField(spc, field); - foreach (var statement in statements) { - yield return statement; - } - } + yield break; + } + } - continue; - } + var segments = segmentManager.Commit(); - if (HasBinarySerializableAttribute(fieldType)) { - yield return SyntaxFactory.ParseStatement($"{outputName}.{field.Name} = {fieldType.Name}.FromBinary(reader);"); - } else { - ReportUnmarkedSerializableForField(field); + foreach (var seg in segments) { + if (seg is FixedSegment fixedSeg) { + foreach (var stmt in ProcessFixedSegment(fixedSeg, ctx)) yield return stmt; + } - yield break; - } + if (seg is DynamicSegment dynSeg) { + foreach (var stmt in ProcessDynamicSegment(dynSeg, ctx)) yield return stmt; } } + + segmentManager.Clear(); } - private IEnumerable GetReadStatementsForArrayWithMember(SemanticModel semantics, - IArrayTypeSymbol fieldType, - string outName, - string fieldName, - string memberName - ) { - var elemType = fieldType.ElementType; + private IEnumerable ProcessDynamicSegment(DynamicSegment seg, Context ctx) { + foreach (var field in seg.Fields) { + var elementBytes = GetByteSizeForPrimitive(field.TypeModel.InnerType!); - if (IsPrimitiveLike(elemType)) { + yield return SyntaxFactory.ParseStatement($"result.{field.Name} = new {field.TypeModel.Type}[result.{seg.LengthReferenceFieldName}];"); yield return SyntaxFactory.ParseStatement( - $"for (var i = 0; i < {outName}.{memberName}; i++) {outName}.{fieldName}[i] = reader.{GetReadMethodNameForPrimitive(elemType)}();"); - - yield break; + $"for (var i = 0; i < result.{seg.LengthReferenceFieldName}; i++)" + + " " + + $"result.{field.Name}[i] = {GetBinaryPrimitiveReaderForPrimitive(field.TypeModel.Type)}(buf.Slice({ctx.Offset} + ({elementBytes} * i), {elementBytes}));" + ); + + // TODO: cannot add dynamic size to context. maybe be able to dynamically edit the context in the partial function + // and be able to change from a fixed/dynamic type. + // ctx.Offset += elementBytes * field.TypeModel.FixedArraySize!.Value; } + } - if (HasBinarySerializableAttribute(elemType)) { - yield return SyntaxFactory.ParseStatement($"for (var i = 0; i < {outName}.{memberName}; i++) {outName}.{fieldName}[i] = {elemType.Name}.FromBinary(reader);"); + private IEnumerable ProcessFixedSegment(FixedSegment seg, Context ctx) { + yield return SyntaxFactory.ParseStatement($"Span buf = stackalloc byte[{seg.Bytes}];"); + yield return SyntaxFactory.ParseStatement($"var __bytes_read = reader.Read(buf);"); + yield return SyntaxFactory.ParseStatement($"if (__bytes_read < {seg.Bytes}) throw new EndOfStreamException();"); - yield break; - } + foreach (var field in seg.Fields) { + if (field.TypeModel.IsFixedArray) { + var elementBytes = GetByteSizeForPrimitive(field.TypeModel.InnerType!); - // idk what to do here, still don't know if we can reach this - throw new NotSupportedException($"Failed to create read statements for `{elemType}`"); - } + yield return SyntaxFactory.ParseStatement($"result.{field.Name} = new {field.TypeModel.Type}[{field.TypeModel.FixedArraySize!.Value}];"); + yield return SyntaxFactory.ParseStatement( + $"for (var i = 0; i < {field.TypeModel.FixedArraySize}; i++)" + " " + + $"result.{field.Name}[i] = {GetBinaryPrimitiveReaderForPrimitive(field.TypeModel.Type)}(buf.Slice({ctx.Offset} + ({elementBytes} * i), {elementBytes}));" + ); - private IEnumerable GetReadStatementsForArrayWithSize(SemanticModel semantics, IArrayTypeSymbol fieldType, string outName, string fieldName, int arrSize) { - var elemType = fieldType.ElementType; + ctx.Offset += elementBytes * field.TypeModel.FixedArraySize!.Value; - // little optimization for bytes - if (elemType.SpecialType == SpecialType.System_Byte) { - yield return SyntaxFactory.ParseStatement($"{outName}.{fieldName} = reader.ReadBytes({arrSize});"); + continue; + } - yield break; + yield return SyntaxFactory.ParseStatement( + $"result.{field.Name} = {GetBinaryPrimitiveReaderForPrimitive(field.TypeModel.Type)}(buf.Slice({ctx.Offset}, {field.ByteSize}));" + ); + + ctx.Offset += field.ByteSize; } + } + + // TODO: Context is required if we want to support endianness. + private string GetBinaryPrimitiveReaderForPrimitive(ITypeSymbol sym) { + return sym.SpecialType switch { + SpecialType.System_Int32 => "BinaryPrimitives.ReadInt32LittleEndian", + + _ => throw new InvalidOperationException( + $"Unexpected type {sym.SpecialType} occurred in GetBinaryPrimitiveReaderForPrimitive" + ), + }; + } + + private IEnumerable GetReadStatementsForArrayWithMember( + IArrayTypeSymbol fieldType, + string outName, + string fieldName, + string memberName + ) { + Debug.WriteLine("Building statements for array with size variable"); + + var elemType = fieldType.ElementType; if (IsPrimitiveLike(elemType)) { - yield return SyntaxFactory.ParseStatement($"for (var i = 0; i < {arrSize}; i++) {outName}.{fieldName}[i] = reader.{GetReadMethodNameForPrimitive(elemType)}();"); + yield return SyntaxFactory.ParseStatement( + $"for (var i = 0; i < {outName}.{memberName}; i++) {outName}.{fieldName}[i] = reader.{GetReadMethodNameForPrimitive(elemType)}();"); yield break; } if (HasBinarySerializableAttribute(elemType)) { - yield return SyntaxFactory.ParseStatement($"for (var i = 0; i < {arrSize}; i++) {outName}.{fieldName}[i] = {elemType.Name}.FromBinary(reader);"); + yield return SyntaxFactory.ParseStatement($"for (var i = 0; i < {outName}.{memberName}; i++) {outName}.{fieldName}[i] = {elemType.Name}.FromBinary(reader);"); yield break; } @@ -261,8 +329,27 @@ private static TypedConstant OneArgOfMany(AttributeData data, params string[] ar private static bool HasBinarySerializableAttribute(ITypeSymbol sym) => HasAttribute(sym, "BinarySerializableAttribute"); private static bool HasAttribute(ITypeSymbol sym, string attrName) => sym.GetAttributes().Any(a => a.AttributeClass?.Name == attrName); + private int GetByteSizeForPrimitive(ITypeSymbol primitive) { + return primitive.SpecialType switch { + SpecialType.System_Boolean => 1, + SpecialType.System_Char => 1, + SpecialType.System_SByte => 1, + SpecialType.System_Byte => 1, + SpecialType.System_Int16 => 2, + SpecialType.System_UInt16 => 2, + SpecialType.System_Int32 => 4, + SpecialType.System_UInt32 => 4, + SpecialType.System_Int64 => 8, + SpecialType.System_UInt64 => 8, + SpecialType.System_Decimal => 16, + SpecialType.System_Double => 32, + + _ => throw new InvalidOperationException("Unexpected case encountered."), + }; + } + private string GetReadMethodNameForPrimitive(ITypeSymbol primitive) { - return (primitive.SpecialType) switch { + return primitive.SpecialType switch { SpecialType.System_Boolean => "ReadBoolean", SpecialType.System_Char => "ReadChar", SpecialType.System_SByte => "ReadSByte", @@ -271,8 +358,8 @@ private string GetReadMethodNameForPrimitive(ITypeSymbol primitive) { SpecialType.System_UInt16 => "ReadUInt16", SpecialType.System_Int32 => "ReadInt32", SpecialType.System_UInt32 => "ReadUInt32", - SpecialType.System_Int64 => "ReadInt32", - SpecialType.System_UInt64 => "ReadUInt62", + SpecialType.System_Int64 => "ReadInt64", + SpecialType.System_UInt64 => "ReadUInt64", SpecialType.System_Decimal => "ReadDecimal", SpecialType.System_Double => "ReadDouble", SpecialType.System_String => "ReadString", @@ -346,40 +433,40 @@ out TypedConstant typedConstant return false; } - private void ReportArrayHasConflictingSizeArguments(IFieldSymbol field) { + private void ReportArrayHasConflictingSizeArguments(SourceProductionContext spc, IFieldSymbol field) { var location = GetVariableDeclaratorNameLocation(field); - _spc.ReportDiagnostic(Diagnostic.Create( + spc.ReportDiagnostic(Diagnostic.Create( Diagnostics.ArrayHasConflictingSizeArguments, location, field.Name )); } - private void ReportArrayIsMissingSizeArgument(IFieldSymbol field) { + private void ReportArrayIsMissingSizeArgument(SourceProductionContext spc, IFieldSymbol field) { var location = GetVariableDeclaratorNameLocation(field); - _spc.ReportDiagnostic(Diagnostic.Create( + spc.ReportDiagnostic(Diagnostic.Create( Diagnostics.MarkedArraylikeHasNoSizeOrSizeProviderRule, location, field.Name )); } - private void ReportArrayIsMissingAttribute(IFieldSymbol field) { + private void ReportArrayIsMissingAttribute(SourceProductionContext spc, IFieldSymbol field) { var location = GetVariableDeclaratorNameLocation(field); - _spc.ReportDiagnostic(Diagnostic.Create( + spc.ReportDiagnostic(Diagnostic.Create( Diagnostics.ArrayHasNoBinaryArrayAttributeRule, location, field.Name )); } - private void ReportUnmarkedSerializableForField(IFieldSymbol field) { + private void ReportUnmarkedSerializableForField(SourceProductionContext spc, IFieldSymbol field) { var location = GetVariableDeclaratorLocation(field); - _spc.ReportDiagnostic(Diagnostic.Create( + spc.ReportDiagnostic(Diagnostic.Create( Diagnostics.MissingBinarySerializableAttributeRule, location, field.Type.Name diff --git a/BinaryWizard/Model/FieldDef.cs b/BinaryWizard/Model/FieldDef.cs new file mode 100644 index 0000000..c63f6bf --- /dev/null +++ b/BinaryWizard/Model/FieldDef.cs @@ -0,0 +1,32 @@ +/* + * Copyright 2025 glomdom + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.CodeAnalysis; + +namespace BinaryWizard.Model; + +public sealed record FieldDef { + public string Name { get; set; } + public int ByteSize { get; set; } + public TypeModel TypeModel { get; set; } + + public bool IsDynamic => ByteSize == -1; + + public FieldDef(string name, ITypeSymbol type) { + Name = name; + TypeModel = new TypeModel(type); + } +} \ No newline at end of file diff --git a/BinaryWizard/Model/TypeModel.cs b/BinaryWizard/Model/TypeModel.cs new file mode 100644 index 0000000..30bade9 --- /dev/null +++ b/BinaryWizard/Model/TypeModel.cs @@ -0,0 +1,34 @@ +/* + * Copyright 2025 glomdom + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.CodeAnalysis; + +namespace BinaryWizard.Model; + +public sealed record TypeModel { + public ITypeSymbol Type { get; set; } + + public ITypeSymbol? InnerType { get; set; } + public int? FixedArraySize { get; set; } + public int? InnerTypeByteSize { get; set; } + public bool IsFixedArray => FixedArraySize is not null && InnerType is not null; + public bool IsDynamicArray => FixedArraySize is null && InnerType is not null; + + public TypeModel(ITypeSymbol type, int? fixedArraySize = null) { + Type = type; + FixedArraySize = fixedArraySize; + } +} \ No newline at end of file diff --git a/BinaryWizard/Segmenting/DynamicSegment.cs b/BinaryWizard/Segmenting/DynamicSegment.cs new file mode 100644 index 0000000..82f8dce --- /dev/null +++ b/BinaryWizard/Segmenting/DynamicSegment.cs @@ -0,0 +1,30 @@ +/* + * Copyright 2025 glomdom + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; +using BinaryWizard.Model; + +namespace BinaryWizard.Segmenting; + +public record DynamicSegment : Segment { + public IReadOnlyList Fields { get; set; } + public string LengthReferenceFieldName { get; } + + public DynamicSegment(List fields, string lengthRef) { + Fields = fields; + LengthReferenceFieldName = lengthRef; + } +} \ No newline at end of file diff --git a/BinaryWizard/Segmenting/FixedSegment.cs b/BinaryWizard/Segmenting/FixedSegment.cs new file mode 100644 index 0000000..3f9721d --- /dev/null +++ b/BinaryWizard/Segmenting/FixedSegment.cs @@ -0,0 +1,30 @@ +/* + * Copyright 2025 glomdom + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; +using BinaryWizard.Model; + +namespace BinaryWizard.Segmenting; + +public record FixedSegment : Segment { + public IReadOnlyList Fields { get; set; } + public int Bytes { get; set; } + + public FixedSegment(List fields, int bytes) { + Fields = fields; + Bytes = bytes; + } +} \ No newline at end of file diff --git a/BinaryWizard/Segmenting/Segment.cs b/BinaryWizard/Segmenting/Segment.cs new file mode 100644 index 0000000..38ca2df --- /dev/null +++ b/BinaryWizard/Segmenting/Segment.cs @@ -0,0 +1,19 @@ +/* + * Copyright 2025 glomdom + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace BinaryWizard.Segmenting; + +public abstract record Segment; \ No newline at end of file diff --git a/BinaryWizard/Segmenting/SegmentManager.cs b/BinaryWizard/Segmenting/SegmentManager.cs new file mode 100644 index 0000000..82dcbbe --- /dev/null +++ b/BinaryWizard/Segmenting/SegmentManager.cs @@ -0,0 +1,75 @@ +/* + * Copyright 2025 glomdom + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using BinaryWizard.Model; + +namespace BinaryWizard.Segmenting; + +sealed internal class SegmentManager { + private readonly List _segments = []; + private readonly List _currentFields = []; + private int _currentFixedSize; + + public void AddField(FieldDef field, string? lengthRef = null) { + if (field.IsDynamic) { + if (string.IsNullOrEmpty(lengthRef)) throw new ArgumentNullException(nameof(lengthRef), "Length reference was not provided when field is dynamic"); + + CommitFixed(); + + _segments.Add(new DynamicSegment([field], lengthRef!)); + + Debug.WriteLine($"Added dynamic segment for {field.Name} (dep. {lengthRef})"); + } else { + _currentFields.Add(field); + + var size = field.TypeModel.IsFixedArray + ? field.TypeModel.FixedArraySize!.Value * field.TypeModel.InnerTypeByteSize!.Value + : field.ByteSize; + + _currentFixedSize += size; + } + } + + public IReadOnlyList Commit() { + CommitFixed(); + + Debug.WriteLine($"Finalized segmenting with {_segments.Count} segments"); + + return _segments; + } + + private void CommitFixed() { + if (_currentFields.Count == 0) { + return; + } + + Debug.WriteLine($"Committing fixed segment with {_currentFields.Count} segments and size of {_currentFixedSize} bytes"); + + _segments.Add(new FixedSegment(_currentFields.ToList(), _currentFixedSize)); + _currentFields.Clear(); + _currentFixedSize = 0; + } + + public void Clear() { + _segments.Clear(); + _currentFields.Clear(); + _currentFixedSize = 0; + } +} \ No newline at end of file