Repository navigation
story(gen-go): every receiver the generator accepts yields a package that compiles, whatever the layout - #431
Conversation
…e that compiles, whatever the layout The generator now composes every method under a placeholder receiver and, once the package exists, type-checks it with stubbed imports and renames whichever of its own names a method reaches that the configured receiver would shadow: the codec.Reader/codec.Writer parameter, an import (aliased), a local, or a package-level helper. A parameter's prose moves with it. Predeclared identifiers cannot move, so every one is refused when the options are parsed, for every layout. `r` and `w` now work, so example/receivables uses `r` instead of the `a` it had to work around with. Closes #428 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
A new test helper (reached()) misclassifies imported package members as “package” declarations, weakening the intended coverage assertion and potentially masking regressions.
1 open issue
2 files not reviewed
cmd/cpybkc-gen-go/generate.go: Generated fileexample/receivables/receivables/codec.go: Generated file
What changed in this PR
This PR updates cpybkc-gen-go so any accepted receiver value yields generated Go packages that compile across layouts by composing methods under a safe placeholder receiver and then renaming colliding in-scope identifiers after the full package is generated and type-checked (with stubbed imports).
Changes:
- Generate methods under a placeholder receiver (
receiver__) and post-process sources viaclearReceiverto avoid receiver collisions. - Refuse predeclared identifiers (
len,nil,error, …) as receiver values at option-parse time and document the rule/rationale. - Update the receivables example to use
receiver=rand regenerate generated code accordingly.
| File | Description |
|---|---|
| example/receivables/receivables/codec.go | Regenerated example output showing receiver rename behavior (r receiver, r0 param). |
| example/receivables/cpybkc.json | Switches example receiver workaround from a to r. |
| cmd/cpybkc-gen-go/receiver.go | Adds clearReceiver implementation plus supporting rename/type-check utilities. |
| cmd/cpybkc-gen-go/receiver_test.go | Adds tests covering accepted/refused receivers and compilation/behavior invariants. |
| cmd/cpybkc-gen-go/README.md | Updates receiver option rule and documents the rename-based approach. |
| cmd/cpybkc-gen-go/generate.go | Composes generated methods under placeholder receiver, then calls clearReceiver. |
| cmd/cpybkc-gen-go/args.go | Refuses predeclared receivers and adds isPredeclared. |
| cmd/cpybkc-gen-go/args_test.go | Adds parsing tests for accepted “spent” receivers and refusal of predeclared identifiers. |
| .gitignore | Ignores _receivers scratch output from receiver tests. |
Review effort: Lite
| case obj.Parent() == types.Universe: | ||
| kind = "predeclared" | ||
| case obj.Pkg() != nil && obj.Parent() == obj.Pkg().Scope(): | ||
| kind = "package" |
There was a problem hiding this comment.
Right: codec.SignUnsigned's selected name resolves to a constant in codec's package scope, so it would have filled the package bucket by itself. Fixed in 0856500. reached() now counts a name as package only when it is declared in the generated package's own scope, and anything an import declares falls through to the skip. The assertion still holds on orders, which reaches refuseStaircase, resized, fresh, zeroFill and literalsFor.
…package names a method reaches Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Closes #428
The approach: rename, not refuse
randware the letters a shop picks for a record decoder, and exactly the ones the methods' parameters are spelled. Refusing them would refuse the receiver the option most often exists to set. So the generator now moves its own names out of the receiver's way:receiver__). Nothing munged from a copybook can have that spelling, and the pass checks that nothing else does either.clearReceiver(cmd/cpybkc-gen-go/receiver.go) type-checks the package's own files with every import stubbed. It doesn't need the imported packages' source, only which declaration each identifier names. Every object a method reaches that is spelled like the receiver gets renamed at its declaration and every use. That covers a parameter, a local, a package-level helper, or an import, which gets an alias. The new name is the receiver's spelling plus the lowest number nothing in the package carries. Fields and methods are only ever reached through a selector, so they're left alone.lenhas no other spelling.isReceiverNamenow refuses every predeclared identifier, whether or not a method uses it today, when the options are parsed and for every layout. The refusal names the receiver and says why.With
receiver=r:With
receiver=codec,codec.go, and onlycodec.go, importscodec0 "github.com/Zaba505/cobol-go/codec".The default receiver
xcollides with nothing, so every golden package is byte-for-byte unchanged.Acceptance criteria
receivervaluecpybkc-gen-goaccepts yields a generated package that compiles and passes its generated tests, for every layout. The README andisReceiverName's doc comment state which approach was chosen and why.If the generator refuses: n/a. Renaming was chosen. The one refusal left, predeclared identifiers, happens at option parse and names the receiver and the reason (TestParseRefusesAPredeclaredReceiver, which covers every predeclared identifier).receiver=r,w,codecanderreach generate a package that compiles and whose generated tests pass (TestTheLettersAShopPicksPassTheirGeneratedTestsrunsgo teston them). The layout isorders, and the test asserts that its methods reach a parameter, an import, a local, a package-level declaration and a predeclared identifier, and something spelled each of those four receivers.TestEveryNameInScopeInsideAMethodIsRefusedOrCompilestype-checks each of the 8 golden layouts. It collects every name in scope anywhere inside a generated method: parameter, locals of every block, file imports, package declarations, universe. Each name is either refused for a layout-independent reason (exported, predeclared or blank) or generated as the receiver and type-checked against the realcodec, tests included. A new local or import the generator starts emitting is covered automatically. Mutation check: with the rename disabled, this test fails for 50+ derived names across every layout (i0,k0,n1…n4,lits,count1,matched2,holds2,fresh,resized,zeroFill,refuseStaircase,literalsFor,bytes,fmt,codec,err,r,w, …).example/receivables/cpybkc.json: the workaround"receiver": "a"is replaced with"r", the letter it wanted in the first place, and the example is regenerated. Nothing in the example's README mentions the receiver. This meansgo test ./...and the example regeneration test now exercise a renamed parameter on every CI run.receiverrow incmd/cpybkc-gen-go/README.mdstates the rule as implemented, and the rationale paragraph explains the rename. There is no gen-go SPEC that states the rule.Judgment calls
complex,iota, …). Refusing only the ones currently used would make acceptance depend on what the generator happens to emit for a layout, which is exactly what the story rules out.rrather than keepingaor dropping the option.aexisted only as the workaround this story removes, andrputs the rename under the existing example tests.go testrun builds incmd/cpybkc-gen-go/_receivers/inside the module, followinginternal/conformance/goadapter/_scratch. The leading underscore keeps it out of./..., the run removes it afterwards, and.gitignorecovers one left behind by a killed run.Verification
go test ./cmd/cpybkc-gen-go/... ./example/...passes, with the example regeneration test run uncached.dagger call cifrom an ordinary clone of the branch exits 0 and itscitrace passed. The run also lists a failedgenerator stalecheck ("generated files are not up to date"). That check covers Dagger's own module codegen, this branch doesn't touch.dagger/, and it fails identically onorigin/main(1440bd3).🤖 Generated with Claude Code