Fork me on GitHub

Programming Languages and Types, Winter term 2012/13

Lecture on the structure of syntax and semantics; and their connection by evaluation (in one direction) and reification (in the other direction).

Substitute lecture by Tillmann Rendel

  

INTRODUCTION

The motivating example for this lecture is the following FAE program:

App(Fun(‘y, Fun('x, Add('x, Add('y, 1)))), 2)

Evaluating this program yields …

… in the substitution-based interpreter:

Fun('x, Add('x, Add(2, 1)))

… in the environment-based interpreter:

FunV('x, Add('x, Add('y, 1)), 'y –> 2)

Neither of these interpreters computes 2 + 1 = 3. This computation is left for later, when the function is actually called.

There are (at least) two reasons for performing some computations in a function body earlier:

(1) It can save work: If the function is called often, we should do as much work as possible before constructing the function value.

(2) It can help us to recognize equivalent programs.

To understand the second point, we have to understand that there are many programs that describe the same function. For example:

Abs('x, Add('x, Add(1, 1)))
Abs('x, Add(1, Add('x, 1)))
Abs('x, Add('x, 2))

All of these programs describe the function that adds two to its argument. We say that these programs are equivalent. Among this set of equivalent programs, the last program is in a normal form, because it does not contain any subcomputations that we could perform right now. One goal of today’s lecture is to transform a program into its normal form.

I also want to use this lecture to introduce my idea of how an interpreter should be structured: I believe that one should carefully distinguish between the syntactical and the semantical structure of a language.

        Syntax                           Semantics    

Objects: Terms / Expressions / Programs Values

Structure: substitution, … application, …

Evaluation is a mapping from Syntax to Semantics:

Syntax    —-evaluation—>  Semantics

SYNTAX

We start with the syntax of FAE as presented in earlier lectures. I put everything into a Syntax object to distinguish syntax and semantics more clearly from each other.

object Syntax {
  sealed abstract class Exp
  case class Num(n: Int) extends Exp
  case class Id(name: Symbol) extends Exp
  case class Add(lhs: Exp, rhs: Exp) extends Exp
  case class Fun(param: Symbol, body: Exp) extends Exp
  case class App (funExpr: Exp, argExpr: Exp) extends Exp

  implicit def num2exp(n: Int) = Num(n)
  implicit def id2exp(s: Symbol) = Id(s)
  def wth(x: Symbol, xdef: Exp, body: Exp) : Exp = App(Fun(x, body), xdef)
}

Examples

As example terms, we use the motivating example from above, with two different ways to apply the function to 2. Either directly, or indirectly via an application function written in FAE.

object Example {
  import Syntax._

  val TERM = Fun('y, Fun('x, Add('x, Add('y, 1))))
  val APPLY = Fun('f, Fun('x, App('f, 'x)))

  val TEST1 = App(TERM, 2)
  val TEST2 = App(App(APPLY, TERM), 2)
}

SEMANTICS

We now define the semantic structure of FAE. In this first version of the semantics, we only include features that we need for evaluation.

Notes:

(1) The semantics is only about values, and not about expressions at all. We make this explicit by not even importing the members of the Syntax object above.

(2) The interpreter is meta-circular: FAE numbers are represented as Scala integers, and FAE functions are represented as Scala functions.

(3) We introduce a name for the function type Env => Value. This will be the return type of the eval function. One way to understand the name “domain” is: From all the things in the (mathematical) world, we select a subset of things we want to talk about in our language FAE, and this subset of things of interest forms the domain of our language. (Not to be confused with the domain of a function, which is the subset of all the things in the world a function can be applied to).

(4) We have a function for each syntactic construct of FAE. These functions operate on things in the Domain. They perform all the work that is necessary to implement the syntactic construct.

object Semantics {
  type Domain = Env => Value

  type Env = Map[Symbol, Value]

  abstract sealed class Value
  case class Fun(f : Value => Value) extends Value
  case class Num(n : Int) extends Value

  def num(n : Int) : Domain =
    (env : Env) => Num(n)

  def id(x : Symbol) : Domain =
    (env : Env) => env(x)

  def add(lhs : Domain, rhs : Domain) : Domain =
    (env : Env) => (lhs(env), rhs(env)) match {
      case (Num(n1), Num(n2)) => Num(n1 + n2)
      case _ => sys.error("not happy")
    }

  def app(fun : Domain, arg : Domain) : Domain =
    (env : Env) => fun(env) match {
      case Fun(f) => f(arg(env))
      case _ => sys.error("not happy")
    }
 
  def fun(x : Symbol, body : Domain) : Domain =
    (env : Env) => Fun(arg => body(env + (x -> arg)))
}

EVALUATION

Evaluation maps syntactic structure to semantic structure. All the hard work is done in the semantics above, so evaluation is almost boring.

object Evaluation {
  def eval(e : Syntax.Exp) : Semantics.Domain =
    e match {
      case Syntax.Num(n) => Semantics.num(n)
      case Syntax.Id(x) => Semantics.id(x)
      case Syntax.Add(e1, e2) => Semantics.add(eval(e1), eval(e2))
      case Syntax.Fun(x, e) => Semantics.fun(x, eval(e))
      case Syntax.App(e1, e2) => Semantics.app(eval(e1), eval(e2))
    }
}

REIFICATION

Reification is the inverse of evaluation:

       —-evaluation—>
Syntax                   Semantics
       <—reification—

Reification comes from latin “res” = thing. It means to represent something abstract as something more concrete. In our case, the abstract is a value in the domain, and the concrete is an FAE expression.

The first step is to extend the semantic structure with support for symbolic computation. All changed parts are marked with comments.

object Semantics2 {
  type Domain = Env => Value

  type Env = Map[Symbol, Value]

  abstract class Value
  case class Fun(f : Value => Value) extends Value
  case class Num(n : Int) extends Value

  // Three new case classes for symbolic values:
  case class Id(x : Symbol) extends Value
  case class Add(lhs : Value, rhs : Value) extends Value
  case class App(fun : Value, arg : Value) extends Value

  // A new helper function to decide whether a value
  // could mean a number
  def canBeNumber(v : Value) : Boolean = v match {
    case Fun(_) => false
    case Num(_) => true
    case Id(_) => true
    case Add(_, _) => true
    case App(_, _) => true
  }

  // A new helper function to decide whether a value
  // could mean a function
  def canBeFunction(v : Value) : Boolean = v match {
    case Fun(_) => true
    case Num(_) => false
    case Id(_) => true
    case Add(_, _) => false
    case App(_, _) => true
  }

  def num(n : Int) : Domain =
    (env : Env) => Num(n)

  def id(x : Symbol) : Domain =
    (env : Env) => env(x)

  def add(lhs : Domain, rhs : Domain) : Domain =
    (env : Env) => (lhs(env), rhs(env)) match {
      case (Num(n1), Num(n2)) => Num(n1 + n2)
      // new case for symbolic addition:
      case (val1, val2) if canBeNumber(val1) && canBeNumber(val2) => Add(val1, val2)
      case _ => sys.error("not happy")
    }

  def app(fun : Domain, arg : Domain) : Domain =
    (env : Env) => fun(env) match {
      case Fun(f) => f(arg(env))
      // new case for symbolic application:
      case (funVal) if canBeFunction(funVal) => App(funVal, arg(env))
      case _ => sys.error("not happy")
    }
 
  def fun(x : Symbol, body : Domain) : Domain =
    (env : Env) => Fun(arg => body(env + (x -> arg)))
}

This enhanced semantic structure is still suitable for evaluation. We don’t have to change anything at all in the evaluation function:

object Evaluation2 {
  def eval(e : Syntax.Exp) : Semantics2.Domain =
    e match {
      case Syntax.Num(n) => Semantics2.num(n)
      case Syntax.Id(x) => Semantics2.id(x)
      case Syntax.Add(e1, e2) => Semantics2.add(eval(e1), eval(e2))
      case Syntax.Fun(x, e) => Semantics2.fun(x, eval(e))
      case Syntax.App(e1, e2) => Semantics2.app(eval(e1), eval(e2))
    }
}

And the enhanced semantics structure with its support for symbolic computation is also suitable for reification.

Notes:

(1) We use a cheap trick for generating fresh identifiers. Don’t do that at home.

(2) The first four cases of reify are straight-forward.

(3) The case for functions is all-important.

object Reification {
  var i = 0

  def fresh() : Symbol = {
    i += 1
    Symbol("x" + i)
  }

  def reify(v : Semantics2.Value) : Syntax.Exp = v match {
    case Semantics2.Id(name) => Syntax.Id(name)
    case Semantics2.Num(n) => Syntax.Num(n)
    case Semantics2.Add(lhs, rhs) => Syntax.Add(reify(lhs), reify(rhs))
    case Semantics2.App(fun, arg) => Syntax.App(reify(fun), reify(arg))

    case Semantics2.Fun(f) => {
      val x = fresh()
      Syntax.Fun(x, reify(f(Semantics2.Id(x))))
    }
  }
}

Going from syntax to semantics and back is normalization.

def norm(e : Syntax.Exp) : Syntax.Exp =
  Reification.reify(Evaluation2.eval(e)(Map()))

Some tests.

val test1 = norm(Example.TEST1)
val test2 = norm(Example.TEST2)

SUMMARY

We implemented an interpreter where syntax and semantics is strictly separated. Most of the hard work is in the semantics, in functions like app and add. Evaluation is very simple: We just map the syntactic structure to the semantic structure. We also defined reification, the inverse of evaluation. This required two tricks: (1) We enhanced the semantics with support for symbolic computation, effectively adding a copy of all Exp constructors to the Value type. (2) In the case for the reifications of functions, we apply the function to a fresh identifier.