This page describes the assembler language assumed for the PL/0 p-code machine emulated by another page on this site.
The PL/0 virtual machine was originally specified by Nicklaus Wirth in his book Algorithms + Data Structures = Programs; it's used as the target machine for a PL/0 compiler.
Just as PL/0 is simpler than full Pascal, so also the virtual machine used by the PL/0 compiler is simpler than the p-code machines used by various Pascal compilers (e.g. Pascal-P from ETH Zürich and UCSD Pascal's P-System). Students learning about p-code and virtual machines may find the PL/0 p-code easier to grasp than the p-code systems used by full Pascal compilers.
The machine has four registers and two storage areas. The two storage areas are:
The registers are:
Integer is the only datatype.
There is no input or output; a useful convention is for programs to leave their results in a pre-arranged location on the stack.
Each instruction consists of an op code and two arguments,
which Wirth's source code refers to as l
(short for
level
, and good luck to you if you are displaying
things in a font that makes lower-case L and digit 1 look
similar) and a
(which might be argument
or address
). The op-code mnemonics used here (lit
,
opr
,
lod
,
sto
,
cal
,
int
,
jmp
, and
jpc
)
are drawn from the declaration of the fct
type in
Wirth's interpreter for the virtual machine.
(Alas, I don't know what fct
might be short for.)
The examples here will also allow a comment which begins with // and runs to the end of the line.
E.g.
STO 1 4 // stores the value in the // fourth storage cell one level up
In some cases, one or both arguments are ignored.
In an assembler intended for practical use, symbolic references to locations would be possible. Wirth's p-code, however, is only a target language for the compiler, so symbolic references are not needed, and I haven't yet added them in this emulator.
Load (i.e. push onto the stack) the value of the cell identified by level and offset. A level value of 0 means the variable is in the currently executing procedure; 1 means it's in the immediately enclosing region of the program. 2 means it's the region outside that (in PL/0 as in Pascal procedures can nest indefinitely). The offset distinguishes among the variables declared at that level.
E.g.
LOD 0 0
Store the value currently at the top of the stack to the memory cell identified by level and offset, popping the value off the stack in the process.
E.g.
STO 0 0
Push the literal value arg onto the stack.
E.g.
LIT 0 20
Jump to the instruction at address.
E.g.
JMP 0 28 // go to instruction 28
Pop the current value from the top of the stack. If it's 0 (false), jump to the instruction at address. Otherwise, continue with the current location of the program counter.
E.g.
JPC 0 29
Call the subroutine at location address, which is level nesting levels different from the nesting level of the currently executing code. This instruction pushes a stack frame (or block mark) onto the stack, storing
E.g.
CAL 2 56 // goes up two levels in the block hierarchy, // calls subroutine at instruction 56
Return from a subroutine.
This instruction uses the stack frame (or block mark) from the
current invocation of the subroutine to clear the stack of all data
local to the current subroutine,
restore the base register,
and restore the program counter.
Like all operations which require no arguments, it
uses the op code OPR
, with a second
argument (here zero) indicating which of the zero-argument operations
to perform.
E.g.
OPR 0 0 // return
All of these tests operate on the value(s) at the top of the
stack and require no other arguments. They are all represented
by the op code OPR
with a first argument of zero
and a second argument indicating which test to perform.
The tests remove one (for the unary test odd?
) or
two (for the binary tests) items from the stack and then place
a 1 or a 0 on the stack to indicate that the condition tested
for is true or false.
Test the value at the top of the stack to see if it's odd or not.
Test the two values at the top of the stack to see if they are equal or not.
Test the two values at the top of the stack to see if they are unequal or not.
Test the two values x and y at the top of the stack to see if x is less than y or not.
Here, y is the value on the top of the stack, and x is below it on the stack. Example:
LIT 10 LIT 11 OPR 0 10
This code will leave a 1 on the stack, because 10 is less than 11.
Test the two values x and y at the top of the stack to see if x is greater than or equal to y, or not.
Example:
LIT 10 LIT 11 OPR 0 11
This code will leave a 0 on the stack, because 10 is not greater than or equal to 11.
Test the two values x and y at the top of the stack to see if x is greater than y or not.
Here, y is the value on the top of the stack, and x is below it on the stack. Example:
LIT 11 LIT 10 OPR 0 12
This code will leave a 1 on the stack, because 11 is greater than 10.
Test the two values x and y at the top of the stack to see if x is less than or equal to y, or not.
Example:
LIT 11 LIT 10 OPR 0 13
This code will leave a 0 on the stack, because 11 is not less than or equal to 10.
All of these tests operate on the value(s) at the top of the
stack and require no other arguments. They are all represented
by the op code OPR
with a first argument of zero
and a second argument indicating which test to perform.
The operations remove one (for the unary operation, negation) or two (for the binary operations) items from the stack and then place the result of the operation on the stack.
Negate the value on the top of the stack (i.e. multiply by -1).
Add the two values at the top of the stack and replace them with their sum.
Subtract the value at the top of the stack from the value below it; replace the diminuend and the subtrahend with their difference.
Multiply the two values at the top of the stack and replace them with their product.
Perform integer division on the two values at the top of the stack. The value on top of the stack becomes the divisor, the value below it the dividend. Replace the two values with their integer quotient.
No provision is made for arithmetic overflow or underflow. Students may find it a useful exercise to think about how provisions might be made for such conditions.
Similarly, no provision is made for passing parameters to subroutines or returning values from them. (In Pascal terms: PL/0 supports no functions, only procedures, and only zero-parameter procedures at that.) It is a useful exercise to think about what changes to the machine language might be required to support parameters and return values.
Wirth's PL/0 compiler (apparently from Algorithms + Data Structures = Programs [1975]).
Wikipedia article on PL/0.
Wikipedia article on P-code machine (includes
the source code for Wirth's interpret
function, but not much explanation of PL/0 or the
p-code developed for it).