CpS 450 Language Translation Systems

Multiple Classes

Introduction

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:

  • Symbol table issues are more involved
  • Runtime memory management is much more complicated

Symbol Table

Scope

When processing a class declaration, you must

  • Add the class name as a symbol in the current scope
  • Create a new type for the class (see Class Types below)
  • SymbolTable.BeginScope( )
  • Perform semantic processing on instance variable and method declarations (these symbols go in the new scope)
  • SymbolTable.EndScope( ) to remove the instance variable and method declarations from the symbol table
  • Update the class declaration info in the symbol table

Declaration Info

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.

Class Types

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:

  • Define a private member variable to hold newly created types:
    private static HashMap types<String, Type>
    
  • Define an instance variable to hold information about a type’s class:
    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.

Class Semantics

See the Dream Semantics document.

Code Generation

Object Runtime Storage Layout

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

Instantiating Objects

Consider the following expression:

new Point

the compiler must generate code to do the following:

  • Allocate memory from the heap to hold the instance variables for Point
  • Initialize the values of the instance variables to 0
  • Leave a reference to the memory on the top of the stack

Allocating Memory

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

Method Calls with Objects

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).

The me Expression

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).

Accessing Instance Variables

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

Calling Methods in the Same Class

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.

Standard Startup Code

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

  • instantiate the class in which start( ) is located
  • push a reference to the object on the stack
  • call start

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

Null Pointer Checking

Your compiler must generate code that detects attempts to call a method on a null object. Here’s a recommended solution:

A Null Pointer Test Function in C

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.

Checking for Null

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.

A Comprehensive Example


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