CS 312 Problem Set 4
Interpreter for a Simple Language

Issued: February 26, 2003
Due: March 14, 11:59pm


Updates


Instructions

You will do this problem set by modifying the source files in ps4.zip and submitting the program that results. The program that you submit must compile without any warnings. Programs that do not compile or compile with warnings will receive an automatic zero.

Several parts of this assignment require that you implement a specification (and in some cases finish designing the specification).  For each part, we expect you to provide the following documentation in an attached file for each part.

We will be evaluating your problem set on several different criteria: the specifications you write (where appropriate), the correctness of your implementation, code style, efficiency, and validation strategy. Correctness is worth about half the total score and the importance of the other criteria varies from part to part.


Overview

As the Internet has grown in popularity, so has online gaming.  Most online games require some element of chance, which they often obtain by using dice roll generators.  They have code to simulate the rolling of a single die, and then build some kind of dice rolling scheme upon which the game's result depends.  The problem of course is where to run this code.  If an individual gamer is allowed to run the code on his home machine, this allows the possibility of cheating by fabricating dice rolls.  One solution to this problem is Internet-based dice-rolling servers.  These servers accept an input expression in a predefined dice-rolling language and return the result to one or more computers.

There are many different games that require rolling dice, some requiring more complex actions than simply rolling a couple of six-sided dice. For example, Yahtzee, Risk, Dungeons & Dragons, and Warhammer also have more complicated dice-rolling protocols. To support these games, dice-rolling languages need a number of additional features, such as summing and counting dice, repeated rolling, and discarding or selecting rolled dice based on a condition.

In this problem set you will implement an interpreter for a subset of a simple dice-rolling language called Roll.


The Roll Language

The Roll language is a functional language.  When the interpreter (in evaluate mode) reads an expression in Roll, it computes a possible value and prints the output.  Possible expressions exp in the subset of Roll we are implementing include the following:

a positive integer constant Examples: 3, 5, 2.
a bag of values Examples: {3,4,2}, {1,2,3,4,5,6}, {}, {4}
d exp d is the fundamental expression in Roll.  dn means roll an n-sided die and return the result.  For example, d6 evaluates to any of the integers 1–6.  The expression after d need not be an integer, but it should evaluate to an integer; for example, d(d6) is a valid expression.  To compute this expression, the interpreter will roll a six-sided die to get a value n, then roll an n-sided die and return the result.
exp # exp # is the "n times" operator.  The interpreter evaluates the first expression to get an integer n.  Then it evaluates the second expression n times and unions the result into a single bag of integers.  For example, 3#d6 rolls 3 six-sided dice and returns a bag containing the results.

Evaluation of this expression will always result in bags of integers; it will never result in bags of bags. For example, 3#(2#d6) is equivalent to 6#d6.
exp @ exp The result of this is the union of the two subexpressions.  For example, (2#d4)@(3#d6) returns the value from rolling 2 four-sided dice and 3 six-sided dice. Both expressions should evalutate to bags; 2 @ (2#d12) should cause a Runtime error.
exp + exp This expression acts differently depending on the results of the subexpressions.  If both of the operands are integers, it computes the sum of the two integers.  If one operand is an integer and the other is a bag, it adds the integer value to each element of the bag and returns the result. It is a run-time type error for both operands to be bags.
sum exp If the subexpression is a bag, the result is to sum the elements of the bag.  If it is an integer, this expression returns that integer.  So, sum(3#d6) would return the sum of 3 six-sided dice.
count exp Similar to sum, but it returns the number of elements in the subexpression.  If the subexpression is an integer, it returns 1.
least exp exp Given operands n and b respectively, returns the least n elements of bb must be a bag.  An example is least 2 (3#d6), which rolls 3 six-sided dice and returns the two least values rolled.
let id=exp in exp end Binds the result of evaluating the first subexpression to the identifier id and uses the binding to evaluate the second subexpression.  Identifiers start with a letter and consist of letters, underscores, and primes.  For example, let x = d6 in x+x end will roll a six-sided die and double the result.  Notice that the d6 in this expression is evaluated only once.
if exp relop exp
then exp
else exp
Very similar to the ML if/then/else expression.  If the conditional returns true, returns the result of evaluating the first expression, otherwise returns the result of evaluating the second expression. For example, if d6 < 4 then d4 else d5: Roll a 6-sided dice. If the result is less than 4, then roll a 4-sided dice else roll a 5-sided dice. The list of relational operators is mentioned below. Note: since each element of the test is parsed separately, it is NOT possible to put parentheses around the comparison as in if (exp op exp) then .... Relational operators relop may be any of <, <=, =, !=, >=, >  
filter relop exp exp   filter op e1 e2 will perform the following:  evaluate e1 and e2 to get result v1 and v2v1 must be an integer; v2 must be a bag.  Then compute (v1 op v) for each element v of bag v2 and return the bag with all elements for which (v1 op v) evaluated to true.

This is easiest to see with an example.  filter < 4 3#d6 will roll 3 six-sided dice and return any values greater than or equal to 4.  If the 3#d6 evaluates to {1,2,6}, the result will be {6}.

You should experiment with the interpreter to get a feel for the language syntax.  At the command prompt, enter parse to enter the parser mode where the expressions are parsed and displayed but not evaluated. Use this mode to familiarize yourself with the abstract syntax tree that will be passed in as input to the eval functions.

Some sample dice expressions:

Errors in dice expressions:

Abstract syntax

You will also want to study the abstract syntax tree for expressions as defined in AbSyn.sml.  Here you will find the datatype exp, which represents an expression in Roll, and the datatype value, which represents the result of evaluating a Roll expression.  Notice that values in Roll can be either integers or collections of integers.  To represent collections of integers, we will use multisets. You should reuse your implementation from Problem Set 3, fixing it if necessary to address correctness or performance problems.  The datatype for value is:

type 'a bag = 'a Multiset.multiset
datatype value = Int_v of int | Bag_v of int bag

You are allowed to add to the multiset signature as you see fit for this problem set. You are also allowed to change your multiset implementation for this problem set as you see fit. We will not be testing you on your multiset implementation directly; we will be testing it indirectly through its use by the rest of your implementation. Note: the usual specification and documentation is still required for the multiset as for all of your code!


Notes on Probability

Central to dice rolling is the concept of probability.  Probability is the branch of mathematics that is concerned with analyzing experiments, that is, processes that return outcomes.  Generally, an experiment has multiple possible outcomes, and which outcome will occur is not known beforehand.  Rolling a 6 sided die is an experiment whose outcome is a number between 1 and 6. However, one does not know beforehand what the outcome is going to be . The set of all possible outcomes is called the sample space. For example, the sample space of rolling a 6 sided dice is {1, 2, 3, 4, 5, 6}.

Some outcomes may correspond to an event; for example, winning a game. Each event is defined by the favorable outcomes in which the event is said to occur. For example, you might win the game by rolling any number less than or equal to 2. In this case, the set of favorable outcomes for the event would be {1, 2}. The likelihood of any of the favorable outcomes occurring is the probability of the event.  If all outcomes are equally likely, we can define the probability of an event A, denoted by Pr(A), as

Pr(A) = (number of favorable outcomes for A) / (number of all possible outcomes)

Hence, the probability of winning in the above example would be Pr(Win) = 2/6.

There are more complicated examples. Suppose the game requires you to toss a coin 3 times. You win if there are exactly two heads. Here, flipping a coin three times is the experiment. Each toss can turn up either a head (H) or a tail (T), so one possible outcome is THT. That is, the first toss is a tail, the second a head, and the third another tail. The sample space is {TTT, TTH, THT, THH, HTT, HTH, HHT, HHH}, and among these, the favorable outcomes are { THH, HTH, HHT }. The probability of winning therefore is 3/8. Notice HTH, THH and HHT are counted as 3 distinct outcomes even though both have 2 heads and 1 tail.


Using the Interpreter

After compiling the problem set, to start the interpreter simply run the function Roll.interpreter(). You should see the following:

CS312 Roll Interpreter
(c) 2003 Cornell University
>

From here, you can type in Roll expressions, and they will be evaluated in the current mode. There are three different modes, eval, dist, and sample.

To view the current mode, type mode at the interpreter prompt. To switch modes, type eval, dist, or sample, depending on which mode you wish to switch to. The interpreter starts in eval mode. Typing help gives you help on how to use the interpreter, and q quits the interpreter.


Part 1: Evaluator (30 pts)

Implement the function Evaluator.eval of type AbSyn.exp -> AbSyn.value. This function will take in a Roll expression, and output a Roll value based on the expression. For example, eval(d6) will return an Int_v that has a value between 1 and 6. And eval(3#d6) will return a Bag_v that contains 3 numbers, each between 1 and 6 inclusive.

The evaluator must be able to handle all of the Roll expressions described above, and be able to return all possible valid values for any given expression. In order to return bag values, you must add your multiset implementation from Problem Set 3.  The code we have given you in multiset/ms.sml is only for compilation and does not implement any of the multiset functions.  You should replace this file with your multiset implementation.

You may also notice that efficiency in your program depends largely on the efficiency of your multiset implementation. Your program should be able to handle (2000 # (2000 # d1)) in less than 2 minutes without running out of memory on a Pentium 4, 1.7 GHz machine with 256 MB RAM (where the reference solution runs in under 23 seconds). If you find your code running slowly, which may be the case with the previous expression, you will have to improve your multiset implementation to achieve better performance. To accurately check your running time, you may want to disable the pretty-printing since printing the result of the previous expression could take a while.

To generate random die rolls, you will need to implement a random number generator as described in Part 5.

You will also need to figure out a way to handle variables and declarations in let expressions. For example, let x = d3 in x#x end should return either {1}, or {2,2} or {3,3,3} depending on the random number. This is because d3 returns one of x = 1, 2 or 3. Hence x#x is interpreted respectively as 1#1, 2#2, or 3#3. One way to accomplish this would be to keep an environment (like the ML scratch paper) to store the latest bound values for a variable. A different way to do it would be to use the substitution model learned in class where unbound occurrences of a variable are replaced with its value after evaluation. Note that because the same expression can evaluate to more than one value (for example, d6), we must evaluate before substituting.

Hint: If your code starts to crawl for (2000 # (2000 # d1)), consider optimizing the multiset functions that are involved. If optimizing doesn't help, you may need to change your multiset representation to a more compact form.

Your Task: Complete eval/Evaluator.sml and multiset/ms.sml. Attach additional documentation in an ASCII file doc1.txt.

Part 2: Distributions (26 pts)

Implement the function Distribution.eval : AbSyn.exp -> AbSyn.value AbSyn.Bag

The returned bag should represent the distribution for the roll expression. We will define the distribution as a multiset with the following property.

This is best understood with a simple example. Consider the roll sum(2#d6). The sample space is {2, 3, ..., 12}. Consider I = 4. Then Ai is the event that sum(2#d6) = 4. Pr(Ai) = 3/36 since the only favorable cases among the 36 possible combinations are sum({1,3}), sum({3,1}) and sum({2,2}). Hence the frequency of 4 in the distribution should be 3.

Expression

Sample Space

d6 {1, 2, 3, 4, 5, 6}
2#d3 {{1, 1}, {1, 2}, {1, 2}, {1, 3}, {1, 3}, {2, 2}, {2, 3}, {2, 3}, {3, 3}}
sum(2#d6) {2, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 11, 11, 12}
d3#d3 {{1}, {2}, {3}, {1}, {2}, {3}, {1}, {2}, {3}, {1}, {2}, {3}, {1}, {2}, {3}, {1}, {2}, {3}, {1}, {2}, {3}, {1}, {2}, {3}, {1}, {2}, {3}, {1, 1}, {1, 2}, {1, 2}, {1, 3}, {1, 3}, {2, 2}, {2, 3}, {2, 3}, {3, 3}, {1, 1}, {1, 2}, {1, 2}, {1, 3}, {1, 3}, {2, 2}, {2, 3}, {2, 3}, {3, 3}, {1, 1}, {1, 2}, {1, 2}, {1, 3}, {1, 3}, {2, 2}, {2, 3}, {2, 3}, {3, 3}, {1, 1, 1}, {1, 1, 2}, {1, 1, 2}, {1, 1, 2}, {1, 1, 3}, {1, 1, 3}, {1, 1, 3}, {1, 2, 2}, {1, 2, 2 }, {1, 2, 2}, {1, 2, 3}, {1, 2, 3}, {1, 2, 3}, {1, 2, 3}, {1, 2, 3}, {1, 2, 3}, {1, 3, 3}, {1, 3, 3}, {1, 3, 3}, {2, 2, 2}, {2, 2, 3}, {2, 2, 3}, {2, 2, 3}, {2, 3, 3}, {2, 3, 3}, {2, 3, 3}, {3, 3, 3}}
sum(d3#d3) {1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 8, 8, 8, 9}
if d3=3 then d2 else d1 {1, 1, 1, 1, 1, 2}
You may want to work out d3#d3 by hand and figure out why it is what it is.  Also note that the sets given in these examples are the the smallest sets that yield the correct probabilities.  Your distribution may return a larger set with extra copies of the elements, as long as it represents the correct probabilities.

Hint: If you seem to be rewriting large chunks of code that you already wrote in Part 1, there is likely a better and shorter way to do this; look for ways to share code between the two parts. You are encouraged to make heavy use of the basis library.

Your Task: Complete eval/Distribution.sml. Attach additional documentation in an ASCII file doc2.txt.

Part 3: Distribution printer (10 pts)

Implement DistributionPrinter, which prints the distribution of a Roll expression in the following format. For every unique item i with the frequency n in the distribution, it prints i : ######## where the number of # marks is n. When we are using bags for representing outcomes, remember that ordering does not matter; for example, {1,2} is the same as {2,1}. Hence it should only be printed once with the appropriate number of # marks.

Contents of the bags should be sorted, hence {2,1} should be printed as {1,2}. Also, integers should be printed before bags, and smaller bags should be printed before larger bags. In case of equal sized bags, they should be printed in the order of the smallest element; e.g., {1,2} comes before {1,3} and {1,1,4} comes before {1,2,3}.

Example: sum(d3#d3)

     1 : #########
     2 : ############
     3 : ################
     4 : ############
     5 : ############
     6 : ##########
     7 : ######
     8 : ###
     9 : #
 

Example: let x = d6 in if x < 3 then least x {1,2,3} else d6 end (thanks Joel)

    1 : ####
    2 : ####
    3 : ####
    4 : ####
    5 : ####
    6 : ####
    {1} : ######
    {1, 2} : ######

Example: d(d2)

    1 : ###
    2 : #

Example: d(d6)

    1 : ###################################################################################################################################################
    2 : #######################################################################################
    3 : #########################################################
    4 : #####################################
    5 : ######################
    6 : ##########

Example: 2#d2

     {1, 1} : #
     {1, 2} : ##
     {2, 2} : #
 
Example: d3#d2
     {1} : ####
     {2} : ####
     {1, 1} : ##
     {1, 2} : ####
     {2, 2} : ##
     {1, 1, 1} : #
     {1, 1, 2} : ###
     {1, 2, 2} : ###
     {2, 2, 2} : #
 
Your Task: Modify toplevel/Util.sml to achieve the desired behavior. Attach additional documentation in an ASCII file doc3.txt.

Part 4: Sampling (5 pts)

Implement the function Sample.eval : AbSyn.exp -> AbSyn.value AbSyn.Bag

In Part 2, you implemented a brute force method for finding the distribution. Even though the brute force method is accurate, it can be slow. Sampling can be a more efficient way to approximate the distribution. The idea is to generate many independent samples from the distribution and use the number of times each outcome was generated as an estimate of the actual distribution. For example, to find the distribution of d6, imagine evaluating d6 ten times and storing the results in the multiset. The result might be {1, 6, 4, 2, 5, 1, 3, 6, 3, 1}. You can see that this already approximates the true distribution. If instead of ten times, one sampled it 1000 times, then you would get each number from 1 to 6 about 167 times.

Sampling is an approximate method. If a particular outcome is expected to occur n times on average after acquiring a given series of samples, the actual number of occurrences will usually be within about √n. For example, if you flip a coin 20000 times, you will rarely get exactly 10000 heads, but you will usually get between 9900 and 10100 heads. Therefore, your estimate of the probability of getting a head usually will be accurate to within 1% (in general, accuracy of roughly 1/n). Polling is an example of using sampling to estimate a distribution. To get the usual desired accuracy of ±5%, pollsters call about 400 randomly chosen people because 1/√400 = 0.05. Thus, getting highly accurate information through sampling requires a lot of samples!

Hint: If you seem to be rewriting large chunks of code that you already wrote in Part 1 or Part 2, there may be a better and shorter way to do this. You are encouraged to make heavy use of basis library functions and the functions from the previous parts.

Your Task: Complete eval/Sample.sml. Attach additional documentation in an ASCII file doc4.txt.

Part 5: Random Number Generation (15 pts)

Specify and implement a pseudorandom number generator using the linear congruential multiplier technique described in this section.

Dice rolling requires randomness. If each possible die roll does not occur with equal probability, one player may gain an advantage in winning a game.  To level the playing field, we need to simulate a die roll by generating a number with a uniform distribution, meaning each possible value occurs with equal probability.  We will call the method used to generate such a number a random number generator.

One problem in using computers for dice rolling is that computers do not have randomness built into them.  We can try to approximate such a number with some kind of algorithm, but generating truly random numbers with a deterministic algorithm is impossible. Sometimes programmers try to solve this by generating "random" numbers from the value of the system clock.  This method breaks down when a program asks for random numbers rapidly in succession, or requests a random number periodically at regular intervals.

Although a deterministic computer can never truly generate such a random number, it is possible to generate numbers that seem to be random to an observer who does not know how they are generated.  Several algorithms are known to generate so-called pseudo-random numbers, numbers that approximate a uniform distribution.  One simple implementation is based on linear congruential multipliers.  Without getting into the mathematics, the algorithm functions by starting with some seed value.  The algorithm then computes a sequence of  random numbers x1, x2, x3, ... deterministically based on that seed.  Given a good seed, values tend to occur with approximately equal probability. The algorithm computes the sequence as follows:

x0 = seed
xi+1 = (a*xi + c) mod m

Here a, c, and m are fixed parameters for the multiplier. Even though the new number xi+1 depends on the old number xi, the other operations reduce the dependence significantly.

It is clear that not all values of a, c and m generate good pseudo-random numbers. For example a=0, c=2 will make the generator always return 2. If a=1, c=1 then the generator is a counter that increments by 1 every time. Some choices of a and c will cause the sequence of xi to wrap back to the original seed quickly, which would be bad because the random numbers will also repeat. Ideally all m possible values of xi will be visited before the sequence repeats. The choice of a=31421, c=314159262 is supposed to give a random distribution according to Knuth. You must specify the signature for and implement the structure Random that uses the values of a and c above along with seed = 312 and m = 231-1. You are not allowed to use the Random or Rand structures provided by the Basis Library.

Given a pseudo-random number generator, you will need to use its output xi (a number between 0 and m-1) to construct random die rolls. When generating a die roll for an n-sided die, it is tempting to use xi mod n. This would be a bad idea because it effectively reduces the LCM modulus from m to n. A better approach is to start by computing (xi*n)/m.

Note: You may find the IntInf structure provided by the basis library helpful for manipulating large numbers (same as LargeInt).

Your Task: Complete lcm/Random.sml. You will need to write the specification and implementation for your random number generator from scratch. Writing a good specification is very important for this part. Attach any additional documentation in an ASCII file doc5.txt.


Part 6: Proof by induction (15 pts)

In class we saw a data abstraction for polynomials. For this problem you will prove that the implementation of the addition operator on polynomials works correctly. The signature for polynomials looks as follows:

signature POLYNOMIAL =
  sig
    (* Overview: A poly is a polynomial with integer coefficients.
     * For example, 2 + 3*x - 5*x3. *)
    type poly 
    (* plus(p, q) is p+q. *)
    val plus: poly*poly -> poly
    ...
  end

Here is an implementation showing just the representation and the code for plus:

structure Polynomial :> POLYNOMIAL =
  struct
    type poly = int list
    (* AF: [a0,...,an] represents a0 + a1*x + a2*x2 + ... + an*xn
     *     [] represents 0
     * RI: if the list is not empty, its last element is not zero
     *)
    fun plus(p: poly, q: poly): poly =
      case (p,q) of
        (p, nil) => p
      | (nil, q) => q
      | (a::p', b::q') =>
          case (a+b)::plus(p',q') of
            [0] => []
          | r => r
  end

Prove by induction that this implementation of plus is correct:

  1. State the condition to be proved.
  2. Express this condition as a proposition in terms of an integer n. Hint: let n be the length of the shorter of the two lists p and q.
  3. Show that plus works correctly in the base case (what is n for the base case?)
  4. Prove the induction step. Remember to establish that the rep invariant holds on output!

Your Task: Turn in a file polynomial.txt in simple ASCII format containing solutions to parts a–d. Note: this is a good problem to do as a warm-up for the prelim.


Optional Part 7: Probabilistic Karma

Now that you have implemented all of the required parts, you are in a position to answer the following questions, which will earn you karma points. Karma points should not be confused with real points. Seek further enlightenment regarding the value of karma points from your favorite course-staff member.

Apples and Oranges

The parameters a and c that are used to generate pseudo-random numbers are generally chosen only if they pass a number of tests of randomness. We have implemented one such test in Warm.sml. We use your LCM to pick random numbers. Complete the rand function in lcm/Warm.sml to use your implementation. For each pair of random numbers, we check if the numbers are coprime or not. A pair of numbers (a, b) is called coprime if GCD(a,b) = 1, GCD being the greatest common divisor. We keep track of the percentage of such pairs that are coprime. Denote this percentage as P. We then compute M = √6/P. What is the true value of this M?

Due to the pseudo-randomness of the LCM, you may need to run Warm.apple() multiple times with different values of a and c for your LCM. For good choices of (a, c) the returned values will all lie in the vicinity of the true M. If the returned value for some a and c lies really far away from all the other values, you can conclude that the (a, c) pair generated noticeably nonrandom numbers.

Divide and Conquer

In the board game Risk™, an attacker's army consisting of n soldiers and a defender's army consisting of m soldiers go to war according to the following dice language. The attacker rolls 3 (6-sided) dice, of which the greatest two values are retained. Call these values A1 and A2 where A1 is the larger of the two. The defender rolls (at most) 2 dice. Call these values D1 and D2 where D1 is the larger of the two. Now, if AiDi the attacker loses one soldier. Otherwise, the defender loses one soldier. This continues until one party has no soldiers left.

Assuming that the defender rolls exactly two dice and attacker rolls 3 dice; write a dice language program that returns the multiset {1, 1} if the attacker loses 2 soldiers, {1, 2} if the attacker and defender both lose one soldier each, and {2, 2} if the defender loses two soldiers. Compute the distribution for the program you wrote and determine whether the Risk dice roll is partial towards attackers or towards defenders.

Your Task: Enter your solution in karma/karma.txt


Many cans of soda were harmed in the making of this problem set.