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
48 changes: 48 additions & 0 deletions Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,54 @@ public void AMutableStaticFieldCompilesInsideAClass()
});
}

/// <summary>
/// Tests that a constant string longer than the small-string buffer compiles in each position a
/// constant may stand.
/// </summary>
/// <remarks>
/// <c>constexpr std::string</c> compiles only while the value fits libstdc++'s small-string buffer,
/// so a short literal would pass whichever way the generator spelled it. The value here is longer
/// than that buffer, which is what makes the spelling load-bearing.
/// </remarks>
[TestMethod]
public void ALongConstantStringCompilesWhereverAConstantMayStand()
{
string? compiler = ToolchainHarness.FindOnPath("--version", Compilers);
if (compiler is null)
{
Assert.Inconclusive("No C++ compiler on the path, so nothing was compiled.");
return;
}

const string Long = "a string longer than fifteen chars";

SourceFile file = new("strings") { IsHeader = true, Imports = { "<string>" } };
file.Members.Add(new FieldDeclaration("GREETING", "string") { IsConstant = true, InitialValue = Literal.Text(Long) });

ClassDeclaration holder = new("Names") { Kind = TypeDeclarationKind.Struct };
holder.Members.Add(new FieldDeclaration("Name", "string") { IsStatic = true, IsConstant = true, InitialValue = Literal.Text(Long) });
file.Members.Add(holder);

ClassDeclaration labels = new("Labels");
labels.Members.Add(new VariableDeclaration("Title", "string") { Visibility = Visibility.Public, IsConstant = true, InitialValue = Literal.Text(Long) });
file.Members.Add(labels);

ToolchainHarness.InTemporaryDirectory(directory =>
{
File.WriteAllText(Path.Combine(directory, "strings.h"), new CppGenerator().Generate(file));
File.WriteAllText(
Path.Combine(directory, "driver.cpp"),
"#include \"strings.h\"\n\nint main()\n{\n\treturn static_cast<int>(GREETING.size() + Names::Name.size() + Labels::Title.size()) * 0;\n}\n");

(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>
Expand Down
25 changes: 25 additions & 0 deletions Coder.Test/Languages/CppGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,31 @@ public void ConstantDeclaration_IsQualified()
Assert.AreEqual($"const int limit = 10;{CodeBlocker.DefaultNewLineString}", Generator.Generate(constant));
}

/// <summary>
/// Tests that a constant field of a type that allocates is <c>const</c> rather than <c>constexpr</c>,
/// and that a literal type keeps <c>constexpr</c>.
/// </summary>
[TestMethod]
public void ConstantField_OfANonLiteralType_IsConstRatherThanConstexpr()
{
FieldDeclaration text = new("Name", "string") { IsConstant = true, InitialValue = Literal.Text("a string longer than fifteen chars") };
FieldDeclaration number = new("Limit", "int") { IsConstant = true, InitialValue = Literal.Number(10) };

Assert.StartsWith("inline const std::string Name = ", Generator.Generate(text), StringComparison.Ordinal);
Assert.StartsWith("inline constexpr int Limit = ", Generator.Generate(number), StringComparison.Ordinal);

ClassDeclaration holder = new("Names") { Kind = TypeDeclarationKind.Struct };
holder.Members.Add(text);
holder.Members.Add(number);
holder.Members.Add(new VariableDeclaration("Title", "string", Literal.Text("a title")) { IsConstant = true });
string code = Generator.Generate(holder);

Assert.Contains("inline static const std::string Name = ", code, StringComparison.Ordinal);
Assert.Contains("static constexpr int Limit = ", code, StringComparison.Ordinal);
Assert.Contains("inline static const std::string Title = ", code, StringComparison.Ordinal);
Assert.DoesNotContain("constexpr std::string", code, StringComparison.Ordinal);
}

/// <summary>
/// Tests that logical operators use the C++ symbolic spellings rather than Python's words.
/// </summary>
Expand Down
50 changes: 48 additions & 2 deletions Coder/Languages/CppGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@
{ "object", "std::any" }
};

/// <summary>
/// The standard-library types this generator writes that are not literal types.
/// </summary>
private static readonly HashSet<string> NonLiteralTypes = new(StringComparer.Ordinal)
{
"std::string",
"std::vector",
"std::map",
"std::any",
};

/// <summary>
/// Gets the unique identifier for this language generator.
/// </summary>
Expand Down Expand Up @@ -330,7 +341,7 @@
/// describing a scope.
/// </para>
/// </remarks>
protected override void GenerateClassDeclaration(ClassDeclaration classDecl, CodeBlocker code)

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

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

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

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

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

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

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

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

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

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

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

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

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

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

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

View workflow job for this annotation

GitHub Actions / ci / .NET / 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 @@ -607,13 +618,15 @@
/// A constant member is emitted as <c>static constexpr</c>. A plain <c>const</c> member is a
/// per-instance value initialised once per object, which is not what a constant means; the
/// <c>static constexpr</c> spelling is the one that gives the class a single compile-time value,
/// and it is available because a constant declared here is initialised from a literal.
/// and it is available because a constant declared here is initialised from a literal. A type
/// that is not a literal type is <c>inline static const</c> instead; see <see cref="IsLiteralType"/>.
/// </remarks>
private void GenerateField(VariableDeclaration field, CodeBlocker code)
{
if (field.IsConstant && field.InitialValue is not null)
{
code.Write("static constexpr ");
bool literal = field.IsTypeInferred || field.Type is null || IsLiteralType(field.Type);
code.Write(literal ? "static constexpr " : "inline static const ");
}
else if (field.IsConstant)
{
Expand Down Expand Up @@ -786,11 +799,20 @@
/// inline, so saying it inside a class would be noise at best. <see cref="insideType"/> is what
/// tells the two apart, and it is a depth rather than a flag so a type nested in a type stays
/// balanced.
/// <para>
/// A constant of a type that is not a literal type is <c>const</c> rather than <c>constexpr</c>;
/// see <see cref="IsLiteralType"/>.
/// </para>
/// </remarks>
private string SpellStorage(FieldDeclaration field)
{
if (field.IsConstant)
{
if (field.Type is not null && !IsLiteralType(field.Type))
{
return insideType > 0 ? "inline static const " : "inline const ";
}

return insideType > 0 ? "static constexpr " : "inline constexpr ";
}

Expand All @@ -804,6 +826,30 @@
return string.Empty;
}

/// <summary>
/// Reports whether a type can be declared <c>constexpr</c>.
/// </summary>
/// <param name="type">The declared type.</param>
/// <returns>False for a standard-library type that allocates; otherwise true.</returns>
/// <remarks>
/// <c>std::string</c> and the containers allocate, and an allocation cannot outlive the constant
/// evaluation that made it, so a <c>constexpr</c> one does not compile. A short string appears to
/// work only because it fits libstdc++'s small-string buffer, so whether the output builds would
/// otherwise depend on the literal's length and the standard library. Before C++20 it is never
/// valid. A pointer or reference to one is a literal type, and a type this generator does not map
/// is the caller's, which is assumed to have been written to be one.
/// </remarks>
private static bool IsLiteralType(TypeReference type)
{
if (type.Indirection != TypeIndirection.None)
{
return true;
}

string name = TypeMappings.TryGetValue(type.Name, out string? mapped) ? mapped : type.Name;
return !NonLiteralTypes.Contains(name);
}

/// <summary>
/// Spells a type's argument list, supplying the one a bare container does not name.
/// </summary>
Expand Down
Loading