Specialize branched loops

This pull request introduces a mutator that pulls branch statements outside of loops, breaking the loops into various special cases for each branch. We are conservative in deciding when to branch, the main trigger being explicit IfThenElse loops, which are introduced during the previous specialize_clamped_ramps pass. Select nodes of a vector type, but with a scalar condition can also trigger this, but I'm not sure how useful that is.

In order to explicitly create the branches I have also added a LinearSolver mutator, which may or may not be more generally useful. Basically, this mutator solves an expression that must be a comparison node (i.e. EQ, NE, LE, GE, LT, or GT) for a named variable. The expression must be linear in all "free" variables, i.e. variables not in the scope provided to the solver. The result of running the solver is an equivalent comparison Expr, where op->a (i.e. the LHS of the expression) is the requested variable.

I've done initial testing of the performance this code by running a 4-point Laplacian operator on the domain with varying boundary conditions. The rough data throughput rates for 2 basic boundary conditions are compared below:

repeat_edge:
------------------------------
master:  ~350 GB/s
branch:  ~1800 GB/s

constant_exterior:
------------------------------
master:  ~250 GB/s
branch:  ~500 GB/s

Yeah, I thought that was the case with remove_lets, that's why I tried this approach. I will add some more comments to this in the next commit. But let me give you an idea of the problem.

This mutator is basically removing all logical operators from the conditions of if stmts. So it will convert, for example:

```
if (a && b) {
....
} else {
....
}
```

into a stmt like:

```
if (a) {
  if(b) {
  ...
  } else {
  ...
  }
} else {
...
}
```

This just an intermediate phase so that I can apply the linear solver to simple conditionals that do not contain any logical connectives. I never rewrite an if stmt like this in the final IR. I can't just leave lets alone, because of the last test case you suggested I add where the condition containing the && appears in a LetStmt. To get that right I need this normalization procedure to dive into the lets.

I think what I need to do then is to "normalize" the let stmts as well. So if I have something like `let t10.s = a && b...` then I should mutate this into `let t10.s.p1 = a in let t10.s.p2 = b...` and substitute all appearances of `t10.s` with the expr `t10.s.p1 && t10.s.p2`. Does this seem right? I don't think it can lead to combinatorial explosion, but maybe I'm missing something.