diff --git a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs index 8e0f69a..b72621e 100644 --- a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs @@ -99,6 +99,7 @@ private static SourceFile Exemplar() geometry.Members.Add(CompiledExemplar.OriginTable()); geometry.Members.Add(CompiledExemplar.Measure()); geometry.Members.Add(Boxed()); + geometry.Members.Add(Clamp()); geometry.Members.Add(new CompileTimeAssertion { Condition = "std::mem::size_of::() == 4", @@ -109,6 +110,23 @@ private static SourceFile Exemplar() return file; } + /// + /// A function whose body assigns to one of its parameters. + /// + /// The declaration. + /// + /// Every other target lets a parameter be reassigned; Rust binds it immutably, and without a + /// mut on the declaration the assignment is error E0384. + /// + private static FunctionDeclaration Clamp() + { + FunctionDeclaration clamp = new("clamp") { ReturnType = "int" }; + clamp.Parameters.Add(new Parameter("value", "int")); + clamp.Body.Add(new AssignmentStatement(new VariableReference("value"), new LiteralExpression(0))); + clamp.Body.Add(new ReturnStatement(new VariableReference("value"))); + return clamp; + } + /// /// A type written over one parameter, so its impl block has to carry the parameter too. /// diff --git a/Coder.Test/Languages/RustGeneratorTests.cs b/Coder.Test/Languages/RustGeneratorTests.cs index 6500be3..ee8e798 100644 --- a/Coder.Test/Languages/RustGeneratorTests.cs +++ b/Coder.Test/Languages/RustGeneratorTests.cs @@ -61,6 +61,44 @@ public void Types_AreMappedToRustSpellings() StringComparison.Ordinal); } + /// + /// Tests that a parameter the body assigns to is declared mut, and only that one, since + /// Rust binds a parameter immutably. + /// + [TestMethod] + public void ReassignedParameter_IsDeclaredMut() + { + FunctionDeclaration function = new("clamp") { ReturnType = "int" }; + function.Parameters.Add(new Parameter("a", "int")); + function.Parameters.Add(new Parameter("b", "int")); + function.Body.Add(new AssignmentStatement(new VariableReference("a"), new VariableReference("b"))); + function.Body.Add(new ReturnStatement(new VariableReference("a"))); + + StringAssert.Contains( + Generator.Generate(function), + "pub fn clamp(mut a: i32, b: i32) -> i32", + StringComparison.Ordinal); + } + + /// + /// Tests that an assignment after a local has taken over a parameter's name is to the local, so + /// the parameter is left immutable. + /// + [TestMethod] + public void ParameterShadowedBeforeAssignment_IsNotDeclaredMut() + { + FunctionDeclaration function = new("shadow") { ReturnType = "int" }; + function.Parameters.Add(new Parameter("a", "int")); + function.Body.Add(new VariableDeclaration("a", "int", new LiteralExpression(1))); + function.Body.Add(new AssignmentStatement(new VariableReference("a"), new LiteralExpression(2))); + function.Body.Add(new ReturnStatement(new VariableReference("a"))); + + StringAssert.Contains( + Generator.Generate(function), + "pub fn shadow(a: i32) -> i32", + StringComparison.Ordinal); + } + /// /// Tests that an unrecognized type name is passed through, so a caller can name a real Rust type. /// diff --git a/Coder/Languages/RustGenerator.cs b/Coder/Languages/RustGenerator.cs index 6892a04..5a54e44 100644 --- a/Coder/Languages/RustGenerator.cs +++ b/Coder/Languages/RustGenerator.cs @@ -117,6 +117,17 @@ public class RustGenerator : StandardLanguageGenerator /// private string implBounds = string.Empty; + /// + /// The parameters of the function being written that its body assigns to. + /// + /// + /// Held rather than passed because a parameter is written through the shared parameter list, + /// which knows nothing of the body. Rust binds a parameter immutably, so one the body assigns to + /// has to be declared mut, and one it does not must not be, or rustc warns that the + /// mut is unused. + /// + private IReadOnlySet reassignedParameters = new HashSet(StringComparer.Ordinal); + /// /// Spells type parameters where they are being declared, bounds and all. /// @@ -826,7 +837,11 @@ private void GenerateFunction(FunctionDeclaration funcDecl, CodeBlocker code, st code.Write($"fn {SpellFunctionName(funcDecl)}{SpellParameterDeclarations(funcDecl.TypeParameters)}("); WriteReceiver(funcDecl, enclosingType, code); + + IReadOnlySet outerReassigned = reassignedParameters; + reassignedParameters = ReassignedParameters(funcDecl); GenerateParameterList(funcDecl.Parameters, code); + reassignedParameters = outerReassigned; code.Write(")"); if (funcDecl.Kind == FunctionKind.Constructor) @@ -960,6 +975,42 @@ private static bool ReturnsAValue(FunctionDeclaration funcDecl) => _ => funcDecl.Name ?? "unnamed_function", }; + /// + /// Finds the parameters a function's body assigns to before any local takes the name over. + /// + /// The function to inspect. + /// The names of the parameters to declare mut. + /// + /// 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) + { + HashSet parameters = new( + funcDecl.Parameters.Select(parameter => parameter.Name).OfType(), + StringComparer.Ordinal); + HashSet reassigned = new(StringComparer.Ordinal); + + foreach (AstNode statement in funcDecl.Body) + { + switch (statement) + { + case VariableDeclaration local: + parameters.Remove(local.Name); + break; + + case AssignmentStatement { Target: VariableReference target } when parameters.Contains(target.Name): + reassigned.Add(target.Name); + break; + + default: + break; + } + } + + return reassigned; + } + /// /// /// A parameter's default value is written beside it as a comment. Rust has no default arguments, @@ -971,6 +1022,11 @@ protected override void GenerateParameter(Parameter parameter, CodeBlocker code, Ensure.NotNull(parameter); Ensure.NotNull(code); + if (parameter.Name is not null && reassignedParameters.Contains(parameter.Name)) + { + code.Write("mut "); + } + code.Write($"{parameter.Name ?? $"param{position}"}: "); code.Write(SpellType(parameter.Type ?? new TypeReference(UnknownTypeName)));