From 104babb262c1ef5a684e3d397cef31063bfe987c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 03:41:54 +0000 Subject: [PATCH 1/2] Keep Go constant-field values and apply instance-field initialisers GoGenerator placed a field by IsStatic alone, so a constant that was not static became a struct member and its value was dropped, and an instance field's initialiser was lost from the generated constructor. A constant now goes to package-level storage, as static fields already did and as Rust does, and each constructor sets the initialised instance fields it does not set itself. The struct member carries a note saying where its default is applied, since a bare composite literal still starts it at zero. Fixes ktsu-dev/Coder#111 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JgoVAth3nGZUgbKrSgVCYX --- .../GoGeneratedSourceCompilesTests.cs | 53 +++++++++++++++ Coder.Test/Languages/GoGeneratorTests.cs | 36 ++++++++++ Coder/Languages/GoGenerator.cs | 65 +++++++++++++++++-- 3 files changed, 149 insertions(+), 5 deletions(-) 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..821a1d4 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,16 @@ 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) + { + if (!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 +1944,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. /// From cb45547871e97ddbe88753a54c349ce9e2d93f0b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 03:47:41 +0000 Subject: [PATCH 2/2] Filter the constructor's default initialisers with Where Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JgoVAth3nGZUgbKrSgVCYX --- Coder/Languages/GoGenerator.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Coder/Languages/GoGenerator.cs b/Coder/Languages/GoGenerator.cs index 821a1d4..b19bbfa 100644 --- a/Coder/Languages/GoGenerator.cs +++ b/Coder/Languages/GoGenerator.cs @@ -1188,12 +1188,10 @@ private void WriteConstructorBody(FunctionDeclaration funcDecl, string? enclosin // 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) + foreach (FieldDeclaration field in _instanceFieldDefaults + .Where(field => !funcDecl.Initialisers.Any(initialiser => initialiser.Name == field.Name))) { - if (!funcDecl.Initialisers.Any(initialiser => initialiser.Name == field.Name)) - { - value.Arguments.Add(new MemberInitialiser(field.Name ?? UnnamedMember, (Expression)field.InitialValue!.DeepClone())); - } + value.Arguments.Add(new MemberInitialiser(field.Name ?? UnnamedMember, (Expression)field.InitialValue!.DeepClone())); } code.Write("return ");