22  Lab: Small-Step Operational Semantics

Learning Goals

The primary learning goals of this assignment are to build intuition for the following:

  • the distinction between a big-step and a small-step operational semantics;
  • evaluation order; and
  • substitution and program transformation.
Functional Programming Skills
Iteration. Introduction to higher-order functions.
Programming Language Ideas
Semantics: evaluation order. Small-step operational semantics. Substitution and program transformation.

Instructions

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

You will be replacing ??? or case _ => ??? in the Lab3.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 Lab3Spec.scala file. In the Lab3Spec.scala, there is empty test class Lab3StudentSpec that you can use to separate your tests from the given tests in the Lab3Spec class. You are also likely to edit Lab3.worksheet.sc for any scratch work. You can also use Lab3.worksheet.js to write and experiment in a JavaScript file that you can then parse into a JavaScripty AST (see Lab3.worksheet.sc).

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

Note that there is a section with concept exercises (). Make sure to complete the concept exercises in that section and turn in this file as part of your submission for the concept exercises. However, all code and testing exercises from other sections are submitted in Lab3.scala or Lab3Spec.scala.

Recall that you need to switch kernels between running JavaScript and Scala cells.

22.1 Small-Step Interpreter: JavaScripty Functions

We consider the same JavaScripty variant as in the previous exercise on big-step operational semantics () where the interesting language feature are first-class functions:

trait Expr
case class Fun(xopt: Option[String], y: String, e1: Expr) extends Expr // e ::= x?(y) => e1
case class Call(e1: Expr, e2: Expr) extends Expr                       // e ::= e1(e2) 
defined trait Expr
defined class Fun
defined class Call

We consider a Fun constructor for representing JavaScripty function literals. This version of Fun allows for named functions. When a function expression x(y)=>e has a name, then it is can be recursive. As noted previously about recursive functions (), variable x is an additional formal parameter, and the function body e may have free variable uses of x. The variable x gets bound to itself (i.e., the function value for x(y)=>e) on a function call.

In the abstract syntax representation, the xopt: Option[String] parameter in our Fun constructor is None if there is no identifier present (cannot be used recursively), or Some(x: String) if there is an identifier, x, present (can be used recursively).

In this lab, we will do two things. First, we will move to implementing a small-step interpreter with a function step that takes an e: Expr and returns a one-step reduction of e. A small-step interpreter makes explicit the evaluation order of expressions. Second, we will remove environments and instead use a language semantics based on substitution. This change will result in static, lexical scoping without needing closures, thus demonstrating another way to fix dynamical scoping.

These two changes are orthogonal, that is, one could implement a big-step interpreter using substitution (as in ) or a small-step interpreter using environments. Substitution is a fairly simple way to get lexical scoping, but in practice, it is rarely used because it is not the most efficient implementation.

22.2 Static Scoping

Exercise 22.1 (Substitute) Since our implementation requires substitution, we begin by implementing substitute, which substitutes value v for all free occurrences of variable x in expression e:

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

We advise defining substitute by induction on e. The cases to be careful about are ConstDecl and Fun because these are the variable binding constructs (as discussed in the reading on substitution in ). In particular, calling substitute on expression

a; { const a = 4; a }

with value 3 for variable a should return

3; { const a = 4; a }

not

3; { const a = 4; 3 }

This function is a helper for the step function, but you might want to implement all of the cases of step that do not require substitute first.

22.3 Iteration

Our step performs a single reduction step. We may want to test it by repeatedly calling it with an expression until reducing to value. Thus, from a software engineering standpoint, you might want to evolve the iterate function described below together with your implementation of step.

This idea of repeatedly performing an action until some condition is satisfied is a loop or iteration. We have seen that we can iterate with a tail-recursive helper function. For example, consider the sumTo function that sums the integers from 0 to n:

def sumTo(n: Int): Int = {
  def loop(acc: Int, i: Int): Int = {
    require(n >= 0)
    if (i > n) acc
    else loop(acc + i, i + 1)
  }
  loop(0, 0)
}
sumTo(100)
defined function sumTo
res2_1: Int = 5050

This pattern of repeating something until a condition is satisfied is exceedingly common (e.g., computing the square root using Newton-Raphson approximation until the error is small enough from a previous assignment).

Because this pattern is so common, we want to get practice refactoring this pattern into a library function. This library function will be a higher-order function because it takes the “something” (i.e., what to do in each loop iteration) as a function parameter.

Exercise 22.2 (Iterate with Error Side-Effects) Implement the generic, higher-order library function iterateBasic. The iterateBasic function repeatedly calls (i.e., iterates) the callback stepi until it returns None starting from acc0: A. Note that iterateBasic is generic in the accumulation type A. The stepi callback takes the current accumulator of type A and the iteration number as an Int and indicates continuing by returning Some(acc) for some next accumulator value acc.

def iterateBasic[A](acc0: A)(stepi: (A, Int) => Option[A]): A = {
  def loop(acc: A, i: Int): A = ???
  loop(acc0, 0)
}
defined function iterateBasic

We can test iterateBasic by using it with a client like sumTo:

def sumTo(n: Int) = {
  iterateBasic(0) { case (acc, i) =>
    require(n >= 0)
    if (i > n) None
    else Some(acc + i)
  }
}
sumTo(100)
scala.NotImplementedError: an implementation is missing
  scala.Predef$.$qmark$qmark$qmark(Predef.scala:345)
  ammonite.$sess.cmd3$Helper.loop$1(cmd3.sc:2)
  ammonite.$sess.cmd3$Helper.iterateBasic(cmd3.sc:3)
  ammonite.$sess.cmd4$Helper.sumTo(cmd4.sc:2)
  ammonite.$sess.cmd4$Helper.<init>(cmd4.sc:8)
  ammonite.$sess.cmd4$.<clinit>(cmd4.sc:7)

We see how sumTo can use iterateBasic.

Exercise 22.3 (Iterate with Error Values) One unfortunate aspect of the above is that sumTo “exits iterateBasic with an error” by throwing an exception (i.e., with the require(n >= 0)). Let us refactor iterateBasic to allow for explicit error values using Either[Err, A]:

def iterate[Err, A](acc0: A)(stepi: (A, Int) => Option[Either[Err, A]]): Either[Err, A] = {
  def loop(acc: A, i: Int): Either[Err, A] = ???
  loop(acc0, 0)
}

def sumTo(n: Int): Either[IllegalArgumentException, Int] = {
  iterate(0) { case (acc, i) =>
    if (n < 0) Some(Left(new IllegalArgumentException("requirement failed")))
    else if (i > n) None
    else Some(Right(acc + i))
  }
}
sumTo(100)
sumTo(-1)
scala.NotImplementedError: an implementation is missing
  scala.Predef$.$qmark$qmark$qmark(Predef.scala:345)
  ammonite.$sess.cmd5$Helper.loop$1(cmd5.sc:2)
  ammonite.$sess.cmd5$Helper.iterate(cmd5.sc:3)
  ammonite.$sess.cmd5$Helper.sumTo(cmd5.sc:7)
  ammonite.$sess.cmd5$Helper.<init>(cmd5.sc:13)
  ammonite.$sess.cmd5$.<clinit>(cmd5.sc:7)

The iterate is now parametrized by an error type Err and returns an Either[Err, A]. The stepi callback should return None if it wants to stop normally, Some(Left(err)) if it wants to stop with an error, and Some(Right(acc) if it wants to continue with an accumulator value acc.

We can now see how we can use iterate as a library function to iterate your step implementation. In particular, this is how iterate will be used to iterate step while adding some debugging output:

def iterateStep(e: Expr) = {
  require(closed(e), s"iterateStep: ${e} not closed")
  if (debug) {
    println("------------------------------------------------------------")
    println("Evaluating with step ...")
  }
  val v = iterate(e) { (e: Expr, n: Int) =>
    if (debug) { println(s"Step $n: $e") }
    if (isValue(e)) None else Some(step(e))
  }
  if (debug) { println("Value: " + v) }
  v
}

Of particular interest is the anonymous function passed to iterate that calls your implementation of step.

22.4 Small-Step Interpreter

In this section, we implement the one-step evaluation judgment form er that says, “Expression e can take one step of evaluation to a step-result r.”

step-resultsr::=typeerroree

A step-result r is either a typeerrore indicating a dynamic type error in attempting to reduce e or a successful one-step reduction to an expression e.

We represent a step-result r in Scala using a type Either[DynamicTypeError, Expr]:

case class DynamicTypeError(e: Expr) {
  override def toString = s"TypeError: in expression $e"
}
type Result = Either[DynamicTypeError, Expr] // r ::= typeerror e | e
defined class DynamicTypeError
defined type Result

Note that unlike before, DynamicTypeError is not an Exception, so it cannot be thrown.

The small-step semantics that we should implement are given in the section below (). The language we implement is JavaScripty with numbers, booleans, strings, undefined, printing, and first-class functions. It is a simpler language than the previous lab because we remove type coercions (except to booleans) and replace most coercion cases with dynamic type error typeerrore.

Exercise 22.4 (Step without Dynamic Type Checking) We advise first implementing the cases restricted to judgments of the form ee, that is, implement the Do and Search rules while ignoring the TypeError and Propagate rules. Start with implementing a stepBasic function with type:

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

That is, just crash with a MatchError exception if your step encounters any ill-typed expression e.

The suggested practice here is to read some rules, write a few tests for those rules, and implement the cases for those tests.

Exercise 22.5 (Step with Dynamic Type Checking) Then, copy your code from stepBasic to stepCheck:

def stepCheck(e: Expr): Either[DynamicTypeError, Expr] = ???
defined function stepCheck

to then add dynamic type checking. You will likely need to refactor your code to satisfy the new types before implementing the TypeError and Propagate rules.

Exercise 22.6 (To Boolean) You will need to implement a toBoolean function to convert JavaScripty values to booleans, following the ToBoolean rules in .

def toBoolean(e: Expr): Boolean = ???
defined function toBoolean

However, you will not need any other type coercion functions here.

Notes

  • Note that the tests call the step function that is originally defined as:

    //def step(e: Expr): Either[DynamicTypeError, Expr] = Right(stepBasic(e))
    def step(e: Expr): Either[DynamicTypeError, Expr] = stepCheck(e)

    You can first test stepBasic by uncommenting the first line and commenting out the second line.

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

22.5 Small-Step Operational Semantics

In this section, we give the small-step operational semantics for JavaScripty with numbers, booleans, strings, undefined, printing, and first-class functions. We have type coercions to booleans but otherwise use dynamic type error for other cases.

We write [v/x]e for substituting value v for all free occurrences of the variable x in expression e (i.e., a call to substitute).

It is informative to compare the small-step semantics used in this lab and the big-step semantics from last homework.

22.5.1 Do Rules

ee

DoNegn=n1-n1n

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

DoPlusStringstr=str1str2str1+str2str

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

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

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

DoNotv1b1!v1¬b1

DoAndTruev1truev1&&e2e2

DoAndFalsev1falsev1&&e2v1

DoOrTruev1truev1||e2v1

DoOrFalsev1falsev1||e2e2

DoIfTruev1truev1?e2:e3e2

DoIfFalsev1falsev1?e2:e3e3

DoSeqv1,e2e2

DoPrintv1 printedconsole.log(v1)undefined

DoConstconstx=v1;e2[v1/x]e2

DoCall((x)=>e1)(v2)[v2/x]e1

DoCallRecv1=(x1(x2)=>e1)v1(v2)[v1/x1][v2/x2]e1

Figure 22.1: The Do rules for JavaScripty with numbers, booleans, strings, undefined, printing, and first-class functions.

22.5.2 Search Rules

ee

SearchUnarye1e1uope1uope1

SearchBinary1e1e1e1bope2e1bope2

SearchBinary2e2e2v1bope2v1bope2

SearchIfe1e1e1?e2:e3e1?e2:e3

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

SearchConste1e1constx=e1;e2constx=e1;e2

SearchCall1e1e1e1(e2)e1(e2)

SearchCall2e2e2v1(e2)v1(e2)

Figure 22.2: The Search rules for JavaScripty with numbers, booleans, strings, undefined, printing and first-class functions.

22.5.3 Coercing to Boolean

vb

ToBooleanNumFalsen{0.0,0.0,NaN}nfalse

ToBooleanNumTruen{0.0,0.0,NaN}ntrue

ToBooleanBooleanbb

ToBooleanStrFalse""false

ToBooleanStrTruestr""strtrue

ToBooleanUndefinedundefinedfalse

ToBooleanFunx?(y)=>etrue

Figure 22.3: The ToBoolean rules for JavaScripty with numbers, booleans, strings, undefined, and first-class functions.

22.5.4 Dynamic Typing Rules

er

TypeErrorNegv1n1-v1typeerror(-v1)

TypeErrorPlus1v1n1v1str1v1+v2typeerror(v1+v2)

TypeErrorPlusString2v2str2str1+v2typeerror(str1+v2)

TypeErrorArith1v1n1bop{-,*,/}v1bopv2typeerror(v1bopv2)

TypeErrorArith2v2n2bop{+,-,*,/}n1bopv2typeerror(n1bopv2)

TypeErrorInequality1v1n1v1str1bop{<,<=,>,>=}v1bopv2typeerror(v1bopv2)

TypeErrorInequalityNumber2v2n2bop{<,<=,>,>=}n1bopv2typeerror(n1bopv2)

TypeErrorInequalityString2v2str2bop{<,<=,>,>=}str1bopv2typeerror(str1bopv2)

TypeErrorCallv1x?(y)=>e1v1(v2)typeerror(v1(v2))

PropagateUnarye1typeerroreuope1typeerrore

PropagateBinary1e1typeerroree1bope2typeerrore

PropagateBinary2e2typeerrorev1bope2typeerrore

PropagateIfe1typeerroree1?e2:e3typeerrore

PropagatePrinte1typeerroreconsole.log(e1)typeerrore

PropagateConste1typeerroreconstx=e1;e2typeerrore

PropagateCall1e1typeerroree1(e2)typeerrore

PropagateCall2e2typeerrorev1(e2)typeerrore

Figure 22.4: The TypeError and Propagate rules for JavaScripty with numbers, booleans, strings, undefined, printing, and first-class functions.

22.6 Concept Exercises

Make sure to complete the concept exercises in this section and turn in this file as part of your submission. However, all code and testing exercises from other sections are submitted in Lab3.scala or Lab3Spec.scala.

Exercise 22.7 (Evaluation Order) Consider the small-step operational semantics shown in . What is the evaluation order for e1+e2? Explain.

Edit this cell:

???

Exercise 22.8 (Changing Evaluation Order) How do we change the rules to obtain the opposite evaluation order?

Edit this cell:

???

Exercise 22.9 (Using Short-Circuit Evaluation) Give an example that illustrates the usefulness of short-circuit evaluation. Explain your example.

Edit this cell:

???

Exercise 22.10 (Removing Short-Circuit Evaluation) Consider the small-step operational semantics shown in . Does e1&&e2 short circuit? Explain. If e1&&e2 short circuits, give rules that eliminates short circuiting. If it does not short circuit, give the short-circuiting rules.

Edit this cell:

???

22.7 Testing

This section has some space to write some tests in our subset of JavaScript. You might want to work on these tests while you are implementing step. As before, you will add your tests to Lab3StudentSpec. Your interpreter will run the tests against the expected result you provide. We will write three tests, all of these tests must properly parse.

Exercise 22.11 (Test 1: Higher-Order Function) Write a test case that has a function that takes a function value as an argument (i.e., is a higher order function):

???

Exercise 22.12 (Test 2: Recursion) Write a test case that uses recursion

???

Exercise 22.13 (Test 3: Any Test in this variant of JavaScripty) Write another test

???

Notes

  • Remember to add these to Lab3StudentSpec in Lab3Spec.scala.
  • Add the JavaScripty code as a string in jsyStr and the expected result in answer.

22.8 Accelerated Component

For the accelerated component of this lab, we will give rules and implement the behavior that enables us to match JavaScript semantics. In particular, we will give rules and implement type coercions for numbers and strings, and we will update our small-step operational semantics to use them.

22.8.1 Additional Type Coercions

Exercise 22.14 Give the inference rules defining the judgment form for coercing a value to a number vn

Edit this cell:

???

Exercise 22.15 Give the inference rules defining the judgment form for coercing a value to a string vstr

Edit this cell:

???

Exercise 22.16 Implement the toNumber and toStrcoercions.

def toNumber(v: Expr): Double = ???
def toStr(v: Expr): String = ???
defined function toNumber
defined function toStr

Notes

  • If your recall Lab 2, we implemented these functions. They will be the same here, except we must add the rules for functions.

22.8.2 Updating the Small-Step Operational Semantics

Now that we are allowing type coersions, our operational semantics will change. For example, consider the following Do rule:

DoNegvn-vn

This is a new rule for DoNeg, which is read as if v coerces to n, then -v steps to n. Now that we are allowing non-numbers to be coerced and then negated, we no longer have our TypeErrorNeg rule.

Note that we only need to update Do rules with coercions and remove TypeError rules. We do not need to update the Search or Propagage rules.

Exercise 22.17 Explain why we only need to update the Do rules with coercions and remove TypeError rules, and we do not need to update the Search or Propagage rules.

Edit this cell:

???

One rule of particular interest is DoArith, which we need to split to account for e1+e2 being overloaded for numbers and strings. Given this, we need to rewrite DoArith so it does not include + (and adds coersions), add the rule DoPlusNumber, and alter DoPlusString to become two rules.

Exercise 22.18 Give these new Do rules

Edit this cell:

???

Similar to this, our DoInequalityNumber rule must be split into two and altered:

Exercise 22.19 Give the two new DoInequalityNumber1 and DoInequalityNumber2 rules:

Edit this cell:

???

Once we have all of these rules defined, we notice that most of our typeerror rules no longer result in type errors. Therefore, most of them should be deleted. In fact, the only non-propagate rule left for type errors is TypeErrorCall, since we are still not allowed to call something that is not a function (in JavaScript).

22.8.3 Update Step

Exercise 22.20 Now that we have our type conversion functions implemented and our new rules defined, we are ready to update step. Implement stepCoerce by first copying your code from stepCheck and then update based on your new rules.

def stepCoerce(e: Expr): Either[DynamicTypeError,Expr] = ???
defined function stepCoerce

As before, let the rules guide your implementation.

Notes

  • None of your other functions should need to be altered.

Submission

If you are a University of Colorado Boulder student, we use Gradescope for assignment submission. In summary,

You need to have a GitHub identity and must have your full name in your GitHub profile in case we need to associate you with your submissions.