docs/faq.pod - Parrot FAQ
Parrot is a virtual machine for dynamic languages such as PHP, Perl, Python, Ruby, Scheme, Tcl, etc. It compiles and executes bytecode, but is also designed to act as an interpreter.
The name "Parrot" started with Simon Cozens's April Fool's Joke where Larry Wall and Guido van Rossum announced the merger of the Perl and Python languages.
A year later, when we were looking for a name for our virtual machine that could run both Perl and Python, it seemed like a perfect fit.
No. Perl 6 is just one of the languages that will run on Parrot. For information about Perl 6 on Parrot, see "perl6/" in languages.
Although Parrot is currently still under development, Parrot has been usable for a long time. The primary way to use Parrot is to write Parrot Intermediate Representation (PIR), described in PDD19. PIR is a high-level assembly language. PIR maps to the more low-level Parrot Assembly language (PASM), which is harder to write and read. Although you could write PASM instead of PIR, PIR is the recommended way to program for Parrot. See the examples to see what both PIR and PASM looks like.
You can also create dynamic content within Apache using Ask Bjorn Hansen's mod_parrot module. You are strongly advised that mod_parrot is a toy, and should not be used with any production code.
Lots of reasons, actually. :^)
- All the cool kids are doing it.
- It's easy to write and read.
- You get all the pleasure of programming in assembly language (albeit a high-level assembly language) without any of the requisite system crashes.
Seriously, though, PIR is generally easy to learn if you have a background in dynamic languages. Programming in PIR is an effective way to write libraries for Parrot, and one of the best ways to write test cases for Parrot.
You can already today! There are quite a few high level languages being targeted to Parrot. Since the introduction of the Parrot Compiler Tools (the Parrot Grammar Engine (PGE) and the Tree Grammar Engine (TGE)), targeting a language to Parrot has become a snap! Please note that, although some languages have come a long way, due to the fact that Parrot is still under active development, most library development is still done in PIR.
Below is a list of some languages that are actively worked on.
- Will Coleda and Matt Diephouse are working hard on their Tcl port to Parrot (called ParTcl). Will also created an APL implementation with Patrick R. Michaud.
- Patrick R. Michaud is working on a Perl 6 implementation using the Parrot Compiler Tools. (although the Perl 6 specification is not finished yet).
- FranÃ§ois Perrad is working on a Lua implementation for Parrot.
- Allison Randal is working on a Perl 1 port to Parrot, called Punie.
- Jonathan Worthington has been working on a .NET to Parrot translator.
- Many other languages are worked on,
some more actively than others.
http://www.parrotcode.org/languages/for a complete list.
Because it's the best we've got.
So true. Regardless, C's available pretty much everywhere. Perl 5's in C, so we can potentially build any place Perl 5 builds.
Because of one of:
- Not available everywhere.
- Limited talent pool for core programmers.
- Not fast enough.
The most common issues are:
- License compatibility.
Parrot has an odd license -- it currently uses the same license as Perl 5, which is the disjunction of the GNU GPL and the Artistic License, which can be written (Artistic|GPL) for short. Thus, Parrot's license is compatible with the GNU GPL, which means you can combine Parrot with GPL'ed code.
Code accepted into the core interpreter must fall under the same terms as Parrot. Library code (for example the ICU library we're using for Unicode) we link into the interpreter can be covered by other licenses so long as their terms don't prohibit this.
- Platform compatibility.
Parrot has to work on most of Perl 5's platforms, as well as a few of its own. Perl 5 runs on eighty platforms; Parrot must run on Unix, Windows, Mac OS (X and Classic), VMS, Crays, Windows CE, and Palm OS, just to name a few. Among its processor architectures will be x86, SPARC, Alpha, IA-64, ARM, and 68x00 (Palms and old Macs). If something doesn't work on all of these, we can't use it in Parrot.
Not only does Parrot have to run on all those platforms, but it must also run efficiently. Parrot's core size is currently between 250K and 700K, depending on compiler. That's pushing it on the handheld platforms. Any library used by Parrot must be fast enough to have a fairly small performance impact, small enough to have little impact on core size, and flexible enough to handle the varying demands of Perl, Python, Tcl, Ruby, Scheme, and whatever else some clever or twisted hacker throws at Parrot.
These tests are very hard to pass; currently we're expecting we'll probably have to write everything but the Unicode stuff.
Those VMs are designed for statically typed languages. That's fine, since Java, C#, and lots of other languages are statically typed. Perl isn't. For a variety of reasons, it means that Perl would run more slowly there than on an interpreter geared towards dynamic languages.
The .NET VM didn't even exist when we started development, or at least we didn't know about it when we were working on the design. We do now, though it's still not suitable.
Sure we will. They're just not our first target. We build our own interpreter/VM, then when that's working we start in on the JVM and/or .NET back ends.
While I'm sure that's a perfectly nice, fast VM, it's probably got the same issues as do the languages in the "Why not something besides C" question does. I realize that the Scheme-48 interpreter's darned fast, for example, but we're looking at the same sort of portability and talent pool problems that we are with, say, Erlang or Haskell as an implementation language.
It's not anymore. As of July 2006, the list is called parrot-porters to reflect the growing list of languages and platforms embraced by Parrot. The old perl6-internals list forwards to the new one.
The JVM and the CLR (Mono and .NET) are two successful stack-based virtual machines. Many interpreters such as Perl, Python, and Ruby are also internally stack-based.
On the other hand, most hardware is register-based, as are several virtual machines designed to emulate hardware. (Such as the 68K emulator Apple shipped with its PPC-enabled versions of Mac OS.)
A few reasons we chose a register-based architecture:
- Executing opcodes on a register-based VM takes fewer instructions (since you eliminate all the steps to push items on to the stack and pull them off again), which means less CPU time.
- One class of security problems in modern software are a result of problems with the stack (stack overflows, stack smashing). We can't entirely eliminate these (since Parrot is written in C, which is stack-based), but we can significantly minimize them.
- A register-based VM is far more pleasant to work with than a stack-based VM.
- In the early days of Parrot one motivating factor was taking advantage of decades of register-based hardware research. We've now moved away from a fixed number of registers to a variable (and potentially unlimited) number of registers per sub. This change opens up many new possibilities, but it does push us past the limits of most research into register-based hardware.
- We're pushing forward the state-of-the art in virtual machines. Innovation often involves breaking with tradition. We're pleased with the results so far, and we're not finished yet.
Reference counting has three big issues.
- Code complexity
Every single place where an object is referenced, and every single place where a reference is dropped, must properly alter the refcount of the objects being manipulated. One mistake and an object (and everything it references, directly or indirectly) lives forever or dies prematurely. Since a lot of code references objects, that's a lot of places to scatter reference counting code. While some of it can be automated, that's a lot of discipline that has to be maintained.
It's enough of a problem to track down garbage collection systems as it is, and when your garbage collection system is scattered across your entire source base, and possibly across all your extensions, it's a massive annoyance. More sophisticated garbage collection systems, on the other hand, involve much less code. It is, granted, trickier code, but it's a small chunk of code, contained in one spot. Once you get that one chunk correct, you don't have to bother with the garbage collector any more.
For reference counting to work right, you need to twiddle reference counts every time an object is referenced, or unreferenced. This generally includes even short-lived objects that will exist only briefly before dying. The cost of a reference counting scheme is directly linked to the number of times code references, or unreferences, objects. A tracing system of one sort or another (and there are many) has an average-case cost that's based on the number of live objects.
There are a number of hidden costs in a reference-counting scheme. Since the code to manipulate the reference counts must be scattered throughout the interpreter, the interpreter code is less dense than it would be without reference counts. That means that more of the processor's cache is dedicated to reference count code, code that is ultimately just interpreter bookkeeping, and not dedicated to running your program. The data is also less dense, as there has to be a reference count embedded in it. Once again, that means more cache used for each object during normal running, and lower cache density.
A tracing collector, on the other hand, has much denser code, since all it's doing is running through active objects in a tight loop. If done right, the entire tracing system will fit nicely in a processor's L1 cache, which is about as tight as you can get. The data being accessed is also done in a linear fashion, at least in part, which lends itself well to processor's prefetch mechanisms where they exist. The garbage collection data can also be put in a separate area and designed in a way that's much tighter and more cache-dense.
Having said that, the worst-case performance for a tracing garbage collecting system is worse than that of a reference counting system. Luckily the pathological cases are quite rare, and there are a number of fairly good techniques to deal with those. Refcounting schemes are also more deterministic than tracing systems, which can be an advantage in some cases. Making a tracing collector deterministic can be somewhat expensive.
- Self-referential structures live forever
Or nearly forever. Since the only time an object is destroyed is when its refcount drops to zero, data in a self-referential structure will live on forever. It's possible to detect this and clean it up, of course... by implementing a full tracing garbage collector. That means that you have two full garbage collection systems rather than one, which adds to the code complexity.
Well... no. It's all or nothing. If we were going to do a partial scheme we might as well do a full scheme. (A partial refcounting scheme is actually more expensive, since partial schemes check to see whether refcounts need twiddling, and checks are more expensive than you might think)
Whether we have a lot or not actually depends on how you count. In absolute, unique op numbers we have more than pretty much any other processor, but that is in part because we have *no* runtime op variance.
It's also important to note that there's no less code (or, for the hardware, complexity) involved in doing it our way or the decode-at-runtime way -- all the code is still there in every case, since we all have to do the same things (add a mix of ints, floats, and objects, with a variety of ways of finding them) so there's no real penalty to doing it our way. It actually simplifies the JIT some (no need to puzzle out the parameter types), so in that we get a win over other platforms since JIT expenses are paid by the user every run, while our form of decoding's only paid when you compile.
Finally, there's the big "does it matter, and to whom?" question. To someone actually writing Parrot assembly, it looks like Parrot only has one "add" op -- when emitting PASM or PIR you use the "add" mnemonic. That it gets qualified and assembles down to one variant or another based on the (fixed at assemble time) parameters is just an implementation detail. For those of us writing op bodies, it just looks like we've got an engine with full signature-based dispatching (which, really, we do -- it's just a static variant), so rather than having to have a big switch statement or chain of ifs at the beginning of the add op we just write the specific variants identified by function prototype and leave it to the engine to choose the right variant.
Heck, we could, if we chose, switch over to a system with a single add op with tagged parameter types and do runtime decoding without changing the source for the ops at all -- the op preprocessor could glob them all together and autogenerate the big switch/if ladder at the head of the function. (We're not going to, of course, but we could.)
As for what the rationale is... well, it's a combination of whim and necessity for adding them, and brutal reality for deleting them.
Our ops fall into two basic categories. The first, like add, are just basic operations that any engine has to perform. The second, like time, are low-level library functions.
For something like hardware, splitting standard library from the CPU makes sense -- often the library requires resources that the hardware doesn't have handy. Hardware is also often bit-limited -- opcodes need to fit in 8 or 9 bits.
Parrot, on the other hand, *isn't* bit-limited, since our ops are 32 bits. (A more efficient design on RISC systems where byte-access is expensive.) That opens things up a bunch.
If you think about it, the core opcode functions and the core low-level libraries are *always* available. Always. The library functions also have a very fixed parameter list. Fixed parameter list, guaranteed availability... looks like an opcode function to me. So they are. We could make them library functions instead, but all that'd mean would be that they'd be more expensive to call (our sub/method call is a bit heavyweight) and that you'd have to do more work to find and call the functions. Seemed silly.
you could think of it as if we had *no* opcodes at all other than
we've a loadable opcode system -- it'd not be too much of a stretch to consider all the opcode functions other than those two as just functions with a fast-path calling system.
The fact that a while bunch of 'em are available when you start up's just a convenience for you.
See http://www.nntp.perl.org/group/perl.perl6.internals/22003 for more details.