Tal is the programming language for the Uxn stack-machine.
The programming language for creating Uxn-compatible applications is called Uxntal or simply Tal, it is a unique flavor of assembly designed for this unique stack-machine. The program that transforms Tal files, into Uxn-compatible roms, is the Uxn Assembler.
Stack machine programming might look at bit odd, as it uses a postfix notation, which means that operators are always found at the end of an operation. For instance, one would write
3 4 + instead of
3 + 4. The expression written
(5 + 10) * 3 in conventional notation would be written
10 5 + 3 * in reverse Polish notation.
There are 32 opcodes, each opcode occupies 5 bits of a byte, the remaining 3 bits are used for the modes of that opcode, modes are explained below.
|keep ||return ||short ||opcode |
Operator modes are indicated by appending extra characters at the end of the opcode, for example, the short mode for the
ADD opcode is
ADD2, modes can also be combined, for example:
|0x03||a||a a||0x13||val a|
|0x05||a b||b a||0x15||val a*|
|0x06||a b||a b a||0x16||a||val|
|0x07||a b c||b c a||0x17||val a|
|0x08||a b||flag||0x18||a b||res|
|0x09||a b||flag||0x19||a b||res|
|0x0a||a b||flag||0x1a||a b||res|
|0x0b||a b||flag||0x1b||a b||res|
|0x0d||flag a||0x1d||a b||res|
The jump operators will jump to a relative address in normal mode, and to an absolute address in short mode. All memory operators expect a single byte, except for the absolute operators expecting an absolute address. Shift in short mode is expecting a single byte.
By default, operators operate on bytes, notice how in the following example only the last two bytes
#67 are added, even if there are two shorts on the stack.
#1234 #4567 ADD 12 34 ac 00 00 00 00 00
The short mode consumes two bytes from the stack. In the case of jump opcodes, the short-mode operation jumps to an absolute address in memory. For the memory accessing opcodes, the short mode operation indicates the size of the data to read and write.
#1234 #4567 ADD2 57 9b 00 00 00 00 00 00
The keep mode does not consume items from the stack, and pushes the result on top. The following example adds the two shorts together, but does not consume them.
#1234 #4567 ADD2k 12 34 45 67 57 9b 00 00
|a||a||a b||a b b a|
|a||a a a||a b||a b a b a|
|a b||a b b||a b c||a b c b c a|
The return mode makes it possible for any operator to operate on the return-stack directly. For that reason, there is no dedicated return opcode. For example, the JumpStash(
JSR) operator pushes the program's address onto the return stash before jumping, to return to that address, the
JMP2r opcode is used, where instead of using the address on the working-stack, it takes its address from the return-stack.
#1234 #4567 STH ROT STH ADDr STHr 34 45 79 00 00 00 00 00
Uxntal has no reserved words besides the 32 opcodes, each element type begins with its own special character, or rune. Comments are within parentheses, curlies are used in the definition of macros, and the square brackets are ignored.
|sublabel spacer||raw char|
In the following example, our program begins with a padding to the address
|18, followed by the definition of the
Console/write label. From that point onward, the absolute or zero-page address of that label can be referenced by name. The zero-page is the name of the first 256 bytes in memory, the
0000-0100 range, it is typically used to store variables and devices.
Next, we pad to
|0100, which is where the first page of memory ends, and where all Uxn programs begin. Our program begins by pushing to the stack, the absolute address of the label
hello-world, which is defined at the end of the program and points to a series of characters. An absolute address is made of two bytes. We then assign the label
&loop to this position of the program so we can return to it later.
LDAk opcode takes two bytes at the top of the stack to form an absolute address, and loads the value in memory found at that address to the top of the stack, in this case, the ascii value of the letter H. That value is sent to the Console's port named
(#18) which prints that character to the terminal.
Next, we increment the absolute address found on top of the stack, we use
ADD2 because we know the items on the stack to be an address made of two bytes. We load the incremented value, the
JCN opcode will jump to the position of label
&loop as long as the item before the address is not zero. We complete the program with
POP2 to remove the address on the stack, to keep the stack clean.
( dev/console ) |18 @Console/write ( init ) |0100 ( -> ) ;hello-word &loop ( send ) LDAk .Console/write DEO ( loop ) INC2 LDAk ,&loop JCN POP2 BRK @hello-word "Hello 20 "World!
@loop are ways to define labels, but using
&loop will automatically prefix your new
&label with the name of the last
@label. Since label names are unique identifiers and you might need multiple instances of something called loop, it's best to use sublabels when possible.
The memory and stacks contain 8-bit values. To differentiate between operations and literal numbers, the
LIT opcode will push the following byte from memory onto the stack, the 16-bits mode
LIT2 opcode will push the following short.
Immediate, or literal, addressing allows to directly specify a byte or short constant. Deferred addressing is a form of immediate addressing where the address of a label is put on the stack.