Fix record accessors for higher-rank field types#6239
Open
runarorama wants to merge 3 commits into
Open
Conversation
A record-style data declaration whose field has a rank-N type — e.g.
type Box = { unbox : forall a. a -> a }
— failed to typecheck. The auto-generated getter
@\b -> case b of Box f -> f@ has no type annotation, so synthesis
infers @box -> a2 -> a1@ (the inner forall instantiates to
distinct existentials that can't be re-generalized in a purely
inferred result), and the typechecker complains:
I found a value of type: a2 -> a1
where I expected to find: a -> a
The setter and modifier hit the same issue from the other side
(constructor application with a rank-N argument).
The fix is to give each auto-generated accessor an explicit type
annotation built from the data declaration's tyvars and the
field's type. Check-direction skolemization handles the nested
'forall's correctly, so the same accessor body that previously
failed inference now type-checks cleanly.
Each field's annotation is shaped as:
getter: forall ddTyvars. T ddTyvars -> fieldType
setter: forall ddTyvars. fieldType -> T ddTyvars -> T ddTyvars
modifier:
forall ddTyvars. (fieldType -> fieldType)
-> T ddTyvars -> T ddTyvars
The field types come from the /resolved/ data declaration's
constructor (which already has every type reference bound to the
namespace) rather than the raw parsed types — otherwise an
annotation built from a parsed field type referencing another
type in the same file would surface as
"I don't know about the type X" because that reference hasn't
been resolved at accessor-generation time.
Refs #498.
The rank-N accessor fix makes auto-generated setter/modifier types consistent in their data-type tyvars: instead of an inferred 'Decoy a1 -> Decoy a' (distinct existentials at input vs output positions), they now report 'Decoy a -> Decoy a'. Regenerated 'fix-pattern-capture' transcript accordingly. Tests proof refreshed; transcripts proof left untouched (a flaky timing-noise diff in mcp.md keeps causing local re-records to record 'fail' even though the run itself succeeds — CI will re-verify in a controlled environment).
pchiusano
requested changes
Jun 1, 2026
Member
pchiusano
left a comment
There was a problem hiding this comment.
I could have sworn I added a review of this but maybe I left it in drafts on my phone. Anyway, while I haven't reviewed the code in detail, this could use a transcript test to exercise, and it should illustrate being able to change the type of a field that is the sole field referencing some type variable(s). I remember this being the tricky part of writing out the fully general type.
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.
A record-style data declaration whose field has a rank-N type — e.g.
failed to typecheck. The auto-generated getter
cases Box f -> fhas no type annotation, so synthesis infersBox -> a2 -> a1(the innerforallinstantiates to distinct existentials that can't be re-generalized in a purely inferred result), and the typechecker complains:The setter and modifier hit the same issue from the other side (constructor application with a rank-N argument).
The fix is to give each auto-generated accessor an explicit type annotation built from the data declaration's tyvars and the field's type. Check-direction Skolemization handles the nested
foralls correctly, so the same accessor body that previously failed inference now type-checks cleanly.Each field's annotation is shaped as:
The field types come from the /resolved/ data declaration's constructor (which already has every type reference bound to the namespace) rather than the raw parsed types — otherwise an annotation built from a parsed field type referencing another type in the same file would surface as "I don't know about the type X" because that reference hasn't been resolved at accessor-generation time.
Fixes #498.