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