Skip to content

Add a Go language generator - #59

Merged
matt-edmondson merged 4 commits into
mainfrom
claude/practical-mendel-gg7bjv
Sep 12, 2026
Merged

Add a Go language generator#59
matt-edmondson merged 4 commits into
mainfrom
claude/practical-mendel-gg7bjv

Conversation

@matt-edmondson

@matt-edmondson matt-edmondson commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Adds GoGenerator, a seventh target alongside C#, C++, C, Rust, Python and JavaScript.

C was interesting because it had almost nothing to map onto and Rust because it had a feature for nearly everything. Go is interesting for a third reason: most of what the AST says, Go already had a way to say — and the handful it doesn't, it left out deliberately, so each gap has an answer rather than a shrug.

The AST says Go writes
ClassDeclaration a struct for the data, with its methods declared beside it — Go has no member list, a method is a package-level func with a receiver
a member function a value receiver where IsReadOnly is set and a pointer receiver where it is not. That is the promise C++ spells as a trailing const, made by the shape of the declaration: a value receiver is a copy, so a method that took one cannot change the caller's value even by mistake
BaseType on a struct an embedded field, whose members are promoted — as near as Go comes to inheritance, and nearer than any other target without it manages
an interface an interface, satisfied by whatever has the methods and declared by nothing. BaseType on one is an embedded interface, which is Rust's supertrait exactly
a constructor func NewPoint(…) Point, built from the initialiser list. With nothing to build from it needs no fallback: Point{} is the zero value, which every Go type has and which is what the declaration asked for
a destructor Close — the io.Closer convention, and the one thing it does not share with a destructor is that somebody has to call it
an operator a method named from the AST's own word for it: Add, LessThan, Negate
a conversion String() string when the target is a string, which is fmt.Stringer and not merely a naming convention; To<Type> otherwise
an enumeration type Colour int plus a const block of prefixed constants, since Go's constants share the package's scope the way C's do
CompileTimeAssertion var _ = map[bool]struct{}{false: {}, cond: {}} — a map literal's keys must be distinct and a constant key is checked while compiling, so this is a compile error exactly when the condition is false. No import, no build tag, no macro
ConditionalExpression an if statement, lowered into whichever statement holds it — see below
a namespace the file's package clause, plus a note where the name has more than one part, since a Go package is named for one directory
list / dict []T and map[K]V, out of the grammar rather than from a package; a type with arguments gets square brackets

What is left over is visibility, and it is the one thing here that no generator can write. Go exports a name whose first letter is a capital and has no keyword at all. Renaming a declaration to match what it asked for would not rename the references to it — the rule every generator here keeps — so a name that disagrees with its declared visibility gets a note saying so, and one that already agrees gets nothing.

And the const. A Go constant is a number, a string or a boolean the compiler worked out, never a struct or a slice, so a constant table is a var with a note. That is the language's meaning of constant rather than a gap in it.

The conditional, which Go has no expression for

ConditionalExpression arrived on main while this was open, and Go is the furthest of any target from it. The C family writes ?:, Python reorders the operands, Rust makes if an expression — Go's if is a statement and yields nothing, so there is no expression to spell one as. The inherited ?: would not have been an unidiomatic spelling; it would not have parsed.

So it is lowered to the statement around it, which is what anybody writing Go by hand does, and which needs nobody to name the branches' type:

  • in a return → if cond { return a } then return b (one arm: the first branch leaves the statement)
  • in an assignment → if cond { t = a } else { t = b } (both arms: falling through would leave the target holding what it held before)
  • in a declaration naming its type → var x T then the assignment form — the only one of the three that knows what the branches are

Where the surrounding statement cannot take it — nested in another expression, or passed as an argument — what is left is a function literal called where it stands, whose result type is read off whichever branch says what it is and is any when neither does.

Two consequences of Go having exactly one formatter fell out of this: } and else must share a line (a line break between them ends the statement), and an if clause carries no parentheses — the AST has no precedence, so an operator applied to operands is parenthesised wherever it stands, and gofmt strips exactly those from a control clause and nowhere else.

The formatting is part of the output

Go has one formatter, everybody runs it, and a generated file gofmt disagrees with is a diff the first time anybody opens it. So the generator writes what gofmt would write — including the tab, and including lining up the columns of a struct's fields and a constant block — and GoGeneratedSourceCompilesTests runs gofmt over the output and fails if it would rewrite a byte. No other target here can have that test, and it is what caught both of the formatting rules above.

Testing

  • GoGeneratorTests — 60 tests pinning which feature each part of a declaration became.
  • GoGeneratedSourceCompilesTests — compiles the output with a real go build, then checks gofmt agrees with it. Go refuses several things the other two only warn about (an unused import, an unused local, a name declared twice, a const it cannot evaluate), each of which a spelling test would pass. A driver file in the same package uses every declaration, which is what proves the two claims that are otherwise only claims: that Circle satisfies Shape without saying so anywhere, and that an embedded Point answers Sum.
    I verified both halves have teeth — dropping the column padding makes gofmt reject it, and naming a unary - after the binary one makes go reject Subtract redeclared.
  • Full suite: 692 passed, 0 failed.

Changes outside the new generator

All of these remove a duplicate or stop one appearing:

  • LanguageGeneratorBase.IndentString becomes an overridable property rather than a const. It was already a language-specific choice (four spaces "because PEP 8 asks for it"); Go is the one target that does not get a say. GenerateReturnStatement and GenerateAssignmentStatement become virtual, so the conditional lowering can reach the statement that knows the type.
  • StandardLanguageGenerator gains three things more than one generator needed: the run-of-members walk, the "two undocumented members of a kind stay in one block" rule, and the operator names built from the AST's own vocabulary. The C family and Rust each held a copy of the first two; CGenerator held the third. A list's padding becomes overridable, because Go writes Point{X: 1} and everything else writes { x }.
  • The unary operator names are built separately from the binary ones. A type declaring both operator- forms would otherwise declare Subtract twice, which Go refuses — and the compile test is what found it.
  • CompiledExemplar gains the declarations the Rust and Go compile tests are both checked against, Pick included, so the claim stays one AST, three real compilers rather than becoming two parallel fixtures.
  • Coder.Editor/EditorSyntax.cs hands the highlighter the Rust and Go definitions. It ships fifteen languages and neither is one of them; an id it has never heard of is not an error to it, so the preview pane would have drawn generated Go in one colour and said nothing. The existing EveryGenerator_ProducesSourceTheHighlighterClassifies test catches exactly that.

README, CLAUDE.md and docs/design.md are updated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7

Adds GoGenerator, a seventh target alongside C#, C++, C, Rust, Python and
JavaScript.

Go answers most of what the AST says with something it already had. A type
is a struct with its methods declared beside it; an interface is an
interface that nothing declares it implements; a base type is an embedded
field, whose members are promoted. A member that promises not to modify
what it is called on takes a value receiver and one that does takes a
pointer receiver — the promise C++ writes as a trailing const, made by the
shape of the declaration rather than by a modifier on it.

What Go left out it left out on purpose, so each gap has an answer rather
than a note. An operator is a method named from the AST's own word for it,
a constructor is New<Type>, a destructor is the Close a caller defers, an
enumeration is a named type and a block of iota constants, and a
compile-time assertion is a map literal whose duplicate constant key Go
refuses. What is left is visibility, which Go says with the first letter of
the name and no keyword: a name that disagrees with what it asked for gets
a note, because renaming it would not rename the references to it.

The output is already what gofmt would write — tab indentation and the
columns of a struct's fields and a constant block lined up — which is
checked by running gofmt over it. Go has one formatter and everybody runs
it, so a generated file it disagrees with is a diff the first time anyone
opens it.

Shared rather than duplicated:

- LanguageGeneratorBase.IndentString becomes an overridable property, since
  Go's indentation is not this generator's to pick.
- StandardLanguageGenerator gains the run-of-members walk, the
  two-undocumented-members-of-a-kind grouping rule and the operator names
  built from the AST's vocabulary; the C family and Rust now reach them
  there instead of each holding a copy. A list's padding becomes
  overridable, because Go writes Point{X: 1}.
- CompiledExemplar gains the declarations the Rust and Go compile tests are
  both checked against, so the claim stays "one AST, three real compilers".

Coder.Editor registers a Go highlighter definition, since the highlighter
ships fifteen languages and Go is not one of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7
Comment thread Coder.Test/Languages/GoGeneratorTests.cs Fixed
CodeQL's "missed opportunity to use Select": the loop bound a declaration
only to generate from it, so it now iterates what it was after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7
Main added CallExpression, ExpressionStatement and ConditionalExpression
while this branch was open. GoGenerator derives from
StandardLanguageGenerator, so it inherited all three dispatch cases and two
of the three defaults were already right for it: Go spells a member call
`a.b(c)` and ends a statement with the line break.

The third was not, and Go is the furthest of any target from it.
ConditionalExpression is a choice between two values; Go's `if` is a
statement and yields nothing, so there is no expression to spell one as.
The inherited `?:` would not have been an unidiomatic spelling, it would
not have parsed.

So it is lowered to the statement around it, which is what anybody writing
Go by hand does and which needs nobody to name the branches' type:

- in a return, `if cond { return a }` then `return b` — one arm, since the
  first branch leaves the statement
- in an assignment, `if cond { t = a } else { t = b }` — both arms, since
  falling through would leave the target holding what it held before
- in a declaration naming its type, `var x T` then the assignment form,
  which is the only one of the three that knows what the branches are

Where the statement around it cannot take that — nested in another
expression, or passed as an argument — what is left is a function literal
called where it stands, whose result type is read off whichever branch says
what it is and is `any` when neither does.

Two consequences of Go having one formatter:

- `}` and `else` share a line, because a line break between them ends the
  statement. The braces are written out rather than opened as a scope.
- an `if` clause carries no parentheses. The AST has no precedence so an
  operator applied to operands is parenthesised wherever it stands, and
  gofmt removes exactly those parentheses from a control clause and nowhere
  else — which the formatting assertion catches.

GenerateReturnStatement and GenerateAssignmentStatement become virtual so
the lowering can reach the statement that holds the type.
CompiledExemplar gains `Pick`, main's member that calls another for its
effect and then chooses between two values, so both compilers check both
nodes rather than the Rust test alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7
The quality gate passed, but three of the sixty-three are worth not leaving
behind.

S3776, reported as critical, is mine: GenerateSourceFile took the guard that
stops a language whose imports are written elsewhere from getting a blank
line per import, and that pushed its cognitive complexity to 18. What a file
depends on is a thing of its own, so it is now a method of its own and the
four steps of writing a file read as four steps.

S1192 asked for a constant where `string` repeats, which it does six times
in GoGenerator because the generator asks three different questions of it:
which Go type a name maps to, whether a type is already a view of what it
holds, and whether a conversion is the one the standard library prints a
value with. The map key stays a literal - that one is the AST's name for a
type rather than Go's spelling of it, and they only happen to agree.

MSTEST0037 is the assertion that says what it means on a count, which this
suite adopted while this branch was open.

The sixty MSTEST0046 reports are left as they are, which is the same call
the suite already made: it calls StringAssert.Contains in every one of its
string assertions and has no bare Assert.Contains anywhere, so writing these
the other way would read as a mistake rather than as an improvement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit f77a8a3 into main Sep 12, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/practical-mendel-gg7bjv branch September 12, 2026 10:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants