Avoiding exponential blowup when processing DAGs

Google's Bazel (https://bazel.build/) open-source project is a
publicly-released version of their build system, Blaze.  Blaze builds every
line of source code that is written by any Google programmer — all of
that source code appears in a single repository!  Therefore, Bazel/Blaze needs to
be fast.  Bazel represents all of the source code and its dependencies as a
large DAG (a
directed
acyclic graph (https://en.wikipedia.org/wiki/Directed_acyclic_graph)).  It needs to manipulate these DAGs efficiently.

One of the biggest problems that the Bazel developers face is exponential
blowup of DAG sizes and therefore of run time.  Periodically, one of the
Bazel developers makes such a mistake, and Bazel becomes unusable until
they can diagnose and fix the problem.

Here are two different ways to view the problem.

-     In a DAG, multiple nodes may have the same child.  Traversing the DAG
    naively would visit the child multiple times — in the worst case,
    exponentially many times.  It is necessary to avoid doing so.

-     Bazel contains a function that takes a DAG as input and generates a DAG
    as output (the output is an object graph).  The Bazel developers want to
    ensure that the size of the output DAG is O(|input DAG|). The input DAG is
    processed bottom-up (it ensures that each input node is visited once) and
    Bazel stores the results of intermediate computations that construct the
    output DAG with nodes of the input DAG. The key thing the Bazel developers
    want to avoid is copying intermediate subgraphs that have unbounded size.

More concretely, there is only one Java type for all DAGs, and there is a
method flatten().  It's a mistake to call
flatten() on certain DAGs, because doing so may cause
exponential blowup.

The goal of this project would be to better understand the problem with
Bazel, to formalize it, and to create a program analysis that solves
the problem.  You would evaluate your work by running it on the Bazel
codebase to discover latent problems, or by providing it to the Bazel
developers to run each time they propose a code change.  The Bazel
team is interested in collaborating by evaluating a tool.

Array indexing (index-out-of-bounds errors)

An index-out-of-bounds error occurs when a programmer provides an illegal
index to an array or list, as in a[i] or a.get(i)
where i is less than 0 or greater than the length of
a.  In languages like C, this is disastrous:  buffers
overflows lead to about 1/6 of all security vulnerabilities.  In languages
like Java, the result is “merely” that the program crashes.  In
both languages, it is desirable to prevent programmers from making this
error and to prevent users from suffering the bad consequences.

This project will be a substantial case study with
the Index
Checker (https://checkerframework.org/manual/#index-checker).  The first goal is to identify its merits and limitations.
Does it scale up to big,
interesting programs?  Are there common, important code patterns that it
fails to handle?  Does it produce too many false positive warnings?  Does
it place too heavy a burden on the user, either in terms of annotations or
in terms of complexity of the error messages?  Worst of all, are there
unknown unsoundnesses in the tool?
The second goal is to improve its precision enough to make it usable by
real-world programmers.

A related project is to extend
the Index Checker to handle
mutable collections such as
Lists, where the remove() method makes sound,
precise analysis very tricky.

Bazel tool

This project is related to the
Bazel (https://bazel.build/) build system, and was
proposed by its development manager.

The Bazel codebase contains 1586 occurrences of the @Nullable
annotation.  This annotation indicates that a variable may hold a null
value.  This is valuable documentation and helps programmers avoid null
pointer exceptions that would crash Bazel.  However, these annotations are
not checked by any tool.  Instead, programmers have to do their best to
obey the @Nullable specifications in the source code.  This is
a lost opportunity, since documentation is most useful when it is
automatically processed and verified.  (For several years, Google tried
using FindBugs (http://findbugs.sourceforge.net/), but they
eventually abandoned it:  its analysis is too weak, suffering too many
false positives and false negatives.)

Despite the programmers' best efforts, null pointer exceptions do still
creep into the code, impacting users.  The Bazel developers would like to
prevent these.  They want a guarantee, at compile time, that no null
pointer exceptions will occur at run time.

Such a tool already exists:  the
Nullness
Checker (https://checkerframework.org/manual/#nullness-checker) of the Checker
Framework (https://checkerframework.org/).  It runs as a compiler plug-in, and it issues a warning at
every possible null pointer dereference.  If it issues no warnings, the
code is guaranteed not to throw a NullPointerException at run time.

The goal of this project is to do a large-scale case study of the Nullness
Checker on Bazel.  The main goal is to understand how the Nullness Checker
can be used on a large-scale industrial codebase.  How many lurking bugs
does it find?  What
@Nullable (https://checkerframework.org/releases/1.9.13/api/org/checkerframework/checker/nullness/qual/Nullable.html)
annotations are missing from the codebase because the developers failed to
write them?  What are its limitations, such as code patterns that it cannot
recognize as safe?  (You might create new analyses and incorporating them
into the Nullness Checker, or you might just reporting bugs to the Nullness
Checker developers for fixing.)  What burdens does it place on users?  Is
the cost-benefit tradeoff worth the effort — that is, should Google
adopt this tool more broadly?  How should it be improved?  Are the most
needed improvements in the precision of the analysis, or in the UI of the
tooling?

BCEL library

  Annotate the BCEL library to express its contracts with respect to nullness.
  Show that the BCEL library has no null pointer exceptions (or find bugs
  in BCEL).  There are
  already some
  annotations (https://github.com/apache/commons-bcel/compare/trunk...typetools:trunk?expand=1) in BCEL, but they have not been verified as correct by
  running the Nullness Checker on BCEL.  (Currently, those annotations are
  trusted when type-checking clients of BCEL.)

  To get started:

- Fork https://github.com/typetools/commons-bcel.git
  
- Clone your new fork
  
- git checkout typecheck-nullness
  
- mvn verify

   Some challenging aspects of this case study are:

-     There is some poor design that needs to be resolved in discussions with
    the BCEL maintainers.  For example, consider the copy()
    method.  Some implementations of copy() return null, but
    are not documented to do so.  In addition, some implementations
    of copy() catch and ignore exceptions.  I think it would
    be nicest to change the methods to never return null, but to throw an
    exception instead.  (This is no more burdensome to users, who currently
    have to check for null.)  Alternately, the methods could all be
    documented to return null.

Invent your own new type system

We also welcome your ideas for new type systems.  For example, any run-time
failure can probably be prevented at compile time with the right
analysis.  Can you come up with a way to fix your pet peeve?

It is easiest, but not required, to choose an existing type system from the
literature, since that means you can skip the design stage and go right to
implementation.

This task can be simple or very
challenging, depending on how ambitious the type system is.  Remember to
focus on what helps a software developer most!

Bounded-size strings

Windows cannot run command lines longer than 8191 characters.  Creating a
too-long command line causes failures when the program is run on Windows.
These failures are irritating when discovered during testing, and
embarrassing or worse when discovered during deployment.  The same command
line would work on Unix, which has longer command-line limits, and as a
result developers may not realize that their change to a command can cause
such a problem.

Programmers would like to enforce that they don't accidentally pass a
too-long string to the exec() routine.  The goal of this
project is to give a compile-time tool that provides such a guarantee.

Here are two possible solutions.

Simple solution:
For each array and list, determine whether its length is known at compile
time.  The routines that build a command line are only allowed to take such
constant-length lists, on the assumption that if the length is constant,
its concatenation is probably short enough.

More complex solution:
For each String, have a compile-time estimate of its maximum length.  Only
permit exec() to be called on strings whose estimate is no more than 8191.
String concatenation would return a string whose estimated size is the sum
of the maximums of its arguments, and likewise for concatenating an array
or list of strings.

Lock ordering

The Lock
Checker (https://checkerframework.org/manual/#lock-checker) prevents race conditions by ensuring that locks are held when
they need to be.  It does not prevent deadlocks that can result from locks
being acquired in the wrong order.  This project would extend the Lock
Checker to address deadlocks, or create a new checker to do so.

Suppose that a program contains two different locks.  Suppose that one
thread tries to acquire lockA then lockB, and another thread tries to
acquire lockB then lockA, and each thread acquires its first lock.  Then
both locks will wait forever for the other lock to become available.  The
program will not make any more progress and is said to
be deadlocked (https://en.wikipedia.org/wiki/Deadlock).

If all threads acquire locks in the same order — in our example, say
lockA then lockB — then deadlocks do not happen.  You will extend the
Lock Checker to verify this property.

Upgrade to a newer version of ASM

  The
  Annotation
  File Utilities (https://checkerframework.org/annotation-file-utilities/), or AFU, insert annotations into, and extract
  annotations from, .java files, .class files,
  and text files.  These programs were written before the
  ASM (https://asm.ow2.org/) bytecode library supported Java 8's
  type annotations.  Therefore, the AFU has its own custom version of ASM
  that supports type annotations.  Now that ASM 6 has been released and it
  supports type annotations, the AFU needs to be slightly changed to use
  the official ASM 6 library instead of its own custom ASM variant.

  This project is a good way to learn about .class files and
  Java bytecodes:  how they are stored, and how to manipulate them.

  (Kush Gupta is working on this project.)

Stateful type systems

This project is to improve support for
typestate checking (https://checkerframework.org/manual/#typestate-checker)

Ordinarily, a program variable has
the same type throughout its lifetime from when the variable is declared
until it goes out of scope. "Typestate"
permits the type of an object or variable to change in a controlled way.
Essentially, it is a combination of standard type systems with dataflow
analysis. For instance, a file object changes from unopened, to opened, to
closed; certain operations such as writing to the file are only permitted
when the file is in the opened typestate. Another way of saying this is
that write is permitted after open, but not after close.
Typestate
is applicable to many other types of software properties as well.

Two typestate checking frameworks (https://checkerframework.org/manual/)
exist for the Checker Framework.  Neither is being maintained; a new one
needs to be written.

Tool for analysis diffs

   Many program analyses are too verbose for a person to read their entire
   output.  However, after a program change, the analysis results may
   change only slightly.  An "analysis diff" tool could show the
   difference between the analysis run on the old code and the analysis run
   on the new code.

-     The analysis diffs may help the programmer to better understand the
    changes.

-     Bug detection tools. such
    as FindBugs (http://findbugs.sourceforge.net/) or
    the Checker Framework (https://checkerframework.org/), have
    extremely verbose output when first run on a program.  Programmers
    could examine and fix only the warnings about code they have changed
    (and that they are currently thinking about).

-     Tools that always have large output, such as inference tools, could
    become manageable to users if output is shown in small doses.

-     You can probably think of other uses.

  The analysis diff tool would take as input two analysis results (the
  previous and the current one).  It would output only the new parts of its
  second input.  (It could optionally output a complete diff between two
  analysis results.)

  One challenge is dealing with changed line numbers and other analysis
  output differences between runs.

It would be nice to integrate the tool with git pre-commit hooks or GitHub
pull requests, to enable either of the following functionality (for either
commits to master or for pull requests):

-     Permit only those commits/pulls that do not add any new analysis warnings.

-     Permit only those commits/pulls that are "clean" — the analysis
    issues no warnings for any changed line.

  A concrete example of an analysis diff tool
  is checklink-persistent-errors (https://github.com/plume-lib/checklink/blob/master/checklink-persistent-errors);
  see the documentation at the top of the file.  That tool only works for
  one particular analysis, the W3C Link Checker.
  An analysis diff tool also appears to be built into FindBugs.
  The goal of this project is to build a general-purpose tool that is easy
  to apply to new analyses.

Performance improvements

  The Checker Framework runs much slower than the standard javac compiler
  — often 20 times slower!  This is not acceptable as part of a
  developer's regular process, so we need to speed up the Checker
  Framework.  This project involves determining the cause of slowness in
  the Checker Framework, and correcting those problems.

This is a good way to learn about performance tuning for Java applications.

  Some concrete tasks include:

- Profile the Checker Framework.  Run a profiler such as
  YourKit (https://www.yourkit.com/java/profiler/) to determine
  which parts of the Checker Framework consume the most CPU time and memory.

- Perhaps compare a profile of the Checker Framework against a profile
  of regular javac.  This probably is not necessary because the Checker
    Framework is so much slower than regular javac.

- Consider interning string values.  The Checker Framework does a fair
  amount of string manipulation, in part because it reads from resources
  such as stub files that do not produce Elements.  Interning
  could save time when doing comparisons.  You can verify the correctness
  of the optimization by running the
  Interning
    Checker (https://checkerframework.org/manual/#interning-checker) on the Checker Framework code.  Compare the run time of the
  Checker Framework before and after this optimization.

-  Based on profiling results, devise other optimizations, implement
    them, and evaluate them.

Run-time checking

Implement run-time checking to complement compile-time checking.  This will
let users combine the power of static checking with that of dynamic
checking.

Every type system is too strict: it rejects some programs that never go
wrong at run time. A human must insert a type loophole to make such a
program type-check.  For example, Java takes this approach with its
cast operation (and in some other places).

When doing type-checking, it is desirable to automatically insert run-time
checks at each operation that the static checker was unable to verify.
(Again, Java takes exactly this approach.)  This guards against mistakes by
the human who inserted the type loopholes.  A nice property of this
approach is that it enables you to prevent errors in a program
with no type annotations:  whenever the static checker is unable
to verify an operation, it would insert a dynamic check.  Run-time checking
would also be useful in verifying whether the suppressed warnings are
correct — whether the programmer made a mistake when writing them.

The annotation processor (the pluggable type-checker) should automatically
insert the checks, as part of the compilation process.

There should be various modes for the run-time checks:

- fail immediately.
  
- logging, to permit post-mortem debugging without crashing the program.

The run-time penalty should be small:  a run-time check is necessary only
at the location of each cast or suppressed warning.  Everywhere that the
compile-time checker reports no possible error, there is no need to insert a
check.  But, it will be an interesting project to determine how to minimize
the run-time cost.

Another interesting, and more challenging, design question is whether you need to add and maintain
a run-time representation of the property being tested.  It's easy to test
whether a particular value is null, but how do you test whether it is
tainted, or should be treated as immutable?  For a more concrete example,
see the discussion of the (not yet implemented)
[Javari run-time checker](http://pag.csail.mit.edu/pubs/ref-immutability-oopsla2005-abstract.html).
Adding this run-time support would be an interesting and challenging project.

We developed a prototype for the
EnerJ runtime system (https://ece.uwaterloo.ca/~wdietl/publications/pubs/EnerJ11-abstract.html).
That code could be used as starting point, or you could start afresh.

In the short term, this could be prototyped as a source- or
bytecode-rewriting approach; but integrating it into the type checker is a
better long-term implementation strategy.

IDE and build system support

The Checker Framework comes with support for
external tools (https://checkerframework.org/manual/#external-tools),
including both IDEs (such as an Eclipse plug-in) and build tools
(instructions for Maven, etc.).

  These plug-ins and other integration should be improved.
  We have a number of concrete ideas, but you will also probably come up
  with some after a few minutes of using the existing IDE plugins!

  This is only a task for someone who is already an expert, such as someone
  who has built IDE plugins before or is very familiar with the build system.
  One reason is that these tools tend to be complex, which can lead to
  subtle problems.
  Another reason is that we don't want to be stuck maintaining code written by
  someone who is just learning how to write an IDE plugin.

  Rather than modifying the Checker Framework's existing support or
  building new support from scratch, it may be better to adapt some other
  project's support for build systems and IDEs.  For instance, you might
  make coala (https://github.com/coala/coala) support the
  Checker Framework, or you might adapt the tool integration provided
  by Error Prone (http://errorprone.info/).

Model checking of a type system

Design and implement an algorithm to check type soundness of a type system
by exhaustively verifying the type checker on all programs up to a certain
size. The challenge lies in efficient enumeration of all programs and
avoiding redundant checks, and in knowing the expected outcome of the
tests.  This approach is related to bounded exhaustive
testing and model checking; for a reference, see
[Efficient Software Model Checking of Soundness of Type Systems](http://www.eecs.umich.edu/~bchandra/publications/oopsla08.pdf).

A valuable project all by itself would be to compare heavy-weight and
light-weight type inference this whole-program inference vs. Checker
Framework Inference vs. Julia, to understand when each one is worth using.

