Non-exhaustive enums, variants, records, function params
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.
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.
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
enumwith 2 cases, I leave it up to the programmer to read the docs to know that they must always have adefaultcase (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
othercase and the developer writescase other:, then when a new WIT-level casecis added, it won't end up incase other:; what we needed was for the developer to writedefault:.
Brainstorming options (again, for languages without non-exhaustive enum):
- Maybe don't emit an
enumat all, but instead emit anintorstringand 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_CASEwhich, upon seeing (or being warned about), would remind the developer?
Just for cross-referencing: A similar issue was raised here regarding the protocol-version type.
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.
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.
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.
+1. Was about to file the same request after noticing that evolving an error variant is breaking the component API :/