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

/// <summary>
/// Tests that a constant field keeps its value and that a constructor starts an initialised
/// instance field at its value, by running the generated code rather than reading it.
/// </summary>
/// <remarks>
/// Go placed a field by <see cref="FieldDeclaration.IsStatic"/> alone, so a constant became a
/// struct member and its value went nowhere, and an instance field's initialiser was dropped from
/// the constructor: both printed 0 (issue #111).
/// </remarks>
[TestMethod]
public void ConstantAndInitialisedFields_KeepTheirValuesWhenRun()
{
if (ToolchainHarness.FindOnPath("version", "go") is null)
{
Assert.Inconclusive("No Go toolchain on the path, so nothing was compiled.");
return;
}

SourceFile file = new("main");
ClassDeclaration cfg = new("Cfg") { Kind = TypeDeclarationKind.Struct };
cfg.Members.Add(new FieldDeclaration("Ratio", new TypeReference("double")) { IsConstant = true, InitialValue = Literal.DecimalValue(0.5) });
cfg.Members.Add(new FieldDeclaration("Hits", new TypeReference("int")) { InitialValue = Literal.Number(5) });
cfg.Members.Add(new FunctionDeclaration("Cfg") { Kind = FunctionKind.Constructor });
file.Members.Add(cfg);

const string driver = """
package main

import "fmt"

func main() {
built := NewCfg()
fmt.Println(CfgRatio, built.Hits)
}

""";

ToolchainHarness.InTemporaryDirectory(directory =>
{
File.WriteAllText(Path.Combine(directory, "go.mod"), Module);
File.WriteAllText(Path.Combine(directory, "driver.go"), driver);
File.WriteAllText(Path.Combine(directory, "cfg.go"), new GoGenerator().Generate(file));

(int exitCode, string output) = ToolchainHarness.Run("go", "run .", directory);
Assert.AreEqual(0, exitCode, $"go rejected the generated source:{Environment.NewLine}{output}");
Assert.AreEqual("0.5 5", output.Trim());

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

[TestMethod]
public void ConditionalExpressions_InferParameterTypesAndLowerNestedReturns()
{
Expand Down
36 changes: 36 additions & 0 deletions Coder.Test/Languages/GoGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,42 @@
Assert.AreEqual("go", Generator.FileExtension);
}

/// <summary>
/// Tests that a constructor sets an initialised instance field it does not set itself, leaves the
/// one it does set to its own value, and that the struct member says where its value comes from.
/// </summary>
[TestMethod]
public void Constructor_SetsInstanceFieldDefaultsItDoesNotSetItself()
{
ClassDeclaration cfg = new("Cfg") { Kind = TypeDeclarationKind.Struct };
cfg.Members.Add(new FieldDeclaration("Hits", new TypeReference("int")) { InitialValue = Literal.Number(5) });
cfg.Members.Add(new FieldDeclaration("Misses", new TypeReference("int")) { InitialValue = Literal.Number(7) });
FunctionDeclaration constructor = new("Cfg") { Kind = FunctionKind.Constructor };
constructor.Initialisers.Add(new MemberInitialiser("Misses", Literal.Number(1)));
cfg.Members.Add(constructor);

string generated = Generator.Generate(cfg);

StringAssert.Contains(generated, "return Cfg{Misses: 1, Hits: 5}");

Check warning on line 53 in Coder.Test/Languages/GoGeneratorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaEUgf3DuQQGkwG8puCt&open=AaEUgf3DuQQGkwG8puCt&pullRequest=187
StringAssert.Contains(generated, "// defaults to 5: Go has no field initialisers, so only a constructor sets it");

Check warning on line 54 in Coder.Test/Languages/GoGeneratorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaEUgf3DuQQGkwG8puCu&open=AaEUgf3DuQQGkwG8puCu&pullRequest=187
}

/// <summary>
/// Tests that a constant field that is not static is still written as package-level storage,
/// with its value, rather than as a struct member at its zero.
/// </summary>
[TestMethod]
public void ConstantField_IsPackageLevelEvenWhenNotStatic()
{
ClassDeclaration cfg = new("Cfg") { Kind = TypeDeclarationKind.Struct };
cfg.Members.Add(new FieldDeclaration("Ratio", new TypeReference("double")) { IsConstant = true, InitialValue = Literal.DecimalValue(0.5) });

string generated = Generator.Generate(cfg);

StringAssert.Contains(generated, "const CfgRatio float64 = 0.5");

Check warning on line 69 in Coder.Test/Languages/GoGeneratorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaEUgf3DuQQGkwG8puCv&open=AaEUgf3DuQQGkwG8puCv&pullRequest=187
Assert.IsFalse(generated.Contains("\tRatio", StringComparison.Ordinal), generated);
}

/// <summary>
/// Tests that a function's statements carry no terminator, since Go's lexer supplies the one its
/// grammar wants and gofmt deletes any that were written.
Expand Down
63 changes: 58 additions & 5 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 @@ -595,19 +595,47 @@
GenerateStruct(classDecl, name, code);
WriteInterfaceAssertions(classDecl, name, code);

foreach (FieldDeclaration field in classDecl.Members.OfType<FieldDeclaration>().Where(field => field.IsStatic))
foreach (FieldDeclaration field in classDecl.Members.OfType<FieldDeclaration>().Where(IsPackageLevelField))
{
code.NewLine();
WriteStorage(Join(name, field.Name ?? UnnamedMember), field, code);
}

foreach (FunctionDeclaration function in classDecl.Members.OfType<FunctionDeclaration>())
IReadOnlyList<FieldDeclaration> outerDefaults = _instanceFieldDefaults;
_instanceFieldDefaults = [.. classDecl.Members.OfType<FieldDeclaration>()
.Where(field => !IsPackageLevelField(field) && field.InitialValue is not null)];
try
{
code.NewLine();
GenerateFunction(function, code, name);
foreach (FunctionDeclaration function in classDecl.Members.OfType<FunctionDeclaration>())
{
code.NewLine();
GenerateFunction(function, code, name);
}
}
finally
{
_instanceFieldDefaults = outerDefaults;
}
}

/// <summary>
/// The instance fields of the struct whose functions are being written that say what they
/// start at, which its constructors set because a Go struct field cannot say so itself.
/// </summary>
private IReadOnlyList<FieldDeclaration> _instanceFieldDefaults = [];

/// <summary>
/// Reports whether a field belongs to its type rather than to each value of it, and so is
/// written as package-level storage rather than as a struct member.
/// </summary>
/// <param name="field">The field to place.</param>
/// <returns>True for a static or a constant field.</returns>
/// <remarks>
/// A constant belongs to the type in every other target, and a struct member has nowhere to
/// hold its value, so treating one as an instance field would write it at its zero.
/// </remarks>
private static bool IsPackageLevelField(FieldDeclaration field) => field.IsStatic || field.IsConstant;

/// <summary>
/// Asserts, at compile time, that a type implements what it said it implements.
/// </summary>
Expand Down Expand Up @@ -746,7 +774,7 @@
yield return Field(field.Name, field.Type, field.Visibility);
break;

case FieldDeclaration field when !field.IsStatic:
case FieldDeclaration field when !IsPackageLevelField(field):
yield return Field(field.Name, field.Type, field.Visibility, field);
break;

Expand All @@ -773,6 +801,11 @@
notes.Add((CommentPrefix, note));
}

if (declaration?.InitialValue is Expression initialValue)
{
notes.Add((CommentPrefix, $"defaults to {GenerateExpression(initialValue)}: Go has no field initialisers, so only a constructor sets it"));
}

return new AlignedLine(notes, name ?? UnnamedMember, SpellType(type ?? new TypeReference(UnknownTypeName)), declaration);
}

Expand Down Expand Up @@ -1153,6 +1186,14 @@
value.Arguments.Add(initialiser);
}

// A field the declaration gave a starting value, and this constructor did not set, starts
// at that value rather than at its zero, as it would in every other target.
foreach (FieldDeclaration field in _instanceFieldDefaults
.Where(field => !funcDecl.Initialisers.Any(initialiser => initialiser.Name == field.Name)))
{
value.Arguments.Add(new MemberInitialiser(field.Name ?? UnnamedMember, (Expression)field.InitialValue!.DeepClone()));
}

code.Write("return ");
GenerateConstructionExpression(value, code);
EndStatement(code);
Expand Down Expand Up @@ -1901,6 +1942,18 @@
}
}

/// <summary>
/// Writes an expression to a string, for the places a comment has to quote one.
/// </summary>
/// <param name="expression">The expression to write.</param>
/// <returns>Its Go source.</returns>
private string GenerateExpression(Expression expression)
{
using CodeBlocker inline = CodeBlocker.Create(IndentString);
GenerateInternal(expression, inline);
return inline.ToString().TrimEnd('\r', '\n');
}

/// <summary>
/// Spells a type in Go.
/// </summary>
Expand Down
Loading