The variables of the source languages are mapped into memory addresses.
x = x + 1;Translated code (in some assembly language). Assume that x is mapped into the memory location 1010 and that operations can only be performed on registers. Let R1 be a register.
LOAD 1010,R1 //copy content of location 1010 in R1 ADD R1,#1 //add constant 1 to R1 STO R1,1010 //copy content of R1 in location 1010One of the main issues is the association between variables and locations: it is in general impossible to know at compile time how many variables are going to be created at run time (because of recursive procedures containing local variables and because of dynamic variables). Hence we cannot solve all the allocations at compile time. Additionally, we want to be efficient in the management of the memory: We only want to allocate storage for a variable when needed, and we want to deallocate when possible.
We will call lifetime of a variable the period of time during execution in which the variable has storage allocated for it.
We want lifetime to correspond to the need for the variable - must be at least as long as the need. Typically, we compromise and choose standard times for allocation and deallocation:
In this kind of implementation, the memory of the machine is divided in three parts:
We will discuss here how the stack is handled. Let us consider in detail the various operations that take place when a procedure is called. We describe here what happens in the standard implementation of C and similar ones; other languages there may be some differences, mainly due to different parameter-passing strategies. We consider here call by vale and call by value-result.
Typically, an AR contains storage (locations) for:
In languages with nested procedures, however, the situation is more complicated. Next section is dedicated to discussing this issue.
There are two possible scoping rules which determine the declaration which should be associated to the occurrence of a non-local variable x in p:
In this way, we can always find the address of a non-local variable x: we just follow the chain of the static links until we "find" a declaration for x. (Actually, in real implementations the number of static links we need to traverse is determined statically, and the address for x in the AR is determined by a fixed offset.)
In dynamically-scoped languages we don't need a static link: the declaration valid for a variable x can be found by following the chain of the dynamic links.
procedure p p
procedure q / \
<body of q>; / \
procedure r q r
procedure s |
<body of s>; |
<body of r>; s
<body of p>;
We have that:
procedure p;
var x,y: integer; // variables x,y local to p
procedure q(y: integer); // procedure q local to p
begin if x = y then r else write(x) end; // body of q
procedure r; // procedure r local to p
var x : integer; // variable x local to r
begin x := 2; if y = 2 then q(x) else write(x) end; // body of r
begin // begin body of p
x:= 1;
y:= 2;
q(x)
end; // end body of p
We have that an activation of p prints:
caller of p p I A.R. where p is declared
^ ------------------ ^
CL |___|_ | |
| | |
------------------ |
| _|______| SL
| |
------------------
|-->| ----- |<-|---|----|
| | x | 1 | | | | |
| | ----- | | | |
| | y | 2 | | | | |
| | ----- | | | |
CL | ------------------ | | |
| | | |
| q I | | |
| ------------------ | | |
|___|_ | | | |
| | | | |
------------------ | | |
| _|__|SL | |
| | | |
------------------ | |
|-->| ----- | | |
| | y | 1 | | | |
| | ----- | | |
CL | ------------------ | |
| | |
| r I | |
| ------------------ | |
|___|_ | | |
| | | |
------------------ | |
| _|______| SL |
| | |
------------------ |
|-->| ----- | |
| | x | 2 | | |
| | ----- | |
CL | ------------------ |
| |
| Q II |
| ------------------ |
|___|_ | |
| | |
------------------ |
| _|___________| SL
| |
------------------
| ----- |
| y | 2 | |
| ----- |
------------------
CL = Control Link
SL = Static Link