Skip to content

fix: construct, infer, and match generic enums like any other enum - #804

Merged
kacy merged 1 commit into
mainfrom
fix-generic-enum-match
Aug 19, 2026
Merged

fix: construct, infer, and match generic enums like any other enum#804
kacy merged 1 commit into
mainfrom
fix-generic-enum-match

Conversation

@kacy

@kacy kacy commented Aug 19, 2026

Copy link
Copy Markdown
Owner

what this fixes

A generic enum was only usable through a typed context, and even there only half-usable. Three defects, one root: the bare base name (Opt) has no type id — only instances (Opt[Int]) are registered — and neither the constructor path nor the pattern path knew how to bridge that.

  • x := Opt.Some(5) typed as the error wildcard, so a later match on it checked nothing and ran no arm at runtime — a silent wrong answer, not a diagnostic
  • an annotated binding's match died at every arm with E202 unknown type: Opt, because patterns resolved the bare name against the type table instead of the subject
  • the emitter read generic payload slots with the declaration's kind (the literal parameter name T), so an int payload was treated as a string handle downstream

Nothing in the tree ever matched on a generic enum — the recursive-types test builds a Chain[Int] and prints "chain built" without looking inside it — which is how all three stayed invisible.

the change

Checker. A constructor with a payload argument infers its instance (a payload declared as the bare parameter or its optional binds the type argument). An annotated binding records its instance on a payload-free constructor expression, so the match checker and the emitter both see a typed value. A variant pattern's bare name resolves against the subject's instance — which also brings generic-enum matches under the exhaustiveness check (E204 now fires for a missing variant). A payload-free constructor bound with no annotation has nothing to infer from and is rejected loudly (new E262) instead of staying silently untyped. Payload arguments are checked against the instantiated payload types, with a plain value widening to an optional payload slot the same way a binding does.

Emitter. Concrete payload kinds substitute in from the checked instance type, per slot, only where the declaration left an unresolved parameter name — a concrete declared kind (including an optional payload's tuple) stays untouched. Applied at match payload extraction, payload literal conditions, and variant construction, so an Opt[String] payload is retained and released correctly at the binding.

Known gap, documented in limitations: a generic enum erases to one IR struct whose destructor is built from the declaration, so a heap payload is not released when the enum box dies — the same erasure gap generic struct fields have.

Docs: limitations entry rewritten, E262 added to docs/errors.md, and examples/generics.pith now actually matches on Option[T].

what was tested

  • new tests/cases/test_generic_enum_match.pith: inferred construction, annotated payload-free variant, string payload round trip, two instantiations of one base coexisting — output matches its golden
  • new tests/invalid/generic_enum_uninferable pins E262; check-invalid 50/50
  • run-regressions-only 364/364, including the recursive Chain[T] test whose construction shape (plain tail into an optional payload) the new argument check must accept
  • bootstrap seed regenerated; make bootstrap-verify reports the fixed point, and its run-examples-self pass covers the updated generics example
  • fmt --check and lint clean on every edited file

a generic enum was only usable through a typed context, and even there
only half-usable. three defects, one root: the bare base name (Opt) has
no type id — only instances (Opt[Int]) are registered — and neither the
constructor path nor the pattern path knew how to bridge that.

- an unannotated constructor (x := Opt.Some(5)) typed as the error
  wildcard, so a later match on it checked nothing and ran NO arm at
  runtime — a silent wrong answer, not a diagnostic
- an annotated binding's match died at every arm with E202 unknown
  type: Opt, because patterns resolved the bare name against the type
  table instead of the subject
- the emitter read generic payload slots with the declaration's kind
  (the literal parameter name T), so an int payload was treated as a
  string handle downstream

the checker now infers the instance from a constructor's payload
arguments (a payload declared as the bare parameter or its optional
binds it), records an annotation's instance on a payload-free
constructor expression, and resolves a variant pattern's bare name
against the subject's instance — which also brings generic-enum
matches under the exhaustiveness check. a payload-free constructor
bound with no annotation has nothing to infer from and is now rejected
loudly (new E262) instead of staying silently untyped. the emitter
substitutes concrete payload kinds from the checked instance type, per
slot, only where the declaration left an unresolved parameter name — a
concrete declared kind, including an optional payload's tuple, stays
untouched.

payload arguments are checked against the instantiated payload types,
with a plain value widening to an optional payload slot the same way a
binding does (a recursive variant's tail is handed a plain Chain[Int]
all over the tree).

known gap, documented in limitations: a generic enum erases to one IR
struct whose destructor is built from the declaration, so a heap
payload is not released when the enum box itself dies — the same
erasure gap generic struct fields have.

## what was tested

- new tests/cases/test_generic_enum_match.pith: inferred construction,
  annotated payload-free variant, string payload round trip, two
  instantiations of one base coexisting — output matches its golden
- new tests/invalid/generic_enum_uninferable pins E262; check-invalid
  50/50
- run-regressions-only 364/364, including the recursive Chain[T] test
  whose construction shape (plain tail into an optional payload) the
  new argument check must accept
- bootstrap seed regenerated; make bootstrap-verify reports the fixed
  point, and its run-examples-self pass covers the updated generics
  example
- fmt --check and lint clean on every edited file
@kacy
kacy merged commit b439472 into main Aug 19, 2026
2 checks passed
@kacy
kacy deleted the fix-generic-enum-match branch August 19, 2026 02:28
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