From 8db0e91ffeda988e2d43172a7831dba9099168a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 04:34:00 +0000 Subject: [PATCH] Widen the operands of a Go if-clause comparison [patch] WriteCondition writes a top-level comparison itself to leave off the outer parentheses gofmt removes, which bypassed the operand widening GenerateBinary does. It now widens each operand to the type the two meet at, except for shifts and logical operators, and still parenthesises a struct literal operand. Fixes #395 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW4VpqLXsf6UDQnX46h --- .../GoGeneratedSourceCompilesTests.cs | 54 +++++++++++++++++++ Coder/Languages/GoGenerator.cs | 33 +++++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) 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.