diff --git a/Schema.Test/CodeGenerationRoundTripTests.cs b/Schema.Test/CodeGenerationRoundTripTests.cs
index d9a76c1..14e8ec7 100644
--- a/Schema.Test/CodeGenerationRoundTripTests.cs
+++ b/Schema.Test/CodeGenerationRoundTripTests.cs
@@ -230,6 +230,68 @@ public void TestAPromisingClassStartsAtItsDefaults()
Assert.AreEqual(2.5f, triple.GetProperty("X")!.GetValue(instance));
}
+ ///
+ /// A member of a semantic type, of a vector or of a colour starts at its declared default,
+ /// the value the C++ generator starts it at.
+ ///
+ ///
+ /// Validation accepts a numeric default on all of these, and the attribute carrying it was
+ /// always written - but with no initialiser, a fresh instance started at zero. Reading the
+ /// attribute back cannot see that; only constructing an instance can.
+ ///
+ /// Whether the class is a struct, which needs a constructor
+ /// before its members may have initialisers.
+ [TestMethod]
+ [DataRow(false)]
+ [DataRow(true)]
+ public void TestSemanticVectorAndColourMembersStartAtTheirDefaults(bool travelsAsBytes)
+ {
+ Schema original = new();
+ original.AddSemanticType("Kilograms".As())!.SetUnderlyingType(new Models.Types.Float());
+ original.AddSemanticType("Count".As())!.SetUnderlyingType(new Models.Types.Int());
+ original.AddSemanticType("Heavy".As())!.SetUnderlyingType(
+ new Models.Types.Semantic { SemanticTypeName = "Kilograms".As() });
+
+ SchemaClass body = original.AddClass("Body".As())!;
+ body.TravelsAsBytes = travelsAsBytes;
+ AddDefaulted(body, "Mass", new Models.Types.Semantic { SemanticTypeName = "Kilograms".As() }, 2.5);
+ AddDefaulted(body, "Offset", new Models.Types.Semantic { SemanticTypeName = "Kilograms".As() }, -1.5);
+ AddDefaulted(body, "Parts", new Models.Types.Semantic { SemanticTypeName = "Count".As() }, 3);
+ AddDefaulted(body, "Ballast", new Models.Types.Semantic { SemanticTypeName = "Heavy".As() }, 4);
+ AddDefaulted(body, "Scale", new Models.Types.Vector3(), 1);
+ AddDefaulted(body, "Cell", new Models.Types.Vector2 { ElementType = new Models.Types.Int() }, 2);
+ AddDefaulted(body, "Tint", new Models.Types.ColorRGBA(), 0.5);
+
+ SchemaGenerationResult result = SchemaGenerator.Generate(original, CodeGenerationTests.ConfigureGenerator(original));
+ Assert.IsTrue(result.IsSuccess, result.Message);
+
+ Assembly assembly = GeneratedSourceCompiler.Compile(result.Files);
+ Type type = assembly.GetType("Generated.Body", throwOnError: true)!;
+ object instance = Activator.CreateInstance(type)!;
+
+ Assert.AreEqual(2.5f, SemanticValue(instance, "Mass"));
+ Assert.AreEqual(-1.5f, SemanticValue(instance, "Offset"));
+ Assert.AreEqual(3, SemanticValue(instance, "Parts"));
+ Assert.AreEqual(4f, SemanticValue(instance, "Ballast"));
+ Assert.AreEqual(new System.Numerics.Vector3(1f), type.GetProperty("Scale")!.GetValue(instance));
+ Assert.AreEqual(Activator.CreateInstance(type.GetProperty("Cell")!.PropertyType, 2, 2), type.GetProperty("Cell")!.GetValue(instance));
+ Assert.AreEqual(new Runtime.ColorRgba(0.5f, 0.5f, 0.5f, 0.5f), type.GetProperty("Tint")!.GetValue(instance));
+
+ static void AddDefaulted(SchemaClass owner, string name, Models.Types.BaseType memberType, double value)
+ {
+ SchemaMember member = owner.AddMember(name.As())!;
+ member.SetType(memberType);
+ member.DefaultValue = new NumberDefault { Value = value };
+ }
+
+ // A semantic member's value, read through the Value property of the type it holds.
+ static object SemanticValue(object owner, string property)
+ {
+ object held = owner.GetType().GetProperty(property)!.GetValue(owner)!;
+ return held.GetType().GetProperty("Value")!.GetValue(held)!;
+ }
+ }
+
///
/// A class that travels as bytes, holding one that does the same - which is what the promise
/// permits and what the C# side could not represent before.
diff --git a/Schema/Generation/CSharpCodeGenerator.cs b/Schema/Generation/CSharpCodeGenerator.cs
index 7edfe7b..1a862af 100644
--- a/Schema/Generation/CSharpCodeGenerator.cs
+++ b/Schema/Generation/CSharpCodeGenerator.cs
@@ -754,26 +754,72 @@ private static string ObjectInitialiserFor(Models.Types.Object objectType) =>
///
/// Gets the initialiser for a member's default, or null when it has none that fits.
///
+ private static string? DefaultInitialiserFor(SchemaMember member) =>
+ member.DefaultValue is { } value && DefaultLiteralFor(value, member.Type) is string literal
+ ? $" = {literal};"
+ : null;
+
+ ///
+ /// Writes a default as an expression of the given type, or null when it has none that fits.
+ ///
///
+ /// Validation reads a default through a semantic type to its representation, and lets a single
+ /// number stand for every component of a vector, so both are written here the way the C++
+ /// generator writes them: a semantic type through its explicit conversion, and a vector with the
+ /// number in each component.
+ ///
/// A default of a kind the member cannot hold is a validation error, and generation is refused
/// for a schema that has one - so the mismatched cases here are only reachable by calling this
/// generator directly on a schema that was never validated. They fall through to the type's
/// own initialiser rather than emitting source that does not compile.
+ ///
///
- private static string? DefaultInitialiserFor(SchemaMember member) => (member.DefaultValue, member.Type) switch
+ private static string? DefaultLiteralFor(MemberDefault value, BaseType type) => (value, type) switch
{
- (NumberDefault number, Int) => $" = {(long)number.Value};",
- (NumberDefault number, Long) => $" = {(long)number.Value}L;",
- (NumberDefault number, Float) when !double.IsFinite(number.Value) => $" = float.{NonFinite(number.Value)};",
- (NumberDefault number, Double) when !double.IsFinite(number.Value) => $" = double.{NonFinite(number.Value)};",
- (NumberDefault number, Float) => $" = {number.Value.ToString("R", CultureInfo.InvariantCulture)}f;",
- (NumberDefault number, Double) => $" = {Literal(number.Value)};",
- (BooleanDefault boolean, Bool) => $" = {(boolean.Value ? "true" : "false")};",
- (TextDefault text, Models.Types.String) => $" = {Quote(text.Value)};",
- (TextDefault text, Models.Types.Enum enumType) => $" = {CSharpKeywords.Identifier(enumType.EnumName)}.{CSharpKeywords.Identifier(text.Value)};",
+ (NumberDefault number, Int) => $"{(long)number.Value}",
+ (NumberDefault number, Long) => $"{(long)number.Value}L",
+ (NumberDefault number, Float) when !double.IsFinite(number.Value) => $"float.{NonFinite(number.Value)}",
+ (NumberDefault number, Double) when !double.IsFinite(number.Value) => $"double.{NonFinite(number.Value)}",
+ (NumberDefault number, Float) => $"{number.Value.ToString("R", CultureInfo.InvariantCulture)}f",
+ (NumberDefault number, Double) => Literal(number.Value),
+ (BooleanDefault boolean, Bool) => boolean.Value ? "true" : "false",
+ (TextDefault text, Models.Types.String) => Quote(text.Value),
+ (TextDefault text, Models.Types.Enum enumType) => $"{CSharpKeywords.Identifier(enumType.EnumName)}.{CSharpKeywords.Identifier(text.Value)}",
+
+ // The colours before their vector bases, as in MapType: ColorRGB derives from Vector3.
+ (NumberDefault, ColorRGB or ColorRGBA) => ComponentsLiteral(value, type, new Float()),
+ (NumberDefault, Vector vector) => ComponentsLiteral(value, vector, vector.ElementType),
+
+ // A representation that is itself a semantic type is a refinement chain that never reached
+ // anything real, which validation reports; following it again would have no bottom.
+ (_, Semantic { Declaration: SchemaSemanticType declaration } semantic)
+ when declaration.Representation() is not Semantic
+ && DefaultLiteralFor(value, declaration.Representation()) is string represented
+ => $"({MapType(semantic)})({represented})",
+
_ => null,
};
+ ///
+ /// Writes a vector or colour default: one number, in every component.
+ ///
+ private static string? ComponentsLiteral(MemberDefault value, BaseType type, BaseType elementType)
+ {
+ if (DefaultLiteralFor(value, elementType) is not string component)
+ {
+ return null;
+ }
+
+ int count = type switch
+ {
+ Vector2 => 2,
+ Vector3 => 3,
+ _ => 4,
+ };
+
+ return $"new {MapType(type)}({string.Join(", ", Enumerable.Repeat(component, count))})";
+ }
+
///
/// Names one of the two values a floating-point number can hold that has no literal.
///