Method generators: q.GenStringer, q.GenEnumJSONStrict, q.GenEnumJSONLax¶
Package-level "directives" — var _ = q.GenX[T]() — that synthesize companion methods on T at compile time. The methods land in a generated _q_gen.go file alongside your package; the directive itself rewrites to a no-op so the runtime cost is zero. No go generate step, no stringer invocation, no method-list to maintain by hand.
Signatures¶
// All return q.GenMarker (an empty struct) so `var _ = q.GenX[T]()`
// is type-checkable Go.
func GenStringer[T comparable]() GenMarker
func GenEnumJSONStrict[T comparable]() GenMarker
func GenEnumJSONLax[T comparable]() GenMarker
T must be a defined named type with constants declared in the same package — same restriction as q.EnumValues and friends.
At a glance¶
type Color int
const (Red Color = iota; Green; Blue)
type Status string
const (Pending Status = "pending"; Done Status = "done"; Failed Status = "failed")
var _ = q.GenStringer[Color]()
var _ = q.GenEnumJSONStrict[Color]()
var _ = q.GenEnumJSONLax[Status]()
After the toolexec pass, the user's package has these methods auto-generated:
// From q.GenStringer[Color]:
func (v Color) String() string { switch v { case Red: return "Red" /* ... */ } return "" }
// From q.GenEnumJSONStrict[Color]:
func (v Color) MarshalJSON() ([]byte, error) // "Red" / "Green" / "Blue"; errors on unknown
func (v *Color) UnmarshalJSON([]byte) error // accepts known names; errors on others
// From q.GenEnumJSONLax[Status]:
func (v Status) MarshalJSON() ([]byte, error) // pass-through to json.Marshal(string(v))
func (v *Status) UnmarshalJSON([]byte) error // pass-through; preserves any string
Strict vs. Lax — which to pick¶
| Generator | Wire format | Unknown value on unmarshal | Pairs with q.Exhaustive? |
|---|---|---|---|
GenEnumJSONStrict |
constant name | error wrapping q.ErrEnumUnknown |
Yes (every parsed value is a declared constant) |
GenEnumJSONLax |
underlying value (string/int) | preserved (cast to T verbatim) | Yes — but default: is required to handle the genuinely-unknown values |
Strict when wire-drift should crash loudly (your service's contract is the closed enum; anything new is a bug).
Lax when forward-compat matters (newer producers shouldn't crash older consumers). Pair with q.Exhaustive's default: arm to handle unrecognised values explicitly:
type Status string
const (Pending Status = "pending"; Done Status = "done")
var _ = q.GenEnumJSONLax[Status]()
func handle(s Status) string {
switch q.Exhaustive(s) {
case Pending: return "wait"
case Done: return "ok"
default: return forwardOpaque(s) // unknown wire value — don't drop it
}
}
Adding a new constant later still triggers the missing-case diagnostic at compile time, even though default: is present.
How the synthesis works¶
- Scanner walks each file's top-level
*ast.GenDecl(kind=var) forvar _ = q.GenX[T]()initializers. Each match becomes acallShapewith form=formDiscardand a synthetic*ast.ExprStmtwrapping the call. - Typecheck pass resolves
Tto a*types.Named, walks T's declaring package for*types.Constof identical type, and populates the shape'sEnumConsts(names),EnumConstValues(constant values as Go-source text), andEnumUnderlyingKind(e.g."string","int"). - Rewriter substitutes each
q.GenX[T]()call's source span withq.GenMarker{}so the runtime initializer is a no-op (rather than the panic stub the unrewritten body would hit). - File-synthesis pass (
internal/preprocessor/gen.go) collects all Gen directives across the package, deduplicates by (family, type), and emits a single_q_gen.goto$TMPDIR. The file is appended to the compile argv. Imports (encoding/json,fmt) are also added to the importcfg via the existingimportsToInjectplumbing.
The generated file uses package <userPkg>, declares methods on the unqualified type names, and references constants by name — same scope restrictions as q.EnumName. Cross-package T is rejected for the same reason.
What if I forget the directive?¶
Without var _ = q.GenX[T]() somewhere in the package, no methods are synthesized. The user's code that depends on (say) c.String() will fail to compile with the standard "type Color has no method String" error from Go. q has no runtime side-effects beyond the rewrites.
What if the preprocessor doesn't run?¶
The directive's stub-body calls panicUnrewritten("q.GenStringer"), so var _ = q.GenStringer[T]() panics at package init. q's link gate catches the missing preprocessor first (the _q_atCompileTime symbol fails to resolve at link time), so this panic isn't reachable in practice — but it's the same loud-failure-on-miss principle as every other q.* helper.
See also¶
q.EnumValues/q.EnumName/ … — value-level enum helpers;GenStringer's output is essentially a method form ofq.EnumName.q.Exhaustive— pairs withGenEnumJSONLaxfor the forward-compat-friendly switch shape (cover all known +default:for unknowns).