diff --git a/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs
index 4dc1831..41a3d62 100644
--- a/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs
+++ b/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs
@@ -130,6 +130,59 @@ public void GeneratedSource_CompilesAndIsFormatted()
});
}
+ ///
+ /// 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.
+ ///
+ ///
+ /// Go placed a field by 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).
+ ///
+ [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()
{
diff --git a/Coder.Test/Languages/GoGeneratorTests.cs b/Coder.Test/Languages/GoGeneratorTests.cs
index ab18d59..a945af4 100644
--- a/Coder.Test/Languages/GoGeneratorTests.cs
+++ b/Coder.Test/Languages/GoGeneratorTests.cs
@@ -34,6 +34,42 @@ public void Identity_IsGo()
Assert.AreEqual("go", Generator.FileExtension);
}
+ ///
+ /// 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.
+ ///
+ [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}");
+ StringAssert.Contains(generated, "// defaults to 5: Go has no field initialisers, so only a constructor sets it");
+ }
+
+ ///
+ /// 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.
+ ///
+ [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");
+ Assert.IsFalse(generated.Contains("\tRatio", StringComparison.Ordinal), generated);
+ }
+
///
/// 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.
diff --git a/Coder/Languages/GoGenerator.cs b/Coder/Languages/GoGenerator.cs
index c5cfcf9..b19bbfa 100644
--- a/Coder/Languages/GoGenerator.cs
+++ b/Coder/Languages/GoGenerator.cs
@@ -595,19 +595,47 @@ private void GenerateTypeDeclaration(ClassDeclaration classDecl, string name, Co
GenerateStruct(classDecl, name, code);
WriteInterfaceAssertions(classDecl, name, code);
- foreach (FieldDeclaration field in classDecl.Members.OfType().Where(field => field.IsStatic))
+ foreach (FieldDeclaration field in classDecl.Members.OfType().Where(IsPackageLevelField))
{
code.NewLine();
WriteStorage(Join(name, field.Name ?? UnnamedMember), field, code);
}
- foreach (FunctionDeclaration function in classDecl.Members.OfType())
+ IReadOnlyList outerDefaults = _instanceFieldDefaults;
+ _instanceFieldDefaults = [.. classDecl.Members.OfType()
+ .Where(field => !IsPackageLevelField(field) && field.InitialValue is not null)];
+ try
{
- code.NewLine();
- GenerateFunction(function, code, name);
+ foreach (FunctionDeclaration function in classDecl.Members.OfType())
+ {
+ code.NewLine();
+ GenerateFunction(function, code, name);
+ }
+ }
+ finally
+ {
+ _instanceFieldDefaults = outerDefaults;
}
}
+ ///
+ /// 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.
+ ///
+ private IReadOnlyList _instanceFieldDefaults = [];
+
+ ///
+ /// 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.
+ ///
+ /// The field to place.
+ /// True for a static or a constant field.
+ ///
+ /// 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.
+ ///
+ private static bool IsPackageLevelField(FieldDeclaration field) => field.IsStatic || field.IsConstant;
+
///
/// Asserts, at compile time, that a type implements what it said it implements.
///
@@ -746,7 +774,7 @@ private IEnumerable StructFields(ClassDeclaration classDecl)
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;
@@ -773,6 +801,11 @@ private AlignedLine Field(string? name, TypeReference? type, Visibility visibili
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);
}
@@ -1153,6 +1186,14 @@ private void WriteConstructorBody(FunctionDeclaration funcDecl, string? enclosin
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);
@@ -1901,6 +1942,18 @@ private void WriteAligned(IReadOnlyList lines, CodeBlocker code)
}
}
+ ///
+ /// Writes an expression to a string, for the places a comment has to quote one.
+ ///
+ /// The expression to write.
+ /// Its Go source.
+ private string GenerateExpression(Expression expression)
+ {
+ using CodeBlocker inline = CodeBlocker.Create(IndentString);
+ GenerateInternal(expression, inline);
+ return inline.ToString().TrimEnd('\r', '\n');
+ }
+
///
/// Spells a type in Go.
///