diff --git a/Coder.Test/Languages/CStaticFieldTests.cs b/Coder.Test/Languages/CStaticFieldTests.cs
new file mode 100644
index 0000000..979d466
--- /dev/null
+++ b/Coder.Test/Languages/CStaticFieldTests.cs
@@ -0,0 +1,124 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Coder.Test.Languages;
+
+using ktsu.Coder.Ast;
+using ktsu.Coder.Languages;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+///
+/// Tests that a static field of a type is written beside the C struct rather than inside it.
+///
+///
+/// C has no static data member. Written inside the struct, a static field became one copy per
+/// instance, lost its initial value and changed the struct's size, and a static constant table became
+/// a flexible array member that does not compile anywhere but last (issue #167).
+///
+[TestClass]
+public class CStaticFieldTests
+{
+ private static readonly string[] Compilers = ["cc", "gcc", "clang"];
+
+ private const string Driver = """
+ #include
+ #include "table.h"
+
+ int main(void)
+ {
+ Table one = { 4 };
+ Table_instances += 1;
+ printf("%d %d %d %d %d\n", Table_primes[0], Table_primes[1], Table_instances, one.count, (int)(sizeof(Table) == sizeof(int)));
+ return 0;
+ }
+
+ """;
+
+ ///
+ /// Tests that the static fields leave the struct body and are declared at file scope as
+ /// Type_member, and that the instance field stays a member.
+ ///
+ [TestMethod]
+ public void StaticFields_AreDeclaredBesideTheStruct()
+ {
+ string code = new CGenerator().Generate(Table());
+
+ StringAssert.Contains(code, "static const int Table_primes[] = { 2, 3 };", StringComparison.Ordinal);
+ StringAssert.Contains(code, "static int Table_instances = 0;", StringComparison.Ordinal);
+
+ string body = code[code.IndexOf("typedef struct Table", StringComparison.Ordinal)..code.IndexOf("} Table;", StringComparison.Ordinal)];
+ StringAssert.Contains(body, "int count;", StringComparison.Ordinal);
+ Assert.DoesNotContain("primes", body, StringComparison.Ordinal, code);
+ Assert.DoesNotContain("instances", body, StringComparison.Ordinal, code);
+ }
+
+ ///
+ /// Tests that an instance constant keeps its member and note, since only a static field leaves.
+ ///
+ [TestMethod]
+ public void AnInstanceConstant_StaysAMember()
+ {
+ ClassDeclaration type = new("Limits");
+ type.Members.Add(new FieldDeclaration("max", new TypeReference("int")) { IsConstant = true, InitialValue = new LiteralExpression(9) });
+
+ string code = new CGenerator().Generate(type);
+
+ StringAssert.Contains(code, "int max; // constant, 9", StringComparison.Ordinal);
+ Assert.DoesNotContain("Limits_max", code, StringComparison.Ordinal, code);
+ }
+
+ ///
+ /// Tests that a type holding a static constant table before an instance field compiles as strict
+ /// C, and that the statics keep their values and stay out of the struct's size.
+ ///
+ [TestMethod]
+ public void StaticFields_CompileAndKeepTheirValues()
+ {
+ 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 =>
+ {
+ string source = new CGenerator().Generate(Table());
+ File.WriteAllText(Path.Combine(directory, "table.h"), source);
+ File.WriteAllText(Path.Combine(directory, "driver.c"), Driver);
+
+ (int exitCode, string output) = ToolchainHarness.Run(
+ compiler,
+ "-std=c11 -Wall -Wextra -pedantic-errors -Wno-unused-variable driver.c -o table",
+ directory);
+ Assert.AreEqual(0, exitCode, $"{compiler} rejected the generated source:{Environment.NewLine}{source}{Environment.NewLine}{output}");
+
+ (int ran, string printed) = ToolchainHarness.Run(Path.Combine(directory, "table"), string.Empty, directory);
+ Assert.AreEqual(0, ran, printed);
+ Assert.AreEqual("2 3 1 4 1", printed.Trim(), source);
+ });
+ }
+
+ ///
+ /// The issue's reproduction: a static constant table, a static counter and an instance field.
+ ///
+ private static SourceFile Table()
+ {
+ ClassDeclaration table = new("Table");
+
+ ConstructionExpression primes = new();
+ primes.Arguments.Add(new LiteralExpression(2));
+ primes.Arguments.Add(new LiteralExpression(3));
+ table.Members.Add(new FieldDeclaration("primes", new TypeReference("int") { IsArray = true })
+ {
+ IsStatic = true,
+ IsConstant = true,
+ InitialValue = primes,
+ });
+ table.Members.Add(new FieldDeclaration("instances", new TypeReference("int")) { IsStatic = true, InitialValue = new LiteralExpression(0) });
+ table.Members.Add(new FieldDeclaration("count", new TypeReference("int")));
+
+ SourceFile file = new("table") { IsHeader = true };
+ file.Members.Add(table);
+ return file;
+ }
+}
diff --git a/Coder/Languages/CGenerator.cs b/Coder/Languages/CGenerator.cs
index d23c2fa..5cab3b8 100644
--- a/Coder/Languages/CGenerator.cs
+++ b/Coder/Languages/CGenerator.cs
@@ -605,10 +605,13 @@ protected override void GenerateClassDeclaration(ClassDeclaration classDecl, Cod
List functions = isInterface
? []
: [.. classDecl.Members.OfType()];
+ List staticFields = [.. classDecl.Members.OfType().Where(field => field.IsStatic)];
List fields =
[
.. classDecl.Members.Where(member =>
- !IsTypeDeclaration(member) && (isInterface || member is not FunctionDeclaration)),
+ !IsTypeDeclaration(member)
+ && (isInterface || member is not FunctionDeclaration)
+ && member is not FieldDeclaration { IsStatic: true }),
];
foreach (AstNode nested in nestedTypes)
@@ -666,6 +669,16 @@ .. classDecl.Members.Where(member =>
code.Outdent();
code.WriteLine($"}} {name};");
+ if (staticFields.Count > 0)
+ {
+ code.NewLine();
+ }
+
+ foreach (FieldDeclaration field in staticFields)
+ {
+ WriteFileScopeField(field, $"{name}_{field.Name ?? "Unnamed"}", code);
+ }
+
ClassDeclaration? outerConstructed = constructedType;
constructedType = classDecl;
try
@@ -1037,6 +1050,24 @@ protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlo
return;
}
+ WriteFileScopeField(field, field.Name ?? string.Empty, code);
+ }
+
+ ///
+ /// Writes a field as a declaration at file scope.
+ ///
+ /// The field to write.
+ /// The name to declare it with.
+ /// The writer to emit into.
+ ///
+ /// Where a namespace's fields go, and also where a type's static ones go: C has no static data
+ /// member, so a field that belongs to the type rather than to an instance is an object beside the
+ /// struct, named Type_member the way the type's functions are. Inside the struct it would
+ /// be one copy per instance, lose its initial value and change the struct's size — and a constant
+ /// table there is a flexible array member, which does not compile anywhere but last.
+ ///
+ private void WriteFileScopeField(FieldDeclaration field, string name, CodeBlocker code)
+ {
WritePreamble(field, code, PreambleSite.Field);
TypeReference type = field.Type ?? new TypeReference(UnknownTypeName);
@@ -1058,7 +1089,7 @@ protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlo
code.Write("static ");
}
- code.Write(SpellDeclarator(type, field.Name ?? string.Empty));
+ code.Write(SpellDeclarator(type, name));
if (field.InitialValue is not null)
{