Skip to content

Add support for package-scope WIT types - #2607

Open
yordis wants to merge 6 commits into
bytecodealliance:mainfrom
yordis:yordis/top-level-type-694
Open

Add support for package-scope WIT types#2607
yordis wants to merge 6 commits into
bytecodealliance:mainfrom
yordis:yordis/top-level-type-694

Conversation

@yordis

@yordis yordis commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds support for declaring record, variant, enum, flags, and type
aliases directly at package scope in WIT, and encodes/decodes them following the
approved design in WebAssembly/component-model#699.

A package-scope type is encoded like a use of an interface, just without the
wrapping instance: the definition is restated locally and bound with an eq
import named by its fully-qualified namespace:package/name. Because the type
is structural, the eq bound means nothing has to be supplied to satisfy the
import. A referencing interface or world aliases that import into its
instance/component type; a world places the import inside its inner exported
component-type.

This replaces the earlier approach of inlining a package-scope type into every
interface/world instance and re-exporting it, which did not pass Component Model
validation.

Encoding

Given:

package local:demo;

record point { x: u32, y: u32 }

interface api {
  move-to: func(p: point);
}

the package-scope point is exported once at the top level and imported with
eq into the wrapping component-type:

(component
  (type (;0;) (record (field "x" u32) (field "y" u32)))
  (export (;1;) "point" (type 0))
  (type (;2;) (component
    (type (;0;) (record (field "x" u32) (field "y" u32)))
    (import "local:demo/point" (type (;1;) (eq 0)))
    (type (;2;) (instance
      (alias outer 1 1 (type (;0;)))
      (type (;1;) (func (param "p" 0)))
      (export (;0;) "move-to" (func (type 1)))))
    (export (;0;) "local:demo/api" (instance (type 2)))))
  (export (;3;) "api" (type 2)))

A package-scope type that depends on one from another package puts the import
on the package's own component, since there is no wrapping component-type to
hold it. Decoding recognizes qualified type imports (both nested and at the
package root) as package-scope dependencies and keeps ownership with the
defining package; the previous inline-coalescing pass in the decoder is removed.
The printer emits the corresponding top-level use before the definitions that
reference it.

Types-only packages

Decoding recovers the package name from the fully-qualified export inside an
interface's or world's component type, so a package with only package-scope
types would otherwise have no package name in the binary at all. For that case
the encoding is generalized (as discussed with Luke): each type is additionally
bound with a self-referential eq import named by its fully-qualified name,
which is where the package name is recovered from on decode:

(component
  (type (;0;) (record (field "x" u32) (field "y" u32)))
  (export (;1;) "point" (type 0))
  (import "local:demo/point" (type (;2;) (eq 1)))
  (type (;3;) (list 2))
  (export (;4;) "path" (type 3))
  (import "local:demo/path" (type (;5;) (eq 4))))

Later types reference the imported index so every import satisfies the
component model's named-type validation rules, and the import is placed after
the export so V2 format sniffing still sees a label first. Packages that do
have an interface or world encode byte-identically to the approved WIT.md
examples; the self-imports may be worth a small follow-up note in the spec's
Package Format section.

Tests

  • Golden interface fixtures mirroring the approved WIT.md examples
    (package-scope-spec-interface, package-scope-spec-world), a types-only
    fixture (package-scope-types-only), plus the existing package-scope
    fixtures re-blessed to the eq-import encoding. All round-trip through
    encode → validate → decode → print → re-encode.
  • Combination fixtures covering package-scope types mixed with classic
    use iface.{...} projections on the same wrapping component-type:
    package-scope-with-iface-use, package-scope-with-world-use,
    package-scope-with-resource-use, and package-scope-mixed-use (foreign
    package-scope use plus a local interface projection).
  • Unit tests covering: foreign use of a package-scope type, a package-scope
    type defined in terms of a foreign one (import on the package component),
    versioned fully-qualified names, the export-before-use ordering for a type
    referring to another, types-only round-trips (with and without a foreign
    dependency), rejection of a resource at package scope, and that building a
    component (component new) exports the interface directly without inventing
    a types interface (WIT: allow type declarations at package scope WebAssembly/component-model#694).

References

@yordis
yordis marked this pull request as ready for review August 15, 2026 19:05
@yordis
yordis requested a review from a team as a code owner August 15, 2026 19:05
@yordis
yordis requested review from dicej and removed request for a team August 15, 2026 19:05
yordis added 3 commits August 18, 2026 11:36
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Bless CLI nominal JSON expectations for Package.types, and drop the
interfaces foreign-use fixture whose parse/decode prints cannot match
under inline encoding.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis
yordis force-pushed the yordis/top-level-type-694 branch from e49b637 to 4db4f5b Compare August 18, 2026 15:37
Align the encoding with the approved package-scope types design in
WebAssembly/component-model#699. Instead of inlining a package-scope
type into every interface/world instance and re-exporting it, a
package-scope type is now encoded like a `use` of an interface without
the wrapping instance: the definition is restated and bound with an
`eq` import named by its fully-qualified `namespace:package/name`, so
nothing has to be supplied to satisfy it. The referencing interface or
world aliases that import into its instance/component type.

A package-scope type that depends on one from another package puts the
`import` on the package's own component, since there is no wrapping
component-type to hold it there. Decoding recognizes those qualified
type imports as package-scope dependencies and keeps ownership with the
defining package, dropping the previous inline-coalescing pass. The
printer emits the corresponding top-level `use` before the definitions
that reference it.

This makes the encoded output valid per Component Model validation and
round-trips through decode/encode. Add golden fixtures mirroring the
approved WIT.md examples plus unit tests covering foreign use, foreign
dependency, versioned names, the export-before-use ordering, the
resource rejection, and that `component new` exports the interface
directly without inventing a types interface.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
types,
}
} else if !types.is_empty() {
bail!(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is odd; so waiting for Luke's direction here

yordis added 2 commits August 18, 2026 15:40
Previously a package containing only package-scope types (no interfaces
or worlds) could be encoded but not decoded, because the package name is
only recoverable from the fully-qualified export inside an interface's
or world's component type and such a package has neither.

Generalize the encoding for that case: each package-scope type is
additionally bound with a self-referential `eq` import named by its
fully-qualified `namespace:package/name`, placed right after the type's
export so V2 format sniffing still sees a label as the first export.
Later types reference the imported index so every import satisfies the
component model's named-type validation rules. Packages that do have an
interface or world encode byte-identically to before.

Decoding detects a self-import by resolving its `eq` bound back to one
of the package's own exported types and recovers the package name from
the import, keeping foreign package-scope dependencies (whose bounds are
restated definitions) on the existing path. The types-only bail is now
only reachable for binaries produced before these imports existed.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Cover the remaining matrix of package-scope types mixed with classic
`use iface.{...}` projections: same-interface use, world-level use,
resource projection use, and a foreign package-scope use alongside a
local interface projection. Each fixture encodes both the instance
import path and the package-scope `eq` type import on the same wrapping
component-type and round-trips through encode/decode/print.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
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