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
75 changes: 75 additions & 0 deletions Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,81 @@ public void UnaryOperatorsRustSpellsDifferently_Compile()
});
}

/// <summary>
/// Tests that a parameter whose field the body assigns, and an operator's <c>self</c> or operand
/// whose field its body assigns, are bound <c>mut</c> — which Rust asks for to assign through a
/// field of a value just as it does to assign the whole of it.
/// </summary>
[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}");
});
}

/// <summary>
/// Tests that an operator whose body assigns nothing still takes its operands as they are, since
/// a <c>mut</c> nothing needs is a warning in Rust.
/// </summary>
[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);
}

/// <summary>
/// Tests that the arguments an entry point is handed are the ones the user typed, without the
/// program's own path that <c>std::env::args</c> puts first — which is what they are in every
Expand Down
63 changes: 52 additions & 11 deletions Coder/Languages/RustGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
/// </remarks>
private const string BaseMemberName = "base";

/// <summary>
/// The receiver, as a body names it and as an operator's signature binds it.
/// </summary>
private const string SelfName = "self";

private static readonly Dictionary<string, string> TypeMappings = new(StringComparer.OrdinalIgnoreCase)
{
{ "str", "String" },
Expand Down Expand Up @@ -765,14 +770,19 @@
}

// 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<string> reassigned = op.ReturnsBool
? []
: ReassignedNames(funcDecl.Body, [SelfName, .. funcDecl.Parameters.Select(OperandName)]);
string self = op.ReturnsBool ? "&self" : reassigned.Contains(SelfName) ? $"mut {SelfName}" : SelfName;

Check warning on line 778 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 778 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 778 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 778 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 778 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 778 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 778 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 778 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 778 in Coder/Languages/RustGenerator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaELwaaRMQZK9AjBYX9h&open=AaELwaaRMQZK9AjBYX9h&pullRequest=153
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)} ");
Expand All @@ -781,6 +791,13 @@
WriteBody(funcDecl.Body, code);
}

/// <summary>
/// Names the operand beside the instance.
/// </summary>
/// <param name="parameter">The operand as the declaration carries it.</param>
/// <returns>Its name, or <c>rhs</c>, which is what the standard library's traits call it.</returns>
private static string OperandName(Parameter parameter) => parameter.Name ?? "rhs";

/// <summary>
/// Spells the type of the operand beside the instance.
/// </summary>
Expand Down Expand Up @@ -1113,27 +1130,40 @@
/// </summary>
/// <param name="funcDecl">The function to inspect.</param>
/// <returns>The names of the parameters to declare <c>mut</c>.</returns>
private static HashSet<string> ReassignedParameters(FunctionDeclaration funcDecl) =>
ReassignedNames(funcDecl.Body, funcDecl.Parameters.Select(parameter => parameter.Name).OfType<string>());

/// <summary>
/// Finds which of the given bindings a body assigns to, or assigns a field of, before any local
/// takes the name over.
/// </summary>
/// <param name="body">The statements to inspect.</param>
/// <param name="candidates">The names bound on entry: parameters, and <c>self</c> where it is taken by value.</param>
/// <returns>The names to declare <c>mut</c>.</returns>
/// <remarks>
/// Member access is a dotted <see cref="VariableReference"/>, so <c>p.x = …</c> names <c>p</c> by
/// the text before its first dot — and Rust asks for <c>mut p</c> to assign through a field of a
/// value just as it does to assign the whole of it.
/// <para>
/// 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.
/// </para>
/// </remarks>
private static HashSet<string> ReassignedParameters(FunctionDeclaration funcDecl)
private static HashSet<string> ReassignedNames(IEnumerable<AstNode> body, IEnumerable<string> candidates)
{
HashSet<string> parameters = new(
funcDecl.Parameters.Select(parameter => parameter.Name).OfType<string>(),
StringComparer.Ordinal);
HashSet<string> bound = new(candidates, StringComparer.Ordinal);
HashSet<string> 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:
Expand All @@ -1144,6 +1174,17 @@
return reassigned;
}

/// <summary>
/// Gives the binding a possibly dotted name starts from.
/// </summary>
/// <param name="name">A name, or a member access written as one.</param>
/// <returns>The text before the first dot, or the whole name when there is none.</returns>
private static string RootOf(string name)
{
int dot = name.IndexOf('.', StringComparison.Ordinal);
return dot < 0 ? name : name[..dot];
}

/// <inheritdoc/>
/// <remarks>
/// A parameter's default value is written beside it as a comment. Rust has no default arguments,
Expand Down
Loading