Parrot Assembly Language
We've seen some of the common ways for programming Parrot in earlier chapters: PIR is the intermediate language that's used most often for implementing routines in Parrot, NQP is used for writing grammar actions for high-level language compilers, PGE is used for specifying grammar rules, and various high-level languages that target Parrot are used for most other programming tasks. These options, though many and versatile, are not the only ways to interface with Parrot.
In regular assemblers,
assembly language mnemonics share a one-to-one correspondence with the underlying machine code words that they represent.
A simple assembler (and,
for that matter,
a simple disassembler) could be implemented as a mere lookup table.
PIR does not have this kind of direct correspondance to PBC.
A number of PIR features,
especially the various directives,
typically translate into a number of individual operations.
Register names,
such as $P7
don't indicate the actual storage location of the register in PIR either.
The register allocator will intelligently move and rearrange registers to conserve memory,
so the numbers you use to specify registers in PIR will be mapped to different numbers when compiled into PBC.
Because PIR and PBC can't be directly translated to one another, and because it can be difficult to disassemble low-level PBC back into the higher-level composite statements of PIR, especially after optimization, another tool is needed. That tool is PASM.
PASM,
the Parrot Assembly Language,
is the lowest-level interface to Parrot.
PASM instruction mnemonics do share a one-to-one correspondence to the underlying PBC opcodes,
and for this reason is used by the Parrot disassembler instead of PIR.
PASM is missing some of the features of PIR: Most directives,
symbolic operators,
if
and unless
compound statements,
automatic register allocation,
and a few other bits of syntactic sugar are missing from PASM.
Because of these omissions,
it is strongly recommended that most developers do not use PASM to write any large amount of code.
Use PIR if you need to,
or a higher-level language if you can.
PASM Files
The Parrot compilers,
IMCC and PIRC,
differentiate between PIR and PASM code files based on the file extension.
A file with a .pasm extension is treated as pure PASM code by Parrot,
as is any file run with the -a
command-line option.
Early in the Parrot project's history,
PIR was treated as a pure superset of PASM.
All PASM was valid PIR,
but PIR added a few extra features that the programmers found to be nice.
However,
this situation has changed and PIR is no longer a strict superset of PASM.
For this reason,
PASM and PIR code need to be kept in files with separate extensions.
As we mentioned before,
.pasm
files are always treated as containing only PASM,
while .pir
files are used for PIR code,
by convention.
Basics
PASM has a simple syntax that will be familiar to people who have experience programming other assembly languages. Each statement stands on its own line and there is no end-of-line delimiter. Statements begin with a Parrot instruction, commonly referred to as an "opcode"More accurately, it should probably be referred to as a "mnemonic". The arguments follow, separated by commas:
[label] opcode dest, source, source ...
If the opcode returns a result, it is stored in the first argument. Sometimes the first register is both a source value and the destination of the result, this is the case when we want to modify a value in place, without consuming a new Parrot register to hold the value. The arguments can be either registers or constants, although the destination argument cannot be constant.
LABEL: print "The answer is: " print 42 print "\n" end # halt the interpreter
A label names a line of code so other instructions can refer to it. Label names consist of letters, numbers, and underscores, exactly the same syntax as is used for labels in PIR. Simple labels are often all capital letters to make them stand out from the rest of the source code more clearly. This is just a common convention and is not a rule. A label can be in front of a line of code, or it can be on it's own line. Keeping labels separate is usually recommended for readability, but again this is just a suggestion and not a rule.
LABEL: print "Norwegian Blue\n"
LABEL: print "Norwegian Blue\n"
POD (plain old documentation) is also allowed in PASM like it is in PIR. An equals sign in the first column marks the start of a POD block, and a =cut
marker signals the end of a POD block.
=head2 This is POD documentation, and is treated like a comment. The PASM interpreter ignores this. =cut end
Besides POD, there are also ordinary 1-line comments using the # sign, which is the same in PIR:
LABEL: # This is a comment print "Norwegian Blue\n" # Print a color name
Constants
We've already seen constants in PIR, and for the most part the syntax is the same in PASM. We will give a brief refresher here, but see the chapter on PIR for a more in-depth discussion of constants and datatypes.
Integer constants in Parrot are signed integers.The sizes of integers and all other data values like floats are defined when Parrot is configured and built. Integers are typically 32 bits wide on 32-bit computers (a range of -231 to +231-1) and twice that size on 64-bit processors. Decimal integer constants can have a positive (+
) or negative (-
) sign in front. Binary integers are preceded by 0b
or 0B
, and hexadecimal integers are preceded by 0x
or 0X
:
print 42 # Decimalinteger constant print +144 # integer constant print 0x2A # hexadecimal integer print 0b1101 # binary integer
Floating-point constants can also be positive or negative. Scientific notation provides an exponent, marked with e
or E
(the sign of the exponent is optional):
print 3.14159 # floating point constant print 1.e6 # scientific notation print -1.23e+45
String constants are wrapped in single or double quotation marks. Quotation marks inside the string must be escaped by a backslash. Other special characters also have escape sequences. These are the same as for Perl 5's qq()
operator: \t
(tab), \n
(newline), \r
(return), \f
(form feed), \\
(literal slash), \"
(literal double quote), etc.
print "string\n" # string constant with escaped newline print "\\" # a literal backslash print 'a\n' # three chars: 'a', a backslash, and a 'n'
Working with Registers
Parrot is a register-based virtual machine. It has 4 typed register sets: integers, floating-point numbers, strings, and Parrot objects (called PMCs). Register names consist of a capital letter indicating the register set type and the number of the register. Register numbers are non-negative (zero and positive numbers), and do not have a pre-defined upper limit At least not a restrictive limit. Parrot registers are stored internally as an array. More registers means a larger allocated array, which can bring penalties on some systems. For example:
I0 integer register #0 N11 number or floating point register #11 S2 string register #2 P33 PMC register #33
We see the immediate difference here that PASM registers do not have the $
dollar sign in front of them like PIR registers do. The syntactical difference indicates that there is an underlying semantic difference: In PIR, register numbers are just suggestions and registers are automatically allocated; In PASM, register numbers are literal offsets into the register array, and registers are not automatically managed. Let's take a look at a simple PIR function:
.sub 'foo' $I33 = 1 .end
This function allocates only one register. The register allocator counts that there is only one register needed, and converts $I33
to I0
internally. Now, let's look at a similar PASM subroutine:
foo: set I33, 1 end
This function, which looks to perform the same simple operation actually is a little different. This small snippet of code actually allocates 33 registers, even though only one of them is needed. It's up to the programmer to keep track of memory usage and not allocate more registers then are needed.
Register assignment
The most basic operation on registers is assignment using the set
opcode:
set I0, 42 # set integer register #0 to the integer value 42 set N3, 3.14159 # set number register #3 to an approximation of pi set I1, I0 # set register I1 to what I0 contains set I2, N3 # truncate the floating point number to an integer
The exchange
opcode swaps the contents of two registers of the same type:
exchange I1, I0 # set register I1 to what I0 contains # and set register I0 to what I1 contains
PMC registers contain references to PMC structures internally. So, the set opcode doesn't copy the entire PMC, it only copies the reference to the PMC data.
new P0, "String" set P0, "Ford" set P1, P0 set P1, "Zaphod" print P0 # prints "Zaphod" print P1 # prints "Zaphod" end
In this example, both P0
and P1
are both references to the same internal data structure, so when we set P1
to the string literal "Zaphod"
, it overwrites the previous value "Ford"
. Now, both P0
and P1
point to the String PMC "Zaphod"
, even though it appears that we only set one of those two registers to that value.
Strings in Parrot are also stored as references to internal data structures like PMCs. However, strings use Copy-On-Write (COW) optimizations. When we call set S1, S0
we copy the pointer only, so both registers point to the same string memory. We don't actually make a copy of the string until one of two registers is modified. Here's the same example using string registers instead of PMC registers:
set S0, "Ford" set S1, S0 set S1, "Zaphod" print S0 # prints "Ford" print S1 # prints "Zaphod" end
Some developers have suggested that PMCs should also use COW semantics to help optimize copy operations like this. However, it hasn't been implemented yet. One day in the future, Parrot might change this, but it hasn't changed yet.
PMC object types
Every PMC has a distinct type that determines its behavior through the vtable interface. Vtables, as we have mentioned previously, are arrays of function pointers to implement various operations and behaviors.
The typeof
opcode can be used to determine the type of a PMC. When the source argument is a PMC and the destination is a string register, typeof
returns the name of the type:
new P0, "String" typeof S0, P0 # S0 is "String" print S0 print "\n" end
Using typeof
with a PMC output parameter instead, it returns the Class PMC for that type.
Autoboxing
As we've seen in the previous chapters about PIR, we can convert between primitive string, integer, and number types and PMCs. PIR used the =
operator to make these conversions. PASM uses the set
opcode to do the same thing. set
will perform the type conversions for us automatically, in a process called autoboxing.
Assigning a primitive data type to a PMC of a String, Integer, or Float type converts that PMC to the new type. So, assigning a string to a Number PMC converts it into a String PMC. Assigning an integer value converts it to a Integer
, and assigning undef
converts it to an Undef
PMC:
new P0, "String" set P0, "Ford\n" print P0 # prints "Ford\n" set P0, 42 print P0 # prints 42 print "\n" typeof S0, P0 print S0 # prints "Integer" print "\n" end
P0
starts as a String
, but when set
assigns it an integer value 42 (replacing the old string value "Ford"
), it changes type to Integer
. This behavior only works for the wrapper PMC types for the primitive values string, int, and num. Other PMC classes will have different behaviors when you try to assign a primitive value to them.
We can also use the box
opcode to explicitly convert an integer, a float, or a string into an appropriate PMC type.
box P0, 3 typeof S0, P0 # P0 is an "Integer" box P1, "hello" typeof S0, P1 # P1 is a "String" box P2, 3.14 typeof S0, P2 # P2 is a "Number"
Math Operations
PASM has a full set of math instructions. These work with integers, floating-point numbers, and PMCs that implement the vtable methods of a numeric object. Most of the major math opcodes have two- and three-argument forms:
add I0, I1 # I0 += I1 add I10, I11, I2 # I10 = I11 + I2
The three-argument form of add
stores the sum of the last two registers in the first register. The two-argument form adds the first register to the second and stores the result back in the first register.
The source arguments can be Parrot registers or constants, but they must be compatible with the type of the destination register. Generally, "compatible" means that the source and destination have to be the same type, but there are a few exceptions:
sub P0, P1, 2 # P0 = P1 - 2 sub P0, P1, 1.5 # P0 = P1 - 1.5
If the destination register is an integer register, like I0
, the other arguments must be integer registers or integer constants. A floating-point destination, like N0
, usually requires floating-point arguments, but many math opcodes also allow the final argument to be an integer. Opcodes with a PMC destination register may take an integer, floating-point, or PMC final argument:
mul P0, P1 # P0 *= P1 mul P0, I1 mul P0, N1 mul P0, P1, P2 # P0 = P1 * P2 mul P0, P1, I2 mul P0, P1, N2
Operations on a PMC are implemented by the vtable method of the destination (in the two-argument form) or the left source argument (in the three argument form). The result of an operation is entirely determined by the PMC. A class implementing imaginary number operations might return an imaginary number, for example.
We won't list every math opcode here, but we'll list some of the most common ones. You can get a complete list in "PASM Opcodes" in Chapter 11.
Unary math opcodes
The unary opcodes have either a destination argument and a source argument, or a single argument as destination and source. Some of the most common unary math opcodes are inc
(increment), dec
(decrement), abs
(absolute value), neg
(negate), and fact
(factorial):
abs N0, -5.0 # the absolute value of -5.0 is 5.0 fact I1, 5 # the factorial of 5 is 120 inc I1 # 120 incremented by 1 is 121
Binary math opcodes
Binary opcodes have two source arguments and a destination argument. As we mentioned before, most binary math opcodes have a two-argument form in which the first argument is both a source and the destination. Parrot provides add
(addition), sub
(subtraction), mul
(multiplication), div
(division), and pow
(exponent) opcodes, as well as two different modulus operations. mod
is Parrot's implementation of modulus, and cmod
is the %
operator from the C library. It also provides gcd
(greatest common divisor) and lcm
(least common multiple).
div I0, 12, 5 # I0 = 12 / 5 mod I0, 12, 5 # I0 = 12 % 5
Floating-point operations
Although most of the math operations work with both floating-point numbers and integers, a few require floating-point destination registers. Among these are ln
(natural log), log2
(log base 2), log10
(log base 10), and exp
(ex), as well as a full set of trigonometric opcodes such as sin
(sine), cos
(cosine), tan
(tangent), sec
(secant), cosh
(hyperbolic cosine), tanh
(hyperbolic tangent), sech
(hyperbolic secant), asin
(arc sine), acos
(arc cosine), atan
(arc tangent), asec
(arc secant), exsec
(exsecant), hav
(haversine), and vers
(versine). All angle arguments for the trigonometric functions are in radians:
sin N1, N0 exp N1, 2
The majority of the floating-point operations have a single source argument and a single destination argument. Even though the destination must be a floating-point register, the source can be either an integer or floating-point number.
The atan
opcode also has a three-argument variant that implements C's atan2()
:
atan N0, 1, 1
Working with Strings
String operations work with string registers and with PMCs that implement a string class. String operations on PMC registers require all their string arguments to be String PMCs.
Concatenating strings
Use the concat
opcode to concatenate strings. With string register or string constant arguments, concat
has both a two-argument and a three-argument form. The first argument is a source and a destination in the two-argument form:
set S0, "ab" concat S0, "cd" # S0 has "cd" appended print S0 # prints "abcd" print "\n" concat S1, S0, "xy" # S1 is the string S0 with "xy" appended print S1 # prints "abcdxy" print "\n" end
The first concat
concatenates the string "cd" onto the string "ab" in S0
. It generates a new string "abcd" and changes S0
to point to the new string. The second concat
concatenates "xy" onto the string "abcd" in S0
and stores the new string in S1
.
For PMC registers, concat
has only a three-argument form with separate registers for source and destination:
new P0, "String" new P1, "String" new P2, "String" set P0, "ab" set P1, "cd" concat P2, P0, P1 print P2 # prints abcd print "\n" end
Here, concat
concatenates the strings in P0
and P1
and stores the result in P2
.
Repeating strings
The repeat
opcode repeats a string a certain number of times:
set S0, "x" repeat S1, S0, 5 # S1 = S0 x 5 print S1 # prints "xxxxx" print "\n" end
In this example, repeat
generates a new string with "x" repeated five times and stores a pointer to it in S1
.
Length of a string
The length
opcode returns the length of a string in characters. This won't be the same as the length in bytes for multibyte encoded strings:
set S0, "abcd" length I0, S0 # the length is 4 print I0 print "\n" end
length
doesn't have an equivalent for PMC strings.
Substrings
The simplest version of the substr
opcode takes four arguments: a destination register, a string, an offset position, and a length. It returns a substring of the original string, starting from the offset position (0 is the first character) and spanning the length:
substr S0, "abcde", 1, 2 # S0 is "bc"
This example extracts a two-character string from "abcde" at a one-character offset from the beginning of the string (starting with the second character). It generates a new string, "bc", in the destination register S0
.
When the offset position is negative, it counts backward from the end of the string. So an offset of -1 starts at the last character of the string.
substr
also has a five-argument form, where the fifth argument is a string to replace the substring. This modifies the second argument and returns the removed substring in the destination register.
set S1, "abcde" substr S0, S1, 1, 2, "XYZ" print S0 # prints "bc" print "\n" print S1 # prints "aXYZde" print "\n" end
This replaces the substring "bc" in S1
with the string "XYZ", and returns "bc" in S0
.
When the offset position in a replacing substr
is one character beyond the original string length, substr
appends the replacement string just like the concat
opcode. If the replacement string is an empty string, the characters are just removed from the original string.
When you don't need to capture the replaced string, there's an optimized version of substr
that just does a replace without returning the removed substring.
set S1, "abcde" substr S1, 1, 2, "XYZ" print S1 # prints "aXYZde" print "\n" end
The PMC versions of substr
are not yet implemented.
Chopping strings
The chopn
opcode removes characters from the end of a string. It takes two arguments: the string to modify and the count of characters to remove.
set S0, "abcde" chopn S0, 2 print S0 # prints "abc" print "\n" end
This example removes two characters from the end of S0
. If the count is negative, that many characters are kept in the string:
set S0, "abcde" chopn S0, -2 print S0 # prints "ab" print "\n" end
This keeps the first two characters in S0
and removes the rest. chopn
also has a three-argument version that stores the chopped string in a separate destination register, leaving the original string untouched:
set S0, "abcde" chopn S1, S0, 1 print S1 # prints "abcd" print "\n" end
Copying strings
The clone
opcode makes a deep copy of a string or PMC. Instead of just copying the pointer, as normal assignment would, it recursively copies the string or object underneath.
new P0, "String" set P0, "Ford" clone P1, P0 set P0, "Zaphod" print P1 # prints "Ford" end
This example creates an identical, independent clone of the PMC in P0
and puts a pointer to it in P1
. Later changes to P0
have no effect on P1
.
With simple strings, the copy created by clone
, as well as the results from substr
, are copy-on-write (COW). These are rather cheap in terms of memory usage because the new memory location is only created when the copy is assigned a new value. Cloning is rarely needed with ordinary string registers since they always create a new memory location on assignment.
Converting characters
The chr
opcode takes an integer value and returns the corresponding character as a one-character string, while the ord
opcode takes a single character string and returns the integer that represents that character in the string's encoding:
chr S0, 65 # S0 is "A" ord I0, S0 # I0 is 65
ord
has a three-argument variant that takes a character offset to select a single character from a multicharacter string. The offset must be within the length of the string:
ord I0, "ABC", 2 # I0 is 67
A negative offset counts backward from the end of the string, so -1 is the last character.
ord I0, "ABC", -1 # I0 is 67
Formatting strings
The sprintf
opcode generates a formatted string from a series of values. It takes three arguments: the destination register, a string specifying the format, and an ordered aggregate PMC (like a Array
) containing the values to be formatted. The format string and the destination register can be either strings or PMCs:
sprintf S0, S1, P2 sprintf P0, P1, P2
The format string is similar to the one for C's sprintf
function, but with some extensions for Parrot data types. Each format field in the string starts with a %
and ends with a character specifying the output format. The output format characters are listed in Table 9-1.
Each format field can be specified with several options: flags, width, precision, and size. The format flags are listed in Table 9-2.
The width is a number defining the minimum width of the output from a field. The precision is the maximum width for strings or integers, and the number of decimal places for floating-point fields. If either width or precision is an asterisk (*
), it takes its value from the next argument in the PMC.
The size modifier defines the type of the argument the field takes. The flags are listed in Table 9-3.
The values in the aggregate PMC must have a type compatible with the specified size.
Here's a short illustration of string formats:
new P2, "Array" new P0, "Int" set P0, 42 push P2, P0 new P1, "Num" set P1, 10 push P2, P1 sprintf S0, "int %#Px num %+2.3Pf\n", P2 print S0 # prints "int 0x2a num +10.000" print "\n" end
The first eight lines create a Array
with two elements: a Int
and a Num
. The format string of the sprintf
has two format fields. The first, %#Px
, takes a PMC argument from the aggregate (P
) and formats it as a hexadecimal integer (x
), with a leading 0x (#
). The second format field, %+2.3Pf
, takes a PMC argument (P
) and formats it as a floating-point number (f
), with a minimum of two whole digits and a maximum of three decimal places (2.3
) and a leading sign (+
).
The test files t/op/string.t and t/src/sprintf.t have many more examples of format strings.
Testing for substrings
The index
opcode searches for a substring within a string. If it finds the substring, it returns the position where the substring was found as a character offset from the beginning of the string. If it fails to find the substring, it returns -1:
index I0, "Beeblebrox", "eb" print I0 # prints 2 print "\n" index I0, "Beeblebrox", "Ford" print I0 # prints -1 print "\n" end
index
also has a four-argument version, where the fourth argument defines an offset position for starting the search:
index I0, "Beeblebrox", "eb", 3 print I0 # prints 5 print "\n" end
This finds the second "eb" in "Beeblebrox" instead of the first, because the search skips the first three characters in the string.
Joining strings
The join
opcode joins the elements of an array PMC into a single string. The second argument separates the individual elements of the PMC in the final string result.
new P0, "Array" push P0, "hi" push P0, 0 push P0, 1 push P0, 0 push P0, "parrot" join S0, "__", P0 print S0 # prints "hi__0__1__0__parrot" end
This example builds a Array
in P0
with the values "hi"
, 0
, 1
, 0
, and "parrot"
. It then joins those values (separated by the string "__"
) into a single string, and stores it in S0
.
Splitting strings
Splitting a string yields a new array containing the resulting substrings of the original string.
split P0, "", "abc" set P1, P0[0] print P1 # 'a' set P1, P0[2] print P1 # 'c' end
This example splits the string "abc" into individual characters and stores them in an array in P0
. It then prints out the first and third elements of the array. For now, the split pattern (the second argument to the opcode) is ignored except for a test to make sure that its length is zero.
Logical and Bitwise Operations
The logical opcodes evaluate the truth of their arguments. They're often used to make decisions on control flow. Logical operations are implemented for integers and PMCs. Numeric values are false if they're 0, and true otherwise. Strings are false if they're the empty string or a single character "0", and true otherwise. PMCs are true when their get_bool
vtable method returns a nonzero value.
The and
opcode returns the second argument if it's false and the third argument otherwise:
and I0, 0, 1 # returns 0 and I0, 1, 2 # returns 2
The or
opcode returns the second argument if it's true and the third argument otherwise:
or I0, 1, 0 # returns 1 or I0, 0, 2 # returns 2 or P0, P1, P2
Both and
and or
are short-circuiting. If they can determine what value to return from the second argument, they'll never evaluate the third. This is significant only for PMCs, as they might have side effects on evaluation.
The xor
opcode returns the second argument if it is the only true value, returns the third argument if it is the only true value, and returns false if both values are true or both are false:
xor I0, 1, 0 # returns 1 xor I0, 0, 1 # returns 1 xor I0, 1, 1 # returns 0 xor I0, 0, 0 # returns 0
The not
opcode returns a true value when the second argument is false, and a false value if the second argument is true:
not I0, I1 not P0, P1
The bitwise opcodes operate on their values a single bit at a time. band
, bor
, and bxor
return a value that is the logical AND, OR, or XOR of each bit in the source arguments. They each take a destination register and two source registers. They also have two-argument forms where the destination is also a source. bnot
is the logical NOT of each bit in a single source argument.
bnot I0, I1 band P0, P1 bor I0, I1, I2 bxor P0, P1, I2
The bitwise opcodes also have string variants for AND, OR, and XOR: bors
, bands
, and bxors
. These take string register or PMC string source arguments and perform the logical operation on each byte of the strings to produce the final string.
bors S0, S1 bands P0, P1 bors S0, S1, S2 bxors P0, P1, S2
The bitwise string opcodes only have meaningful results when they're used with simple ASCII strings because the bitwise operation is done per byte.
The logical and arithmetic shift operations shift their values by a specified number of bits:
shl I0, I1, I2 # shift I1 left by count I2 giving I0 shr I0, I1, I2 # arithmetic shift right lsr P0, P1, P2 # logical shift right
Working with PMCs
In most of the examples we've shown so far, PMCs just duplicate the functionality of integers, numbers, and strings. They wouldn't be terribly useful if that's all they did, though. PMCs offer several advanced features, each with its own set of operations.
Aggregates
PMCs can define complex types that hold multiple values. These are commonly called " aggregates." The most important feature added for aggregates is keyed access. Elements within an aggregate PMC can be stored and retrieved by a numeric or string key. PASM also offers a full set of operations for manipulating aggregate data types.
Since PASM is intended to implement Perl, the two most fully featured aggregates already in operation are arrays and hashes. Any aggregate defined for any language could take advantage of the features described here.
Arrays
The Array
PMC is an ordered aggregate with zero-based integer keys. The syntax for keyed access to a PMC puts the key in square brackets after the register name:
new P0, "Array" # obtain a new array object set P0, 2 # set its length set P0[0], 10 # set first element to 10 set P0[1], I31 # set second element to I31 set I0, P0[0] # get the first element set I1, P0 # get array length
A key on the destination register of a set
operation sets a value for that key in the aggregate. A key on the source register of a set
returns the value for that key. If you set P0
without a key, you set the length of the array, not one of its values.Array
is an autoextending array, so you never need to set its length. Other array types may require the length to be set explicitly. And if you assign the Array
to an integer, you get the length of the array.
By the time you read this, the syntax for getting and setting the length of an array may have changed. The change would separate array allocation (how much storage the array provides) from the actual element count. The currently proposed syntax uses set
to set or retrieve the allocated size of an array, and an elements
opcode to retrieve the count of elements stored in the array.
set P0, 100 # allocate store for 100 elements set I0, P0 # obtain current allocation size elements I0, P0 # get element count
Some other useful instructions for working with arrays are push
, pop
, shift
, and unshift
(you'll find them in "PASM Opcodes" in Chapter 11).
Hashes
The Hash
PMC is an unordered aggregate with string keys:
new P1, "Hash" # generate a new hash object set P1["key"], 10 # set key and value set I0, P1["key"] # obtain value for key set I1, P1 # number of entries in hash
The exists
opcode tests whether a keyed value exists in an aggregate. It returns 1 if it finds the key in the aggregate, and returns 0 if it doesn't. It doesn't care if the value itself is true or false, only that the key has been set:
new P0, "Hash" set P0["key"], 0 exists I0, P0["key"] # does a value exist at "key" print I0 # prints 1 print "\n" end
The delete
opcode is also useful for working with hashes: it removes a key/value pair.
Iterators
Iterators extract values from an aggregate PMC. You create an iterator by creating a new Iterator
PMC, and passing the array to new
as an additional parameter:
new P1, "Iterator", P2
The include file iterator.pasm defines some constants for working with iterators. The .ITERATE_FROM_START
and .ITERATE_FROM_END
constants are used to select whether an array iterator starts from the beginning or end of the array. The shift
opcode extracts values from the array. An iterator PMC is true as long as it still has values to be retrieved (tested by unless
below).
.include "iterator.pasm" new P2, "Array" push P2, "a" push P2, "b" push P2, "c" new P1, "Iterator", P2 set P1, .ITERATE_FROM_START iter_loop: unless P1, iter_end shift P5, P1 print P5 # prints "a", "b", "c" branch iter_loop iter_end: end
Hash iterators work similarly to array iterators, but they extract keys. With hashes it's only meaningful to iterate in one direction, since they don't define any order for their keys.
.include "iterator.pasm" new P2, "Hash" set P2["a"], 10 set P2["b"], 20 set P2["c"], 30 new P1, "Iterator", P2 set P1, .ITERATE_FROM_START_KEYS iter_loop: unless P1, iter_end shift S5, P1 # one of the keys "a", "b", "c" set I9, P2[S5] print I9 # prints e.g. 20, 10, 30 branch iter_loop iter_end: end
Data structures
Arrays and hashes can hold any data type, including other aggregates. Accessing elements deep within nested data structures is a common operation, so PASM provides a way to do it in a single instruction. Complex keys specify a series of nested data structures, with each individual key separated by a semicolon:
new P0, "Hash" new P1, "Array" set P1[2], 42 set P0["answer"], P1 set I1, 2 set I0, P0["answer";I1] # $i = %hash{"answer"}[2] print I0 print "\n" end
This example builds up a data structure of a hash containing an array. The complex key P0["answer";I1]
retrieves an element of the array within the hash. You can also set a value using a complex key:
set P0["answer";0], 5 # %hash{"answer"}[0] = 5
The individual keys are integers or strings, or registers with integer or string values.
PMC Assignment
We mentioned before that set
on two PMCs simply aliases them both to the same object, and that clone
creates a complete duplicate object. But if you just want to assign the value of one PMC to another PMC, you need the assign
opcode:
new P0, "Int" new P1, "Int" set P0, 42 set P2, P0 assign P1, P0 # note: P1 has to exist already inc P0 print P0 # prints 43 print "\n" print P1 # prints 42 print "\n" print P2 # prints 43 print "\n" end
This example creates two Int
PMCs: P0
and P1
. It gives P0
a value of 42. It then uses set
to give the same value to P2
, but uses assign
to give the value to P1
. When P0
is incremented, P2
also changes, but P1
doesn't. The destination register for assign
must have an existing object of the right type in it, since assign
doesn't create a new object (as with clone
) or reuse the source object (as with set
).
Properties
PMCs can have additional values attached to them as "properties" of the PMC. What these properties do is entirely up to the language being implemented. Perl 6 uses them to store extra information about a variable: whether it's a constant, if it should always be interpreted as a true value, etc.
The setprop
opcode sets the value of a named property on a PMC. It takes three arguments: the PMC to be set with a property, the name of the property, and a PMC containing the value of the property. The getprop
opcode returns the value of a property. It also takes three arguments: the PMC to store the property's value, the name of the property, and the PMC from which the property value is to be retrieved:
new P0, "String" set P0, "Zaphod" new P1, "Int" set P1, 1 setprop P0, "constant", P1 # set a property on P0 getprop P3, "constant", P0 # retrieve a property on P0 print P3 # prints 1 print "\n" end
This example creates a String
object in P0
, and a Int
object with the value 1 in P1
. setprop
sets a property named "constant" on the object in P0
and gives the property the value in P1
.The "constant" property is ignored by PASM, but is significant to the Perl 6 code running on top of it. getprop
retrieves the value of the property "constant" on P0
and stores it in P3
.
Properties are kept in a separate hash for each PMC. Property values are always PMCs, but only references to the actual PMCs. Trying to fetch the value of a property that doesn't exist returns a Undef
.
delprop
deletes a property from a PMC.
delprop P1, "constant" # delete property
You can also return a complete hash of all properties on a PMC with prophash
.
prophash P0, P1 # set P0 to the property hash of P1
Flow Control
Although it has many advanced features, at heart PASM is an assembly language. All flow control in PASM--as in most assembly languages--is done with branches and jumps.
Branch instructions transfer control to a relative offset from the current instruction. The rightmost argument to every branch opcode is a label, which the assembler converts to the integer value of the offset. You can also branch on a literal integer value, but there's rarely any need to do so. The simplest branch instruction is branch
:
branch L1 # branch 4 print "skipped\n" L1: print "after branch\n" end
This example unconditionally branches to the location of the label L1
, skipping over the first print
statement.
Jump instructions transfer control to an absolute address. The jump
opcode doesn't calculate an address from a label, so it's used together with set_addr
:
set_addr I0, L1 jump I0 print "skipped\n" end L1: print "after jump\n" end
The set_addr
opcode takes a label or an integer offset and returns an absolute address.
You've probably noticed the end
opcode as the last statement in many examples above. This terminates the execution of the current run loop. Terminating the main bytecode segment (the first run loop) stops the interpreter. Without the end
statement, execution just falls off the end of the bytecode segment, with a good chance of crashing the interpreter.
Conditional Branches
Unconditional jumps and branches aren't really enough for flow control. What you need to implement the control structures of high-level languages is the ability to select different actions based on a set of conditions. PASM has opcodes that conditionally branch based on the truth of a single value or the comparison of two values. The following example has if
and unless
conditional branches:
set I0, 0 if I0, TRUE unless I0, FALSE print "skipped\n" end TRUE: print "shouldn't happen\n" end FALSE: print "the value was false\n" end
if
branches if its first argument is a true value, and unless
branches if its first argument is a false value. In this case, the if
doesn't branch because I0
is false, but the unless
does branch. The comparison branching opcodes compare two values and branch if the stated relation holds true. These are eq
(branch when equal), ne
(when not equal), lt
(when less than), gt
(when greater than), le
(when less than or equal), and ge
(when greater than or equal). The two compared arguments must be the same register type:
set I0, 4 set I1, 4 eq I0, I1, EQUAL print "skipped\n" end EQUAL: print "the two values are equal\n" end
This compares two integers, I0
and I1
, and branches if they're equal. Strings of different character sets or encodings are converted to Unicode before they're compared. PMCs have a cmp
vtable method. This gets called on the left argument to perform the comparison of the two objects.
The comparison opcodes don't specify if a numeric or string comparison is intended. The type of the register selects for integers, floats, and strings. With PMCs, the vtable method cmp
or is_equal
of the first argument is responsible for comparing the PMC meaningfully with the other operand. If you need to force a numeric or string comparison on two PMCs, use the alternate comparison opcodes that end in the _num
and _str
suffixes.
eq_str P0, P1, label # always a string compare gt_num P0, P1, label # always numerically
Finally, the eq_addr
opcode branches if two PMCs or strings are actually the same object (have the same address):
eq_addr P0, P1, same_pmcs_found
Iteration
PASM doesn't define high-level loop constructs. These are built up from a combination of conditional and unconditional branches. A do-while style loop can be constructed with a single conditional branch:
set I0, 0 set I1, 10 REDO: inc I0 print I0 print "\n" lt I0, I1, REDO end
This example prints out the numbers 1 to 10. The first time through, it executes all statements up to the lt
statement. If the condition evaluates as true (I0
is less than I1
) it branches to the REDO
label and runs the three statements in the loop body again. The loop ends when the condition evaluates as false.
Conditional and unconditional branches can build up quite complex looping constructs, as follows:
# loop ($i=1; $i<=10; $i++) { # print "$i\n"; # } loop_init: set I0, 1 branch loop_test loop_body: print I0 print "\n" branch loop_continue loop_test: le I0, 10, loop_body branch out loop_continue: inc I0 branch loop_test out: end
This example emulates a counter-controlled loop like Perl 6's loop
keyword or C's for
. The first time through the loop it sets the initial value of the counter in loop_init
, tests that the loop condition is met in loop_test
, and then executes the body of the loop in loop_body
. If the test fails on the first iteration, the loop body will never execute. The end of loop_body
branches to loop_continue
, which increments the counter and then goes to loop_test
again. The loop ends when the condition fails, and it branches to out
. The example is more complex than it needs to be just to count to 10, but it nicely shows the major components of a loop.
Lexicals and Globals
So far, we've been treating Parrot registers like the variables of a high-level language. This is fine, as far as it goes, but it isn't the full picture. The dynamic nature and introspective features of languages like Perl make it desirable to manipulate variables by name, instead of just by register or stack location. These languages also have global variables, which are visible throughout the entire program. Storing a global variable in a register would either tie up that register for the lifetime of the program or require some unwieldy way to shuffle the data into and out of registers.
Parrot provides structures for storing both global and lexically scoped named variables. Lexical and global variables must be PMC values. PASM provides instructions for storing and retrieving variables from these structures so the PASM opcodes can operate on their values.
Globals
Global variables are stored in a Hash
, so every variable name must be unique. PASM has two opcodes for globals, set_global
and get_global
:
new P10, "Int" set P10, 42 set_global "$foo", P10 # ... get_global P0, "$foo" print P0 # prints 42 end
The first two statements create a Int
in the PMC register P10
and give it the value 42. In the third statement, set_global
stores that PMC as the named global variable $foo
. At some later point in the program, get_global
retrieves the PMC from the global variable by name, and stores it in P0
so it can be printed.
The set_global
opcode only stores a reference to the object. If we add an increment statement:
inc P10
after the set_global
it increments the stored global, printing 43. If that's not what you want, you can clone
the PMC before you store it. Leaving the global variable as an alias does have advantages, though. If you retrieve a stored global into a register and modify it as follows:
get_global P0, "varname" inc P0
the value of the stored global is directly modified, so you don't need to call set_global
again.
The two-argument forms of set_global
and get_global
store or retrieve globals from the outermost namespace (what Perl users will know as the "main" namespace). A simple flat global namespace isn't enough for most languages, so Parrot also needs to support hierarchical namespaces for separating packages (classes and modules in Perl 6). Use set_rootglobal
and get_root_global
add an argument to select a nested namespace:
set_root_global ["Foo"], "var", P0 # store P0 as var in the Foo namespace get_root_global P1, ["Foo"], "var" # get Foo::var
Eventually the global opcodes will have variants that take a PMC to specify the namespace, but the design and implementation of these aren't finished yet.
Lexicals
Lexical variables are stored in a lexical scratchpad. There's one pad for each lexical scope. Every pad has both a hash and an array, so elements can be stored either by name or by numeric index.
Basic instructions
To store a lexical variable in the current scope pad, use store_lex
. Likewise, use find_lex
to retrieve a variable from the current pad.
new P0, "Int" # create a variable set P0, 10 # assign value to it store_lex "foo", P0 # store the var with the variable name "foo" # ... find_lex P1, "foo" # get the var "foo" into P1 print P1 print "\n" # prints 10 end
Subroutines
Subroutines and methods are the basic building blocks of larger programs. At the heart of every subroutine call are two fundamental actions: it has to store the current location so it can come back to it, and it has to transfer control to the subroutine. The bsr
opcode does both. It pushes the address of the next instruction onto the control stack, and then branches to a label that marks the subroutine:
print "in main\n" bsr _sub print "and back\n" end _sub: print "in sub\n" ret
At the end of the subroutine, the ret
instruction pops a location back off the control stack and goes there, returning control to the caller. The jsr
opcode pushes the current location onto the call stack and jumps to a subroutine. Just like the jump
opcode, it takes an absolute address in an integer register, so the address has to be calculated first with the set_addr
opcode:
print "in main\n" set_addr I0, _sub jsr I0 print "and back\n" end _sub: print "in sub\n" ret
Calling Conventions
A bsr
or jsr
is fine for a simple subroutine call, but few subroutines are quite that simple. Who is responsible for saving and restoring the registers, however, so that they don't get overwritten when we perform a c<bsr> or jsr
? How are arguments passed? Where are the subroutine's return values stored? A number of different answers are possible. You've seen how many ways Parrot has of storing values. The critical point is that the caller and the called subroutine have to agree on all the answers.
Parrot calling conventions
Internal subroutines can use whatever calling convention serves them best. Externally visible subroutines and methods need stricter rules. Since these routines may be called as part of an included library or module and even from a different high level language, it's important to have a consistent interface.
The .sub
directive defines globally accessible subroutine objects.
Subroutine objects of all kinds can be called with the invoke
opcode. There is also an invoke
Px
instruction for calling objects held in a different register.
The invokecc
opcode is like invoke
, but it also creates and stores a new return continuation. When the called subroutine invokes this return continuation, it returns control to the instruction after the function call. This kind of call is known as Continuation Passing Style (CPS).
Native Call Interface
A special version of the Parrot calling conventions are used by the Native Call Interface (NCI) for calling subroutines with a known prototype in shared libraries. This is not really portable across all libraries, but it's worth a short example. This is a simplified version of the first test in t/pmc/nci.t:
loadlib P1, "libnci_test" # get library object for a shared lib print "loaded\n" dlfunc P0, P1, "nci_dd", "dd" # obtain the function object print "dlfunced\n" set I0, 1 # prototype used - unchecked set_args "0", 4.0 # set the argument get_results "0", N5 # prepare to store the return value invokecc P0 # call nci_dd ne N5, 8.0, nok_1 # the test functions returns 2*arg print "ok 1\n" end nok_1: #...
This example shows two new instructions: loadlib
and dlfunc
. The loadlib
opcode obtains a handle for a shared library. It searches for the shared library in the current directory, in runtime/parrot/dynext, and in a few other configured directories. It also tries to load the provided filename unaltered and with appended extensions like .so
or .dll
. Which extensions it tries depends on the OS Parrot is running on.
The dlfunc
opcode gets a function object from a previously loaded library (second argument) of a specified name (third argument) with a known function signature (fourth argument). The function signature is a string where the first character is the return value and the rest of the parameters are the function parameters. The characters used in NCI function signatures are listed in Table 9-5.
For more information on callback functions, read the documentation in docs/pdds/pdd16_native_call.pod and docs/pmc/struct.pod.
Coroutines
As we mentioned in the previous chapter, coroutines are subroutines that can suspend themselves and return control to the caller--and then pick up where they left off the next time they're called, as if they never left.
In PASM, coroutines are subroutine-like objects:
newsub P0, .Coroutine, _co_entry
The Coroutine
object has its own user stack, register frame stacks, control stack, and pad stack. The pad stack is inherited from the caller. The coroutine's control stack has the caller's control stack prepended, but is still distinct. When the coroutine invokes itself, it returns to the caller and restores the caller's context (basically swapping all stacks). The next time the coroutine is invoked, it continues to execute from the point at which it previously returned:
new_pad 0 # push a new lexical pad on stack new P0, "Int" # save one variable in it set P0, 10 store_lex -1, "var", P0 newsub P0, .Coroutine, _cor # make a new coroutine object saveall # preserve environment invoke # invoke the coroutine restoreall print "back\n" saveall invoke # invoke coroutine again restoreall print "done\n" pop_pad end _cor: find_lex P1, "var" # inherited pad from caller print "in cor " print P1 print "\n" inc P1 # var++ saveall invoke # yield( ) restoreall print "again " branch _cor # next invocation of the coroutine
This prints out the result:
in cor 10 back again in cor 11 done
The invoke
inside the coroutine is commonly referred to as yield. The coroutine never ends. When it reaches the bottom, it branches back up to _cor
and executes until it hits invoke
again.
The interesting part about this example is that the coroutine yields in the same way that a subroutine is called. This means that the coroutine has to preserve its own register values. This example uses saveall
but it could have only stored the registers the coroutine actually used. Saving off the registers like this works because coroutines have their own register frame stacks.
Continuations
A continuation is a subroutine that gets a complete copy of the caller's context, including its own copy of the call stack. Invoking a continuation starts or restarts it at the entry point:
new P1, "Int" set P1, 5 newsub P0, .Continuation, _con _con: print "in cont " print P1 print "\n" dec P1 unless P1, done invoke # P0 done: print "done\n" end
This prints:
in cont 5 in cont 4 in cont 3 in cont 2 in cont 1 done
Evaluating a Code String
This isn't really a subroutine operation, but it does produce a code object that can be invoked. In this case, it's a bytecode segment object.
The first step is to get an assembler or compiler for the target language:
compreg P1, "PASM"
Within the Parrot interpreter there are currently three registered languages: PASM
, PIR
, and PASM1
. The first two are for parrot assembly language and parrot intermediate representation code. The third is for evaluating single statements in PASM. Parrot automatically adds an end
opcode at the end of PASM1
strings before they're compiled.
This example places a bytecode segment object into the destination register P0
and then invokes it with invoke
:
compreg P1, "PASM1" # get compiler set S1, "in eval\n" compile P0, P1, "print S1" invoke # eval code P0 print "back again\n" end
You can register a compiler or assembler for any language inside the Parrot core and use it to compile and invoke code from that language. These compilers may be written in PASM or reside in shared libraries.
compreg "MyLanguage", P10
In this example the compreg
opcode registers the subroutine-like object P10
as a compiler for the language "MyLanguage". See examples/compilers and examples/japh/japh16.pasm for an external compiler in a shared library.
Exceptions and Exception Handlers
Exceptions provide a way of calling a piece of code outside the normal flow of control. They are mainly used for error reporting or cleanup tasks, but sometimes exceptions are just a funny way to branch from one code location to another one. The design and implementation of exceptions in Parrot isn't complete yet, but this section will give you an idea where we're headed.
Exceptions are objects that hold all the information needed to handle the exception: the error message, the severity and type of the error, etc. The class of an exception object indicates the kind of exception it is.
Exception handlers are derived from continuations. They are ordinary subroutines that follow the Parrot calling conventions, but are never explicitly called from within user code. User code pushes an exception handler onto the control stack with the set_eh
opcode. The system calls the installed exception handler only when an exception is thrown (perhaps because of code that does division by zero or attempts to retrieve a global that wasn't stored.)
newsub P20, .ExceptionHandler, _handler set_eh P20 # push handler on control stack null P10 # set register to null get_global P10, "none" # may throw exception clear_eh # pop the handler off the stack #... _handler: # if not, execution continues here is_null P10, not_found # test P10 #...
This example creates a new exception handler subroutine with the newsub
opcode and installs it on the control stack with the set_eh
opcode. It sets the P10
register to a null value (so it can be checked later) and attempts to retrieve the global variable named none
. If the global variable is found, the next statement (clear_eh
) pops the exception handler off the control stack and normal execution continues. If the get_global
call doesn't find none
it throws an exception by pushing an exception object onto the control stack. When Parrot sees that it has an exception, it pops it off the control stack and calls the exception handler _handler
.
The first exception handler in the control stack sees every exception thrown. The handler has to examine the exception object and decide whether it can handle it (or discard it) or whether it should rethrow
the exception to pass it along to an exception handler deeper in the stack. The rethrow
opcode is only valid in exception handlers. It pushes the exception object back onto the control stack so Parrot knows to search for the next exception handler in the stack. The process continues until some exception handler deals with the exception and returns normally, or until there are no more exception handlers on the control stack. When the system finds no installed exception handlers it defaults to a final action, which normally means it prints an appropriate message and terminates the program.
When the system installs an exception handler, it creates a return continuation with a snapshot of the current interpreter context. If the exception handler just returns (that is, if the exception is cleanly caught) the return continuation restores the control stack back to its state when the exception handler was called, cleaning up the exception handler and any other changes that were made in the process of handling the exception.
Exceptions thrown by standard Parrot opcodes (like the one thrown by get_global
above or by the throw
opcode) are always resumable, so when the exception handler function returns normally it continues execution at the opcode immediately after the one that threw the exception. Other exceptions at the run-loop level are also generally resumable.
new P10, 'Exception' # create new Exception object set P10, 'I die' # set message attribute throw P10 # throw it
Exceptions are designed to work with the Parrot calling conventions. Since the return addresses of bsr
subroutine calls and exception handlers are both pushed onto the control stack, it's generally a bad idea to combine the two.
Events
An event is a notification that something has happened: a timer expired, an IO operation finished, a thread sent a message to another thread, or the user pressed Ctrl-C
to interrupt program execution.
What all of these events have in common is that they arrive asynchronously. It's generally not safe to interrupt program flow at an arbitrary point and continue at a different position, so the event is placed in the interpreter's task queue. The run loops code regularly checks whether an event needs to be handled. Event handlers may be an internal piece of code or a user-defined event handler subroutine.
Events are still experimental in Parrot, so the implementation and design is subject to change.
Timers
Timer
objects are the replacement for Perl 5's alarm
handlers. They are also a significant improvement. Timers can fire once or repeatedly, and multiple timers can run independently. The precision of a timer is limited by the OS Parrot runs on, but it is always more fine-grained then a whole second. The final syntax isn't yet fixed, so please consult the documentation for examples.
Signals
Signal handling is related to events. When Parrot gets a signal it needs to handle from the OS, it converts that signal into an event and broadcasts it to all running threads. Each thread independently decides if it's interested in this signal and, if so, how to respond to it.
newsub P20, .ExceptionHandler, _handler set_eh P20 # establish signal handler print "send SIGINT:\n" sleep 2 # press ^C after you saw start print "no SIGINT\n" end _handler: .include "signal.pasm" # get signal definitions print "caught " set I0, P5["type"] # if _type is negative, the ... neg I0, I0 # ... negated type is the signal ne I0, .SIGINT, nok print "SIGINT\n" nok: end
This example creates a signal handler and pushes it on to the control stack. It then prompts the user to send a SIGINT
from the shell (this is usually Ctrl-C
, but it varies in different shells), and waits for 2 seconds. If the user doesn't send a SIGINT in 2 seconds the example just prints "no SIGINT" and ends. If the user does send a SIGINT, the signal handler catches it, prints out "caught SIGINT" and ends.Currently, only Linux installs a SIGINT
sigaction
handler, so this example won't work on other platforms.
Threads
Threads allow multiple pieces of code to run in parallel. This is useful when you have multiple physical CPUs to share the load of running individual threads. With a single processor, threads still provide the feeling of parallelism, but without any improvement in execution time. Even worse, sometimes using threads on a single processor will actually slow down your program.
Still, many algorithms can be expressed more easily in terms of parallel running pieces of code and many applications profit from taking advantage of multiple CPUs. Threads can vastly simplify asynchronous programs like internet servers: a thread splits off, waits for some IO to happen, handles it, and relinquishes the processor again when it's done.
Parrot compiles in thread support by default (at least, if the platform provides some kind of support for it). Unlike Perl 5, compiling with threading support doesn't impose any execution time penalty for a non-threaded program. Like exceptions and events, threads are still under development, so you can expect significant changes in the near future.
As outlined in the previous chapter, Parrot implements three different threading models. (Note: As of version 1.0, the TQueue
PMC will be deprecated, rendering the following discussion obsolete.) The following example uses the third model, which takes advantage of shared data. It uses a TQueue
(thread-safe queue) object to synchronize the two parallel running threads. This is only a simple example to illustrate threads, not a typical usage of threads (no-one really wants to spawn two threads just to print out a simple string).
get_global P5, "_th1" # locate thread function new P2, "ParrotThread" # create a new thread find_method P0, P2, "thread3" # a shared thread's entry new P7, "TQueue" # create a Queue object new P8, "Int" # and a Int push P7, P8 # push the Int onto queue new P6, "String" # create new string set P6, "Js nte artHce\n" set I3, 3 # thread function gets 3 args invoke # _th1.run(P5,P6,P7) new P2, "ParrotThread" # same for a second thread get_global P5, "_th2" set P6, "utaohrPro akr" # set string to 2nd thread's invoke # ... data, run 2nd thread too end # Parrot joins both .pcc_sub _th1: # 1st thread function w1: sleep 0.001 # wait a bit and schedule defined I1, P7 # check if queue entry is ... unless I1, w1 # ... defined, yes: it's ours set S5, P6 # get string param substr S0, S5, I0, 1 # extract next char print S0 # and print it inc I0 # increment char pointer shift P8, P7 # pull item off from queue if S0, w1 # then wait again, if todo invoke P1 # done with string .pcc_sub _th2: # 2nd thread function w2: sleep 0.001 defined I1, P7 # if queue entry is defined if I1, w2 # then wait set S5, P6 substr S0, S5, I0, 1 # if not print next char print S0 inc I0 new P8, "Int" # and put a defined entry push P7, P8 # onto the queue so that if S0, w2 # the other thread will run invoke P1 # done with string
This example creates a ParrotThread
object and calls its thread3
method, passing three arguments: a PMC for the _th1
subroutine in P5
, a string argument in P6
, and a TQueue
object in P7
containing a single integer. Remember from the earlier section "Parrot calling conventions" that registers 5-15 hold the arguments for a subroutine or method call and I3
stores the number of arguments. The thread object is passed in P2
.
This call to the thread3
method spawns a new thread to run the _th1
subroutine. The main body of the code then creates a second ParrotThread
object in P2
, stores a different subroutine in P5
, sets P6
to a new string value, and then calls the thread3
method again, passing it the same TQueue
object as the first thread. This method call spawns a second thread. The main body of code then ends, leaving the two threads to do the work.
At this point the two threads have already started running. The first thread (_th1
) starts off by sleeping for a 1000th of a second. It then checks if the TQueue
object contains a value. Since it contains a value when the thread is first called, it goes ahead and runs the body of the subroutine. The first thing this does is shift the element off the TQueue
. It then pulls one character off a copy of the string parameter using substr
, prints the character, increments the current position (I0
) in the string, and loops back to the w1
label and sleeps. Since the queue doesn't have any elements now, the subroutine keeps sleeping.
Meanwhile, the second thread (_th2
) also starts off by sleeping for a 1000th of a second. It checks if the shared TQueue
object contains a defined value but unlike the first thread it only continues sleeping if the queue does contain a value. Since the queue contains a value when the second thread is first called, the subroutine loops back to the w2
label and continues sleeping. It keeps sleeping until the first thread shifts the integer off the queue, then runs the body of the subroutine. The body pulls one character off a copy of the string parameter using substr
, prints the character, and increments the current position in the string. It then creates a new Int
, pushes it onto the shared queue, and loops back to the w2
label again to sleep. The queue has an element now, so the second thread keeps sleeping, but the first thread runs through its loop again.
The two threads alternate like this, printing a character and marking the queue so the next thread can run, until there are no more characters in either string. At the end, each subroutine invokes the return continuation in P1
which terminates the thread. The interpreter waits for all threads to terminate in the cleanup phase after the end
in the main body of code.
The final printed result (as you might have guessed) is:
Just another Parrot Hacker
The syntax for threads isn't carved in stone and the implementation still isn't finished but as this example shows, threads are working now and already useful.
Several methods are useful when working with threads. The join
method belongs to the ParrotThread
class. When it's called on a ParrotThread
object, the calling code waits until the thread terminates.
new P2, "ParrotThread" # create a new thread set I5, P2 # get thread ID find_method P0, P2, "join" # get the join method... invoke # ...and join (wait for) the thread set P16, P5 # the return result of the thread
kill
and detach
are interpreter methods, so you have to grab the current interpreter object before you can look up the method object.
set I5, P2 # get thread ID of thread P2 getinterp P3 # get this interpreter object find_method P0, P3, "kill" # get kill method invoke # kill thread with ID I5 find_method P0, P3, "detach" invoke # detach thread with ID I5
By the time you read this, some of these combinations of statements and much of the threading syntax above may be reduced to a simpler set of opcodes.
Loading Bytecode
In addition to running Parrot bytecode on the command-line, you can also load pre-compiled bytecode directly into your PASM source file. The load_bytecode
opcode takes a single argument: the name of the bytecode file to load. So, if you create a file named file.pasm containing a single subroutine:
# file.pasm .sub _sub2: # .sub stores a global sub print "in sub2\n" invoke P1
and compile it to bytecode using the -o
command-line switch:
$ parrot -o file.pbc file.pasm
You can then load the compiled bytecode into main.pasm and directly call the subroutine defined in file.pasm:
# main.pasm main: load_bytecode "file.pbc" # compiled file.pasm get_global P0, "_sub2" invokecc end
The load_bytecode
opcode also works with source files, as long as Parrot has a compiler registered for that type of file:
# main2.pasm main: load_bytecode "file.pasm" # PASM source code set_global P0, "_sub2" invokecc end
Subroutines marked with :load
run as soon as they're loaded (before load_bytecode
returns), rather than waiting to be called. A subroutine marked with :main
will always run first, no matter what name you give it or where you define it in the file.
# file3.pasm .sub :load # mark the sub as to be run print "file3\n" invoke P1 # return # main3.pasm first: # first is never invoked print "never\n" invoke P1 .sub :main # because _main is marked as the print "main\n" # MAIN entry of program execution load_bytecode "file3.pasm" print "back\n" end
This example uses both :load
and :main
. Because the main
subroutine is defined with :main
it will execute first even though another subroutine comes before it in the file. main
prints a line, loads the PASM source file, and then prints another line. Because _entry
in file3.pasm is marked with :load
it runs before load_bytecode
returns, so the final output is:
main file3 back
Classes and Objects
This section revolves around one complete example that defines a class, instantiates objects, and uses them. The whole example is included at the end of the section.
Class declaration
The newclass
opcode defines a new class. It takes two arguments, the name of the class and the destination register for the class PMC. All classes (and objects) inherit from the ParrotClass
PMC, which is the core of the Parrot object system.
newclass P1, "Foo"
To instantiate a new object of a particular class, you first look up the integer value for the class type with the find_type
opcode, then create an object of that type with the new
opcode:
find_type I1, "Foo" new P3I I1
The new
opcode also checks to see if the class defines a method named "__init" and calls it if it exists.
Attributes
The addattribute
opcode creates a slot in the class for an attribute (sometimes known as an instance variable) and associates it with a name:
addattribute P1, ".i" # Foo.i
This chunk of code from the __init
method looks up the position of the first attribute, creates a Int
PMC, and stores it as the first attribute:
classoffset I0, P2, "Foo" # first "Foo" attribute of object P2 new P6, "Int" # create storage for the attribute setattribute P2, I0, P6 # store the first attribute
The classoffset
opcode takes a PMC containing an object and the name of its class, and returns an integer index for the position of the first attribute. The setattribute
opcode uses the integer index to store a PMC value in one of the object's attribute slots. This example initializes the first attribute. The second attribute would be at I0 + 1
, the third attribute at I0 + 2
, etc:
inc I0 setattribute P2, I0, P7 # store next attribute #...
There is also support for named parameters with fully qualified parameter names (although this is a little bit slower than getting the class offset once and accessing several attributes by index):
new P6, "Int" setattribute P2, "Foo\x0.i", P6 # store the attribute
You use the same integer index to retrieve the value of an attribute. The getattribute
opcode takes an object and an index as arguments and returns the attribute PMC at that position:
classoffset I0, P2, "Foo" # first "Foo" attribute of object P2 getattribute P10, P2, I0 # indexed get of attribute
or
getattribute P10, P2, "Foo\x0.i" # named get
To set the value of an attribute PMC, first retrieve it with getattribute
and then assign to the returned PMC. Because PMC registers are only pointers to values, you don't need to store the PMC again after you modify its value:
getattribute P10, P2, I0 set P10, I5
Methods
Methods in PASM are just subroutines installed in the namespace of the class. You define a method with the .pcc_sub
directive before the label:
.pcc_sub _half: # I5 = self."_half"() classoffset I0, P2, "Foo" getattribute P10, P2, I0 set I5, P10 # get value div I5, 2 invoke P1
This routine returns half of the value of the first attribute of the object. Method calls use the Parrot calling conventions so they always pass the invocant object (often called self) in P2
. Invoking the return continuation in P1
returns control to the caller.
The .pcc_sub
directive automatically stores the subroutine as a global in the current namespace. The .namespace
directive sets the current namespace:
.namespace [ "Foo" ]
If the namespace is explicitly set to an empty string or key, then the subroutine is stored in the outermost namespace.
The callmethodcc
opcode makes a method call. It follows the Parrot calling conventions, so it expects to find the invocant object in P2
, the method object in P0
, etc. It adds one bit of magic, though. If you pass the name of the method in S0
, callmethodcc
looks up that method name in the invocant object and stores the method object in P0
for you:
set S0, "_half" # set method name set P2, P3 # the object callmethodcc # create return continuation, call print I5 # result of method call print "\n"
The callmethodcc
opcode also generates a return continuation and stores it in P1
. The callmethod
opcode doesn't generate a return continuation, but is otherwise identical to callmethodcc
. Just like ordinary subroutine calls, you have to preserve and restore any registers you want to keep after a method call. Whether you store individual registers, register frames, or half register frames is up to you.
Overriding vtable functions
Every object inherits a default set of vtable functions from the ParrotObject
PMC, but you can also override them with your own methods. The vtable functions have predefined names that start with a double underscore "__". The following code defines a method named __init
in the Foo
class that initializes the first attribute of the object with an integer:
.sub __init: classoffset I0, P2, "Foo" # lookup first attribute position new P6, "Int" # create storage for the attribute setattribute P2, I0, P6 # store the first attribute invoke P1 # return
Ordinary methods have to be called explicitly, but the vtable functions are called implicitly in many different contexts. Parrot saves and restores registers for you in these calls. The __init
method is called whenever a new object is constructed:
find_type I1, "Foo" new P3, I1 # call __init if it exists
A few other vtable functions in the complete code example for this section are __set_integer_native
, __add
, __get_integer
, __get_string
, and __increment
. The set
opcode calls Foo's __set_integer_native
vtable function when its destination register is a Foo
object and the source register is a native integer:
set P3, 30 # call __set_integer_native method
The add
opcode calls Foo's __add
vtable function when it adds two Foo
objects:
new P4, I1 # same with P4 set P4, 12 new P5, I1 # create a new store for add add P5, P3, P4 # __add method
The inc
opcode calls Foo's __increment
vtable function when it increments a Foo
object:
inc P3 # __increment
Foo's __get_integer
and __get_string
vtable functions are called whenever an integer or string value is retrieved from a Foo
object:
set I10, P5 # __get_integer #... print P5 # calls __get_string, prints 'fortytwo'
Inheritance
The subclass
opcode creates a new class that inherits methods and attributes from another class. It takes 3 arguments: the destination register for the new class, a register containing the parent class, and the name of the new class:
subclass P3, P1, "Bar"
For multiple inheritance, the addparent
opcode adds additional parents to a subclass.
newclass P4, "Baz" addparent P3, P4
To override an inherited method, define a method with the same name in the namespace of the subclass. The following code overrides Bar's __increment
method so it decrements the value instead of incrementing it:
.namespace [ "Bar" ] .sub __increment: classoffset I0, P2, "Foo" # get Foo's attribute slot offset getattribute P10, P2, I0 # get the first Foo attribute dec P10 # the evil line invoke P1
Notice that the attribute inherited from Foo
can only be looked up with the Foo
class name, not the Bar
class name. This preserves the distinction between attributes that belong to the class and inherited attributes.
Object creation for subclasses is the same as for ordinary classes:
find_type I1, "Bar" new P5, I1
Calls to inherited methods are just like calls to methods defined in the class:
set P5, 42 # inherited __set_integer_native inc P5 # overridden __increment print P5 # prints 41 as Bar's __increment decrements print "\n" set S0, "_half" # set method name set P2, P5 # the object callmethodcc # create return continuation, call print I5 print "\n"
Additional Object Opcodes
The isa
and can
opcodes are also useful when working with objects. isa
checks whether an object belongs to or inherits from a particular class. can
checks whether an object has a particular method. Both return a true or false value.
isa I0, P3, "Foo" # 1 isa I0, P3, "Bar" # 1 can I0, P3, "__add" # 1
Complete Example
newclass P1, "Foo" addattribute P1, "$.i" # Foo.i find_type I1, "Foo" new P3, I1 # call __init if it exists set P3, 30 # call __set_integer_native method new P4, I1 # same with P4 set P4, 12 new P5, I1 # create a new LHS for add add P5, P3, P4 # __add method set I10, P5 # __get_integer print I10 print "\n" print P5 # calls __get_string prints 'fortytwo' print "\n" inc P3 # __increment add P5, P3, P4 print P5 # calls __get_string prints '43' print "\n" subclass P3, P1, "Bar" find_type I1, "Bar" new P3, I1 set P3, 100 new P4, I1 set P4, 200 new P5, I1 add P5, P3, P4 print P5 # prints 300 print "\n" set P5, 42 print P5 # prints 'fortytwo' print "\n" inc P5 print P5 # prints 41 as Bar's print "\n" # __increment decrements set S0, "_half" # set method name set P2, P3 # the object callmethodcc # create return continuation, call print I5 # prints 50 print "\n" end .namespace [ "Foo" ] .sub __init: classoffset I0, P2, "Foo" # lookup first attribute position new P6, "Int" # create a store for the attribute setattribute P2, I0, P6 # store the first attribute invoke P1 # return .sub __set_integer_native: classoffset I0, P2, "Foo" getattribute P10, P2, I0 set P10, I5 # assign passed in value invoke P1 .sub __get_integer: classoffset I0, P2, "Foo" getattribute P10, P2, I0 set I5, P10 # return value invoke P1 .sub __get_string: classoffset I0, P2, "Foo" getattribute P10, P2, I0 set I5, P10 set S5, P10 # get stringified value ne I5, 42, ok set S5, "fortytwo" # or return modified one ok: invoke P1 .sub __increment: classoffset I0, P2, "Foo" getattribute P10, P2, I0 # as with all aggregates, this inc P10 # has reference semantics - no invoke P1 # setattribute needed .sub __add: classoffset I0, P2, "Foo" getattribute P10, P2, I0 # object getattribute P11, P5, I0 # argument getattribute P12, P6, I0 # destination add P12, P10, P11 invoke P1 .sub _half: # I5 = _half(self) classoffset I0, P2, "Foo" getattribute P10, P2, I0 set I5, P10 # get value div I5, 2 invoke P1 .namespace [ "Bar" ] .sub __increment: classoffset I0, P2, "Foo" # get Foo's attribute slot offset getattribute P10, P2, I0 # get the first Foo attribute dec P10 # the evil line invoke P1
This example prints out:
42 fortytwo 43 300 fortytwo 41 50