Skip to content

[WIP] Implement @catch for tagged variants#8306

Draft
zth wants to merge 1 commit intomasterfrom
tagged-variant-catch-all
Draft

[WIP] Implement @catch for tagged variants#8306
zth wants to merge 1 commit intomasterfrom
tagged-variant-catch-all

Conversation

@zth
Copy link
Member

@zth zth commented Mar 20, 2026

This is the successor of #7996.

Add support for providing "catch all" variant constructors to tagged variants, to allow capturing all non-literal discriminator cases of a variant. This lets you write a variant that can enumerate a few known discriminators, and "catch" the rest at runtime in a single catch-all constructor, for those runtime values you haven't statically enumerated.

@catch(...) adds a pattern-only catch-all case for tagged variants:

Example:

@tag("kind")
type response =
  | @as(200) Ok({body: string})
  | @catch(int) Other({kind: int, body: string})

let decode = (x: response) =>
  switch x {
  | Ok(r) => r.body
  | Other(r) => "unexpected status: " ++ Int.toString(r.kind)
  }

Rules:

  • @catch(...) is only valid on constructors with an inline record payload.
  • The payload may optionally expose the discriminant through one field matching the variant @tag("...") name, or a field renamed to that runtime name with @as("...").
  • If that field is present, it must appear exactly once and its type must exactly match the catch-all primitive.
  • @as("...") is only needed there when the source field name differs from the runtime discriminant name.
  • The discriminant field type must exactly match the catch-all primitive:
    • @catch(int) -> int
    • @catch(float) -> float
    • @catch(string) -> string
  • @catch(...) requires an explicit type-level @tag("...").
  • @catch(...) is not allowed on @unboxed variants.
  • At most one numeric catch-all (int or float) and one string catch-all may appear in a variant.

Only allowed in patterns

For this to be sound, we need to disallow constructing @catch constructors. This is rejected:

@tag("kind")
type t =
  | @as("one") One({thing: string})
  | @catch(string) Other({kind: string, thing: int})

let x = Other({kind: "two", thing: 1})

If we don't reject this, you could do:

let x = Other({kind: "one", thing: 1})

...and you'd end up with One({thing: 1}), which is wrong type wise (thing is string in One). But, by disallowing expressions for just the constructors with @catch, you can't create that constructor (and with that can't do that unsound thing), but you can still create the other, well defined constructors, and you can still pattern match and so on. You can even do | Other(_) as o => o since that doesn't touch the constructor as an expression.

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