From 639575a8c24a07b7f28b00ae126b348e6d14109a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 02:31:34 +0000 Subject: [PATCH] fix: generate an empty enum that compiles in C and Rust [patch] ISO C has no empty enumerator list, so typedef enum Empty { } Empty; failed with "empty enum is invalid". An enum with no members is now a typedef of its underlying type, or of int, with a note saying why. rustc rejects #[repr] on a zero-variant enum (E0084), so the Rust generator leaves it off when there are no members. Fixes #190 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X8BPy6trRZiUxiwJcbUGzR --- .../Languages/CGeneratedSourceCompilesTests.cs | 2 ++ Coder.Test/Languages/CGeneratorTests.cs | 15 +++++++++++++++ .../Languages/RustGeneratedSourceCompilesTests.cs | 1 + Coder.Test/Languages/RustGeneratorTests.cs | 13 +++++++++++++ Coder/Languages/CGenerator.cs | 12 ++++++++++++ Coder/Languages/RustGenerator.cs | 5 +++-- 6 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs index 10fb2cc..7bf8267 100644 --- a/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs @@ -203,6 +203,8 @@ private static SourceFile Exemplar() file.Members.Add(CompiledExemplar.Shape()); file.Members.Add(CompiledExemplar.OriginAlias()); file.Members.Add(CompiledExemplar.OriginTable()); + file.Members.Add(new EnumDeclaration("Empty")); + file.Members.Add(new EnumDeclaration("EmptyFlags") { UnderlyingType = "long" }); file.Members.Add(new CompileTimeAssertion { Condition = "sizeof(Point) == 2 * sizeof(int)", diff --git a/Coder.Test/Languages/CGeneratorTests.cs b/Coder.Test/Languages/CGeneratorTests.cs index fe6957d..c2bc2f9 100644 --- a/Coder.Test/Languages/CGeneratorTests.cs +++ b/Coder.Test/Languages/CGeneratorTests.cs @@ -404,6 +404,21 @@ public void Enum_UnderlyingType_IsANote() Assert.IsFalse(code.Contains("enum Colour : int", StringComparison.Ordinal)); } + /// + /// Tests that an enumeration with no members is a typedef of the type that would hold one, since + /// ISO C has no empty enumerator list. + /// + [TestMethod] + public void Enum_WithNoMembers_IsATypedefOfItsType() + { + string bare = Generator.Generate(new EnumDeclaration("Empty")); + string typed = Generator.Generate(new EnumDeclaration("Flags") { UnderlyingType = "long" }); + + StringAssert.EndsWith(bare, $"typedef int Empty;{NewLine}", StringComparison.Ordinal); + StringAssert.EndsWith(typed, $"typedef long long Flags;{NewLine}", StringComparison.Ordinal); + Assert.IsFalse(bare.Contains("typedef enum", StringComparison.Ordinal)); + } + /// /// Tests that a constant is static const. A file-scope const in C has external /// linkage, so a header declaring one and included twice is the same object defined twice. diff --git a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs index ddc0fef..0a2ffbf 100644 --- a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs @@ -298,6 +298,7 @@ private static SourceFile Exemplar() geometry.Members.Add(CompiledExemplar.Measure()); geometry.Members.Add(Boxed()); geometry.Members.Add(Clamp()); + geometry.Members.Add(new EnumDeclaration("Empty") { UnderlyingType = "int" }); geometry.Members.Add(new CompileTimeAssertion { Condition = "std::mem::size_of::() == 4", diff --git a/Coder.Test/Languages/RustGeneratorTests.cs b/Coder.Test/Languages/RustGeneratorTests.cs index c7d2dc6..167f316 100644 --- a/Coder.Test/Languages/RustGeneratorTests.cs +++ b/Coder.Test/Languages/RustGeneratorTests.cs @@ -562,6 +562,19 @@ public void Enum_IsScopedAndCanFixItsRepresentation() code); } + /// + /// Tests that an enumeration with no variants leaves off #[repr], which rustc rejects on + /// one (E0084). + /// + [TestMethod] + public void Enum_WithNoVariants_HasNoRepr() + { + string code = Generator.Generate(new EnumDeclaration("Empty") { UnderlyingType = "int" }); + + Assert.IsFalse(code.Contains("#[repr", StringComparison.Ordinal)); + StringAssert.StartsWith(code, "pub enum Empty {", StringComparison.Ordinal); + } + /// /// Tests that a constant is a const and an ordinary field a static, and that an /// array one is a borrowed slice — the bound-free spelling Rust does have, which is what a table diff --git a/Coder/Languages/CGenerator.cs b/Coder/Languages/CGenerator.cs index a0f6dcc..f09c105 100644 --- a/Coder/Languages/CGenerator.cs +++ b/Coder/Languages/CGenerator.cs @@ -878,6 +878,10 @@ protected override void WriteListValue(AstNode value, CodeBlocker code) /// that needs the guarantee can assert it with a , which is a /// thing the AST can already say. /// + /// + /// An enumeration with no members is a typedef of its underlying type, or of int, since + /// ISO C has no empty enumerator list and typedef enum Name { } Name; does not compile. + /// /// protected override void GenerateEnumDeclaration(EnumDeclaration enumDecl, CodeBlocker code) { @@ -886,6 +890,14 @@ protected override void GenerateEnumDeclaration(EnumDeclaration enumDecl, CodeBl string name = enumDecl.Name ?? "UnnamedEnum"; + if (enumDecl.Members.Count == 0) + { + WriteInexpressible(code, $"enum {name} has no members: C has no empty enum, so it is the type that would hold one"); + WritePreamble(enumDecl, code, PreambleSite.Enum); + code.WriteLine($"typedef {(enumDecl.UnderlyingType is TypeReference type ? MapToCType(type) : "int")} {name};"); + return; + } + // Above the documentation rather than below it, so the comment block in front of the // declaration stays one block rather than the note splitting it in two. if (enumDecl.UnderlyingType is TypeReference underlying) diff --git a/Coder/Languages/RustGenerator.cs b/Coder/Languages/RustGenerator.cs index b1fb54d..5229f71 100644 --- a/Coder/Languages/RustGenerator.cs +++ b/Coder/Languages/RustGenerator.cs @@ -1335,7 +1335,8 @@ private static string SpellStorageType(TypeReference type) /// An enumeration's variants are named through it — Colour::Red — so nothing has to be /// prefixed to keep two enumerations with a None each apart, which is the thing C cannot /// do. A fixed underlying type is #[repr], which is the guarantee rather than a comment - /// about one. + /// about one. An enumeration with no variants has no representation to fix, and rustc rejects + /// #[repr] on one (E0084), so it is left off. /// protected override void GenerateEnumDeclaration(EnumDeclaration enumDecl, CodeBlocker code) { @@ -1344,7 +1345,7 @@ protected override void GenerateEnumDeclaration(EnumDeclaration enumDecl, CodeBl WritePreamble(enumDecl, code, PreambleSite.Enum); - if (enumDecl.UnderlyingType is TypeReference underlying) + if (enumDecl.UnderlyingType is TypeReference underlying && enumDecl.Members.Count > 0) { code.WriteLine($"#[repr({SpellType(underlying)})]"); }