fix: construct, infer, and match generic enums like any other enum - #804
Merged
Conversation
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
This was referenced Aug 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 diagnosticE202 unknown type: Opt, because patterns resolved the bare name against the type table instead of the subjectT), so an int payload was treated as a string handle downstreamNothing 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
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 goldentests/invalid/generic_enum_uninferablepins E262; check-invalid 50/50run-regressions-only364/364, including the recursiveChain[T]test whose construction shape (plain tail into an optional payload) the new argument check must acceptmake bootstrap-verifyreports the fixed point, and its run-examples-self pass covers the updated generics examplefmt --checkand lint clean on every edited file