Week 6 Code Snippets

Algebraic Data Types

We implement an algebraic data type to model abstract syntax trees formed by the grammar below.

Grammar of a Simple Programming Language

Grammar (EBNF)

Expr ::=   Number
         | Expr '+' Expr 
         | Expr '-' Expr 
         | Expr '*' Expr
         | '(' Expr ')'
         | Ident
         | Expr '>=' Expr
         | Ident ':=' Expr
         | Expr ';' Expr
         | 'if' Expr 
              'then' Expr 
              'else' Expr 'fi'
         | 'while' Expr 'do' 
              Expr 
           'od'

Algebraic Data Type in Scala

enum Expr:
  // Arithmetic expressions
  case N(n:Int)
  case Plus(l:Expr, r:Expr)
  case Minus(l:Expr, r:Expr)
  case Times(l:Expr, r:Expr)

  // Parenthesized expressions (e)
  case Par(e:Expr)

  // Identifiers
  case I(n:String)

  // Comparisons l>=r
  case GEq(l:Expr, r:Expr)
  
  // Programs
  
  // Assignment x := v
  case Assign(x:I, v:Expr)

  // Sequential composition p ; r
  case Seq(p:Expr, r:Expr)  
  
  // Conditional if p then t else e
  case If(p:Expr, t:Expr, e:Expr)
  
  // Loop while p do b
  case While(p:Expr, b:Expr)

Formal Semantics of a Simple Programming Language

The formal semantics, a structural operational big-step semantics, is a detailed specification for an interpreter that almost literally translates to Scala.

Store and Judgments

We use the judgments as guidance to define the signature of the interpreter.

type Store = Map[I, Int]
type FunctionStore = Map[I, Expr]

// Evaluate expression
def eval(e: Expr, s: Store, f: FunctionStore): (Int, Store, FunctionStore) = e match

Integer Arithmetic

  case N(n)           => (n, s, f)
  
  case Plus(e1, e2)   => chain2(e1, e2, s, f, _ + _)    
  case Minus(e1, e2)  => chain2(e1, e2, s, f, _ - _)
  case Times(e1, e2)  => chain2(e1, e2, s, f, _ * _)
  
  case Par(e)         => eval(e, s, f)

Note that the common functionality of chaining the operator evaluation in addition, subtraction, and multiplication, is refactored into the higher-order function chain2 that is parameterized with the concrete operation (+,-,*).

def chain2(e1: Expr, e2: Expr, s: Store, f: FunctionStore, op: (Int,Int)=>Int) = 
  val (v1, s1, f1) = eval(e1, s, f)
  val (v2, s2, f2) = eval(e2, s1, f1)
  (op(v1,v2), s2, f2)
end chain2

Variables, Assignment, and Sequential Composition

  case x: I           => (s(x), s, f)
  
  case Assign(x, e)   =>
    val (v,s1,f1) = eval(e, s, f)
    (v, s1.updated(x,v), f1)

  case Seq(e1, e2)    => chain2(e1, e2, s, f, (_,v2)=>v2)

Comparison, Conditional, and Loop

  case GEq(e1, e2)    => chain2(e1, e2, s, f, (v1,v2)=>math.max(0, v1-v2+1))    

  case If(e1, e2, e3) =>
    val (v1, s1, f1) = eval(e1, s, f)
    if v1!=0 
      then eval(e2, s1, f1)
      else eval(e3, s1, f1)

  case w@While(e1, e2)  =>
    val (v1, s1, f1) = eval(e1, s, f)
    if v1==0 
      then (0, s1, f1)
      else eval(Seq(e2, w), s1, f1)

Full Algebraic Data Type and Interpreter

/** Expressions */
enum Expr:
  // Arithmetic expressions
  case N(n:Int)
  case Plus(l:Expr, r:Expr)
  case Minus(l:Expr, r:Expr)
  case Times(l:Expr, r:Expr)

  // Parenthesized expressions (e)
  case Par(e:Expr)

  // Identifiers
  case I(n:String)

  // Comparisons l>=r
  case GEq(l:Expr, r:Expr)
  
  // Programs
  
  // Assignment x := v
  case Assign(x:I, v:Expr)

  // Sequential composition p ; r
  case Seq(p:Expr, r:Expr)  
  
  // Conditional if p then t else e
  case If(p:Expr, t:Expr, e:Expr)
  
  // Loop while p do b
  case While(p:Expr, b:Expr)

  // Function definition and application
  case FunctionDef(name: I, body: Expr)
  case FunctionApp(name: I)
end Expr

import Expr.*

type Store = Map[I, Int]
type FunctionStore = Map[I, Expr]

// Evaluate expression
def eval(e: Expr, s: Store, f: FunctionStore): (Int, Store, FunctionStore) = e match
  case N(n)           => (n, s, f)
  
  case Plus(e1, e2)   => chain2(e1, e2, s, f, _ + _)    
  case Minus(e1, e2)  => chain2(e1, e2, s, f, _ - _)
  case Times(e1, e2)  => chain2(e1, e2, s, f, _ * _)
  
  case Par(e)         => eval(e, s, f)
  case x: I           => (s(x), s, f)
  
  case Assign(x, e)   =>
    val (v,s1,f1) = eval(e, s, f)
    (v, s1.updated(x,v), f1)

  case Seq(e1, e2)    => chain2(e1, e2, s, f, (_,v2)=>v2)

  case GEq(e1, e2)    => chain2(e1, e2, s, f, (v1,v2)=>math.max(0, v1-v2+1))    

  case If(e1, e2, e3) =>
    val (v1, s1, f1) = eval(e1, s, f)
    if v1!=0 
      then eval(e2, s1, f1)
      else eval(e3, s1, f1)

  case w@While(e1, e2)  =>
    val (v1, s1, f1) = eval(e1, s, f)
    if v1==0 
      then (0, s1, f1)
      else eval(Seq(e2, w), s1, f1)

  case FunctionDef(n, e) => 
    (0, s, f.updated(n, e))
  case FunctionApp(n) => 
    // val e = f(n)
    // val (v, s1, f1) = eval(e, s, f)
    // (v, s1, f1)
    eval(f(n), s, f)
end eval

Examples of Using the Algebraic Data Type and Interpreter

Absolute value of a number:

// Absolute value of n: if n>=0 then n else 0-n
def abs(n: Expr) =
  If(
    GEq(n, N(0)),
    n,
    Minus(N(0), n)
  )
eval(abs(N(-2)), Map.empty, Map.empty)

Imperative factorial of a number:

// // Factorial n: i:=abs(n); f:=1; while(i>=1) do f:=f*i; i:=i-1 od; f
def fact(n: Int) =
  Seq(
    Assign(I("i"), abs(N(n))),
    Seq(
      Assign(I("f"), N(1)),
      Seq(
        While(
          GEq(I("i"), N(1)),
          Seq(
            Assign(I("f"), Times(I("f"), I("i"))),
            Assign(I("i"), Minus(I("i"), N(1)))
          )
        ),
        I("f")
      )

Recursive factorial of a number:

// Recursive factorial with global store
// n:=<n>; 
// def fact = if n>=1 then n*(n:=n-1;fact) else 1 fi; 
// fact()
def factRec(n: Int) =
  Seq(
    Assign(I("n"), N(n)),
    Seq(
      FunctionDef(
        I("fact"),
        If(
          GEq(I("n"), N(1)),
          Times(I("n"), Seq(
            Assign(I("n"), Minus(I("n"), N(1))), 
            FunctionApp(I("fact")))),
          N(1)
        )
      ),
      FunctionApp(I("fact"))      
    )
  )
eval(factRec(5), Map.empty, Map.empty)