His contention (and I hope he will correct me if I misrepresent his position) is that Julia has a compiler and most code is compiled to machine instructions at runtime. Therefore, it is misleading and actually incorrect to call Julia interpreted.
My contention is that Julia has an interpreter, this interpreter is running in every Julia process, and that numerous other implementations of interpreted languages use JIT compilers to produce machine code at runtime (albeit not as extensively or to as great an effect)
I’m interested to hear what other Julia users and especially language maintainers think about this topic.
I think this is key to why people classify languages with both compilation and interpretation as one or the other. Your argument that Python is interpreted but has compiling implementations muddles the question; it’s not really the language but the implementation that can be classified as one or the other. PyPy JIT-compiles to machine code, CPython interprets compiled bytecode. The Numba package can be used with CPython to incorporate limited JIT compilation into an otherwise interpreted implementation. Julia’s (only?) implementation is compiled by that logic, no question; the compiler is what runs our code, and unlike in CPython, the interpreter has a minor role.
Your opinion that interactivity is a property of interpreted languages is not common, they’re considered orthogonal concepts, even if correlated. You could change CPython to a non-interactive yet interpreted implementation, just take away the REPL and force people to run scripts in the terminal.
Practically speaking, julia is a compiled language because its performance characteristics match that of a compiled language.
Albeit a very late one, with a very fast compiler (at least compared to say a C++ compiler).
The time before you can start running julia code is nontrivial, you need to wait for it to compile before it can be run.
Once it runs its really fast – julia has an optimizing compiler, it rewrites your code to be semantically identical but faster, including e.g. deleting code that has no user visible effects beyound taking time to run (a lot of compilers don’t do this, and no interpreters do this).
Less pratically speaking, I have heard it said that the julia compiler runs an bstract interpretation of the source code, which happens to output compiled code as a side effect.
Which is catchy but idk that it is that meaningful.
Julia’s only implementation is also interpreted. These two things are not mutually exclusive.
(also, in the name of pedantry—we’re all nerds here—I want to point out that compilers don’t run code at all, but this is not really your point, I understand)
CPython is also compiled, but nobody calls it a compiled implementation or even compiled-then-interpreted. It’s a misleading classification, nothing we can really do about it, but it does have a logic to it that puts Julia in the compiled camp.
The time before you can start running julia code is nontrivial, you need to wait for it to compile before it can be run.
--compile=min can be used to avoid this startup time because it uses the interpreter more extensively. You don’t need to wait for the Julia compiler. You just get more optimized code if you do, so it’s the default.
deleting code that has no user visible effects beyound taking time to run (a lot of compilers don’t do this, and no interpreters do this).
Some interpreters actually do do this. Specifically, this is a feature of Python 3.12 which they call “tier 2 optimization”. It does inlining and eliminates redundant bytecode, among other things.
Is this correct? For one, 3.12 is not out yet, and for another, tier 2 optimization is said to be planned for 3.13. I also haven’t heard of generalized inlining happening in CPython, in fact this PEP from this year outright states it’s “near-impossible” in the general case where functions can be changed (and Python doesn’t do method invalidations like Julia). I have observed CPython doing limited constant folding and dead code elimination when compiling to bytecode.
Sure, but even there the calls are invoking compiled code, so the interpreter does very little.
Release candidates are out, so the interpreter at least exists. It may be that I said too much about tier 2 optimizations being in 3.12. This was my impression from my reading, but it may be that I misunderstood. In any case, as you said, some dead code elimination is already in Python.
As far as I understand, there are some runtime checks to make sure the code is on the “happy path” and it uses the inlined code if it is. It’s the normal kind of stuff that every tracing JIT does, just without the JIT.
edit: I check some docs and I was definitely wrong about tier 2 optimizations in 3.12. There were some other optimizations slated for 3.12 which would eliminate some redundant bytecode, but it appears these were also postponed. Most of the improvements this time were around the object representation.
Still, JIT is its own category of how to run code, and not just “another interpreter”.
And compared to other JIT languages, Julia has a particularly simple interpreter with a much simpler heuristic, which is used much less then in other JIT languages (which is sadly also the reason, that we have those horrible compile time latency problems).
Because of that, I’d categorize Julia much closer to a compiled language than most other JIT compiled languages - and therefore even further from any interpreted language.
The actual heuristics Julia uses are also very different from other JITs. I don’t think Julia does profiling, but someone can correct me if I’m wrong. My understanding was that it was more about inference and inferability (sounds like a title for a Jane Austen novel…), but it’s been a while since I kept up with the specifics of the Julia compiler. My understanding, in any event, is that this snippet from wikipedia is not true for Julia.
Julia doesn’t use many JIT heuristics, it mostly just compiles what it parses.
We have to manually call Base.invokelatest to stop julia from trying to compile through code, say in an expensive to compile but rarely-used branch.
Yes, branches deep in your call stack that will not be used are usually still compiled unless the compiler can prove from the types that they are not used. Its pretty far from being interpreted.
Because the vast, vast majority of Julia code is executed via LLVM compilation. The interpreter as I understand it is a shim to make bootstrapping easier and slightly reduce some of the JIT tradeoffs — not a major feature of the language design.
At a glance, I must admit this looks like you’ve seized upon a relatively minor detail and are using it to make much more sweeping statements than are actually supported.
The core devs have previously described Julia as a “Just ahead of time” compiled language for good reason.
Its execution model involves an interpreter and a compiler.
Because that’s pretty close to the definition of a JIT compiled language, which is it’s own execution category - which behaves significantly differently from an interpreted language…
So anyone hearing “interpreted” won’t think of “ah yes, a JIT compiled language”, so people will attribute the wrong attributes to Julia if you call it interpreted (E.g. no compile times, slow performance, which just doesn’t fit Julia).
edit:
You are correct, that the precise categorization is pretty hard to nail down because of all those subtleties involved, but calling it interpreted without mentioning Julia’s JIT is just misleading.
Something else I notice in the github repo, you say
it compiles to LLVM bytecode at runtime
it’s probably worth stating that with the changes in the last few Julia releases, most package code is actually compiled earlier. That’s the whole reason why we’ve now go .so files as well as .ji now.