As I was going through my online lectures, it is stated that python is interpreted with Cpython, a software written in C. Similarly, they have Jython (written in Java) and PyPy (written in Python). My question is is these interpreters break down the language into low level code like Assembly and machine code, then who helps interpret the software of these interpreters so that they run in the computer?
If Interpreters are used to run code and are written in another language, won't interpreters need interpreters?
Yes. Maybe. But mostly yes. Depending on how you look at it.
You need an interpreter to execute something. Period. There is no way around that. Even if you just read the source code and figure out the result by hand, you are interpreting the code in your head: you are the interpreter. For native machine code, the CPU is the interpreter.
You cannot execute code without interpreting it.
If you have a program written in some language X, and you want to know what the result of that program is, there are only two things you can do:
- Interpret the program with an interpreter for language X, or
- Compile the program with a compiler from X to Y to some other language Y, for which you have an interpreter.
Note that you can construct a longer chain if necessary: If you have a program written in X, and you have a compiler from X to Y, but no interpreter for Y, but instead you have an interpreter for Z and a compiler from Y to Z, you can
- Compile the program from X to Y, then
- Compile the program from Y to Z, then
- Interpret the program using your Z interpreter.
Likewise, if you have a program written in X, and you have an interpreter for X written in Y, but no interpreter for Y, but instead you have an interpreter for Y written in Z, you can
- Interpret the program using your interpreter for X running on …
- your interpreter for Y running on …
- your interpreter for Z.
Two ways of implementing programming languages
Fundamentally, there are two ways of implementing a programming language: compilation and interpretation.
tl;dr: An interpreter runs the program, a compiler translates the program to another language.
My question is is these interpreters break down the language into low level code like Assembly and machine code,
Interpreters don't "break down the language into low level code like Assembly and machine code". Interpreters interpret programs written in the programming language they are interpreting, i.e. they run the programs. They do not, in some way, produce another output program in something like assembly or machine code. That would be a compiler.
The fundamental difference between an interpreter and a compiler is that the interpreter runs the code (but never translates it), and a compiler translates the code (but never runs it).
Importantly, the fact that a compiler only translates the code but never runs it means that you need an interpreter to run the code. You cannot run code without an interpreter. It doesn't have to be an interpreter for the specific language your program is running in (you could compile it to some other language and then interpret that), but somewhere, sometime, at some point, ultimately, you need an interpreter.
then who helps interpret the software of these interpreters so that they run in the computer?
If we take the example of Jython, it is an interpreter for Python written in Java. (It's actually more complicated than that, but this will suffice for the moment.)
The typical way how a Python program is executed using Jython is something like the following:
- You feed the Python program to Jython. Jython then runs the Python program.
- Oh, but Jython is written in Java! So, you need either an interpreter for Java, or you need to compile Jython to another language that you already have an interpreter for.
- Typically, Jython is compiled to JVM bytecode using a Java compiler.
- Oh, but now we need a Java compiler! Well, that's okay, we have one:
javacships as part of the JDK.
- Well, now we have Jython in the form of JVM bytecode. But how do we run that? We need an interpreter for JVM bytecode!
- Luckily, we have such an interpreter, it is called a Java Virtual Machine.
- Oh, but wait! Our JVM is written in C++! We don't know how to run C++. So, we need either an interpreter or a compiler for C++.
- Luckily, we have such a compiler as well, it is called Clang (or GCC or Microsoft Visual C++ or …)
- Great! Now we have a JVM in the form of AMD64 machine code.
- But wait! How do we run that AMD64 machine code? Well, we need an interpreter …
- Puh, we actually have one: it is called a CPU.
So, the way Jython executes Python is as follows:
- Python is interpreted by …
- Jython (originally written in Java and then compiled to JVM bytecode) which is interpreted by …
- the JVM (originally written in C++ and then compiled to AMD64 native machine code) which is interpreted by …
- the CPU.
This is actually a significant simplification.
For example: the
javac Java compiler that ships with the JDK is actually itself written in Java, so we need a Java compiler to compile the Java compiler to JVM bytecode which then gets interpreted by the JVM. And the C++ compiler is itself written in C++, so we need a C++ compiler to compile the C++ compiler. And actually, the JVM does not only interpret the JVM bytecode, it also compiles it to native machine code after a while. And modern CPUs often compile their "external" instruction set into a simplified "internal" instruction set. Also, I believe that Jython actually compiles Python to an internal representation and then interprets that representation instead of interpreting Python directly.
Also, consider this: what if, instead of compiling our JVM to AMD64 machine code, we instead compile it to MIPS machine code and then run it in an emulator …
Compilers and interpreters
An interpreter for language X is a program (or a machine, or just some kind of mechanism in general) that executes any program p written in language X such that it performs the effects and evaluates the results as prescribed by the specification of X. CPUs are usually interpreters for their respective instructions sets, although modern high-performance workstation and server CPUs are actually more complex than that; they may actually have an underlying proprietary private instruction set and either translate (compile) or interpret the externally visible public instruction set.
A compiler from X to Y is a program (or a machine, or just some kind of mechanism in general) that translates any program p from some language X into a semantically equivalent program p′ in some language Y in such a way that the semantics of the program are preserved, i.e. that interpreting p′ with an interpreter for Y will yield the same results and have the same effects as interpreting p with an interpreter for X. (Note that X and Y may be the same language.)
In some cases, we have more specialized names for certain kinds of compilers, depending on what X and Y are, and what the compiler does:
- if X is perceived to be assembly language and Y is perceived to be machine language, then we call it an assembler,
- if X is perceived to be machine language and Y is perceived to be assembly language, then we call it a disassembler,
- if X is perceived to be lower-level than Y, then we call it a decompiler,
- if X and Y are the same language, and the resulting program is in some way faster or lighter, then we call it an optimizer,
- if X and Y are the same languages, and the resulting program is smaller, then we call it a minifier,
- if X and Y are the same languages, and the resulting program is less readable, then we call it an obfuscator,
- if X and Y are perceived to be at roughly the same level of abstraction, then we call it a transpiler, and
- if X and Y are perceived to be at roughly the same level of abstraction and the resulting program preserves formatting, comments, and programmer intent such that it is possible to maintain the resulting the program in the same fashion as the original program, then we call it a re-engineering tool.
Also, note that older sources may use the terms "translation" and "translator" instead of "compilation" and "compiler". For example, C talks about "translation units".
You may also stumble across the term "language processor". This can mean either a compiler, an interpreter, or both compilers and interpreters depending on the definition.
So, we have:
- AOT compiler: compiles before running
- JIT compiler: compiles while running
- interpreter: runs
Within the family of JIT compilers, there are still many differences as to when exactly they compile, how often, and at what granularity.
Some JIT compilers for example only compils code once (when it is loaded) and compile a whole module at a time. Other compilers may gather information while the program is running and recompile code several times as new information becomes available that allows them to better optimize it. Some JIT compilers are even capable of de-optimizing code. Now, you might ask yourself why one would ever want to do that? De-optimizing allows you to perform very aggressive optimizations that might actually be unsafe: if it turns out you were too aggressive you can just back out again, whereas, with a JIT compiler that cannot de-optimize, the program would crash or return a wrong result; in other words, you simply can't allow yourself to perform the aggressive optimizations in the first place.
JIT compilers may either compile some static unit of code in one go (one module, one class, one function, one method, …; these are typically called method-at-a-time JIT, for example) or they may trace the dynamic execution of code to find dynamic traces (typically loops) that they will then compile (these are called tracing JITs).
Combining Interpreters and Compilers
Interpreters and compilers may be combined into a single language execution engine. There are two typical scenarios where this is done.
Combining an AOT compiler from X to Y with an interpreter for Y. Here, typically X is some higher-level language optimized for readability by humans, whereas Y is a compact language (often some kind of bytecode) optimized for interpretability by machines. For example, the CPython Python execution engine has an AOT compiler that compiles Python sourcecode to CPython bytecode and an interpreter that interprets CPython bytecode. Likewise, the YARV Ruby execution engine has an AOT compiler that compiles Ruby sourcecode to YARV bytecode and an interpreter that interprets YARV bytecode. Why would you want to do that? Ruby and Python are both very high-level and somewhat complex languages, so we first compile them into a language that is easier to parse and easier to interpret, and then interpret that language.
The other way to combine an interpreter and a compiler is a mixed-mode execution engine. Here, we "mix" two "modes" of implementing the same language together, i.e. an interpreter for X and a JIT compiler from X to Y. (So, the difference here is that in the above case, we had multiple "stages" with the compiler compiling the program and then feeding the result into the interpreter, here we have the two working side-by-side on the same language.) Code that has been compiled by a compiler tends to run faster than code that is executed by an interpreter, but actually compiling the code first takes time (and particularly, if you want to heavily optimize the code to run really fast, it takes a lot of time). So, to bridge this time where the JIT compiler is busy compiling the code, the interpreter can already start running the code, and once the JIT is finished compiling, we can switch execution over to the compiled code. This means that we get both the best possible performance of the compiled code, but we don't have to wait for the compilation to finish, and our application starts running straight away (although not as fast as could be).
This is actually just the simplest possible application of a mixed-mode execution engine. More interesting possibilities are, for example, to not start compiling right away, but let the interpreter run for a bit, and collect statistics, profiling information, type information, information about the likelihood of which specific conditional branches are taken, which methods are called most often etc. and then feed this dynamic information to the compiler so that it can generate more optimized code. This is also a way to implement the de-optimization I talked about above: if it turns out that you were too aggressive in optimizing, you can throw away (a part of) the code and revert back to interpreting. The HotSpot JVM does this, for example. It contains both an interpreter for JVM bytecode as well as a compiler for JVM bytecode. (In fact, it actually contains two compilers!)
It is also possible and in fact common to combine those two approaches: two phases with the first being an AOT compiler that compiles X to Y and the second phase being a mixed-mode engine that both interprets Y and compiles Y to Z. The YARV Ruby execution engine works this way, for example: it has an AOT compiler that compiles Ruby sourcecode to YARV bytecode and a mixed-mode engine that first interprets YARV bytecode and once it has gathered some information compiles the most often called methods into native machine code. Most ECMAScript execution engines work this way, too, as do most Smalltalk environments and many Lisps.
Note that the role that the interpreter plays in the case of a mixed-mode execution engine, namely providing fast startup, and also potentially collecting information and providing fallback capability may alternatively also be played by a second JIT compiler.
This is how the second generation of Google's V8 ECMAScript engine worked, for example. This generation of V8 never interprets, it always compiles. The first compiler is a very fast, very slim compiler that starts up very quick. The code it produces isn't very fast, though. This compiler also injects profiling code into the code it generates. The other compiler is slower and uses more memory, but produces much faster code, and it can use the profiling information collected by running the code compiled by the first compiler. The current generation of V8 is different from that again. It includes an interpreter as well. In contrast, the first generation of V8 only contained a single compiler which compiled ECMAScript straight to native machine code.
Every programming language can be implemented by an interpreter and every programming language can be implemented by a compiler.
Case in point: there are interpreters for C and there are native code compilers for Java and for JVM byte code.
However, it is important to understand that all a compiler does is translate a program from one language to another language. That's it. That's all it does.
Only an interpreter can actually execute code.
Interestingly, here is no pure Python interpreter in existence at the moment. All widely-used mainstream Python implementations have compilers:
- CPython compiles Python to CPython byte code (which it then interprets),
- Jython (sometimes) compiles Python to JVM byte code,
- IronPython compiles Python to DLR Trees,
- GraalPython is a strange one, it parses Python into Truffle ASTs, and then interprets the Truffle ASTs with a specializing interpreter, which is essentially equivalent to compiling the Truffle ASTs (it's related to Partial Evaluation and Abstract Interpretation (aka Supercompilation)),
- PyPy is another strange one, it compiles Python to PyPy byte code, interprets that PyPy byte code, and uses a tracing compiler to compile a specialized version of the interpreter which can only interpret that one program, which is essentially equivalent to compiling the PyPy byte code (it's related to Partial Evaluation and Abstract Interpretation (aka Supercompilation)).