diff --git a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs
index e7e7acb..b80aa46 100644
--- a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs
+++ b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs
@@ -106,6 +106,96 @@ public void StringLiteralsReceivedAsOwnedStrings_Compile()
});
}
+ ///
+ /// Tests that a string literal compiles in every other place an owned String receives one:
+ /// a constant, a constructor's initialiser, a struct literal's field, an assignment to a field,
+ /// and both branches of a conditional answering one.
+ ///
+ ///
+ /// String::from is not a const fn, so a constant cannot be an owned String
+ /// at all: it is a &'static str, and a table of them a slice of those.
+ ///
+ [TestMethod]
+ public void StringLiteralsFillingStringFieldsAndConstants_Compile()
+ {
+ if (ToolchainHarness.FindOnPath("--version", "rustc") is null)
+ {
+ Assert.Inconclusive("No Rust compiler on the path, so nothing was compiled.");
+ return;
+ }
+
+ ClassDeclaration person = new("Person") { Kind = TypeDeclarationKind.Struct };
+ person.Members.Add(new VariableDeclaration("name", "string"));
+ person.Members.Add(new FieldDeclaration("nickname", "str") { InitialValue = Literal.Text("none") });
+ person.Members.Add(new FieldDeclaration("DEFAULT", "string")
+ {
+ IsStatic = true,
+ IsConstant = true,
+ InitialValue = Literal.Text("d"),
+ });
+
+ FunctionDeclaration ctor = new("Person") { Kind = FunctionKind.Constructor };
+ ctor.Initialisers.Add(new MemberInitialiser("name", Literal.Text("Anon")));
+ person.Members.Add(ctor);
+
+ ConstructionExpression built = new(new TypeReference("Person"));
+ built.Arguments.Add(new MemberInitialiser("name", Literal.Text("x")));
+ built.Arguments.Add(new MemberInitialiser("nickname", Literal.Text("y")));
+ FunctionDeclaration make = new("make") { ReturnType = "Person", IsStatic = true };
+ make.Body.Add(new ReturnStatement(built));
+ person.Members.Add(make);
+
+ FunctionDeclaration rename = new("rename");
+ rename.Body.Add(new AssignmentStatement(new VariableReference("self.name"), Literal.Text("y")));
+ person.Members.Add(rename);
+
+ FunctionDeclaration pick = new("pick") { ReturnType = "string", IsStatic = true };
+ pick.Parameters.Add(new Parameter("c", "bool"));
+ pick.Body.Add(new ReturnStatement(new ConditionalExpression(new VariableReference("c"), Literal.Text("a"), Literal.Text("b"))));
+ person.Members.Add(pick);
+
+ // A struct built outside its own impl, so the field types come from the declaration rather
+ // than from the type whose functions are being written.
+ ConstructionExpression stranger = new(new TypeReference("Person"));
+ stranger.Arguments.Add(new MemberInitialiser("name", Literal.Text("z")));
+ stranger.Arguments.Add(new MemberInitialiser("nickname", Literal.Text("w")));
+ FunctionDeclaration meet = new("meet") { ReturnType = "Person" };
+ meet.Body.Add(new ReturnStatement(stranger));
+
+ ConstructionExpression names = new();
+ names.Arguments.Add(Literal.Text("a"));
+ names.Arguments.Add(Literal.Text("b"));
+
+ SourceFile file = new("owned_strings");
+ file.Members.Add(person);
+ file.Members.Add(meet);
+ file.Members.Add(new FieldDeclaration("GREETING", "string") { IsStatic = true, IsConstant = true, InitialValue = Literal.Text("hi") });
+ file.Members.Add(new FieldDeclaration("MOTTO", "str") { IsStatic = true, InitialValue = Literal.Text("go") });
+ file.Members.Add(new FieldDeclaration("NAMES", new TypeReference("string") { IsArray = true })
+ {
+ IsStatic = true,
+ IsConstant = true,
+ InitialValue = names,
+ });
+
+ ToolchainHarness.InTemporaryDirectory(directory =>
+ {
+ string source = new RustGenerator().Generate(file);
+ string allowed = "#![allow(unused_parens, dead_code)]";
+
+ File.WriteAllText(
+ Path.Combine(directory, "owned_strings.rs"),
+ $"{allowed}{Environment.NewLine}{source}");
+
+ (int exitCode, string output) = ToolchainHarness.Run(
+ "rustc",
+ "--crate-type lib --edition 2021 -o owned_strings.rlib owned_strings.rs",
+ directory);
+
+ Assert.AreEqual(0, exitCode, $"rustc rejected the generated source:{Environment.NewLine}{source}{Environment.NewLine}{output}");
+ });
+ }
+
///
/// Tests that bitwise complement and unary plus compile: Rust has neither ~ nor a unary
/// +, so both have to be spelled its way.
diff --git a/Coder/Languages/RustGenerator.cs b/Coder/Languages/RustGenerator.cs
index f604c6b..9a63018 100644
--- a/Coder/Languages/RustGenerator.cs
+++ b/Coder/Languages/RustGenerator.cs
@@ -1038,12 +1038,25 @@ protected override void GenerateReturnStatement(ReturnStatement returnStmt, Code
/// map to the owned String, so a literal received as one is written
/// String::from("…"). Rust converts neither way on its own. Where the receiving type is
/// borrowed, or not declared, the literal stays bare, which is what those positions want.
+ ///
+ /// A conditional answers what its branches do, so each branch is written for the same type.
+ ///
///
private void WriteValue(AstNode value, TypeReference? target, CodeBlocker code)
{
- bool owned = value is LiteralExpression
- && target is not null
- && SpellType(target) == TypeMappings["string"];
+ if (value is ConditionalExpression conditional && IsOwnedString(target))
+ {
+ code.Write("(if ");
+ GenerateInternal(conditional.Condition, code);
+ code.Write(" { ");
+ WriteValue(conditional.WhenTrue, target, code);
+ code.Write(" } else { ");
+ WriteValue(conditional.WhenFalse, target, code);
+ code.Write(" })");
+ return;
+ }
+
+ bool owned = value is LiteralExpression && IsOwnedString(target);
if (owned)
{
@@ -1058,6 +1071,74 @@ private void WriteValue(AstNode value, TypeReference? target, CodeBlocker code)
}
}
+ ///
+ ///
+ /// A value assigned to a field of self is written for the type the field was declared
+ /// with, so a string literal assigned to a String field is made into one.
+ ///
+ protected override void GenerateAssignmentStatement(AssignmentStatement assignment, CodeBlocker code)
+ {
+ Ensure.NotNull(assignment);
+ Ensure.NotNull(code);
+
+ if (assignment.Operator == AssignmentOperator.Assign
+ && assignment.Target is VariableReference { Name: string target }
+ && target.StartsWith(SelfFieldPrefix, StringComparison.Ordinal)
+ && InstanceFieldType(constructedType, target[SelfFieldPrefix.Length..]) is TypeReference fieldType)
+ {
+ GenerateInternal(assignment.Target, code);
+ code.Write($" {GetAssignmentOperator(AssignmentOperator.Assign)} ");
+ WriteValue(assignment.Value, fieldType, code);
+ EndStatement(code);
+ return;
+ }
+
+ base.GenerateAssignmentStatement(assignment, code);
+ }
+
+ ///
+ /// How a body names a field of the instance its function was called on.
+ ///
+ private const string SelfFieldPrefix = "self.";
+
+ ///
+ /// Reports whether a type is spelled as Rust's owned String.
+ ///
+ /// The type, or null where none is declared.
+ /// True for str and string as a plain value.
+ private static bool IsOwnedString(TypeReference? type) =>
+ type is not null && SpellType(type) == TypeMappings["string"];
+
+ ///
+ /// Finds the declared type of an instance field of a type.
+ ///
+ /// The type, or null where it is not declared in the document.
+ /// The field's name.
+ /// The field's type, or null where the field or its type is not declared.
+ private static TypeReference? InstanceFieldType(ClassDeclaration? type, string? name) =>
+ type?.Members
+ .Select(member => member switch
+ {
+ FieldDeclaration field when !IsAssociatedField(field) && field.Name == name => field.Type,
+ VariableDeclaration field when field.Name == name => field.Type,
+ _ => null,
+ })
+ .FirstOrDefault(found => found is not null);
+
+ ///
+ /// Finds the declaration of a type a struct literal builds.
+ ///
+ /// The type the literal names.
+ /// The declaration, or null where the document does not declare it.
+ ///
+ /// Self is the type whose functions are being written. Any other name is looked up in the
+ /// document, so a literal built outside the type's own impl is written for its fields too.
+ ///
+ private ClassDeclaration? BuiltType(TypeReference type) =>
+ type.Name == "Self" || type.Name == constructedType?.Name
+ ? constructedType
+ : DeclaredTypes(GenerationRoot).FirstOrDefault(declared => declared.Name == type.Name);
+
///
/// Writes a function's statements.
///
@@ -1533,16 +1614,20 @@ protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlo
///
/// The declared type.
/// The type as Rust writes it in that position.
+ ///
+ /// A string is the borrowed &'static str rather than the owned String it is
+ /// everywhere else: String::from is not a const fn, so no constant or static can
+ /// hold a String built from a literal, and the literal itself is a &'static str.
+ /// The lifetime is spelled because an associated constant may not leave it out.
+ ///
private static string SpellStorageType(TypeReference type)
{
- if (!type.IsArray)
- {
- return SpellType(type);
- }
-
TypeReference element = type.Clone();
element.IsArray = false;
- return $"&[{SpellType(element)}]";
+
+ string spelled = IsOwnedString(element) ? "&'static str" : SpellType(element);
+
+ return type.IsArray ? $"&[{spelled}]" : spelled;
}
///
@@ -1681,7 +1766,18 @@ protected override void GenerateConstructionExpression(ConstructionExpression co
if (construction.Arguments.Any(argument => argument is MemberInitialiser))
{
code.Write($"{type} ");
- WriteElementList(construction, code, "{", "}", "{}");
+
+ ClassDeclaration? outerLiteral = literalType;
+ literalType = BuiltType(construction.Type);
+ try
+ {
+ WriteElementList(construction, code, "{", "}", "{}");
+ }
+ finally
+ {
+ literalType = outerLiteral;
+ }
+
return;
}
@@ -1689,6 +1785,12 @@ protected override void GenerateConstructionExpression(ConstructionExpression co
WriteCallArguments(construction, code);
}
+ ///
+ /// The type whose struct literal is being written, where the document declares it, which says
+ /// what each field named in the literal receives.
+ ///
+ private ClassDeclaration? literalType;
+
///
/// Writes the arguments of a construction that names none of its members.
///
@@ -1733,6 +1835,26 @@ protected override void WriteListElement(AstNode argument, CodeBlocker code)
return;
}
+ // A field is written for the type it was declared with, so a string literal filling a
+ // String field is made into one, as it is where a function returns one.
+ if (argument is MemberInitialiser { Value: AstNode value } field
+ && InstanceFieldType(literalType, field.Name) is TypeReference fieldType)
+ {
+ ClassDeclaration? outerLiteral = literalType;
+ literalType = null;
+ try
+ {
+ WriteDesignator(field.Name ?? string.Empty, code);
+ WriteValue(value, fieldType, code);
+ }
+ finally
+ {
+ literalType = outerLiteral;
+ }
+
+ return;
+ }
+
base.WriteListElement(argument, code);
}