28  Lab: Static Type Checking

Learning Goals

The primary goals of this lab are:

  • Programming with higher-order functions.
  • Static type checking and understanding the interplay between type checking and evaluation.
Functional Programming Skills
Higher-order functions with collections and callbacks.
Programming Language Ideas
Static type checking and type safety. Records.

Instructions

A version of project files for this lab resides in the public pppl-lab4 repository. Please follow separate instructions to get a private clone of this repository for your work.

You will be replacing ??? or case _ => ??? in the Lab4.scala file with solutions to the coding exercises described below.

Your lab will not be graded if it does not compile. You may check compilation with your IDE, sbt compile, or with the “sbt compile” GitHub Action provided for you. Comment out any code that does not compile or causes a failing assert. Put in ??? as needed to get something that compiles without error.

You may add additional tests to the Lab4Spec.scala file. In the Lab4Spec.scala, there is empty test class Lab4StudentSpec that you can use to separate your tests from the given tests in the Lab4Spec class. You are also likely to edit Lab4.worksheet.sc for any scratch work. You can also use Lab4.worksheet.ts to write and experiment in a JavaScript file that you can then parse into a TypeScripty AST (see Lab4.worksheet.sc).

If you like, you may use this notebook for experimentation. However, please make sure your code is in Lab4.scala; code in this notebook will not graded.

28.1 Static Typing: TypeScripty: Functions and Objects

Static Typing

As we have seen in the prior labs, dealing with coercions and checking for dynamic type errors complicate the interpreter implementation (i.e., step). Some languages restrict the possible programs that it will execute to ones that it can guarantee will not result in a dynamic type error. This restriction of programs is enforced with an analysis phase after parsing but before evalation known as type checking. Such languages are called statically-typed. In this lab, we implement a statically-typed version of JavaScripty that we affectionately call TypeScripty. We will not permit any type coercions and simultaneously guarantee the absence of dynamic type errors.

Multi-Parameter Recursive Functions

Using our skills working with higher-order functions on collections from previous assignments, we now consider functions with zero-or-more parameters (instead of exactly one):

typesτ::=(y:τ)=>τvaluesv::=x?(y:τ)τ?=>e1expressionse::=x?(y:τ)τ?=>e1e1(e2)optional variablesx?::=xεoptional type annotationsτ?::=:τε

We write a sequence of things using either an overbar or dots (e.g., y or y1,,yn for a sequence of variables). Functions can now take any number of parameters y:τ. We have a language of types τ and function parameters y are annotated with types τ.

Functions can be named or unnamed x? and can be annotated with a return type or unannotated τ?. To define recursive functions, the function needs to be named and annotated with a return type.

To represent an arbitrary number of function parameters or function call arguments in Scala, we use an appropriate List:

trait Typ  // t
case class TFun(yts: List[(String,Typ)], tret: Typ) extends Typ // t ::= (yts) => tret

trait Expr // e
case class Fun(xopt: Option[String], yts: List[(String,Typ)], tretopt: Option[Typ], e1: Expr) extends Expr // e ::= xopt(yts)tretopt => e1
case class Call(e0: Expr, es: List[Expr]) extends Expr                                                     // e ::= e0(es)
defined trait Typ
defined class TFun
defined trait Expr
defined class Fun
defined class Call

Immutable Objects (Records)

Similarly, we now consider immutable objects that can have an arbitrary number of fields: typesτ::={f:τ}valuesv::={f:v}expressionse::={f:e}e1.f

An object literal expression {f1:e1,,fn:en} is a comma-separated sequence of field names with initialization expressions surrounded by braces. Objects here are more like records in other programming languages compared to actual JavaScript objects, as we do not have any form of mutation or dynamic extension. Fields here correspond to what JavaScript calls properties but which can be dynamically added or removed from objects. We use the term fields to emphasize that they are fixed based on their type: {f1:e1,,fn:en}:{f1:τ1,,fn:τn}

Note that an object value is an object literal expression where each field is a value:

{f1:v1,,fn:vn}

The field read expression e1.f evaluates e1 to an object value and then looks up the field named f.

To represent object types and object literal expressions in Scala, we use an appropriate Map:

case class TObj(fts: Map[String, Typ]) extends Typ    // t ::= { fts }
case class Obj(fes: Map[String,Expr]) extends Expr    // e ::= { fes }
case class GetField(e1: Expr, f: String) extends Expr // e ::= e1.f
defined class TObj
defined class Obj
defined class GetField

Otherwise, we consider our base JavaScripty language that has numbers with arithmetic expressions, booleans with logic and comparison expressions, strings with concatenation, undefined with printing, and const-variable declarations. In summary, the type language τ includes base types for numbers, booleans, strings, and undefined, as well as constructed types for functions and objects described above:

typesτ::=numberboolstringUndefined(y:τ)=>τ{f:τ}

As an aside, we have chosen a concrete syntax that is compatible with the TypeScript language that adds typing to JavaScript. TypeScript is a proper superset of JavaScript, so it is not as strictly typed as TypeScripty is here in this lab.

28.2 Interpreter Implementation

We break our interpreter implementation into evaluation and type checking.

Small-Step Reduction

For evaluation, we continue with implementing a small-step operational semantics with a step that implements a single reduction step ee on closed expressions. Because of the static type checking, the reduction-step cases can be greatly simplified: we eliminate performing all coercions, and what’s cool is that we no longer need to represent the possibility of a dynamic typeerror (e.g., with a Either[DynamicTypeError,Expr]).

We can use the more basic type signature for step:

def step(e: Expr): Expr = ???
defined function step

corresponding to the more basic judgment form ee (given in the subsequent sections).

To make easier to identify implementation bugs, we introduce another Scala exception type to throw when there is no possible next step.

case class StuckError(e: Expr) extends Exception
defined class StuckError

However, the intent of this exception is that it should get thrown at run-time! If it does get thrown, that signals a bug in our interpreter implementation rather than an error in the TypeScripty test input.

In particular, if the TypeScripty expression e passed into step is closed and well-typed (i.e., inferType(e) does not throw StaticTypeError), then step should never throw a StuckError. This property is type safety.

Recall that to implement step, we need to implement a substitution function substitute corresponding to [v/x]e that we use to eagerly apply variable bindings:

def substitute(v: Expr, x: String, e: Expr) = ???
defined function substitute

Static Type Checking

We implement a static type checker that up front rules out programs that would get stuck in taking reduction steps. This type checker is very similar to a big-step interpreter. Instead of computing the value of an expression by recursively computing the value of each sub-expression, we infer the type of an expression, by recursively inferring the type of each sub-expression. An expression is well-typed if we can infer a type for it.

Given its similarity to big-step evaluation, we formalize a type inference algorithm in a similar way. That is, we define the judgment form Γe:τ, which says, “In type environment Γ, expression e has type τ.” We then implement a function hastype:

type TEnv = Map[String, Typ]
def hastype(tenv: TEnv, e: Expr): Typ = ???
defined type TEnv
defined function hastype

that corresponds directly to this judgment form. It takes as input a type environment tenv: TEnv (Γ) and an expression e: Expr (e) to return a type Typ (τ). It is informative to compare the rules defining typing with a big-step operational semantics.

To signal a type error, we will use a Scala exception

case class StaticTypeError(tbad: Typ, esub: Expr, e: Expr) extends Exception
defined class StaticTypeError

where tbad is the type that is inferred sub-expression esub of input expression e. These arguments are used to construct a useful error message. We also provide a helper function err to simplify throwing this exception.

While it is possible to implement iterative reduction via step and type inference viahastype independently, it is generally easier to “incrementally grow the language” by going language-feature by-language-feature for all functions rather than function-by-function. In the subsequent steps, we describe the small-step operational semantics and the static typing semantics together incrementally by language feature.

Notes

For testing your implementation, there are some interface functions defined that calls your step and hastype implementations with some debugging information:

  • The iterateStep: Expr => Expr function repeatedly calls your step implementation until reaching a value.
  • The inferType: Expr => Typ function calls your hastype function with an empty type environment.

Note that the provided tests are minimal. You will want to add your own tests to cover most language features.

28.3 Base TypeScripty

28.3.1 Small-Step Reduction

We consider the base TypeScripty that has numbers with arithmetic expressions, booleans with logic and comparison expressions, strings with concatenation, undefined with printing, and const-variable declarations from previous assignments and remove all coercions.

ee

DoNegn=n1-n1n

DoArithn=n1bopn2bop{+,-,*,/}n1bopn2n

DoPlusStringstr=str1str2str1+str2str

DoInequalityNumberb=n1bopn2bop{<,<=,>,>=}n1bopn2b

DoInequalityStringb=str1bopstr2bop{<,<=,>,>=}str1bopstr2b

DoEqualityb=(v1bopv2)bop{===,!==}v1bopv2b

DoNotb=¬b1!b1b

DoAndTruetrue&&e2e2

DoAndFalsefalse&&e2false

DoOrTruetrue||e2true

DoOrFalsefalse||e2e2

DoIfTruetrue?e2:e3e2

DoIfFalsefalse?e2:e3e3

DoSeqv1,e2e2

DoPrintv1 printedconsole.log(v1)undefined

DoConstconstx=v1;e2[v1/x]e2

SearchUnarye1e1uope1uope1

SearchBinary1e1e1e1bope2e1bope2

SearchBinary2e2e2v1bope2v1bope2

SearchIfe1e1e1?e2:e3e1?e2:e3

SearchPrinte1e1console.log(e1)console.log(e1)

SearchConste1e1constx=e1;e2constx=e1;e2

Figure 28.1: Small-step operational semantics of base TypeScripty, including numbers with arithmetic expressions, booleans with logic and comparison expressions, strings with concatenation, undefined with printing, and const-variable declarations.

Exercise 28.1 (Small-Step Reduction for Base TypeScripty) Implement step for base TypeScripty following the small-step operational semantics in defining the reduction-step judgment form ee.

Note that your task here is simpler than what you have done before in previous assignments. There are no judgment forms or rules defining coercions (e.g., toBoolean) or stepping to a typeerror result (e.g., Left(DynamicTypeError(e))).

You will need to implement a helper function substitute for base TypeScripty to perform scope-respecting substitution [v/x]e as in previous assignments.

Notes

  • You may use (or ignore) the provided helper function doInequality to implement the DoInequalityNumber and DoInequalityString rules.

28.3.2 Static Type Checking

We define static typing with the judgment form Γe:τ of base TypeScripty that has numbers with arithmetic expressions, booleans with logic and comparison expressions, strings with concatenation, undefined with printing, and const-variable declarations.

Γe:τ

TypeNumberΓn:number

TypeStringΓstr:string

TypeNegΓe1:numberΓ-e1:number

TypeArithΓe1:numberΓe2:numberbop{+,-,*,/}Γe1bope2:number

TypePlusStringΓe1:stringΓe2:stringΓe1+e2:string

TypeInequalityNumberΓe1:numberΓe2:numberbop{<,<=,>,>=}Γe1bope2:bool

TypeInequalityStringΓe1:stringΓe2:stringbop{<,<=,>,>=}Γe1bope2:bool

TypeEqualityΓe1:τΓe2:τbop{===,!==}Γe1bope2:bool

TypeBoolΓb:bool

TypeNotΓe1:boolΓ!e1:bool

TypeAndOrΓe1:boolΓe2:boolbop{&&,||}Γe1bope2:bool

TypeIfΓe1:boolΓe2:τΓe3:τΓe1?e2:e3:τ

TypeUndefinedΓundefined:Undefined

TypeSeqΓe1:τ1Γe2:τ2Γe1,e2:τ2

TypePrintΓe1:τ1Γconsole.log(e1):Undefined

TypeVarΓx:Γ(x)

TypeConstDeclΓe1:τ1Γ,x:τ1e2:τ2Γconstx=e1;e2:τ2

Figure 28.2: Typing of base TypeScripty, including numbers with arithmetic expressions, booleans with logic and comparison expressions, strings with concatenation, undefined with printing, and const-variable declarations.

Observe how closely the Type rules align with the Do rules in , except for having a big-step evaluation structure with types.

Exercise 28.2 (Static Type Checking for Base TypeScripty) Implement a function hastype for base TypeScript following the static typing semantics in defining the typing judgment form Γe:τ.

type TEnv = Map[String, Typ]
def hastype(tenv: TEnv, e: Expr): Typ = ???
defined type TEnv
defined function hastype

28.4 Immutable Objects (Records)

Next, we extend our interpreter implementation for immutable objects. We consider the implementation for immutable objects next, as it is a bit simpler than that for multi-parameter functions.

28.4.1 Small-Step Reduction

We extend the reduction-step judgment form ee for immutable objects:

ee

DoGetField{f1:v1,,fi:vi,,fn:vn}.fivi

SearchObjecteieiej=vjfor all j<i{f1:e1,,fi:ei,}{f1:e1,,fi:ei,}

SearchGetFielde1e1e1.fe1.f

Figure 28.3: Small-step operational semantics of TypeScripty with immutable objects.

Exercise 28.3 (Small-Step Reduction for Immutable Objects) Implement the cases in step for reducing immutable object expressions using the rules given in for the reduction-step judgment form ee.

Notes

  • Field names f are different than variable names x, even though they are both represented in Scala with a String. Object expressions are not variable binding constructs—what does that mean about substitute for them?

  • For SearchObject, you should make the reduction step apply to the first non-value as given by the left-to-right iteration of the collection using the find method on Maps:

    (m: Map[K,V]).find(f: ((K,V)) => Boolean): Option[(K,V)]
  • Other helpful Scala library methods not previously mentioned to use here include the following:

    (m: Map[K,V]).get(k: K): Option[V]

28.4.2 Static Type Checking

We extend the static typing judgment form Γe:τ for immutable objects:

TypeObjectΓei:τifor all iΓ{,fi:ei,}:{,fi:τi,}

TypeGetFieldΓe:{,f:τ,}Γe.f:τ

Figure 28.4: Typing of TypeScripty with immutable objects.

Exercise 28.4 (Static Type Checking for Immutable Objects) Implement the cases in hastype for typing immutable object expressions using the rules given in defining the typing judgment form Γe:τ.

Notes

  • Other helpful Scala library methods not previously mentioned to use here include the following:

    (m: Map[K,V]).map(f: ((K,V)) => (J,U)): Map[J,U]

28.5 Multi-Parameter Recursive Functions

Finally, we extend our interpreter implementation for multi-parameter recursive functions.

28.5.1 Small-Step Reduction

We extend the reduction-step judgment form ee for multi-paramter recursive functions:

ee

DoCall((y1:τ1,,yn:τn)τ?=>e)(v1,vn)[v1/y1][vn/yn]e

DoCallRecv=(x(y1:τ1,,yn:τn):τ=>e)v(v1,vn)[v/x][v1/y1][vn/yn]e

SearchCall1eee(e1,,en)e(e1,,en)

SearchCall2eieiej=vjfor all j<iv(e1,,ei,,en)v(e1,,ei,,en)

Figure 28.5: Small-step operational semantics for TypeScripty with multi-parameter recursive functions.

Exercise 28.5 (Small-Step Reduction for Multi-Parameter Recursive Functions) Implement the cases in step for reducing multi-parameter recursive functions using the rules given in for the reduction-step judgment form ee.

Notes

  • Other helpful Scala library methods not previously mentioned to use here include the following:

    (l: List[A]).map(f: A => B): List[B]
    (l: List[A]).exists(f: A => Boolean): Boolean
    (la: List[A]).zip(lb: List[B]): List[(A,B)]
    (l: List[A]).forall(f: A => Boolean): Boolean
    (l: List[A]).foldRight(f: (A,B) => B): B
    • You may want to use the zip method for the DoCall and DoCallRec cases to match up formal parameters and actual arguments.
  • You might want to use your mapFirst function from Homework 4 here.

28.5.2 Static Type Checking

We extend the static typing judgment form Γe:τ for multi-parameter recursive functions:

TypeCallΓe:(y1:τ1,,yn:τn)=>τΓe1:τ1Γen:τnΓe(e1,,en):τ

TypeFunctionΓ,y1:τ1,,,yn:τne:τΓ(y:τ)=>e:(y:τ)=>τ

TypeFunctionAnnΓ,y1:τ1,,,yn:τne:τΓ(y:τ):τ=>e:(y:τ)=>τ

TypeFunctionRecΓ,x:τx,y1:τ1,,,yn:τne:ττx=(y:τ)=>τΓx(y:τ):τ=>e:τx

Figure 28.6: Typing of TypeScripty with multi-parameter recursive functions.

Exercise 28.6 (Static Type Checking for Immutable Objects) Implement the cases in hastype for typing multi-parameter recursive function expressions using the rules given in defining the typing judgment form Γe:τ.

Notes

  • Other helpful Scala library methods not previously mentioned to use here include the following:

    (l: List[A]).foldLeft(f: (B,A) => B): B
    (l: List[A]).foreach(f: A => Unit): Unit
    (l: List[A]).length: Int
    (m1: Map[K,V]).++(m2: Map[K,V]): Map[K,V]
    • The ++ method on Maps appends two Maps together.