JavaCOP Tutorial

Contact Shane Markstrum (smarkstr at cs dot ucla dot edu) with any questions or comments.

How to run JavaCOP as an extensible type checking compiler

Using JavaCOP is as easy as using javac. In fact, JavaCOP is simply an extension of the OpenJDK version of javac. This tutorial will teach you how to run JavaCOP using the C shell scripts that we provide. To learn how to run JavaCOP by directly invoking java and javac, you can read our scripts, or take a look at this older tutorial.

First Steps:

  1. Obtain a copy of the JavaCOP jar and the JavaCOP scripts (jcopc and javacop), which can be found on the JavaCOP SourceForge site.

  2. Set the $JCOPJAR to the full path to the JavaCOP jar in your C shell environment. Below is an example line from a .cshrc file.
    setenv JCOPJAR /home/smarkstr/libraries/JavaCop.jar
    
    

  3. Verify that your environment variable is maintained in a fresh C shell environment.
    echo 'echo $JCOPJAR' | csh -t
    /home/smarkstr/libraries/JavaCop.jar
    
    

  4. You're ready to start using JavaCOP!

Compiling JavaCOP Rules

If you are eager to start type checking Java programs with JavaCOP, then this section is optional so long as you download NonnullRules.jar. Skip to next section

This section will teach you how to go from a JavaCOP rule file to a compiled JavaCOP checker which can be utilized by the JavaCOP Compiler. If you are interested in learning how to develop rules, you can visit the rule development section.

  1. Obtain a copy of the JavaCOP Repository from the JavaCOP SourceForge site and untar it. Go to the javacop/examples/nonnull directory. The directory should contain two subdirectories (javacop and tests) and the file Nonnull.jcop.

  2. Nonnull.jcop is the file that contains the rule specification for a flow-sensitive non-null checker. To compile this into a Java class for use with JavaCOP compiler, you will use the jcopc script. The jcopc script takes in the name of the rules to be compiled (e.g., Nonnull) and any number of javac options after it. Since the Nonnull rules specify the package javacop.nonnull, the -d . option needs to be specified to place the generated class file in the examples/nonnull/javacop/nonnull directory.

    prompt> jcopc Nonnull -d .
    done
    

  3. At this point, there should now be one new Java file in the examples/nonnull directory: Nonnull.java. In the examples/nonnull/javacop/nonnull directory, you should have four Java class files: Nonnull.class, Nonnull$1.class, NonNullFlowFacts.class, RawTypesFlowFacts.class. If compilation has succeeded, then you now have a working copy of the flow-sensitive non-null type checker. This checker can now be used by JavaCOP to type check Java programs. The next section will go into further details of how to type check Java programs.

Type Checking Java Files

At this point, you should have either a jar file or directory structure containing the classes for the flow-sensitive type checker which you can specify on the classpath of an invocation of the javacop script. For the remainder of this part of the tutorial, the placeholder NonnullRules.jar will be used to represent this classpath entry, such as:

prompt> javacop -jcp NonnullRules.jar ...
  1. To type check Java files using JavaCOP, you will use the javacop script. The javacop script has the following usage, where the -jcp flag indicates classpath information containing compiled JavaCOP rules and rules are specified by their qualified Java class name:
    javacop [-jcp paths] rules [javac_options] java_files
    
    

  2. Before you can type check a Java file, you will need a file that exercises the pluggable type system which you will load. In this case, we will make a very simple Java class that has one field annoted as non-null and which assigns into that field a new Object instance. Save this class as TutorialClass.java
    import javacop.annotations.Nonnull;
    
    public class TutorialClass {
    
    	@Nonnull Object f;
    	
    	public TutorialClass() {
    		f = new Object();
    	}
    }
    

  3. Now, if you run the javacop script to compile TutorialClass.java and pass javacop.nonnull.Nonnull as the first argument, it should compile without an error.
    javacop -jcp NonnullRules.jar javacop.nonnull.Nonnull TutorialClass.java
    Loading constraints class: javacop.nonnull.Nonnull
    Done, loaded 1 constraint sets.
    

  4. To create an error in TutorialClass, change the line
    f = new Object();
    to the line
    f = null;

  5. Now, rerun javacop and it will report an error.
    javacop -jcp NonnullRules.jar javacop.nonnull.Nonnull TutorialClass.java
    Loading constraints class: javacop.nonnull.Nonnull
    Done, loaded 1 constraint sets.
    TutorialClass.java:8: Nonnull: Using possibly null value where a @Nonnull value is expected
                    f = null;
                        ^
    1 error
    

That's all there is to using JavaCOP. It can act as a javac replacement in your toolchain, or as a supplementary tool. The JavaCOP compiler is based on the OpenJDK compiler, which is currently at version 1.7. To run any classes compiled with JavaCOP, you will need to target the compilation to a version of Java compatible with your java executable.

Developing JavaCOP rules

JavaCOP is not only a set of pluggable type checking utilities, but also a declarative language for writing pluggable type systems. In this portion of the tutorial, you will learn how to build a set of rules for a simple non-null type system. While this type system will not exercise every aspect of the JavaCOP language, it will provide insight into using the basic JavaCOP building blocks. For a more complete understanding of the features of the JavaCOP language, you should refer to the (currently non-existent) JavaCOP language manual.

At this point, it is expected that you are familiar with using the jcopc and javacop tools. If you are unfamiliar with these scripts, you should first read the scripts tutorial and ensure that the JavaCOP libraries and scripts are accessible in your programming environment.

Let's move on to the motivating example which will show how you can use a pluggable type system to provide some assurance that you will not generate unexpected NullPointerExceptions.

Motivating Example: A List Data Structure

When developing a library or data structure which is meant to be used in multiple places, you want more assurance that these reused components are bug-free. The goal for this section of the tutorial is for you to create a pluggable type system which ensures that a list data structure will not generate a NullPointerException. The example class for this tutorial is shown below and can be downloaded here.
public class List {

    protected Node root = new EmptyNode();

    public void insert(T t) {
	root = root.insert(t);
    }
}

interface Node {
    Node insert(T t);
}

class DataNode implements Node {
    protected T value;
    protected Node next;

    public DataNode(T value, Node next) {
	this.value = value;
	this.next = next;
    }

    public Node insert(T t) {
	next = next.insert(t);
	return this;
    }
}

class EmptyNode implements Node {

    public Node insert(T t) {
	return new DataNode(t, this);
    }

}
This List class has very simple functionality. The root field points to the head of the chain of Nodes which contain the values stored in the list. The only method which the class contains is the insert method, which simply delegates to the insert method of the root node. As a result, any value inserted in a List object will be stored in whatever manner the Nodes dictate.

There are also two implementations of the Node interface: DataNode and EmptyNode. A DataNode acts as the main link in a List and stores a value. Calling insert on a DataNode will cause it to delegate that call to its next field and store the result as the updated next reference. An EmptyNode is meant to act as the sentinel value of a List. Calling insert on an EmptyNode instance will return a new DataNode object which contains the provided value t, thus extending the list.

The First JavaCOP Rule - Null Dereference Checking

The first step to verifying non-nullness properties of the List class is to create a new JavaCOP program. For the purpose of this tutorial, you should create a new file called NonnullRules.jcop and open it in your favorite editing program. An empty file, however, is not going to do you any good, so let's create a rule that defines a discipline on dereferences in a program.

Check programs with the rule declaration

In particular, one important property of a non-null type system is to verify that dereferences at runtime will not generate NullPointerExceptions. One very conservative type system that is guaranteed to have this property is a type system that disallows any explicit dereferences (i.e., there are no dots in the program). To check this proprety, you need to define a rule in JavaCOP. This is done as a top level declaration in the JavaCOP file using the rule construct. Add the declaration below to your NonnullRules.jcop file.
rule nonnullDerefs(JCFieldAccess d){

}
You can break down this declaration into five main components:
  1. The leading keyword: rule

  2. The name of the rule: nonnullDerefs

  3. The type of node in the abstract syntax tree where it will be enforced: JCFieldAccess (i.e., explicit dereferences)

  4. A variable for accessing information about the abstract syntax tree node: d

  5. The body of the rule
The type of AST node to visit is the most crucial aspect of the rule signature, as this provides the point at which rules will be checked. Since JavaCOP is built as an extension of the OpenJDK javac, you need to use the internal javac AST nodes in order to do type checking at the appropriate places in your Java programs. Some of the names of these nodes may be counterintuitive, such as JCFieldAccess in the example matching all places in the code where a dot is encounted, including method calls invoked on expressions. While JCTree will match any node, a brief overview of some of the other AST node names can be found in the AST quick reference guide.

Program constraits via the require statement

At this stage, you have a JavaCOP rule that you can use to type check your program, but as it has an empty body, it will always succeed. You need to specify some checks to perform on your Java program in order to guarantee the behavior you want. These checks come in the form of program constraints specified by a require statement, which takes a boolean expression clause as its parameter. Change your nonnullDerefs to include the require statement inserted below.
rule nonnullDerefs(JCFieldAccess d){
    require(false):warning(d, "Possible null dereference!");
}
This rule will now fail everytime an explicit dereference is found in a Java program. In particular, it will print a warning to the screen every time for every dotted expression. Note that in the body of a rule declaration, every statement may be associated with either a warning or error message that specifies where in the AST the failure occured (e.g., d) and a String expression that provides a user-specified failure message.

At this point, you are ready to compile your first JavaCOP file and use it to type check the List class. Save your NonnullRules.jcop file and use the jcopc script to compile it into NonnullRules.class. Then use the javacop script with the compiled rules to type check List.java.
prompt> jcopc NonnullRules
Done

prompt> javacop NonnullRules List.java
Loading constraints class: NonnullRules
Done, loaded 1 constraint sets.
List.java:6: warning: NonnullRules: Possible null dereference!
        root = root.insert(t);
                   ^
List.java:19: warning: NonnullRules: Possible null dereference!
        this.value = value;
            ^
List.java:20: warning: NonnullRules: Possible null dereference!
        this.next = next;
            ^
List.java:24: warning: NonnullRules: Possible null dereference!
        next = next.insert(t);
                   ^
4 warnings

Type checking List.java now prints out four warning for the places that might have null dereferences in the program. However, this result is less than satisfying, because it means that users of this type system will have to manually inspect each of these results to verify that there is no error. Moreover, it is clear without even looking at the program that two of these warnings are spurious: the this reference in Java is always guaranteed to be non-null.

Predicate definition with the declare declaration

To resolve this issue, you need to make the require statement's boolean clause indicate which kinds of Java expressions are known to be non-null. You could simply enumerate all the cases with the || operator, but the definition of what should and should not be treated as non-null is bound to be needed in multiple places in a non-null type system. Instead, you should define a boolean predicate that returns true if a Java expression should be considered non-null. In JavaCOP, such predicates are defined using the declare declaration. Add the following predicate definitions to your NonnullRules.jcop file.
declare defNonnull(JCIdent i){
    require(i.fullName.equals("this") || i.fullName.equals("super"));
}

declare defNonnull(JCNewClass c){
    require(true);
}
Predicate declaration signatures are the same as method signatures in Java, although they do not support primitive or generic types. HOwever, one key difference is that multiple declarations can be given for the same predicate. In the example above, you have two definitions of the predicate defNonnull. This means the predicate will be satisfied if any predicate declaration is satisfied (i.e., the predicate is the logical disjunction of the declarations). In this case, if the defNonnull predicate will be satisfied if it is checked on a Java new expression or an identifier with either the name this or super. The predicate can be further expanded by adding new declarations.

To incorporate this new predicate into your nonnullDerefs rule, change the body of the rule to the one shown below.
rule nonnullDerefs(JCFieldAccess d){
    require(defNonnull(d.selected)):warning(d, "Possible null dereference!");
}
Now, your nonnullDerefs rule will be satisfied if it sees a dereference of a known non-null entity, as defined by the defNonnull predicate. In this case, the two dereferences of this that you saw previously should now satisfy the rule and no longer generate a warning. Recompile your NonnullRules.jcop file and use it to type check List.java.
prompt> jcopc NonnullRules
Done

prompt> javacop NonnullRules List.java
Loading constraints class: NonnullRules
Done, loaded 1 constraint sets.
List.java:7: warning: NonnullRules: Possible null dereference!
        root = root.insert(t);
                   ^
List.java:25: warning: NonnullRules: Possible null dereference!
        next = next.insert(t);
                   ^
2 warnings
As expected, you now only have two places in List.java that are generating warnings. Without further information, though, JavaCOP will not be able to reason about these other two explicit dereferences. In the next section, you will use Java metadata to introduce non-null annotations into your Java program for which you can write JavaCOP rules to reason.

Java annotations introduce pluggable type qualifiers

At this point, there are a few more Java constructs and expressions which you can add to defNonnull, but they will not help to get rid of the warnings that are being generated when type checking List.java. To handle this situation, you want to add more documentation to your program to indicate that these fields should be considered non-null. To do this, you can use the Java metadata framework and decorate your program with annotations. In particular, you can create a @Nonnull annotation which will indicate that variables, parameters, and fields have a non-null type and methods return non-null types.

These annotations are very simple, such as the one shown below.
package myrules;

public @interface Nonnull { }

If you compile this annotation and put the class in the appropriate package directory, you can now use it in your Java programs. Annotations can be used in declarations just as you would use any other modifier such as public or static. In particular, you can now declare that the root field of List, and the next field of DataNode are @Nonnull. Don't forgot to include the annotation just like you would include another interface or class, or Java will complain! Your class should now look similar to this class.

If you recompile this class with your NonnullRules type checker, you will notice that just adding the annotations to Java does not cause the type checker to instantly recognize that these additions relate to the non-null type checker. But this makes sense, because there is no reason why you had to use the name @Nonnull. You could have called it @NonNull, started non-null variable names with two capital Ns, or decided to use a non-null-by-default semantics and added a @Nullable annotation instead. All of these are valid ways to introduce non-null types into a program. So, what you need to do at this point is augment your NonnullRules type checker to reason about the @Nonnull annotation that you created.

To add this functionality, you will define a new predicate hasNonnullAnnot which will be satisfied if it is checked on a program entity which has the @Nonnull annotation.
declare hasNonnullAnnot(Symbol s){
  require(s.hasAnnotation("myrules.Nonnull"));
}

declare hasNonnullAnnot(JCTree t){
  require(t.holdsSymbol && hasNonnullAnnot(t.getSymbol));
}
Now, the first thing you might notice about this predicate definition is that the first declaration matches Symbol. I will not go into great detail about this type here, but a Symbol object is a separate tree hierarchy in the compiler which contains information about declarations, such as type information and members. This information is also the same information that is recoverable from bytecode. Since annotations are always added at declarations in Java 7.0 (and earlier), this is the best and most direct place to look for annotations on source code or bytecode.

The second declaration utilizes the first declaration, but with a caveat: only declared entities can safely be tested for annotations. If the JCTree is a declared entity, then it is tested for the annotation and the predicate is satisfied if the annotation is found. If the holdsSymbol guard is not checked here, then the result of t.getSymbol might be null, which would result in a null pointer exception -- the exact thing we are trying to avoid in our Java code!

This predicate can now be incorporated into the defNonnull predicate to determine if a Java expression is non-null.
declare defNonnull(JCTree t){
  require(hasNonnullAnnot(t));
}
Using this new set of rules to test your List class now results in no warnings! But you're not done yet unless you trust that the @Nonnull annoations are being used correctly. What you want to do next is actually use JavaCOP to ensure that null values are not actually being stored in variables and fields declared with @Nonnull. This is the focus of the next section.

Checking deeper properties of the type system

In this section, we will create more rules that use the predicates we previously defined. These rules will ensure that the first rule we developed actually prevents null dereferences.

Rule: Assignments to @Nonnull reference

Here, we will make sure that updates to the fields and locals with the @Nonnull annotation do not violate the non-null discipline we have established so that unexpected null pointer exceptions are avoided. The following rule says that at an assignment statement, if the left hand side reference claims to be @Nonnull, then the right hand side expression must be a non-null value.
rule nonnullAssign(JCAssign a){
  where(hasNonnullAnnot(a.lhs)){
    require(defNonnull(a.rhs)):warning(a, "Possible assignment of null value to @Nonnull reference!");
  }
}

Rule: @Nonnull return statements

rule nonnullReturn(JCReturn r){
  where(hasNonnullAnnot(r.enclMethod)){
    require(defNonnull(r.expr)):warning(r, "Possible return of null value from @Nonnull method!");
  }
}

Overriding Rule: @Nonnull information flow

rule nonnullFlow(a <<: b){
  where(hasNonnullAnnot(b)){
    require(defNonnull(a)):warning(a, "Using possibly null value where a @Nonnull value is expected!");
  }
}

Rule: @Nonnull reference initialization

rule nonnullInit(JCVariableDecl v){
  where(hasNonnullAnnot(v)){
    require(Expr e; v => [^^ ^* = e]):warning(a, "@Nonnull fields and locals must be initialized!");
  }
}

More to come!

How to use the JavaCOP runtime framework

Hopefully coming Soon!