50.054 - Memory Management¶
Learning Outcomes¶
- Extend PA to support function call and array operations
- Extend the dynamic semantics to model the run-time memory operations
- Define activation records
- Extend SIMP to support function call and array operations
- Describe the challenges of memory management
- Apply linear type system to ensure memory saftey in SIMP.
Pseudo Assembly (Extended)¶
So far, we have been dealing with a toy language without function call nor complex data structure. We consider extending the Pseudo Assembly language syntax as follows
Besides the existing instructions, we include
- \(begin\ f\ d\) - denotes the start of a function name \(f\) (\(f\) is a variable) and the formal argument (operand) \(d\).
- \(call\ f\ s\) - denotes a function invocation of \(f\) with actual argument \(s\).
- \(d \leftarrow alloc\ s\) - denotes the memory allocation. It allocates \(s\) bytes of unoccupied memory and assigns the reference address to \(d\).
- \(free\ s\) - deallocates the allocated memory at address stored in \(s\).
- \(d \leftarrow ref\ s\) - references the value at the memory address stored in \(s\) and copies it to \(d\).
- \(deref\ s_1\ s_2\) - dereferences the memory location stored in \(s_1\) and updates with the value of \(s_2\).
Operational Semantics for Extended PA¶
We extend the operational semantics of Pseudo Assembly defined in here. Instead of mixing the temp variable-to-constant mappings and register-to-constant mappings in \(L\), we move the register-to-constant mappings in \(R\).
- \(G\) - denotes a set of address tuples. The first address in the tuple denotes the starting address of the allocated memory in the heap (inclusive) and the second one denotes the ending address (exclusive).
- \(H\) - denotes the mapping from addresses to constants. \(G\) and \(H\) together model the run-time heap memory
- \(M\) - a tuple of 5 items. A stack of stack frames \(\overline{L}\), a stack of function invocation labels \(\overline{l}\) (a sequence of labels marking the function calling instructions), the heap and the register environment. \(\overline{L}\) and \(\overline{l}\) should have the same size. Given an index \(i\), the \(i\)-th elements in \(\overline{L}\) and \(\overline{l}\) form the activation record.
The small step operational semantic rules in shape \(P \vdash (L, li) \longrightarrow (L, li)\) introduced in our earlier class are modified to have shape of \(P \vdash (M, li) \longrightarrow (M, li)\).
We highlight the important rules
- The rule \((\tt pAlloc)\) defines the memory allocation routine. Given the asking size, \(L(s)\), we make use of the run-time built-in function \(findfree()\) to locate the starting address of the free memory region. We save the starting address in \(d\), and "zero-out" the allocated region addresses ranging from \(loc\) to \(loc+L(s)\).
- The rule \((\tt pFree)\) defines the memory deallocation routine. Given the starting address of the memory to be freed \(loc\), we remove the pair \((loc, loc')\) from \(G\) and the keys in the range \((loc, loc')\) from \(H\).
- The rule \((\tt pRef)\) defines the memory reference operation. Given the referenced address \(loc\), we ensure the address is in \(G\) (which implies it is in \(H\)). We store the value \(H(loc)\) into the stack frame and move onto the next instruction.
- The rule \((\tt pDeref)\) defines the memory deference operation. Given the dereferenced address \(loc\), we ensure the address is in \(G\) (which implies it is in \(H\)). We update the value in \(H\) at location \(loc\) to \(L(s_2)\).
- The rule \((\tt pCall)\) handles the function call instruction. In this case, we search for the begin statement of the callee. We push the new stack frame into the stack with the binding of the input argument. We push the caller's label into the labels stack. The executation context is shifted to the function body instruction.
- The rule \((\tt pBegin)\) processes the begin instruction. Since it is the defining the function, we skip the function body and move to the instruction that follows the return instruction.
- The rule \((\tt pRet1)\) manages the termination of a function call. We pop the stack frame and the top label \(l'\) from the stack. We search for the caller instruction by the label \(l'\). We update the caller's stack frame with the returned value of the function call.
- The rule \({\tt pRet2}\) defines the termination of the entire program.
We omit the rest of rules as we need to change the \(L\) to \(M = (L:\overline{L}, \overline{l}, H, G, R)\).
For example given a PA program
We have the following derivation
(Optional Content) Call Stack Design in Target Platform¶
The call stack in the target platform is often implemented as a sequence of memory locations. The bottom of the stack has the lowest address of the entire stack and the top of the stack has the highest address (at a particular point in time.)
frame for main() |
frame for plus1(x) |
... |
If we zoom into the frame for plus1(x)
address | content |
---|---|
fp-4 | param x |
fp | caller label/return address |
fp+4 | tempvar y |
the frame pointer fp
marks the memory address where ther caller's label/address is stored. If we subtract the parameter size offset from fp
, say fp-4
, we can access the paramter x
and if we add the variable size offset to fp
, we access the temp variables, in this case we can statically determine the size of the call frame, as 3 * 4 bytes. As a convention, the begin
instruction in the target code is associated with the frame size required by this function. When we make a function call in the target code, we have to push the parameter into the call stack one by one.
i.e. the instruction 5
in the above example PA1
should be broken into
- At 5.1, we push the actual argument as the parameter
x
. - At 5.2, we call the function and shift the program counter to the starting label/address of the function body.
- When the function terminates, we jump back to 5.2, then at 5.3, we pop the stack frame based on its size.
In case a function has multiple parameters, the parameters are pushed from right to left.
For example, if we have a min(x,y)
function which has no local variable and we call min(-10,9)
, we generate the following target code
Another example with heap memory access¶
The following PA program is an example of using the memory from the heap.
Instructions 1-12 define the range(x)
function, which initializes an array with the given size x
and the values are from 0
to x
. Lines 13-14 invoke the function and access the 3rd element. Line 15 frees the memory.
Cohort Exercise¶
As an exercise, can you work out the derivation of "running" the above program PA2
using the operational semantics?
SIMP (extended with function and array)¶
We consider syntax of the SIMP language extended with function and array.
New statement syntax includes:
- \(X[E] = E\) - denotes an array element assignment statement
- \(free\ X\) - dellocate the array referenced by variable \(X\).
New expression syntax includes
- \(f[E]\) - function application, where \(f\) is a function name (a special variable)
- \(T[E]\) - array initialization, where \(T\) is the element type of the array and \(E\) denotes the size of the array.
- \(X[E]\) - array element dereference, where \(X\) is an array and \(E\) is the element index.
Function declaration syntax includes
- \(func\ f\ (x:T)\ T\ \{\overline{S}\}\) - \(f\) is the function name; \((x:T)\) is the formal parameter and its type. The second \(T\) is the return type. \(\overline{S}\) is the body.
New type sytanx includes
- \([T]\) - array type
- \(T \rightarrow T\) - function type
- \(unit\) - the type has only one value \(unit\) (its role is similar to the \(void\) type in Java and C)
Value syntax includes
- \(C\) - constant
- \(D\) - declaration
- \((loc, loc)\) - memory segment, the first item is the starting address (inclusive) and the second item is the ending address (exclusive)
- \(unit\) - the only value for the \(unit\) type.
A SIMP program is a sequence of function declarations followed by a sequence of statements.
For example, the following SIMP program is equivalent to Pseudo Assembly program PA1
introduced earlier.
The following SIMP program is equivalent to Pseudo Assembly program PA2
Operational Semantic of extended SIMP¶
For brevity, we consider the big step operational semantics of extended SIMP by extending the rules.
First and foremost, in the extended SIMP, values include function declarations, memory tuples and unit, besides constants. Hence the \(\Delta\) environment maps variables to values. Besides the variable environment \(\Delta\), we define the object (heap) environment \(\rho\) as a mapping from memory location to values.
Big Step Operational Semantics for extended SIMP expression¶
The rules of shape \(\Delta \vdash E \Downarrow C\) have to be adapted to the shape of \(\overline{\Delta} \vdash (\rho, E) \Downarrow (\rho', V)\), where \(\overline{\Delta}\) is the stack of variable environments.
For example, the \((\tt bVar)\) rule is updated as follows
The rest of the rules can be easily adapted to the new scheme. Now we consider cases for the new syntax.
In case of a function application, we first evaluate the function argument into a value. We search for the function definition from the variable environment \(\overline{\Delta}\) (in the order from the top frame to the bottom frame). A new variable environment (frame) \(\Delta\) is created to store the binding between the function's formal argument and the actual argument. We then call the statement evaluation rules (to be discussed shortly) to run the body the of the function. Finally, we retrieve the returned value from the call.
In case of an array instantiation, we first evaluate the size argument into a value (must be an integer constant). We find a sequence of unsused memory locations \(loc(m)\) to \(loc(m+V_2)\) and initialize the value to the default value.
In case of an array reference, we lookup the memory location boundaries of \(X\), then we evaluate \(E_2\) into a constant (integer) \(V_2\). If the index is within the boundary, we lookup the value associated with the address.
Big Step Operational Semantics for extended SIMP statement¶
Similarly, to support the change of SIMP statement, we adapt the big step oeprational semantics rule of shape \((\Delta, S) \Downarrow \Delta\) to shape \((\overline{\Delta}, \rho, S) \Downarrow (\overline{\Delta'}, \rho', S')\)
For example the assignment statement rule is updated as follows
We focus on the new rules and omit the rest of the rules
In case of array deference, we first compute the index argument into an integer constant \(V_1\). We lookup the memory range \((loc(m_1), loc(m_2))\) of \(X\) and ensure that the \(m_1 + V_1\) is within range. Evaluating \(E_2\) yields the value to be assigned to the memory location \(loc(m_1 + V_1)\). Finally we return the updated object memory environment.
In case of free statement, we ensure that the argument is a variable that holding some reference to the object envrionment. We remove the memory assignment from \(\rho\) and remove \(X\) from \(\Delta\). (In some system, \(X\) is not removed from \(\Delta\), which causes the "double-freeing" error.)
Big Step Operational Semantics for extended SIMP Program¶
The above two rules define the execution of a SIMP program and statement sequences. The rule \((\tt bProg)\) records the variable \(f\) to function definition binding in \(\Delta'\) and we use \(\Delta':\overline{\Delta}\) to evaluate the rest of the evaluation. The rule \((\tt bSeq)\) evaluates the first statement until it becomes \(nop\) and moves on the the rest of the statement.
For example, running the SIMP1
program yields the following
where sub derivation[sub tree 1]
is as follows
Cohort Exercise¶
As an exercise, can you work out the derivation of "running" the program SIMP2
using the big step operational semantics?
SIMP to PA conversion (Extended)¶
We consider the update to the maximal munch algorithm.
The \({\tt (m2App)}\) rule converts a function application expression to PA instructions.
The \({\tt (m2ArrRef)}\) rule converts an array reference expression to PA instructions.
The \({\tt (m2ArrInst)}\) rule converts an array instanstiation expression to PA instructions. Note that we assume that we can assess the size of \(T\) in bytes.
The \({\tt (m2ArrDeref)}\) rule converts an array derference assignment statement to PA instructions.
The \({\tt (m2FuncDecl)}\) rule converts a function declaration into PA instructions.
Applying the above algorithm to SIMP1
yields PA1
and applying to SIMP2
produces PA2
.
Extend SIMP Type checking¶
We consider extending the static semantic (type checking) of SIMP to support function and array.
Type Checking SIMP Expression (Extended)¶
Let's consider the type checking rules for the new SIMP expressions. Overall the type rule shape remains unchanged. Recall
In the rule \((\tt tArrRef)\) we type check memory reference expression. We validate \(X\)'s type is an array type and \(E_2\)'s type must be an \(int\) type.
The rule \((\tt tArrInst)\) defines the type checking for array instantiation. The entire expression is of type \([T]\) if the size argument \(E_2\) is of type \(int\).
The rule \((\tt tApp)\) defines the type checking for function application. The entire expression is of type \(T_2\) if \(f\) has type \(T_1 \rightarrow T_2\) and \(E_2\) has type \(T_1\).
The rule \((\tt tUnit)\) defines the type checking for unit value.
Type Checking SIMP Statement (Extended)¶
For the extended SIMP statement type checking, we need to adjust the typing rules of shape \(\Gamma \vdash S\) to \(\Gamma \vdash S : T\).
We adjust the typing rules for the standard statements as follows.
The assignment statement and nop statement are of type \(unit\). The return statement has type \(T\) if the return variable \(X\) has type \(T\). The head and the tail of a sequence of statements are typed indepdently. The two alternatives of an if statement should share the smae time. (In fact, we can be more specific to state that the type of if and while are \(unit\), though it is unnecessarily here.)
We turn into the typing for new syntax.
The free statement has type \(unit\) provided the variable \(X\) is having type \([T]\).
The array dereference statement has type \(unit\) if the index expression \(E_1\) has type \(int\), \(X\) has type \([T]\) and the right hand side \(E_2\) has type \(T\).
Type Checking SIMP Declaration (Extended)¶
In rule \((\tt tFuncDecl)\), we type check the function declaration by extending the type environment with the type of \(f\) and the formal argument \(X\) and type check the body. In rule \((\tt tProg)\), we type check the function declarations independently from the main program statement \(\overline{S}\).
Example¶
We find the type checking derivation of the program SIMP1
Let Γ1= {(y,int)}
and Γ={(plus1,int->int),(z,int)}
where [sub tree 2] is
[sub tree 3] is
Cohort Exercise¶
As an exercise, apply the type checking rule to SIMP2
.
Cohort Exercise¶
As an exercise, develop a type inference algorithm for the extended SIMP.
Run-time errors caused by illegal memory operations¶
There are several issues arising with the memory management.
Double-freeing¶
As motivated earlier, in some system, the operational semantics of the free x
statement does not remove the variable x
in the stack frame, which causes a double free error. For example
Missing free¶
On the other hand, missing free statement after array initialization might cause memory leak.
Array out of bound¶
Another common error is array out of bound access.
Note that all these three examples are well-typed in the current type checking system. The array out of bound error can be detected via dependent type system (recall GADT example in some earlier class). The other two kinds of errors can be flagged out using Linear Type system.
Linear Type System¶
Linear Type was inspired by the linear logic proposed by Jean-Yves Girard.
Linear Type System is a popular static semantic design choice to ensure memory safety. It has strong influence in languages such as Cyclone and Rust.
The basic principal is that
- each variable has only one entry in a type environment \(\Gamma\) (same as the normal type system)
- after a variable's type assignment from some type environment \(\Gamma\) is used in some proof derivation, it will be removed from \(\Gamma\).
Type Checking Expression using Linear Type System¶
The linear typing rules for SIMP expression are in the form of \(\Gamma \vdash E : T, \Gamma'\) which reads as "we type check \(E\) to have type \(T\) under \(\Gamma\), after that \(\Gamma\) becomes \(\Gamma'\).
The above rule type checks the variable \(X\) to have type \(T\), this is valid if we can find \((X,T)\) in the type environment \(\Gamma\), and we remove that entry from \(\Gamma\) to produce \(\Gamma'\).
The array instantion expression is treated as before except that the post-checking type environment is taken into consideration.
When type checking the function application expression, we linearly type-check the argument \(E_2\) to have type \(T_1\) and we check the function \(f\) is having the function type \(T_1 \rightarrow T_2)\) in the update type environment. (Note that unlike other system, we do not remove the type assignment for functions after "use", so that a function can be reused.)
Similarly, when type checking the array reference expression, we linearly type check the index expression against type \(int\), and we check the array variable \(X\) is in the type environment \(\Gamma'\) without removing it.
We omit the linearly typing rules for the rest of expressions as they contain no surprise.
Cohort Exercise¶
Work out the linear typing rules for \(C\), \(unit\), \((E)\) and \(E\ op\ E\).
Type Checking Statement using Linear Type System¶
The linear typing rules for SIMP statements are in the form of \(\Gamma \vdash S : T, \Gamma'\) which reads as "we type check \(S\) to have type \(T\) under \(\Gamma\), after that \(\Gamma\) becomes \(\Gamma'\).
In rule \((\tt ltAssign)\) we type check the assignment statement, we first type check the RHS expression, and we "transfer" the ownership of type \(T\) to \(X\) in the resulting environment. For example, in an assignment statement, \(X = Y\), \(Y\)'s type is transferred to \(X\), and \(Y\)'s type assignment is no longer accessible after this statement. Hence the following programming is not typeable in the linear type system.
Typing \(Nop\) does not change the type environment.
In case of array dereference statement, we type check the RHS expression. With the updated type environemnt \(Gamma_1\) we type check the index expression \(E_1\) having type \(int\). Finally we make sure that \(X\) is an array type in \(\Gamma_2\) without removing it.
When type checking the free statement, \(X\)'s type must be an array type. (Note that \((X,[T]))\) will be removed.)
Return statement carries the type of the variable being returned. After that, the type assignment of \(X\) will be removed.
In the rule \((\tt ltIf)\), we first type check the condition expression \(E_1\) against \(bool\). We then type check the then and else statements under the same type \(T\) and resulting type environment \(\Gamma_2\).
The typing rule \((\tt ltWhile)\) is similar to \((\tt ltIf)\), except that the type environments "before" and "after" the while body should be unchanged to ensure linearity.
In the rule \((\tt ltSeq)\), we type check a sequence of statements by propogating the updated type environments from top to bottom (left to right).
Type Checking SIMP Declaration using Linear Type System¶
When type checking a function declaration, we extend the type environemnt with the function's type assignment and its argument type assignment, then type check the body. The additional requirement is that the resulting environment must be exactly the same as \(\Gamma\oplus(f:T_1 \rightarrow T_2)\) to maintain linearity.
In \((\tt ltPRog)\), we type check the function declaration idependently with an empty type environment then type check the main statement sequence left to right.
Rejecting SIMP3
via linear type system¶
Let's apply the linear type checking rules type check the function f
from our earlier example SIMP3
.
[sub tree 5] is as follows
[sub tree 6] is as follows
Since the type checking fails, SIMP3
will be rejected by the linear type system.
Rejecting SIMP4
via linear type system¶
Let's try to type check SIMP4
.
where [sub tree 7] is as follows
The above program fails to type check as the result type environment of the \((\tt ltFuncDelc)\) rule is not matching with the input type environment.
Make linear type system practical¶
The linear type checking is a proof-of-concept that we could use it to detect run-time errors related to memory management.
The current system is still naive.
- We probably need to apply the linearity restriction to heap object such as array but not to primitive values such as
int
andbool
. For example, the following program will not type check
- We need a type inference algorithm, which should reject
SIMP3
. But forSIMP4
, its type constraints should identify the missingfree
statement and let the compiler insert the statement on behalf of the programmers.
We leave these as future work.