Logo
  • Home
  • Classes
  • Conveying Computing
  • Exams
  • Fractal Gallery
  • Guides
  • Problem Sets
  • Syllabus

Problem Set 8 (Part 2): Typed Aazda

Turn-in Checklist: This assignment is due by electronic submission by 7:59pm on Monday, 5 December.

Collaboration Policy - Read Carefully

For this assignment, you may work alone or with a partner of your choice. If you work with a partner, both partners should contribute equally to the assignment and both partners must fully understand everything you submit. Remember to follow the pledge you read and signed at the beginning of the semester. For this assignment, you may consult any outside resources, including books, papers, web sites and people. If you use resources other than the class materials, lectures and course staff, explain what you used in a comment at the top of your submission or next to the relevant question.

If you haven't figured out to start early and take advantage of the scheduled help hours and office hours by now, probably it won't do any good to remind you to do so again on this assignment.

Purpose

The main purpose of this assignment is to understand static typing and how to implement type checking in an interpreter, and to provide additional experience using Java to be well prepared to take cs2110 in the Spring.

Static Type Checking

As introduced in Chapter 5, a type defines a possibly infinite set of values and the operations that can be performed on them. For example, Number is a type. Addition, multiplication, and comparisons can be performed on Numbers.

In Scheme (and in Charme and the current Aazda interpreter before you modify it), types are latent: they are not explicitly denoted in the program text. Nevertheless, they are very important for the correct execution of a Scheme program. If the value passed as an operand is not of the correct type, an error will result. For example, evaluating (+ 1 true) produces a type error in Scheme and Aazda.

Scheme has fairly strong type checking. If the types of operands do not match the expected types, it reports an error. Python has weaker type checking. Evaluating 1 + True in Python does not produce a type error. (Try guessing what the result will be before evaluating it in the Python interpreter to see what the actual result is.) One advantage of stronger type checking is it is often better to get errors than to get mysterious result. (See Martin Rinard's papers on the acceptability envelope for an alternate view.)

Both Scheme and Python have dynamic type checking. This means types are checked only when an expression is evaluated. For example, evaluating
   (define (badtype a b) (+ (> a b) b))
does not produce a type error even though their are no possible inputs for which badtype evaluates to a value: any application of badtype must produce a run-time type error. Expressions that are not well-typed produce errors when those expressions are evaluated, but not earlier.

Your goal for the rest of this assignment is to add static type checking to Aazda. Static type checking checks that expressions are well-typed when they are defined, rather than waiting until they are evaluated. Static type checking reduces the expressiveness of our programming language; some expressions that would have values with dynamic type checking are no longer valid expressions. If a correct static type checker deduces the program is well-typed, there is no input on which it could produce a run-time type error. In many cases, it is much better to detect an error early, than to detect it later when a program is running. This is especially true in safety-critical software (such as flight avionics software) where a program failure can have disastrous results (such as a plane crashing).

As with Java (but unlike Scheme and Python), Aazda will use manifest types: every definition and parameter will include explicit type information that describes the expected type of the variable or parameter. This increases the length of the program text and sacrifices expressiveness, but makes it easier to understand and reason about programs. To provide manifest types, we change the language grammar to support explicit type declarations.

Download: Download taazda.zip to your machine. You do not need to extract the zip file (but instead will import it into Eclipse, following the directions below). This is based on the Aazda interpreter you used for Part 1, but several files are modified to provide partial support for static types. It also includes definitions of the evaluation methods for assignment and begin that you implemented in Part 1. The zip file contains a directory containing several Java files that implement the statically-typed Aazda interpreter, taazda.
To start withing with taazda, select File | Import in Eclipse. In the import dialog, select General | Existing Projects into Workspace. Then, Select Archive File and use Browse to find the taazda.zip file. Then, select Finish to import the project. After this, you should see the project taazda in your workspace.
For the questions in this assignment, you will modify several files in the workspace. Please use comments to mark your changes. You should surround each change you make with
/* Modified for Question [N] */
your changed code
/* End Modifications for Question [N]*/

Manifest Types

To provide manifest types, we modify the grammar for Aazda to include type declarations. Two rules need to change: definitions and lambda expressions. We replace the original definition rule,

Definition → (define Name Expression)
with a rule that includes a type specification after the name (we will define the Type nonterminal for denoting a type later):
Definition → (define Name : Type Expression)

We modify the lambda expression grammar rule similarly to include a type specification for each parameter:

Expression → ProcedureExpression
ProcedureExpression → (lambda (Parameters)Expression)
Parameters → ε
Parameters → Name : Type Parameters

The new nonterminal, Type, is used to describe a type specification. A type specification can specify a primitive type. Like a primitive value, a primitive expression is pre-defined by the language and its meaning cannot be broken into smaller parts. Aazda supports only two primitive types: Number (for representing numbers), and Boolean (for representing the Boolean values true and false). (Adding support for Pair types is not required for this assignment, so the cons, car, cdr, and list primitives are not part of static Aazda.)

We also need constructs for specifying procedure types. The type of a procedure is specified by the type of its inputs (that is, a list of the input types), and the type of its results (an Aazda procedure can only return one value, so the result type is a singleton). The arrow symbol, -> symbol is used to denote a procedure type, with the operand type list on the left side of the arrow and the result type on the right side of the arrow. To avoid ambiguity when specifying procedures that have procedure result types, we use parentheses around the procedure type specification.

The grammar for type specifications is:

Type → PrimitiveType | ProcedureType
PrimitiveType → Number | Boolean
ProcedureType → ( ProductType -> Type)
ProductType → (TypeList)
TypeList → Type TypeList | ε

For example,

(define x : Number 3)
defines x as a Number and initializes its value to 3.

The definition,

(define square : ((Number) -> Number) 
  (lambda ((x : Number)) (* x x)))
  
defines square as a procedure that takes one Number input and produces a Number as its output.

Here is a definition of a compose procedure that takes two procedures as inputs and produces a procedure as its output:

(define compose : ((((Number) -> Number) ((Number) -> Number))
                   -> ((Number) -> Number)) 
  (lambda ((f : ((Number) -> Number))
           (g : ((Number) -> Number)))
    (lambda ((x : Number)) (g (f x)))))

The compose example reveals two important disadvantages of static, manifest types. The first is that manifest types add a lot to the size of a program. You will notice this in your Java programs also. The second is that static types reduce expressiveness of Aazda compared with Charme. This procedure only works on a small subset of the inputs for which the typeless compose procedure works, namely procedures that take Number inputs and produce a Number output. If we want to compose procedures that operate on Boolean inputs, we would need to define a separate procedure with a different type specification.

To support type definitions without spaces, we also modify the Tokenizer to treat : as a separate token (like ( and ) in the original Tokenizer.

Exercise 1. Try running the taazda interpreter as provided (select the REPL main method. It supports a subset of the Aazda language, with support for explicit types. As provided, the interpreter does not support the begin expression, set! expression, cond expression, or recursive definitions. But, you should be able to define the square and compose procedures as above, as well as your own simple procedure. Try defining an identity procedure that works on Number types. Try evaluating some applications, as well as some expressions that are not well types to see the static type errors.

Representing Types

The first step in adding static type checking to our interpreter is to define classes for representing types. We will use inheritance to define the APrimitiveType and AProcedureType classes as subtypes of the AType class (we prefix our names with A for Aazda, to avoid confusion with the other type names).

There are three classes (included in taazda.zip): AType.java, APrimitiveType.java, and AProcedureType.java. AType is the supertype class; all of the types we represent are AType objects. The APrimitiveType and AProcedureType are subclasses that inherit from AType. The APrimitiveType class provides a specialized class for representing primtive types (Boolean and Number); the AProcedureType is for representing procedure types which have a return type and parameter types.

The AType class is an abstract class. This means that it provides no constructors. Hence, there is no way to construct an AType directly, only to create its subtypes. The AType class defines one abstract method:

    public abstract boolean match(AType p_t);
An abstract method is not implemented, but it specifies a method that must be implemented by subtypes of AType. The match method takes an AType as its input, and outputs a boolean that indicates if this type matches the input type.

The APrimitiveType class is a subtype of AType for representing the primitive types. In Java, this is done using extends in the class declaration. We use a String to represent each type:
package aazda;

public class APrimitiveType extends AType {
	private String tname;
	
	public APrimitiveType(String p_name) {
		tname = p_name;
	}
Since APrimitiveType is a concrete type, it must implement the abstract match(AType t) method from the superclass:
	public boolean match(AType t) {
		if (t instanceof APrimitiveType) {
			APrimitiveType pt = (APrimitiveType) t;
			return tname.equals(pt.tname);
		} else {
			return false;
		}
	}
This method needs to work for any AType input, not just for other APrimitiveTypes. So, the first thing it does is check if the input is a primitive type (that is, is its type APrimitiveType) by using t instanceof APrimitiveType). If the predicate is false, the input is some other kind of type, so it cannot match and it returns false. If it is a primitive type, we check if the type names are the same using equals.

The APrimitiveType class also implements a toString() method that overrides the toString() method provided by the java.lang.Object class (which every Java class inherits from). This will produce a human-readable String that represents the type.

Representing procedure types is more complicated since they have both a return type (an AType) and parameters (an ArrayList<AType>). The AProcedureType class does this using the result and params private instance variables. Look at the provided code in AProcedureType.java to understand how procedure types are represented, and how the match method works for procedure types.

The AType class provide a method AType parseType(SExpr) that converts a type expressed as an s-expression (that is, the result of Parser.parse(String)) into an AType. You should be able to read and understand the code for parseType(SExpr) in AType.java, but will not need to modify this code.

The AType class includes a main method for testing the type implementation. The provided code includes some simple tests. For the next exercise, you should modify this code.

Exercise 2. Add code to the main method in AType to create the type of the compose procedure (shown above) without using parseType. If your type is correct, the result of composetype.match(yourtype) should be true.

Modifying the Environment

With static typing, each place has an associated type. In Java, a variable declaration introduces a new name and gives that name a type. For example,

    int a; 
declares a place named a that can only hold values of type int.

To support typed places, we need to modify the Environment type to associate types with places. We do this by defining a Place class that packages a type (AType) and a value (SVal):

class Place {
	// Place has a type and value
	AType type;
	SVal val;
	
	public Place(AType p_type, SVal p_val) {
		type = p_type;
		val = p_val;
	}
}
Then, we change the frame to be a HashMap<String, Place> so there is both a type and value associated with a name in the frame. The lookup methods are changed accordingly to support both looking up the type and value associcated with a name.

Type Checking

The main change to the evaluator is adding a method,

   public static AType typeCheck(SExpr expr, Environment env)
that performs static type checking and returns the type of expr in the environment env. The meval method is modified to call typeCheck before evaluating an expression:
	public static SVal meval(SExpr expr, Environment env) throws EvalError, TypeError {
		typeCheck(expr, env);

		if (isPrimitive(expr)) {
			...
If the expression is not well-typed, typeCheck will throw a TypeError exception and exection of meval will terminate.

The typeCheck method (defined in Evaluator.java) is very similar to meval except instead of calling evalRule it calls typeRule:
	public static AType typeCheck(SExpr expr, Environment env) {
		if (isPrimitive(expr)) {
			return typePrimitive(expr);
		} else if (isName(expr)) {
			return typeName(expr, env);
		} else if (isIf(expr)) {
			return typeIf(expr, env);
		} else if (isCond(expr)) {
			return typeCond(expr, env);
		} else if (isAssignment(expr)) {
			typeAssignment(expr, env);
			return AType.Void;
		} else if (isDefinition(expr)) {
			typeDefinition(expr, env);
			return AType.Void;
		} else if (isLambda(expr)) {
			return typeLambda(expr, env);
		} else if (isBegin(expr)) {
			return typeBegin(expr, env);
		} else if (isApplication(expr)) {
			return typeApplication(expr, env);
		} else {
			throw new TypeError("Unknown expression type: " + expr.toString());
		}
	}
Each of the typeRule methods takes an expression and environment as its inputs, and returns the type of that expression (or throws an exception if it is not well-typed). For the expression that have no type, we use the AType.Void to represent a non-value.

For example, below is the definition of typeIf (with some error checking removed, see the full code in Evaluator.java). It first checks that the type of the predicate expression is a Boolean. This uses typeCheck recursively, similarly to the way evalIf uses meval to evaluate the value of the predicate expression. (Indeed, the typeRule methods were created by starting from a copy of the code for the corresponding evalRule.) The key difference, though, is that the type checker does not actually evaluate any expression, it just determines its type.

	public static AType typeIf(SExpr expr, Environment env) throws EvalError {
		...
		SExpr pred = ifExpr.get(1); // error checking code removed
		SExpr consequent = ifExpr.get(2);
		SExpr alternate = ifExpr.get(3);
		
		// Check the predicate is a Boolean
		AType predType = typeCheck(pred, env);
		if (!predType.isBoolean()) {
			throw new TypeError(
					"Predicate for an in expression must be a Boolean.  Type is: "
							+ predType);
		}

		// Check both clauses have the same type
		AType consequentType = typeCheck(consequent, env);
		AType alternateType = typeCheck(alternate, env);

		if (consequentType.match(alternateType)) {
			return consequentType;
		} else {
			throw new TypeError(
					"Clauses for if must have matching types.  Types are: "
							+ consequentType.toString() + " / "
							+ alternateType.toString());
		}
	}

We have provided a testing class, TestAazda.java, for testing taazda. You can run this by selecting the TestAazda main method for running.

As provided, not all of the tests pass. Your assignment is to finish the taazda implementation; after this, all of the tests in TestAazda.java should pass.

Problem 1. Define the typeAssignment method for type checking an assignment. It should produce a type error if the assignment value does not match the type of the place. (After this, all tests in the TestAazda.testAssignment() method should pass.)
Problem 2. Define the typeBegin method for type checking a begin expression. (After this, all tests in the TestAazda.testBegin() method should pass.)
Problem 3. Define the typeCond method for type checking a cond expression. It should produce a type error if any predicate does not have a Boolean type, or if the type of any of the consequent clauses is different. If there are not clauses, the cond expression has type AType.Void. Otherwise, the type of the cond expression is the type of the first consequent clause (which must be the same as the type of all the other consequence clauses). (After this, all tests in the TestAazda.testCond() method should pass.)

As provided, the Taazda interpreter cannot handle recursive definitions! For example, when we try to define factorial in Taazda (this is TestAazda.testFactorial()) we get an Undefined name factorial error.

This is a serious problem, since without recursive definition we do not have a universal programming language.

Problem 4. Fix the Taazda interpreter to handle recursive definitions correctly. For this question, you need to figure out yourself which parts of the interpreter to modify. The actual changes needed are fairly minor, so don't start changing a lot of code without a clear plan of what you want to do. (After this, all tests in the TestAazda should pass, including the testFactorial() tests.)

Note that meval is not an algorithm. It (hopefully) correctly always produces the value of any Taazda expression, but it is not guaranteed to terminate. Indeed, it is impossible to produce an algorithm for this, since that would require solving the Halting Problem. Since Taazda is a universal programming language, we can implement any mechanical computation in Taazda, including simulating a universal Turing Machine. Hence, we know the Taazda-Value problem described below is noncomputable:

Taazda-Value
Input: A string, expr, that is an expression in the Taazda language.
Output: The value of expr in the standard global environment, or Void if expr has no value.
We could prove this by showing how to define halts, an algorithm that solves the Halting Problem, using a taazdaValue procedure that solves the Taazda-Value problem. This is slightly complicated by the Taazda constructs that have no value including definitions, set!, and (cond). An easy solution is to rewrite each of these to be a value-producing expression, e.g., (begin Non-Value Expression false). We assume a rewriteNoValue(s) procedure that does this.
def halts(s):
   return taazdaValue(rewriteNoValue(s)) != Void
For the last problem, consider the Taazda-Well-Typed problem described below:
Taazda-Well-Typed
Input: A string, expr, that is an expression in the Taazda language.
Output: If expr is well-typed according to the Taazda type checking rules, output true. Otherwise, output false.
Problem 5. (Submit your answer to this as a comment at the end of Evaluator.java.) Is the Taazda-Well-Typed problem computable?

Your answer should either (1) argue that type-checking is computable, supporting that argument by explaining why typeCheck always terminates; or (2) prove that type checking is noncomputable by showing how halts could be implemented using an algorithm that solves the type checking problem.

You've completed enough to receive "Gold star" credit on this assignment (and a "Double Gold star" if your code works and you have a good answer to Problem 5. Below, we describe a potential extension for extra ambitious students, but you are not expected to do these for PS8, but should read them to understand some of the other issues with static types. Students who do complete one of the suggested extensions below well, however, will almost certainly be offered summer jobs in the Security Research Group. (There is no deadline for this; you can do it after submitting PS8 with the rest of your answers.)

Pairs and Lists

To simplify things, we removed cons, car, cdr and list from our Taazda interpreter. These cause extra challenges since we need to add support for parameterized types (or generics as they are called in Java). We don't want to just have a Pair type, since then we don't know the type of (car Pair). We need to have a Pair<E> type, similar to the way Java has ArrayList<E>. Then, the type of cons is, ((E E) -> Pair) where E is a type variable, and the type of car is ((Pair) -> E). Note that type variables would also reclaim some of the expressiveness that was lost by adding static type checking. For example, we could not define an identity procedure that works on any value:
   (define identity : ((E) -> E) (lambda ((x: E)) x))

Supporting Lists also requires a special solution for null (what type should null have?) and if you want to support the list procedure for making arbitrarily long lists, a way to have parameter lists that can be any size.

Bonus Problem. Extend Taazda to support type variables and typed Pairs and Lists. It is up to you to figure out a good way to extend the surfact forms to support type variables, and determine and implement good typing rules for cons, car, cdr, and list.

Turn-in Reminder: Submit a zip file containing all of the files in your final taazda directory using https://church.cs.virginia.edu/cs1120/ps8j.html.
Print Friendly Print Get a PDF version of this webpage PDF

9 Responses to “Problem Set 8 (Part 2): Typed Aazda”

  1. cls2be says:
    30 November 2011 at 11:07 am

    Is there a reason all the test passed in my TestAzzada class? I haven’t edited any code yet and the instructions indicate that not all the test should pass initially.

    Log in to Reply
    • David Evans says:
      30 November 2011 at 3:45 pm

      In the distributed code, the failing tests are commented out. The main method in TestAazda.java is:

      public static void main(String[] args) {
      testIf();
      testCompose();

      // These tests are commented out for now, since they will fail with the provided interpreter.
      // As you answer the problems, uncomment each test.

      // testAssignment(); // will fail until Problem 1
      // testBegin(); // will fail until Problem 2
      // testCond(); // will fail until Problem 3
      // testFactorial(); // will fail until Problem 4
      }

      When you start, it should pass the first two tests. As you make progress in the assignment, you should uncomment the other tests by removing the // in front of testX();.

      Log in to Reply
  2. Anonymous says:
    1 December 2011 at 10:43 pm

    For typeBegin and typeCond, is the return expression currently there (AType.Error) only a placeholder for our input, or should that remain there after we add code?

    Log in to Reply
    • David Evans says:
      1 December 2011 at 11:11 pm

      Yes, that is just there so the code compiles before you implement those methods. You should replace them with your code.

      Log in to Reply
  3. Filip says:
    2 December 2011 at 8:43 pm

    Is the Alonzo Bot ready for us? I’ve tried to submit the ps8 about three times over the course of today, and it always claims the page cannot be found. (the ubiquitous 404 error)

    Log in to Reply
    • David Evans says:
      3 December 2011 at 2:43 am

      Sorry about that. It is up now. Please try again and let me know if you run into any problems.

      Log in to Reply
      • Filip says:
        3 December 2011 at 8:18 pm

        Thank you.

        The bot is there now…
        although it gives me this error

        “Zipfile is not complete. Missing Evaluator.java. Are you sure you zipped all the files in your /taazda/src/taazda directory?”

        I am positive Evaluator.java is there. I tried it several times.

        It takes the zip nonetheless, so I’m good :-).

        Log in to Reply
        • David Evans says:
          3 December 2011 at 9:13 pm

          The problem is your zipfile is the taazda directory, not the files in it. When your file unzips, it produces the directory
          taazda
          which contains all the source files.

          The tester expects the zip file to just be the files, not contained in any directories. You should be able to create it by just selecting all the files in your taazda/src/taazda directory and zipping them.

          Log in to Reply
          • Filip says:
            5 December 2011 at 11:29 am

            Now, that’s embarrassing.

            Done.

Leave a Reply Cancel reply

You must be logged in to post a comment.

cs1120 | RSS | Comments RSS | Book | Using These Materials | Login | Admin | Powered by Wordpress