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
8 changes: 7 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,13 @@ source in seven target languages. The solution uses:
cannot overload one and so have to call it something.
- `Coder/Languages/CFamilyGenerator.cs` — what C and C++ share beyond what every generator shares,
and all of it is about C: the preprocessor (`#pragma once`, `#include`), the braced list with its
designated initialisers, and the declarator that puts an array's brackets after the name. The type
designated initialisers, and the declarator that puts an array's brackets after the name. Every
position in either language that declares a name goes through that declarator, which is the whole
of why it exists: a type spelled on its own carries no brackets, because the positions that spell
one without a name — a return type, a base type, an enumeration's underlying type — are ones
neither language lets an array stand in at all. `CppGeneratedSourceCompilesTests` is what holds
C++ to it, and is why the rule is checked rather than asserted; before it there was no C++ compile
test and an array-typed parameter came out as `int[] steps`. The type
mappings deliberately stay
with each generator, because `str` is a `std::string` in one language and a `const char*` in the
other and the whole of what a mapping is is the spelling.
Expand Down
159 changes: 159 additions & 0 deletions Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// 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>
/// Compiles what <see cref="CppGenerator"/> writes, with a real C++ compiler.
/// </summary>
/// <remarks>
/// C, Rust and Go were each given a compile test because their rules are not visible in the text.
/// C++ was not, and the gap had a cost: an array-typed parameter came out as <c>int[] steps</c> in
/// every release until this test existed. Every pinned spelling around it passed, because a spelling
/// can be pinned and still be one a compiler refuses.
/// <para>
/// Where an array's brackets go is exactly that kind of rule. C++ has one position for them — after
/// the name being declared — and no position at all in the places that spell a type without a name,
/// so the same <see cref="TypeReference"/> is written two ways depending on where it stands. Only a
/// compiler can say whether the generator picked the right one each time.
/// </para>
/// <para>
/// The test is inconclusive rather than failing where no compiler is on the path, which is the
/// honest result: nothing was checked.
/// </para>
/// </remarks>
[TestClass]
public class CppGeneratedSourceCompilesTests
{
/// <summary>
/// The compilers to look for, in the order a C++ project would.
/// </summary>
private static readonly string[] Compilers = ["c++", "g++", "clang++"];

/// <summary>
/// The consumer of the generated header, which calls each declaration so that a declaration that
/// compiles but cannot be used fails here rather than passing quietly.
/// </summary>
private const string Driver = """
#include "arrays.h"

int main()
{
int steps[3] = { 1, 2, 3 };
return (Table::sum(steps, 3) + STRIDES[1]) * 0;
}

""";

/// <summary>
/// Tests that the two positions an unbounded array may stand in compile.
/// </summary>
/// <remarks>
/// A parameter, and a namespace-scope constant table whose initialiser supplies the bound — which
/// is the pair <see cref="TypeReference.IsArray"/> exists for, an array with no bound having
/// nowhere else to stand: a data member and a local both need a bound C++ can see, and
/// <c>IsArray</c> deliberately carries none.
/// <para>
/// Both are declarators, so both take the brackets after the name. The function's return type is
/// the position with no name to put them after, and the generator has to leave them off there
/// rather than write a type C++ cannot form.
/// </para>
/// </remarks>
[TestMethod]
public void AnArrayDeclarationCompilesWhereverAnUnboundedOneMayStand()
{
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, "arrays.h"),
new CppGenerator().Generate(ArrayExemplar()));
File.WriteAllText(Path.Combine(directory, "driver.cpp"), Driver);

(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}");
});
}

/// <summary>
/// Builds a header declaring an array in each position one with no bound may stand in.
/// </summary>
/// <returns>The file to generate.</returns>
private static SourceFile ArrayExemplar()
{
SourceFile file = new("arrays") { IsHeader = true };
file.HeaderComment.Add("Generated by Coder. Do not edit.");

// A constant table, which is what IsArray was added for. The initialiser is what gives the
// bound C++ needs, which is why this is a position an unbounded array may stand in at all.
ConstructionExpression strides = new(type: null);
strides.Arguments.Add(new LiteralExpression<int>(1));
strides.Arguments.Add(new LiteralExpression<int>(2));

FieldDeclaration table = new()
{
Name = "STRIDES",
Type = new TypeReference("int") { IsArray = true, IsReadOnly = true },
IsConstant = true,
InitialValue = strides,
};
table.Documentation.Add("How far each step goes.");
file.Members.Add(table);

// The free function taking the array, declared before what calls it.
file.Members.Add(Walk("walk_impl"));

ClassDeclaration holder = new("Table") { Kind = TypeDeclarationKind.Struct };
holder.Documentation.Add("Something taking an array through a member.");

FunctionDeclaration sum = new("sum") { ReturnType = "int", IsStatic = true };
sum.Parameters.Add(new Parameter("steps") { Type = new TypeReference("int") { IsArray = true } });
sum.Parameters.Add(new Parameter("count", "int"));
sum.Body.Add(new ReturnStatement(
new CallExpression("walk_impl")
{
Arguments = { new VariableReference("steps"), new VariableReference("count") },
}));
holder.Members.Add(sum);

file.Members.Add(holder);

return file;
}

/// <summary>
/// Builds a function taking an array by parameter and reading through it.
/// </summary>
/// <param name="name">What to call it.</param>
/// <returns>The declaration.</returns>
/// <remarks>
/// The body subscripts the parameter, so the parameter's spelling is load-bearing rather than
/// merely present: a signature nobody reads through would compile with the brackets anywhere.
/// </remarks>
private static FunctionDeclaration Walk(string name)
{
FunctionDeclaration walk = new(name) { ReturnType = "int", IsStatic = true };
walk.Parameters.Add(new Parameter("steps") { Type = new TypeReference("int") { IsArray = true } });
walk.Parameters.Add(new Parameter("count", "int"));
walk.Body.Add(new ReturnStatement(
new BinaryExpression(
new VariableReference("steps[0]"),
BinaryOperator.Add,
new VariableReference("count"))));

return walk;
}
}
53 changes: 53 additions & 0 deletions Coder.Test/Languages/CppGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,59 @@
Assert.AreEqual("false", Generator.Generate(Literal.Bool(false)));
}

/// <summary>
/// Tests that an array-typed parameter puts its brackets on the declarator.
/// </summary>
/// <remarks>
/// <c>int steps[]</c> is the parameter; <c>int[] steps</c> is a compile error. C++ has no
/// position for an array's brackets other than after the name it declares.
/// </remarks>
[TestMethod]
public void ArrayParameter_PutsTheBracketsOnTheDeclarator()
{
FunctionDeclaration function = new("walk");
function.Parameters.Add(new Parameter("steps") { Type = new TypeReference("int") { IsArray = true } });

string code = Generator.Generate(function);

StringAssert.Contains(code, "void walk(int steps[])");

Check warning on line 154 in Coder.Test/Languages/CppGeneratorTests.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=AaCYdwDwNo70Q9qDR93n&open=AaCYdwDwNo70Q9qDR93n&pullRequest=61
}

/// <summary>
/// Tests that an array-typed local puts its brackets on the declarator.
/// </summary>
[TestMethod]
public void ArrayVariable_PutsTheBracketsOnTheDeclarator()
{
VariableDeclaration local = new("steps") { Type = new TypeReference("int") { IsArray = true } };

string code = Generator.Generate(local);

Assert.AreEqual($"int steps[];{CodeBlocker.DefaultNewLineString}", code);
}

/// <summary>
/// Tests that a type spelled on its own carries no brackets, there being no declarator to put
/// them on.
/// </summary>
/// <remarks>
/// The positions that spell a type without a name — a return type, a base type, an enumeration's
/// underlying type — are ones C++ does not let an array stand in at all, so brackets there would
/// be a compile error wearing the shape of a feature.
/// </remarks>
[TestMethod]
public void ArrayReturnType_DoesNotSpellBracketsInTypePosition()
{
FunctionDeclaration function = new("collect")
{
ReturnType = new TypeReference("int") { IsArray = true },
};

string code = Generator.Generate(function);

Assert.IsFalse(code.Contains("[]", StringComparison.Ordinal), $"Expected no brackets in type position, got: {code}");
}

/// <summary>
/// Tests that a node the generator does not handle is refused rather than silently mis-generated.
/// </summary>
Expand Down
10 changes: 9 additions & 1 deletion Coder/Languages/CFamilyGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,18 +120,26 @@ protected override bool WriteFileDirectives(SourceFile file, CodeBlocker code)
/// <c>T name[]</c>, never <c>T[] name</c> — so a declaration cannot be built by writing the type
/// and the name in that order, which is what every other language here does. This is the one
/// place that difference lives.
/// <para>
/// An empty name is a declarator with nothing to declare, which is what an unnamed parameter is:
/// the brackets still belong after it, so <c>T[]</c> comes out with no space in front of the
/// nothing.
/// </para>
/// </remarks>
protected string SpellDeclarator(TypeReference type, string name)
{
Ensure.NotNull(type);
Ensure.NotNull(name);

TypeReference element = type.IsArray ? type.Clone() : type;
if (type.IsArray)
{
element.IsArray = false;
}

return $"{SpellType(element)} {name}{(type.IsArray ? "[]" : string.Empty)}";
string declared = name.Length == 0 ? string.Empty : $" {name}";

return $"{SpellType(element)}{declared}{(type.IsArray ? "[]" : string.Empty)}";
}

/// <inheritdoc/>
Expand Down
41 changes: 24 additions & 17 deletions Coder/Languages/CppGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@
/// describing a scope.
/// </para>
/// </remarks>
protected override void GenerateClassDeclaration(ClassDeclaration classDecl, CodeBlocker code)

Check warning on line 333 in Coder/Languages/CppGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 333 in Coder/Languages/CppGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 333 in Coder/Languages/CppGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 333 in Coder/Languages/CppGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 333 in Coder/Languages/CppGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 333 in Coder/Languages/CppGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 333 in Coder/Languages/CppGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 333 in Coder/Languages/CppGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.
{
Ensure.NotNull(classDecl);
Ensure.NotNull(code);
Expand Down Expand Up @@ -620,7 +620,7 @@
code.Write("const ");
}

code.Write($"{GetDeclaredType(field)} {field.Name}");
code.Write(SpellVariableDeclarator(field));

if (field.InitialValue is not null)
{
Expand Down Expand Up @@ -665,15 +665,12 @@
Ensure.NotNull(parameter);
Ensure.NotNull(code);

code.Write(MapToCppType(parameter.Type ?? new TypeReference(UnknownTypeName)));

// An empty name means deliberately unnamed, which C++ allows and a deleted copy constructor
// wants: the parameter exists to make the signature, and naming it would only invite someone
// to look for where it is used. A null name means nobody said, so one is invented.
if (parameter.Name is not "")
{
code.Write($" {parameter.Name ?? $"param{position}"}");
}
string name = parameter.Name is "" ? string.Empty : parameter.Name ?? $"param{position}";

code.Write(SpellDeclarator(parameter.Type ?? new TypeReference(UnknownTypeName), name));

AppendDefaultValue(parameter, code);
}
Expand All @@ -689,7 +686,7 @@
code.Write("const ");
}

code.Write($"{GetDeclaredType(varDecl)} {varDecl.Name}");
code.Write(SpellVariableDeclarator(varDecl));

if (varDecl.InitialValue is not null)
{
Expand All @@ -701,22 +698,26 @@
}

/// <summary>
/// Spells the type a declaration is introduced with.
/// Spells a local declaration up to its name.
/// </summary>
/// <param name="varDecl">The declaration being emitted.</param>
/// <returns>The C++ type name, or a deduced placeholder.</returns>
/// <returns>The type and the name, with an array's brackets where C++ puts them.</returns>
/// <remarks>
/// The type and the name are spelled together rather than one after the other, because an array
/// separates them: <see cref="CFamilyGenerator.SpellDeclarator"/> is the one place that knows it.
/// <para>
/// <c>auto</c> needs an initializer to deduce from, so an inferred declaration without one falls
/// back to <c>std::any</c>.
/// back to <c>std::any</c>. Neither deduces an array, so neither goes through the declarator.
/// </para>
/// </remarks>
private static string GetDeclaredType(VariableDeclaration varDecl)
private string SpellVariableDeclarator(VariableDeclaration varDecl)
{
if (!varDecl.IsTypeInferred && varDecl.Type is TypeReference declared)
{
return MapToCppType(declared);
return SpellDeclarator(declared, varDecl.Name);
}

return varDecl.InitialValue is not null ? "auto" : "std::any";
return $"{(varDecl.InitialValue is not null ? "auto" : "std::any")} {varDecl.Name}";
}

/// <summary>
Expand All @@ -742,6 +743,14 @@
/// Only the name is mapped; the shape around it — arguments, <c>const</c>, <c>&amp;</c> and
/// <c>*</c> — is C++'s own spelling of what the type says, which is what the string form could
/// not express.
/// <para>
/// An array's brackets are not among them, because they belong to the declarator rather than to
/// the type. Everything that declares a name goes through
/// <see cref="CFamilyGenerator.SpellDeclarator"/> to get them; the positions that spell a type
/// with no name to put them after — a return type, a base type, an enumeration's underlying type
/// — are ones C++ does not let an array stand in at all, so writing them there would produce a
/// compile error wearing the shape of a feature.
/// </para>
/// </remarks>
private static string MapToCppType(TypeReference type)
{
Expand All @@ -756,9 +765,7 @@
_ => string.Empty,
};

string array = type.IsArray ? "[]" : string.Empty;

return $"{(type.IsReadOnly ? "const " : string.Empty)}{name}{arguments}{array}{indirection}";
return $"{(type.IsReadOnly ? "const " : string.Empty)}{name}{arguments}{indirection}";
}

/// <inheritdoc/>
Expand Down