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)
{