The first example clearly has to be an example that prints out "Hello world!".
.sub foo # Trivial example print "Hello world!\n" .end
Running this generates:
First, .sub foo tells the compiler that we are beginning a subroutine called foo. In this case, the name of the sub isn't important, only it's position - unless you tell it otherwise, parrot will execute the first subroutine that's defined. The .end at the end tells parrot that our sub definition is complete.
# indicates a comment, and is followed by a fairly obvious print opcode.
Note that PIR requires that any code you give it must be placed in a subroutine.
In PASM, you're required to keep track of all your registers. PIR lets you use "temporary" registers, so you no longer have to worry about register lifetimes.
.sub temps $I99 = 1 print $I99 print ", " $I98 = 2 print $I98 print ", " $N64 = 4.2 print $N64 print ", " $S71 = "Resting" print $S71 print "\n" .end
1, 2, 4.200000, Resting
Note that by specifying a $ in front of a register, we don't know which of the "physical" registers that is being used, just the type. A trace of this program (parrot -t 1 temps.pir) shows:
0 set I1, 1 I1=-888 3 print I1 I1=1 5 print ", " 7 set I0, 2 I0=-888 10 print I0 I0=2 12 print ", " 14 set N0, 4.2 N0=-88.800000 17 print N0 N0=4.200000 19 print ", " 21 set S0, "Resting" S0="(null)" 24 print S0 S0="Resting" 26 print "\n" 1, 2, 4.200000, Resting 28 set_returns PC5 30 returncc
The trace shows you the offset in the bytecode (useful for telling when you've branched somewhere), the actual opcode used (our simple assignment has been mapped to a set opcode, e.g.), as well as the values of any of the registers (from before the opcode is executed).
So, these temporary registers map to actual registers in the PVM, but using them frees the compiler writer from dealing with the details. Parrot's PIR assembler will automatically handle variable lifetimes, and depending on optimization level, may reuse actual registers when possible.
You should also note the last two opcodes: PIR is automatically handling the calling conventions for you as the subroutine exits.
Variables! Well, named registers
In addition to temporary registers, you can declare named registers, which are effectively subroutine-specific variables. They don't correspond to high level language variables. (Those are more likely to be declared as lexicals or globals.)
.sub pie .local num almost_pi almost_pi = 22/7.0 print almost_pi print "\n" .end
If you examine the trace for this code, you'll see that there's no corresponding line for the .local directive above - parrot again picks a register for us.
Another interesting note from the trace: our variable assignment is converted to the appropriate div opcode. PIR provides syntactic sugar for most of the arithmetic and comparative operators.
Branching and conditionals
Labels are presented at the start of the line with a colon. While PIR does not provide high level language constructs like loops, with its conditional handling, it's very easy to generate them.
.sub loopy .local int counter counter = 0 LOOP: if counter > 10 goto DONE print counter print " " inc counter goto LOOP DONE: print "\n" end .end
0 1 2 3 4 5 6 7 8 9 10
One of the strengths of PIR is its syntax for both defining and calling subroutines that support the Parrot Calling Conventions.
.sub double .param int arg arg *= 2 .return(arg) .end .sub main :main .local int result result = double(42) print result print " was returned\n" .end
84 was returned
If you trace this program, you can see that it's handling the calling conventions for us, setting up the number of arguments of each type, setting the appropriate registers to the arguments. PIR will also transparently handle saving registers around subroutine calls if necessary.
Notice the :main directive on the main subroutine? This tells parrot that when running this file, this subroutine should be run first. Otherwise, the first subroutine we defined (double), would be run instead. This bears repeating: The directive is what makes this the main routine, not the name.
To get arguments for your subroutine, you can use the .param directive to get named arguments, or manage things yourself, as you did in PASM.
When calling a subroutine from PIR, you can either use the name of another local subroutine, as we have above, or you can use a PMC register that contains an invokable PMC. For example, if you wanted to use a subroutine from parrot's standard library:
.sub main :main load_bytecode "library/Data/Escape.pbc" .local pmc escaper .local string result escaper = find_global "Data::Escape", "String" result = escaper( "This is an embedded newline:\n", "'") print "result of '" print result print "'\n" .end
result of 'This is an embedded newline: \n'
One more PIR convenience you can see above. Since most opcodes that have an out parameter have that as a the first parameter, PIR lets you specify it as a left hand of an assignment. So, the PIR above:
escaper = find_global "Data::Escape", "String"
is syntactic sugar for
find_global escaper, "Data::Escape", "String"