Check let bindings through active patterns like a match (#16856) - #20383
Check let bindings through active patterns like a match (#16856)#20383edgarfgp wants to merge 4 commits into
Conversation
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
|
T-Gro
left a comment
There was a problem hiding this comment.
This is a lovely fix — the framing that a pattern binding through an active pattern is just match rhs with pat -> ..., so it should generalize exactly like the match does, is the right mental model and makes the whole change fall out naturally.
A few things I checked that make this safe to take:
-
The no-regression argument holds in both directions. When the RHS is generalizable the old code always hit the internal error, and when it isn't there were no inferred typars to drop, so
[]changes nothing. And because the flag is only set inTcPatLongIdentActivePatternCase, union-case patterns likelet (Some x) = Some idstill generalize — the narrowing is exactly the pattern class the match compiler can't compile at a generic instantiation. -
The threading is airtight:
usesAP1 || usesAP2is the correct join for or-patterns, and sinceCheckedBindingInfois abstract inCheckExpressions.fsithe extra field needs no signature change while the DU arity still makes it impossible to miss a construction site. -
The tests earn their keep by asserting the active pattern runs exactly once and that the value-restriction case matches
let g = match id with Id g -> g, rather than just checking the crash is gone.
One optional follow-up: an active pattern anywhere in the pattern de-generalizes the whole binding (let (Id g), h = id, id no longer generalizes h). That's match-consistent and correct, but a one-line test would pin the intent so a future reader doesn't mistake it for an oversight.
Problem
Using an active pattern in a
letbinding whose right-hand side is a generic value crashes the compiler with an internal error, while the equivalentmatchis fine.Fixes #16856.
Before
After
Cause
TcLetBindinggeneralizes the right-hand side before it looks at the pattern.idis a generalizable value, so the binding becomes a genericpatternInputTmp<'a>and the match compiler is asked to compile an active pattern against a generic input, which it can't do (the active pattern result is computed once at a dummy instantiation), hence the guard.A pattern binding through an active pattern evaluates that active pattern, so it is now checked exactly like
match rhs with pat -> ...: as for a non-generalizable right-hand side, no inferred type parameters are candidates for generalization. Phase-1 pattern checking records whether an active pattern occurs (TcPatLinearEnv.usesActivePattern, set inTcPatLongIdentActivePatternCase),CheckedBindingInfocarries it, andTcLetBindinguses it. Nothing that compiled before is affected, since any such binding with generalized type parameters hit the internal error.Pattern bindings without active patterns are unchanged and still generalize, e.g.
let (a, b) = id, id.