Skip to content

Implement proper bidirectional type inference

The Practical Type Inference paper (JFP '07) lays out a nice, relatively straightforward bidirectional type inference algorithm for GHC.

The problem is that there is not great hygiene practiced around transitions between "down" checking and "up" checking, neither in the paper nor in the implementation. Examine Figure 8 of the paper, which presents the bidirectional typing rules. There are several syntactic forms which are fundamentally "up" forms -- forms that cannot gain any benefit from any type information being propagated downwards. Examples include App, Var, and Annot. Other forms propagate the very direction they are operated in, such as Let. And then some forms depend somewhat delicately on direction, such as the Abs rules.

I propose changing these rules to be more in the style of Figure 4 from Dunfield & Krishnaswami's ICFP'13 paper here. The particular rule I'm interested in is DeclSub, paraphrased here:

G |-up e : s1
s1 <= s2
---------------
G |-down e : s2

(I'm not advocating other aspects of their approach, just the general idea around this rule being the only transition between down and up.)

In GHC, mediating between "down" checking and "up" checking is done in an ad-hoc manner, made even more difficult by the fact that it takes two separate steps to pull off properly: we must first deeplyInstantiate the "up" type that we get and then call tcSubType (or one of its variants). And, because this has to be done for each "up" expression form (forms that cannot use down-propagated type information), it's easy to miss a case. As a case in point, consider the following code:

foo :: forall b. b -> (Int -> Int) -> b
foo = undefined

bar :: ((forall a. a -> a) -> Int) -> Int
bar = undefined

baz = bar (foo 3)
bum = bar (3 `foo`)

In 7.10.1, baz is accepted, while bum is not. This is because the code for left-sections doesn't ever call tcSubType -- it uses unifyType.

While that one case is easy enough to fix, I advocate a redesign of tcExpr to break it into two functions: one for "down" forms and one for "up" forms. These would, of course, be mutually recursive in order so that both can handle all expression forms. But these transitions could then be policed more easily.

I don't actually think this would entail all that much work, and it would structurally remove the possibility of bugs such as the one above.

Trac metadata
Trac field Value
Version 7.10.1
Type Bug
TypeOfFailure OtherFailure
Priority normal
Resolution Unresolved
Component Compiler (Type checker)
Test case
Differential revisions
BlockedBy
Related
Blocking
CC
Operating system
Architecture
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information