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 [[b]] = 10, then converting to decimal representation is trivial, but
storing bignums requires lots of memory.
- The larger [[b]] is, the less memory is required, and the more
efficient everything is.
- If [[b]] is a power of 10, converting to decimal is
relatively easy and is very efficient.
Otherwise it requires (possibly long) division.
- If [[(b-1) * (b-1)]] fits in a machine word, than you can implement
multiplication in high-level languages without difficulty.
(Serious implementations pick the largest [[b]] such that [[a[i]]] is
guaranteed to fit in a machine word, e.g., [[2^32]] on modern
machines.
Unfortunately, to work with such large values of [[b]] requires
special machine instructions to support ``add with carry'' and 64-bit
multiply, so serious implementations have to be written in assembly
language.)
- If [[b]] is a power of 2, bit-shift can be very efficient, but
conversion to decimal is expensive.
Fast bit-shift can be important in cryptographic and communications
applications.
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:
- The operations in the cluster must not mutate existing numbers.
- [[digits]] returns a
list of the digits of
the number, with the least significant digit first.
Leading zeroes
should be suppressed unless the value of the Bignat is itself zero.
Very important: [[digits]] must return a list of decimal
digits, even if base 10 is not what is used in the representation.
You will find the [[addl]] and [[addh]] operations useful.
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:
- Implement the cluster using an internal base of 10. Measure the time needed
to compute the first 175 factorials.
- 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:
- You can practically copy code from
Hanson's implementation
(see also his distribution), but
unless you've looked at the book you may be a bit overwhelmed.
[[XP_add]] does add with carry.
[[XP_sub]] does subtract with borrow.
[[XP_mul]] does [[z := z + x * y]], which is useful, but is not what we want unless
[[z]] is zero initially.
Moreover, Hanson has to pass all the lengths explicitly.
You won't have to do that because length is built in to the CLU arrays.
- Mutation is used heavily in
Hanson's implementation, but
you are
called upon to implement a mutation-free specification.
It's OK to mutate things internally, though, as long as we mutate only
newly allocated things.
We can't let mutation be visible outside the interface.
- You can probably just use [[array[int]]] for the representation,
and use the [[array[int]$size]] operation to keep track of the size.
If you use the [[digit]] procedure carefully, you'll only have to
worry about sizes when you allocate new results.
- It's easiest to write ``add with carry'' and ``subtract with
borrow,'' then treat normal add and subtract as special cases.
- In order to implement the [[digits]] operation without long division,
you will find it convenient to choose a base b that is a power
of 10.
- Auxiliary functions should be defined within the cluster (but not
`exported'). Do not define auxiliary functions at top level.
- See the excerpts from the CLU documentation
- Make sure you understand the difference between
[[array[int]$create(0)]] and [[array[int]$new()]].
Here lurks a trap for the unwary.
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:
- A file contains a list of clusters and procedures.
Define a type [[toplevel]] to represent these.
You may want your [[toplevel]] also to include simple expressions, to
make things easier to debug.
- A cluster has a name, a list of type parameters,
an [[exports]] list, a [[rep]] type, and a list
of (internal) declarations.
- A declaration binds a name to a variable, a type, or a procedure.
Variables are typed.
Define a type [[decl]] to represent declarations.
- Types may be
- application of a type constructor (like [[array]] or [[int]]) to
a (possibly empty) list of types.
- Record types. A record type is a list of (name, type) pairs, one for
each field.
- A procedure includes a list of arguments (each with a type), an optional result
type, and a body that is a sequence of statements.
- Statements include if, while, set, begin, print, and procedure call.
Use type [[stmt]] to represent statements.
- Expressions include unqualified variables, qualified variables
(T$name), application, and record construction.
You may assume there is no syntactic sugar.
Use type [[exp]] to represent expressions.
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:
- Design a concrete syntax for your subset, using the parenthesized
prefix notation.
- Steal the parsing code from the Scheme interpreter, and write a
function that translates the type [[ipt]] into your abstract syntax.
- Associate values with the clusters in the initial basis.
For example, the value associated with [[fetch]] will be
<>=
PRIMOP (fn [ARRAY a, INT i] => Array.sub(a, i))
@ Your type checker will guarantee that [[fetch]] always gets the
right number and types of arguments.
- You can try writing a [[use]] primitive that reads in a file and then
interprets top-level expressions.
You can also just read in a file and execute the procedure called
[[main]].
What to submit
For this assignment you should submit the files README,
and one of the following:
-
big.clu, and possibly files for extra credit.
Don't forget that your big.clu should use the largest
possible base that is still a power of 10.
- All of the following:
- File clu.sml, containing your abstract syntax and your
type checker.
Your abstract syntax should have type [[exp]], and your checker should
include:
<>=
val expty : exp * type_env -> ty
val declty : decl * type_env -> type_env
val stmtCheck : stmt * type_env -> unit
@
- File findmin.sml, containing your abstract syntax for the
``find minimum'' procedure, which should be [[val findmin : decl]].
- A README.ps or hard copy that includes the formal type rules
for your system.