Skip to content

feat: variants, options, enum lift, and interface-scoped name collisions#197

Merged
arcjet-rei merged 1 commit into
mainfrom
rei/feat/go-sensi
May 26, 2026
Merged

feat: variants, options, enum lift, and interface-scoped name collisions#197
arcjet-rei merged 1 commit into
mainfrom
rei/feat/go-sensi

Conversation

@arcjet-rei

Copy link
Copy Markdown
Contributor

Summary

Extends gravity codegen to cover canonical-ABI shapes that previously
weren't supported:

  • variants with mixed unit and payload cases (e.g. variant entity { email, phone-number, custom(string) })
  • option<T> in every position (params, returns, record fields, list elements)
  • enum lifting
  • interface-scoped type-name collisions (multiple interfaces declaring
    same-named types in the same world)
  • direction-aware module accessor so option/list/string lowering works
    on the import-side host wrapper path

Variant codegen

Each WIT variant resolves to a sealed Go marker interface plus one Go
type per case:

type Entity interface {
  isEntity()
}

type EntityEmail struct{}
func (EntityEmail) isEntity() {}

type EntityCustom struct {
  Value string
}
func (EntityCustom) isEntity() {}

WIT shorthand case-name(case-name) (where the payload record shares
the case's name) uses direct-record dispatch: the existing record
satisfies the marker interface directly, so callers write MyRecord{...}
rather than Wrapper{Value: MyRecord{...}}. This preserves the natural
shape for the common pattern:

variant config {
  allow(allow),
  deny(deny),
}
// Caller passes the record directly — no wrapper.
instance.Choose(ctx, Allow{Entities: []Entity{EntityEmail{}}})

Other shapes (unit cases, primitive payloads, list payloads) use a
dedicated {VariantName}{CaseName} wrapper struct with an optional
Value field.

VariantLower collapses the switch binder when every case is
direct-record:

// All cases direct-record: compact form.
switch variantPayload := value.(type) {
case Allow: ...
case Deny: ...
}

// Mixed dispatch: per-arm unwrap.
switch case7 := value.(type) {
case EntityEmail:
  // unit — no payload referenced
case EntityCustom:
  variantPayload := case7.Value
  ...
}

Top-level variant parameters in method signatures resolve to
interface{}
(via a new resolve_param_type helper). The marker
interface is still emitted as an opt-in type for callers who want
compile-time safety, but binding method signatures stay flexible. Records,
list elements, and return positions are still strongly typed.

Option representation

option<T> is now *T in Go: nil for none, &v for some. A single
pointer composes uniformly in every position where the prior (T, bool)
comma-ok shape couldn't (record fields, list elements, callback returns).
OptionLift / OptionLower rewritten for pointer semantics.

Enum lifting

EnumLift mirrors the existing EnumLower, reading the discriminant and
producing the named Go constant.

Interface-scoped type collisions

WIT permits two interfaces in the same world to declare types of the same
name (e.g. both email-validator-overrides and verify-bot declaring an
enum validator-response). Without qualification the generated Go
redeclared the same type twice.

New qualified_type_name prefixes interface-owned types with the owning
interface's name only when the bare name would collide with another
concrete type in the world:

WIT type Generated Go (no collision) Generated Go (collision)
enum algorithm-result (only one) AlgorithmResult
enum validator-response (two interfaces) EmailValidatorOverridesValidatorResponse / VerifyBotValidatorResponse

Follows alias chains via wit_bindgen_core::dealias so world-level
re-exports (use foo.{x}) resolve to the canonical interface-scoped Go
name rather than emitting a duplicate definition.

Direction-aware module accessor

Lift/lower instructions now use a per-direction module_handle
i.module for exports, mod for imports — so option/list/string
lowering invoked from import-side host wrappers no longer references an
undefined i. Several store instructions (I32Store, LengthStore,
PointerStore, F32Store, F64Store, plus the literal-byte arm of
I32Store8) collapse their Direction match into a single body that uses
$module_handle.

New example coverage

  • examples/variants/ — mixed unit/payload cases, direct-record
    shorthand, list<variant> payloads, variants nested in records, and
    option<u32> record fields. The entity variant deliberately mirrors
    the shapes that initially exposed each gap.
  • examples/regressions/ extended with:
    • Regression 4 — two interfaces in the same world both defining
      enum validator-response (cross-interface enum collision).
    • Regression 5 — import callback returning option<string>
      (exercises import-side option lowering through mod.Memory() /
      mod.ExportedFunction("cabi_realloc")).

examples/basic/basic_test.go updated for the new *T option shape.

Test plan

  • cargo test — 34 unit tests pass
  • cargo test --test cli — 8 snapshot tests pass
  • cd examples && go test ./... — basic, iface-method-returns-string,
    instructions, records, regressions, variants all pass

🤖 Generated with Claude Code

… collisions

Extends gravity codegen to cover the canonical-ABI shapes needed for richer
WIT worlds: variants with mixed unit and payload cases, `option<T>` in any
position, enum lifting, and interface-scoped type-name collisions.

**Variant codegen**

  * `Variant` resolves to a sealed Go marker interface (e.g.
    `type SensitiveInfoEntity interface { isSensitiveInfoEntity() }`) and
    one Go type per case implements it.
  * WIT shorthand `case-name(case-name)` (where the payload record shares
    the case's name) uses *direct-record dispatch*: the existing record
    type satisfies the marker interface directly, so callers construct
    `MyRecord{...}` rather than `Wrapper{Value: MyRecord{...}}`.
  * All other cases (unit cases like `email`, primitive-payload cases like
    `custom(string)`, list-payload cases like `allow(list<entity>)`) get a
    dedicated `{VariantName}{CaseName}` wrapper struct with an optional
    `Value` field.
  * When every case in a variant is direct-record, `VariantLower` emits
    the compact `switch variantPayload := value.(type)` form; mixed
    variants use a temp binder plus per-arm `variantPayload := caseN.Value`
    unwrapping.
  * Top-level variant parameters in method signatures resolve to
    `interface{}` (via the new `resolve_param_type` helper) so existing
    callers passing variants through `any`-typed plumbing keep compiling.
    Records, list elements, and return positions remain strongly typed.

**Option representation**

  * `option<T>` is now `*T` in Go: `nil` for none, `&v` for some. A single
    pointer composes uniformly in every position — params, returns, record
    fields, list elements — where the prior `(T, bool)` comma-ok shape
    couldn't.
  * `OptionLift`/`OptionLower` rewritten for pointer semantics; no
    `reflect` dependency.

**Enum lifting**

  * `EnumLift` mirrors the existing `EnumLower`, reading the discriminant
    and producing the named Go constant.

**Interface-scoped type collisions**

  * New `qualified_type_name` resolves interface-owned WIT types to
    `{interface-name}-{type-name}` only when their bare name would
    collide with another concrete type in the world (skipping `Type`
    aliases so a world-level `use foo.{x}` doesn't trigger
    qualification). Single-instance names stay flat.
  * Follows alias chains via `wit_bindgen_core::dealias` so world-level
    re-exports resolve to the canonical interface-scoped Go name.

**Direction-aware module accessor**

  * Lift/lower instructions now use a per-direction `module_handle`
    (`i.module` for exports, `mod` for imports), fixing previously
    broken codegen for option/list/string lowering invoked from
    import-side host wrappers.
  * Several store instructions (`I32Store`, `LengthStore`, `PointerStore`,
    `F32Store`, `F64Store`, plus the literal-byte arm of `I32Store8`)
    collapse their Direction match into a single body that uses
    `$module_handle`.

**Examples and regressions**

  * New `examples/variants/` covering mixed unit/payload cases, the
    direct-record shorthand, `list<variant>` payloads, variants nested in
    records, and `option<u32>` record fields.
  * `examples/regressions/` extended with:
      * Two interfaces in the same world defining `enum validator-response`
        (cross-interface enum collision).
      * An import callback returning `option<string>` (exercises
        import-side option lowering through `mod.Memory()` /
        `mod.ExportedFunction("cabi_realloc")`).
  * `examples/basic/basic_test.go` updated for the new `*T` option shape.

**Tests**

  * `cargo test` — 34 pass.
  * `cargo test --test cli` — 8 snapshots green.
  * `cd examples && go test ./...` — basic, iface-method-returns-string,
    instructions, records, regressions, variants all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arcjet-rei arcjet-rei requested a review from a team as a code owner May 21, 2026 18:27

@davidmytton davidmytton left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good. There's a bit of followup needed to get this over to generating the final Wasm for the Go SDK, but this is the major Gravity work done.

@arcjet-rei

Copy link
Copy Markdown
Contributor Author

This should be all of the changes to Gravity, but I needed this piece first so that I could do clean PRs for the SDK and monorepo work. Thanks for the review!

@arcjet-rei arcjet-rei merged commit fe2e11f into main May 26, 2026
4 checks passed
@arcjet-rei arcjet-rei deleted the rei/feat/go-sensi branch May 26, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants