Skip to content

feat: generate a world's exported interfaces - #245

Open
robfischer1 wants to merge 5 commits into
arcjet:mainfrom
robfischer1:interface-exports
Open

robfischer1 wants to merge 5 commits into
arcjet:mainfrom
robfischer1:interface-exports

Conversation

@robfischer1

Copy link
Copy Markdown

Stacked on #244. Until that merges this PR's diff includes its commits; only the last commit is new here. Once #244 lands I'll rebase onto main.

export <interface> panicked at generation:

not yet implemented: generate interface exports

An exported interface now generates three things:

  • Its types.
  • <World><Interface>Exports, a defined type over the instance's struct, which the instance returns from <Interface>(). It is a pointer conversion with no allocation.
  • One method per function. Each calls ns:pkg/iface@ver#func, the name Resolve::wasm_export_name gives under legacy mangling, which is the name wit-bindgen exports.

An accessor that would shadow an instance method is refused at generation.

The workspace gains default-members = ["cmd/*"]. Built for the host, a guest that exports a versioned interface does not link, because the linker's version script rejects its export names:

; expected, but got @

Tests:

  • exports::tests::test_exported_interface
  • exports::tests::test_exported_interface_accessor_clash
  • exports::tests::test_exported_interface_accessor_clashes_with_world_function
  • The interface-exports example: a versioned package that exports two interfaces beside a world function, one of which calls back into the host

🤖 Generated with Claude Code

https://claude.ai/code/session_01K99bnsYaj3w4MMWSF5hTMu

robfischer1 and others added 5 commits October 7, 2026 10:23
WIT scopes a case name to its enum; gravity emitted every case as an
unqualified package constant. Two enums sharing a case name, or a case
sharing a type's name, then redeclared an identifier and the bindings
failed go vet:

    enum_collisions.go:41:2: PeerSvid redeclared in this block

The identity lift had to spell its `via` cases `via-peer-svid`, ... to
dodge `credential-kind`'s `peer-svid`. Every case constant is now the
enum's Go name followed by the case's (ViaPeerSvid,
CredentialKindPeerSvid, ShapePoint beside the record Point), as Go
spells an enumerated constant (http.MethodGet). The enum's qualified
name is already unique (arcjet#197), so its constants are too.

BREAKING: every enum constant is renamed (Red becomes ColorRed). The
upstream examples' tests are updated, and the regressions WIT drops the
comment that called constant qualification out of scope. Qualifying
only colliding names, as arcjet#197 does for types, was rejected: a new enum
would then rename an existing enum's constants.

Tests: imports::tests::test_enum_case_constants_are_named_for_their_enum
(fails without the fix) and the new enum-collisions example
(TestSharedCaseNamesStayDistinct, TestCaseNamedLikeAType; without the
fix go vet fails with the error above).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gravity lowered only result<T, string> and result<_, string>; any other
err type panicked at generation:

    not yet implemented: TODO(arcjet#4): implement remaining result conversion

so the identity lift had to return a `decision { ok: option<resolved>,
refused: option<refusal> }` record instead of result<resolved, refusal>.

The Go shape stays (T, error) / error. The err case comes back as a
*ResultError[E], a generic error type emitted once per file when any
function needs it; errors.As recovers the E, and a trap, a host failure
or any other error is never one. On the host side a function returns
&ResultError[E]{Value: e} for the err case; an error that is not a
*ResultError[E] has no WIT representation and traps the guest, as a
panic in a host function does. Enums, records and variants (including a
record dispatched directly as a variant case) work as E, with or
without an ok type.

ResultError.Error formats its value with %v, so each enum's private
type gains a String that answers the WIT case name ("untyped-peer"), and
the error text reads as WIT wrote it.

Tests: exports::tests::test_export_result_with_enum_err,
imports::tests::test_import_result_with_record_err (both fail without
the fix with the panic above),
imports::tests::test_enum_string_answers_the_wit_case,
factory::tests::test_result_error_only_when_needed, and the new
result-errors example: TestEnumErr, TestRecordErr, TestVariantErr,
TestErrWithoutOk, TestHostReturnsTypedErrs, TestHostPlainErrorTraps
(without the fix the bindings do not generate).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A world that imports an interface only for its types — through
`use types.{...}`, or as the dependency of an exported interface — imports
no function from it. Gravity still emitted an empty Go interface
(`type IIdentityCoreTypes interface {}`), a factory parameter of that
type and an empty host module, so every caller passed a placeholder
(the identity lift's `struct{}{}`, the spike's leading nil). Such an
interface now gets none of the three; its types are still generated.

BREAKING: New<World>Factory loses the parameter for each types-only
interface.

Tests: imports::tests::test_types_only_interface_needs_no_host (fails
without the fix: a host module chain and ITestWorldTypes are emitted);
the enum-collisions and result-errors examples call their factories
without the placeholder (without the fix they do not compile: not
enough arguments).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host module was named `<namespace>:<package>/<interface>`, without
the package version. A guest built from a versioned package imports
`<namespace>:<package>/<interface>@<version>`, so instantiation failed:

    module[gravity:interface-exports/log@0.1.0] not instantiated

The module is now named by Resolve::id_of, the interface's full id,
which carries the version when the package has one. A package declared as
`package example:pkg@0.1.0` names its imports with that version, so any
import it gains needs this.

Tests: imports::tests::test_host_module_name_carries_the_package_version
(fails without the fix: "test:fixture/host" for "test:fixture/host@1.2.3");
the interface-exports example (next commit) imports from a versioned
package at runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gravity generated only world-level function exports; `export <interface>`
panicked at generation:

    not yet implemented: generate interface exports

so the identity lift flattened its `interface identity` into world-level
functions. An exported interface now generates:

- its own types, through the import side's type generation;
- <World><Interface>Exports, a defined type over the instance's struct,
  holding the interface's functions. The instance answers it with
  <Interface>() — a pointer conversion, no allocation — so its methods
  use the instance's module and export memo, and the instance's own
  methods do not carry over;
- one method per function, calling the guest's
  `<namespace>:<package>/<interface>@<version>#<function>` export (and
  its cabi_post_), named by Resolve::wasm_export_name with legacy
  mangling, as wit-bindgen exports it. Func::with_wasm_name carries the
  name to CallWasm.

The workspace gains default-members = ["cmd/*"]: built for the host,
a guest exporting a versioned interface does not link, because its
export names (`ns:pkg/iface@1.0.0#func`) are not valid in the linker's
version script ("; expected, but got @"). Guests are built with -p and
--target wasm32-unknown-unknown, as generate.go already does.

World functions and exported interfaces coexist. An interface whose
accessor would shadow an instance method (Close, or a world function of
the same name) is refused at generation, naming the clash.

Tests: exports::tests::test_exported_interface,
test_exported_interface_accessor_clash,
test_exported_interface_accessor_clashes_with_world_function, and the new interface-exports
example — a versioned package exporting two interfaces (one with its own
enum and record using another interface's record, one calling back into
an imported host) beside a world function: TestInterfaceTypesAndFunctions,
TestInterfaceCallsBackIntoTheHost, TestWorldFunctionsBesideInterfaces.
Without the fix the example does not generate (the panic above).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@robfischer1
robfischer1 requested a review from a team as a code owner October 7, 2026 14:26
@arcjet-review arcjet-review Bot added needs review Awaiting human review and removed needs review Awaiting human review labels Oct 7, 2026
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.

1 participant