From 399c2c8582c9d68d06c51ab2389accb557016da6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 10:42:12 +0000 Subject: [PATCH 1/2] Promote mixed int and float operands to the wider type [patch] StaticType typed a binary expression as its left operand's type and a conditional as its first branch's, so in Go `ready ? 1 : 2.5` passed as an argument became `func() int { ... return 2.5 }()` and `x := 1 + 0.5` was registered as int, and neither compiled. Combining two numeric types now takes the wider one (int < long < float < double). Go's conditional function literal reads the conditional's own StaticType, so its ExpectedType is honoured too. Fixes #152 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012KjGQ13RexM4WYjuEaBtE6 --- .../GoGeneratedSourceCompilesTests.cs | 55 ++++++++++++++++++- Coder/Languages/GoGenerator.cs | 10 ++-- Coder/Languages/StandardLanguageGenerator.cs | 34 +++++++++++- 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs index 35ebe6c..c6dd74c 100644 --- a/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2026 ktsu-dev contributors +// Copyright (c) 2023-2026 ktsu-dev contributors namespace ktsu.Coder.Test.Languages; @@ -229,6 +229,59 @@ public void ConditionalExpressions_InferParameterTypesAndLowerNestedReturns() }); } + /// + /// Tests that mixing an integer and a floating operand types the whole as the floating one, so + /// the function literal a conditional argument becomes returns float64, and a local + /// inferred from 1 + 0.5 is known to be one where a later conditional uses it (issue #152). + /// + [TestMethod] + public void MixedIntegerAndFloatingOperands_PromoteToFloating() + { + if (ToolchainHarness.FindOnPath("version", "go") is null) + { + Assert.Inconclusive("No Go toolchain on the path, so nothing was compiled."); + return; + } + + SourceFile file = new("promotion"); + + FunctionDeclaration half = new("half") { ReturnType = new TypeReference("double") }; + half.Parameters.Add(new Parameter("v", "double")); + half.Body.Add(new ReturnStatement(new BinaryExpression(new VariableReference("v"), BinaryOperator.Divide, Literal.DecimalValue(2.0)))); + file.Members.Add(half); + + FunctionDeclaration argument = new("argument") { ReturnType = new TypeReference("double") }; + argument.Parameters.Add(new Parameter("ready", "bool")); + CallExpression halfOfChoice = new("half"); + halfOfChoice.Arguments.Add(new ConditionalExpression(new VariableReference("ready"), Literal.Number(1), Literal.DecimalValue(2.5))); + argument.Body.Add(new ReturnStatement(halfOfChoice)); + file.Members.Add(argument); + + FunctionDeclaration inferred = new("inferred") { ReturnType = new TypeReference("double") }; + inferred.Parameters.Add(new Parameter("ready", "bool")); + inferred.Body.Add(new VariableDeclaration("x", initialValue: new BinaryExpression(Literal.Number(1), BinaryOperator.Add, Literal.DecimalValue(0.5)))); + CallExpression halfOfLocal = new("half"); + halfOfLocal.Arguments.Add(new ConditionalExpression(new VariableReference("ready"), new VariableReference("x"), Literal.Number(1))); + inferred.Body.Add(new ReturnStatement(halfOfLocal)); + file.Members.Add(inferred); + + string source = new GoGenerator().Generate(file); + StringAssert.Contains(source, "func() float64 {", StringComparison.Ordinal); + Assert.DoesNotContain("func() int {", source, StringComparison.Ordinal); + + ToolchainHarness.InTemporaryDirectory(directory => + { + File.WriteAllText(Path.Combine(directory, "go.mod"), Module); + File.WriteAllText(Path.Combine(directory, "promotion.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 promotion.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."); + }); + } + /// /// 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 075e21d..f1058d4 100644 --- a/Coder/Languages/GoGenerator.cs +++ b/Coder/Languages/GoGenerator.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2026 ktsu-dev contributors +// Copyright (c) 2023-2026 ktsu-dev contributors namespace ktsu.Coder.Languages; @@ -1763,15 +1763,17 @@ private void WriteConditionOperand(AstNode operand, CodeBlocker code) /// Where the statement around it could not take the lowering — nested inside another expression, /// or passed as an argument — what is left is a function literal called where it stands, which is /// the only expression Go has that can choose. It has to name what it answers with and the AST - /// does not say, so the type is read off whichever branch says what it is, and is any when - /// neither does — which is what this generator writes wherever a type was never given. + /// may not say, so the type is the conditional's own expected type if it has one, and otherwise is + /// read off its branches — the wider of the two when both are numbers, since 1 fits a + /// float64 and 2.5 does not fit an int — and is any when neither says, + /// which is what this generator writes wherever a type was never given. /// protected override void GenerateConditionalExpression(ConditionalExpression conditional, CodeBlocker code) { Ensure.NotNull(conditional); Ensure.NotNull(code); - TypeReference? branchType = StaticType(conditional.WhenTrue) ?? StaticType(conditional.WhenFalse); + TypeReference? branchType = StaticType(conditional); code.WriteLine($"func() {(branchType is null ? TypeMappings[UnknownTypeName] : SpellType(branchType))} {{"); code.Indent(); if (branchType is null) diff --git a/Coder/Languages/StandardLanguageGenerator.cs b/Coder/Languages/StandardLanguageGenerator.cs index 311d3c7..a5e9a9f 100644 --- a/Coder/Languages/StandardLanguageGenerator.cs +++ b/Coder/Languages/StandardLanguageGenerator.cs @@ -183,7 +183,7 @@ protected void RegisterStaticType(VariableDeclaration declaration) case ConstructionExpression { Type: not null } construction: return construction.Type; case ConditionalExpression conditional: - return StaticType(conditional.WhenTrue) ?? StaticType(conditional.WhenFalse); + return Promoted(StaticType(conditional.WhenTrue), StaticType(conditional.WhenFalse)); case BinaryExpression binary when binary.Operator is BinaryOperator.Equal or BinaryOperator.NotEqual or BinaryOperator.LessThan @@ -194,7 +194,7 @@ or BinaryOperator.LogicalAnd or BinaryOperator.LogicalOr: return new TypeReference("bool"); case BinaryExpression binary: - return StaticType(binary.Left) ?? StaticType(binary.Right); + return Promoted(StaticType(binary.Left), StaticType(binary.Right)); case VariableReference reference: foreach (StaticTypeScope scope in staticTypeScopes) { @@ -210,6 +210,36 @@ or BinaryOperator.LogicalAnd } } + /// + /// The numeric types in the order a mixed operation widens to, narrowest first. + /// + private static readonly string[] NumericPromotionOrder = ["int", "long", "float", "double"]; + + /// + /// Combines the types of two operands, or of a conditional's two branches, into the type of the + /// whole. + /// + /// The first operand's type, if known. + /// The second operand's type, if known. + /// + /// The wider of two numeric types, so 1 + 0.5 and ready ? 1 : 2.5 are a + /// double as they are in every C-family language; otherwise the first known type. + /// + protected static TypeReference? Promoted(TypeReference? first, TypeReference? second) + { + int firstRank = NumericRank(first); + int secondRank = NumericRank(second); + return firstRank >= 0 && secondRank > firstRank ? second : first ?? second; + } + + /// + /// Reports where a type stands in . + /// + /// The type to place. + /// Its position, or -1 when it is not a plain numeric type. + private static int NumericRank(TypeReference? type) => + type is null ? -1 : Array.IndexOf(NumericPromotionOrder, type.ToString()); + /// /// Reads the type of a return statement from its enclosing function. /// From d265b133ce193afceea1300a598c0604e8979509 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 10:58:28 +0000 Subject: [PATCH 2/2] Keep the StaticType overloads adjacent and use Assert.Contains Addresses the two SonarCloud findings on this PR (S4136, MSTEST0046). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012KjGQ13RexM4WYjuEaBtE6 --- .../GoGeneratedSourceCompilesTests.cs | 2 +- Coder/Languages/StandardLanguageGenerator.cs | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs index c6dd74c..5429bc3 100644 --- a/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs @@ -266,7 +266,7 @@ public void MixedIntegerAndFloatingOperands_PromoteToFloating() file.Members.Add(inferred); string source = new GoGenerator().Generate(file); - StringAssert.Contains(source, "func() float64 {", StringComparison.Ordinal); + Assert.Contains("func() float64 {", source, StringComparison.Ordinal); Assert.DoesNotContain("func() int {", source, StringComparison.Ordinal); ToolchainHarness.InTemporaryDirectory(directory => diff --git a/Coder/Languages/StandardLanguageGenerator.cs b/Coder/Languages/StandardLanguageGenerator.cs index a5e9a9f..2a711a9 100644 --- a/Coder/Languages/StandardLanguageGenerator.cs +++ b/Coder/Languages/StandardLanguageGenerator.cs @@ -210,6 +210,17 @@ or BinaryOperator.LogicalAnd } } + /// + /// Reads the type of a return statement from its enclosing function. + /// + /// The return statement. + /// The function's return type, or null when it has none. + protected TypeReference? StaticType(ReturnStatement statement) + { + Ensure.NotNull(statement); + return staticTypeScopes.TryPeek(out StaticTypeScope? scope) ? scope.ReturnType : null; + } + /// /// The numeric types in the order a mixed operation widens to, narrowest first. /// @@ -240,17 +251,6 @@ or BinaryOperator.LogicalAnd private static int NumericRank(TypeReference? type) => type is null ? -1 : Array.IndexOf(NumericPromotionOrder, type.ToString()); - /// - /// Reads the type of a return statement from its enclosing function. - /// - /// The return statement. - /// The function's return type, or null when it has none. - protected TypeReference? StaticType(ReturnStatement statement) - { - Ensure.NotNull(statement); - return staticTypeScopes.TryPeek(out StaticTypeScope? scope) ? scope.ReturnType : null; - } - private sealed record StaticTypeScope(Dictionary Types, TypeReference? ReturnType); private sealed class StaticTypeScopeGuard(StandardLanguageGenerator generator) : IDisposable