THRIFT-6197: Fix Go code generation for typedefs of structs and forward typedefs - #3812
slachiewicz wants to merge 1 commit into
Conversation
106bd76 to
b8f57e6
Compare
b8f57e6 to
1b36db8
Compare
1b36db8 to
88a3422
Compare
fishy
left a comment
There was a problem hiding this comment.
changing typedef to alias in go is a much bigger change that can have consequences and backward-incompatibilities.
88a3422 to
ab34128
Compare
|
Thanks for pushing back on this — it sent me to measure instead of argue, and three things should have been in the PR from the start. The generated code does not compile today. For a typedef of a struct the IDL actually uses, master emits THRIFT-3037, THRIFT-3491 and THRIFT-4901 are three people reporting those same errors against three different versions. For the used case there is no working code to break. THRIFT-5685 does not come back. That is the one I would worry about too, since it is why the last THRIFT-5601 fix was reverted. Generating 5685's own IDL on this branch gives the block that ticket calls expected: var Bar_Bar_DEFAULT *Foo
func (p *Bar) GetBar() *Foo { ... }The Three of 325 files change. Regenerating every IDL under The one real compatibility case is a typedef of a struct the IDL declares but never uses. That compiles today, so hand-written Go can hold one, and the pointer moves from inside the alias to outside it. Separately: I was wrong to suggest THRIFT-5463 might be the same defect. It is not, and I have removed that line from THRIFT-6197. If you would still rather this not land unflagged in a minor, I am happy to put the alias behind a This comment was created with AI assistance. |
|
Unrelated find while running the Go suite against this branch: This comment was created with AI assistance. |
|
The discussion of whether to use type alias in go had came up before and rejected. The reason we don't want it is it will lose some compile time enforcement. For example, someone can have this typedef in thrift: typedef i64 TimestampMillisecondsWith the current not-type-alias implementation, the generated go code is: It's convertible with int64 but not the same type. so when using it, you mostly are required to use an explicit typecasting like this obj.StartTime = package.TimestampMilliseconds(t.UnixMilli())while this won't compilre: obj.StartTime = t.UnixMilli()as now imagine if someone has a bug and used obj.StartTime = package.TimestampMilliseconds(t.UnixNano())You lost that when using type alias. |
|
That holds for base types, and this PR leaves them alone. The alias is gated on Structs have no equivalent enforcement to lose. Do you have a pointer to the earlier discussion? I could not find it on the tracker. If it covered typedef-of-struct specifically rather than typedefs in general, I would like to read it before pushing this further. This comment was created with AI assistance. |
|
that discussion probably came up in one of the ticket comments. @dcelasun what do you think? this creates a divergence between typedef implementations in go that I don't like, but maybe it's acceptable? |
I don't like it either, but it does address a real problem and I can't think of a cleaner solution. As long as this is limited to Definitely needs a |
|
@fishy, the drift question is the right one, so I measured it rather than argued it. In short: the alias does remove a distinction. Exactly two of the changes it causes are silent rather than compile errors, and both are bounded to a shape that is rare and already close to unusable. Everything below is reproducible from this branch. How I measuredTwo compilers built from source, compared on identical inputs:
The reported defects, on each reporter's own IDLThe IDL below is verbatim from each ticket. For THRIFT-4901 I also used the two files from the reporter's own test branch, johnboiles@c2a6220, unmodified.
The illegal-IDL case Jens recorded on THRIFT-5685, a forward-declared exception in a struct field, compiles before and after and generates identically. This PR neither legitimizes nor breaks it. The typedef example from your review, in both declaration ordersThis is the part of your review I most wanted to check, because if the alias reached base types you would be right to block it.
The alias is gated on The drift surfaceTo find out whether code that already exists can drift, I generated a package whose struct typedefs are declared but never used in a field or a signature. That's the only shape that compiles on master, so it's the only shape hand-written Go can already depend on. The same hand-written consumer file then went through both compilers' output.
Four of those are compile errors, which is the kind of break a caller can see. Two are silent: the Change scope, measured independentlyRegenerating every IDL under Compiling both corpora is the stronger check. The baseline fails in five packages: The Go gate runs clean with the patched compiler. All 26 packages in the Beyond the tickets, one more IDL covers a const of an aliased struct, field defaults, union and exception aliases, an alias of an alias, One correction to the PR descriptionThe scope table says the What the other bindings doGenerating THRIFT-3491's IDL for each language shows which bindings give a struct typedef an identity of its own.
None of them give a struct typedef a nominal identity. For base typedefs it inverts: Go alone gives a defined type, and this PR keeps that. The nominal alternativeIf the distinction matters more than the ergonomics, the alternative is a defined type over the struct with the exported surface forwarded. Here it is prototyped by hand for the cross-file shape, which is the hard one: type NominalTThingA testa.ThingA
func (p *NominalTThingA) Read(ctx context.Context, iprot thrift.TProtocol) error {
return (*testa.ThingA)(p).Read(ctx, iprot)
}
// Write, Equals, String, and GetValue forward the same way
var _ thrift.TStruct = (*NominalTThingA)(nil)That compiles, satisfies What I proposeLand the alias, and widen the release note from the pointer move to the two silent classes above, naming the One request stands from my earlier reply: if the discussion that rejected aliases covered typedef-of-struct specifically, I would like to read it. The tracker carries the opposite on record, in THRIFT-3037 and THRIFT-3491, where Duru Can Celasun proposed Go aliases for these typedefs in 2017 and offered a PR. This comment was created with AI assistance. |
|
Four IDL corpora from outside this repository, generated with the parent commit and with this branch, to see what the change moves in the wild. One of them reproduces the defect on its own IDL, and the other three are byte-identical.
uber/thriftrw-go reproduces it independentlyThe test IDL under
That is THRIFT-4901's error text arriving from a third party's IDL, and Two caveats on method. The facebook/fbthrift fixtures confirm parity, not much moreOf 217 fixture sources, 8 parse under the Apache grammar and 209 don't, because they use the modern dialect: The failures are clean and correctly positioned, which is worth asserting even though this change doesn't touch that path. For the The Jaeger and the Evernote SDK don't moveBoth generate byte-identical Go, and every generated package builds, including the What this addsThe in-repo measurements show that the corpus doesn't move. These show the same thing on four projects that don't share this repository's test IDL, and one of them turns the defect into something other than a synthetic reproduction: an independent project's checked-in IDL that produces a Go package which doesn't compile. This comment was created with AI assistance. |
9077dfc to
f780929
Compare
|
Rebased on master (f780929) to pick up the TNonblockingServerTest fix from THRIFT-6244 behind the AppVeyor failure. The diff is unchanged (same patch-id). |
f0d7d2e to
b210021
Compare
fishy
left a comment
There was a problem hiding this comment.
looks good to me. the failed cpp tests don't seem related
…rd typedefs
Client: go
A typedef of a struct was emitted as a defined type over a pointer,
type Alias *Inner. That type carries none of the struct's methods, so the
generated package did not compile at all: the read path assigns &Inner{}
to it and then calls Read, Write and Equals on it. Three reporters filed
the same three errors over eleven years - THRIFT-3037, THRIFT-3491 and
THRIFT-4901 - for the include, service-signature and struct-field shapes.
Emit a Go type alias, type Alias = Inner. An alias names the struct itself
and keeps its method set, which is what a Thrift typedef means. Because
the old output never built, no working code can depend on it, with one
exception: a typedef of a struct that the IDL declares but never uses does
compile today, and hand-written Go treating that alias as a pointer has to
move the pointer out.
The <Name>Ptr helper that generate_typedef emits for every typedef is
skipped for an alias of a struct. Nothing in the generator calls it; it
exists so an optional field of a base typedef can be filled with
TimestampPtr(123). A struct is only ever held through a pointer and gets
no such helper, so the alias gets none either.
Also stop the forward-typedef unwrapping at the first declared type. The
old single if jumped past every intermediate typedef and dropped the name
the field asked for (THRIFT-5489, THRIFT-5601). The loop leaves
forward-declared structs alone, so their getters keep returning a pointer
and THRIFT-5685 does not come back - UsesForwardStruct in the test IDL is
there to hold that.
Do not undo this in favour of a defined type: type Alias Inner has no
methods either, and making it work would mean generating forwarding Read,
Write and Equals for every typedef of a struct.
Regenerating every IDL under test/, lib/go/test/ and tutorial/ with both
compilers changes 3 of 325 generated files; every changed line is a
typedef-of-struct declaration or the Ptr helper that went with it.
CHANGES.md carries the change under Breaking Changes for 0.25.0, and
lib/go/README.md gains a note on typedefs of structs.
Co-Authored-By: Google Gemini <noreply@google.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
b210021 to
f2ab5a6
Compare
A typedef of a struct is now generated as a Go type alias (
type Alias = Inner) instead of a defined type over a pointer (type Alias *Inner), and forward-typedef unwrapping stops at the first declared type instead of jumping to the underlying one.The backward-compatibility question is the right one to ask of this change, and the sections below are the measurements that answer it.
Why the alias
type Alias *Inneris a distinct Go type whose underlying type is a pointer. It carries none of the struct's methods, and the generated read path immediately needs them:So the package does not build. Three reporters filed those same errors independently — THRIFT-3037 (2015), THRIFT-3491 (2015), THRIFT-4901 (2019) — against three different Thrift versions, for the include, service-signature and struct-field shapes of one defect.
A Go alias names the struct itself and shares its method set, which is what a Thrift typedef means. The alias gets exactly what the struct gets: the
<Name>Ptrhelper thatgenerate_typedefemits for every other typedef is skipped for an alias of a struct, since structs have no such helper and one taking the struct by value would only copy it. Typedefs of base types keep theirs. The alternative,type Alias Inner, has no methods either; making it work would mean generating forwardingRead,WriteandEqualsfor every typedef of a struct.THRIFT-5685 does not come back
The forward-typedef half of this is the fix that was reverted once. THRIFT-5601 was fixed, the fix made a forward-declared struct field a value instead of a pointer,
bar.GetBar().GetFoo()stopped compiling, and THRIFT-5685 reverted it in 0.18.1.Generating THRIFT-5685's own IDL with this branch produces the output that ticket calls expected:
The
whileloop stops at the first declared type, and a forward-declared struct resolves to a struct, which takes the pointer branch.UsesForwardStructinTypedefStructTest.thriftandforwardStructGetterChainin the Go test hold this shape so a future change has to trip over it.Blast radius
Regenerating every IDL under
test/,lib/go/test/andtutorial/with the master generator and with this branch produces 325 files. Three differ:test/TypedefTest.thrifttype MyStruct *TypedefTestStruct→= TypedefTestStruct, and theMyStructPtrhelper is no longer generated. The alias is unused in fields.lib/go/test/DuplicateImportsTest.thrifttype A *common.A→= common.A, same forB, and theAPtrandBPtrhelpers are no longer generated.lib/go/test/StructKeyTest.thrift--gen go:struct_key_entries, which is what the Makefile uses: the declaration and the droppedKeyAliasPtrhelper, and field types stay[]thrift.MapEntry[*Key, string]. Under default generation the map key types also move, frommap[KeyAlias]stringtomap[*KeyAlias]string, along with the DEFAULT vars, the getters, and themake()calls. That output did not compile before, withk.Write undefined (type KeyAlias has no field or method Write), so it is a further fix rather than a regression.Outside the default-generation
StructKeyTestcase described in the table, every changed line is a typedef-of-struct declaration or the<Name>Ptrhelper that went with it. Nothing else in the corpus moves.As an out-of-tree check, jaeger-idl generates byte-identical Go with both compilers, and all eight generated packages build. The only typedefs in that project's history are
typedef string BaggageKeyandtypedef i32 MaxValueLength, both base types, which this change leaves alone.lib/go/test/tests/struct_key_test.gocarried a comment explaining that a struct key behind a typedef has to be written as*Key"because the generatedtype KeyAlias *Keyhas no methods of its own". That workaround is no longer needed; the comment is updated and the code is unchanged.What can still break
A typedef of a struct that the IDL declares but never uses is the only shape that compiles today, so it is the only shape hand-written Go can already depend on. Compiling the same consumer code against both compilers' output, these change:
var x InnerAlias = &Inner{}InnerAliasPtrhelper*Innercase *Inner:andcase *InnerAlias:duplicate case *InnerAlias in type switchtype W struct{ InnerAlias }embedded field type cannot be a pointerWsatisfiesthrift.TStructby promotion%Tandreflect.TypeOf(x).String()for a value held as anInnerAliaspkg.InnerAlias*pkg.InnerThe first four are compile errors. The last is silent, and so is the loss of distinction between two aliases of one struct: after this change a value of one is assignable to the other.
DuplicateImportsTestandTypedefTestare that shape. All of it is worth a release note.Tickets
includeRelease note
CHANGES.mdcarries the change under Breaking Changes for 0.25.0, andlib/go/README.mdgains a "A note about typedefs of structs" section covering the alias, the dropped<Name>Ptrhelper and what moves for hand-written code. The struct-key note in the same README no longer says the typedef carries none of the struct's methods.Tests
TypedefStructTest.thriftandTypedefIncludeTest.thriftcover a typedef declared after use, a struct declared after use, a typedef of a struct in the same file and across an include, a typedef of a typedef, a typedef of an exception, a typedef of a container, and a typedef'd struct in a service signature, which also exercises the-remotestub.tests/typedef_struct_test.goadds compile-time identity assertions and binary, compact and JSON round trips.This change was created with AI assistance.