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