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
visitDef
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
- use
- type error otherwise
Project
(2 weeks)
Implement your type-checker
- References to keep on hand
- Keep open the following while implementing the type checker
- The grammar (Grammar.g4)
- The type specification
- The ANTLR visitor implementation tutorial
- The Type and Scope APIs (Type.java, Scope.java)
You can generate html API documentation by running the following:
javadoc -d doc Type.java Scope.java
Then opening doc/overview-tree.html in your browser.
- The API usage guides
- The type checker pseudo-code
- Keep open the following while implementing the type checker
- 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
- Run
- Double-check that all test cases have the expected output (both type-correct and type-incorrect programs)