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
68 changes: 68 additions & 0 deletions Coder.Test/Languages/FloatingPointLiteralTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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 floating-point literal is written so that each language reads it as one.
/// </summary>
/// <remarks>
/// A whole number written without its fraction is an integer in every target, so <c>1.0 / 2.0</c>
/// written as <c>1 / 2</c> is integer division — zero — in C, C++ and Go, and a type error in Rust.
/// </remarks>
[TestClass]
public class FloatingPointLiteralTests
{
/// <summary>
/// The generators that spell a double through the shared literal formatting.
/// </summary>
private static readonly ILanguageGenerator[] Generators =
[
new CGenerator(),
new CppGenerator(),
new GoGenerator(),
new RustGenerator(),
new PythonGenerator(),
new JavaScriptGenerator(),
];

/// <summary>
/// Tests that dividing two whole-number doubles is written as a floating-point division.
/// </summary>
[TestMethod]
public void AWholeNumberDoubleKeepsItsFraction()
{
BinaryExpression half = new(
new LiteralExpression<double>(1.0),
BinaryOperator.Divide,
new LiteralExpression<double>(2.0));

foreach (ILanguageGenerator generator in Generators)
{
string code = generator.Generate(half);

Assert.Contains("1.0 / 2.0", code, StringComparison.Ordinal, $"{generator.GetType().Name} wrote {code}");
}
}

/// <summary>
/// Tests that a value that already reads as a float is written as it round-trips, unchanged.
/// </summary>
/// <param name="value">The value to write.</param>
/// <param name="expected">What each generator should write.</param>
[TestMethod]
[DataRow(0.5, "0.5")]
[DataRow(-3.25, "-3.25")]
[DataRow(1e300, "1E+300")]
[DataRow(0.1, "0.1")]
public void AFractionalOrExponentDoubleIsWrittenAsItRoundTrips(double value, string expected)
{
foreach (ILanguageGenerator generator in Generators)
{
Assert.AreEqual(expected, generator.Generate(new LiteralExpression<double>(value)).Trim(), generator.GetType().Name);
}
}
}
20 changes: 19 additions & 1 deletion Coder/Languages/LanguageGeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ protected bool TryGenerateCommonNode(AstNode node, CodeBlocker code)
return true;

case LiteralExpression<double> doubleLit:
code.Write(doubleLit.Value.ToString(CultureInfo.InvariantCulture));
code.Write(FormatDouble(doubleLit.Value));
return true;

// Legacy support for AstLeafNode types
Expand Down Expand Up @@ -772,6 +772,24 @@ protected static string EscapeString(string value)
/// </remarks>
protected static string GetBinaryOperator(BinaryOperator op) => OperatorSymbols.GetSymbol(op);

/// <summary>
/// Spells a floating-point value so that it reads as one.
/// </summary>
/// <param name="value">The value to spell.</param>
/// <returns>The value's round-trip text, with <c>.0</c> added when it would otherwise be an integer.</returns>
/// <remarks>
/// Round-trip formatting drops the fraction of a whole number, and <c>2</c> is an integer in every
/// target: <c>1.0 / 2.0</c> would become integer division in C, C++ and Go, fail to compile in Rust,
/// and type an inferred local as an integer. A value with a point or an exponent already reads as a
/// float, and one that is not finite is left as it is — there is no number to add a point to.
/// </remarks>
protected static string FormatDouble(double value)
{
string text = value.ToString("R", CultureInfo.InvariantCulture);
bool readsAsInteger = double.IsFinite(value) && text.IndexOfAny(['.', 'E', 'e']) < 0;
return readsAsInteger ? text + ".0" : text;
}

/// <summary>
/// Maps a unary operator to its C-family spelling.
/// </summary>
Expand Down
Loading