---
title: Optimisation through Constraint Propagation
date: August 06, 2017
---

Constraint propagation is a new optimisation proposed for implementation
in the Urn compiler[^mr]. It is a variation on the idea of
flow-sensitive typing in that it is not applied to increasing program
safety, rather being used to improve _speed_.

### Motivation

The Urn compiler is decently fast for being implemented in Lua.
Currently, it manages to compile itself (and a decent chunk of the
standard library) in about 4.5 seconds (when using LuaJIT; When using
the lua.org interpreter, this time roughly doubles). Looking at
a call-stack profile of the compiler, we notice a very interesting data
point: about 11% of compiler runtime is spent in the `(type)` function.

There are two ways to fix this: Either we introduce a type system (which
is insanely hard to do for a language as dynamic as Urn - or Lisp in
general) or we reduce the number of calls to `(type)` by means of
optimisation. Our current plan is to do the latter.

### How

The proposed solution is to collect all the branches that the program
has taken to end up in the state it currently is. Thus, every branch
grows the set of "constraints" - the predicates which have been invoked
to get the program here.

Most useful predicates involve a variable: Checking if it is or isn't
nil, if is positive or negative, even or odd, a list or a string, and
etc. However, when talking about a single variable, this test only has
to be performed _once_ (in general - mutating the variable invalidates
the set of collected constraints), and their truthiness can be kept, by
the compiler, for later use.

As an example, consider the following code. It has three branches, all
of which imply something different about the type of the variable `x`.

```lisp
(cond
  [(list? x)]    ; first case
  [(string? x)]  ; second case
  [(number? x)]) ; third case
```

If, in the first case, the program then evaluated `(car x)`, it'd end up
doing a redundant type check. `(car)`, is, in the standard library,
implemented like so:

```lisp
(defun car (x)
  (assert-type! x list)
  (.> x 0))
```

`assert-type!` is merely a macro to make checking the types of arguments
more convenient. Let's make the example of branching code a bit more
complicated by making it take and print the `car` of the list.

```lisp
(cond
  [(list? x)
   (print! (car x))])
  ; other branches elided for clarity
```

To see how constraint propagation would aid the runtime performance of
this code, let's play optimiser for a bit, and see what this code would
end up looking like at each step.

First, `(car x)` is inlined.
```lisp
(cond
  [(list? x)
   (print! (progn (assert-type! x list)
                  (.> x 0)))])
```

`assert-type!` is expanded, and the problem becomes apparent: the type
of `x` is being computed _twice_!

```lisp
(cond
  [(list? x)
   (print! (progn (if (! (list? x))
                    (error! "the argument x is not a list"))
                  (.> x 0)))])
```

If the compiler had constraint propagation (and the associated code
motions), this code could be simplified further.

```lisp
(cond
  [(list? x)
   (print! (.> x 0))])
```

Seeing as we already know that `(list? x)` is true, we don't need to
test anymore, and the conditional can be entirely eliminated. Figuring
out `(! (list? x))` from `(list? x)` is entirely trivial constant
folding (the compiler already does it)

This code is optimal. The `(list? x)` test can't be eliminated because
nothing else is known about `x`. If its value were statically known, the
compiler could eliminate the branch and invocation of `(car x)`
completely by constant propagation and folding (`(car)` is, type
assertion notwithstanding, a pure function - it returns the same results
for the same inputs. Thus, it is safe to execute at compile time)

### How, exactly

In this section I'm going to outline a very simple implementation of the
constraint propagation algorithm to be employed in the Urn compiler.
It'll work on a simple Lisp with no quoting or macros (thus, basically
the lambda calculus).

```lisp
(lambda (var1 var2) exp) ; λ-abstraction
(foo bar baz) ; procedure application
var ; variable reference
(list x y z) ; list
t, nil ; boolean
(cond [t1 b1] [t2 b2]) ; conditional
```

The language has very simple semantics. It has three kinds of values
(closures, lists and booleans), and only a couple reduction rules. The
evaluation rules are presented as an interpretation function (in Urn,
not the language itself).

```lisp
(defun interpret (x env)
  (case x
    [(lambda ?params . ?body)
     `(:closure ,params ,body ,(copy env))] ; 1
    [(list . ?xs)
     (map (cut interpret <> env) xs)] ; 2
    [t true] [nil false] ; 3
    [(cond . ?alts) ; 4
     (interpret
       (block (map (lambda (alt)
                     (when (interpret (car alt) env)
                       (break (cdr alt))))))
       env)]
    [(?fn . ?args)
     (case (eval fn env)
       [(:closure ?params ?body ?cl-env) ; 5
        (map (lambda (a k)
               (.<! cl-env (symbol->string a) (interpret k env)))
             params args)
        (last (map (cut interpret <> env) body))]
       [_ (error! $"not a procedure: ${fn}")])]
    [else (.> env (symbol->string x))]))
```

1. In the case the expression currently being evaluated is a lambda, we
   make a copy of the current environment and store it in a _closure_.
2. If a list is being evaluated, we recursively evaluate each
   sub-expression and store all of them in a list.
3. If a boolean is being interpreted, they're mapped to the respective
   values in the host language.
4. If a conditional is being evaluated, each test is performed in order,
   and we abort to interpret with the corresponding body.
5. When evaluating a procedure application, the procedure to apply is
   inspected: If it is a closure, we evaluate all the arguments, bind
   them along with the closure environment, and interpret the body. If
   not, an error is thrown.

Collecting constraints in a language as simple as this is fairly easy,
so here's an implementation.

```lisp
(defun collect-constraints (expr (constrs '()))
  (case expr
    [(lambda ?params . ?body)
     `(:constraints (lambda ,params
                      ,@(map (cut collect-constraints <> constrs) body))
                    ,constrs)]
```

Lambda expressions incur no additional constraints, so the inner
expressions (namely, the body) receive the old set.
The same is true for lists:

```lisp
    [(list . ?xs)
     `(:constraints (list ,@(map (cut collect-constraints <> constrs) xs))
                    ,constrs)]
```

Booleans are simpler:

```lisp
    [t `(:constraints ,'t ,constrs)]
    [nil `(:constraints ,'nil ,constrs)]
```

Since there are no sub-expressions to go through, we only associate the
constraints with the boolean values.

Conditionals are where the real work happens. For each case, we add that
case's test as a constraint in its body.

```lisp
    [(cond . ?alts)
     `(:constraints
         (cond
           ,@(map (lambda (x)
                    `(,(collect-constraints (car x) constrs)
                      ,(collect-constraints (cadr x) (cons (car x) constrs))))
                  alts))
         ,constrs)]
```

Applications are as simple as lists. Note that we make no distinction
between valid applications and invalid ones, and just tag both.

```lisp
    [(?fn . ?args)
     `(:constraints
        (,(collect-constraints fn constrs)
         ,@(map (cut collect-constraints <> constrs)
                args))
        ,constrs)]
```

References are also straightforward:

```lisp
    [else `(:constraints ,expr ,constrs)]))
```

That's it! Now, this information can be exploited to select a case
branch at compile time, and eliminate the overhead of performing the
test again.

This is _really_ easy to do in a compiler that already has constant
folding of alternatives. All we have to do is associate constraints to
truthy values. For instance:

```lisp
(defun fold-on-constraints (x)
  (case x
    [((:constraints ?e ?x)
      :when (known? e x))
     't]
    [else x]))
```

That's it! We check if the expression is in the set of known
constraints, and if so, reduce it to true. Then, the constant folding
code will take care of eliminating the redundant branches.

### When

This is a really complicated question. The Urn core language,
unfortunately, is a tad more complicated, as is the existing optimiser.
Collecting constraints and eliminating tests would be in completely
different parts of the compiler.

There is also a series of code motions that need to be in place for
constraints to be propagated optimally, especially when panic edges are
involved. Fortunately, these are all simple to implement, but it's still
a whole lot of work.

I don't feel confident setting a specific timeframe for this, but
I _will_ post more blags on this topic. It's fascinating (for me, at
least) and will hopefully make the compiler faster!

[^mr]: The relevant merge request can be found
[here](https://gitlab.com/urn/urn/issues/27).

