Essentially, what interpretation means is (correct me if I'm wrong):
- Scan a piece of code.
- Come up with a piece of code in another language, with the same meaning.
- Execute that new piece of code.
Interpreted execution is a very fuzzy term these days, but for understanding what interpretation means, this is not a good view.
At a fundamental level, a piece of code has a meaning that can be expressed as a series of real-world effects. At the lowest level, a machine instruction is a series of bytes that means, for example, "take the contents of registers A and B, treat them as numbers, add them together, and store the result in register C". Concrete electrical signals in the computer.
Executing a piece of code means performing the real-world effect. Again, at the lowest level (machine code), we call this just "execution". Hardware circuits in the CPU are triggered by the instruction's values to redirect signals so that the effect happens (e.g. they connect the adder circuit's inputs to the registers A and B, and the output to register C - this is of course a massively simplified view of things).
But when your code is in a format that the hardware doesn't directly understand (i.e. everything except machine code, even something as simple as assembly), then you need a different way of executing it. And that is where the fundamental difference between interpretation and compilation lies.
One thing you can do with code is translate it to a different form. Translation means taking code as a series of tiny bits, looking up (in a table, a switch, a series of if
s, whatever) equivalent code in the other language, and stringing the results together.
Classically, if the target language is machine code, this is called compiling.
The other thing you can do is write a program that takes a bit of the code, looks up its meaning (again, table, switch, etc.), and executes a bit of preexisting code (another little function in the program) that generates the intended real-world effect. This is the most pure form of interpreting.
However, pure interpretation in this way is not efficient. This is where things get muddled.
A pure interpreter must re-read code over and over. Unless you work in an extremely memory-sensitive environment, this is stupid. (And even if you do, it's stupid, but in a different way.) A better thing to do is do at least part of the job only one time. High-level languages are complex to understand, and I/O to read the code is costly. So there are things that you can do.
For example, you can read the code in larger chunks and build some representation of it in memory - say, an AST. That saves you from reading it over and over. But ASTs are not very memory-efficient, because they hold too much information about the exact structure of the original code.
Another thing you can do is first translate the code to a more compact form that is still a series of instructions, just simpler. Then a pure interpreter for this simpler form can execute a lot faster, because it's simpler. The CPython interpreter, for example, does this. (.pyc files are cached versions of that simpler form.)
You can also go the full compiler route and translate to machine code and let the CPU execute it, only instead of doing that once and distributing the compiled code, you do it as the program starts. This is called JIT (just in time) compilation.
You can add multiple stages to this, first translating to some simpler form, and then compiling that to machine code. And you can vary when you execute each stage. Before the program is distributed. At program startup. When the actual execution reaches a function. Etc.
Thus, you get a wide spectrum of possible execution strategies, and the simple terms "compiling" and "interpreting" become blurred. Also, there aren't compiled and interpreted languages anymore, because a single language may well have different implementations using different strategies.
But here are some examples.
C and C++ are still typically compiled to machine code ahead of time, and the compiled version is distributed. Of course, the compilers are very complex and will use intermediate formats too.
Java and C# are translated to intermediate forms (Java Bytecode and MSIL, respectively) ahead of time in a process also called compilation (because bytecode is kinda similar to machine code in structure and representation), and the compiled form is distributed. Execution of the intermediate form differs between platforms. Java implementations tend to have a pure interpreter run first, but eventually compile down often-used parts of the code to machine code. .Net, as far as I know, only compiles to machine code, at the granularity of functions (i.e. when execution calls a function for the first, time, the IL gets compiled and stored for subsequent executions).
CPython, the standard python implementation, translates to a bytecode when a file is first read (caching the result in another file), and then does pure interpretation of that bytecode.
JavaScript engines have extremely complex implementation strategies because their performance demands are so high. Mozilla's engine, for example, starts out translating to one intermediate form, and interpreting that. Then it has a JIT compiler from that form to machine code for often-used code. And for code that is used very often (and for specially annotated code), it will translate the intermediate form to a different intermediate form, and use a second JIT compiler to translate that to machine code. (The difference between the two JITs is a different trade-off between time spent compiling and quality (read: speed) of the generated code. And the second intermediate form exists because it's more suitable for the JIT that spends more time trying to optimize.)
There are hardly any pure interpreters for high-level languages anymore. In modern usage, an interpreter is a program that takes in high-level code and produces real-world effects, no matter how many translations are in-between. A compiler is a program that takes in code and produces machine code or some not-human-readable intermediate form. And a VM is a program that takes such an intermediate form and produces real-world effects. Thus you can roughly find three categories of implementations (and languages, if you go by typical implementation):
- Compiled languages, where you have a compiler that produces machine code.
- VM-based languages, where you have a compiler that produces intermediate code and a VM that executes it.
- Interpreted languages, where you have some kind of interpreter.