PIR Examples

Hello world!

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:

Hello world!

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.

Temporary Registers

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

Prints:

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

Prints:

3.142857

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

Prints:

0 1 2 3 4 5 6 7 8 9 10 

Subroutines

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

Prints:

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

Prints:

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"