← Lectures
Back

1 / 7

Next

Slide 1 of 7: The question is about implementations

Arrow keys move through the slides while these controls have focus.

The theatre is not shown, because your system asks for reduced motion. The lecture itself is below, in full.

Lecture · 7 slides

Compiled, interpreted, and the space between

"Is it compiled or interpreted?" is one of the first questions a beginner is taught to ask about a language, and one of the first that stops being useful. It survives because it once picked out a real difference, and because the answer used to correlate with things people care about: how fast the program runs, how quickly you can try a change, whether you ship source or a binary.

Those correlations have been breaking for forty years and are now mostly gone. The same language routinely has a compiler and an interpreter. A single program can be interpreted for its first thousand executions and compiled to machine code for the ones after that, in the same process, while it runs. C has interpreters. Python has ahead-of-time compilers. JavaScript, the language most often called interpreted, is executed by some of the most sophisticated compilers ever written.

What follows tries to replace the question with better ones. There is a real distinction here, and it is worth understanding — but it is a property of an implementation rather than of a language, and it is a point on a spectrum rather than one of two boxes.

AssumesYou have run a program. It helps to have seen an error message from a compiler and one from a runtime.

  1. Slide 1

    The question is about implementations

    The first correction is the one that dissolves most of the confusion: nothing in the definition of a language says how it must be executed.

    A language is a grammar and a set of rules about what programs mean. Whether some particular program is translated into machine code before it runs, or walked over by another program while it runs, is a decision made by whoever wrote the implementation. There have been C interpreters since the nineteen-eighties, used for debugging and for teaching. There are compilers that turn Python into native executables. Neither changes what the language is.

    So "Python is interpreted" is shorthand for "the implementation of Python that almost everybody uses interprets bytecode", which is true, useful, and a different kind of statement than the one people think they are making.

  2. Slide 2

    What a compiler actually does

    A compiler reads your whole program, decides what it means, and writes a second program that means the same thing in a language closer to the machine.

    c

    That function becomes two or three instructions. The names are gone, the types are gone, the structure is gone; what remains is an instruction that adds two registers and one that returns. Because the compiler saw everything before deciding anything, it can rearrange the program in ways that would be unsafe otherwise: inlining a call, keeping a variable in a register, deleting work whose result nobody reads.

    The costs are the mirror image of the benefits. You wait for the translation before you can run anything. The output is specific to one processor and one operating system. And the compiler has to decide, without ever seeing the program run, which branch is the common one — a guess it cannot revise.

  3. Slide 3

    What an interpreter actually does

    An interpreter keeps your program as data and executes it by walking over that data, deciding at every step what to do next.

    python

    Nothing here is decided in advance. When the call happens, the interpreter looks up what a and b currently are, discovers that both are integers, finds the routine for adding integers, and calls it. Ask again with two lists and it will concatenate them instead, having made no arrangements beforehand.

    That flexibility is the whole appeal. You can change a function while the program is running, inspect anything at any point, and run the same source anywhere the interpreter runs. The price is paid on every single operation: the work of deciding what to do is done again each time round the loop, and that is where the famous slowness lives — not in the language, but in the repetition.

  4. Slide 4

    Bytecode, where nearly everyone actually lives

    Almost no widely used implementation sits at either end. The overwhelmingly common arrangement is to compile to an invented instruction set and then interpret that.

    plaintext

    Python does this, and so do Java, C#, Ruby, Lua and Erlang. Your source is parsed and compiled — genuinely compiled, with syntax errors reported before anything runs — into instructions for a machine that does not exist. Then a program written in C pretends to be that machine.

    This gets most of the portability of interpretation and much of the speed of compilation, because the expensive part of interpreting is parsing and name resolution, and bytecode has already done both. It also makes the original question unanswerable. Java compiles, then interprets, then compiles again. The honest answer to which box it belongs in is that the box was never a good shape.

  5. Slide 5

    Compiling at the last possible moment

    The most interesting machinery in modern runtimes waits until the program is running before compiling it, because by then it knows things a compiler never could.

    A just-in-time compiler watches which functions run often and compiles those to machine code, using what it has just observed. If a function has been called ten thousand times and both arguments were integers every time, it emits the integer version — with a check at the top that bails out to the interpreter if that assumption ever fails. This is called speculation, and it is why JavaScript is within a small factor of C on numeric code despite having no static types at all.

    The costs are real. The runtime is large, startup is slower because the fast code does not exist yet, memory use is higher, and performance is harder to predict: the same function is slow, then fast, then slow again when an assumption breaks. For a long-running server this is an excellent trade. For a command-line tool that runs for forty milliseconds it is a bad one, which is why the same technology is celebrated in one setting and avoided in the other.

  6. Slide 6

    Ahead of time, on the way back

    The pendulum has swung back, for reasons that have nothing to do with the elegance of either approach.

    Mobile platforms and serverless functions made startup time matter again, and a runtime that needs ten thousand executions before it is fast is a poor fit for a process that handles one request and exits. So Android compiles ahead of time on the device, browsers cache compiled WebAssembly, and both Java and .NET have grown ahead-of-time modes that trade peak throughput for starting immediately.

    The lesson is not that one approach won. It is that the choice is made against a workload, and when the workload changed the answer changed with it — twice so far, in opposite directions.

  7. Slide 7

    What the distinction is still good for

    None of this means the words are useless. They are just answering a narrower question than the one they get asked.

    The thing you usually want to know is when errors are found. A language whose implementation examines the whole program before running it can tell you about a misspelled name in a branch you have never executed; one that decides as it goes cannot, and will find it in production at three in the morning. That is a real and important difference, and it is about static checking rather than about compilation, which is why a typed language with an interpreter gives you the good half and an untyped language with a compiler does not.

    The other thing you want to know is what you ship and what the recipient needs. A single binary and a source tree plus a matching runtime are genuinely different things to operate.

    Ask those two questions instead. They have answers, the answers stay true, and neither of them requires deciding which of two boxes a language belongs in.

Works in the collection

The arguments above are hanging on the walls of the museum, in one form or another. These are the ones worth looking at next.

The rest of the programme

  1. What a compiler actually doesFour jobs in a row, each mechanical, none magic: text to tokens, tokens to a tree, a tree to a judgement, and a judgement to instructions.
  2. Functional programming and object orientationTwo ways of arranging a program, what each one genuinely makes easy, and the trade-off underneath the argument that neither side can escape.
  3. The principles of object orientationEncapsulation, inheritance, polymorphism and SOLID, one at a time: what each actually claims, which held up, and which its own community abandoned.
  4. Types: what they can and cannot proveA type checker proves one thing about every possible run of your program. Knowing which proposition explains both the enthusiasm and the disappointment.
  5. Null, and the mistake its inventor apologised forTony Hoare called it his billion-dollar mistake. What was actually wrong with it, what the alternatives cost, and why the fix took forty years to arrive.
  6. What abstraction costsAbstraction is sold as free and is not. What you buy, what you pay, and how to tell before writing it which of the two is larger.
  7. Immutability, and what it is not free ofValues that never change buy sharing, comparison and time travel. They are not free, and it is worth knowing where they are expensive before you commit.
  8. Recursion, and why it feels like cheatingA function that calls itself looks like an unpaid debt. What makes it terminate, what it costs on the stack, and why some problems resist any other shape.
  9. Big-O, and what it deliberately ignoresComplexity notation throws away constants, hardware and every input you will actually see. Knowing what it discards is what makes the number useful.
  10. What regular expressions cannot matchThere is a precise boundary around what a regex can recognise. It explains the famous refusal to parse HTML, and why some patterns run forever.
  11. Concurrency is not parallelismOne is a way of structuring a program, the other a way of executing it. Keeping them apart explains why async helps a web server and threads often do not.
  12. Errors: exceptions, values, and what each hidesThrowing makes the happy path readable and the failure paths invisible. Returning errors as values does the opposite. Neither side has won.
  13. Why programs are hard to changeSoftware is called soft because it can be edited. Why editing gets harder every year, what the mechanism is, and which of the usual remedies work.

← All lecturesAtrium