Compilers vs. Interpreters
2024-11-21
I think of the compiler/interpreter issue this way: All (reasonable) programming languages can be interpreted. But,,, only some languages can be compiled.
To interpret a language: just read the code and immediately do what it says.
If the code says "loop", your interpreter loops and re-executes every statement in the loop, even though the statements have already been seen before and interpreted before.
Only some languages can be compiled.
Compilation is a pre-optimization step. The compiler is just an app that reads the code and translates it into opcodes. If the code says "loop", the compiler compiles in a "loop" opcode,,, but, the compiler doesn't need to re-read the statements in the loop, it just tells the CPU to loop at runtime.
If the language being compiled contains types, the type checking is done at compile-time by a compiler. A compiler removes all of the type checking, so that the type-checking doesn't need to be done at runtime.
An interpreter, though, does type-checking at runtime as it interprets the code. In a loop an interpreter will re-type-check the same statement over and over again, without caching such information once it's been figured out.
To compile a language, you have to ensure that the language makes it possible to pre-check and pre-optimize stuff in the code. This means that you have to tweak the language syntax to allow for this kind of pre-optimization.
The C language, for example, has been tweaked to require all definitions to appear before they get used in the code (kinda like a legal document, which defines terms and words at the top, before diving into the rest of the legalese).
In the early days, it was considered sinful to waste computer time, so one-pass compilers were the rage. So programming languages were bent to allow for one-pass compilation. One-pass compilers don't need to back up through the source code to look-up definitions nor hope that declarations will be found later on. It is assumed that everything has already been declared, or else, a "semantic error" is raised. Compilers simply record declarations in symbol tables, as the code is read in - in a one-way, streaming manner.
Compilers are, actually, transpilers. Compilers convert HLL (high-level language) text into LLL (low-level language) text, i.e. assembler. Compilers are transpilers that, also, do type-checking during transpilation.
In the early days, there was no verbal distinction between compilers and transpilers. Compiler apps were big, fat programs that were essentially boring to write and tricky to write without creating lots of bugs. So, to recognize the high degree of boring complexity, those kinds of apps were given a “special” name - “compiler”. Everyone knew that compilers translated HLL text to LLL text and the word “compiler” was coined to mean that kind of transpilation. Later, it was discovered that text-to-text transformation was interesting in its own right, so every app that didn’t convert HLL text to LLL text was termed to be a “transpiler”. No one thought to think of “compilers” that way, nor to use the word “transpiler” for what had already been named “compiler”.
Text-to-text transpilers don’t need to automatically toss away all whitespace,,, but, the idea of ignoring whitespace was ingrained in the compiler meme. Even today, compilers summarily toss away whitespace in irrecoverable ways. It would be better to turn whitespace into stand-alone tokens (during scanning, say), but, we continue to ignore whitespace during compilation because “we’ve always done it that way”.
Languages that contain features, like Eval, can’t be pre-optimized and defy compilation. Eval can’t be pulled out at compile-time and must be evaluated at runtime. Such languages aren’t compiled, or are only partially compiled. It used to be believed that Lisp couldn’t be compiled,,, but, then Common Lisp came along and made compilation of Lisp possible (well, other than calls to eval). Earlier Lisps used to have “shallow binding”, which was hard to pre-compile, since variable lookup depended on the call-chain at runtime. That was done away with due to the emphasis on “static scoping” and Common Lisp. Javascript brought back shallow-binding, because it is a useful concept, even if it is hard to pre-compile. In fact developers use Eval every day - compilers are just Eval wrapped in sheep’s clothing.
See Also
References: https://guitarvydas.github.io/2024/01/06/References.html
Blog: https://guitarvydas.github.io
Videos: https://www.youtube.com/@programmingsimplicity2980
Discord: https://discord.gg/qtTAdxxU
Leanpub: [WIP] https://leanpub.com/u/paul-tarvydas
Gumroad: https://tarvydas.gumroad.com
Twitter: @paul_tarvydas

