From 3272818fa45d150fc8c8fb1394bd8612e6712b2e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 04:06:45 +0000 Subject: [PATCH 1/4] Add _AsValue property for blittable struct properties in View structs Emit {PropertyName}_AsValue property on generated View structs for blittable struct properties using MemoryMarshal.Read. Add unit test verifying _AsValue presence on blittable struct properties and absence on non-blittable struct properties. --- src/ZeroSerializerGenerator.cs | 30 ++++++++++++++++++++++ tests/SerializationTests.cs | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/ZeroSerializerGenerator.cs b/src/ZeroSerializerGenerator.cs index bbf1538..41d99ec 100644 --- a/src/ZeroSerializerGenerator.cs +++ b/src/ZeroSerializerGenerator.cs @@ -1350,6 +1350,36 @@ var propertyReturnType sourceBuilder.CloseBlock(); sourceBuilder.CloseBlock(); + + if (property.Kind == PropertySerializationKind.BlittableStruct) + { + sourceBuilder.AppendLine(); + string returnType = property.Symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + string readType = GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + sourceBuilder.AppendLine($"{propertyAccessibility} {returnType} {EscapeIdentifier(property.Symbol.Name)}_AsValue"); + sourceBuilder.OpenBlock(); + sourceBuilder.AppendLine("get"); + sourceBuilder.OpenBlock(); + if (containingModel.IsBlittableStruct) + { + sourceBuilder.AppendLine($"return MemoryMarshal.Read<{readType}>(serializedMemory.Span.Slice({property.BlittableByteOffset}, {property.ElementByteCount}));"); + } + else + { + sourceBuilder.AppendLine("ReadOnlySpan serializedData = serializedMemory.Span;"); + sourceBuilder.AppendLine($"int propertyDataOffset = BinaryPrimitives.ReadInt32LittleEndian(serializedData.Slice({propertyIndex * 4}, 4));"); + if (IsNullRepresentedByZeroPropertyOffset(property)) + { + sourceBuilder.AppendLine("if (propertyDataOffset == 0)"); + sourceBuilder.OpenBlock(); + sourceBuilder.AppendLine("return default;"); + sourceBuilder.CloseBlock(); + } + sourceBuilder.AppendLine($"return MemoryMarshal.Read<{readType}>(serializedData.Slice(propertyDataOffset, {property.ElementByteCount}));"); + } + sourceBuilder.CloseBlock(); + sourceBuilder.CloseBlock(); + } } private static void EmitViewCollectionHeader( diff --git a/tests/SerializationTests.cs b/tests/SerializationTests.cs index 927393b..9ee0c17 100644 --- a/tests/SerializationTests.cs +++ b/tests/SerializationTests.cs @@ -1387,4 +1387,50 @@ public void NullableStructViewsSerializeAndDeserializeCorrectly() TestAssert.Equal(ByteState.Ready, viewNull.NonBlittableStruct.ByteState, "NonBlittableStruct.ByteState"); Assert.Null(viewNull.NullableNonBlittableStruct); } + + [Fact] + public void ViewStructProvidesAsValuePropertyOnlyForBlittableStructProperties() + { + Type viewType = typeof(NullableStructContainerModelView); + + // Verify BlittableStruct has _AsValue property + PropertyInfo? blittableAsValueProp = viewType.GetProperty("BlittableStruct_AsValue"); + Assert.NotNull(blittableAsValueProp); + TestAssert.Equal(typeof(PackedRecord), blittableAsValueProp.PropertyType, "BlittableStruct_AsValue PropertyType"); + + // Verify NullableBlittableStruct has _AsValue property + PropertyInfo? nullableBlittableAsValueProp = viewType.GetProperty("NullableBlittableStruct_AsValue"); + Assert.NotNull(nullableBlittableAsValueProp); + TestAssert.Equal(typeof(PackedRecord?), nullableBlittableAsValueProp.PropertyType, "NullableBlittableStruct_AsValue PropertyType"); + + // Verify NonBlittableStruct does NOT have _AsValue property + PropertyInfo? nonBlittableAsValueProp = viewType.GetProperty("NonBlittableStruct_AsValue"); + Assert.Null(nonBlittableAsValueProp); + + // Verify NullableNonBlittableStruct does NOT have _AsValue property + PropertyInfo? nullableNonBlittableAsValueProp = viewType.GetProperty("NullableNonBlittableStruct_AsValue"); + Assert.Null(nullableNonBlittableAsValueProp); + + // Verify behavior on serialized instance + NullableStructContainerModel source = new NullableStructContainerModel + { + BlittableStruct = new PackedRecord { Number = 100, State = SignedState.Positive }, + NullableBlittableStruct = new PackedRecord { Number = 200, State = SignedState.Negative }, + NonBlittableStruct = new EnumStruct(ByteState.Ready, SignedState.Positive), + NullableNonBlittableStruct = null + }; + + byte[] buffer = new byte[256]; + int written = source.Serialize(buffer); + NullableStructContainerModelView view = new NullableStructContainerModelView(buffer.AsMemory(0, written)); + + PackedRecord blittableValue = view.BlittableStruct_AsValue; + TestAssert.Equal(100, blittableValue.Number, "BlittableStruct_AsValue.Number"); + TestAssert.Equal(SignedState.Positive, blittableValue.State, "BlittableStruct_AsValue.State"); + + PackedRecord? nullableBlittableValue = view.NullableBlittableStruct_AsValue; + Assert.NotNull(nullableBlittableValue); + TestAssert.Equal(200, nullableBlittableValue!.Value.Number, "NullableBlittableStruct_AsValue.Number"); + TestAssert.Equal(SignedState.Negative, nullableBlittableValue!.Value.State, "NullableBlittableStruct_AsValue.State"); + } } From b02eb48c0ccd369ad53ee79190df92b0a4dd9e92 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 04:32:09 +0000 Subject: [PATCH 2/4] Add _AsValue property for non-nullable blittable struct properties in View structs Emit {PropertyName}_AsValue property on generated View structs for non-nullable blittable struct properties using MemoryMarshal.Read. Update unit test to verify that _AsValue is present for non-nullable blittable struct properties and absent for nullable or non-blittable properties. --- src/ZeroSerializerGenerator.cs | 18 +++++------------- tests/SerializationTests.cs | 9 ++------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/ZeroSerializerGenerator.cs b/src/ZeroSerializerGenerator.cs index 41d99ec..1bced44 100644 --- a/src/ZeroSerializerGenerator.cs +++ b/src/ZeroSerializerGenerator.cs @@ -1351,31 +1351,23 @@ var propertyReturnType sourceBuilder.CloseBlock(); sourceBuilder.CloseBlock(); - if (property.Kind == PropertySerializationKind.BlittableStruct) + if (property.Kind == PropertySerializationKind.BlittableStruct && !property.IsNullableType) { sourceBuilder.AppendLine(); - string returnType = property.Symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - string readType = GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - sourceBuilder.AppendLine($"{propertyAccessibility} {returnType} {EscapeIdentifier(property.Symbol.Name)}_AsValue"); + string valueType = property.Symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + sourceBuilder.AppendLine($"{propertyAccessibility} {valueType} {EscapeIdentifier(property.Symbol.Name)}_AsValue"); sourceBuilder.OpenBlock(); sourceBuilder.AppendLine("get"); sourceBuilder.OpenBlock(); if (containingModel.IsBlittableStruct) { - sourceBuilder.AppendLine($"return MemoryMarshal.Read<{readType}>(serializedMemory.Span.Slice({property.BlittableByteOffset}, {property.ElementByteCount}));"); + sourceBuilder.AppendLine($"return MemoryMarshal.Read<{valueType}>(serializedMemory.Span.Slice({property.BlittableByteOffset}, {property.ElementByteCount}));"); } else { sourceBuilder.AppendLine("ReadOnlySpan serializedData = serializedMemory.Span;"); sourceBuilder.AppendLine($"int propertyDataOffset = BinaryPrimitives.ReadInt32LittleEndian(serializedData.Slice({propertyIndex * 4}, 4));"); - if (IsNullRepresentedByZeroPropertyOffset(property)) - { - sourceBuilder.AppendLine("if (propertyDataOffset == 0)"); - sourceBuilder.OpenBlock(); - sourceBuilder.AppendLine("return default;"); - sourceBuilder.CloseBlock(); - } - sourceBuilder.AppendLine($"return MemoryMarshal.Read<{readType}>(serializedData.Slice(propertyDataOffset, {property.ElementByteCount}));"); + sourceBuilder.AppendLine($"return MemoryMarshal.Read<{valueType}>(serializedData.Slice(propertyDataOffset, {property.ElementByteCount}));"); } sourceBuilder.CloseBlock(); sourceBuilder.CloseBlock(); diff --git a/tests/SerializationTests.cs b/tests/SerializationTests.cs index 9ee0c17..d86b6eb 100644 --- a/tests/SerializationTests.cs +++ b/tests/SerializationTests.cs @@ -1398,10 +1398,9 @@ public void ViewStructProvidesAsValuePropertyOnlyForBlittableStructProperties() Assert.NotNull(blittableAsValueProp); TestAssert.Equal(typeof(PackedRecord), blittableAsValueProp.PropertyType, "BlittableStruct_AsValue PropertyType"); - // Verify NullableBlittableStruct has _AsValue property + // Verify NullableBlittableStruct does NOT have _AsValue property PropertyInfo? nullableBlittableAsValueProp = viewType.GetProperty("NullableBlittableStruct_AsValue"); - Assert.NotNull(nullableBlittableAsValueProp); - TestAssert.Equal(typeof(PackedRecord?), nullableBlittableAsValueProp.PropertyType, "NullableBlittableStruct_AsValue PropertyType"); + Assert.Null(nullableBlittableAsValueProp); // Verify NonBlittableStruct does NOT have _AsValue property PropertyInfo? nonBlittableAsValueProp = viewType.GetProperty("NonBlittableStruct_AsValue"); @@ -1428,9 +1427,5 @@ public void ViewStructProvidesAsValuePropertyOnlyForBlittableStructProperties() TestAssert.Equal(100, blittableValue.Number, "BlittableStruct_AsValue.Number"); TestAssert.Equal(SignedState.Positive, blittableValue.State, "BlittableStruct_AsValue.State"); - PackedRecord? nullableBlittableValue = view.NullableBlittableStruct_AsValue; - Assert.NotNull(nullableBlittableValue); - TestAssert.Equal(200, nullableBlittableValue!.Value.Number, "NullableBlittableStruct_AsValue.Number"); - TestAssert.Equal(SignedState.Negative, nullableBlittableValue!.Value.State, "NullableBlittableStruct_AsValue.State"); } } From 8aa952b3cb2b3fd5c3525eb0cbd59aed586993b4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 05:25:49 +0000 Subject: [PATCH 3/4] Remove unnecessary null-check fallback in _AsValue generation Remove null-check logic from _AsValue property generation for non-nullable blittable structs per PR review. From 683c1d1f3d8afad391da8df6549d1b98c53346f9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 05:55:42 +0000 Subject: [PATCH 4/4] Remove fallback comment block in generator for blittable struct property view getter Clean up dead fallback branch in PropertySerializationKind.BlittableStruct view property getter generation. --- src/ZeroSerializerGenerator.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/ZeroSerializerGenerator.cs b/src/ZeroSerializerGenerator.cs index 1bced44..fb89257 100644 --- a/src/ZeroSerializerGenerator.cs +++ b/src/ZeroSerializerGenerator.cs @@ -1316,15 +1316,7 @@ var propertyReturnType "propertyDataOffset"); break; case PropertySerializationKind.BlittableStruct: - if (property.NestedSerializableType is not null) - { - sourceBuilder.AppendLine($"return new {GetQualifiedViewName(property.NestedSerializableType)}(serializedMemory.Slice(propertyDataOffset, {property.ElementByteCount}));"); - } - else - { - sourceBuilder.AppendLine("// Fallback generated unexpectedly. According to the specification, this fallback should not be reached (the view always returns the view in any case)."); - sourceBuilder.AppendLine($"return MemoryMarshal.Read<{GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(serializedData.Slice(propertyDataOffset, {property.ElementByteCount}));"); - } + sourceBuilder.AppendLine($"return new {GetQualifiedViewName(property.NestedSerializableType!)}(serializedMemory.Slice(propertyDataOffset, {property.ElementByteCount}));"); break; case PropertySerializationKind.String: EmitViewCollectionHeader(sourceBuilder, "serializedData", "propertyDataOffset");