Skip to content

story(gen-go): every receiver the generator accepts yields a package that compiles, whatever the layout - #431

Merged
zaba505-auto-merge[bot] merged 2 commits into
mainfrom
issue-428
Oct 24, 2026
Merged

zaba505-auto-merge[bot] merged 2 commits into
mainfrom
issue-428

Conversation

@Zaba505

@Zaba505 Zaba505 commented Oct 24, 2026

Copy link
Copy Markdown
Owner

Closes #428

The approach: rename, not refuse

r and w are 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:

  • Every method is composed under a placeholder receiver (receiver__). Nothing munged from a copybook can have that spelling, and the pass checks that nothing else does either.
  • Once the whole package exists, 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.
  • A parameter is named in its method's prose ("reads one ORDER-RECORD out of r"), so comments inside that method move with it. No other prose is rewritten.
  • A predeclared identifier can't be renamed, because len has no other spelling. isReceiverName now 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:

// UnmarshalCOBOL reads one ORDER-RECORD out of r0, in the order …
func (r *OrderRecord) UnmarshalCOBOL(r0 *codec.Reader) error {

With receiver=codec, codec.go, and only codec.go, imports codec0 "github.com/Zaba505/cobol-go/codec".

The default receiver x collides with nothing, so every golden package is byte-for-byte unchanged.

Acceptance criteria

  • Every receiver value cpybkc-gen-go accepts yields a generated package that compiles and passes its generated tests, for every layout. The README and isReceiverName'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).
  • If the generator renames: receiver=r, w, codec and err each generate a package that compiles and whose generated tests pass (TestTheLettersAShopPicksPassTheirGeneratedTests runs go test on them). The layout is orders, 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.
  • A test takes the names it checks from the generated code itself: TestEveryNameInScopeInsideAMethodIsRefusedOrCompiles type-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 real codec, 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 means go test ./... and the example regeneration test now exercise a renamed parameter on every CI run.
  • The receiver row in cmd/cpybkc-gen-go/README.md states the rule as implemented, and the rationale paragraph explains the rename. There is no gen-go SPEC that states the rule.

Judgment calls

  • Every predeclared identifier is refused, including ones no method uses today (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.
  • The example moves to r rather than keeping a or dropping the option. a existed only as the workaround this story removes, and r puts the rename under the existing example tests.
  • The go test run builds in cmd/cpybkc-gen-go/_receivers/ inside the module, following internal/conformance/goadapter/_scratch. The leading underscore keeps it out of ./..., the run removes it afterwards, and .gitignore covers 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 ci from an ordinary clone of the branch exits 0 and its ci trace passed. The run also lists a failed generator stale check ("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 on origin/main (1440bd3).

🤖 Generated with Claude Code

…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 file
  • example/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 via clearReceiver to 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=r and 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

Comment on lines +402 to +405
case obj.Parent() == types.Universe:
kind = "predeclared"
case obj.Pkg() != nil && obj.Parent() == obj.Pkg().Scope():
kind = "package"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@Zaba505 Zaba505 added the auto-merge Hand this pull request to the auto merge workflow label Oct 24, 2026
@zaba505-auto-merge
zaba505-auto-merge Bot merged commit 11bee72 into main Oct 24, 2026
4 checks passed
@zaba505-auto-merge
zaba505-auto-merge Bot deleted the issue-428 branch October 24, 2026 02:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-merge Hand this pull request to the auto merge workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

story(gen-go): every receiver the generator accepts yields a package that compiles, whatever the layout

2 participants