From aee9983adcea446ca259b21099bfe00a320d9d6f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 18:27:48 +0000 Subject: [PATCH] Spell a mutable static C++ field inline static inside a type [patch] Every field is written with an initialiser, and ISO C++ forbids one on a non-const static data member unless it is inline, so a class with a mutable static field never compiled. Fixes #80. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012B4b6sZaZ7tFJzNvax1B89 --- .../CppGeneratedSourceCompilesTests.cs | 40 +++++++++++++++++++ Coder/Languages/CppGenerator.cs | 13 ++++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs index 3de0193..95daef9 100644 --- a/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs @@ -88,6 +88,46 @@ public void AnArrayDeclarationCompilesWhereverAnUnboundedOneMayStand() }); } + /// + /// Tests that a class's mutable static fields compile, with and without an initialiser. + /// + /// + /// ISO C++ forbids an in-class initialiser on a non-const static data member, and the generator + /// writes one on every field — the value it was given, or {} when it was given none. Only + /// inline static lets both stand inside the class. + /// + [TestMethod] + public void AMutableStaticFieldCompilesInsideAClass() + { + string? compiler = ToolchainHarness.FindOnPath("--version", Compilers); + if (compiler is null) + { + Assert.Inconclusive("No C++ compiler on the path, so nothing was compiled."); + return; + } + + SourceFile file = new("statics") { IsHeader = true }; + ClassDeclaration config = new("Cfg") { Kind = TypeDeclarationKind.Struct }; + config.Members.Add(new FieldDeclaration("count", "int") { IsStatic = true, InitialValue = new LiteralExpression(0) }); + config.Members.Add(new FieldDeclaration("limit", "int") { IsStatic = true }); + file.Members.Add(config); + + ToolchainHarness.InTemporaryDirectory(directory => + { + File.WriteAllText(Path.Combine(directory, "statics.h"), new CppGenerator().Generate(file)); + File.WriteAllText( + Path.Combine(directory, "driver.cpp"), + "#include \"statics.h\"\n\nint main()\n{\n\tCfg::count = Cfg::limit + 1;\n\treturn Cfg::count * 0;\n}\n"); + + (int exitCode, string output) = ToolchainHarness.Run( + compiler, + "-std=c++20 -Wall -Wextra -pedantic -c driver.cpp -o driver.o", + directory); + + Assert.AreEqual(0, exitCode, $"{compiler} rejected the generated header:{Environment.NewLine}{output}"); + }); + } + /// /// Builds a header declaring an array in each position one with no bound may stand in. /// diff --git a/Coder/Languages/CppGenerator.cs b/Coder/Languages/CppGenerator.cs index 1fb8d7e..21b4d98 100644 --- a/Coder/Languages/CppGenerator.cs +++ b/Coder/Languages/CppGenerator.cs @@ -37,8 +37,8 @@ public class CppGenerator : CFamilyGenerator /// /// /// A depth rather than a flag, so a type declared inside a type leaves the count right when it - /// closes. The one thing it decides is whether a constant field says inline or - /// static; see . + /// closes. What it decides is how a constant or static field spells its storage; see + /// . /// private int insideType; @@ -794,7 +794,14 @@ private string SpellStorage(FieldDeclaration field) return insideType > 0 ? "static constexpr " : "inline constexpr "; } - return field.IsStatic ? "static " : string.Empty; + // A non-const static data member may only carry an in-class initialiser when it is inline, + // and every field is written with one, so inside a type the plain keyword never compiles. + if (field.IsStatic) + { + return insideType > 0 ? "inline static " : "static "; + } + + return string.Empty; } ///