diff --git a/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs index 95daef9..a5ae01d 100644 --- a/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs @@ -128,6 +128,54 @@ public void AMutableStaticFieldCompilesInsideAClass() }); } + /// + /// Tests that a constant string longer than the small-string buffer compiles in each position a + /// constant may stand. + /// + /// + /// constexpr std::string 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. + /// + [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 = { "" } }; + 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(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}"); + }); + } + /// /// Builds a header declaring an array in each position one with no bound may stand in. /// diff --git a/Coder.Test/Languages/CppGeneratorTests.cs b/Coder.Test/Languages/CppGeneratorTests.cs index 260cde0..6885cce 100644 --- a/Coder.Test/Languages/CppGeneratorTests.cs +++ b/Coder.Test/Languages/CppGeneratorTests.cs @@ -115,6 +115,31 @@ public void ConstantDeclaration_IsQualified() Assert.AreEqual($"const int limit = 10;{CodeBlocker.DefaultNewLineString}", Generator.Generate(constant)); } + /// + /// Tests that a constant field of a type that allocates is const rather than constexpr, + /// and that a literal type keeps constexpr. + /// + [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); + } + /// /// Tests that logical operators use the C++ symbolic spellings rather than Python's words. /// diff --git a/Coder/Languages/CppGenerator.cs b/Coder/Languages/CppGenerator.cs index 21b4d98..cdcd970 100644 --- a/Coder/Languages/CppGenerator.cs +++ b/Coder/Languages/CppGenerator.cs @@ -57,6 +57,17 @@ public class CppGenerator : CFamilyGenerator { "object", "std::any" } }; + /// + /// The standard-library types this generator writes that are not literal types. + /// + private static readonly HashSet NonLiteralTypes = new(StringComparer.Ordinal) + { + "std::string", + "std::vector", + "std::map", + "std::any", + }; + /// /// Gets the unique identifier for this language generator. /// @@ -607,13 +618,15 @@ protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlo /// A constant member is emitted as static constexpr. A plain const member is a /// per-instance value initialised once per object, which is not what a constant means; the /// static constexpr 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 inline static const instead; see . /// 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) { @@ -786,11 +799,20 @@ protected override string SpellType(TypeReference type) /// inline, so saying it inside a class would be noise at best. is what /// tells the two apart, and it is a depth rather than a flag so a type nested in a type stays /// balanced. + /// + /// A constant of a type that is not a literal type is const rather than constexpr; + /// see . + /// /// 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 "; } @@ -804,6 +826,30 @@ private string SpellStorage(FieldDeclaration field) return string.Empty; } + /// + /// Reports whether a type can be declared constexpr. + /// + /// The declared type. + /// False for a standard-library type that allocates; otherwise true. + /// + /// std::string and the containers allocate, and an allocation cannot outlive the constant + /// evaluation that made it, so a constexpr 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. + /// + 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); + } + /// /// Spells a type's argument list, supplying the one a bare container does not name. ///