diff --git a/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs
index 35ebe6c..5429bc3 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);
+ Assert.Contains("func() float64 {", source, 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..2a711a9 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)
{
@@ -221,6 +221,36 @@ or BinaryOperator.LogicalAnd
return staticTypeScopes.TryPeek(out StaticTypeScope? scope) ? scope.ReturnType : null;
}
+ ///
+ /// 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());
+
private sealed record StaticTypeScope(Dictionary Types, TypeReference? ReturnType);
private sealed class StaticTypeScopeGuard(StandardLanguageGenerator generator) : IDisposable