Within Intrinsically-Typed Expressions we saw how to type expressions. In this section, we will look at typing statements and ensuring that statements are in a certain shape i.e. a normal form.

Statement Normal Form

Within Olaf’s syntax we see that statements are inductively defined. We can use our knowledge of AST representations, from earlier, to translate this to the following ADT instance.

data Stmt = Return Expr
          | Print Expr
          | Mutate String Expr
          | Let String Ty Expr Stmt
          | Var String Ty Expr Stmt
          | If Expr Stmt Stmt
          | While Expr Stmt
          | Seq Stmt Stmt

We saw from earlier, that with Expr we can produce ill-typed and ill-scoped terms. With Stmt, as above, we can produce statements that are not in a normal form. By normal form we mean where:

  1. Return statements are not the last statement in a sequence of statements;
  2. Sequencing of statements cannot be nested within the left operand of a sequence;

In some AST versions, Seq is even replaced with an explicit list of statements and that binders do not capture a variable’s scope, the rest of a block does.

data Stmt = Return Expr
          | Print Expr
          | Mutate String Expr
          | Let String Ty Expr
          | Var String Ty Expr
          | If Expr Stmt Stmt
          | While Expr Stmt
          | Block (List Stmt)

Although both version’s of this AST are a quick way to capture the output of parsing a statements from concrete syntax 1 they do not lend themselves well to creating intrinsically typed syntax that are in statement normal form.

We can fix these issues in two ways:

  1. Create an inductive representation of statements, removing the need for an explicit block term;
  2. use dependent types to ensure that the last term in a statement is a return statement;

Ensuring Statements have the Right Shape

First we will show how to ensure statements have the correct shape.

We begin by embedding the idea of sequencing within all statements that may continue with performing computations. Second, we will (re)introduce the notion of the Stop term from Olaf’s dynamic semantics. By treating both return and stop as leafs we know that computation will stop there. All other statements will be branches that contain children. During evaluation we will need to explore the internal branches, depending on dynamic semantics, before potentially exploring the continuation. Thus, statements will have the shape:

data Stmt = Stop                    -- Leaf Nodes
          | Return Expr
          | Mutate String Expr Stmt -- Branch Nodes
          | Let String Ty Expr Stmt
          | Var String Ty Expr Stmt
          | If Expr Stmt Stmt Stmt

Try writing down the following statements using Stmt. You will need to have a version of Expr available.

  1. if true { return "warm hugs"; } else { stop }
  2. stop
  3. if true { return "warm hugs"; } else { stop }; stop; return 1;
  4. let foo : int = 1 in if eq(foo, 1) { return 1; } else { return "snowman"; } stop

What do you notice about their shape and correctness?

If we are defining method bodies, we must ensure that the last statement in the body is a return statement. With Stmt as it has been defined, we cannot do so. We could define an extrinsic proposition acting on instances of Stmt. It would be better, however, if we can make this check intrinsic. We can do so by keep track of what a continuation does within its last statement.

Given the inductive structure of Stmt we know that a statement’s continuation will either stop or return. Thus, we will index the type of Stmt by how it ends, which we will encode in the following datatype:

data HowEnds = STOP | RETURN

Here is a partial rewrite of Stmt, demonstrating how HowEnds works. The two leaf nodes, Stop and Return populate the type level index with how they end. Each branch then uses how the continuation ends to propagate that information back up to the root statement. Second, each child with a branch node may end differently. Enabling early returns to be specified and have children that end differently.

We can use this knowledge to ensure that a statement returns by pinning the value in Stmt with RETURN.

data Stmt : HowEnds -> Type where
  Stop : Stmt STOP

  Return : (expr : Expr)
                -> Stmt RETURN

  Let : (name : String)
     -> (type : Ty)
     -> (expr : Expr)
     -> (kont : Stmt a)
             -> Stmt a

  If : (expr   : Expr)
    -> (whenTT : Stmt a)
    -> (whenFF : Stmt b)
    -> (kont   : Stmt c)
              -> Stmt c

For each of the following statements, try and write instances of Stmt that satisfy the following Idris signatures: You will need to have a version of Expr available.

  1. if true { return "warm hugs"; } else { stop }
  2. stop
  3. if true { return "warm hugs"; } else { stop }; stop; return 1;
  4. let foo : int = 1 in if eq(foo, 1) { return 1; } else { return "snowman"; } stop

Which of the expressions can be written with which Idris type signature and which one’s cannot be written?

Almost Intrinsically-Typed Statements

Our specification of Stmt can ensure statements are in the correct shape, yet we cannot ensure that when we return values that those values all have the same type.

We can use our knowledge of intrinsically-typed expressions to ensure that statements are ‘well-typed’, that is all return statements have the same type. Thus, beginning our journey into intrinsically typed statements.

We do so by further indexing Stmt with the type expected for all return expressions.

data Stmt : (he : HowEnds) -> (type : Ty) -> Type where

With this approach, we can then ensure that the return type has the expected type and propagate the type information along all branches.

We will show this for leaf nodes and let-statements, and assume that you have an intrinsically-typed version of Expr from earlier minus let-expressions.

data Stmt : (he : HowEnds) -> (type : Ty) -> Type where
  Stop : Stmt STOP type
  Return : (expr : Expr type)
                -> Stmt RETURN type
  Let  : (name : String)
      -> (type : Ty)
      -> (expr : Expr typeV)
      -> (kont : Stmt he typeK)
              -> Stmt he typeK

The interesting constructors are Stop and Let. Here, Stop inherits the type from any parent node, and Let ensures that the type of the continuation is the one that types let itself.

Extend Stmt to support conditionals.

Intrinsically-Typed Statements, Finally

Finally, we need to ensure that are variables are well scoped.

As from earlier we must insert a typing context within the type of Stmt. Thus, Stmt becomes:

data Stmt : (he   : HowEnds)
         -> (ctxt : SnocList Ty)
         -> (type : Ty)
                 -> Type where

Adding in contexts into data constructors follows that of how we do expressions. For instance, here are the constructors for Stop, Return, and Let:

data Stmt : (he   : HowEnds)
         -> (ctxt : SnocList Ty)
         -> (type : Ty)
                  -> Type
  where
  Stop : Stmt STOP ctxt type
  Return : (expr : Expr        ctxt type)
                -> Stmt RETURN ctxt type

  Let  : (expr : Expr     ctxt    typeV)
      -> (kont : Stmt he (ctxt :< typeV) typeK)
              -> Stmt he  ctxt           typeK

Anti-climactic, huh?

Taking your existing definition for intrinsically-typed expressions, taken from earlier, provide an intrinsically-typed version that includes statements for:


  1. I know becasue that is how we do it in Olaf.↩︎