From 25711f97f8f40b0563852b8159e74b7f65f49a2b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 13:34:13 +0000 Subject: [PATCH] Write a Go type down over the whole declaration, not its first line (closes #63) [patch] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A type parameter on a ClassDeclaration is written down rather than written, which is the decision CLAUDE.md states. The emission only half kept it: the type was declared without parameters, and then a field, a parameter and a result went on spelling them — `held T` and `Boxed[T]` beside a `Boxed` that takes none. That is `undefined: T` and `Boxed is not a generic type`, neither of which the text shows. The write-down now reaches the whole declaration. While a type's members are written, its parameters are names the file does not hold, so a type spelled for one is spelled `any`, and a mention of the type itself carries no arguments. A type parameter on a function is a real one and reaches none of this. The note above the declaration says what became of them, so the comment and the file under it agree. GoGeneratedSourceCompilesTests grows the case it was missing — a struct with a parameterised field, a static factory answering the type, and a driver using both — so the answer is compiled rather than asserted, which is the only way either error is visible. GoGeneratorTests pins the same shape headlessly, for where no Go toolchain is on the path. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KGA295odwYgzWvncYaLEu3 --- CLAUDE.md | 8 +- .../GoGeneratedSourceCompilesTests.cs | 47 +++++- Coder.Test/Languages/GoGeneratorTests.cs | 56 +++++++ Coder/Languages/GoGenerator.cs | 140 +++++++++++++++--- 4 files changed, 230 insertions(+), 21 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d165d46..c29ca63 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -126,7 +126,13 @@ source in seven target languages. The solution uses: Go spells one, `Implements` being exactly a Go constraint interface, and only for a *function*: a method on a generic type needs the parameters in three places and spelled two ways (`NewPoint` for the constructor's name, `Point[T]` for its receiver and result), so a generic type is written - down instead. C++ writes `template ` and notes every constraint, the standard concepts + down instead — **and so is everything written over it**, which is the half that makes the decision + hold: a member typed for a parameter is typed `any`, and a mention of the type carries no + arguments, because a type declared without parameters takes none. Writing the type down and then + spelling `Point[T]` anyway is neither decision, only `undefined: T` and `Point is not a generic + type` — errors nothing in the text shows, which is why `GoGeneratedSourceCompilesTests` compiles a + type parameter on a `ClassDeclaration` rather than asserting one. C++ writes `template ` and notes every constraint, the standard concepts needing an include the AST does not carry. C, Python and JavaScript write the whole parameter down. `RustGeneratedSourceCompilesTests` compiles a generic struct with a load-bearing bound, so the `impl` repetition is checked rather than asserted. diff --git a/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs index 1833d9f..5cc51e7 100644 --- a/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs @@ -81,10 +81,15 @@ func use() int { circle := Circle{Point: built, radius: 1.0} circle.Close() + // Boxed was written down rather than written, so nothing about it is spelled over a + // parameter: it is held as the most general thing there is and read back as one. + boxed := BoxedHold(3) + // circle.Sum is Point's, reached through the embedded field rather than inherited. return built.Sum() + zeroed.y + first.x + named.x + circle.Sum() + built.Pick() + built.Add(zeroed).x + built.Negate().x + Corner.y + - int(built.ToFloat64()) + int(shape.area()) + int(ColourGreen) + measure("a", []int{1}) + int(built.ToFloat64()) + int(shape.area()) + int(ColourGreen) + measure("a", []int{1}) + + boxed.Held().(int) } """; @@ -162,6 +167,7 @@ private static SourceFile Exemplar() file.Members.Add(CompiledExemplar.OriginTable()); file.Members.Add(Corner()); file.Members.Add(CompiledExemplar.Measure()); + file.Members.Add(Boxed()); file.Members.Add(new CompileTimeAssertion { Condition = "unsafe.Sizeof(Point{}) == 2*unsafe.Sizeof(0)", @@ -171,6 +177,45 @@ private static SourceFile Exemplar() return file; } + /// + /// Builds a type written over a parameter, which Go writes down rather than writes. + /// + /// The declaration. + /// + /// The one shape no other case here has, and the one a compiler is needed for: a type parameter + /// on a is written down — the decision CLAUDE.md states — + /// so the file holds a Boxed that takes no parameters, and every place the declaration + /// spelled one has to agree with that. A field typed T, a result typed Boxed<T> + /// and a value built as one are the three positions that can disagree, and all three are here: + /// a file naming a parameter the type never declared is undefined: T, and one subscripting + /// a type that takes none is Boxed is not a generic type. Neither is visible in the text. + /// + private static ClassDeclaration Boxed() + { + ClassDeclaration boxed = new("Boxed") { Kind = TypeDeclarationKind.Struct }; + boxed.Documentation.Add("Holds one of whatever it was given."); + boxed.TypeParameters.Add(TypeParameter.Parse("T : Stringer")); + boxed.Members.Add(new FieldDeclaration("held", "T")); + + ConstructionExpression built = new(TypeReference.Parse("Boxed")); + built.Arguments.Add(new MemberInitialiser("held", new VariableReference("value"))); + + FunctionDeclaration hold = new("Hold") + { + ReturnType = TypeReference.Parse("Boxed"), + IsStatic = true, + }; + hold.Parameters.Add(new Parameter("value", "T")); + hold.Body.Add(new ReturnStatement(built)); + boxed.Members.Add(hold); + + FunctionDeclaration held = new("Held") { ReturnType = "T", IsReadOnly = true }; + held.Body.Add(new ReturnStatement(new VariableReference("self.held"))); + boxed.Members.Add(held); + + return boxed; + } + /// /// Builds a value constructed from a type and two plain arguments, which is the shape neither of /// the other two composite literals here has. diff --git a/Coder.Test/Languages/GoGeneratorTests.cs b/Coder.Test/Languages/GoGeneratorTests.cs index b3a267b..cb1a639 100644 --- a/Coder.Test/Languages/GoGeneratorTests.cs +++ b/Coder.Test/Languages/GoGeneratorTests.cs @@ -141,6 +141,62 @@ public void GenericType_IsWrittenWithSquareBrackets() StringAssert.Contains(Generator.Generate(function), ") Result[Handle, Error]", StringComparison.Ordinal); } + /// + /// Tests that a type written over parameters has them written down everywhere rather than on its + /// first line only. + /// + /// + /// The decision is that a generic type is written down, and it only holds if the rest of + /// the declaration agrees with it: a file declaring Boxed and then spelling a member + /// T and a result Boxed[T] is undefined: T and Boxed is not a generic + /// type, neither of which the text shows. + /// compiles the same shape; this is here so the + /// answer is still pinned where no Go toolchain is on the path. + /// + [TestMethod] + public void TypeParametersOfAType_AreWrittenDownEverywhere() + { + ClassDeclaration boxed = new("Boxed") { Kind = TypeDeclarationKind.Struct }; + boxed.TypeParameters.Add(TypeParameter.Parse("T : Stringer")); + boxed.Members.Add(new FieldDeclaration("held", "T")); + + FunctionDeclaration hold = new("Hold") + { + ReturnType = TypeReference.Parse("Boxed"), + IsStatic = true, + }; + hold.Parameters.Add(new Parameter("value", "T")); + boxed.Members.Add(hold); + + string written = Generator.Generate(boxed); + + Assert.Contains("// over T : Stringer", written, StringComparison.Ordinal); + Assert.Contains("type Boxed struct", written, StringComparison.Ordinal); + Assert.Contains("held any", written, StringComparison.Ordinal); + Assert.Contains("func BoxedHold(value any) Boxed", written, StringComparison.Ordinal); + Assert.DoesNotContain( + "Boxed[", + written, + "Boxed was declared without parameters, so nothing in the file may subscript it."); + } + + /// + /// Tests that a type parameter on a function stays a real one, which is the half of the decision + /// that is not a write-down. + /// + [TestMethod] + public void TypeParametersOfAFunction_AreWrittenAsGenerics() + { + FunctionDeclaration first = new("first") { ReturnType = "T" }; + first.TypeParameters.Add(TypeParameter.Parse("T : any")); + first.Parameters.Add(new Parameter("values") { Type = new TypeReference("T") { IsArray = true } }); + + Assert.Contains( + "func first[T any](values []T) T", + Generator.Generate(first), + StringComparison.Ordinal); + } + /// /// Tests that both of the AST's indirections become the one Go has. /// diff --git a/Coder/Languages/GoGenerator.cs b/Coder/Languages/GoGenerator.cs index 8318dba..2bee170 100644 --- a/Coder/Languages/GoGenerator.cs +++ b/Coder/Languages/GoGenerator.cs @@ -32,6 +32,16 @@ namespace ktsu.Coder.Languages; /// var — which is what the language means by constant, rather than a gap in it. /// /// +/// A type parameter is the one place Go has the feature and this generator declines it, and the +/// decline reaches the whole declaration rather than its first line. A method on a generic type needs +/// the parameters in three places and spelled two ways, so a generic type is written down — +/// and then so is everything written over it: a member typed for a parameter is typed any, and +/// a mention of the type carries no arguments, because a type declared without parameters takes none. +/// Writing the type down and then spelling Type[T] anyway would be neither decision, only a +/// file that reads correctly and does not build. A parameter on a function is a real one and is +/// written as one. +/// +/// /// Visibility is the one thing Go says in a way no generator can write: a name is exported when its /// first letter is a capital, and there is no keyword. Renaming a declaration to match what it asked /// for would not rename the references to it, which is the rule every generator here keeps, so a @@ -164,6 +174,26 @@ public class GoGenerator : StandardLanguageGenerator /// private bool insideInterface; + /// + /// The names the type being written was written down over. + /// + /// + /// A type parameter on a type declaration is written down rather than written, so the file holds + /// no declaration of the name — which makes every later mention of it a name nothing declares. + /// These are the names that have to be answered with something the file does hold. + /// + private HashSet writtenDownParameters = []; + + /// + /// The name of the type those parameters were written down from, when there is one. + /// + /// + /// Held beside the names because a type written without parameters is a type that takes none, so + /// a mention of it carries none either. Knowing which name that is, is the whole of the + /// difference between Boxed and a generic type the file really does declare. + /// + private string? writtenDownType; + /// /// Gets the unique identifier for this language generator. /// @@ -478,6 +508,14 @@ protected override void GenerateNamespaceDeclaration(NamespaceDeclaration namesp /// becomes a package-level var, because Go has no static data member and a note in place /// of the table would lose it. /// + /// + /// A type written over parameters is written down, and that decision reaches the whole + /// declaration rather than its first line: while its members are being written, the parameters + /// are names the file does not hold, so stands for the + /// duration and answers them. A type declared inside this one is + /// written beside it and is not written over these parameters, which is why it is emitted before + /// the scope opens. + /// /// protected override void GenerateClassDeclaration(ClassDeclaration classDecl, CodeBlocker code) { @@ -495,6 +533,31 @@ protected override void GenerateClassDeclaration(ClassDeclaration classDecl, Cod code.NewLine(); } + HashSet enclosingParameters = writtenDownParameters; + string? enclosingType = writtenDownType; + + writtenDownParameters = [.. classDecl.TypeParameters.Select(parameter => parameter.Name)]; + writtenDownType = writtenDownParameters.Count > 0 ? name : null; + + try + { + GenerateTypeDeclaration(classDecl, name, code); + } + finally + { + writtenDownParameters = enclosingParameters; + writtenDownType = enclosingType; + } + } + + /// + /// Writes a type declaration, with whatever it was written over already written down. + /// + /// The declaration to emit. + /// The name it is written under. + /// The writer to emit into. + private void GenerateTypeDeclaration(ClassDeclaration classDecl, string name, CodeBlocker code) + { if (classDecl.IsSpecialisation) { GenerateDocumentation(classDecl, code); @@ -545,7 +608,7 @@ protected override void GenerateClassDeclaration(ClassDeclaration classDecl, Cod /// method set, and one declared on the value is in both. /// /// - private static void WriteInterfaceAssertions(ClassDeclaration classDecl, string name, CodeBlocker code) + private void WriteInterfaceAssertions(ClassDeclaration classDecl, string name, CodeBlocker code) { // Not for a type this generator wrote down rather than wrote. The assertion names the type, // and the name of a generic type is not the name of a type -- `(*Mass)(nil)` where the @@ -565,6 +628,36 @@ private static void WriteInterfaceAssertions(ClassDeclaration classDecl, string } } + /// + /// Writes down what a type declaration is written over, and what became of it. + /// + /// The declaration being emitted. + /// The name it is written under. + /// The writer to emit into. + /// + /// Go has generics, and a generic type is still written down here. A method on one needs its + /// parameters in three places and spelled two ways — NewPoint for the constructor's name + /// but Point[T] for its receiver and for what the constructor answers with — so making a + /// type generic is a change to how this generator writes a whole type rather than to how it + /// writes one line. A function of its own has none of that, and is written as a generic one. + /// + /// The second line is the half that was missing, and it is what makes the note true of the file + /// under it. Writing a type down and then referring to it as generic anyway is neither decision: + /// it is undefined: T, which is why the parameters stop being names here and say so. + /// + /// + private void WriteWrittenDownParameters(ClassDeclaration classDecl, string name, CodeBlocker code) + { + WriteTypeParametersDown(classDecl.TypeParameters, code); + + if (classDecl.TypeParameters.Count > 0) + { + WriteInexpressible( + code, + $"each of those is {TypeMappings[UnknownTypeName]} below, and {name} itself takes none"); + } + } + /// /// Reports whether a member declares a type rather than data or behaviour. /// @@ -596,12 +689,7 @@ private void GenerateStruct(ClassDeclaration classDecl, string name, CodeBlocker WriteAnnotations(classDecl.Annotations, code); WriteTypePromises(classDecl, code); - // Go has generics, and a generic type is still written down here. A method on one needs its - // parameters in three places and spelled two ways -- `NewPoint` for the constructor's name - // but `Point[T]` for its receiver and for what the constructor answers with -- so making a - // type generic is a change to how this generator writes a whole type rather than to how it - // writes one line. A function of its own has none of that, and is written as a generic one. - WriteTypeParametersDown(classDecl.TypeParameters, code); + WriteWrittenDownParameters(classDecl, name, code); WriteExportNote(name, classDecl.Visibility, code); List fields = [.. StructFields(classDecl)]; @@ -688,7 +776,7 @@ private void GenerateInterface(ClassDeclaration classDecl, string name, CodeBloc GenerateDocumentation(classDecl, code); WriteAnnotations(classDecl.Annotations, code); WriteTypePromises(classDecl, code); - WriteTypeParametersDown(classDecl.TypeParameters, code); + WriteWrittenDownParameters(classDecl, name, code); WriteExportNote(name, classDecl.Visibility, code); List members = [.. classDecl.Members.Where(member => member is FunctionDeclaration or FieldDeclaration)]; @@ -860,14 +948,14 @@ private void WriteSignature(FunctionDeclaration funcDecl, string name, CodeBlock /// declaration instead. /// /// - private static string SpellTypeParameters(IEnumerable parameters) + private string SpellTypeParameters(IEnumerable parameters) { string[] declared = [.. parameters.Select(SpellOneTypeParameter)]; return declared.Length == 0 ? string.Empty : $"[{string.Join(", ", declared)}]"; } - private static string SpellOneTypeParameter(TypeParameter parameter) + private string SpellOneTypeParameter(TypeParameter parameter) { string[] required = [ @@ -896,7 +984,7 @@ .. parameter.Constraints /// A constructor answers the type it builds, which the declaration does not carry — it is named /// after the type rather than typed by it, the same as everywhere else in the AST. /// - private static string SpellResult(FunctionDeclaration funcDecl, string? enclosingType) => + private string SpellResult(FunctionDeclaration funcDecl, string? enclosingType) => funcDecl.Kind == FunctionKind.Constructor && enclosingType is not null ? enclosingType : SpellType(funcDecl.ReturnType ?? new TypeReference("void")); @@ -941,7 +1029,7 @@ private static bool TakesReceiver(FunctionDeclaration funcDecl, string? enclosin /// no Point.zero in Go for a reference to have named in the first place. /// /// - private static string SpellFunctionName(FunctionDeclaration funcDecl, string? enclosingType) + private string SpellFunctionName(FunctionDeclaration funcDecl, string? enclosingType) { string bare = funcDecl.Kind switch { @@ -991,7 +1079,7 @@ private static string OperatorName(FunctionDeclaration funcDecl) /// standard library that prints anything. Every other conversion is ToType, which /// is only a name. /// - private static string ConversionName(TypeReference? target) + private string ConversionName(TypeReference? target) { string spelled = SpellType(target ?? new TypeReference(UnknownTypeName)); return string.Equals(spelled, StringTypeName, StringComparison.Ordinal) @@ -1559,7 +1647,7 @@ protected override void GenerateConditionalExpression(ConditionalExpression cond /// /// The expression being written. /// The type as Go writes it. - private static string BranchType(ConditionalExpression conditional) => + private string BranchType(ConditionalExpression conditional) => TypeOfValue(conditional.WhenTrue) ?? TypeOfValue(conditional.WhenFalse) ?? TypeMappings[UnknownTypeName]; @@ -1569,7 +1657,7 @@ private static string BranchType(ConditionalExpression conditional) => /// /// The value to read. /// The type as Go writes it, or null where the value does not say. - private static string? TypeOfValue(AstNode value) => value switch + private string? TypeOfValue(AstNode value) => value switch { LiteralExpression or AstLeafNode => StringTypeName, LiteralExpression or AstLeafNode => "int", @@ -1775,7 +1863,7 @@ private static void WriteAligned(IReadOnlyList lines, CodeBlocker c /// a map each hold a pointer already, so a pointer to one is a pointer to a pointer and nobody /// means that. /// - private static string SpellType(TypeReference type) + private string SpellType(TypeReference type) { string core = SpellCoreType(type); @@ -1802,7 +1890,7 @@ private static bool IsView(string spelled) => /// /// The type to spell. /// The value form. - private static string SpellCoreType(TypeReference type) + private string SpellCoreType(TypeReference type) { string core = SpellTypeName(type); return type.IsArray ? $"[]{core}" : core; @@ -1822,8 +1910,17 @@ private static string SpellCoreType(TypeReference type) /// A type with arguments is written with square brackets, which is where Go put its generics and /// the one place its spelling of a familiar thing surprises a reader of the others. /// + /// + /// The exception is the type this generator wrote down, and it is the whole of what + /// writing one down costs. A parameter of a type declaration names nothing in the file, so what + /// was written over it is written as the most general type there is; and a type declared without + /// parameters takes none, so a mention of it carries none. Spelling either the way the + /// declaration did would produce a file that reads correctly and does not build — + /// undefined: T, and Boxed is not a generic type. A type parameter on a + /// function is a real one and reaches none of this. + /// /// - private static string SpellTypeName(TypeReference type) + private string SpellTypeName(TypeReference type) { string unknown = TypeMappings[UnknownTypeName]; @@ -1839,9 +1936,14 @@ private static string SpellTypeName(TypeReference type) : $"map[{StringTypeName}]{unknown}"; } + if (writtenDownParameters.Contains(type.Name)) + { + return unknown; + } + string name = TypeMappings.TryGetValue(type.Name, out string? mapped) ? mapped : type.Name; - return type.TypeArguments.Count == 0 + return type.TypeArguments.Count == 0 || string.Equals(name, writtenDownType, StringComparison.Ordinal) ? name : $"{name}[{string.Join(", ", type.TypeArguments.Select(SpellType))}]"; }