From 842e1d8c9abfee2d4ed23cab48d6209e56a9833a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 11:01:33 +0000 Subject: [PATCH] [patch] Bind a Rust parameter or operator self mut when the body assigns its field A parameter was declared mut only when the body assigned the parameter itself, by its exact name. Member access is a dotted VariableReference, so p.x += 1 never matched and rustc rejected the output with E0594. Operator impls never looked at the body at all, so self.x += rhs.x in an operator failed the same way. The scan now matches an assignment by the root of its target, the text before the first dot, and the operator path runs it over self and its operands for the by-value traits. A comparison still borrows both, so it is left alone. Fixes #99 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015zULxdhphqscknnLMm1Bk1 --- .../RustGeneratedSourceCompilesTests.cs | 75 +++++++++++++++++++ Coder/Languages/RustGenerator.cs | 63 +++++++++++++--- 2 files changed, 127 insertions(+), 11 deletions(-) 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,