Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <typename T>` 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 <typename
T>` 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.
Expand Down
47 changes: 46 additions & 1 deletion Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

""";
Expand Down Expand Up @@ -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)",
Expand All @@ -171,6 +177,45 @@ private static SourceFile Exemplar()
return file;
}

/// <summary>
/// Builds a type written over a parameter, which Go writes down rather than writes.
/// </summary>
/// <returns>The declaration.</returns>
/// <remarks>
/// The one shape no other case here has, and the one a compiler is needed for: a type parameter
/// on a <see cref="ClassDeclaration"/> is written down — the decision <c>CLAUDE.md</c> states —
/// so the file holds a <c>Boxed</c> that takes no parameters, and every place the declaration
/// spelled one has to agree with that. A field typed <c>T</c>, a result typed <c>Boxed&lt;T&gt;</c>
/// 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 <c>undefined: T</c>, and one subscripting
/// a type that takes none is <c>Boxed is not a generic type</c>. Neither is visible in the text.
/// </remarks>
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<T>"));
built.Arguments.Add(new MemberInitialiser("held", new VariableReference("value")));

FunctionDeclaration hold = new("Hold")
{
ReturnType = TypeReference.Parse("Boxed<T>"),
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;
}

/// <summary>
/// Builds a value constructed from a type and two plain arguments, which is the shape neither of
/// the other two composite literals here has.
Expand Down
56 changes: 56 additions & 0 deletions Coder.Test/Languages/GoGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,62 @@ public void GenericType_IsWrittenWithSquareBrackets()
StringAssert.Contains(Generator.Generate(function), ") Result[Handle, Error]", StringComparison.Ordinal);
}

/// <summary>
/// Tests that a type written over parameters has them written down everywhere rather than on its
/// first line only.
/// </summary>
/// <remarks>
/// The decision is that a generic <em>type</em> is written down, and it only holds if the rest of
/// the declaration agrees with it: a file declaring <c>Boxed</c> and then spelling a member
/// <c>T</c> and a result <c>Boxed[T]</c> is <c>undefined: T</c> and <c>Boxed is not a generic
/// type</c>, neither of which the text shows.
/// <see cref="GoGeneratedSourceCompilesTests"/> compiles the same shape; this is here so the
/// answer is still pinned where no Go toolchain is on the path.
/// </remarks>
[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<T>"),
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.");
}

/// <summary>
/// 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.
/// </summary>
[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);
}

/// <summary>
/// Tests that both of the AST's indirections become the one Go has.
/// </summary>
Expand Down
Loading