Documentation for ISCAL
(ISC Assembly Language)

Robert Keller
Computer Science Department
Harvey Mudd College

Last updated 17 November 2008

Table of Contents

Introduction

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

Using the ISC in UNIX

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.

ISCAL Comments

Comments in ISCAL source are as in C++: Comments have absolute no effect on the code loaded into the ISC or its execution.

ISCAL Directives (instructions to the assembler)

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.

ISC Machine Instructions (instructions to the ISC)

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.

Subroutine (Function and Procedure) Calling Sequences

The ISC does not force any particular calling sequence as a standard. However the following naming convention is suggested:

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:

  1. Use separate sets of registers for each subroutine.
  2. 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.

Stack Usage

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.

Input and Output

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

Halting the Program

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.

Example Execution

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:

Diagnostic Features of the ISC Assembler Simulator

The following features are included in the ISC assembler: 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 following occur with execution in general:

Download