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
84 changes: 84 additions & 0 deletions Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,90 @@ public void ConditionalExpressions_InferParameterTypesAndLowerNestedReturns()
});
}

/// <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.
/// </summary>
/// <remarks>
/// Go reads <c>Pair{</c> in the header of an <c>if</c> as the start of the block, so the literal
/// has to be parenthesised once the clause's own parentheses are left off (issue #105).
/// </remarks>
[TestMethod]
public void ConditionComparingAgainstAStructLiteral_Compiles()
{
if (ToolchainHarness.FindOnPath("version", "go") is null)
{
Assert.Inconclusive("No Go toolchain on the path, so nothing was compiled.");
return;
}

SourceFile file = new("literals");
ClassDeclaration pair = new("Pair") { Kind = TypeDeclarationKind.Struct };
pair.Members.Add(new VariableDeclaration("x", "int"));
pair.Members.Add(new VariableDeclaration("y", "int"));
file.Members.Add(pair);

// The return the issue reproduced with, the struct on the right.
FunctionDeclaration returned = new("returned") { ReturnType = new TypeReference("int") };
returned.Parameters.Add(new Parameter("p", "Pair"));
returned.Body.Add(new ReturnStatement(new ConditionalExpression(
new BinaryExpression(new VariableReference("p"), BinaryOperator.Equal, PairLiteral()),
new LiteralExpression<int>(1),
new LiteralExpression<int>(0))));
file.Members.Add(returned);

// An assignment, the struct on the left and spelled with field names.
FunctionDeclaration assigned = new("assigned") { ReturnType = new TypeReference("int") };
assigned.Parameters.Add(new Parameter("p", "Pair"));
assigned.Body.Add(new VariableDeclaration("r", "int", new LiteralExpression<int>(0)));
ConstructionExpression named = new(new TypeReference("Pair"));
named.Arguments.Add(new MemberInitialiser("x") { Value = new LiteralExpression<int>(1) });
named.Arguments.Add(new MemberInitialiser("y") { Value = new LiteralExpression<int>(2) });
assigned.Body.Add(new AssignmentStatement(
new VariableReference("r"),
new ConditionalExpression(
new BinaryExpression(named, BinaryOperator.NotEqual, new VariableReference("p")),
new LiteralExpression<int>(1),
new LiteralExpression<int>(0))));
assigned.Body.Add(new ReturnStatement(new VariableReference("r")));
file.Members.Add(assigned);

// Inside another expression, where the conditional becomes a function literal called in place.
FunctionDeclaration nested = new("nested") { ReturnType = new TypeReference("int") };
nested.Parameters.Add(new Parameter("p", "Pair"));
nested.Body.Add(new ReturnStatement(new BinaryExpression(
new ConditionalExpression(
new BinaryExpression(new VariableReference("p"), BinaryOperator.Equal, PairLiteral()),
new LiteralExpression<int>(1),
new LiteralExpression<int>(0)),
BinaryOperator.Add,
new LiteralExpression<int>(1))));
file.Members.Add(nested);

string generated = new GoGenerator().Generate(file);
Assert.Contains("p == (Pair{1, 2})", generated, StringComparison.Ordinal, generated);

ToolchainHarness.InTemporaryDirectory(directory =>
{
File.WriteAllText(Path.Combine(directory, "go.mod"), Module);
File.WriteAllText(Path.Combine(directory, "literals.go"), generated);
(int exitCode, string output) = ToolchainHarness.Run("go", "build ./...", directory);
Assert.AreEqual(0, exitCode, $"Go rejected the generated source:{Environment.NewLine}{output}{Environment.NewLine}{generated}");

(int formatted, string differs) = ToolchainHarness.Run("gofmt", "-l literals.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.");
});

static ConstructionExpression PairLiteral()
{
ConstructionExpression literal = new(new TypeReference("Pair"));
literal.Arguments.Add(new LiteralExpression<int>(1));
literal.Arguments.Add(new LiteralExpression<int>(2));
return literal;
}
}

/// <summary>
/// Tests that the arguments an entry point is handed are the ones the user typed, without the
/// program's own path that Go puts first — which is what they are in every other target.
Expand Down
44 changes: 40 additions & 4 deletions Coder/Languages/GoGenerator.cs
Original file line number Diff line number Diff line change
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 @@ -1638,20 +1638,26 @@
/// Only the outermost pair: <c>gofmt</c> takes no view on the ones inside, which are what make
/// the expression unambiguous in the first place.
/// </para>
/// <para>
/// Taking that pair off exposes the operands to the clause, and a struct literal is the one operand
/// that cannot stand there: Go reads <c>Point{</c> in the header of an <c>if</c> as the start of
/// the block, so <c>if p == Point{1, 2} {</c> is a syntax error. The language's own answer is to
/// parenthesise the literal, which <c>gofmt</c> keeps, so <see cref="WriteConditionOperand"/> does.
/// </para>
/// </remarks>
private void WriteCondition(AstNode condition, CodeBlocker code)
{
switch (condition)
{
case BinaryExpression binary:
GenerateInternal(binary.Left, code);
WriteConditionOperand(binary.Left, code);
code.Write($" {GetOperatorSpelling(binary.Operator)} ");
GenerateInternal(binary.Right, code);
WriteConditionOperand(binary.Right, code);
return;

case UnaryExpression unary:
code.Write(GetUnaryOperatorSpelling(unary.Operator));
GenerateInternal(unary.Operand, code);
WriteConditionOperand(unary.Operand, code);
return;

default:
Expand All @@ -1660,6 +1666,25 @@
}
}

/// <summary>
/// Writes one operand of the expression an <c>if</c> tests, parenthesising a struct literal so the
/// clause does not end at its opening brace.
/// </summary>
/// <param name="operand">The operand to write.</param>
/// <param name="code">The writer to emit into.</param>
private void WriteConditionOperand(AstNode operand, CodeBlocker code)
{
if (operand is ConstructionExpression construction && IsCompositeLiteral(construction))
{
code.Write("(");
GenerateInternal(operand, code);
code.Write(")");
return;
}

GenerateInternal(operand, code);
}

/// <inheritdoc/>
/// <remarks>
/// Where the statement around it could not take the lowering — nested inside another expression,
Expand Down Expand Up @@ -1720,7 +1745,7 @@

string type = SpellType(construction.Type);

if (construction.Arguments.Count != 1 || construction.Arguments.Any(argument => argument is MemberInitialiser))
if (IsCompositeLiteral(construction))
{
code.Write(type);
WriteElementList(construction, code, "{", "}", "{}");
Expand All @@ -1732,6 +1757,17 @@
code.Write(")");
}

/// <summary>
/// Reports whether <see cref="GenerateConstructionExpression"/> writes a construction as a
/// composite literal in braces rather than as a conversion in parentheses.
/// </summary>
/// <param name="construction">The construction to classify.</param>
/// <returns>True when it is written with braces.</returns>
private static bool IsCompositeLiteral(ConstructionExpression construction) =>
construction.Type is null
|| construction.Arguments.Count != 1
|| construction.Arguments.Any(argument => argument is MemberInitialiser);

/// <inheritdoc/>
/// <remarks>
/// Go's <c>main</c> takes no arguments and returns nothing, so a program that wants either
Expand Down
Loading