Most of you have worked with Java at some point in your Computer Science careers. You may have noticed that the languages that you have built so far have been quite different from Java. Now you get the chance to see how Java is made!
Your programs will now consist of a list of classes and a
expression which is your program's
A class consists of a class name, a super class, a list of fields,
and a list of methods. Each class must have a super class (it is not
optional), but there is implicitly a top-level class called
Object that has no fields or
methods. Each field has a name and a type associated with it
and each method has a return type, name, parameter name, and parameter type
associated with it. For simplicity we have made methods single-arity, so
methods can only take in one argument and must return a value.
For example, to make a class called
Foo that extends
Object with number field named
x and a
bar that takes
in a number and returns the sum of x with the input number would be:
(class Foo extends Object (fields (x Num)) (methods (method Num bar (Num y) (+ (get this x) y))))
The grammar for fields is similar to the one that you have seen in records and the grammar for a method is similar to methods in java:
(method <return-type> <method-name> (<param-type> <param-name>) <method-body>)
Just like Java, the language has subtyping from inheritance. Subclasses are subtypes of superclasses. Any type is also a subtype of itself.
When inheriting from a superclass, a subclass can override methods defined in the superclass. The return type of the overriden method must be a subtype of the return type in the superclass, and the argument type in the superclass must be a subtype of the argument type in the subclass. (This is called covariance/contravariance.) Here is an example:
(classes (class A extends Object (fields) (methods (method A foo (B x) (...)))) (class B extends A (fields) (methods (method B foo (A x) (...)))))
There is no method overloading (i.e., there cannot be two methods in the same class with the same name). Duplicate method names will be rejected by the parser.
It is possible for a class to declare a field with the same name as a field in one of its superclasses.
You can instantiate an instance of a class with
The static type of an object is the type that it is declared to have, whereas the dynamic type of an object is the type that is was instantiated with at runtime. So for instance, in the Java program:
A ac = new C();
the static type of
A, while the dynamic type of
C. This example can also be written in this assignment:
(let (A ac (new C)) ...code-that-uses-ac...)
The fields of an object can be set by calling
<object-expr> <field-name> <value-expr>) and
accessed by calling
(set ...) should return the
new value being assigned. Both
should use the field given by the static type of the
To call a method on an object, you can use
<object-expr> <method-name> <arg-value>).
The method called is determined by the dynamic type of the
object it is called on.
When an instance of an object is instantiated, all of its number field values should
be initialized to 0 and all of its object field values should be initialized to null.
null in this language is a little different than in Java since it takes a type.
This makes the type checking portion of the assignment much easier.
Fields and methods of a certain class can reference its own class. Therefore,
we can have a class,
Foo, which has a field named
f of type
can also have recursive calls and call other methods within the class by passing
this keyword as the object-expr to the
Other types of expressions such as addition and
work as in Paret.
ifzero works like
from previous assignments, except that instead of checking weather
the condition is
true, it checks weather it is 0.
(So, for example,
(ifzero 0 1 2) should evaluate to 1.)
Before interpreting a Java program, you need to check its types and compile it
to a mostly untyped representation. Unlike in
type-checker you must implement
subtyping with inheritance as in Java. Also, objects are automatically upcasted
whenever a superclass is expected.
The following errors can be raised during type checking:
You should raise
err-class-not-found if a class name is referenced but
You should raise
err-method-not-found if a field
or method is not found on the relevant class.
You should raise
err-type-mismatch if some found type is not a subtype of
the expected type (for instance in a method call or in
If the condition in an
ifzero is not a number, raise
You should raise
this is used in the main
You should raise
err-non-object if you were expecting an object but didn't
You should raise
err-unbound-id if you encounter an unbound identifier
err-if-brancheswhen an if statement has branches that have different types.
then-typeis the type of the "then" branch, and
else-typeis the type of the "else" branch.
set expressions, you have to add the static type of
the object whose field is being accessed.
The interpreter takes in a program and interprets its main method.
There are a couple of tricky parts of the semantics to note:
set set the field declared in the type specified in the compiled
Method calls use the definition provided in the most specific superclass of the dynamic type of the object that implements the method.
Note that the only runtime exception is the
null-pointer-exception. This is
because your compiler/type-checker will prevent everything else that might
Unlike in Java, your language should not conflate identifiers and field
names. So if the identifier
x appears in a method,
x can only refer to a
method parameter or a binding in a
let. To refer to the object's field
x you must write
(get this x) (i.e.
this.x in Java).
<program> ::= | (program (classes <class> ...) <expr>) <class> ::= | (class <class-name> extends <class-name> (fields <field> ...) (methods <method> ...)) <field> ::= | (<field-name> <type>) <method> ::= | (method <type> <method-name> (<type> <id>) <expr>) <expr> ::= | <num> | <id> | (+ <expr> <expr>) | (do <expr> <expr> ...) | (let (<type> <id> <expr>) <expr>) | (ifzero <expr> <expr> <expr>) | (get <expr> <field-name>) | (set <expr> <field-name> <expr>) | (call <expr> <method-name> <expr>) | (new <class-name>) | this | (null <class-name>) <type> ::= | Num | <class-name>
### Source language (before compilation) ### 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-ifzero(cond :: JExprSrc, consq :: JExprSrc, altern :: JExprSrc) | src-do(exprs :: List<JExprSrc>) | src-id(var-name :: String) end ### Core language (after compilation) ### data JProgram: | j-program(classes :: List<JClass>, psvm :: JExpr) end data JClass: | j-class(name :: String, superclass :: String, fields :: List<JField>, methods :: List<Method>) 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-ifzero(cond :: JExpr, consq :: JExpr, altern :: JExpr) | j-do(exprs :: List<JExpr>) | j-id(var-name :: String) end ### Shared ### data JType: | t-num | t-obj(class-name :: String) end type TEnv = List<TEnvCell> data TEnvCell: | t-env-cell(name :: String, var-type :: JType) end type Env = List<EnvCell> data EnvCell: | env-cell(name :: String, val :: JVal) end data JVal: | v-num(num :: Number) | v-null # `class-name` is the dynamic type of the object. # `field-sets` stores all of the object's fields, grouped by class. | v-object(class-name :: String, field-sets :: List<FieldSet>) end data FieldSet: # Since fields are stored in a MutableStringDict, # you don't need to use store-passing style like you did for interp-state. | field-set(class-name :: String, fields :: D.MutableStringDict<String, JVal>) end data TypeError: | err-class-not-found(class-name :: String) | err-field-not-found(field-name :: String) | err-method-not-found(method-name :: String) | err-type-mismatch(expected :: JType, found :: JType) | err-this-outside-of-method | err-non-object(val :: JType) | err-unbound-id(id :: String) | err-if-branches(then-type :: JType, else-type :: JType) end data DynamicError: | null-pointer-exception end
To get started, open the
First submit your test cases (named "java-tests.arr") in Captain Teach:
Finally, submit a zip file containing both your test and code files for
java. Call the files "java-tests.arr" and "java-code.arr".