Documentation for ISCAL
(ISC Assembly Language)
Robert Keller
Computer Science Department
Harvey Mudd College
Last updated 17 November 2008
Table of Contents
ISCAL is the assembly language for the simulated ISC (Incredibly Simple Computer).
For more information on the structure and coding of the ISC, see the notes
on Principles of Computing.
Below, the first item (in lower case) is the instruction mnemonic.
The other items (starting with upper case) are variables.
For example, an example of define Identifier Value
would be
define m1 -1
For an example of complete programs, see ~cs/cs60/isc/:
cio.isc echo program (demonstrating console input/output)
io.isc echo program (demonstrating input/output)
fac.isc iterative factorial program
rfac.isc recursive factorial program
array.isc summing an array
array_io.isc array input/output
part.isc partitioning phase of quicksort
Assuming you have isc in your path (it is in ~cs/cs60/bin/), execute the
assembler and simulator by
isc Filename.isc
Optional command-line parameters are in the form -l, -s, or -t or any
combination of these (such as -lst):
t: provide execution trace
l: provide listing showing how assembly code has been loaded into memory
s: provide symbol table showing how symbolic names are equated to
values, registers, etc.
Also -r can be used, followed by the number of registers (default 32), for
example:
isc -sr 100 Filename.isc
means to provide a symbol table and use up to 100 registers.
Comments in ISCAL source are as in C++:
- // starts a comment which continues to the end of the line.
- /* starts a comment which continues to */
Comments have absolute no effect on the code loaded into the ISC or its
execution.
Directives are static pseudo-instructions that deal with the code textually.
They do not create executable machine instructions, and do not
take up space in a location.
-
define Identifier Value
-
defines Identifier to have Value, a numeric constant
-
origin Address
-
directs the assembler to begin loading code at Address. If not
included, the assembler will load starting at 0. This directive can be
given multiple times.
-
label Identifier
-
instructs the assembler to equate Identifier with the next address
in the assembly sequence.
-
register Identifier Value
-
defines Identifier to have Value and reserves Value
as a register in use.
-
use Identifier
-
instructs the assembler to equate Identifier to some unused register.
-
release Identifier
-
instructs the assembler to discontinue use of Identifier as a register.
Identifier should have been reserved using either the register
directive or the use directive.
-
trace Integer
-
if Integer is 0, turns off tracing of the instructions which
follow. If it is 1, turns on tracing. The default is on. However,
tracing only occurs when -t is used on the command line.
Below Ra, Rb, and Rc stand for register numbers.
They should normally be identifiers
that have been defined using the register directive.
C is a constant, which can be a literal constant or one defined using the
define or label directives.
-
lim Ra C
-
reg[Ra] = C
Load immediate to register Ra signed 24-bit integer (or address) constant C.
-
aim Ra C
-
reg[Ra] += C
Add immediate to register Ra a signed 24-bit integer (or address) constant C.
-
load Ra Rb
-
reg[Ra] = mem[reg[Rb]]
Load into Ra the contents of the memory location addressed by Rb.
-
store Ra Rb
-
mem[reg[Ra]] = reg[Rb]
Store into the memory location addressed by Ra the contents of Rb.
-
copy Ra Rb
-
reg[Ra] = reg[Rb]
Copy into Ra the contents of register Rb.
-
add Ra Rb Rc
-
reg[Ra] = reg[Rb] + reg[Rc]
Put into Ra the sum of the contents of Rb and the contents of Rc.
-
sub Ra Rb Rc
-
reg[Ra] = reg[Rb] - reg[Rc]
Put into Ra the contents of Rb minus the contents of Rc.
-
mul Ra Rb Rc
-
reg[Ra] = reg[Rb] * reg[Rc]
Put into Ra the product of the contents of Rb and the contents of Rc.
-
div Ra Rb Rc
-
reg[Ra] = reg[Rb] / reg[Rc]
Put into Ra the contents of Rb divided by the contents of Rc.
-
and Ra Rb Rc
-
reg[Ra] = reg[Rb] & reg[Rc]
Put into Ra the contents of Rb bitwise-and the contents of Rc.
-
or Ra Rb Rc
-
reg[Ra] = reg[Rb] | reg[Rc]
Put into Ra the contents of Rb bitwise-or the contents of Rc.
-
comp Ra Rb
-
reg[Ra] = ~reg[Rb]
Put into Ra the bitwise-complement of the contents of Rb.
-
shr Ra Rb Rc
-
reg[Ra] = reg[Rb] >> reg[Rc]
The contents of Rb is shifted right by the amount specified in register Rc and
the result is stored in Ra. If the value in Rc is negative, the value is
shifted left by the negative of that amount.
-
shl Ra Rb Rc
-
reg[Ra] = reg[Rb] << reg[Rc]
The value in register Rb is shifted left by the amount specified in register Rc
and the result is stored in Ra. If the value in Rc is negative, the value is
shifted right by the negative of that amount.
-
jeq Ra Rb Rc
-
Jump to the address in Ra if the values in Rb and Rc are equal. Otherwise
continue.
-
jne Ra Rb Rc
-
Jump to the address in Ra if the values in Rb and Rc are not equal. Otherwise
continue.
-
jgt Ra Rb Rc
-
Jump to the address in Ra if the value in Rb is greater than that in Rc.
Otherwise continue.
-
jgte Ra Rb Rc
-
Jump to the address in Ra if the value in Rb is greater than or equal that in
Rc. Otherwise continue.
-
jlt Ra Rb Rc
-
Jump to the address in Ra if the value in Rb is less than that in Rc.
Otherwise continue.
-
jlte Ra Rb Rc
-
Jump to the address in Ra if the value in Rb is less than or equal that in Rc.
Otherwise continue.
-
junc Ra
-
Jump to the address in Ra unconditionally.
-
jsub Ra Rb
-
Jump to subroutine in the address in Ra. The value of the IP (i.e. what would
have been the next instruction) is put into Rb. Therefore this can be used for
jumping to a subroutine. If the return address is not needed, some register
not in use should be specified.
-
cin Ra
-
Read the console input as a decimal integer into register Ra. Console input is simulated through the standard input.
-
cout Ra
-
Write the contents of decimal integer Ra as a decimal integer on the console output device. Console output is simulated through the standard output.
The ISC does not force any particular calling sequence as a standard. However
the following naming convention is suggested:
- Use return as the name of the return address register.
- Use result as the result register (assuming only one result).
- Use arg1, arg2, ... as the names of argument registers.
When one subroutine calls another, it is possible or even likely that the
argument and return registers will get overwritten in making the inner call.
To prepare for this event, there are two options:
- Use separate sets of registers for each subroutine.
- Move the argument and return register contents to memory, then restore
them when necessary.
For a recursive subroutine, moving the contents to memory is
mandatory, since the same registers will always be used for each
recursive call as a single body of text is used for all calls of the routine.
The usual model for moving register contents to memory is to use a stack implemented in memory. This is appropriate because of the last-in, first-out usage of the register contents (the most recently called routine is the first
from which return takes place). To implement a stack, you will need to leave
room for an array in memory. For example, you might decide to put the stack
area right after the code. This could be accomplished by following the
code by:
label save_area_loc // first location in save area
A register, say stack_pointer, is reserved as a pointer to the
top element of the stack. Since the stack is initially empty, this register
could be initialized by:
lim stack_pointer save_area_loc // initialize stack pointer
aim stack_pointer -1 // always point to top of stack
Then in order to push a register's contents to the stack, we use the
following idiom:
aim stack_pointer +1
store stack_pointer ....register....
Note that this maintains the invariant that the stack_pointer always points
to the top of the stack.
Conversely, to pop the stack into a register, we can use:
load ....register.... stack_pointer
aim stack_pointer -1
This also maintains the invariant.
If there is more than one register, then care must be taken to use the
reverse order in popping from that in pushing. For example, if the registers
to be saved are arg1, arg2, and return, then the
saving sequence might be:
aim stack_pointer +1
store stack_pointer return
aim stack_pointer +1
store stack_pointer arg1
aim stack_pointer +1
store stack_pointer arg2
while the corrsponding unsaving sequence would be:
load arg2 stack_pointer
aim stack_pointer -1
load arg1 stack_pointer
aim stack_pointer -1
load arg2 stack_pointer
aim stack_pointer -1
See rfac.isc
for an example of a recursive factorial routine using a stack.
The ISC simulates memory-mapped i/o. It assumes input and output devices
work asynchronously with respect to the program, and a protocol involving
status registers is used. It is strongly advised that the newcomer use
the subroutines for i/o which are provided in the examples, such as
io.isc.
There are three subroutines used:
- io_setup:
- initializes the relevant i/o registers. A register zero
containing 0 is assumed. The following code sets up i/o:
lim zero 0 // constant 0
lim jump_address io_setup // initialize io
jsub jump_address return
- input:
- inputs one word from a device (simulated by the standard input).
This is accomplished by:
jsub input_address return // get input value
where input_address is assumed to be a register holding the
address of input (this is loaded by io_setup).
The word read is returned in the register
result.
- output:
- outputs one word to a device (simulated by the standard output).
The word is assumed to be in register arg1. Output is then
accomplished by:
jsub output_address return // put output value
where output_address is assumed to be a register holding the
address of output (this is loaded by io_setup).
The following dedicated addresses are used in memory-mapped i/o (presented
in the form of ISCAL definitions):
define input_word_loc -1 // fixed location for input word
define input_status_loc -2 // fixed location for input status
define output_word_loc -3 // fixed location for output word
define output_status_loc -4 // fixed location for output status
and the i/o routines assume the following register definitions (presented
in the form of ISCAL register usage directives):
use input_word // register to hold input_word
use input_status // register to hold input_status
use output_word // register to hold output_word
use output_status // register to hold input_status
To halt execution of a program, simply jump to location 0. The input
routine described above is set up so that it will halt in this way whenever
end-of-file occurs on the input device.
The following is an example of ISC execution using all three options
using the code fac.isc.
Bold-face indicates items typed by the user.
isc -lst ~cs/cs60/isc/fac.isc
The output produced from fac.isc
consists of three sections:
The following features are included in the ISC assembler:
-
You cannot use absolute register numbers in instructions; you must instead
use symbolic registers (defined either using use or register).
This tends to head off errors where people use constants like 0 in instructions, expecting them to be the actual value, rather than using a register containing 0.
The ISC simulator provides several diagnostic features which would not be
present in a real-life execution.
The following features appear if the trace is turned on:
- The source line number is indicated. In real-life, the source
line number has no bearing in the actual execution; it is discarded by the
assembler.
- The current content of the register is shown in parentheses.
- In a jump instruction, a message tells whether or not the jump is taken
and to what location.
The following occur with execution in general:
-
If an attempt is made to read or write from or to a
location not in the ISC's memory,
the program will be terminated with a warning.
- If the program attempts to write into a location into which an instruction
was originally loaded, a warning message appears. Chances are that the
value written into that location is not an instruction, and will thus cause
aberrant behavior the next time the instruction in that location is executed.
- The input/output routines require a particular protocol for setting and
checking status registers. If this protocol is not obeyed, warning
messages will be generated. It is best to use the i/o routines provided in
io.isc to get
started.