Skip to content

Non-exhaustive enums, variants, records, function params #454

Open

Description

@MendyBerger

Currently, adding a new case to an enum or variant, would likely be a breaking change, as user code needs to handle a new case.

It would be great to have some sort of non-sealed attribute that would indicate to language bindings that this enum/variant can get new cases, and therefore should require a fall through case when matching on them.
This would be similar to the #[non_exhaustive] attribute in Rust, and IIUC is default in Swift unless opted out with @frozen.

The same can probably also be done for records and function parameters. As long as the new fields/parameters have default values, they should be able to evolve without requiring a breaking change.

This problem came up in wasi:webgpu. In WebGPU, feature detection and feature requests are done with strings, as JS doesn't have enums (feature detection, feature requests). This allows the api to evolve over time without making breaking changes.
If we wanna use real enums in wasi:webgpu , we need a way to let them grow without making breaking changes. Otherwise we'll be stuck on the first version of WebGPU.

Activity

  1. MendyBerger commented on Feb 24, 2025

    @MendyBerger
    Author

    FWIW I'd argue that this should actually be the default in the component model, and should require opting out rather than opting in, as most components will probably want to evolve types over time.

  2. lukewagner commented on Feb 26, 2025

    @lukewagner
    Member

    I agree we need to do some work to relax subtyping. The main thing we have to be careful about is that a non-breaking change we allow at the WIT level causes a breaking change in the updated auto-generated bindings, making it a breaking change in practice. In general, we'll need to think carefully about each subtyping rule and how it manifests in a variety of languages' bindings generators. For example, if we want to allow adding extra default parameters, we'd need to figure what to do for C, which doesn't have default parameters.


    Focusing just on non-exhaustive enums, one question is what bindings are emitted for languages without explicit non-exhaustive-enum support. Let's say I have a non-exhaustive enum e with 2 declared cases a and b:

    • If I generate a source-language enum with 2 cases, I leave it up to the programmer to read the docs to know that they must always have a default case (or anticipate the none-taken fallthrough), and at a certain volume of users, adding a new case will likely break code.
    • If I try to help the programmer by adding a synthetic 3rd other case and the developer writes case other:, then when a new WIT-level case c is added, it won't end up in case other:; what we needed was for the developer to write default:.

    Brainstorming options (again, for languages without non-exhaustive enum):

    • Maybe don't emit an enum at all, but instead emit an int or string and treat the cases as named constants (like JS is effectively doing for WebGPU)?
    • Maybe emit a synthetic case with a silly name like DONT_FORGET_TO_ADD_A_DEFAULT_CASE which, upon seeing (or being warned about), would remind the developer?
  3. badeend commented on Mar 18, 2025

    @badeend
    Member

    Just for cross-referencing: A similar issue was raised here regarding the protocol-version type.

  4. badeend commented on Mar 18, 2025

    @badeend
    Member

    Some other examples:

    Wasi-http's error-code contains a catch-all item, which to me signals this enum "wants" to be non-exhaustive:

        /// This is a catch-all error for anything that doesn't fit cleanly into a
        /// more specific case. It also includes an optional string for an
        /// unstructured description of the error. Users should not depend on the
        /// string for diagnosing errors, as it's not required to be consistent
        /// between implementations.
        internal-error(option<string>)
    

    So does wasi-sockets.

  5. badeend commented on Mar 18, 2025

    @badeend
    Member

    In terms of subtyping; one consideration is whether subtyping should be a toolchain or a runtime/link-time concern.

    If we're aiming for the toolchains to handle it, then the ABI is not allowed to change at all and a 'named integer constants' approach would make sense where each case must be explicitly numbered, like protobuf or capnproto. Something like:

    @non-exhaustive(u16)
    enum color {
      red = 3,
      green = 4,
      blue= 5,
    }
    

    If runtime/link-time subtyping is an option, the generated bindings on both sides of the component boundary could see exhaustive enums/variants, with the component-model runtime mediating the two components through auto-generated glue code that translates between the two enum definitions. For reference, see prior discussion. At face value this seems like the more preferred option to me for the long run, but also (much) more complicated.

  6. cpetig commented on Jun 5, 2025

    @cpetig

    #365 (comment) contains another discussion and proposed syntax for this (the binary representation is already implemented):

    type port = u16;
    http: port = 80;
    pop3: port = 110;
    connect: func(port: port) -> result<connection>;
    

    and I support the decision there that this should be different from an enum (on WIT level), but might map to an enum for C/C++

    PS: I also like the attribute syntax presented above, but I follow Luke's reasoning of why this shouldn't be a WIT enum.

  7. ignatz commented on Nov 12, 2025

    @ignatz

    +1. Was about to file the same request after noticing that evolving an error variant is breaking the component API :/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions