CS152 Assignment: CLU

Due Wednesday, December 1, at 12:10 AM (aka Tuesday night at midnight)

Do one of the problems B and C below. Problem C is worth double problem B; if you choose to do problem C, I'll drop a low 50 points of homework grade. If you choose to do both problems B and C, I'll drop a low 100 points of homework grade.


Running CLU

First of all, there is a CLU emacs mode in /usr/local/pclu/elisp/setup.el. You want to load this, because it will give you access to all of the documentation online. When the ICG fixes thing, this line in your ~/.emacs should do it:
(load "/usr/local/pclu/elisp/setup")
Emacs should go into clu-mode automatically on editing, e.g., big.clu. For a test drive, try C-h d array for info about arrays, or, e.g., C-h d array$addh for info on specific operations. C-h m from within CLU mode will give you more information about CLU and CLU mode in general.

Extensive documentation is available in /usr/local/pclu/doc, but I hope the emacs help will be enough.

You can run the CLU compiler using the scripts in ~cs152/bin. To build your factorial program, you'll want your cluster in big.clu and your main program in fact.clu. You'll need to run the following commands (you can view a transcript):

clulib -spec big.clu -dump big.lib
pclu -merge big.lib -opt -co fact.clu
pclu -opt -co big.clu
clulink -opt -o fact fact.o big.o
If you leave off the -opt, you'll get the debugger instead of just running your program. I found the debugger fairly brain dead, but your mileage may vary.

If you're curious about what's going on here, you can consult the documentation.


B. Bignums.

Sometimes you want to do computations that require more precision than you have available in a machine word. The Scheme and Icon programming languages both provide ``bignums.'' These are integer implementations that automatically expand to as much precision as you need. Because of their dynamic typing discipline, Scheme and Icon make this transition completely transparently to the user---you can't easily tell when you're using native machine integers and when you're using bignums. In real CLU it will be the same, since the notation x + y is syntactic sugar for T$add(x, y) where T is the type of x. This means the data abstraction can almost completely hide whether you have regular or extra-precision integers.

You will find bignums and the bignum algorithms discussed at some length in Dave Hanson's book and in the article by Per Brinch Hansen. Be aware that your assignment below differs significantly from the implementation in Hanson's book.

Because I'm trying to take it easy on you, you will implement a toy version of bignums. (The price you pay is that all the interesting problems---including a real version of bignums---are for extra credit.) You will experiment with a couple of different representation choices. Since the data abstraction hides these choices from the client code, we can be sure that clients aren't at all affected. If we wanted, we could take more serious measurements and pick the representation that is best on efficiency grounds.

The basic idea of bignums is this: we represent a nonnegative integer as an array [[a]] of integers such that an integer [[x]] is represented by the sum of [[a[i]*b^i]], where [[^]] denotes exponentiation. The index [[i]] runs from 0 up to some value beyond with all the [[a[i]]] are zero. The value [[a[i]]] is called a digit, and it has the property that [[0 <= a[i] < b]]. [[b]] is called the base.

Here's more:

Our hardware supports [[b = 2]] and sometimes [[b = 10]], but when we want bignums, it's the choice of [[b]] that is hard to make in the general case:

If you want signed integers, there are more choices --- signed-magnitude or [[b]]'s-complement. Knuth volume 2 is pretty informative about these topics.

Using the real CLU implementation, define a bignat cluster that represents nonnegative integers and exports a small number of arithmetic operations. Do so by completing the following cluster: <>= bignat = cluster is new, add, sub, mul, lt, equal, unparse, digits base = <> % pick a base aint = array[int] rep = aint % least significant digit first, low bound = 0 always % create a new bignum, complaining if negative new = proc(x : int) returns (bignat) signals (negative) if x < 0 then signal negative else a : aint := aint$create(0) % array with low bound of 0 <> return (up(a)) end end new % not exported -- return the coefficient of b^n even when n too large digit = proc (x : aint, n : int) returns (int) return (x[n]) except when bounds: return (0) end end digit % not exported -- remove *all* leading zeroes trim = proc(z: aint) returns (aint) while aint$size(z) > 0 cand aint$top(z) = 0 do aint$remh(z) end return (z) end trim <> % add two bignats add = proc (x, y: cvt) returns (cvt) <> return (trim(z)) end add % subtract two bignats. signals negative if result would be negative sub = proc (x, y: cvt) returns (cvt) signals (negative) <> if b = 0 then return (trim(z)) else signal negative end end sub % implement x = y equal = proc (x, y : cvt) returns (bool) <> end equal % implement x < y lt = proc (x, y : cvt) returns (bool) <> end lt % implement x * y mul = proc(x, y: cvt) returns (cvt) <> return (trim(z)) end mul % return *decimal* digits of x, least significant first % implementation may depend on the value of base digits = proc(x : cvt) returns (array[int]) z : aint := aint$new() <> z := trim(z) if aint$size(z) = 0 then aint$addh(z, 0) end return (z) end digits % convert a bignat to a string unparse = proc(z : bignat) returns (string) zero = char$c2i('0') ds : aint := digits(z) n : int := aint$size(ds) cs : array[char] := array[char]$new() for i: int in int$from_to(aint$low(ds), aint$high(ds)) do % add chars, putting *most* significant digit first array[char]$addl(cs, char$i2c(zero + ds[i])) end return(string$ac2s(cs)) end unparse end bignat @ I had to add 61 lines of code to fill in this cluster. It took me about 2 hours, with many references to the CLU documentation. Notes:

To help you test your work, here is a program that prints the first 50 factorials: <>= factorial = proc (n: bignat) returns (bignat) if n = bignat$new(0) then return(bignat$new(1)) end return(n*factorial(n-bignat$new(1))) end factorial start_up = proc () pi: stream := stream$primary_input() po: stream := stream$primary_output() b : bignat for n: int in int$from_to(1, 50) do b := factorial(bignat$new(n)) stream$putl(po, int$unparse(n) || "! = " || bignat$unparse(factorial(bignat$new(n)))) end end start_up @ We will use other functions to test your clusters.

Solve the following problems:

  1. Implement the cluster using an internal base of 10. Measure the time needed to compute the first 175 factorials.
  2. Make an argument for the largest possible base that is still a power of 10. Change your cluster to use that base internally. (If you are both careful and clever, you should be able to put the base in an internal variable and not change any other code.) Measure the time needed to compute the first 175 factorials. Note both your measurements and your argument in your README file.
You might find it useful to test your implementation with the following table of factorials:
 1! = 1
 2! = 2
 3! = 6
 4! = 24
 5! = 120
 6! = 720
 7! = 5040
 8! = 40320
 9! = 362880
10! = 3628800
11! = 39916800
12! = 479001600
13! = 6227020800
14! = 87178291200
15! = 1307674368000
16! = 20922789888000
17! = 355687428096000
18! = 6402373705728000
19! = 121645100408832000
20! = 2432902008176640000
21! = 51090942171709440000
22! = 1124000727777607680000
23! = 25852016738884976640000
24! = 620448401733239439360000
25! = 15511210043330985984000000

Hints:


Extra Credit (signed integers). Define a Bignum cluster that represents signed integers and exports the same set of operations, except that
[[digits = proc (Bignum) returns (int, array[int])]]
should return two values, of which the first is +1, -1, or 0 depending on the sign of the Bignum, and the second is a list of the digits of the number, with the least significant digit first.

It's probably simplest to use a sign-magnitude representation, and re-use your [[Bignat]] cluster as much as possible. If you wish, you may extend the [[Bignat]] cluster with a subtraction operation that supports ``borrowing.'' Document any extensions in your README file. Again, your bignum operations must not mutate existing numbers.

Using the same large base as you used in part (2), measure the time needed to compute the first 170 factorials using [[Bignum]] instead of [[Bignat]]. Approximately how much overhead is added by tracking signs?

Extra Credit (division). Implement long division for [[Bignat]] and [[Bignum]]. If this changes your argument for the largest base in part 2, explain how.

Extra Credit (large base). Change the base to the largest reasonable base, not necessarily a power of 10. You will have to re-implement [[digits]] using long division. Measure the time needed to compute the first 170 factorials. Does the smaller number of digits recoup the higher cost of converting to decimal?

Extra Credit (space). Instrument your cluster to keep track of the size of numbers, and measure the space cost of the different bases. Estimate the difference in garbage-collection overhead for computing with the different bases, given a fixed-size heap.

Hard Extra Credit (pi). Use a power series to compute the first 100 digits of pi (the ratio of a circle's circumference to its diameter). Be sure to cite your sources for the proper series approximation and its convergence properties. Hint: I vaguely remember that there's a faster convergence for pi over 4. Check with a numerical analyst.


C. CLU type checking. Kamin has missed the point of CLU by leaving out static type checking. Define a subset of CLU, write type rules, and write a type checker.

Using ML, define an abstract syntax for CLU that includes the following elements:

Use the following representation for types. A type is either a cluster, a record type, or a procedure type. <>= datatype ty = CLUSTER of string -> ty | PROCTYPE of {args : ty list, return : ty option } | RECORD of (string * ty) list @ Because there is an infinite recursion going on here ([[int]] is a cluster, which contains an operation [[add]], which returns [[int]], which is a cluster, ...), we resort to trickery with functions to create types: <>= fun binop t = PROCTYPE { args = [t, t], return = SOME t } fun unop t = PROCTYPE { args = [t], return = SOME t } fun boolcluster () = CLUSTER ( fn "and" => binop (boolcluster()) | "or" => binop (boolcluster()) | "not" => unop (boolcluster()) | s => raise NotFound s) val bool = boolcluster() fun compare t = PROCTYPE { args = [t, t], return = SOME bool } fun intcluster() = CLUSTER ( fn "add" => binop (intcluster()) | "sub" => binop (intcluster()) | "mul" => binop (intcluster()) | "div" => binop (intcluster()) | "minus" => unop (intcluster()) | "lt" => compare (intcluster()) | "eq" => compare (intcluster()) | s => raise NotFound s) val int = intcluster() fun array t = CLUSTER ( fn "store" => PROCTYPE { args = [array t, int, t], return = NONE} | "fetch" => PROCTYPE { args = [array t, int], return = SOME t} | "new" => PROCTYPE { args = [int, t], return = SOME (array t)} | s => raise NotFound s) @ Because clusters and procedures can take type parameters, your type environment has to associate cluster and procedure names with functions from types to types. Most such functions are nullary. <>= type type_env = (ty list -> ty) env fun nullary t [] = t | nullary t _ = raise Type fun unary cluster [t] = cluster t | unary cluster _ = raise Type val initialGamma = [("int", nullary int), ("bool", nullary bool), ("array", unary array)] : type_env @ Now if you have an application of a type constructor, you can look up the type constructor in the environment, then apply the associated function to get a type.

Write down formal type formation, introduction, and elimination rules for CLU types.

In ML, write a type checker for your abstract syntax.

Using your abstract syntax, construct a procedure that finds the minimum of an array of integers. Run the procedure through your type checker.


Extra Credit (Eval). Write an [[eval]] function to interpret CLU procedures. Use this representation of values: <>= datatype value = INT of int | ARRAY of value Array.array | PRIMOP of value list -> value | FUNCTION of string list * exp @ You can use [[INT]] to represent integers and booleans, and [[ARRAY]] to represent arrays and records.

More Extra Credit (interpreter). Build a CLU interpreter. You can use the following steps:


What to submit

For this assignment you should submit the files README, and one of the following: