Consider this program:
class Point is
x: int
y: int
getX(): int is
begin
getX := x ~ return value
end getX
setX(int newX) is
begin
x := newX
end setX
end Point
class Main is
loc: Point
print() is
x: int
begin
x := loc.getX()
out.writeint(x)
end print
start() is
begin
loc := new Point
loc.setX(3)
print()
end start
end Main
Consider the issues involved in programs containing multiple Dream classes:
When processing a class declaration, you must
If you have not already done so, it’s time to add a ClassDecl to the Declaration class hierarchy:
class Declaration { Type t }
class VarDecl extends Declaration { ... }
class MethodDecl extends Declaration {
List<Symbol> parmList;
}
class ClassDecl extends Declaration {
ClassDecl inheritsFrom; // information about this object's parent class
List<Symbol> varList, methodList;
}
Notice that Type t
is defined in the parent class, Declaration. That is because both variables and methods have a data type (for methods, it is the return type, which may be void), and classes may also be viewed as having a type (themselves).
At the time a symbol is placed into the symbol table, the compiler may not yet have all the information necessary to construct a complete declaration object for it. For example, when a class name is placed in the symbol table, the compiler has not yet processed all of the class information. Thus, the declaration info must be added later, when the end of the class is reached.
When performing semantic processing on a new class definition, you should create a new Type instance corresponding to the new class.
The Type instance will be needed when processing variable declarations like
pt: Point
It will be helpful if you make the following modifications to the Type class:
private static HashMap types<String, Type>
ClassDecl classDecl;
Also, create the following methods:
public static Type createType(ClassDeclaration decl)
This method should create a new Type instance that stores a reference to decl in classDecl. Store the Type instance in the types map using the class name as the key.public static Type getTypeForName(String className)
Return the Type instance from the types map, or null if no class with the given name has been defined.See the Dream Semantics document.
Object instances are stored in the heap. Objects require 8 bytes of memory (reserved for use in the A version) + 4 bytes for each instance variable. Here’s a diagram showing the layout for an instance of Point
Offset | |
---|---|
0 | Reserved |
4 | Reserved |
8 | x |
12 | y |
Consider the following expression:
new Point
the compiler must generate code to do the following:
Use the standard C library calloc( ) function to allocate memory and zero it.
Dream object variables store references to objects. Here’s what the code would look like for the expression new Point:
; new Point
pushl $16
pushl $1 ; space for reserved area and 2 ints (x and y)
call calloc
addl $8, %esp ; pop off parameter
pushl %eax ; leave pointer returned from calloc on stack
How C++ does it: See examples/codegen/objects/objdemo.cpp
Every Dream method executes in the context of an object. Every time you generate code to call an Dream method, you will have to pass in a reference to the object, as an implied parameter, along with the explicit method parameters.
Consider this call:
pt.setX(3)
Behind the scenes, your compiler needs to pass in a reference to pt. Here’s what the call would look like in C:
Point_setX(pt, 3)
Thus, the activation record for setX would look like this (if parameters are pushed right to left):
newX: int | |
pt | |
return address | |
bp> | Saved bp |
sp> | space for return value |
Note: Parameter offsets have changed. Instead of the first parameter (newX) being located at 8(%ebp), it’s at 12(%ebp).
How does a method access instance variables in the current object?
Inside each method, assuming your compiler pushes arguments right-to-left, the reference to the current object will always be located in the activation record immediately above the return address. For example, inside an invocation of print():
me | |
return address | |
bp> | Saved bp |
space for return value | |
sp> | x |
Thus, the current object reference me is always at 8(%ebp).
Your compiler must assign each instance variable an offset within the object. For example, in the Point class, x would be assigned an offset of +0 and y an offset of +4.
Inside a method, when an instance variable is accessed, the compiler must dereference the object pointer, and access the value of the instance variable. Here’s how the code for setX would look:
Point_setX:
pushl %ebp
movl %esp, %ebp
; Line x := newX
pushl 12(%ebp) ; evaluate newX
popl %eax ; put newX value into AX
movl 8(%ebp), %ebx ; get reference to me
movl %eax, 8(%ebx) ; store newX value in x (offset 8) in me
movl %ebp, %esp ; clear off local variables
popl %ebp ; restore old BP
ret
When you call another method in the same class, you must still pass an object reference. For example, when start( ) calls print( ), it needs to pass in the current object reference.
print( ) thus becomes Main_print(me)
, where me
denotes the current object reference.
The start( ) method, like all other Dream methods, expects to run in the context of an object. So your compiler must generate startup code to call the start( ) method that does the following
Here’s how it would look for our example:
main:
pushl $12 ; sizeof(class Main)
pushl $1
call calloc ; Allocate space for main class object
addl $8, %esp
pushl %eax ; Push pointer to main class object on stack, making it available to start()
call Main_start
pushl $0
call exit
Your compiler must generate code that detects attempts to call a method on a null object. Here’s a recommended solution:
Define the following function in your Dream runtime support library:
void nullpointertest(int lineno, void* ptr) {
if (ptr == NULL) {
print "Null pointer exception on line " & lineno;
exit(0);
}
}
When called, the function aborts if the pointer it is given is NULL. Otherwise, it silently returns, allowing execution to continue.
The visitCallStmt and visitCallExpr methods in the CodeGen class must now generate code to call the nullpointertest function just before calling a method.
Note: Test only the object “before the dot” in the call, not any objects that may be passed as parameters. It is legal (and sometimes necessary) to pass a null pointer as a parameter.
anoopdemo.Dream
class Point is
x: int
y: int
init(inX: int; inY: int): Point is
begin
x := inX
y := inY
init := me
end Point
getX(): int is
begin
getX := x ~ return value
end getX
setX(int newX) is
begin
x := newX
end setX
end Point
class Main is
loc: Point
foo(a: int; b: int): int is
x: int
begin
x := loc.getX()
foo := a + b + x
out.writeint(x)
end print
start() is
a: int
begin
loc := (new Point).init(2, 5)
loc.setX(3)
a := foo(9, 5)
end start
end Main