Desired Soundness Property for program translations: programs before and after translation have the same execution behavior (in our case, termination and same numerical output, but in general the same i/o behavior).
x in Function y -> x * y
is nonlocal: it is not the parameter and is used in the body).
add = Function x -> Function y -> x + yIn the body
x + y, x is nonlocal and
y is local.
What should add 3 return?
Function y -> x + y
would be stupid because the variable x would not
have a value.
Function y -> 3 + y
amounts to a substitution, something a compiler can't do since
compiled code should be fixed (code is not mutable).
Answer: return a closure, consisting of the function and an environment which remembers the values of the nonlocal variables for later use:
(Function y -> x + y, { x |-> 3 })
Some more structure than this is needed in order for the function to
get its x value when invoked; we cover that next.Closure conversion is a global program transformation that explicitly performs this operation in the language itself. Core ideas
Function .. , they
are closures, i.e. tuples of the function and an environment remembering the
values of nonlocal variables
add above translates to
add' = { fn = Function xx ->
{ fn = Function yy -> (yy.envt.x) + (yy.arg); envt = { x = xx.arg } };
envt = {} }
Whew! This is a pretty complicated operation. Some comments.
{ fn = Function ...; envt = {x = ...; ...}},
consisting of the
original function (fn field) and the nonlocals
environment (envt field).
{ x = xx.arg },
x was a nonlocal variable and its value is
remembered in this record under a label of the same name, x.
y are
modified to take an argument named
yy (double up the original
variable name). Note we don't really have to change the name
but it can help save confusion.
yy etc are always expected to
be records of the form
{ envt = ..; arg = ..}, passing both the
environment and the original argument to
the function.
yy.envt.x is the new way to access what
was a nonlocal variable x in the function
body (where the function had parameter previously named
y);
yy.arg is the new way to access what was
the (single) argument to the function y.
{ x = xx.arg }, the left x is a label
and the right xx a variable.
Function call add 3 after closure conversion then must pass in the
environment since the caller needs to know it.
(add'.fn)({ envt = add'.envt; arg = 3})
Translation of add 3 4 takes the result of the above, which is
a function closure { fn = ...; envt = ... }, and does the
same trick to apply 4 to it:
Let add3' = (add'.fn)({ envt = add'.envt; arg = 3}) In
(add3'.fn)({ envt = add3'.envt; arg = 4})
and the result would be 12, the same as the original result,
confirming the soundness of the translation in this case.
In general,
add' 3 closure above,
add3', when
it is applied later to e.g. 7, the
envt will know it is 3 that is to be
added to 7.
Well, its even slightly more complicated if we consider one more level of nesting of functions, for example
triadd = Function x -> Function y -> Function z -> x + y + z--the
z function needs to get the x value,
and since that function is defined in the Function y,
that function has to be an intermediary which passes x.Here is the translation.
triadd' =
{ fn = Function xx ->
{ fn = Function yy ->
{ fn = Function zz -> (zz.envt.x) + (zz.envt.x) + (zz.arg);
envt = { x = yy.envt.x; y = yy.arg } };
envt = { x = xx.arg } };
envt = {} }
Some observations about this example
z function has nonlocals x
and y so those need to be in its environment;
y function doesn't directly use nonlocals, but
it has nonlocal x because the function inside it,
Function z, needs it. So its envt has
x in it.
Function z can get x into its
environment from y's environment, as yy.envt.x.
This is y being middleman to get x to
z. Whew!
clconv(e)
to express the closure conversion function, defined inductively as
follows (this code is informal, it uses concrete DSR syntax which
in the case of e.g. records looks like Caml syntax).
clconv(x) = x (* variables *)
clconv(n) = n (* numbers *)
clconv(b) = b (* booleans *)
clconv(Function x -> e) = letting x, x1, ...,
xn be precisely the free variables in e, the
result is the DSR expression
{ fn = Function xx -> SUB[clconv(e)],
envt = { x1 = x1; ...; xn = xn } }
where SUB[clconv(e)] is clconv(e) with
substitutions (xx.envt.x1)/x1,...,(xx.envt.xn)/xn
and (xx.arg)/x performed on it, but
not substituting in Function's inside
clconv(e) (stop substituting when you hit a Function).
clconv(e e') = Let f = clconv(e) In (f.fn){ envt = f.envt;
arg = clconv(e')}
clconv(e op e') = clconv(e) op clconv(e') for all other
operators in the language (the translation is homormorphic in
all of the other operators). This is pretty clear in every
case except maybe records which we will give just to be sure...
clconv( { l1 = e1; ...; ln = en } ) = { l1 = clconv(e1);...; ln = clconv(en) }
clconv(add) = add'.
The desired soundness result is
Theorem: e computes to a value if and only if clconv(e)
computes to a value. Additionally, if one returns numerical value n,
the other returns the same numerical value n.
This operation results in a language that has no nonlocal variables in functions, more like the C/C++ languages. We are getting closer to machine code.
Let
statements to evaluate expressions in the
order indicated by the operational semantics.
4 + (2 * (3 + 2))Our interpreter defined a tree-notion of evaluation order on such expressions. The order in which evaluation happens on this program can be made explicitly linear by using
Let to factor out the parts that are
evaluated first:
Let v1 = 3 + 2 In Let v2 = 2 * v1 In Let v3 = 4 + v2 In v3
v1 etc variables are temporaries; in
machine code they generally end up being registers
We are in fact going to define a more simple translation, that also first assigns constants and variables to other variables:
Let v1 = 4 In Let v2 = 2 In Let v3 = 3 In Let v4 = 2 In Let v5 = v3 + v4 In Let v6 = v2 * v5 In Let v7 = v1 + v6 In v7Some points
e |-->
v is a Let in the above (Exercise: write out the
operational semantics derivation and compare).
4+v2 may not be
low-level enough for some machine languages since there may be no add
immediate instruction.
Let is a primitive in DSR -- this is not
strictly necessary, but if Let were defined in terms of
application, the A-translation results would be harder to manipulate.
((Function x -> Function y -> y)(4))(2)the function that
2 is being applied to first needs to be
computed. We can make this explicit as well:
Let v1 = (Function x -> Function y -> y)(4) In Let v2 = v1(2) In xThe A-translation given below does even more linearization on this example:
Let v1 =
(Function x ->
Let v1' = (Function y -> Let v1'' = y in v1'') In v1') In
Let v2 = 4 In
Let v3 = v1 v2 In
Let v4 = 2 In
Let v5 = v3 v4 In
v5
Every other evaluation construct can be linearized in this fashion.
Except If:
If (3 = x + 2) Then 3 Else 2 * xcan be turned into (approximately)
Let v1 = x + 2 In Let v2 = (3 = v1) In If v2 Then 3 Else Let v1 = 2 * x In v1but the
If still has a branch in it. If in machine code as
v1 := x + 2 v2 := 3 = v1 BRANCH v2, L2 L1: v3 := 3 GOTO L3 L2: v4 := 4 L3:So, this form is quite close to machine code.
We will give the A-translation the core DSR syntax.
The intermediate result of the translation is a list of tuples
[(v1,e1); ...; (vn,en)] : (ide * term) listwhich is intended to represent
Let v1 = e1 In .. In Let vn = en In vn ...but is a form easier to manipulate in Caml since lists of declarations will be appended together at translation time. In your compilers, you may or may not want to use this intermediate form, it is not much harder to write the functions to work directly on the
Let
representation.
atrans(e) : term -> term. We will always apply A-translation to the
result of closure conversion, but we really don't need to be aware of
that now. We now sketch the translation for the core
primitives.We assume auxiliary functions:
newid() which returns a
fresh DSR variable every time called,
letize
which converts from the list-of-tuples form to the actual Let form,
and
resultId which for list
[(v1,e1); ...; (vn,en)] returns result identifier vn.
let atrans e = letize (atrans0 e)
and atrans0(e) = match e with
(Var x) -> [(newid(),Var x)] |
(Int n) -> [(newid(),Int n)] |
(Bool b) -> [(newid(),Bool b)] |
Function(x,e) -> [(newid(),Function(x,atrans e)] |
Appl(e,e') -> let a = atrans0 e in let a' = atrans0 e' in
a @ a' @ [(newid(),Appl(resultId a,resultId a')] |
...
(* all other D binary operators + - = AND etc of form identical to Appl *)
...
If(e1,e2,e3) -> let a1 = atrans0 e1 in
a1 @ [(newid();If(resultId a1,atrans e2,atrans e3)] |
...
At the end of the A-translation, the code is all "linear" in the way
it runs in the interpreter, not as a tree.
Theorem: The A translation is sound, i.e. e
and atrans(e) both either compute to values or both diverge.
let intermedresult e = atrans(clconv(e))
main function.
hoist function.
4 + (Function x -> x + 1)(4)and replace it by
Let f1 = Function x -> x + 1 In 4 + f1(4)--in general, hoist all functions to the front of the code and give them a name via
Let.We will define this process in a simple iterative (but inefficient) manner:
let hoist e =
ife = e1[(Function ea -> e')/f]for somee1withffree in it, ande'contains no functions (i.e.Function ea -> e'is an innermost function)
thenLet f = (Function ea -> e') In hoist(e1)
elsee.
Let f1 = Function ea -> e1 In
...
fn = Function ea -> en In
e
Where each e1,...,en,e contain no function constants.
Theorem: e computes to a value if and
only if hoist(e) computes to a value.
This Theorem is easily proved from iterative application of the following Lemma
Lemma:
(e1[(Function ea -> e')/f]) ~= (Let f = (Function ea -> e') In e1)
We lastly transform the program to
Let f1 = Function x1ea -> e1 In
...
fn = Function -> Function xnea -> en In
main = Function dummy -> e In
main(anything)
So, the program is officially nothing but a collection of functions.
This brings the program closer to the form of a C program.
let frontend e = hoist(atrans(clconv(e)))
main function
If which is a branch).
Let variable assignments that come out of the
A-translation (in the following vi, vj, vk, f are variables).
Fact: DSR programs that have passed through the first
three phases should have function bodies consisting of tuple lists
where each tuple is of one of the following forms only:
x for variable x
n for number n
b for boolean b
vi vj (application)
vj + vk
vj - vk
vj And vk
vj Or vk
Not vj
vj = vk
Ref vj
vj := vk
!vj
{ l1 = v1; ... ; ln = vn }
vi.l
If vi Then tuples1 Else tuples2 tuples1 and tuples2 are the lists of
variable assigments for the Then and Else
bodies.
sp is used
{ register int sp; /* compiler will assign sp to a register */
*(sp - 5) = 33;
printf(*(sp - 5));
}
Definition. A register/memory location
vi's value is stored boxed if vi
holds a pointer to a block of memory containing the actual value.
A variable's value is unboxed if it is directly in the
register/memory location vi.
For multi-word entities, storing them unboxed means variables directly hold a pointer to the first word of the sequence of space.
Here is C's memory layout convention:
int x; float x; int arr[10]; snork
x for snork a struct.)
v = f" in C for f a
previously declared function and not
"v = &f", but that is because the former is really
syntactic sugar for the latter. A pointer to a function is in
fact a pointer to the start of the code of the function.
int *x; float *x; int *arr[10]; snork
*x for snork a struct.)
int glob;
main()
{
int x;
register int reg;
int* mall;
int arr[10];
x = glob + 1;
reg = x;
mall = (int *) malloc(1);
x = *mall;
arr[2] = 4;
/* arr = arr2; -- illegal in C -- arrays not boxed so can't do this */
}
Assembly (%o1 is a register, [%o0] means
dereference, [%fp-24] means subtract 24 from frame
pointer register %fpand dereference)
main:
sethi %hi(glob), %o1
or %o1, %lo(glob), %o0 /* load global address glob into %o0 */
ld [%o0], %o1 /* dereference */
add %o1, 1, %o0 /* increment */
st %o0, [%fp-20] /* store in [%fp-20], the memory 20 back from fp -- this is x */
/* note x directly contains a number, not a ptr */
ld [%fp-20], %l0 /* %l0 IS reg (its in a register directly) */
mov 1, %o0
call malloc, 0 /* call malloc. resulting address to %o0 */
nop
st %o0, [%fp-24] /* put newspace location in mall ([%fp-24]) */
ld [%fp-24], %o0 /* load mall into %o0 */
ld [%o0], %o1 /* this is a malloced structure -- UNBOX! */
st %o1, [%fp-20] /* store into x */
mov 4, %o0
st %o0, [%fp-56] /* array is directly a sequence of memory on stack - no indirection needed */
.LL2:
ret
restore
Memory layout for our compilers
Observations:
refs must be boxed to be heap-allocated since they can be referred to after a
function returns:
Let f = (Function x -> Ref 5) In !f(_) + 1--if 5 were stored on the stack, after the return it could be wiped out.
Let-defined entities in our tuples (the
vi) can
be either in registers or on the call stack:
register
Word variables:
register Word v1, v2, v3, v4, ... ;
(Function x -> x.l)(If y = 0 Then {l = 3} Else {a = 4; l = 3})
--field l will be in two different positions in these
records so the selection will not know where to look.
4
(5);
vk.l is implemented by hashing on key
"l" in the hashtable vk points to (at run-time).
struct type for each function).
For instance,
(Function x -> x.l)(If y = 0 Then {l = 3} Else {a = 4; l = 3})
the code x.l will invoke a call of approximate form
hashlookup(x,"l"). {a = 4; l = 3} will create a
new hash table and add mappings of "a" to 4 and "l" to 3.
toCTuple mapping an atomic tuple to a C statement string,
toCTuples mapping a list of tuples to C statements,
toCFunction mapping a primitive DSR function to a
string defining a C function,
toC mapping a list of prmitive DSR functions to a
string of C functions.
The translation as informally written below takes a few liberties for simplicity.
"..." below are sloppily written, for
instance "vi = x"
is sloppy shorthand for tostring(vi) ^
" = " ^ tostring(x)
Let x1 = e1 In Let ... In Let xn = en In xn
of function and then/else bodies are assumed to have
been converted to lists of tuples
[(x1,e1),...,(xn,en)], and
similarly for the list of top-level function definitions. In
your compilers, it probably will be easier just to keep them in
Let form, but the choice is yours.
toCTuple(vi = x) = "vi = x;" (* x is a DSR variable *)
toCTuple(vi = n) = "vi = n;"
toCTuple(vi = b) = "vi = b;"
toCTuple(vi = vj + vk) = "vi = vj + vk;"
toCTuple(vi = vj - vk) = "vi = vj - vk;"
toCTuple(vi = vj And vk ) = "vi = vj && vk;"
toCTuple(vi = vj Or vk ) = "vi = vj || vk;"
toCTuple(vi = Not vj ) = "vi = !vj;"
toCTuple(vi = vj = vk) = "vi = (vj == vk);"
toCTuple(vi = (vj vk) = "vi = *vj(vk);"
toCTuple(vi = Ref vj) = "vi = malloc(WORDSIZE); *vi = vj;"
toCTuple(vi = vj := vk) = "vi = *vj = vk;"
toCTuple(vi = !vj) = "vi = *vj;"
toCTuple(vi = { l1 = v1; ... ; ln = vn }) =
/* 1. malloc a new hashtable at vi
2. add mappings l1 -> v1 , ... , ln -> vn */
toCTuple(vi = vj.l) = "vi = hashlookup(vj,"l");"
toCTuple(vi = If vj Then tuples1 Else tuples2) =
"if (vj) { toCTuples(tuples1) } else { toCTuples(tuples2) };"
toCtuples([]) = ""
toCtuples(tuple::tuples) = toCtuple(tuple) ^ toCtuples(tuples)
toCFunction(f = Function xx -> tuples) =
"Word f(Word xx) {" ^ ... declare temporaries ...
toCtuples(tuples) ^
"return(resultId tuples); };"
toCFunctions([]) = ""
toCFunctions(Functiontuple::Functiontuples) = toCFunction(Functiontuple) ^ toCFunctions(Functiontuples)
toC then invokes toCFunctions on its list of functions.
Question: why is a fresh memory location malloc'ed
for a Ref?? This is a subtle issue, but the code
vi = &vj would definately not work for the
Ref case.This translation sketch above leaves out many details. Here is some elaboration.
For typing
Word's, where Word is a 1-word type
(defined as e.g. typedef Word char *).
vi = vj + vk will really
be vi = (Word (int vj) + (int vk)) -- cast the
words to ints, do the addition, and cast back to a word.
(*((Word (*)()) f))(arg).
main function (so, you probably want the DSR main
function to be called something like DSRmain and
then write your own main() by hand which will call
DSRmain);
Word v[22]where 22 is the number of temporaries needed in this particular function, and use names
v[0], v[1], etc
for the temporaries. Note, this works well only if
the newid() function is instructed to start
numbering temporaries at zero again upon compiling each new
function.
main() as alluded to above, etc.
resultId) of the
Then and Else
tuples needs to be in the same variable
vi, which is also the variable where the result of
the tuple is put, for the
If code to be correct; your A-translation should
put If tuples in this form to make this phase
correct so go back and patch it.
let frontend e = hoist(atrans(clconv(e)));; let translator e = toC(frontend(e));;We can assert the correctness of our translator. Assert:
DSR program e
terminates in the DSR operational semantics (or
evaluator) just when the C program translator(e)
terminates, provided the C program does not run out of memory. Core
dump or other run-time errors are equated with nontermination.
Furthermore, if DSR's eval(e) returns a number
n, the compiled translator(e) will also
produce numerical output n.
Some simple optimizations include
{fn = .., envt = .. }
as a pointer to a C struct with fn
and envt fields, instead of using the very slow
hash method. Records which do not not have field names
overlapping with other records can also be implemented in this
manner (there can be two different records with the same
fields, but not two different records with some fields the same
and some different).
3 + 4.
Function, or a number or boolean, and a use is a
record field selection, function application, or numerical or boolean
operator.
Definition: In a run-time image, memory location n is garbage if it never will be read or written to again.
There are many notions of garbage detection. The most common is to be somewhat more conservative and take garbage to be memory locations which are not pointed to by any known ("root") object.
Last modified: Fri Apr 5 14:25:20 EST 2002