Refined type checking for GADTs (was: Pattern matching: next steps after JEP 405)

Brian Goetz brian.goetz at oracle.com
Tue May 24 18:56:31 UTC 2022


>
>  - *Refined type checking for GADTs. *Given a hierarchy like:
>
>     sealed interface Node<T> { }
>     record IntNode(int i) implements Node<Integer> { }
>     record FloatNode(float f) implements Node<Float> { }
>
> we currently cannot type-check programs like:
>
>     <T> Node<T> twice(Node<T> n) {
>         return switch (n) {
>             case IntNode(int x) -> new IntNode(x*2);
>             case FloatNode(float x) -> new FloatNode(x*2);
>        }
>    }
>
> because, while the match constraints the instantiation of T in each 
> arm of the switch, the compiler doesn't know this yet. 

Much of this problem has already been explored by "Generalized Algebraic 
Data Types and Object Oriented Programming" (Kennedy and Russo, 2005); 
there's a subset of the formalism from that paper which I think can 
apply somewhat cleanly to Java.

The essence of the approach is that in certain scopes (which coincide 
exactly with the scope of pattern binding variables), additional _type 
variable equality constraints_ are injected.  For a switch like that 
above, we inject a T=Integer constraint into the first arm, and a 
T=Float into the second arm, and do our type checking with these 
additional constraints.  (The paper uses equational constraints only 
(T=Integer), but we may want additional upper bounds as well (T <: 
Comprable<T>)).

The way it works in this example is: we gather the constraint Node<T> = 
Node<Integer> from the switch (by walking up the hierarchy and doing 
substitution), and unifying, which gives us the new equational 
constraint T=Integer.  We then type-check the RHS using the additional 
constraints.

The type checking adds some new rules to reflect equational constraints, 
FJ-style:

    \Gamma |- T=U   \Gamma |- C<T> OK
    --------------------------------- abstraction
        \Gamma |- C<T> = C<U>

    \Gamma |- C<T> = C<U>
    --------------------- reduction
        \Gamma |- T=U

    \Gamma |- X OK
    --------------  reflexivity
    \Gamma |- X=X

    \Gamma |- U=T
    -------------  symmetry
    \Gamma |- T=U

    \Gamma |- T=U  \Gamma |- U=V
    ----------------------------  transitivity
    \Gamma |= T=V

     \Gamma |- T=U
    ---------------- subtyping
    \Gamma |- T <: U

The key is that this only affects type checking; it doesn't rewrite any 
types.  Since in the first arm we are trying to assign a IntNode to a 
Node<T>, and IntNode <: Node<Integer>, by symmetry + subtyping, we get 
IntNode <: Node<T>, and yay it type-checks.

The main moving parts of this sub-feature are:

  - Defining scopes for additional constraints/bounds.  This can 
piggyback on the existing language of the form "if v is introduced when 
P is true, then v is definitely matched at X"; we can trivially extend 
this to say "a constraint is definitely matched at X".  This is almost 
purely mechanical.
  - Defining additional type-checking rules to support scope-specific 
constraints, along the lines above, in 4.10 (Subtyping).
  - In the description of type and records patterns (14.30.x), appeal to 
inference to gather equational constraints, and which patterns introduce 
an equational constraint.

This is obviously only a sketch; more details to follow.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20220524/0bf05ce2/attachment.htm>


More information about the amber-spec-experts mailing list