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. ///