Add a Go language generator - #59
Merged
Merged
Conversation
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
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
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



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.
ClassDeclarationstructfor the data, with its methods declared beside it — Go has no member list, a method is a package-levelfuncwith a receiverIsReadOnlyis set and a pointer receiver where it is not. That is the promise C++ spells as a trailingconst, 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 mistakeBaseTypeon a structinterface, satisfied by whatever has the methods and declared by nothing.BaseTypeon one is an embedded interface, which is Rust's supertrait exactlyfunc 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 forClose— theio.Closerconvention, and the one thing it does not share with a destructor is that somebody has to call itAdd,LessThan,NegateString() stringwhen the target is a string, which isfmt.Stringerand not merely a naming convention;To<Type>otherwisetype Colour intplus aconstblock of prefixed constants, since Go's constants share the package's scope the way C's doCompileTimeAssertionvar _ = 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 macroConditionalExpressionifstatement, lowered into whichever statement holds it — see belowpackageclause, plus a note where the name has more than one part, since a Go package is named for one directorylist/dict[]Tandmap[K]V, out of the grammar rather than from a package; a type with arguments gets square bracketsWhat 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 avarwith a note. That is the language's meaning of constant rather than a gap in it.The conditional, which Go has no expression for
ConditionalExpressionarrived onmainwhile this was open, and Go is the furthest of any target from it. The C family writes?:, Python reorders the operands, Rust makesifan expression — Go'sifis 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:
if cond { return a }thenreturn b(one arm: the first branch leaves the statement)if cond { t = a } else { t = b }(both arms: falling through would leave the target holding what it held before)var x Tthen the assignment form — the only one of the three that knows what the branches areWhere 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
anywhen neither does.Two consequences of Go having exactly one formatter fell out of this:
}andelsemust share a line (a line break between them ends the statement), and anifclause carries no parentheses — the AST has no precedence, so an operator applied to operands is parenthesised wherever it stands, andgofmtstrips 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
gofmtdisagrees with is a diff the first time anybody opens it. So the generator writes whatgofmtwould write — including the tab, and including lining up the columns of a struct's fields and a constant block — andGoGeneratedSourceCompilesTestsrunsgofmtover 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 realgo build, then checksgofmtagrees with it. Go refuses several things the other two only warn about (an unused import, an unused local, a name declared twice, aconstit 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: thatCirclesatisfiesShapewithout saying so anywhere, and that an embeddedPointanswersSum.I verified both halves have teeth — dropping the column padding makes
gofmtreject it, and naming a unary-after the binary one makesgorejectSubtractredeclared.Changes outside the new generator
All of these remove a duplicate or stop one appearing:
LanguageGeneratorBase.IndentStringbecomes an overridable property rather than aconst. 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.GenerateReturnStatementandGenerateAssignmentStatementbecomevirtual, so the conditional lowering can reach the statement that knows the type.StandardLanguageGeneratorgains 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;CGeneratorheld the third. A list's padding becomes overridable, because Go writesPoint{X: 1}and everything else writes{ x }.operator-forms would otherwise declareSubtracttwice, which Go refuses — and the compile test is what found it.CompiledExemplargains the declarations the Rust and Go compile tests are both checked against,Pickincluded, so the claim stays one AST, three real compilers rather than becoming two parallel fixtures.Coder.Editor/EditorSyntax.cshands 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 existingEveryGenerator_ProducesSourceTheHighlighterClassifiestest catches exactly that.README,
CLAUDE.mdanddocs/design.mdare updated.🤖 Generated with Claude Code
https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7