From 5a5698b54c05d5d0955b31c91ff7ce4426067f6e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 18:36:03 +0000 Subject: [PATCH] Write whole-number doubles with a fraction so they stay floating-point [patch] A double literal was written with its round-trip text alone, so 2.0 became 2 and 1.0 / 2.0 became integer division in C, C++ and Go and a type error in Rust. Part of #78. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012B4b6sZaZ7tFJzNvax1B89 --- .../Languages/FloatingPointLiteralTests.cs | 68 +++++++++++++++++++ Coder/Languages/LanguageGeneratorBase.cs | 20 +++++- 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 Coder.Test/Languages/FloatingPointLiteralTests.cs diff --git a/Coder.Test/Languages/FloatingPointLiteralTests.cs b/Coder.Test/Languages/FloatingPointLiteralTests.cs new file mode 100644 index 0000000..562bfee --- /dev/null +++ b/Coder.Test/Languages/FloatingPointLiteralTests.cs @@ -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; + +/// +/// Tests that a floating-point literal is written so that each language reads it as one. +/// +/// +/// A whole number written without its fraction is an integer in every target, so 1.0 / 2.0 +/// written as 1 / 2 is integer division — zero — in C, C++ and Go, and a type error in Rust. +/// +[TestClass] +public class FloatingPointLiteralTests +{ + /// + /// The generators that spell a double through the shared literal formatting. + /// + private static readonly ILanguageGenerator[] Generators = + [ + new CGenerator(), + new CppGenerator(), + new GoGenerator(), + new RustGenerator(), + new PythonGenerator(), + new JavaScriptGenerator(), + ]; + + /// + /// Tests that dividing two whole-number doubles is written as a floating-point division. + /// + [TestMethod] + public void AWholeNumberDoubleKeepsItsFraction() + { + BinaryExpression half = new( + new LiteralExpression(1.0), + BinaryOperator.Divide, + new LiteralExpression(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}"); + } + } + + /// + /// Tests that a value that already reads as a float is written as it round-trips, unchanged. + /// + /// The value to write. + /// What each generator should write. + [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(value)).Trim(), generator.GetType().Name); + } + } +} diff --git a/Coder/Languages/LanguageGeneratorBase.cs b/Coder/Languages/LanguageGeneratorBase.cs index a0df1f6..527e445 100644 --- a/Coder/Languages/LanguageGeneratorBase.cs +++ b/Coder/Languages/LanguageGeneratorBase.cs @@ -130,7 +130,7 @@ protected bool TryGenerateCommonNode(AstNode node, CodeBlocker code) return true; case LiteralExpression doubleLit: - code.Write(doubleLit.Value.ToString(CultureInfo.InvariantCulture)); + code.Write(FormatDouble(doubleLit.Value)); return true; // Legacy support for AstLeafNode types @@ -772,6 +772,24 @@ protected static string EscapeString(string value) /// protected static string GetBinaryOperator(BinaryOperator op) => OperatorSymbols.GetSymbol(op); + /// + /// Spells a floating-point value so that it reads as one. + /// + /// The value to spell. + /// The value's round-trip text, with .0 added when it would otherwise be an integer. + /// + /// Round-trip formatting drops the fraction of a whole number, and 2 is an integer in every + /// target: 1.0 / 2.0 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. + /// + 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; + } + /// /// Maps a unary operator to its C-family spelling. ///