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))}]"; }