diff --git a/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs index 10fb2cc..4193f2e 100644 --- a/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs @@ -152,6 +152,74 @@ public void ACallWithAReceiver_ReachesTheFunctionItLowersTo() }); } + /// + /// The consumer of the empty-type header, which builds the one with a constructor. + /// + private const string EmptyDriver = """ + #include "empty.h" + + int main(void) + { + Empty built = Empty_create(); + (void)built; + return MathX_twice(0); + } + + """; + + /// + /// Tests that a type with no data and a type with only static functions compile as strict ISO C, + /// which has no empty struct. + /// + [TestMethod] + public void TypesWithNoData_CompileAsStrictC() + { + string? compiler = ToolchainHarness.FindOnPath("--version", Compilers); + if (compiler is null) + { + Assert.Inconclusive("No C compiler on the path, so nothing was compiled."); + return; + } + + ToolchainHarness.InTemporaryDirectory(directory => + { + File.WriteAllText( + Path.Combine(directory, "empty.h"), + new CGenerator().Generate(EmptyExemplar())); + File.WriteAllText(Path.Combine(directory, "driver.c"), EmptyDriver); + + (int exitCode, string output) = ToolchainHarness.Run( + compiler, + "-std=c11 -Wall -Wextra -pedantic-errors -c driver.c -o driver.o", + directory); + + Assert.AreEqual(0, exitCode, $"{compiler} rejected the generated types:{Environment.NewLine}{output}"); + }); + } + + /// + /// Builds a header holding a type with no members but a constructor, and a type with only a + /// static function. + /// + /// The file to generate. + private static SourceFile EmptyExemplar() + { + SourceFile file = new("empty") { IsHeader = true }; + + ClassDeclaration empty = new("Empty"); + empty.Members.Add(new FunctionDeclaration("Empty") { Kind = FunctionKind.Constructor }); + file.Members.Add(empty); + + ClassDeclaration mathX = new("MathX"); + FunctionDeclaration twice = new("twice") { IsStatic = true, ReturnType = "int" }; + twice.Parameters.Add(new Parameter("value", "int")); + twice.Body.Add(new ReturnStatement(new VariableReference("value"))); + mathX.Members.Add(twice); + file.Members.Add(mathX); + + return file; + } + /// /// Builds a header whose one static member calls a member function on a local instance, both as /// a statement and for its value. diff --git a/Coder.Test/Languages/CGeneratorTests.cs b/Coder.Test/Languages/CGeneratorTests.cs index fe6957d..6aadd66 100644 --- a/Coder.Test/Languages/CGeneratorTests.cs +++ b/Coder.Test/Languages/CGeneratorTests.cs @@ -340,6 +340,25 @@ public void Struct_IsTypedefedAndKeepsItsTag() code); } + /// + /// Tests that a type with no data still declares one member, because ISO C forbids an empty + /// struct, while a type with a field or a base gets no placeholder. + /// + [TestMethod] + public void Struct_WithNoData_GetsAPlaceholderMember() + { + ClassDeclaration mathX = new("MathX"); + mathX.Members.Add(new FunctionDeclaration("twice") { IsStatic = true, ReturnType = "int" }); + + string empty = Generator.Generate(new ClassDeclaration("Empty")); + string staticOnly = Generator.Generate(mathX); + string derived = Generator.Generate(new ClassDeclaration("Circle") { BaseType = "Shape" }); + + StringAssert.Contains(empty, $"{{{NewLine} // Empty holds no data: C has no empty struct, so this member stands in{NewLine} char unused_;{NewLine}}} Empty;", StringComparison.Ordinal); + StringAssert.Contains(staticOnly, " char unused_;", StringComparison.Ordinal); + Assert.IsFalse(derived.Contains("unused_", StringComparison.Ordinal), derived); + } + /// /// Tests that a member's initial value becomes a note. C has no default member initialisers, and /// whoever writes the initialiser for the struct is the one who needs to know what it was. diff --git a/Coder/Languages/CGenerator.cs b/Coder/Languages/CGenerator.cs index a0f6dcc..858d55b 100644 --- a/Coder/Languages/CGenerator.cs +++ b/Coder/Languages/CGenerator.cs @@ -65,6 +65,11 @@ public class CGenerator : CFamilyGenerator /// private const string BaseMemberName = "base"; + /// + /// The name of the member that stands in for a type with no data, which C cannot declare. + /// + private const string UnusedMemberName = "unused_"; + /// /// What an embedded interface is called as a member. /// @@ -541,6 +546,11 @@ protected override void GenerateNamespaceDeclaration(NamespaceDeclaration namesp /// does take internal linkage; that is 's business rather than this /// method's. /// + /// + /// A type with no data at all — no fields, no base and no interface, only functions or nothing — + /// still gets one placeholder member. ISO C forbids a struct with no members, and MSVC rejects one + /// outright; the placeholder also gives a constructor's {0} something to zero. + /// /// protected override void GenerateClassDeclaration(ClassDeclaration classDecl, CodeBlocker code) { @@ -587,6 +597,12 @@ .. classDecl.Members.Where(member => WriteEmbeddedBases(classDecl, fields.Count, code); + if (fields.Count == 0 && classDecl.BaseType is null && classDecl.Interfaces.Count == 0) + { + WriteInexpressible(code, $"{name} holds no data: C has no empty struct, so this member stands in"); + code.WriteLine($"char {UnusedMemberName};"); + } + AstNode? previous = null; foreach (AstNode member in fields) {