From 6afc85846f1938634fc05bc98bf543868ea359cc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 04:29:12 +0000 Subject: [PATCH] Parenthesise a negative literal under a unary minus Negating -1 was written (--1), which C, C++, C#, JavaScript and Go read as a decrement of a literal and refuse to compile. GenerateUnaryExpression now wraps an operand whose spelling starts with the operator's own last character, so it comes out (-(-1)). Fixes ktsu-dev/Coder#82 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NtawxkUssqE3PHcqRCW1Ag --- Coder.Test/Languages/UnaryOperandSignTests.cs | 82 +++++++++++++++++++ Coder/Languages/LanguageGeneratorBase.cs | 35 ++++++++ 2 files changed, 117 insertions(+) create mode 100644 Coder.Test/Languages/UnaryOperandSignTests.cs diff --git a/Coder.Test/Languages/UnaryOperandSignTests.cs b/Coder.Test/Languages/UnaryOperandSignTests.cs new file mode 100644 index 0000000..072ba20 --- /dev/null +++ b/Coder.Test/Languages/UnaryOperandSignTests.cs @@ -0,0 +1,82 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Test.Languages; + +using ktsu.Coder.Ast; +using ktsu.Coder.Languages; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests that a unary operator is not run into the sign of a negative operand. +/// +/// +/// Negating -1 written as (--1) is a decrement of a literal in C, C++, C#, JavaScript +/// and Go, which fails to compile. +/// +[TestClass] +public class UnaryOperandSignTests +{ + /// + /// Every generator, since each one negates through the shared unary emitter. + /// + private static readonly ILanguageGenerator[] Generators = + [ + new CSharpGenerator(), + new CGenerator(), + new CppGenerator(), + new GoGenerator(), + new RustGenerator(), + new PythonGenerator(), + new JavaScriptGenerator(), + ]; + + /// + /// Tests that negating a negative integer literal parenthesises the literal. + /// + [TestMethod] + public void NegatingANegativeIntegerDoesNotWriteADecrement() + { + UnaryExpression negated = new(UnaryOperator.Negate, Literal.Number(-1)); + + foreach (ILanguageGenerator generator in Generators) + { + Assert.AreEqual("(-(-1))", generator.Generate(negated).Trim(), generator.GetType().Name); + } + } + + /// + /// Tests that negating a negative double literal parenthesises the literal. + /// + [TestMethod] + public void NegatingANegativeDoubleDoesNotWriteADecrement() + { + UnaryExpression negated = new(UnaryOperator.Negate, Literal.DecimalValue(-1.5)); + + foreach (ILanguageGenerator generator in Generators) + { + string code = generator.Generate(negated); + + Assert.DoesNotContain("--", code, StringComparison.Ordinal, $"{generator.GetType().Name} wrote {code}"); + Assert.StartsWith("(-(-1.5", code.Trim(), StringComparison.Ordinal, $"{generator.GetType().Name} wrote {code}"); + } + } + + /// + /// Tests that an operand that cannot fuse with the operator is still written bare. + /// + /// The operator under test. + /// The literal operand. + /// The expected generated source. + [TestMethod] + [DataRow(UnaryOperator.Negate, 1, "(-1)")] + [DataRow(UnaryOperator.Plus, -1, "(+-1)")] + public void AnOperandThatCannotFuseIsWrittenBare(UnaryOperator op, int value, string expected) + { + UnaryExpression unary = new(op, Literal.Number(value)); + + foreach (ILanguageGenerator generator in Generators) + { + Assert.AreEqual(expected, generator.Generate(unary).Trim(), generator.GetType().Name); + } + } +} diff --git a/Coder/Languages/LanguageGeneratorBase.cs b/Coder/Languages/LanguageGeneratorBase.cs index dae41c6..ec3d004 100644 --- a/Coder/Languages/LanguageGeneratorBase.cs +++ b/Coder/Languages/LanguageGeneratorBase.cs @@ -650,6 +650,11 @@ protected void GenerateBinaryExpression(BinaryExpression binaryExpr, CodeBlocker /// not ready and C#'s !ready both come out right without either language special-casing /// the emitter. /// + /// + /// An operand whose spelling starts with the operator's own last character is parenthesised, so + /// negating -1 is written (-(-1)) rather than (--1), which every C-family + /// target reads as a decrement. + /// /// protected void GenerateUnaryExpression(UnaryExpression unaryExpr, CodeBlocker code, string operatorSpelling) { @@ -665,10 +670,40 @@ protected void GenerateUnaryExpression(UnaryExpression unaryExpr, CodeBlocker co code.Write(" "); } + bool wouldFuse = operatorSpelling.Length > 0 && LeadingSign(unaryExpr.Operand) == operatorSpelling[^1]; + + if (wouldFuse) + { + code.Write("("); + } + GenerateInternal(unaryExpr.Operand, code); + + if (wouldFuse) + { + code.Write(")"); + } + code.Write(")"); } + /// + /// Reports the sign a node's spelling starts with, if any. + /// + /// The node to inspect. + /// '-' for a negative number literal; otherwise null. + /// + /// Every other expression starts with a name, a quote, a digit or a parenthesis, so only a + /// negative literal can run its sign into the operator in front of it. + /// + private static char? LeadingSign(AstNode? node) => node switch + { + LiteralExpression { Value: < 0 } => '-', + LiteralExpression doubleLit when double.IsNegative(doubleLit.Value) => '-', + AstLeafNode { Value: < 0 } => '-', + _ => null, + }; + /// /// Reports whether a node is one of the standard shapes a generator built on these helpers accepts. ///