Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions Schema.Test/CodeGenerationRoundTripTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,68 @@ public void TestAPromisingClassStartsAtItsDefaults()
Assert.AreEqual(2.5f, triple.GetProperty("X")!.GetValue(instance));
}

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="travelsAsBytes">Whether the class is a struct, which needs a constructor
/// before its members may have initialisers.</param>
[TestMethod]
[DataRow(false)]
[DataRow(true)]
public void TestSemanticVectorAndColourMembersStartAtTheirDefaults(bool travelsAsBytes)
{
Schema original = new();
original.AddSemanticType("Kilograms".As<SemanticTypeName>())!.SetUnderlyingType(new Models.Types.Float());
original.AddSemanticType("Count".As<SemanticTypeName>())!.SetUnderlyingType(new Models.Types.Int());
original.AddSemanticType("Heavy".As<SemanticTypeName>())!.SetUnderlyingType(
new Models.Types.Semantic { SemanticTypeName = "Kilograms".As<SemanticTypeName>() });

SchemaClass body = original.AddClass("Body".As<ClassName>())!;
body.TravelsAsBytes = travelsAsBytes;
AddDefaulted(body, "Mass", new Models.Types.Semantic { SemanticTypeName = "Kilograms".As<SemanticTypeName>() }, 2.5);
AddDefaulted(body, "Offset", new Models.Types.Semantic { SemanticTypeName = "Kilograms".As<SemanticTypeName>() }, -1.5);
AddDefaulted(body, "Parts", new Models.Types.Semantic { SemanticTypeName = "Count".As<SemanticTypeName>() }, 3);
AddDefaulted(body, "Ballast", new Models.Types.Semantic { SemanticTypeName = "Heavy".As<SemanticTypeName>() }, 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<MemberName>())!;
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)!;
}
}

/// <summary>
/// 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.
Expand Down
66 changes: 56 additions & 10 deletions Schema/Generation/CSharpCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -754,26 +754,72 @@ private static string ObjectInitialiserFor(Models.Types.Object objectType) =>
/// <summary>
/// Gets the initialiser for a member's default, or null when it has none that fits.
/// </summary>
private static string? DefaultInitialiserFor(SchemaMember member) =>
member.DefaultValue is { } value && DefaultLiteralFor(value, member.Type) is string literal
? $" = {literal};"
: null;

/// <summary>
/// Writes a default as an expression of the given type, or null when it has none that fits.
/// </summary>
/// <remarks>
/// 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.
/// <para>
/// 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.
/// </para>
/// </remarks>
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,
};

/// <summary>
/// Writes a vector or colour default: one number, in every component.
/// </summary>
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))})";
}

/// <summary>
/// Names one of the two values a floating-point number can hold that has no literal.
/// </summary>
Expand Down
Loading