diff --git a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs index dcc6b63..ddc0fef 100644 --- a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs @@ -153,6 +153,81 @@ public void UnaryOperatorsRustSpellsDifferently_Compile() }); } + /// + /// Tests that a parameter whose field the body assigns, and an operator's self or operand + /// whose field its body assigns, are bound mut — which Rust asks for to assign through a + /// field of a value just as it does to assign the whole of it. + /// + [TestMethod] + public void AssigningAFieldOfAParameterOrSelf_BindsItMut() + { + if (ToolchainHarness.FindOnPath("--version", "rustc") is null) + { + Assert.Inconclusive("No Rust compiler on the path, so nothing was compiled."); + return; + } + + ClassDeclaration point = new("Point") { Kind = TypeDeclarationKind.Struct }; + point.Members.Add(new VariableDeclaration("x", "int")); + point.Members.Add(new VariableDeclaration("y", "int")); + + // self.x += rhs.x; rhs.y = 0; return self; + FunctionDeclaration plus = new("+") { Kind = FunctionKind.Operator, ReturnType = "Point" }; + plus.Parameters.Add(new Parameter("rhs", "Point")); + plus.Body.Add(new AssignmentStatement( + new VariableReference("self.x"), new VariableReference("rhs.x"), AssignmentOperator.AddAssign)); + plus.Body.Add(new AssignmentStatement(new VariableReference("rhs.y"), Literal.Number(0))); + plus.Body.Add(new ReturnStatement(new VariableReference("self"))); + point.Members.Add(plus); + + // p.x += 1; return p; + FunctionDeclaration shifted = new("shifted") { ReturnType = "Point" }; + shifted.Parameters.Add(new Parameter("p", "Point")); + shifted.Body.Add(new AssignmentStatement( + new VariableReference("p.x"), Literal.Number(1), AssignmentOperator.AddAssign)); + shifted.Body.Add(new ReturnStatement(new VariableReference("p"))); + + SourceFile file = new("fields"); + file.Members.Add(point); + file.Members.Add(shifted); + + ToolchainHarness.InTemporaryDirectory(directory => + { + string source = new RustGenerator().Generate(file); + string allowed = "#![allow(unused_parens, unused_assignments, dead_code)]"; + + Assert.Contains("fn add(mut self, mut rhs: Point)", source, StringComparison.Ordinal, source); + Assert.Contains("mut p: Point", source, StringComparison.Ordinal, source); + + File.WriteAllText( + Path.Combine(directory, "fields.rs"), + $"{allowed}{Environment.NewLine}{source}"); + + (int exitCode, string output) = ToolchainHarness.Run( + "rustc", + "--crate-type lib --edition 2021 -o fields.rlib fields.rs", + directory); + + Assert.AreEqual(0, exitCode, $"rustc rejected the generated source:{Environment.NewLine}{source}{Environment.NewLine}{output}"); + }); + } + + /// + /// Tests that an operator whose body assigns nothing still takes its operands as they are, since + /// a mut nothing needs is a warning in Rust. + /// + [TestMethod] + public void AnOperatorThatAssignsNothing_BindsNothingMut() + { + ClassDeclaration point = new("Point") { Kind = TypeDeclarationKind.Struct }; + point.Members.Add(new VariableDeclaration("x", "int")); + point.Members.Add(CompiledExemplar.Plus()); + + string source = new RustGenerator().Generate(point); + + Assert.Contains("fn add(self, rhs: Point)", source, StringComparison.Ordinal, source); + } + /// /// Tests that the arguments an entry point is handed are the ones the user typed, without the /// program's own path that std::env::args puts first — which is what they are in every diff --git a/Coder/Languages/RustGenerator.cs b/Coder/Languages/RustGenerator.cs index 2635ef4..cdd231e 100644 --- a/Coder/Languages/RustGenerator.cs +++ b/Coder/Languages/RustGenerator.cs @@ -54,6 +54,11 @@ public class RustGenerator : StandardLanguageGenerator /// private const string BaseMemberName = "base"; + /// + /// The receiver, as a body names it and as an operator's signature binds it. + /// + private const string SelfName = "self"; + private static readonly Dictionary TypeMappings = new(StringComparer.OrdinalIgnoreCase) { { "str", "String" }, @@ -765,14 +770,19 @@ private void GenerateTraitImplementation(FunctionDeclaration funcDecl, string ty } // A comparison borrows what it compares and answers a bool; an arithmetic operator consumes - // its operands and answers the type it named as its Output. - string self = op.ReturnsBool ? "&self" : "self"; + // its operands and answers the type it named as its Output. An operand it consumes is its + // own to change, but only if it is bound mut, as a parameter is. + HashSet reassigned = op.ReturnsBool + ? [] + : ReassignedNames(funcDecl.Body, [SelfName, .. funcDecl.Parameters.Select(OperandName)]); + string self = op.ReturnsBool ? "&self" : reassigned.Contains(SelfName) ? $"mut {SelfName}" : SelfName; code.Write($"fn {op.Method}({self}"); foreach (Parameter parameter in funcDecl.Parameters) { + string name = OperandName(parameter); code.Write(", "); - code.Write($"{parameter.Name ?? "rhs"}: {SpellOperandType(parameter, op, typeName)}"); + code.Write($"{(reassigned.Contains(name) ? "mut " : string.Empty)}{name}: {SpellOperandType(parameter, op, typeName)}"); } code.Write($") -> {(op.ReturnsBool ? "bool" : result)} "); @@ -781,6 +791,13 @@ private void GenerateTraitImplementation(FunctionDeclaration funcDecl, string ty WriteBody(funcDecl.Body, code); } + /// + /// Names the operand beside the instance. + /// + /// The operand as the declaration carries it. + /// Its name, or rhs, which is what the standard library's traits call it. + private static string OperandName(Parameter parameter) => parameter.Name ?? "rhs"; + /// /// Spells the type of the operand beside the instance. /// @@ -1113,27 +1130,40 @@ private static bool ReturnsAValue(FunctionDeclaration funcDecl) => /// /// The function to inspect. /// The names of the parameters to declare mut. + private static HashSet ReassignedParameters(FunctionDeclaration funcDecl) => + ReassignedNames(funcDecl.Body, funcDecl.Parameters.Select(parameter => parameter.Name).OfType()); + + /// + /// Finds which of the given bindings a body assigns to, or assigns a field of, before any local + /// takes the name over. + /// + /// The statements to inspect. + /// The names bound on entry: parameters, and self where it is taken by value. + /// The names to declare mut. /// + /// Member access is a dotted , so p.x = … names p by + /// the text before its first dot — and Rust asks for mut p to assign through a field of a + /// value just as it does to assign the whole of it. + /// /// A local declared with a parameter's name shadows it from there on, so an assignment after /// that point is to the local, which is already let mut, and not to the parameter. + /// /// - private static HashSet ReassignedParameters(FunctionDeclaration funcDecl) + private static HashSet ReassignedNames(IEnumerable body, IEnumerable candidates) { - HashSet parameters = new( - funcDecl.Parameters.Select(parameter => parameter.Name).OfType(), - StringComparer.Ordinal); + HashSet bound = new(candidates, StringComparer.Ordinal); HashSet reassigned = new(StringComparer.Ordinal); - foreach (AstNode statement in funcDecl.Body) + foreach (AstNode statement in body) { switch (statement) { case VariableDeclaration local: - parameters.Remove(local.Name); + bound.Remove(local.Name); break; - case AssignmentStatement { Target: VariableReference target } when parameters.Contains(target.Name): - reassigned.Add(target.Name); + case AssignmentStatement { Target: VariableReference target } when bound.Contains(RootOf(target.Name)): + reassigned.Add(RootOf(target.Name)); break; default: @@ -1144,6 +1174,17 @@ private static HashSet ReassignedParameters(FunctionDeclaration funcDecl return reassigned; } + /// + /// Gives the binding a possibly dotted name starts from. + /// + /// A name, or a member access written as one. + /// The text before the first dot, or the whole name when there is none. + private static string RootOf(string name) + { + int dot = name.IndexOf('.', StringComparison.Ordinal); + return dot < 0 ? name : name[..dot]; + } + /// /// /// A parameter's default value is written beside it as a comment. Rust has no default arguments,