main pita/uxn12 / doc / man / uxntal.7
uxntal(7) Uxntal Reference Guide uxntal(7)

uxntal - assembly language for Varvara virtual machine

Uxntal is an 8-bit instruction set for programming the Varvara virtual machine. It uses the lower 5-bits to specify an opcode, and the upper 3-bits to specify optional modes.

ROMs consist of a 16-bit address space of bytes. Any byte can be interpreted as either data or an instruction. A 2-byte program counter (pc) determines the address of the next instruction to decode and run.

Instructions manipulate data using two stacks: a working stack (wst) and a return stack (rst). Each stack consists of 256 bytes, and in the case of overflow or underflow the stack pointer will wrap (the stacks are circular).

There are also 256 bytes of device memory, which are used to interact with the virtual machine and its devices.

Instructions deal with unsigned 8-bit values (bytes) and unsigned 16-bit values (shorts). There are no other built-in data types. Occasionally values are treated as signed, such as when loading, storing, or jumping with a relative address.

Short values are stored on the stack in big-endian byte order: the least significant byte will be on the top of the stack. This makes #abcd is equivalent to #ab #cd.


0x01 ----
0x02 \
0x04 +- opcode
0x08 /
0x10 ----
0x20 ---- 2: short mode
0x40 ---- r: return mode
0x80 ---- k: keep mode

There are 32 base values for opcodes:


0x00 BRK* 0x08 EQU 0x10 LDZ 0x18 ADD
0x01 INC 0x09 NEQ 0x11 STZ 0x19 SUB
0x02 POP 0x0a GTH 0x12 LDR 0x1a MUL
0x03 NIP 0x0b LTH 0x13 STR 0x1b DIV
0x04 SWP 0x0c JMP 0x14 LDA 0x1c AND
0x05 ROT 0x0d JCN 0x15 STA 0x1d ORA
0x06 DUP 0x0e JSR 0x16 DEI 0x1e EOR
0x07 OVR 0x0f STH 0x17 DEO 0x1f SFT

The "complete" opcode's value can be derived by combining the base value with its flags.

For example, ADD2k is (ADD | 2 | k) = (0x18 | 0x20 | 0x80) = 0xb8.

Unlike other opcodes, 0x00 (BRK*) is contextual: its meaning depends on the mode bits provided:


0x00 BRK 0x80 LIT
0x20 JCI 0xa0 LIT2
0x40 JMI 0xc0 LITr
0x60 JSI 0xe0 LIT2r

Given a stack effect ( a^ b^ c^ -- c^ a^ b^ ) here is what each symbol means:


( and ) are comment delimiters
a^, b^, and c^ are values on the stack
^ indicates that each value is a byte (* would indicate short)
-- separates the "before" and "after" of the stack effect

The effect here is to move the top byte of the stack below the next two bytes, which could be achieved with ROT ROT.

By default stack effects describe the effect on wst. When rst is involved we use [] to differentiate the stacks. For example ( a* [b*] -- a+1* [b+1*] ) will increment the top short of both wst and rst.

Regular instructions have a single stack effect which is modified in a predictable way by any additional modes.

For example the generic effect for ADD is ( x y -- x+y ). The eight combinations of modes have the following effects:


ADD ( x^ y^ -- x+y^ ) sum bytes from wst
ADDr ( [x^ y^] -- [x+y^] ) sum bytes from rst
ADD2 ( x* y* -- x+y* ) sum shorts from wst
ADD2r ( [x* y*] -- [x+y*] ) sum shorts from rst
ADDk ( x^ y^ -- x^ y^ x+y^ ) sum and keep bytes from wst
ADDkr ( [x^ y^] -- [x^ y^ x+y^] ) sum and keep bytes from rst
ADD2k ( x* y* -- x* y* x+y* ) sum and keep shorts from wst
ADD2kr ( [x* y*] -- [x* y* x+y*] ) sum and keep shorts from rst

Thus for regular instructions writing a "generic" effect (leaving sigils off values whose size depends on short mode) is sufficient to describe its behavior across all eight variations. Note that some instructions always read values of a fixed size. For example the boolean condition read by JCN is always one byte, no matter what modes are used.

In return mode the stacks are reversed. Effects on wst will instead affect rst, and effects on rst will instead affect wst. For example, STH reads a byte from wst and writes it to rst, but STHr reads a byte from rst and writes it to wst.

In keep mode all the values on the left-hand side of the stack effect will also appear on the right-hand side before the outputs. For example, SWP is (x y -- y x) but SWPk is (x y -- x y y x).

We consider the top of the stack to be the first value of the stack, and count back from there. For example, given the stack effect ( a b c -- ) we would say that c is the top of the stack, b is the second value (second from the top), and a is the third value (third from the top).

Increment the top value of the stack by 1.

Overflow will be truncated, so #ff INC will evaluate to 0x00.

Remove the top value of the stack.

POPk is guaranteed to have no effect (it will not change the stack).

Remove the second value of the stack.

NIPk is guaranteed to have no effect (it will not change the stack).

Swap the top two values of the stack.

Rotate the top three values of the stack. The lowest becomes the top and the others are each shifted down one place.

Place a copy of the top value of the stack on top of the stack.

Place a copy of the second value of the stack on top of the stack.

Test whether the top two values of the stack are equal.

Result is guaranteed to be boolean (0x00 or 0x01).

Test whether the top two values of the stack are not equal.

Result is guaranteed to be boolean (0x00 or 0x01).

Test whether the second value of the stack is greater than the top.

Result is guaranteed to be boolean (0x00 or 0x01).

Test whether the second value of the stack is less than the top.

Result is guaranteed to be boolean (0x00 or 0x01).

Jump to a location.

The program counter (pc) is unconditionally updated. When x is a byte, it is treated as relative (pc += x) and when x is a short it is treated as absolute (pc = x).

It is common to JMP with boolean bytes (0-1) to handle simple conditionals. For example:


@max ( x^ y^ -- max^ ) GTHk JMP SWP POP JMP2r

Jump to a location when a condition is true.

The program counter (pc) is updated when bool is non-zero. When x is a byte, it is treated as relative (pc += x) and when x is a short it is treated as absolute (pc = x).

Jump to a location, saving a reference to return to.

Stores the next address to execute before unconditionally updating the program counter (pc). This instruction is usually used to invoke subroutines, which use the JMP2r to return. When x is a byte, it is treated as relative (pc += x) and when x is a short it is treated as absolute (pc = x).

The saved address will always be a short regardless of short mode.

Move the top value of the stack to the return stack.

Load data from a zero-page address (0x00 - 0xff).

Store data at a zero-page address (0x00 - 0xff).

Load data from a relative address (pc + x).

Note that unlike LDZk and LDAk the LDRk instruction is not very useful, since a relative address is usually only meaningful when run from a particular address (i.e. for a particular pc value).

Store data at a relative address (pc + x).

Note that unlike STZk and STAk the STRk instruction is not very useful, since a relative address is usually only meaningful when run from a particular address (i.e. for a particular pc value).

Load data from an absolute address (0x0000 - 0xffff).

Store data at an absolute address (0x0000 - 0xffff).

Read data from a device port (0x00 - 0xff).

Reading from some ports may have an effect on the underlying VM; in other cases it will simply read values from device memory. See Varvara device documentation for more details.

Write data to a device port (0x00 - 0xff).

Writing to some ports may have an effect on the underlying VM; in other cases it will simply write values to device memory. See Varvara device documentation for more details.

Add the top two values of the stack.

Overflow will be truncated, so #ff #03 ADD will evaluate to 0x02.

Subtract the top of the stack from the second value of the stack.

Underflow will be truncated, so #01 #03 SUB will evaluate to 0xfe.

Multiply the top two values of the stack.

Overflow will be truncated, so #11 #11 MUL will evaluate to 0x21.

Divide the second value of the stack by the top of the stack.

DIV implements Euclidean division, which is also known as integer division. It returns whole numbers, so #08 #09 DIV evaluates to 0x00.

Division by zero will return zero (instead of signaling an error).

Unlike ADD, SUB, and MUL, DIV does not behave correctly for numbers which should be treated as signed. For example, the signed byte representation of -2 is 0xfe, but #06 #fe DIV evaluates to 0x00 (6 / 254 = 0). For signed values the correct result should instead be 0xfd (6 / -2 = -3).

There is no remainder instruction, but the phrase DIVk MUL SUB can be used to compute the remainder.

Compute the bitwise union of the top two values of the stack.

Compute the bitwise intersection of the top two values of the stack.

Compute the bitwise exclusive-or (xor) of the top two values of the stack.

Compute a bit shift of the second value of the stack; the directions and distances are determined by the top value of the stack.

Given a byte rl consisting of a low nibble (l) and a high nibble (r), this instruction shifts x left by l and then right by r.

Right shifts are unsigned (they introduce zero bits). There are no signed shifts.

For 16-bit (and 8-bit) values, one nibble (0x0 - 0xf) is sufficient to express all useful left or right shifts.


Right: #ff #03 SFT evaluates to 0x1f
Left: #ff #20 SFT evaluates to 0xfc
Both: #ff #23 SFT evaluates to 0x7c

These instructions do not accept all mode flags (some do not accept any).

The break instruction is used to end a vector call and return control to the virtual machine.

The "immediate jump" instructions are produced by the assembler. They interpret the next 2 bytes of the ROM as a relative address (addr) and have the following effects:


JMI ( -- ) jump to addr unconditionally
JCI ( bool^ -- ) jump to addr if bool is non-zero
JSI ( -- [pc*] ) jump to addr saving the current address (pc) on rst

(The instruction pointer will be moved forward 2 bytes, past the relative address.)

These instructions are created by the assembler from special syntax:


!dest produces JMI wx yz
?dest produces JCI wx yz
dest produces JSI wx yz (assuming dest is not a macro or reserved)

Push a literal value on the stack.

The "literal" instructions are used to push new data onto the stacks. They interpret the next 1-2 bytes of the ROM (wx, wxyz) as data and push it onto the corresponding stack:


LIT ( -- wx^ ) push literal byte wx onto the wst
LITr ( -- [wx^] ) push literal byte wx onto the rst
LIT2 ( -- wxyz* ) push literal short wxyz (2 bytes) onto the wst
LIT2r ( -- [wxyz*] ) push literal short wxyz (2 bytes) onto the rst

(The instruction pointer will be moved forward 1-2 bytes, past the literal data.)

Literal values can be updated dynamically using store instructions:


#abcd ;x STA2
( later on... )
LIT2 [ @x $2 ]


https://wiki.xxiivv.com/site/uxntal_opcodes.html Uxntal Opcodes
https://wiki.xxiivv.com/site/uxntal_syntax.html Uxntal Syntax
https://wiki.xxiivv.com/site/uxntal_modes.html Uxntal Modes
https://wiki.xxiivv.com/site/uxntal_immediate.html Immediate opcodes
https://wiki.xxiivv.com/site/varvara.html Varvara

05 Aug 2024 1.0