%% The metatheory of algorithmic F-sub subtyping.
%% By Michael Ashley-Rollman, Karl Crary, and Robert Harper.
%%%%% Syntax %%%%%
tp : type. %name tp T.
top : tp.
arrow : tp -> tp -> tp.
forall : tp -> (tp -> tp) -> tp.
%%%%% Semantics %%%%%
assm : tp -> tp -> type. %% subtyping assumptions
sub : tp -> tp -> type. %% subtyping judgement
%{
The object language (OL) context
x1 <: T1, ..., xn <: Tn
is represented by
x1:tp, d1:assm x1 T1, ..., xn:tp, dn:assm xn Tn
This is the only way an assm judgement can arise, so we can identify
variables as those types T such that a judgement assm T _ is available.
}%
sub_top : sub _ top.
sub_refl : sub X X
<- assm X _.
sub_trans : sub X T
<- assm X U
<- sub U T.
sub_arrow : sub (arrow S1 S2) (arrow T1 T2)
<- sub T1 S1
<- sub S2 T2.
sub_forall : sub (forall S1 ([x] S2 x)) (forall T1 ([x] T2 x))
<- sub T1 S1
<- ({x} assm x T1 -> sub (S2 x) (T2 x)).
%%%%% Variables %%%%%
%{
In the proof, it will be necessary to have a way to identify variables
that does not become entangled with dependencies. The assm judgement
does become so entangled, because "assm X T" is dependent on T. So we
introduce a new judgement var, that mentions only the variable in
question.
}%
var : tp -> type.
%%%% Blocks %%%%%
%{
In the proof, we will maintain the invariant that whenever we add a
variable (x:tp) and its subtyping assumption (d:assm x t, for some t)
to the context, we will also add a variable assumption (dv:var x).
This will allow us to prove that whenever "assm X T" is present, so is
"var X". This lemma is called assm_var below.
}%
assm_var : assm T _ -> var T -> type.
%mode assm_var +X1 -X2.
%{
Since the cases for assm_var depend on assumptions that are on only in
scope once we extend the context, we must put those cases into the
context with them. Thus, in addition to adding var assumptions, we
will also add assm_var assumptions. Hence, we have the following
block from which to build contexts:
}%
%block bind : some {t:tp}
block {x:tp} {dv:var x} {d:assm x t}
{thm:assm_var d dv}.
%{
In addition to the bind block, we have (for highly technical reasons,
explained later) two other blocks that arise:
- Vblock provides variables but not subtyping assumptions. Since it
does not add an assm assumption, it need not add a case for
assm_var. However, it does still provide a var assumption, so it
remains true that a var assumption is available for every variable.
- Ablock provides a subtyping assumption, but not the variable
itself. The some portion of the block asserts that a var
assumption exists for the variable in question. Since ablock adds
an assm assumption, it must also add a case for assm_var.
}%
%block vblock : block {t:tp} {d:var t}.
%block ablock : some {x:tp} {dv:var x} {t:tp}
block {d:assm x t}
{thm:assm_var d dv}.
%{
Assm_var is true in any context made up from bind, vblock, and ablock
blocks.
}%
%worlds (bind | vblock | ablock) (assm_var _ _).
%total {} (assm_var _ _).
%%%%% Reductio ad absurdio %%%%%
%% The underivable judgement.
false : type.
%% False implies any subtyping judgement you like.
raa_sub : false -> sub S T -> type.
%mode +{S:tp} +{T:tp} +{X1:false} -{X2:sub S T} (raa_sub X1 X2).
%worlds (bind | vblock | ablock) (raa_sub _ _).
%total {} (raa_sub _ _).
%%%%% Various things are not variables %%%%%
%{
In any world of interest (that is, any world made from bind, vblock,
and ablock), we never add a var assumption for anything but a fresh
variable. In particular, we will never have a var assumption for any
top, arrow, or forall type.
Put more pithily: top, arrow, and forall are not variables.
}%
top_var_contra : var top -> false -> type.
%mode top_var_contra +X1 -X2.
%worlds (bind | vblock | ablock) (top_var_contra _ _).
%total {} (top_var_contra _ _).
arrow_var_contra : var (arrow T1 T2) -> false -> type.
%mode arrow_var_contra +X1 -X2.
%worlds (bind | vblock | ablock) (arrow_var_contra _ _).
%total {} (arrow_var_contra _ _).
forall_var_contra : var (forall T1 T2) -> false -> type.
%mode forall_var_contra +X1 -X2.
%worlds (bind | vblock | ablock) (forall_var_contra _ _).
%total {} (forall_var_contra _ _).
%%%%% Natural numbers %%%%%
nat : type. %name nat N.
z : nat.
s : nat -> nat.
nat_eq : nat -> nat -> type.
nat_eq_ : nat_eq N N.
%%%%% Transivity and Narrowing %%%%%
%{
The main proof for transitivity and narrowing. The proof is by
induction on:
(1st) the type Q
(2nd) the clause, where we take trans* < narrow*
(that is, narrow* can always call trans* with no change in Q,
but when trans* calls narrow*, Q must decrease)
(3rd) in trans*, the derivation of sub S Q,
and in narrow*, the derivation of sub M N
Twelf requires that induction arguments be explicit, hence the first
two arguments to trans* and narrow*. The third argument ensures that
the clause number is correct for the clause. (That is, the clause
number is zero in trans* and one in narrow*).
The statement of trans* is otherwise straightforward. The statement
of narrow* involves one additional subtlety:
In narrow*, we must identify the variable with respect to which we are
narrowing. The most natural way to do so is to abstract over that
variable; thus taking an argument of type:
{X:tp} var X -> assm X Q -> sub M N
Unfortunately, we run afoul of a complication. Obviously, the LF
context may contain dependencies only on other things in the LF
context. If we abstract over the variable of interest, that variable
is not in the context, and therefore nothing in the context may depend
on it. As a result, we effectively can narrow only with respect to
the last variable. However, this is not strong enough for the proof
to go through, since we must descend inside forall types.
Thus, when we are narrowing with respect to a variable X, we must find
a way to move assumptions that depend on X to the left of it without
violating scoping constraints. If we maintain the context in its
usual form:
t1:tp, dv1:var t1, d1:assm t1 T1, ..., dn:tp, dvn:var tn, dn:assm tn Tn
then this is impossible. However, we may weaken our context
assumption to allow variables to appear apart from their typing
assumptions. To adhere to scoping constraints, we must keep
dependencies to the right of the variables they depend on, but we need
not keep them to the right of those variables typing assumptions.
For example, suppose we encounter:
..., ti:tp, dvi:var ti, di:assm ti Ti, tj:tp, dvj:var tj, dj:assm tj Tj(ti)
and we wish to move tj out of the way. Then we produce:
..., ti:tp, dvi:var ti, tj:tp, dvj:var tj, dj:assm tj Tj(ti), di:assm ti Ti
This keeps the subtyping assumption di last, and violates no scoping constraints.
Thus, we identify the variable for narrowing not by abstracting over
the entire variable, but only over its subtyping assumption.
As a consequence of this, we must now deal with variables bereft of
their subtyping assumptions (vblock), and with disembodied subtyping
assumptions (ablock). Note that disembodied subtyping assumptions are
substantially at odds with our usual experience, and we must
reconsider whether the proof works at all in their presence.
Fortunately, with some minor modifications, the proof still works.
The only issue that arises is that it now appears as though the refl
and trans might apply to non-variable types, since those types might
now have subtyping assumptions. However, the assm_var lemma applies,
and states that subtyping assumptions are available only for
variables, so a contradiction arises in each case where we suppose a
subtyping assumption for a non-variable.
}%
trans* : {Q:tp}
{Ncase:nat}
nat_eq Ncase z
%%
-> sub S Q
-> sub Q T
%%
-> sub S T -> type.
narrow* : {Q:tp}
{Ncase:nat}
nat_eq Ncase (s z)
%%
-> var X %{ We take this argument to maintain the invariant
that var X is available whenever assm X Q is.
Otherwise, the body of the next argument would not
enjoy that invariant. }%
-> (assm X Q -> sub M N)
-> sub P Q
%%
-> (assm X P -> sub M N) -> type.
%mode trans* +Q +N +X1 +X2 +X3 -X4.
%mode narrow* +Q +N +X1 +X2 +X3 +X4 -X5.
-top : trans* _ _ _ D sub_top sub_top.
-refl : trans* _ _ _ (sub_refl _) D D.
-trans* : trans* _ _ nat_eq_ (sub_trans D2 D1) D (sub_trans D' D1)
<- trans* _ _ nat_eq_ D2 D D'.
-arrow : trans* _ _ nat_eq_ (sub_arrow D1b D1a) (sub_arrow D2b D2a) (sub_arrow Db Da)
<- trans* _ _ nat_eq_ D2a D1a Da
<- trans* _ _ nat_eq_ D1b D2b Db.
-forall : trans* _ _ _ (sub_forall D1b D1a) (sub_forall D2b D2a) (sub_forall Db Da)
<- trans* _ _ nat_eq_ (D2a : sub T1 T2) D1a Da
<- ({x} {dv:var x} narrow* _ _ nat_eq_ dv ([d] D1b x d) D2a ([d] D1b' x d))
<- ({x} {dv:var x} {d:assm x T1}
assm_var d dv
-> trans* _ _ nat_eq_ (D1b' x d) (D2b x d) (Db x d)).
%% Contradiction cases, for dealing with disembodied subtyping assumptions for non-variables.
- : trans* top _ _ _ (sub_refl Dassm) D
<- assm_var Dassm Dvar
<- top_var_contra Dvar Dfalse
<- raa_sub Dfalse D.
- : trans* (arrow _ _) _ _ _ (sub_refl Dassm) D
<- assm_var Dassm Dvar
<- arrow_var_contra Dvar Dfalse
<- raa_sub Dfalse D.
- : trans* (forall _ _) _ _ _ (sub_refl Dassm) D
<- assm_var Dassm Dvar
<- forall_var_contra Dvar Dfalse
<- raa_sub Dfalse D.
- : trans* top _ _ _ (sub_trans _ Dassm) D
<- assm_var Dassm Dvar
<- top_var_contra Dvar Dfalse
<- raa_sub Dfalse D.
- : trans* (arrow _ _) _ _ _ (sub_trans _ Dassm) D
<- assm_var Dassm Dvar
<- arrow_var_contra Dvar Dfalse
<- raa_sub Dfalse D.
- : trans* (forall _ _) _ _ _ (sub_trans _ Dassm) D
<- assm_var Dassm Dvar
<- forall_var_contra Dvar Dfalse
<- raa_sub Dfalse D.
-closed : narrow* _ _ _ Dvar ([d] D) _ ([d] D).
-refl : narrow* _ _ _ Dvar ([d] sub_refl d) _ ([d] sub_refl d).
-trans : narrow* _ _ nat_eq_ Dvar ([d] sub_trans (D d) Dassm) Dsub ([d] sub_trans (D' d) Dassm)
<- narrow* _ _ nat_eq_ Dvar D Dsub D'.
-trans : narrow* _ _ nat_eq_ Dvar ([d] sub_trans (D d) d) Dsub ([d] sub_trans (D'' d) d)
<- narrow* _ _ nat_eq_ Dvar D Dsub D'
<- ({d:assm X Q}
assm_var d Dvar
-> trans* _ _ nat_eq_ Dsub (D' d) (D'' d)).
-arrow : narrow* _ _ nat_eq_ Dvar ([d] sub_arrow (D2 d) (D1 d)) Dsub ([d] sub_arrow (D2' d) (D1' d))
<- narrow* _ _ nat_eq_ Dvar D1 Dsub D1'
<- narrow* _ _ nat_eq_ Dvar D2 Dsub D2'.
-forall : narrow* _ _ nat_eq_ Dvar ([d] sub_forall (D2 d) (D1 d)) Dsub ([d] sub_forall (D2' d) (D1' d))
<- narrow* _ _ nat_eq_ Dvar D1 Dsub D1'
<- ({x'} {dv'} {d'}
assm_var d' dv'
-> narrow* _ _ nat_eq_ Dvar ([d] D2 d x' d') Dsub ([d] D2' d x' d')).
%worlds (bind | vblock | ablock) (trans* _ _ _ _ _ _) (narrow* _ _ _ _ _ _ _).
%total {(Q1 Q2) (N1 N2) (D1 D2)} (trans* Q1 N1 _ D1 _ _) (narrow* Q2 N2 _ _ D2 _ _).
%%%%% Peroration %%%%%
%{
At this level we no longer need our context mangling, so trans and
narrow are proven for worlds built from only bind. We also can tidy
away the extra stuff needed for the termination arguments for trans*
and narrow*.
}%
trans : sub S Q -> sub Q T -> sub S T -> type.
%mode trans +X1 +X2 -X3.
- : trans D1 D2 D3
<- trans* _ _ nat_eq_ D1 D2 D3.
%worlds (bind) (trans _ _ _).
%total {} (trans _ _ _).
narrow : ({x} assm x Q -> sub M N) -> sub P Q -> ({x} assm x P -> sub M N) -> type.
%mode narrow +X1 +X2 -X3.
- : narrow D1 D2 D3
<- ({x} {dv} narrow* _ _ nat_eq_ dv ([d] D1 x d) D2 ([d] D3 x d)).
%worlds (bind) (narrow _ _ _).
%total {} (narrow _ _ _).
%%%%% Reflexivity %%%%%
%{
Reflexivity is easy, but when we add a variable to the context, we
need to add its reflexivity case there, hence the reflx assumption in
rblock and rbind. Rblock exists for laziness purposes; since reflx
never calls trans, we needn't bother to maintain trans's invariants.
}%
reflx : {T} sub T T -> type.
%mode reflx +X1 -X2.
%block rblock : some {t:tp}
block {x:tp} {d:assm x t}
{thm:reflx x (sub_refl d)}.
%block rbind : some {t:tp}
block {x:tp} {dv:var x} {d:assm x t}
{thm:assm_var d dv}
{thm':reflx x (sub_refl d)}.
- : reflx top sub_top.
- : reflx (arrow T1 T2) (sub_arrow T2refl T1refl)
<- reflx T1 T1refl
<- reflx T2 T2refl.
- : reflx (forall T1 T2) (sub_forall T2refl T1refl)
<- reflx T1 T1refl
<- ({x} {d}
reflx x (sub_refl d)
-> reflx (T2 x) (T2refl x d)).
%worlds (rblock | rbind) (reflx _ _).
%total T (reflx T _).
%%%%% Declarative subtyping %%%%%
subtype : tp -> tp -> type.
subtype_top : subtype _ top.
subtype_refl : subtype T T.
subtype_trans : subtype T1 T3
<- subtype T1 T2
<- subtype T2 T3.
subtype_arrow : subtype (arrow S1 S2) (arrow T1 T2)
<- subtype T1 S1
<- subtype S2 T2.
subtype_forall : subtype (forall S1 S2) (forall T1 T2)
<- subtype T1 S1
<- ({x} subtype x T1 -> subtype (S2 x) (T2 x)).
%%%%% Correctness of the algorithm %%%%%
assm_to_subtype : assm S T -> subtype S T -> type.
%mode assm_to_subtype +X1 -X2.
%block sbind : some {t:tp}
block {x:tp} {d:assm x t} {d':subtype x t}
{thm:assm_to_subtype d d'}.
%worlds (sbind) (assm_to_subtype _ _).
%total {} (assm_to_subtype _ _).
soundness : sub S T -> subtype S T -> type.
%mode soundness +X1 -X2.
- : soundness sub_top subtype_top.
- : soundness (sub_refl _) subtype_refl.
- : soundness (sub_trans D2 D1) (subtype_trans D2' D1')
<- assm_to_subtype D1 D1'
<- soundness D2 D2'.
- : soundness (sub_arrow D2 D1) (subtype_arrow D2' D1')
<- soundness D1 D1'
<- soundness D2 D2'.
- : soundness (sub_forall D2 D1) (subtype_forall D2' D1')
<- soundness D1 D1'
<- ({x} {d:assm x T} {d':subtype x T}
assm_to_subtype d d'
-> soundness (D2 x d) (D2' x d')).
%worlds (sbind) (soundness _ _).
%total (D) (soundness D _).
completeness : subtype S T -> sub S T -> type.
%mode completeness +X1 -X2.
%block cbind : some {t:tp} {d_refl:sub t t}
block {x:tp} {dv:var x} {d:assm x t}
{thm1:assm_var d dv}
{thm2:reflx x (sub_refl d)}
{d':subtype x t} %% put here due to weakness in world subsumption
{thm3:completeness d' (sub_trans d_refl d)}.
- : completeness subtype_top sub_top.
- : completeness subtype_refl D
<- reflx _ D.
- : completeness (subtype_trans D2 D1) D
<- completeness D1 D1'
<- completeness D2 D2'
<- trans D1' D2' D.
- : completeness (subtype_arrow D2 D1) (sub_arrow D2' D1')
<- completeness D1 D1'
<- completeness D2 D2'.
- : completeness (subtype_forall D2 D1) (sub_forall D2' D1')
<- completeness D1 D1'
<- reflx _ Drefl
<- ({x:tp} {dv:var x} {d:assm x T}
assm_var d dv
-> reflx x (sub_refl d)
-> {d':subtype x T}
completeness d' (sub_trans Drefl d)
-> completeness (D2 x d') (D2' x d)).
%worlds (cbind) (completeness _ _).
%total (D) (completeness D _).