UP | HOME

Type Checking IV
Lecture 10

Table of Contents

Review

Questions about the last class?

Quiz

Prove the following expression is correctly-typed

{(x, int)} |- <x + 2 * 3> : int

according to the SimpleC rules:

// literals: the symbols mean their equivalent mathmetical values for numbers and boolean true/false.

--------------
T : <n> : int

--------------
T : <b> : bool

--------------
T : <s> : string


// operators: the symbols for arithmetic, boolean, and relationship operators each have a function type

S |- <e1> : int     S |- <e2> : int     op \in { "+", "-", "*", "/" }
---------------------------------------------------------------------
S |- <e1 op e2> : int

S |- <e1> : bool     S |- <e2> : bool     op \in { "&&", "||" }
----------------------------------------------------------------
S |- <e1 op e2> : bool

S |- <e1> : bool
--------------------
S |- <!e1> : bool

S |- <e1> : int
--------------------
S |- <-e1> : int


// variables: variables assignments evaluate their right-hand side at define-time and are stored and looked up in a storage context.

S' = [(x, t)] + S
-----------------  [declaration]
S |- <x : t> : S'

t = lookup(x, S)
---------------    [substitution]
S |- <x> : t

S |- <e> : t1     t2 = lookup(x, S)    t1 = t2
---------------------------------------------  [assignment]
S |- <x = e;> : S

Quiz Discussion

The implementation

Relevant skeleton files

  • src/simplec/TypeChecker.java
  • src/simplec/Type.java
  • src/simplec/Scope.java

src/simplec/TypeChecker.java

  • You implement this for your project
  • Visitors for each grammar construct

src/simplec/Type.java

  • Implemented for you
  • Internal representation of types
  • Singleton classes for int, bool, and string
  • Construct for function types
  • Type equality checker

src/simplec/Scope.java

  • Implemented for you
  • The symbol table implementation
  • Support for creating symbol tables in nested scopes

Building and running your type checker

# from root of your repository
source configure.sh
cd src/simplec
make
java Compiler ../../tests/example.simplec

If "make" fails, be sure you have ANTLR in your CLASSPATH and PATH (which source configure.sh will do for you), check that you have no compilation errors in your *.java files, and be sure you are in the =src/simplec` directory.

API guides

Type.java

References primitive types

  • E.g, Type.INT

    @Override
    public Type visitNum(GrammarParser.NumContext ctx) {
      info(ctx, String.format("number constants always have type int"));
      return Type.INT;
    }
    

Reference the singleton class for int, bool, or string within the Type namespace, e.g., Type.INT.

In this example, whenever we visit a number in our parse tree, it will always have type int according to the rules of SimpleC.

Use "info" to write out messages useful for debugging. These are output to standard error, so they will not interfere with the output of the compiler and will be ignored for grading.

Handles construction of function types

  • E.g., Type.FunctionType funType = new Type.FunctionType(paramTypes, returnType);

    @Override
    public Type visitDef(GrammarParser.DefContext ctx) {
      String name = ctx.ID().getText();
      if (scope.hasScope(name)) {
        Type.typeError(ctx, String.format("%s already defined", name));
      }
      Type returnType = visit(ctx.type());
      info(ctx, "create the function's scope and add its parameters");
      Scope localScope = new Scope(scope);
      List<Type> paramTypes = new LinkedList<Type>();
      if (null != ctx.formalParams()) {
        for (GrammarParser.FormalParamContext formalParam : ctx.formalParams().formalParam()) {
          String paramName = formalParam.ID().getText();
          Type paramType = visit(formalParam.type());
          if (localScope.hasSymbol(paramName)) {
            Type.typeError(ctx, "function parameters must have unique names");
          }
          info(ctx, String.format("add parameter to the function-local scope %s : %s", paramName, paramType));
          localScope.addSymbol(paramName, paramType);
          paramTypes.add(paramType);
        }
      }
      localScope.addSymbol(returnTypeSymbol, returnType);
      info(ctx, String.format("check for duplicate definitions of %s", name));
      Type.FunctionType funType = new Type.FunctionType(paramTypes, returnType);
      if (scope.hasSymbol(name)) {
        Type declType = scope.getSymbol(name);
        if (declType.equals(funType)) {
          info(ctx, String.format("the function is declared as the same type in the current scope %s : %s", name, funType));
        } else {
          Type.typeError(ctx, String.format("%s's declaration doesn't match definition %s != %s", name, funType, declType));
        }
      } else {
        info(ctx, String.format("add the function to the current scope %s : %s", name, funType));
        scope.addSymbol(name, funType);
      }
      scope.addScope(name, localScope);
      info(ctx, "enter the function's local scope");
      scope = localScope;
      for (GrammarParser.DeclContext dctx : ctx.decl()) visit(dctx);
      for (GrammarParser.StmtContext sctx : ctx.stmt()) visit(sctx);
      info(ctx, "return to the parent scope");
      scope = scope.getParent();
      return Type.VOID;
    }
    

This code gathers the return type, collects the parameters, creates and populates the local scope, then finally creates a function type out of the parameter types (paramTypes) and return type (returnType).

Checking type equality

  • E.g., Type.BOOL.equals(condition)

    @Override
    public Type visitWhile(GrammarParser.WhileContext ctx) {
      Type condition = visit(ctx.expr());
      info(ctx, String.format("check that the while condition is a bool %s", condition));
      if (! Type.BOOL.equals(condition)) {
        Type.typeError(ctx, "while condition should be bool");
      }
      visit(ctx.stmt());
      return Type.VOID;
    }
    

    This code, from visitWhile, gets the type of the condition, then compares it against the singleton BOOL type.

Scope.java

Creating scopes and adding symbols

  • this.scope = new Scope();
    • This sets the current scope.
  • this.scope.addSymbol("true", Type.BOOL);
@Override
public Type visitProgram(GrammarParser.ProgramContext ctx) {
  // prepare the global scope
  this.scope = new Scope();
  // add the true and false keywords
  this.scope.addSymbol("true", Type.BOOL);
  this.scope.addSymbol("false", Type.BOOL);
  this.scope.addSymbol("printInt", new Type.FunctionType(new LinkedList<Type>() {{ add(Type.INT); }}, Type.INT));
  this.scope.addSymbol("printBool", new Type.FunctionType(new LinkedList<Type>() {{ add(Type.BOOL); }}, Type.INT));
  this.scope.addSymbol("printString", new Type.FunctionType(new LinkedList<Type>() {{ add(Type.STRING); }}, Type.INT));
  this.scope.addSymbol("readInt", new Type.FunctionType(new LinkedList<Type>(), Type.INT));
  this.scope.addSymbol("readBool", new Type.FunctionType(new LinkedList<Type>(), Type.BOOL));
  this.scope.addSymbol("readString", new Type.FunctionType(new LinkedList<Type>(), Type.STRING));
  // set the current function to null, since we are in the global scope
  for (GrammarParser.ToplevelContext tctx : ctx.toplevel()) {
    visit(tctx);
  }
  return Type.VOID;
}

This code creates the global scope and adds the builtin symbols for SimpleC, then visits all the top-level declarations and definitions.

Note that statements and declarations themselves return no type, so Type.VOID is returned.

Creating a nested scope

  • scope.addScope(name, localScope);
  • scope = localScope;
    • This sets the current scope for other visitor functions.
  • scope = scope.getParent();

    @Override
    public Type visitDef(GrammarParser.DefContext ctx) {
      String name = ctx.ID().getText();
      if (scope.hasScope(name)) {
        Type.typeError(ctx, String.format("%s already defined", name));
      }
      Type returnType = visit(ctx.type());
      info(ctx, "create the function's scope and add its parameters");
      Scope localScope = new Scope(scope);
      List<Type> paramTypes = new LinkedList<Type>();
      if (null != ctx.formalParams()) {
        for (GrammarParser.FormalParamContext formalParam : ctx.formalParams().formalParam()) {
          String paramName = formalParam.ID().getText();
          Type paramType = visit(formalParam.type());
          if (localScope.hasSymbol(paramName)) {
            Type.typeError(ctx, "function parameters must have unique names");
          }
          info(ctx, String.format("add parameter to the function-local scope %s : %s", paramName, paramType));
          localScope.addSymbol(paramName, paramType);
          paramTypes.add(paramType);
        }
      }
      localScope.addSymbol(returnTypeSymbol, returnType);
      info(ctx, String.format("check for duplicate definitions of %s", name));
      Type.FunctionType funType = new Type.FunctionType(paramTypes, returnType);
      if (scope.hasSymbol(name)) {
        Type declType = scope.getSymbol(name);
        if (declType.equals(funType)) {
          info(ctx, String.format("the function is declared as the same type in the current scope %s : %s", name, funType));
        } else {
          Type.typeError(ctx, String.format("%s's declaration doesn't match definition %s != %s", name, funType, declType));
        }
      } else {
        info(ctx, String.format("add the function to the current scope %s : %s", name, funType));
        scope.addSymbol(name, funType);
      }
      scope.addScope(name, localScope);
      info(ctx, "enter the function's local scope");
      scope = localScope;
      for (GrammarParser.DeclContext dctx : ctx.decl()) visit(dctx);
      for (GrammarParser.StmtContext sctx : ctx.stmt()) visit(sctx);
      info(ctx, "return to the parent scope");
      scope = scope.getParent();
      return Type.VOID;
    }
    

This code creates a local scope and updates the current scope. Once the body of the function has been visited, it restores the scope to the parent scope.

Checking symbol table and type errors

  • scope.hasSymbol(name)
  • Type.typeError(ctx, String.format("%s already declared", name));

    • The string message you give is irrelevant to grading
    @Override
    public Type visitDecl(GrammarParser.DeclContext ctx) {
      String name = ctx.ID().getText();
      Type type = visit(ctx.type());
      if (! scope.hasSymbol(name)) {
        info(ctx, String.format("add the declaration to the current scope %s : %s", name, type));
        scope.addSymbol(name, type);
      } else {
        Type.typeError(ctx, String.format("%s already declared", name));
      }
      return Type.VOID;
    }
    

Type checker pseudocode

visitProgram

  • initialize global scope
  • add built-ins to symbol table
  • visit the top level nodes

visitDecl

  • visit the type
  • add the name to the symbol table unless it's already declared

visitAssignment

  • compare the type of the symbol to the discovered type of the right-hand-side

visitWhile

visitIfThenElse

  • Check that the condition is a boolean
  • Check the bodies of the if and else

visitIfThen

  • Check that the condition is a boolean
  • Check the body of the if

visitReturn

  • lookup the type of the return (returnTypeSymbol)
  • check it against the expression's type

visitExprStmt

  • check the expression

visitEmpty

  • do nothing

visitCompound

  • check the contents of the compound statement (iterate over the statements)

visitIntType

  • return the int type

visitBoolType

  • return the bool type

visitStringType

  • return the string type

visitFunType

  • construct a function from the list of parameter types and the return type

visitCall

  • make sure the number of parameters matches the actual parameters
  • get the types of the expressions
    • iterate over the expressions and visit them to get the type
    • for (GrammarParser.ExprContext expr : ctx.actualParams().expr())
  • use the function's return type as the resulting type
  • type error otherwise

visitNegate

  • check that the expression is an int
  • return an int
  • type error otherwise

visitNot

  • check that the expression is an bool
  • return a bool
  • type error otherwise

visitMultDiv

  • check that the expressions are int
  • return an int
  • type error otherwise

visitAddSub

  • check that the expressions are int
  • return an int
  • type error otherwise

visitRelational

  • check that the expressions are int
  • return a bool
  • type error otherwise

visitEqNeq

  • check that the expressions are int
  • return a bool
  • type error otherwise

visitAndOr

  • check that the expressions are bool
  • return a bool
  • type error otherwise

visitVar

  • passthrough the expression's type

visitNum

  • return an int type

visitStringLiteral

  • return a string type

visitParens

  • passthrough the expression's type

visitSimple

  • lookup the symbol, if it exists
    • use scope.getTypeAnyScope to search all parent scopes as well
  • type error otherwise

Project

(2 weeks)

Implement your type-checker

  • References to keep on hand
  • Write test programs as you go
    • The type checker reports nothing when the input program is correctly-typed
    • But an unfinished type checker will also report nothing even when the program is incorrectly-typed
    • Need to create incorrectly-typed programs to trigger type errors
    • Should be writing and adding tests cases to the tests/ folder
      • Will check for these when grading
  • Start from leaves of the grammar and simpler constructs in the grammar
    • Build up to complex ones, once the child nodes are well-tested and working
    • Assume the child nodes' are correct, and implement parent visitor on its own
  • Submission instructions
    • Submit your completed TypeChecker.java with your git repo
    • Submit your tests cases in your git repo
    • Double-check that the compiler is buildable and runnable
      • Run make clean and/or reclone your repo in another directory to build from scratch
    • Double-check that all test cases have the expected output (both type-correct and type-incorrect programs)

Author: Paul Gazzillo

Created: 2023-04-13 Thu 14:59