diff --git a/Coder.Test/Languages/NonFiniteDoubleLiteralTests.cs b/Coder.Test/Languages/NonFiniteDoubleLiteralTests.cs index 83957ef..3838132 100644 --- a/Coder.Test/Languages/NonFiniteDoubleLiteralTests.cs +++ b/Coder.Test/Languages/NonFiniteDoubleLiteralTests.cs @@ -154,6 +154,46 @@ public void GeneratedGo_Compiles() directory => File.WriteAllText(Path.Combine(directory, "go.mod"), "module limits\n\ngo 1.21\n")); } + /// + /// Tests that a constant field or local holding NaN or an infinity is a Go var, because + /// math.NaN() and math.Inf are calls and a Go const cannot hold one. + /// + [TestMethod] + public void GoConstantNonFiniteValue_IsAVar() + { + string code = new GoGenerator().Generate(FileWithNonFiniteConstants()); + + StringAssert.Contains(code, "var Unset float64 = math.NaN()", StringComparison.Ordinal); + StringAssert.Contains(code, "var ceiling float64 = math.Inf(1)", StringComparison.Ordinal); + Assert.DoesNotContain("const Unset", code, StringComparison.Ordinal, code); + Assert.DoesNotContain("const ceiling", code, StringComparison.Ordinal, code); + } + + /// + /// Tests that a Go file declaring a constant NaN field and a constant infinite local compiles. + /// + [TestMethod] + public void GeneratedGoConstants_Compile() + { + if (ToolchainHarness.FindOnPath("version", "go") is null) + { + Assert.Inconclusive("No Go toolchain on the path, so nothing was compiled."); + return; + } + + ToolchainHarness.InTemporaryDirectory(directory => + { + File.WriteAllText(Path.Combine(directory, "go.mod"), "module limits\n\ngo 1.21\n"); + + string code = new GoGenerator().Generate(FileWithNonFiniteConstants()); + File.WriteAllText(Path.Combine(directory, "limits.go"), code); + + (int exitCode, string output) = ToolchainHarness.Run("go", "build ./...", directory); + + Assert.AreEqual(0, exitCode, $"go rejected the generated source:{Environment.NewLine}{code}{Environment.NewLine}{output}"); + }); + } + /// /// Tests that a Rust file returning NaN and both infinities compiles. /// @@ -198,6 +238,27 @@ private static SourceFile FileReturning(string name, params double[] values) return file; } + /// + /// Builds a file with a constant NaN field and a function holding a constant infinite local. + /// + private static SourceFile FileWithNonFiniteConstants() + { + SourceFile file = new("limits"); + file.Members.Add(new FieldDeclaration("Unset", new TypeReference("double")) + { + IsConstant = true, + IsStatic = true, + InitialValue = new LiteralExpression(double.NaN), + }); + + FunctionDeclaration function = new("ceiling") { ReturnType = new TypeReference("double") }; + function.Body.Add(new VariableDeclaration("ceiling", "double", new LiteralExpression(double.PositiveInfinity)) { IsConstant = true }); + function.Body.Add(new ReturnStatement(new VariableReference("ceiling"))); + file.Members.Add(function); + + return file; + } + private static int Occurrences(string text, string value) => text.Split(value, StringSplitOptions.None).Length - 1; diff --git a/Coder/Languages/GoGenerator.cs b/Coder/Languages/GoGenerator.cs index b19bbfa..1c200d1 100644 --- a/Coder/Languages/GoGenerator.cs +++ b/Coder/Languages/GoGenerator.cs @@ -1370,7 +1370,24 @@ private void WriteStorageKeyword(string? name, bool wanted, AstNode? value, Code /// Whether the declaration says its value never changes. /// What it starts at. /// True when Go will hold it as a constant. - private static bool IsConstant(bool wanted, AstNode? value) => wanted && IsCompileTimeValue(value); + /// + /// NaN and the infinities are the one kind of number that is not: Go has no constant spelling for + /// them, and math.NaN() and math.Inf(1) are calls, which a const cannot hold. + /// + private static bool IsConstant(bool wanted, AstNode? value) => + wanted && IsCompileTimeValue(value) && !IsNonFinite(value); + + /// + /// Reports whether a value is a NaN or infinite floating-point literal. + /// + /// The value to test. + /// True when it is. + private static bool IsNonFinite(AstNode? value) => value switch + { + LiteralExpression literal => !double.IsFinite(literal.Value), + LiteralExpression literal => !float.IsFinite(literal.Value), + _ => false, + }; /// /// Writes what a declaration starts at, giving a bare list the declaration's own type.