diff --git a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs
index f16ab7c..dcc6b63 100644
--- a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs
+++ b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs
@@ -106,6 +106,53 @@ public void StringLiteralsReceivedAsOwnedStrings_Compile()
});
}
+ ///
+ /// Tests that bitwise complement and unary plus compile: Rust has neither ~ nor a unary
+ /// +, so both have to be spelled its way.
+ ///
+ [TestMethod]
+ public void UnaryOperatorsRustSpellsDifferently_Compile()
+ {
+ if (ToolchainHarness.FindOnPath("--version", "rustc") is null)
+ {
+ Assert.Inconclusive("No Rust compiler on the path, so nothing was compiled.");
+ return;
+ }
+
+ FunctionDeclaration complement = new("complement") { ReturnType = "int" };
+ complement.Parameters.Add(new Parameter("value", "int"));
+ complement.Body.Add(new ReturnStatement(new UnaryExpression(UnaryOperator.BitwiseNot, new VariableReference("value"))));
+
+ FunctionDeclaration identity = new("identity") { ReturnType = "int" };
+ identity.Parameters.Add(new Parameter("value", "int"));
+ identity.Body.Add(new ReturnStatement(new UnaryExpression(UnaryOperator.Plus, new VariableReference("value"))));
+
+ FunctionDeclaration minusOne = new("minus_one") { ReturnType = "int" };
+ minusOne.Body.Add(new ReturnStatement(new UnaryExpression(UnaryOperator.Plus, Literal.Number(-1))));
+
+ SourceFile file = new("unary");
+ file.Members.Add(complement);
+ file.Members.Add(identity);
+ file.Members.Add(minusOne);
+
+ ToolchainHarness.InTemporaryDirectory(directory =>
+ {
+ string source = new RustGenerator().Generate(file);
+ string allowed = "#![allow(unused_parens, dead_code)]";
+
+ File.WriteAllText(
+ Path.Combine(directory, "unary.rs"),
+ $"{allowed}{Environment.NewLine}{source}");
+
+ (int exitCode, string output) = ToolchainHarness.Run(
+ "rustc",
+ "--crate-type lib --edition 2021 -o unary.rlib unary.rs",
+ directory);
+
+ Assert.AreEqual(0, exitCode, $"rustc rejected the generated source:{Environment.NewLine}{source}{Environment.NewLine}{output}");
+ });
+ }
+
///
/// 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.Test/Languages/RustGeneratorTests.cs b/Coder.Test/Languages/RustGeneratorTests.cs
index 44d797f..c7d2dc6 100644
--- a/Coder.Test/Languages/RustGeneratorTests.cs
+++ b/Coder.Test/Languages/RustGeneratorTests.cs
@@ -33,6 +33,29 @@ public void Identity_IsRust()
Assert.AreEqual("rs", Generator.FileExtension);
}
+ ///
+ /// Tests that the unary operators Rust spells differently are written its way: ! for bitwise
+ /// complement, since Rust has no ~, and the bare operand for unary plus, since Rust has no
+ /// unary +.
+ ///
+ /// The operator under test.
+ /// The expected generated source.
+ [TestMethod]
+ [DataRow(UnaryOperator.BitwiseNot, "(!x)")]
+ [DataRow(UnaryOperator.Plus, "(x)")]
+ [DataRow(UnaryOperator.LogicalNot, "(!x)")]
+ [DataRow(UnaryOperator.Negate, "(-x)")]
+ public void UnaryOperators_AreSpelledAsRustWritesThem(UnaryOperator op, string expected)
+ => Assert.AreEqual(expected, Generate(new UnaryExpression(op, new VariableReference("x"))).Trim());
+
+ ///
+ /// Tests that unary plus on a negative literal is written as the literal alone, not the +-1
+ /// rustc rejects.
+ ///
+ [TestMethod]
+ public void UnaryPlusOnANegativeLiteral_IsTheLiteral()
+ => Assert.AreEqual("(-1)", Generate(new UnaryExpression(UnaryOperator.Plus, Literal.Number(-1))).Trim());
+
///
/// Tests that a function with nothing to answer writes no return type, since Rust names the unit
/// only when it has to and nobody writes it.
diff --git a/Coder.Test/Languages/UnaryOperandSignTests.cs b/Coder.Test/Languages/UnaryOperandSignTests.cs
index 072ba20..e421095 100644
--- a/Coder.Test/Languages/UnaryOperandSignTests.cs
+++ b/Coder.Test/Languages/UnaryOperandSignTests.cs
@@ -2,6 +2,7 @@
namespace ktsu.Coder.Test.Languages;
+using System.Linq;
using ktsu.Coder.Ast;
using ktsu.Coder.Languages;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -74,7 +75,8 @@ public void AnOperandThatCannotFuseIsWrittenBare(UnaryOperator op, int value, st
{
UnaryExpression unary = new(op, Literal.Number(value));
- foreach (ILanguageGenerator generator in Generators)
+ // Rust has no unary plus, so it writes the operand alone; RustGeneratorTests covers it.
+ foreach (ILanguageGenerator generator in Generators.Where(g => op != UnaryOperator.Plus || g is not RustGenerator))
{
Assert.AreEqual(expected, generator.Generate(unary).Trim(), generator.GetType().Name);
}
diff --git a/Coder/Languages/RustGenerator.cs b/Coder/Languages/RustGenerator.cs
index 692af05..c0b4488 100644
--- a/Coder/Languages/RustGenerator.cs
+++ b/Coder/Languages/RustGenerator.cs
@@ -302,6 +302,23 @@ private sealed record OperatorTrait(string Name, string Method, bool ReturnsBool
///
protected override string? SpellAnnotation(Annotation annotation) => $"#[{annotation}]";
+
+ ///
+ /// Maps a unary operator to its Rust spelling.
+ ///
+ /// The operator to map.
+ /// The operator's source spelling.
+ ///
+ /// Rust has neither ~ nor a unary +. Its ! is bitwise complement on an
+ /// integer as well as logical negation on a bool, and unary plus, being the identity, is
+ /// written as the operand alone. The emitter still parenthesises it, so precedence is unchanged.
+ ///
+ protected override string GetUnaryOperatorSpelling(UnaryOperator op) => op switch
+ {
+ UnaryOperator.BitwiseNot => "!",
+ UnaryOperator.Plus => string.Empty,
+ _ => GetUnaryOperator(op)
+ };
///
///
/// use, which is what Rust writes where the others write an include or an import. A path