diff --git a/CLAUDE.md b/CLAUDE.md index efff4a8..68b25c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,6 +68,11 @@ source in four target languages. The solution uses: one that cannot be assigned at all; a language without an initialiser list assigns at the top of the constructor instead. `ConstructionExpression` is the one expression that needs a type rather than a name, which is why it could not exist before `TypeReference` did. +- `Coder/Ast/CompileTimeAssertion.cs` — what a generated type promises that the type itself cannot + say. Its `Condition` is text for the same reason `SourceFile.Imports` are: a compile-time predicate + is language-specific in a way most of the AST is not, and there is no shared idea underneath + `std::is_trivially_copyable_v` to model. Only C++ has one; the others write a comment, because a + file that quietly loses a guarantee looks like one that still makes it. - `Coder/Languages/LanguageGeneratorBase.cs` — the emitters every generator shares. - `Coder/Languages/StandardLanguageGenerator.cs` — owns the node dispatch, so a derived generator supplies only the syntax its language does not share. `CSharpGenerator` deliberately diff --git a/Coder.Graph/AstFields.cs b/Coder.Graph/AstFields.cs index 149fa56..eb0f0cb 100644 --- a/Coder.Graph/AstFields.cs +++ b/Coder.Graph/AstFields.cs @@ -165,6 +165,12 @@ public static IReadOnlyList Of(AstNode node) new(ValueField, AstFieldKind.Text, enumMember.Value ?? string.Empty), ], + CompileTimeAssertion assertion => + [ + new("Condition", AstFieldKind.Text, assertion.Condition ?? string.Empty), + new("Message", AstFieldKind.Text, assertion.Message ?? string.Empty), + ], + UsingAlias usingAlias => [ new("Name", AstFieldKind.Text, usingAlias.Name ?? string.Empty), @@ -361,6 +367,9 @@ private static bool TryWriteDeclaration(AstNode node, string fieldName, string v (EnumMember enumMember, "Name") => Assign(() => enumMember.Name = OrNull(value)), (EnumMember enumMember, ValueField) => Assign(() => enumMember.Value = OrNull(value)), + (CompileTimeAssertion assertion, "Condition") => Assign(() => assertion.Condition = OrNull(value)), + (CompileTimeAssertion assertion, "Message") => Assign(() => assertion.Message = OrNull(value)), + (UsingAlias usingAlias, "Name") => Assign(() => usingAlias.Name = OrNull(value)), (UsingAlias usingAlias, "AliasedType") => Assign(() => usingAlias.AliasedType = OrNull(value)), (UsingAlias usingAlias, VisibilityField) => diff --git a/Coder.Graph/AstSchema.cs b/Coder.Graph/AstSchema.cs index 4be230c..28f3c3b 100644 --- a/Coder.Graph/AstSchema.cs +++ b/Coder.Graph/AstSchema.cs @@ -434,7 +434,8 @@ public static bool Accepts(AstSlot slot, AstNode candidate) // running at one, so it belongs to a class or to the document rather than inside a body. AstSlotKind.Statement => candidate is not (Parameter or EntryPoint), AstSlotKind.Member => candidate is FunctionDeclaration or VariableDeclaration or FieldDeclaration - or ClassDeclaration or EnumDeclaration or NamespaceDeclaration or UsingAlias or EntryPoint, + or ClassDeclaration or EnumDeclaration or NamespaceDeclaration or UsingAlias + or CompileTimeAssertion or EntryPoint, AstSlotKind.EnumMember => candidate is EnumMember, _ => false, }; diff --git a/Coder.Test/Ast/CompileTimeAssertionTests.cs b/Coder.Test/Ast/CompileTimeAssertionTests.cs new file mode 100644 index 0000000..481cbd6 --- /dev/null +++ b/Coder.Test/Ast/CompileTimeAssertionTests.cs @@ -0,0 +1,153 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Test.Ast; + +using ktsu.Coder.Ast; +using ktsu.Coder.Graph; +using ktsu.Coder.Languages; +using ktsu.Coder.Serialization; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests for : what a generated type promises that the type itself +/// cannot say. +/// +/// +/// Only C++ has anything checked before the program runs, so this is the clearest case of the rule +/// the whole AST follows — say what is true, and let each language say as much of it as it can. The +/// other three write a comment rather than dropping it, because a file that quietly loses a +/// guarantee looks exactly like one that still makes it. +/// +[TestClass] +public class CompileTimeAssertionTests +{ + /// + /// C++ writes the assertion, with the message on its own line. + /// + [TestMethod] + public void Cpp_WritesTheAssertion() + { + CompileTimeAssertion assertion = new( + "std::is_trivially_copyable_v", + "RigidBody must be trivially copyable: it crosses the C# boundary and the wire as bytes"); + + Assert.AreEqual( + "static_assert(std::is_trivially_copyable_v,\n" + + " \"RigidBody must be trivially copyable: it crosses the C# boundary and the wire as bytes\");\n", + new CppGenerator().Generate(assertion).ReplaceLineEndings("\n")); + } + + /// + /// An assertion with nothing to say when it fails is still an assertion. + /// + [TestMethod] + public void Cpp_WritesAnAssertionWithNoMessage() + { + Assert.AreEqual( + "static_assert(sizeof(Handle) == 8);\n", + new CppGenerator().Generate(new CompileTimeAssertion("sizeof(Handle) == 8")).ReplaceLineEndings("\n")); + } + + /// + /// A message with a quote in it is escaped rather than ending the string early. + /// + [TestMethod] + public void Cpp_EscapesTheMessage() + { + CompileTimeAssertion assertion = new("sizeof(T) == 8", "a \"T\" is eight bytes"); + + Assert.Contains("\\\"T\\\"", new CppGenerator().Generate(assertion), StringComparison.Ordinal); + } + + /// + /// The other three have nothing checked before the program runs, so they say what was asserted + /// rather than dropping it. + /// + [TestMethod] + public void OtherLanguages_SayWhatWasAsserted() + { + CompileTimeAssertion assertion = new("std::is_standard_layout_v", "layout must be stable"); + + Assert.Contains( + "// asserted at build time: std::is_standard_layout_v", + new CSharpGenerator().Generate(assertion), + StringComparison.Ordinal); + Assert.Contains( + "# asserted at build time: std::is_standard_layout_v", + new PythonGenerator().Generate(assertion), + StringComparison.Ordinal); + Assert.Contains( + "// asserted at build time: std::is_standard_layout_v", + new JavaScriptGenerator().Generate(assertion), + StringComparison.Ordinal); + } + + /// + /// Assertions about one type stay together, and are separated from the declaration they are about. + /// + /// + /// The same rule the members of a type follow. Two of a kind that say nothing about themselves are + /// one block; a struct followed by an assertion is two. + /// + [TestMethod] + public void Cpp_GroupsAssertionsAboutTheSameType() + { + SourceFile file = new("RigidBody.gen.hpp"); + file.Members.Add(new ClassDeclaration("RigidBody") { Kind = TypeDeclarationKind.Struct }); + file.Members.Add(new CompileTimeAssertion("std::is_trivially_copyable_v", "bytes")); + file.Members.Add(new CompileTimeAssertion("std::is_standard_layout_v", "offsets")); + + string code = new CppGenerator().Generate(file).ReplaceLineEndings("\n"); + + Assert.Contains("};\n\nstatic_assert(", code, StringComparison.Ordinal); + Assert.Contains("\"bytes\");\nstatic_assert(", code, StringComparison.Ordinal); + } + + /// + /// An assertion survives a round trip through YAML, and a clone carries it. + /// + [TestMethod] + public void Yaml_RoundTripsTheAssertion() + { + CompileTimeAssertion original = new("sizeof(Handle) == 8", "a handle is eight bytes"); + + string yaml = new YamlSerializer().Serialize(original); + CompileTimeAssertion restored = (CompileTimeAssertion)new YamlDeserializer().Deserialize(yaml)!; + + Assert.AreEqual("sizeof(Handle) == 8", restored.Condition); + Assert.AreEqual("a handle is eight bytes", restored.Message); + + CompileTimeAssertion clone = (CompileTimeAssertion)original.Clone(); + + Assert.AreEqual(original.Condition, clone.Condition); + Assert.AreEqual(original.Message, clone.Message); + Assert.AreNotSame(original, clone); + } + + /// + /// A document says nothing for an assertion that asserts nothing. + /// + [TestMethod] + public void Yaml_WritesNothingForAnEmptyAssertion() + { + string yaml = new YamlSerializer().Serialize(new CompileTimeAssertion()); + + Assert.DoesNotContain("condition", yaml, StringComparison.Ordinal); + Assert.DoesNotContain("message", yaml, StringComparison.Ordinal); + } + + /// + /// The editor can place one beside a declaration and edit both of its halves. + /// + [TestMethod] + public void Graph_AcceptsAndEditsAnAssertion() + { + NamespaceDeclaration components = new("holo::components"); + CompileTimeAssertion assertion = new("sizeof(Handle) == 8"); + + Assert.IsTrue(AstSchema.TryAttach(components, AstSchema.SlotsOf(components)[0], assertion)); + Assert.IsTrue(AstFields.TryWrite(assertion, "Message", "a handle is eight bytes")); + + Assert.AreEqual("a handle is eight bytes", assertion.Message); + } +} diff --git a/Coder.Test/Languages/ExemplarHeaderTests.cs b/Coder.Test/Languages/ExemplarHeaderTests.cs index 0c04380..8e43f49 100644 --- a/Coder.Test/Languages/ExemplarHeaderTests.cs +++ b/Coder.Test/Languages/ExemplarHeaderTests.cs @@ -81,6 +81,11 @@ enum class BodyKind : std::uint8_t BodyKind body_kind = BodyKind::Dynamic; }; + static_assert(std::is_trivially_copyable_v, + "RigidBody must be trivially copyable: it crosses the C# boundary and the wire as bytes"); + static_assert(std::is_standard_layout_v, + "RigidBody must be standard layout for its field offsets to be stable"); + } // namespace holo::components """; @@ -136,6 +141,13 @@ private static SourceFile RigidBodyHeader() NamespaceDeclaration components = new("holo::components"); components.Members.Add(rigidBody); + components.Members.Add(new CompileTimeAssertion( + "std::is_trivially_copyable_v", + "RigidBody must be trivially copyable: it crosses the C# boundary and the wire as bytes")); + components.Members.Add(new CompileTimeAssertion( + "std::is_standard_layout_v", + "RigidBody must be standard layout for its field offsets to be stable")); + SourceFile file = new("RigidBody.gen.hpp") { IsHeader = true }; file.HeaderComment.Add("Generated by holo_schemac. Do not edit."); file.HeaderComment.Add(""); diff --git a/Coder.Test/Languages/ExemplarSemanticTypeTests.cs b/Coder.Test/Languages/ExemplarSemanticTypeTests.cs index a1865d5..6de81fe 100644 --- a/Coder.Test/Languages/ExemplarSemanticTypeTests.cs +++ b/Coder.Test/Languages/ExemplarSemanticTypeTests.cs @@ -60,6 +60,11 @@ [[nodiscard]] constexpr underlying value() const noexcept private: underlying value_{}; }; + + static_assert(std::is_trivially_copyable_v, + "EntityId must be trivially copyable: it appears in components"); + static_assert(std::is_standard_layout_v, + "EntityId must be standard layout for its field offsets to be stable"); """; /// @@ -114,6 +119,29 @@ private static ClassDeclaration EntityId() return entity; } + /// + /// The semantic type together with what is asserted about it. + /// + /// The file. + /// + /// A file with no banner and no imports, because the document's section is the declaration and + /// the assertions beside it rather than a whole header. What the assertions say is the reason the + /// type can appear in a component at all. + /// + private static SourceFile EntityIdWithAssertions() + { + SourceFile file = new("EntityId.gen.hpp"); + file.Members.Add(EntityId()); + file.Members.Add(new CompileTimeAssertion( + "std::is_trivially_copyable_v", + "EntityId must be trivially copyable: it appears in components")); + file.Members.Add(new CompileTimeAssertion( + "std::is_standard_layout_v", + "EntityId must be standard layout for its field offsets to be stable")); + + return file; + } + /// /// Builds one of the comparison operators, which are symmetric and so belong beside the type /// rather than to either operand. @@ -146,7 +174,7 @@ private static FunctionDeclaration Comparison(string symbol, string returnType) public void Cpp_GeneratesTheSemanticTypeTheDocumentSpecifies() => Assert.AreEqual( Expected.ReplaceLineEndings("\n").TrimEnd(), - new CppGenerator().Generate(EntityId()).ReplaceLineEndings("\n").TrimEnd()); + new CppGenerator().Generate(EntityIdWithAssertions()).ReplaceLineEndings("\n").TrimEnd()); /// /// A member is initialised rather than assigned, which is the only way to start one that cannot be diff --git a/Coder/Ast/CompileTimeAssertion.cs b/Coder/Ast/CompileTimeAssertion.cs new file mode 100644 index 0000000..775a032 --- /dev/null +++ b/Coder/Ast/CompileTimeAssertion.cs @@ -0,0 +1,84 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Ast; + +/// +/// Something that must be true when the program is built rather than when it runs. +/// +/// +/// What a generated type promises is often not something the type itself can say. A struct that +/// crosses a language boundary and the wire as raw bytes has to be trivially copyable and standard +/// layout, and the moment that stops being true is the moment a saved file starts being wrong — so +/// it is asserted where the type is declared, and the build fails rather than the save file. +/// +/// is text, and deliberately. A compile-time predicate is language-specific +/// in a way most of the AST is not: std::is_trivially_copyable_v<T> has no equivalent +/// anywhere else, so there is no shared idea underneath to model. It is carried the same way +/// are, and for the same reason — an assertion, like an import, is +/// written for the language the file is for. +/// +/// +/// Only C++ has this. Every other target here writes a comment saying what was asserted, because a +/// generated file that silently drops a guarantee looks like one that still makes it. +/// +/// +public class CompileTimeAssertion : AstNode +{ + /// + /// Initializes a new instance of the class. + /// + public CompileTimeAssertion() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The predicate that must hold, as the target language writes it. + /// What to say when it does not. + public CompileTimeAssertion(string condition, string? message = null) + { + Condition = condition; + Message = message; + } + + /// + /// Gets or sets the predicate that must hold, as the target language writes it. + /// + public string? Condition { get; set; } + + /// + /// Gets or sets what to say when the predicate does not hold. + /// + /// + /// Worth writing rather than leaving to the compiler: the predicate says what is false and the + /// message says why anyone cared, and only the second tells whoever hits it what to do. + /// + public string? Message { get; set; } + + /// + /// Gets the type name of this node for serialization purposes. + /// + /// The name of the node type. + public override string GetNodeTypeName() => "CompileTimeAssertion"; + + /// + /// Creates a deep clone of this assertion. + /// + /// A new instance with the same properties. + public override AstNode Clone() + { + CompileTimeAssertion clone = new() + { + Condition = Condition, + Message = Message, + }; + + foreach ((string key, object? value) in Metadata) + { + clone.Metadata[key] = value; + } + + return clone; + } +} diff --git a/Coder/Languages/CSharpGenerator.cs b/Coder/Languages/CSharpGenerator.cs index 5c9d3f6..3bce503 100644 --- a/Coder/Languages/CSharpGenerator.cs +++ b/Coder/Languages/CSharpGenerator.cs @@ -61,6 +61,25 @@ protected override void GenerateInternal(AstNode node, CodeBlocker code) case UnaryExpression unaryExpr: GenerateUnaryExpression(unaryExpr, code, GetUnaryOperator(unaryExpr.Operator)); break; + default: + GenerateExpressionOrLeaf(node, code); + break; + } + } + + /// + /// Emits an expression or a leaf. + /// + /// The node to emit. + /// The writer to emit into. + /// + /// Split from the declarations only because one switch over every node the AST has is more + /// branches than the analyzer accepts. The line is the same one the AST already draws. + /// + private void GenerateExpressionOrLeaf(AstNode node, CodeBlocker code) + { + switch (node) + { case VariableReference varRef: code.Write(varRef.Name); break; @@ -82,6 +101,9 @@ protected override void GenerateInternal(AstNode node, CodeBlocker code) case NamespaceDeclaration namespaceDecl: GenerateNamespace(namespaceDecl, code); break; + case CompileTimeAssertion assertion: + WriteInexpressible(code, $"asserted at build time: {assertion.Condition}"); + break; case UsingAlias usingAlias: GenerateUsingAlias(usingAlias, code); break; diff --git a/Coder/Languages/CppGenerator.cs b/Coder/Languages/CppGenerator.cs index f0bbcc3..ace62f0 100644 --- a/Coder/Languages/CppGenerator.cs +++ b/Coder/Languages/CppGenerator.cs @@ -299,15 +299,18 @@ protected override void GenerateNamespaceDeclaration(NamespaceDeclaration namesp code.WriteLine("{"); code.NewLine(); - bool first = true; + // The same rule the members of a type follow: two of a kind that say nothing about themselves + // stay together, so a run of assertions about one type reads as one block rather than as a + // paragraph each. + AstNode? previous = null; foreach (AstNode member in namespaceDecl.Members) { - if (!first) + if (previous is not null && NeedsSeparation(previous, member)) { code.NewLine(); } - first = false; + previous = member; GenerateInternal(member, code); } @@ -404,6 +407,31 @@ protected override void GenerateClassDeclaration(ClassDeclaration classDecl, Cod } } + /// + /// + /// The message goes on its own line. These are long by nature — the predicate says what is false + /// and the message says why anyone cared — and a compiler quoting the whole declaration back is + /// easier to read when it is two lines rather than one very wide one. + /// + protected override void GenerateCompileTimeAssertion(CompileTimeAssertion assertion, CodeBlocker code) + { + Ensure.NotNull(assertion); + Ensure.NotNull(code); + + code.Write($"static_assert({assertion.Condition}"); + + if (assertion.Message is not null) + { + code.WriteLine(","); + code.Indent(); + code.Write($"\"{EscapeString(assertion.Message)}\""); + code.Outdent(); + } + + code.Write(")"); + EndStatement(code); + } + /// protected override void GenerateUsingAlias(UsingAlias usingAlias, CodeBlocker code) { @@ -532,10 +560,19 @@ protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlo /// The member already written. /// The member about to be written. /// True when a blank line belongs between them. - private static bool NeedsSeparation(AstNode previous, AstNode member) => - previous.GetType() != member.GetType() - || IsDocumented(previous) - || IsDocumented(member); + /// + /// Two of a kind that say nothing about themselves stay together, which is what keeps a run of + /// aliases, of defaulted declarations, or of assertions about one type reading as one block. + /// + protected override bool NeedsSeparation(AstNode previous, AstNode member) + { + Ensure.NotNull(previous); + Ensure.NotNull(member); + + return previous.GetType() != member.GetType() + || IsDocumented(previous) + || IsDocumented(member); + } /// /// Reports whether a member carries documentation. diff --git a/Coder/Languages/JavaScriptGenerator.cs b/Coder/Languages/JavaScriptGenerator.cs index 0145088..a57a12d 100644 --- a/Coder/Languages/JavaScriptGenerator.cs +++ b/Coder/Languages/JavaScriptGenerator.cs @@ -82,6 +82,19 @@ private static void WriteEnumMembers(EnumDeclaration enumDecl, CodeBlocker code) } } + /// + /// + /// JavaScript has nothing that is checked before the program runs, so what was asserted is written as + /// a comment. Dropping it would leave a file that looks like one still making the guarantee. + /// + protected override void GenerateCompileTimeAssertion(CompileTimeAssertion assertion, CodeBlocker code) + { + Ensure.NotNull(assertion); + Ensure.NotNull(code); + + WriteInexpressible(code, $"asserted at build time: {assertion.Condition}"); + } + /// /// /// JavaScript has no types to alias. The name is bound to whatever the alias named, which is a diff --git a/Coder/Languages/LanguageGeneratorBase.cs b/Coder/Languages/LanguageGeneratorBase.cs index 12ec77f..6f39a5c 100644 --- a/Coder/Languages/LanguageGeneratorBase.cs +++ b/Coder/Languages/LanguageGeneratorBase.cs @@ -251,19 +251,31 @@ protected void GenerateSourceFile(SourceFile file, CodeBlocker code) code.NewLine(); } - bool first = true; + AstNode? previous = null; foreach (AstNode member in file.Members) { - if (!first) + if (previous is not null && NeedsSeparation(previous, member)) { code.NewLine(); } - first = false; + previous = member; GenerateInternal(member, code); } } + /// + /// Reports whether two adjacent declarations want a blank line between them. + /// + /// The declaration already written. + /// The declaration about to be written. + /// True when a blank line belongs between them. + /// + /// Always, unless a language says otherwise. A language whose declarations are dense enough to + /// want grouping overrides this with the rule it wants. + /// + protected virtual bool NeedsSeparation(AstNode previous, AstNode member) => true; + /// /// Emits a declaration's documentation, one comment per line. /// @@ -394,6 +406,7 @@ protected static bool CanGenerateStandardNodes(AstNode astNode) { // No null check: a type pattern never matches null. return astNode is FunctionDeclaration + or CompileTimeAssertion or UsingAlias or MemberInitialiser or ConstructionExpression diff --git a/Coder/Languages/PythonGenerator.cs b/Coder/Languages/PythonGenerator.cs index 89dd745..29ed7f5 100644 --- a/Coder/Languages/PythonGenerator.cs +++ b/Coder/Languages/PythonGenerator.cs @@ -67,6 +67,19 @@ protected override void EndStatement(CodeBlocker code) /// protected override string? SpellImport(string import) => $"import {import}"; + /// + /// + /// Python has nothing that is checked before the program runs, so what was asserted is written as + /// a comment. Dropping it would leave a file that looks like one still making the guarantee. + /// + protected override void GenerateCompileTimeAssertion(CompileTimeAssertion assertion, CodeBlocker code) + { + Ensure.NotNull(assertion); + Ensure.NotNull(code); + + WriteInexpressible(code, $"asserted at build time: {assertion.Condition}"); + } + /// /// /// An alias is an ordinary assignment in Python, which is what a type alias is there. diff --git a/Coder/Languages/StandardLanguageGenerator.cs b/Coder/Languages/StandardLanguageGenerator.cs index 1ec3aee..9c24c9c 100644 --- a/Coder/Languages/StandardLanguageGenerator.cs +++ b/Coder/Languages/StandardLanguageGenerator.cs @@ -63,6 +63,10 @@ protected sealed override void GenerateInternal(AstNode node, CodeBlocker code) GenerateNamespaceDeclaration(namespaceDecl, code); break; + case CompileTimeAssertion assertion: + GenerateCompileTimeAssertion(assertion, code); + break; + case UsingAlias usingAlias: GenerateUsingAlias(usingAlias, code); break; @@ -160,6 +164,13 @@ protected virtual void GenerateNamespaceDeclaration(NamespaceDeclaration namespa } } + /// + /// Emits something that must be true when the program is built. + /// + /// The assertion to emit. + /// The writer to emit into. + protected abstract void GenerateCompileTimeAssertion(CompileTimeAssertion assertion, CodeBlocker code); + /// /// Emits an alias giving a type a second name. /// diff --git a/Coder/Serialization/YamlDeserializer.cs b/Coder/Serialization/YamlDeserializer.cs index aaf2438..e96e870 100644 --- a/Coder/Serialization/YamlDeserializer.cs +++ b/Coder/Serialization/YamlDeserializer.cs @@ -57,6 +57,8 @@ public YamlDeserializer() "SourceFile" => DeserializeSourceFile(nodeData), "namespaceDeclaration" => DeserializeNamespaceDeclaration(nodeData), "NamespaceDeclaration" => DeserializeNamespaceDeclaration(nodeData), + "compileTimeAssertion" => DeserializeCompileTimeAssertion(nodeData), + "CompileTimeAssertion" => DeserializeCompileTimeAssertion(nodeData), "usingAlias" => DeserializeUsingAlias(nodeData), "UsingAlias" => DeserializeUsingAlias(nodeData), "memberInitialiser" => DeserializeMemberInitialiser(nodeData), @@ -374,6 +376,28 @@ private NamespaceDeclaration DeserializeNamespaceDeclaration(object? nodeData) return namespaceDecl; } + private static CompileTimeAssertion DeserializeCompileTimeAssertion(object? nodeData) + { + CompileTimeAssertion assertion = new(); + if (nodeData is not Dictionary dict) + { + return assertion; + } + + if (dict.TryGetValue("condition", out object? conditionObj)) + { + assertion.Condition = conditionObj?.ToString(); + } + + if (dict.TryGetValue("message", out object? messageObj)) + { + assertion.Message = messageObj?.ToString(); + } + + DeserializeMetadata(assertion, dict); + return assertion; + } + private static UsingAlias DeserializeUsingAlias(object? nodeData) { UsingAlias usingAlias = new(); diff --git a/Coder/Serialization/YamlSerializer.cs b/Coder/Serialization/YamlSerializer.cs index c4964eb..235103c 100644 --- a/Coder/Serialization/YamlSerializer.cs +++ b/Coder/Serialization/YamlSerializer.cs @@ -127,6 +127,9 @@ private static void SerializeOtherNode(AstNode node, Dictionary { switch (node) { + case CompileTimeAssertion assertion: + SerializeCompileTimeAssertion(assertion, nodeData); + break; case UsingAlias usingAlias: SerializeUsingAlias(usingAlias, nodeData); break; @@ -310,6 +313,19 @@ private static void SerializeNamespaceDeclaration(NamespaceDeclaration namespace /// The key a node's members are written under. private const string MembersKey = "members"; + private static void SerializeCompileTimeAssertion(CompileTimeAssertion assertion, Dictionary nodeData) + { + if (assertion.Condition != null) + { + nodeData["condition"] = assertion.Condition; + } + + if (assertion.Message != null) + { + nodeData["message"] = assertion.Message; + } + } + private static void SerializeUsingAlias(UsingAlias usingAlias, Dictionary nodeData) { if (usingAlias.Name != null)