diff --git a/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs index 01b3bdd..2489c2c 100644 --- a/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs @@ -399,6 +399,60 @@ public void MixedIntegerAndFloatingOperands_PromoteToFloating() }); } + /// + /// Tests that a conditional whose condition compares numbers of different widths widens the + /// narrower one, as the same comparison outside a conditional does, so the output compiles and is + /// still what gofmt writes (issue #395). + /// + [TestMethod] + public void ConditionComparingMixedNumericWidths_Compiles() + { + SourceFile file = new("conditions"); + file.Members.Add(Size("size", "int", new VariableReference("limit"), "long")); + file.Members.Add(Size("half", "int", Literal.DecimalValue(0.5), null)); + file.Members.Add(Size("single", "float", new VariableReference("limit"), "double")); + + string source = new GoGenerator().Generate(file); + Assert.Contains("if int64(n) < limit {", source, StringComparison.Ordinal, source); + Assert.Contains("if float64(n) < 0.5 {", source, StringComparison.Ordinal, source); + Assert.Contains("if float64(n) < limit {", source, StringComparison.Ordinal, source); + + 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); + File.WriteAllText(Path.Combine(directory, "conditions.go"), source); + (int exitCode, string output) = ToolchainHarness.Run("go", "build ./...", directory); + Assert.AreEqual(0, exitCode, $"Go rejected the generated source:{Environment.NewLine}{output}{Environment.NewLine}{source}"); + + (int formatted, string differs) = ToolchainHarness.Run("gofmt", "-l conditions.go", directory); + Assert.AreEqual(0, formatted, $"gofmt did not run:{Environment.NewLine}{differs}"); + Assert.AreEqual(string.Empty, differs.Trim(), $"gofmt would rewrite the generated source.{Environment.NewLine}{source}"); + }); + + // string name( n[, limit]) => n < limit ? "small" : "large"; + static FunctionDeclaration Size(string name, string type, Expression limit, string? limitType) + { + FunctionDeclaration function = new(name) { ReturnType = new TypeReference("string") }; + function.Parameters.Add(new Parameter("n", type)); + if (limitType is not null) + { + function.Parameters.Add(new Parameter("limit", limitType)); + } + + function.Body.Add(new ReturnStatement(new ConditionalExpression( + new BinaryExpression(new VariableReference("n"), BinaryOperator.LessThan, limit), + new LiteralExpression("small"), + new LiteralExpression("large")))); + return function; + } + } + /// /// Tests that a condition comparing against a struct literal compiles in every position a /// conditional is lowered to an if, and is still what gofmt writes. diff --git a/Coder/Languages/GoGenerator.cs b/Coder/Languages/GoGenerator.cs index 48574ee..0d2b6f8 100644 --- a/Coder/Languages/GoGenerator.cs +++ b/Coder/Languages/GoGenerator.cs @@ -1957,9 +1957,16 @@ private void WriteCondition(AstNode condition, CodeBlocker code) switch (condition) { case BinaryExpression binary when !IsBooleanBitwise(binary.Operator, binary.Left, binary.Right): - WriteConditionOperand(binary.Left, code); + // The operands meet at the wider of their types, as GenerateBinary widens them + // everywhere else (#395). A shift's count is not such an operand, and a logical + // operator's operands are booleans. + TypeReference? common = binary.Operator is BinaryOperator.LeftShift or BinaryOperator.RightShift + or BinaryOperator.LogicalAnd or BinaryOperator.LogicalOr + ? null + : Promoted(StaticType(binary.Left), StaticType(binary.Right)); + WriteWidenedConditionOperand(binary.Left, common, code); code.Write($" {GetOperatorSpelling(binary.Operator)} "); - WriteConditionOperand(binary.Right, code); + WriteWidenedConditionOperand(binary.Right, common, code); return; case BinaryExpression binary when BooleanBitwiseSpelling(binary) is string spelling: @@ -1979,6 +1986,28 @@ private void WriteCondition(AstNode condition, CodeBlocker code) } } + /// + /// Writes one operand of a comparison an if tests, converted to the type it meets the other + /// operand at when it is the narrower number. + /// + /// The operand to write. + /// The type the two operands meet at, or null where they are not widened. + /// The writer to emit into. + /// + /// A struct literal is never a number, so it is never converted, and is left to + /// , which parenthesises it. + /// + private void WriteWidenedConditionOperand(AstNode operand, TypeReference? common, CodeBlocker code) + { + if (common is not null && !StartsWithCompositeLiteral(operand)) + { + GenerateWidenedTo(operand, common, code); + return; + } + + WriteConditionOperand(operand, code); + } + /// /// Writes one operand of the expression an if tests, parenthesising a struct literal so the /// clause does not end at its opening brace.