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