The Denest Pass
desugar => hoist.scm => denest.scm =>
The next step of compiliation is denesting. Scheme allows expressions like function application and even
IF to be nested freely, meaning you can write
(+ 1 (if ...)) but in lower level languages like C if is a statement not an expression. That’s why this compilation pass pulls every nested expression apart into a linear sequence of binding statements.
A good example of a deeply nested expression is
(define l '(1 2 3 4 5))
which desugars to
(define "tests/scm/12.scm" l (app cons (datum 1) (app cons (datum 2) (app cons (datum 3) (app cons (datum 4) (app cons (datum 5) (datum ())))))))
and then (hoisting doesn’t change it much) the result of denest is:
((def3d2dd275 "l: tests/scm/12.scm" l 0 (let ((tmp4da32c7e (app (var glo cons) (datum 5) (datum ()))) (tmp6ec68664 (app (var glo cons) (datum 4) (var tmp tmp4da32c7e))) (tmp0e0d31ff (app (var glo cons) (datum 3) (var tmp tmp6ec68664))) (tmp2fd0ad81 (app (var glo cons) (datum 2) (var tmp tmp0e0d31ff)))) (app (var glo cons) (datum 1) (var tmp tmp2fd0ad81))))
As you can see the innermost function application is executed first. (Note: Internal to tarot we use
LET to refer to
LET*) and each temporary expression is given a name.
So the denest pass turns scheme expressions into a big long
LET expression where function applications are always “simple”. This is similar to ANF/A-normal form from compilers which you can read more about here: matt.might.net/articles/a-normalization
A rough BNF of the output format of denest is as follows:
<let> ::= (let ((<var>|#f <expr>) ...) <expr>) <expr> ::= <basic> | (app <simple> ...) | (if <expr> <let> <let>) | (allocate-closure <size> <label>) | (set-closure! <var> <index> <simple>)
(the closure stuff is used for both closures and letrec)
<simple> ::= <variable> | <atomic-data>
To implement this we have created a LET-object type. LET-objects can be joined together and merged and manipulated in various ways. A LET-object has a sequence of bindings called its “table” and it has its “body” too.
To denest a function application we denest all its part then join all those resulting LETs into one big sequence. similarly, to denest a let we denest each binding of the let table and join the resulting LETs into one long one.
I also wanted to go over and example where
if is used in an expression context:
(print (fold (lambda (x tot) (+ tot (if (or^ (zero? (remainder x 3)) (zero? (remainder x 5))) x 0))) 0 (iota 1000)))
after hoist we have the following form:
(closure0e0d31ff "tests/scm/rosetta-sum35-b.scm" #f 2 (app (var glo +) (var loc 1) (if (app (var glo boolean-or) (app (var glo zero?) (app (var glo remainder) (var loc 0) (datum 3))) (app (var glo zero?) (app (var glo remainder) (var loc 0) (datum 5)))) (var loc 0) (datum 0))))
and denesting does this to it:
(closure0e0d31ff "tests/scm/rosetta-sum35-b.scm" #f 2 (let ((tmp47caa567 (app (var glo remainder) (var loc 0) (datum 3))) (tmp48249dbf (app (var glo zero?) (var tmp tmp47caa567))) (tmp1f0e5d0d (app (var glo remainder) (var loc 0) (datum 5))) (tmp026baae9 (app (var glo zero?) (var tmp tmp1f0e5d0d))) (tmp2c02fe8c (if (app (var glo boolean-or) (var tmp tmp48249dbf) (var tmp tmp026baae9)) (let () (var loc 0)) (let () (datum 0))))) (app (var glo +) (var loc 1) (var tmp tmp2c02fe8c))))
You can see that the
(+ tot ..) part is at the very end because it’s the last thing that happens.
You can also note that some calculations are done before the
IF condition, in order to make the
IF condition a simple application. Finally the two branches of a conditional will contain their own
In summary the denest pass is about making every function application “simple” by transforming nested code into linear sequenced statements. In doing this it settles the evaluation order of applications. This is a very general sort of compiler pass that may be useful for any ‘expression’ based language to target a ‘statement’ based langauge like C, bytecode or assembly.