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
68 changes: 68 additions & 0 deletions Coder.Test/Languages/CGeneratedSourceCompilesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,74 @@ public void ACallWithAReceiver_ReachesTheFunctionItLowersTo()
});
}

/// <summary>
/// The consumer of the empty-type header, which builds the one with a constructor.
/// </summary>
private const string EmptyDriver = """
#include "empty.h"

int main(void)
{
Empty built = Empty_create();
(void)built;
return MathX_twice(0);
}

""";

/// <summary>
/// Tests that a type with no data and a type with only static functions compile as strict ISO C,
/// which has no empty struct.
/// </summary>
[TestMethod]
public void TypesWithNoData_CompileAsStrictC()
{
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 =>
{
File.WriteAllText(
Path.Combine(directory, "empty.h"),
new CGenerator().Generate(EmptyExemplar()));
File.WriteAllText(Path.Combine(directory, "driver.c"), EmptyDriver);

(int exitCode, string output) = ToolchainHarness.Run(
compiler,
"-std=c11 -Wall -Wextra -pedantic-errors -c driver.c -o driver.o",
directory);

Assert.AreEqual(0, exitCode, $"{compiler} rejected the generated types:{Environment.NewLine}{output}");
});
}

/// <summary>
/// Builds a header holding a type with no members but a constructor, and a type with only a
/// static function.
/// </summary>
/// <returns>The file to generate.</returns>
private static SourceFile EmptyExemplar()
{
SourceFile file = new("empty") { IsHeader = true };

ClassDeclaration empty = new("Empty");
empty.Members.Add(new FunctionDeclaration("Empty") { Kind = FunctionKind.Constructor });
file.Members.Add(empty);

ClassDeclaration mathX = new("MathX");
FunctionDeclaration twice = new("twice") { IsStatic = true, ReturnType = "int" };
twice.Parameters.Add(new Parameter("value", "int"));
twice.Body.Add(new ReturnStatement(new VariableReference("value")));
mathX.Members.Add(twice);
file.Members.Add(mathX);

return file;
}

/// <summary>
/// Builds a header whose one static member calls a member function on a local instance, both as
/// a statement and for its value.
Expand Down
19 changes: 19 additions & 0 deletions Coder.Test/Languages/CGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,25 @@
code);
}

/// <summary>
/// Tests that a type with no data still declares one member, because ISO C forbids an empty
/// struct, while a type with a field or a base gets no placeholder.
/// </summary>
[TestMethod]
public void Struct_WithNoData_GetsAPlaceholderMember()
{
ClassDeclaration mathX = new("MathX");
mathX.Members.Add(new FunctionDeclaration("twice") { IsStatic = true, ReturnType = "int" });

string empty = Generator.Generate(new ClassDeclaration("Empty"));
string staticOnly = Generator.Generate(mathX);
string derived = Generator.Generate(new ClassDeclaration("Circle") { BaseType = "Shape" });

StringAssert.Contains(empty, $"{{{NewLine} // Empty holds no data: C has no empty struct, so this member stands in{NewLine} char unused_;{NewLine}}} Empty;", StringComparison.Ordinal);

Check warning on line 357 in Coder.Test/Languages/CGeneratorTests.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=AaEekdb42tSR37GcgB9a&open=AaEekdb42tSR37GcgB9a&pullRequest=197
StringAssert.Contains(staticOnly, " char unused_;", StringComparison.Ordinal);

Check warning on line 358 in Coder.Test/Languages/CGeneratorTests.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=AaEekdb42tSR37GcgB9b&open=AaEekdb42tSR37GcgB9b&pullRequest=197
Assert.IsFalse(derived.Contains("unused_", StringComparison.Ordinal), derived);
}

/// <summary>
/// Tests that a member's initial value becomes a note. C has no default member initialisers, and
/// whoever writes the initialiser for the struct is the one who needs to know what it was.
Expand Down
16 changes: 16 additions & 0 deletions Coder/Languages/CGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
/// </remarks>
private const string BaseMemberName = "base";

/// <summary>
/// The name of the member that stands in for a type with no data, which C cannot declare.
/// </summary>
private const string UnusedMemberName = "unused_";

/// <summary>
/// What an embedded interface is called as a member.
/// </summary>
Expand Down Expand Up @@ -166,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.

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.

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.

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.

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.

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.

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.

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 @@ -541,6 +546,11 @@
/// does take internal linkage; that is <see cref="GenerateFunction"/>'s business rather than this
/// method's.
/// </para>
/// <para>
/// A type with no data at all — no fields, no base and no interface, only functions or nothing —
/// still gets one placeholder member. ISO C forbids a struct with no members, and MSVC rejects one
/// outright; the placeholder also gives a constructor's <c>{0}</c> something to zero.
/// </para>
/// </remarks>
protected override void GenerateClassDeclaration(ClassDeclaration classDecl, CodeBlocker code)
{
Expand Down Expand Up @@ -587,6 +597,12 @@

WriteEmbeddedBases(classDecl, fields.Count, code);

if (fields.Count == 0 && classDecl.BaseType is null && classDecl.Interfaces.Count == 0)
{
WriteInexpressible(code, $"{name} holds no data: C has no empty struct, so this member stands in");
code.WriteLine($"char {UnusedMemberName};");
}

AstNode? previous = null;
foreach (AstNode member in fields)
{
Expand Down
Loading