Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2023-2026 ktsu-dev contributors
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Coder.Test.Languages;

Expand Down Expand Up @@ -229,6 +229,59 @@ public void ConditionalExpressions_InferParameterTypesAndLowerNestedReturns()
});
}

/// <summary>
/// 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 <c>float64</c>, and a local
/// inferred from <c>1 + 0.5</c> is known to be one where a later conditional uses it (issue #152).
/// </summary>
[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.");
});
}

/// <summary>
/// Tests that a condition comparing against a struct literal compiles in every position a
/// conditional is lowered to an <c>if</c>, and is still what <c>gofmt</c> writes.
Expand Down
10 changes: 6 additions & 4 deletions Coder/Languages/GoGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2023-2026 ktsu-dev contributors
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Coder.Languages;

Expand Down Expand Up @@ -269,7 +269,7 @@
/// holds one.
/// </remarks>
protected override string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "math.NaN()" : value > 0 ? "math.Inf(1)" : "math.Inf(-1)";

Check warning on line 272 in Coder/Languages/GoGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 272 in Coder/Languages/GoGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 272 in Coder/Languages/GoGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 272 in Coder/Languages/GoGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 272 in Coder/Languages/GoGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 272 in Coder/Languages/GoGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 272 in Coder/Languages/GoGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 272 in Coder/Languages/GoGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <inheritdoc/>
/// <remarks>
Expand Down Expand Up @@ -1763,15 +1763,17 @@
/// 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 <c>any</c> 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 <c>1</c> fits a
/// <c>float64</c> and <c>2.5</c> does not fit an <c>int</c> — and is <c>any</c> when neither says,
/// which is what this generator writes wherever a type was never given.
/// </remarks>
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)
Expand Down
34 changes: 32 additions & 2 deletions Coder/Languages/StandardLanguageGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
{
Expand All @@ -221,6 +221,36 @@ or BinaryOperator.LogicalAnd
return staticTypeScopes.TryPeek(out StaticTypeScope? scope) ? scope.ReturnType : null;
}

/// <summary>
/// The numeric types in the order a mixed operation widens to, narrowest first.
/// </summary>
private static readonly string[] NumericPromotionOrder = ["int", "long", "float", "double"];

/// <summary>
/// Combines the types of two operands, or of a conditional's two branches, into the type of the
/// whole.
/// </summary>
/// <param name="first">The first operand's type, if known.</param>
/// <param name="second">The second operand's type, if known.</param>
/// <returns>
/// The wider of two numeric types, so <c>1 + 0.5</c> and <c>ready ? 1 : 2.5</c> are a
/// <c>double</c> as they are in every C-family language; otherwise the first known type.
/// </returns>
protected static TypeReference? Promoted(TypeReference? first, TypeReference? second)
{
int firstRank = NumericRank(first);
int secondRank = NumericRank(second);
return firstRank >= 0 && secondRank > firstRank ? second : first ?? second;
}

/// <summary>
/// Reports where a type stands in <see cref="NumericPromotionOrder"/>.
/// </summary>
/// <param name="type">The type to place.</param>
/// <returns>Its position, or -1 when it is not a plain numeric type.</returns>
private static int NumericRank(TypeReference? type) =>
type is null ? -1 : Array.IndexOf(NumericPromotionOrder, type.ToString());

private sealed record StaticTypeScope(Dictionary<string, TypeReference> Types, TypeReference? ReturnType);

private sealed class StaticTypeScopeGuard(StandardLanguageGenerator generator) : IDisposable
Expand Down
Loading