HW 5: Scopes and Type Systems

Learning Objectives

After this assignment, you will be able to:

Overview

This week, we will examine the symbol table management required for the IC language and explore type systems as a foundation for describing and reasoning about type checking. The first reading is an article from the Encyclopedia of Computer Science on type systems written by Luca Cardelli, a very influential researcher. (Cardelli designed and implemented the first ML compiler and helped to develop much of the formal theory of object-oriented programming.) This article covers the basic theory and formalism used to describe type systems.

The last reading, from Kim Bruce’s book, touches on many problems specific to designing and type checking object-oriented languages. It should be mostly background reading and will serve as a OOP refresher as you implement IC.

Symbol Tables

A parser checks only that a program has the right shape. It will happily accept x = y + 1; whether or not x and y were ever declared, and whether or not adding 1 to y makes sense. Answering those questions is the job of semantic analysis, and nearly every question it asks starts with the same one: what does this name refer to? A symbol table is the data structure that answers it. At its core, a symbol table is a map from identifiers to information about the entities they name: whether a name is a class, field, method, parameter, or local variable; its type; and where it was declared. Later phases hang more information on the same entries, such as a variable’s location in memory or a field’s offset within an object.

Programs reuse names, so one flat map is not enough. Each scope in the program — the whole program, a class, a method, a block — gets its own table holding just the names declared there, and each table points to the table for the scope that encloses it. Looking up a name starts in the innermost table and walks outward until it finds a declaration. That walk is exactly how a language’s scoping rules decide which declaration a use refers to, and it handles shadowing for free: in

class A {
  int x;
  void f(int y) {
    int x;
    y = x;
  }
}

the x in y = x finds the local declaration in f’s table before the lookup ever reaches the field in A’s table.

In the front end, symbol table construction sits between parsing and type checking. A pass over the AST visits each declaration, enters the name into the table for its scope, and reports errors such as a name declared twice in the same scope. Name resolution then connects every use of a name in the AST to its declaration, reporting any name that is not declared at all. (In IC, a class can be named before its declaration, and a method can use fields and methods declared later in its class, so building all the tables before resolving any uses is often simpler than doing both in one pass.) Once every name is resolved, the type checker can look up the type of each variable, the signature of each method, and the fields of each class, and check each expression and statement against the typing rules we will study in the Cardelli reading.

Questions 1 and 2 below ask you to work through this machinery, first for a small block-structured language and then for IC itself.

Readings

Exercises

  1. [Adapted from Cooper and Torczon] You are writing a compiler for a lexically-scoped programming language. Consider the following source program:

    1 procedure main
    2    integer a,b,c;
    3    procedure f1(integer w, integer x)
    4      integer a;
    5      call f2(w,x);
    6    end;
    7    procedure f2(integer y, integer z)
    8      integer a;
    9      procedure f3(integer m, integer n)
    10        integer b;
    11        c = a * b * m * n;
    12      end;
    13      call f3(c,z);
    14    end;
    15    …
    16 call f1(a,b);
    17 end;

    As in ML, Pascal, or Scheme, the scope of a nested procedure declaration includes all declarations from the enclosing declarations.

    1. Draw the symbol table and its contents at line 11.

    2. What actions are required for symbol table management when the semantic analyzer enters a new procedure and when it exits a procedure?

    3. The compiler must store information in the IR version of the program that allows it to easily recover the relevant details about each name. In general, what are some of the relevant details for the variable and procedure names that you will need to perform semantic analysis, optimization, and code generation? What issues must you consider when designing the data structures to store that information in the compiler?

    4. This part explores how to extend your symbol table scheme to handle the with statement from Pascal. From the Pascal documentation:

      The with statement serves to access the elements of a record or object or class, without having to specify the name of the each time. The syntax for a with statement is:

      with  variable-reference  do  
      `` statement

      The variable reference must be a variable of a record, object or class type. In the with statement, any variable reference, or method reference is checked to see if it is a field or method of the record or object or class. If so, then that field is accessed, or that method is called. Given the declaration:

           Type Passenger = Record
              Name : String[30];
              Flight : String[10];
           end;
      
           Var TheCustomer : Passenger;

      The following statements are completely equivalent:

           TheCustomer.Name := 'Michael';
           TheCustomer.Flight := 'PS901';

      and

           With TheCustomer do
             begin
               Name := 'Michael';
               Flight := 'PS901';
             end;

      In essence, the with statement is a shorthand to access a bunch of fields from a compound structure without fully qualifying each name. Discuss in a few sentences how you would augment the symbol table scheme you followed in parts 1 and 2 to support with. In particular, what information would you store about each record type definition, and how would you modify the symbol table when the semantic analyzer enters and exits a with statement? What information do you attach to any symbol added to the table during these operations?

  2. This question applies the symbol table scheme from the previous question to design the symtab package for your IC compiler. This package will contain two main classes: a SymbolTable class that encodes all symbol information for a scope, and a symbol table building function that will recursively traverse the AST, constructing the symbol tables and annotating each AST node with the symbol table corresponding to its scope, as described in the PA 2 handout.

    Your design need not be long or contain every detail, but it should address the following items:

    • Define the general interface to the SymbolTable class. Describe what each method does, and sketch the internal representation details. Include a description of what information is stored along-side each symbol in the tables (ie, your SymbolTable is really a map from identifier to what?)

    • Describe the operations performed by the scope-building function upon entry and exit to each form of scope (e.g., global, class, method, block).

    • How will your building function create a new symbol entry when it encounters a declaration?

    • When, how, and where will the scoping rules be enforced?

    • Sketch the contents of your SymbolTable structure after processing the following program.

       1:    class A {
       2:      int x,u;
       3:      void f(int y, int z) {
       4:        int x;
       5:        y = x + this.x;
       6:        g(y-u);
       7:      }
       8:      void g(int y) { }
       9:    }
      10:
      11:    class B extends A {
      12:      int a,b,c;
      13:      void g(int y) {
      14:        if (y > 0) {
      15:          int a = 2;
      16:          f(y, x);
      17:        }
      18:      }
      19:    }
    • For each occurrence of ClassId and Id in the IC grammar, identify whether or not it is a defining occurrence that introduces a new name into the current scope. For those that are not, indicate the scope in which the defining occurrence of that name would occur. (No need to hand this in — it is just for your reference.)

      Use your characterization to indicate how your compiler will resolve the following symbols:

      • the first x on line 5.

      • this on line 5.

      • the second x on line 5.

      • u on line 6.

      • x on line 16.

    • In what ways, if any, will you change the AST package to support construction of the symbol tables?

  3. Using the type system of Cardelli’s paper, prove that

    1. \(\emptyset, x:\mathit{Nat} \vdash \mathit{succ~(succ~x)} : \mathit{Nat}\)

    2. \(\emptyset, x:\mathit{Nat} \vdash \mathit{if~true~then~} x \mathit{~else~} 0 : \mathit{Nat}\)

  4. For each of the following IC constructs, state whether it is well-typed in some well-formed typing context, according to the type system from the IC Specification. If so, give the most general typing context in which the construct is well-typed and write the corresponding proof tree. If the construct is not well-typed in any type context, explain why. (Note that, unlike in the Cardelli system, you do not need to prove that environments and types are well-formed. You should be able to convince yourself that any type or environment that you mention is well-formed, however.)

    1. (new int[x.length])[x[2]]

    2. if (x == v[x] && y == "true") x = y;

    3. ((a == b) == c) && (a == (b + "c"))

    4. f(x)[x.length] = y[2]

    5. if (x == a[b[x]] && y) y = b[c[x]];