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
82 changes: 82 additions & 0 deletions Coder.Test/Languages/UnaryOperandSignTests.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Tests that a unary operator is not run into the sign of a negative operand.
/// </summary>
/// <remarks>
/// Negating <c>-1</c> written as <c>(--1)</c> is a decrement of a literal in C, C++, C#, JavaScript
/// and Go, which fails to compile.
/// </remarks>
[TestClass]
public class UnaryOperandSignTests
{
/// <summary>
/// Every generator, since each one negates through the shared unary emitter.
/// </summary>
private static readonly ILanguageGenerator[] Generators =
[
new CSharpGenerator(),
new CGenerator(),
new CppGenerator(),
new GoGenerator(),
new RustGenerator(),
new PythonGenerator(),
new JavaScriptGenerator(),
];

/// <summary>
/// Tests that negating a negative integer literal parenthesises the literal.
/// </summary>
[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);
}
}

/// <summary>
/// Tests that negating a negative double literal parenthesises the literal.
/// </summary>
[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}");
}
}

/// <summary>
/// Tests that an operand that cannot fuse with the operator is still written bare.
/// </summary>
/// <param name="op">The operator under test.</param>
/// <param name="value">The literal operand.</param>
/// <param name="expected">The expected generated source.</param>
[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);
}
}
}
35 changes: 35 additions & 0 deletions Coder/Languages/LanguageGeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,11 @@
/// <c>not ready</c> and C#'s <c>!ready</c> both come out right without either language special-casing
/// the emitter.
/// </para>
/// <para>
/// An operand whose spelling starts with the operator's own last character is parenthesised, so
/// negating <c>-1</c> is written <c>(-(-1))</c> rather than <c>(--1)</c>, which every C-family
/// target reads as a decrement.
/// </para>
/// </remarks>
protected void GenerateUnaryExpression(UnaryExpression unaryExpr, CodeBlocker code, string operatorSpelling)
{
Expand All @@ -665,10 +670,40 @@
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(")");
}

/// <summary>
/// Reports the sign a node's spelling starts with, if any.
/// </summary>
/// <param name="node">The node to inspect.</param>
/// <returns><c>'-'</c> for a negative number literal; otherwise null.</returns>
/// <remarks>
/// 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.
/// </remarks>
private static char? LeadingSign(AstNode? node) => node switch
{
LiteralExpression<int> { Value: < 0 } => '-',
LiteralExpression<double> doubleLit when double.IsNegative(doubleLit.Value) => '-',
AstLeafNode<int> { Value: < 0 } => '-',
_ => null,
};

/// <summary>
/// Reports whether a node is one of the standard shapes a generator built on these helpers accepts.
/// </summary>
Expand Down Expand Up @@ -831,7 +866,7 @@
string? origin = null;
long offset = 0;

foreach (EnumMember member in enumDecl.Members)

Check warning on line 869 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loop should be simplified by calling Select(member => member.Value))

Check warning on line 869 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loop should be simplified by calling Select(member => member.Value))

Check warning on line 869 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loop should be simplified by calling Select(member => member.Value))

Check warning on line 869 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loop should be simplified by calling Select(member => member.Value))

Check warning on line 869 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loop should be simplified by calling Select(member => member.Value))

Check warning on line 869 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loop should be simplified by calling Select(member => member.Value))

Check warning on line 869 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loop should be simplified by calling Select(member => member.Value))

Check warning on line 869 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loop should be simplified by calling Select(member => member.Value))
{
if (member.Value is not null)
{
Expand Down
Loading