11 Java
This assignment has you implement a mini-Java, we’ll call it Javette. There are a few major lessons in this assignment:
The interaction between a type-checker’s guarantees and the error checks an interpreter needs to perform.
The interaction between a type-checker and a simple compiler (Javette has type-directed desugarings).
One instance of a nontrivial class/object model. There are of course many others, but Javette’s layout reflects some of the design decisions you would make when designing an object system.
11.1 Javette
Like Java, Javette programs consist of a collection of class definitions, and a main expression to evaluate. Here’s an example Javette program that defines a Point class, constructs a Point object, sets a field, and does arithmetic with them:
(program (classes (class Point extends Object (fields (x Num) (y Num)) (methods))) (let (Point p (new Point)) (do (set p x 10) (+ (get p x) (get p y)))))
This produces 10.
A few things to note already:
Classes distinguish between fields and methods.
Classes have an explicit and non-optional superclass position (in this case, it’s just Object).
There are no explicit constructors; the new expression initializes Num fields to 0, and object-typed fields to v-null.
We can add a method to the class, and use this to access the fields:
(program (classes (class Point extends Object (fields (x Num) (y Num)) (methods (method Num sum (Num _) (+ (get this x) (get this y)))))) (let (Point p (new Point)) (do (set p x 10) (call p sum 0))))
This also produces 10.
All methods have a single argument with an annotated type, in this case we use _ to simulate a 0-argument function.
11.2 Data Definitions
prog := |
| (program (classes <class> ...) <exp>) |
class := |
| (class <class-name> extends <class-name> |
(fields <field> ...) (methods <method> ...)) |
field := |
| (<field-name> <type>) |
method := |
| (<type> <method-name> (<type> <id>) <exp>) |
|
exp := |
| <num> |
| <id> |
| (+ <exp> <expr>) |
| (do <exp> <expr> ...) |
| (let (<type> <id> <exp>) <expr>) |
| (if <exp> <exp> <exp>) |
|
| (get <exp> <field-name>) |
| (set <exp> <field-name> <expr>) |
| (call <exp> <method-name> <expr>) |
|
| (new <class-name>) |
| this |
| (null <class-name>) |
|
class-name, method-name, field-name, id := |
Any non-keyword symbol |
|
type := |
| Num |
| <class-name> |
The abstract syntax is:
data JProgramSrc: | src-program(classes :: List<JClassSrc>, psvm :: JExprSrc) end data JClassSrc: | src-class(name :: String, superclass :: String, fields :: List<JFieldSrc>, methods :: List<JMethodSrc>) end data JMethodSrc: | src-method(ret-type :: JType, name :: String, arg-type :: JType, arg :: String, body :: JExprSrc) end data JFieldSrc: | src-field(name :: String, field-type :: JType) end data JExprSrc: # Fields & Methods: | src-get-field(obj :: JExprSrc, field :: String) | src-set-field(obj :: JExprSrc, field :: String, val :: JExprSrc) | src-method-call(obj :: JExprSrc, meth :: String, arg :: JExprSrc) # Objects: | src-new(class-name :: String) | src-this | src-null(class-name :: String) # Basic Language Stuff: | src-let(arg-type :: JType, arg :: String, val :: JExprSrc, body :: JExprSrc) | src-num(num :: Number) | src-plus(left :: JExprSrc, right :: JExprSrc) | src-if(cond :: JExprSrc, consq :: JExprSrc, els :: JExprSrc) | src-do(exprs :: List<JExprSrc>) | src-id(var-name :: String) end data JType: | t-num | t-obj(class-name :: String) end
The result of parse is a JProgramSrc.
The evaluation of a JProgramSrc is defined in two stages: first compilation and type-checking, then interpreting the result. Compilation and type-checking produce new ASTs, which have different shape:
data JProgram: | j-program(classes :: List<JClass>, psvm :: JExpr) end data JClass: | j-class(name :: String, superclass :: String, fields :: List<JField>, methods :: List<JMethod>) end data JMethod: | j-method(name :: String, arg :: String, body :: JExpr) end data JField: | j-field(name :: String, field-type :: JType) end data JExpr: # Fields & Methods: | j-get-field(obj :: JExpr, class-name :: String, field :: String) | j-set-field(obj :: JExpr, class-name :: String, field :: String, val :: JExpr) | j-method-call(obj :: JExpr, meth :: String, arg :: JExpr) # Objects: | j-new(class-name :: String) | j-this | j-null # Basic Language Stuff: | j-let(arg :: String, val :: JExpr, body :: JExpr) | j-num(num :: Number) | j-plus(left :: JExpr, right :: JExpr) | j-do(exprs :: List<JExpr>) | j-if(cond :: JExpr, consq :: JExpr, els :: JExpr) | j-id(var-name :: String) end
So compilation takes a JProgramSrc, does some work on it, and produces a JProgram. The interpreter takes a JProgram, and produces a value (as a JVal):
# map from class names -> (map from field name -> value) type FieldSet = D.StringDict<D.MutableStringDict<JVal>> data JVal: | v-num(num :: Number) | v-null # `class-name` is the class used to construct the object # `field-sets` stores all of the object's fields, grouped by class. | v-object(class-name :: String, field-sets :: FieldSet) end data TypeError: | err-class-not-found | err-field-not-found | err-method-not-found | err-type-mismatch | err-this-outside-of-method | err-non-object | err-unbound-id end data DynamicError: | null-pointer-exception end
11.3 Compilation and Type Checking
The compiler needs to do a number of type-checking operations:
11.3.1 Operations to Check
Make sure that all get and set field operations will succeed (e.g. interp cannot throw a field-not-found error, and will look up the field on an object, not a number).
Make sure that methods aren’t mis-applied to the wrong type of argument (just like the type-checker did with function arguments), that methods’ bodies return the annotated return type, and that method calls actually have an object, not a number, in the object position.
Check that subclasses are subtypes of their superclass. Note that this isn’t a given; some work needs to be done in order to ensure this. A class is allowed to be a subtype if for all of its methods, methods with the same name in its superclass and above have: (1) a covariant return type, and (2) a contravariant argument type. Note that there are no restrictions on inheriting fields with the same name and a different type (does this match Java’s behavior?).
Check that, as before, numeric operations (just plus in this case), consume and produce Nums only.
Check that, as before, if expressions have the same type for then and else branches.
These checks ensure that almost all possible errors are caught at compilation/type-checking time, rather than happening in the interpreter. It should be clear from your implementation that all the errors are caught in the compiler, and not left for the interpreter to catch!
11.3.2 Compiling Field Lookups
We noted in lab that the compiler also affects field lookup by using information from the type checker. This motivates the difference between src-get-field and j-get-field:
| src-get-field(obj :: JExprSrc, field :: String) | j-get-field(obj :: JExpr, class-name :: String, field :: String)
The latter, which will be in the output of the compiler, has an explicit position, class-name for keeping track of which class the type checker computes for obj. Note that this can never be Num, since that would be a error because it isn’t possible to look up fields on numbers. An analogous extra position is in j-set-field for src-get-field, which have the same type-directed lookup behavior.
11.3.3 Null and Type-checking
Java (and Javette) has a special value called null which can have any class type. To slightly simplify null handling, in Javette an explicit class name is provided in each user-written use of null. So the type of src-null("C"), for example, is the type t-obj("C"). This avoids adding new rules about “the type of null” to the type system, which makes things more complicated but not more illuminating.
11.3.4 Type Compatibility
When checking if two types match, Java uses subtyping rather than type equality. This includes:
Checking that the return types of a method matches its body’s type
Checking that arguments match the parameter’s annotation
Checking that a field is set to a value of the right type
Checking that the annotation on a let expressions binding is matched by the expression it is bound to
11.3.5 The Type of this
The type of the special this expression is always the type of the enclosing class. If this appears in the main expression of a program, an err-if-outside-of-method exception should be raised.
11.4 Runtime Behavior
11.4.1 The Structure of Objects
Once compiled to a JExpr, the program can be run. The structure of values is interesting, as we saw in lab, objects store a set of field for each class in the chain from the class up to Object. For example, consider this example:
(program (classes (class A extends Object (fields (x Num) (y Num)) (methods)) (class B extends A (fields (y Num)) (methods))) (let (B b (new B)) (let (A a b) (do (set a x 10) (set a y 5) (set b y 3) b))))
This should return this object:
v-object("B", [string-dict: "B", [mutable-string-dict: "y", v-num(3)], "A", [mutable-string-dict: "y", v-num(5), "x", v-num(10)] ])
That is, it:
Has a class tag, "B", that tracks which class was used to construct it (e.g. (new B)).
Has one set of fields for the "A" part of the object, and another set for the "B" part.
Using a mutable string dict lets us avoid passing around a store, which we could do, but would get in the way of what we’re trying to understand about objects in this assignment. Allows for the fields of the object to be imperatively updated (though not the class parts), since they are represented as a mutable-string-dict rather than an immutable one.
Also note that the two set expressions on the y field updated different fields, which gets back to the point of using the type of the object expression to select which field to assign into.
11.4.2 Default Values
When using new, all fields with Num type should be initialized to 0, and all fields with a class type should be initialized to v-null. Example:
(program (classes (class A extends Object (fields (x Num) (y Num)) (methods)) (class B extends A (fields (y A)) (methods))) (new B))
produces
v-object("B", [string-dict: "B", [mutable-string-dict: "y", v-null], "A", [mutable-string-dict: "y", v-num(0), "x", v-num(0)] ])
11.4.3 Null
If you’re wondering why the heck there’s all this nonsense around null, good question. At first glance, it seems like it’s just there to avoid needing to initialize fields, and to use as a dummy value, which would be a pretty weak argument for having it. A better argument is that without some default value for fields, it would be difficult to create cycles between objects. The chapter on cyclic data in PAPL discusses a few design tradeoffs here. There are certainly alternatives to the null design, and they may be preferable. There is one distinguished null value (in Java and Javette), called v-null. It is a runtime error to get or set a field, or call a method, on the null value. It is a specific runtime error So, for example, these programs throw null-pointer-exceptions when run, though they successfully type-check:
(program (classes (class C extends Object (fields (x Num)) (methods))) (get (null C) x)) (program (classes (class C extends Object (fields (x Num)) (methods))) (set (null C) x 0)) (program (classes (class C extends Object (fields) (methods (method Num id (Num x) x)))) (call (null C) m 0))
11.4.4 Runtime Behavior of Method Calls (and Binding this)
On each call to a method, the provided argument is evaluated, and then bound to the method’s parameter name in the environment, just as in a function call. In addition, the object on which the method is called is “passed in” as the current binding for this. It’s up to you how to handle this (no pun intended), which might be part of the environment or an extra parameter to the interpreter, for example.
11.4.5 Lack of Booleans
There are no boolean values in Javette. if expressions evaluate the then branch on 0 and v-null, and the else branch on all other values. There are no errors (type or runtime) for if expressions in Javette.
11.5 Support Code
For reference, the Java quiz:
https://docs.google.com/forms/d/1ij--psfv-AvuMgGKV_XDr3hSF-ZziknYqM-P_GJG_Io/viewform
Tests:
https://code.pyret.org/editor#share=0B32bNEogmncObmlfQ2hXVXFVZTg&v=v0.5r852
Code:
https://code.pyret.org/editor#share=0B32bNEogmncOUHJRZERTWnlXMVE&v=v0.5r852
Definitions:
https://code.pyret.org/editor#share=0B32bNEogmncOUFdMNDhkVjVvOXM&v=v0.5r852
11.6 Written Questions
There are a few written questions you need to submit answers to along with your final submission. Put them in a separate file.
Argue why your compiler doesn’t have an infinite loop, and must terminate on all inputs.
Can either the Javette compiler or type-checker be run without the other? If not, why not? If so, what parts of the process can be split up?
Can the set of field names in an object be changed after creation?
11.7 Testing and Submission Steps
You can work with a partner on this assignment. You should do test submission and review separately, and then work together subsequently. Feel free to start working together before the Thursday test deadline, but do submit separate test suites. This is better for you: it forces you to see more diversity in tests.
Report to me by email by Thursday midnight if you’re working with a partner, and who; CC your partner. If you want to be randomly assigned a partner, I’ll try to accommodate, but since partners are optional, no promises.
By midnight Thursday (April 2), you should submit 5-10 of what you think are the most interesting tests to Captain Teach at the cs91-type-infer link from:
https://www.captain-teach.org/swarthmore-cs091/assignments/
Submit a single Pyret file with your test cases filled into the check block in the template file.
By midnight Friday (April 3), you should submit any reviews you were assigned. You should feel free to complete the reviews at any time before then, and they will be assigned as submissions come in.
Update Bad solutions are up, usable with:
import shared-gdrive("java-bad-interps.arr", "0B32bNEogmncOdmpzN1RSNngzdzg") as G G.dynamic-get G.dynamic-set G.co-arg-aka-typescript G.contra-ret G.trust-method-body G.zero-object-default G.null-propagates-on-get G.let-uses-expr-type G.set-allows-super-type
11.8 Grading
Your grade will be in a few parts:
10% for submitting an interesting initial set of tests.
15% for convincing written explanations to the written prompts.
25% for the quality of your tests, based on the number of bad implementations they catch. As before, I reserve the right to grade based on additional bad implementations that I don’t release, so catching the ones that are out there does not guarantee a perfect testing score.
50% for the quality of your implementation, based on its correctness, clarity, and style.
Peer reviews will not factor into your grade, though you are free to copy tests from others that you see during review. Please don’t share tests with one another outside of what’s provided during test review.