Evaluating source code

This is mostly going to paraphrase Programming Languages: Application and Interpretation, as I found the introduction of that book very good and motivating.

Writing a language

The first question we should ask ourselves is, what does it mean to write a programming language?

Well, it must mean to write some sort of program. This program must work on source code, and… do something to it. Here are some common things one might wish do with source code:

Notice how all of these have a common shape: take source code as input, and produce a value (more source code, the result of running the program, …). All these things are evaluators for our language.

We’ll hopefully play with all of these before we’re done, but our focus will, at least initially, be on interpreters.

The substitution model

Our first program

Let us write a very simple program, and see how we would interpret it:

fun f x = x + 1
f 2

This defines a function that takes an x and adds 1 to it, and then calls it with an argument of 2.

I find it easier to think of this as two distinct kinds of information:

I like to represent this as a table, as it feels clear and readable:

TargetKnowledge
f 2f x = x + 1

Presented like this, we can see that a natural way of interpreting f 2 would be to use whatever knowledge we have to simplify it, until it can no longer be simplified.

Here, for example:

TargetKnowledgeAction
f 2f x = x + 1Substitute f with x + 1
Remember that x is 2
x + 1x = 2Substitute x with 2
2 + 1 Simplify 2 + 1
3 N/A

Note how, by convention, I remove things from the Knowledge column when they’re no longer needed. It does not mean the the knowledge has disappeared, but it does make these tables a lot easier to read.

This model is nice and clear, but can lead to ambiguities.

Innermost or outermost first?

Take the following program:

fun f x = x + 1
fun g y = f (y + 3)
g 2

We can start our substitution process easily enough:

TargetKnowledgeAction
g 2f x = x + 1
g y = f (y + 3)
Substitute g with f (y + 3)
Remember that y is 2
f (y + 3)f x = x + 1
y = 2
???

But we must stop here, because it’s not clear what our next step should be. Do you see it?

We could do one of:

This doesn’t necessarily feel like an important decision - surely both these strategies yield 6?

Well, they do. But this can still have a large impact - including, but not limited to, on performance.

Take the following program:

fun f x = x + x
fun g y = f (y + 3)
g 2

Let’s first run through it using an innermost first substitution strategy - that is, we always simplify the parameters of a function before substituting the function with its body.

TargetKnowledgeAction
g 2f x = x + x
g y = f (y + 3)
Substitute g with f (y + 3)
Remember that y is 2
f (y + 3)f x = x + x
y = 2
Substitute y with 2
f (2 + 3)f x = x + xSimplify 2 + 3
f 5f x = x + xSubstitute f with x + x
Remember that x is 5
x + xx = 5Substitute x with 5
5 + 5 Simplify 5 + 5
10 N/A

You can see that with this approach, we only compute 2 + 3 once, before replacing f. Had we made a different choice though and used an outermost first substitution strategy:

TargetKnowledgeAction
g 2f x = x + x
g y = f (y + 3)
Substitute g with f (y + 3)
Remember that y is 2
f (y + 3)f x = x + x
y = 2
Substitute f with x + x
Remember that x is y + 3
x + xy = 2
x = y + 3
Substitute x with y + 3
y + 3 + y + 3y = 2Substitute y with 2
2 + 3 + 2 + 3 Simplify 2 + 3
5 + 2 + 3 Simplify 5 + 2
7 + 3 Simplify 7 + 3
10 N/A

This has one more step than the previous one, because we had to compute 2 + 3 twice. This substitution strategy can, in some cases, end up being more computationally expensive (at least when applied naively).

The reverse is true, too. Imagine the following program:

fun f x y z = if x then y else z
fun g x' y'  = f x' (y' + 1) (y' + 2)
g true 3

Let’s not run through the whole substitution explicitly, although you should feel absolutely free to do it on a piece of paper. The point is:

In that scenario, innermost first is more expensive than outermost first.

This rather long section is meant to illustrate the following point: innermost or outermost first is an important choice with far reaching consequences. It’s a common enough topic that we have words for this:

We’ll be using eager evaluation, mostly because it’s the way most languages go, and because it’s a more intuitive substitution strategy.

Where does that leave us?

We now have a relatively clear model of how to interpret code: use whatever knowledge we can to simplify it, step by step, until we can no longer substitute anything. At this point, we’ll consider the code fully interpreted.

Our next step is going to be to try and implement such a substitution mechanism.