Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions Coder.Test/Languages/CStaticFieldTests.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Tests that a static field of a type is written beside the C struct rather than inside it.
/// </summary>
/// <remarks>
/// 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).
/// </remarks>
[TestClass]
public class CStaticFieldTests
{
private static readonly string[] Compilers = ["cc", "gcc", "clang"];

private const string Driver = """
#include <stdio.h>
#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;
}

""";

/// <summary>
/// Tests that the static fields leave the struct body and are declared at file scope as
/// <c>Type_member</c>, and that the instance field stays a member.
/// </summary>
[TestMethod]
public void StaticFields_AreDeclaredBesideTheStruct()
{
string code = new CGenerator().Generate(Table());

StringAssert.Contains(code, "static const int Table_primes[] = { 2, 3 };", StringComparison.Ordinal);

Check warning on line 45 in Coder.Test/Languages/CStaticFieldTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaE4UCJiBvN0c-0-0QVc&open=AaE4UCJiBvN0c-0-0QVc&pullRequest=224
StringAssert.Contains(code, "static int Table_instances = 0;", StringComparison.Ordinal);

Check warning on line 46 in Coder.Test/Languages/CStaticFieldTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaE4UCJiBvN0c-0-0QVd&open=AaE4UCJiBvN0c-0-0QVd&pullRequest=224

string body = code[code.IndexOf("typedef struct Table", StringComparison.Ordinal)..code.IndexOf("} Table;", StringComparison.Ordinal)];
StringAssert.Contains(body, "int count;", StringComparison.Ordinal);

Check warning on line 49 in Coder.Test/Languages/CStaticFieldTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaE4UCJiBvN0c-0-0QVe&open=AaE4UCJiBvN0c-0-0QVe&pullRequest=224
Assert.DoesNotContain("primes", body, StringComparison.Ordinal, code);
Assert.DoesNotContain("instances", body, StringComparison.Ordinal, code);
}

/// <summary>
/// Tests that an instance constant keeps its member and note, since only a static field leaves.
/// </summary>
[TestMethod]
public void AnInstanceConstant_StaysAMember()
{
ClassDeclaration type = new("Limits");
type.Members.Add(new FieldDeclaration("max", new TypeReference("int")) { IsConstant = true, InitialValue = new LiteralExpression<int>(9) });

string code = new CGenerator().Generate(type);

StringAssert.Contains(code, "int max; // constant, 9", StringComparison.Ordinal);

Check warning on line 65 in Coder.Test/Languages/CStaticFieldTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaE4UCJiBvN0c-0-0QVb&open=AaE4UCJiBvN0c-0-0QVb&pullRequest=224
Assert.DoesNotContain("Limits_max", code, StringComparison.Ordinal, code);
}

/// <summary>
/// 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.
/// </summary>
[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);
});
}

/// <summary>
/// The issue's reproduction: a static constant table, a static counter and an instance field.
/// </summary>
private static SourceFile Table()
{
ClassDeclaration table = new("Table");

ConstructionExpression primes = new();
primes.Arguments.Add(new LiteralExpression<int>(2));
primes.Arguments.Add(new LiteralExpression<int>(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<int>(0) });
table.Members.Add(new FieldDeclaration("count", new TypeReference("int")));

SourceFile file = new("table") { IsHeader = true };
file.Members.Add(table);
return file;
}
}
35 changes: 33 additions & 2 deletions Coder/Languages/CGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@
/// The <c>&lt;math.h&gt;</c> macros, which is why a file holding one includes it.
/// </remarks>
protected override string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "NAN" : value > 0 ? "INFINITY" : "-INFINITY";

Check warning on line 174 in Coder/Languages/CGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <inheritdoc/>
protected override IEnumerable<string> RequiredImports(SourceFile file)
Expand Down Expand Up @@ -584,7 +584,7 @@
/// outright; the placeholder also gives a constructor's <c>{0}</c> something to zero.
/// </para>
/// </remarks>
protected override void GenerateClassDeclaration(ClassDeclaration classDecl, CodeBlocker code)

Check warning on line 587 in Coder/Languages/CGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check failure on line 587 in Coder/Languages/CGenerator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaE4UCFcBvN0c-0-0QVa&open=AaE4UCFcBvN0c-0-0QVa&pullRequest=224
{
Ensure.NotNull(classDecl);
Ensure.NotNull(code);
Expand All @@ -605,10 +605,13 @@
List<FunctionDeclaration> functions = isInterface
? []
: [.. classDecl.Members.OfType<FunctionDeclaration>()];
List<FieldDeclaration> staticFields = [.. classDecl.Members.OfType<FieldDeclaration>().Where(field => field.IsStatic)];
List<AstNode> 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)
Expand Down Expand Up @@ -666,6 +669,16 @@
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
Expand Down Expand Up @@ -1037,6 +1050,24 @@
return;
}

WriteFileScopeField(field, field.Name ?? string.Empty, code);
}

/// <summary>
/// Writes a field as a declaration at file scope.
/// </summary>
/// <param name="field">The field to write.</param>
/// <param name="name">The name to declare it with.</param>
/// <param name="code">The writer to emit into.</param>
/// <remarks>
/// 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 <c>Type_member</c> 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.
/// </remarks>
private void WriteFileScopeField(FieldDeclaration field, string name, CodeBlocker code)
{
WritePreamble(field, code, PreambleSite.Field);

TypeReference type = field.Type ?? new TypeReference(UnknownTypeName);
Expand All @@ -1058,7 +1089,7 @@
code.Write("static ");
}

code.Write(SpellDeclarator(type, field.Name ?? string.Empty));
code.Write(SpellDeclarator(type, name));

if (field.InitialValue is not null)
{
Expand Down
Loading