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,