From 4d8b44c39e8e7d262af2e1835bcdf7cb0a57f0d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 05:49:04 +0000 Subject: [PATCH] Make string literals owned wherever Rust receives a String [patch] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #120 wrote String::from("…") for a literal returned as, or initialising a local typed as, an owned String, but every other place a String receives a literal still wrote a bare &str and failed with E0308. - A struct literal's field, whether built by a constructor's initialisers, from a field's declared starting value, or by a construction naming the struct, is written for the type the field was declared with. The struct is the one whose impl is being written for Self, and is looked up in the document otherwise. - An assignment to a field of self is written for the field's type. - A conditional answering a String writes each branch as one. - A const or static declared as a string is a &'static str, and a table of them a &[&'static str]: String::from is not a const fn, so no constant can hold a String built from a literal. Fixes ktsu-dev/Coder#154 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NW9fJSB9VKsuPv7M2UmznC --- .../RustGeneratedSourceCompilesTests.cs | 90 +++++++++++ Coder/Languages/RustGenerator.cs | 142 ++++++++++++++++-- 2 files changed, 222 insertions(+), 10 deletions(-) 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); }