Semantics of
Recursive Types

CS-642 project presentation
Matt Bovel

May 31, 2026

Recursive types

This Scala definition is recursive:

enum List[A]:
  case Nil()
  case Cons(head: A, tail: List[A])

Syntax of STLC + recursive types:

\begin{aligned} \text{Terms} &\quad t ::= x \mid \lambda x. t \mid t_1 \; t_2 \\ \text{Types} &\quad T ::= T_1 \to T_2 \; \, {\color{#a626a4} \bf \mid X \mid \mu X. T} \end{aligned}

Additionally using unit, disjoint sum and pair types, we can define:

\text{List[A]} \triangleq \mu X. \, Unit + (A, X)

A recursive type can be unfolded:

\begin{aligned} \mu X. \, & Unit + (A, X) \\ \equiv \; & Unit + (A, \mu X. \, Unit + (A, X)) \\ \equiv \; & Unit + (A, Unit + (A, \mu X. \, Unit + (A, X))) \\ \end{aligned}

We want a typing rule to unfold:

\frac{\Gamma \vdash t : \mu X. T}{\Gamma \vdash t : T[X \mapsto \mu X. T]}

And one to fold:

\frac{\Gamma \vdash t : T[X \mapsto \mu X. T]}{\Gamma \vdash t : \mu X. T}

Syntax & Definitional interpreter

Inductive Term : Type :=
  | tbool : bool -> Term
  | tvar : nat -> Term
  | tabs : Ty -> Term -> Term
  | tapp : Term -> Term -> Term.

Inductive Ty : Type :=
  | TBool : Ty
  | TFun : Ty -> Ty -> Ty.
  • We use de Bruijn indices for bindings
  • Terms and values are separate
  • Types don’t have runtime semantics
  • Lambdas capture their environment
Inductive Value : Type :=
  | vbool : bool -> Value
  | vabs  : (list Value) -> Term -> Value.
  
Fixpoint eval_wrong (env: list Value) (t: Term) : option Value :=
  match t with
  | tbool b => Some (vbool b)
  | tvar i => nth_error env i
  | tabs _ b => Some (vabs env b)
  | tapp f a =>
      match (eval_wrong env f) with
      | Some (vabs envf b) =>
          match (eval_wrong env a) with
          | None => None
          | Some va => eval_wrong (va::envf) b
          end
      | _ => None
      end
  end.

Definitional interpreter (termination)

We bound depth of recursive calls to ensure termination by adding a fuel parameter.
We return a layered option type: None means timeout, Some None means a runtime error.

Fixpoint eval (fuel: nat) (env: list Value) (t: Term) : option (option Value) :=
  match fuel with
  | 0 => None
  | S fuel' =>
    match t with
    …
    | tapp f a =>
        match (eval fuel' env f) with
        | None => None
        | Some (Some (vabs envf b)) =>
            match (eval fuel' env a) with
            | None => None
            | Some None => Some None
            | Some (Some va) => eval fuel' (va::envf) b
            end
        | _ => Some None
    …

Definitional interpreter (reference)

Semantic Types

The value interpretation defines what it means for a value to have a syntactic type:

Fixpoint interp (T: Ty) (v: Value) : Prop :=
  match T with
  | TBool => exists b, v = vbool b
  | TFun A B =>
      exists env body, v = vabs env body
      /\ forall arg, interp A arg ->
           tinterp (arg::env) body (interp B)
  end.

The term interpretation defines what it means for a term to have a semantic type:

Definition tinterp (env: list Value) (t: Term)
                   (T: Value -> Prop) : Prop :=
  exists v fuel,
    eval fuel env t = Some (Some v) /\ T v.

The semantic typing judgement is defined as:

Definition sem_typed (tenv: list Ty) (t: Term)
                     (T: Ty) : Prop :=
  forall env, Forall2 interp tenv env ->
    tinterp env t (interp T).

It allows us to prove typing rules:

Lemma sem_typed_app: forall tenv f a A B,
  sem_typed tenv f (TFun A B) ->
  sem_typed tenv a A ->
  sem_typed tenv (tapp f a) B.
Proof.
  …
Qed.

\frac{ \Gamma \vDash f : A \to B \qquad \Gamma \vDash a : A }{ \Gamma \vDash f \; a : B }

Semantic Types (reference)

Interpretation of recursive types

Recursive types have a body, and an implicit de Bruijn binder. TVar i refers to i-th bound type.

Inductive Ty : Type :=
  …
  | TVar : nat -> Ty
  | TMu : Ty -> Ty.

Remember, we want \mu X. T \equiv T[X \mapsto \mu X. T].

Tentative interp using substitution:

Fixpoint interp_wrong (T: Ty) (v: Value) : Prop :=
  match T with
  …
  | TVar i => False
  | TMu B => interp (subst_ty 0 (TMu B) B)
  end.

We need step-indexing to break the circularity.

Equations? interp (T: Ty) (k: nat)
                  (v: Value) : Prop
  by wf (k, ty_size T)
    (Equations.Prop.Subterm.lexprod _ _ lt lt) :=
  …
  interp (TMu B) 0 v := True;
  interp (TMu B) (S k') v :=
    interp (subst_ty 0 (TMu B) B) k' v.

Semantic typing quantifies over all steps:

Definition sem_typed (tenv: TyEnv) (t: Term)
                     (T: Ty) : Prop :=
  forall env k,
    Forall2 (fun T v => interp T k v) tenv env ->
    tinterp env t (fun v => interp T k v).

Later types

Remember, we want \mu X. T \equiv T[X \mapsto \mu X. T].

But the well-founded version of interp gives us only \llbracket \mu X. T \rrbracket^k \equiv \llbracket T[X \mapsto\mu X. T] \rrbracket^{k-1}.

We define a later type \triangleright \, T to mean “T but one step later”, so we have: \mu X. T \equiv \triangleright \, T[X \mapsto\mu X. T].

Interpretation of “later”:

interp (TLater T') 0 v := True;
interp (TLater T') (S k') v := interp T' k' v;

We can then prove the following typing rules for recursive types:

\frac{ \Gamma \vDash t : \triangleright \, T[X \mapsto \mu X. T] }{ \Gamma \vDash t : \mu X. T } \qquad \frac{ \Gamma \vDash t : \mu X. T }{ \Gamma \vDash t : \triangleright \, T[X \mapsto \mu X. T] }

Can we get rid of these spooky triangles?

Downward closure

We can avoid the later type in the folding rule if we require that semantic types are downward closed:

T^{k} \subseteq T^{k'} \text{ for all } k' \le k

where S \subseteq T means \forall v, S \, v \implies T \, v.

This is not true with our definition, due to function types contravariance.

Downward-close interp by quantifying over smaller steps in the interpretation of function types:

interp (TFun A B) k v :=
  exists env body, v = vabs env body
  /\ forall j (Hj: j <= k) arg, interp A j arg ->
       tinterp (arg::env) body (interp B j);

Thanks to downward closure of interp, we can now have \llbracket \mu X. T \rrbracket^k \subseteq \llbracket T[X \mapsto\mu X. T] \rrbracket^k, so we can get rid of the later type in the folding rule:

\frac{ \Gamma \vDash t : T[X \mapsto \mu X. T] }{ \Gamma \vDash t : \mu X. T } \qquad \frac{ \Gamma \vDash t : \mu X. T }{ \Gamma \vDash t : \triangleright \, T[X \mapsto \mu X. T] }

Later types in the wild: Scala step-by-step: soundness for DOT with step-indexed logical relations in Iris
Paolo G. Giarrusso, Léo Stefanesco, Amin Timany, Lars Birkedal, and Robbert Krebbers

Indexed judgements

Instead of using later types, can we move steps from the interpretation to the typing judgement?

\frac{ \Gamma \vDash_k t : \mu X. T }{ \Gamma \vDash_{k-1} t : \, T[X \mapsto \mu X. T] }

Definition sem_typed (tenv: TyEnv) (n: nat)
                     (t: Term) (T: Ty) : Prop :=
  forall env k, n < k ->
    Forall2 (fun T v => interp T k v) tenv env ->
    tinterp env t (fun v => interp T (k - n) v).

It makes sense:

Lemma sem_typed_distribute: forall env tenv n e T,
  sem_typed tenv n e T ->
  (forall k,
     Forall2 (fun T v => interp T k v) tenv env) ->
  tinterp env e (fun v => forall j, interp T j v).

For the application rule to go through, we also need to allow a gap inside the function interpretation:

Inductive Ty : Type :=
  …
  | TFun : nat -> Ty -> Ty -> Ty
  …
interp (TFun g A B) k v :=
  exists env body, v = vabs env body
  /\ forall j (Hjlo: g < j) (Hjhi: j <= k) arg,
       interp A j arg ->
       tinterp (arg::env) body (interp B (j - g));

So we still have noise in the syntax,
and now also in the typing rules:

\frac{ \Gamma \vDash_{i} f : A \to_j B \qquad \Gamma \vDash_{k} a : A }{ \Gamma \vDash_{i + j + k} f \; a : B }

Existential gaps

Can we existentially quantify over the gaps?

No.

More in the report.

Science to the rescue

Insight 1. Global steps

In the definitional interpreter, count global evaluation steps instead of depth of recursive calls (🥲).

Equations? eval_sig (fuel: nat) (env: ValueEnv) (t: Term) :
  option ({fuel' : nat | fuel' < fuel} * option Value) by wf fuel lt :=
eval_sig 0 _ _ := None;
…
eval_sig (S fuel) env (tapp f a) with eval_sig fuel env f => {
  | None => None;
  | Some (exist _ fuel1 _, None) => Some (exist _ fuel1 _, None);
  | Some (exist _ fuel1 _, Some (vbool _)) => Some (exist _ fuel1 _, None);
  | Some (exist _ fuel1 _, Some (vabs envf body)) with eval_sig fuel1 env a => {
    | None => None;
    | Some (exist _ fuel2 _, None) => Some (exist _ fuel2 _, None);
    | Some (exist _ fuel2 _, Some va) with eval_sig fuel2 (va::envf) body => {
      | None => None;
      | Some (exist _ fuel3 _, r) => Some (exist _ fuel3 _, r)
    }
  }
}.

Insight 2. Make steps arithmetic work

  1. Tie type steps to evaluation steps.
  2. Decrease steps in the interpretation of all types
    but recursive types.
  3. Interp recursive types as function power.
Definition SemTy : Type := nat -> Value -> Prop.

Definition tinterp (env: list Value) (k: nat)
                   (t: Term) (T: SemTy) : Prop :=
  forall k' r, eval k env t = Some (k', r) ->
    exists v, r = Some v /\ T k' v.

Fixpoint fun_pow {A: Type} (k: nat)
                 (f: A -> A) (z: A) : A :=
  match k with
  | 0 => z
  | S k => f (fun_pow k f z)
  end
Definition mu_F (F: SemTy -> SemTy) : SemTy :=
  fun k v => fun_pow (S k) F Bot k v.

Fixpoint interp (tenv: list SemTy) (T: Ty)
                (k: nat) (v: Value) : Prop :=
  match T with
  | TVar i =>
      match nth_error tenv i with
      | Some A => A k v
      | None => False
      end
  | TBool => exists b, v = vbool b
  | TFun A B =>
      exists env body, v = vabs env body /\
        forall j (Hj: j < k) arg,
          interp tenv A j arg ->
          tinterp (arg::env) j body (interp tenv B)
  | TMu B =>
    mu_F (fun Self => interp (Self :: tenv) B) k v
  end.

Insight 3. Contractive magic

Definition approx (k: nat) (F: SemTy): SemTy :=
  fun j v => j < k /\ F j v.

Definition wf (F: SemTy -> SemTy) : Prop :=
  forall k Z,
    approx (S k) (F Z)
    = approx (S k) (F (approx k Z)).

Lemma F_well_founded: forall tenv B,
  contractive B ->
  wf (fun X => interp (X :: tenv) B).
Definition contractive (T: Ty) : Prop :=
  match T with
  | TVar _ => False
  | TBool => True
  | TFun _ _ => True
  | TMu _ => False
  end.

Means the binder only occurs under other types.
\mu X. X is not contractive, but \mu X. X \to X is.

Insight 3. Contractive magic (continued)

This allows us to prove the following rule:

\frac{ \Gamma \vDash t : T[X \mapsto \mu X. T] \qquad X \text{ contractive in } T }{ \Gamma \vDash t : \mu X. T }\qquad \frac{ \Gamma \vDash t : \mu X. T \qquad X \text{ contractive in } T }{ \Gamma \vDash t : T[X \mapsto \mu X. T] }

Maybe we can have the contractiveness condition by construction.

enum List[A]:
  case Nil()
  case Cons(head: A, tail: List[A]) // okay
type A = A // not okay

Conclusion

We mechanized soundness of 3 different semantics for recursive types:

  1. Depth-bounded + later types
  2. Depth-bounded + indexed judgements
  3. Global steps + contractiveness

This allowed us to better understand the tradeoffs between these approaches.

Approach 3. seems better than what I previously had (positivity restriction).

We also investigated the existential gap approach, and found it doesn’t work.

Equations is a bit annoying to work with.