Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<i32>() == 4",
Expand All @@ -109,6 +110,23 @@ private static SourceFile Exemplar()
return file;
}

/// <summary>
/// A function whose body assigns to one of its parameters.
/// </summary>
/// <returns>The declaration.</returns>
/// <remarks>
/// Every other target lets a parameter be reassigned; Rust binds it immutably, and without a
/// <c>mut</c> on the declaration the assignment is error E0384.
/// </remarks>
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<int>(0)));
clamp.Body.Add(new ReturnStatement(new VariableReference("value")));
return clamp;
}

/// <summary>
/// A type written over one parameter, so its impl block has to carry the parameter too.
/// </summary>
Expand Down
38 changes: 38 additions & 0 deletions Coder.Test/Languages/RustGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,44 @@
StringComparison.Ordinal);
}

/// <summary>
/// Tests that a parameter the body assigns to is declared <c>mut</c>, and only that one, since
/// Rust binds a parameter immutably.
/// </summary>
[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);

Check warning on line 80 in Coder.Test/Languages/RustGeneratorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaDhK6qJBiDDEjYc0TgJ&open=AaDhK6qJBiDDEjYc0TgJ&pullRequest=93
}

/// <summary>
/// Tests that an assignment after a local has taken over a parameter's name is to the local, so
/// the parameter is left immutable.
/// </summary>
[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<int>(1)));
function.Body.Add(new AssignmentStatement(new VariableReference("a"), new LiteralExpression<int>(2)));
function.Body.Add(new ReturnStatement(new VariableReference("a")));

StringAssert.Contains(
Generator.Generate(function),
"pub fn shadow(a: i32) -> i32",
StringComparison.Ordinal);

Check warning on line 99 in Coder.Test/Languages/RustGeneratorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaDhK6qJBiDDEjYc0TgK&open=AaDhK6qJBiDDEjYc0TgK&pullRequest=93
}

/// <summary>
/// Tests that an unrecognized type name is passed through, so a caller can name a real Rust type.
/// </summary>
Expand Down
56 changes: 56 additions & 0 deletions Coder/Languages/RustGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,17 @@ public class RustGenerator : StandardLanguageGenerator
/// </remarks>
private string implBounds = string.Empty;

/// <summary>
/// The parameters of the function being written that its body assigns to.
/// </summary>
/// <remarks>
/// 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 <c>mut</c>, and one it does not must not be, or rustc warns that the
/// <c>mut</c> is unused.
/// </remarks>
private IReadOnlySet<string> reassignedParameters = new HashSet<string>(StringComparer.Ordinal);

/// <summary>
/// Spells type parameters where they are being declared, bounds and all.
/// </summary>
Expand Down Expand Up @@ -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<string> outerReassigned = reassignedParameters;
reassignedParameters = ReassignedParameters(funcDecl);
GenerateParameterList(funcDecl.Parameters, code);
reassignedParameters = outerReassigned;
code.Write(")");

if (funcDecl.Kind == FunctionKind.Constructor)
Expand Down Expand Up @@ -960,6 +975,42 @@ private static bool ReturnsAValue(FunctionDeclaration funcDecl) =>
_ => funcDecl.Name ?? "unnamed_function",
};

/// <summary>
/// Finds the parameters a function's body assigns to before any local takes the name over.
/// </summary>
/// <param name="funcDecl">The function to inspect.</param>
/// <returns>The names of the parameters to declare <c>mut</c>.</returns>
/// <remarks>
/// 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 <c>let mut</c>, and not to the parameter.
/// </remarks>
private static HashSet<string> ReassignedParameters(FunctionDeclaration funcDecl)
{
HashSet<string> parameters = new(
funcDecl.Parameters.Select(parameter => parameter.Name).OfType<string>(),
StringComparer.Ordinal);
HashSet<string> 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;
}

/// <inheritdoc/>
/// <remarks>
/// A parameter's default value is written beside it as a comment. Rust has no default arguments,
Expand All @@ -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)));

Expand Down
Loading