component-model icon indicating copy to clipboard operation
component-model copied to clipboard

Non-exhaustive enums, variants, records, function params

Open MendyBerger opened this issue 10 months ago • 7 comments

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.

MendyBerger avatar Feb 24 '25 15:02 MendyBerger

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.

MendyBerger avatar Feb 24 '25 15:02 MendyBerger

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?

lukewagner avatar Feb 26 '25 01:02 lukewagner

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

badeend avatar Mar 18 '25 19:03 badeend

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.

badeend avatar Mar 18 '25 19:03 badeend

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.

badeend avatar Mar 18 '25 19:03 badeend

https://github.com/WebAssembly/component-model/issues/365#issuecomment-2636052868 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.

cpetig avatar Jun 05 '25 11:06 cpetig

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

ignatz avatar Nov 12 '25 12:11 ignatz