
Tal is the programming language for the Uxn virtual machine.

Uxn programs are written in a unique flavor of assembly designed especially for this virtual machine. TAL files are human-readable source files, ROM files are uxn-compatible binary program files; applications that transform TAL files into ROM files are called Assemblers.
To get started, equip yourself with an emulator and assembler for your system.
- Download emulator & assembler, 40kb
- Introduction to Uxntal, online book
Uxntal Opcodes
Uxn has 64kb of memory, 16 devices, 2 stacks, and 36 opcodes with 3 modes each. The list below show the opcodes and their effect on a given stack a b c, where PC: Program Counter, M: Memory, D: Devices, and rs: Return Stack.
LIT a b c M[PC] EQU a b?c LDZ a b M[c8] ADD a b+c INC a b c+1 NEQ a b!c STZ a {M[c8]=b} SUB a b-c POP a b GTH a b>c LDR a b M[PC+c8] MUL a b*c NIP a c LTH a b<c STR a {M[PC+c8]=b} DIV a b/c SWP a c b JMP a b {PC+=c} LDA a b M[c16] AND a b&c ROT b c a JCN a {(b8)PC+=c} STA a {M[c16]=b} ORA a b|c DUP a b c c JSR a b {rs.PC PC+=c} DEI a b D[c8] EOR a b^c OVR a b c b STH a b {rs.c} DEO a {D[c8]=b} SFT a b>>c8l<<c8h JMI PC=M[PC] JCI (a8)PC=M[PC] JSI {rs.PC} M[PC] --2 a16 b16+c16 --r a b c {rs.b+rs.c} --k a b c b+c
To learn more about each opcode, see the Uxntal Reference.
Uxntal Modes
Each opcode has 3 possible modes, which can combined:
- The short mode 2 operates with shorts instead of bytes.
- The keep mode k operates without consuming items.
- The return mode r operate on the return stack.
By default, operators consume bytes from the working stack, notice how in the following example only the last two bytes #45
and #67
are added, even if there are two shorts on the stack.
#1234 #4567 ADD12 34 ac
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
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. Under the hood, the keep mode keeps a temporary stack pointer that is decremented on POP
.
#1234 #4567 ADD2k 12 34 45 67 57 9b
The return mode makes it possible for any opcode to operate on the return-stack directly. For that reason, there is no dedicated return opcode. For example, the JSR
opcode pushes the program's address onto the return stack 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.
LITr 12 #34 STH ADDr STHr 46
Uxntal stacks
In stack-machine programming, there are no precedence rules, the calculations are merely performed in the sequence in which they are presented. The order with which elements come off a stack is known as last in, first out. In the stack a b c, the c item was the last to be added, and will be the first to be removed.
#12 12 #345634 56 LIT 12 STH 12 LIT2r 1234 12 34 LIT2r 1234 STHr 12 34
All programming in Unxtal is done by manipulating the working stack(wst), and return stack(rst). Each stack contains 254 bytes, items from one stack can be moved into the other. Here are the stack primitives:
POP | a b | Discard top item. |
---|---|---|
NIP | a c | Discard second item. |
SWP | a c b | Move second item to top. |
ROT | b c a | Move third item to top. |
DUP | a b c c | Copy top item. |
OVR | a b c b | Copy second item to top. |
Shorts are made of two bytes, each byte can be manipulated individually.
#1234 POP 12 #1234 NIP 34 #12 DUP 12 12 #1234 OVR 12 34 12 #abcd #5678 SWP2 56 78 ab cd #1234 #5678 SWP 12 34 78 56
A literal is a byte or short value to be pushed on the stacks, it must be prefixed with the LIT opcode, some runes will automatically prefix the value with the opcode.
Byte | Char | Relative | ZeroPage | Absolute* | Short* | |
---|---|---|---|---|---|---|
Literal | #ab | ,label | .label | ;label | #abcd | |
Plain | ab | "Q | _label | -label | =label | abcd |
Hexadecimal values are always lowercase, either a byte or a short in length. Give a special attention to the add2
hexadecimal value which, equals 44498 in decimal, and will not be interpreted as an opcode.
Uxntal Memory
There are 64kb of addressable memory. Roms are loaded at 0x0100. Once in memory, a Uxn program can write over itself, store values among its running code, it is not uncommon for a uxntal program to directly modify the value of a literal in memory, or to change an opcode for another instead of branching. When writing or reading a short in memory, the position is that of the high byte.
#12 #0200 STA 0x0200=12 #3456 #0400 STA2 0x0400=34, 0x0401=56 #0400 LDA2 34 56
The zero-page is the memory located between 0x0000 and 0x0100, its purpose is to store variables that will be accessed often. It is sligthly faster to read and write from the zero-page using the LDZ and STZ opcodes as they use only a single byte instead of a short. This memory space cannot be pre-filled in the rom prior to assembly.
#1234 #80 STZ2 0x0080=12, 0x0081=34 #80 LDZ2 12 34
Uxntal Devices
Uxn can communicate with 16 devices at once, each device has 16 ports, each port handles a specific I/O message. Ports are mapped to the devices memory page, which is located outside of the main addressable memory.
Uxn is non-interruptible, vectors are locations in programs that are evaluated when certain events occur. A vector is evaluated until a BRK opcode is encountered, no new events will be triggered while a vector is evaluated, but events may be queued. Only one vector is executed at a time. The content of the stacks are preserved between vectors.
For example, the address stored in the Mouse/vector
port points to a part of the program to be evaluated when the cursor is moved, or a button state has changed.
Uxntal syntax
Uppercased opcodes are reserved words, hexadecimal values are always lowercased. Comments are within parentheses, curlies are used in the definition of macros, and the square brackets are ignored.
The first token in this program is the padding operation |0100, which is where the first page of memory ends, and where all Uxn programs begin. We, then, push the absolute address of the label @hello-world to the stack, which points to a series of characters in memory.

Next, we push the address of the @print-text routine, jump to it while also leaving a return address onto the return stack.
Both &while and @while are ways to define labels, but using &while
will automatically prefix our new label with the name of the last @label
, in this example print-text/while
.
Padding | Literals | ||||||
---|---|---|---|---|---|---|---|
| | absolute | $ | relative | # | literal hex | ||
Labels | Ascii | ||||||
@ | parent | & | child | " | raw ascii | ||
Addressing | Pre-processor | ||||||
, | literal relative | _ | raw relative | % | macro-define | ~ | include |
. | literal zero-page | - | raw zero-page | ||||
; | literal absolute | = | raw absolute |
Next, the LDAk opcode takes two bytes at the top of the stack to form an absolute address, and puts 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 Console/write(port #18) which prints that character to the terminal.
We increment the absolute address found on top of the stack with INC2, because the address is made of two bytes. We load the incremented value, the JCN opcode will jump to the position of label &while for as long as the item on the stack not zero. We complete the program with POP2 to remove the address on the stack, to keep the stack clean at the end of the program.
Errors
Errors occur when a program behaves unexpectedly. Errors are normally handled by the emulator, but programs can set a system vector to evaluate when errors occurs. There are three known error types, and each one has an error code:
01
Underflow: Occurs when an opcode is trying to pop an item from an empty stack.02
Overflow: Occurs when an opcode is trying to push an item to a full stack.03
Division By Zero: Occurs when the DIV opcode is done on a value of zero.
A collection of commonly used routines in Uxntal projects.
The following snippets are in the standard format. If you discover faster and smaller helpers, please get in touch with me.
Numbers
To print hexadecimal numbers:
@print ( v* -- ) SWP ,&byte JSR &byte ( byte -- ) DUP #04 SFT ,&char JSR &char ( char -- ) #0f AND DUP #09 GTH #27 MUL ADD #30 ADD #18 DEO JMP2r
To print the binary value from a short:
@pbin ( v* -- ) ,&t STR2 #1000 &l #0f OVR SUB [ LIT2 &t $2 ] ROT SFT2 NIP #01 AND LIT "0 ADD #18 DEO INC GTHk ?&l POP2 JMP2r
To print a decimal number from a short, without leading zeroes:
@pdec ( v* -- ) #00 ,&z STR #2710 ,&parse JSR #03e8 ,&parse JSR #0064 ,&parse JSR #000a ,&parse JSR NIP &emit DUP [ LIT &z $1 ] EQU ,&skip JCN #ff ,&z STR DUP #30 ADD #18 DEO &skip POP JMP2r &parse DIV2k DUP ,&emit JSR MUL2 SUB2 JMP2r
To convert a decimal string to a hexadecimal value.
@sdec ( str* -- val* ) LIT2r 0000 &w LIT2r 000a MUL2r LITr 00 LDAk #30 SUB STH ADD2r INC2 LDAk ,&w JCN POP2 STH2r JMP2r
To convert a hexadecimal string to a hexadecimal value.
@shex ( str* -- val* ) LIT2r 0000 &w LITr 40 SFT2r LITr 00 LDAk ,chex JSR STH ADD2r INC2 LDAk ,&w JCN POP2 STH2r JMP2r
To convert a hexadecimal character to a nibble.
@chex ( c -- val|ff ) LIT "0 SUB DUP #09 GTH JMP JMP2r #27 SUB DUP #0f GTH JMP JMP2r POP #ff JMP2r
Strings
To print a string.
|0100 ;string ,pstr JSR BRK @string "Hello 20 "World $1 @pstr ( str* -- ) &w LDAk #18 DEO INC2 LDAk ,&w JCN POP2 JMP2r
It was also discovered by Yeti that inline strings can be printed, the following routine was submitted by Kira:
|0100 ,pinl JSR "hello 20 "World $1 BRK @pinl ( -- ) LDArk STHr DUP #18 DEO INC2r ,pinl JCN JMP2r
Helpers for strings.
@scap ( str* -- end* ) LDAk #00 NEQ JMP JMP2r &w INC2 LDAk ,&w JCN JMP2r @spop ( str* -- ) LDAk ,&n JCN POP2 JMP2r &n ,scap JSR #0001 SUB2 #00 ROT ROT STA JMP2r @sput ( chr str* -- ) ,scap JSR INC2k #00 ROT ROT STA STA JMP2r @slen ( str* -- len* ) DUP2 ,scap JSR SWP2 SUB2 JMP2r @scat ( src* dst* -- ) ,scap JSR @scpy ( src* dst* -- ) STH2 &w LDAk STH2kr STA INC2r INC2 LDAk ,&w JCN POP2 #00 STH2r STA JMP2r @scmp ( a* b* -- f ) STH2 &l LDAk LDAkr STHr ANDk #00 EQU ,&e JCN NEQk ,&e JCN POP2 INC2 INC2r ,&l JMP &e NIP2 POP2r EQU JMP2r @sclr ( str* -- ) LDAk ,&w JCN POP2 JMP2r &w STH2k #00 STH2r STA INC2 LDAk ,&w JCN POP2 JMP2r
zstr
For length-prefixed zero-page strings:
@zstr ( zero-page length-prefixed string ) &push ( zstr c -- zstr ) OVR LDZk INC SWP STZk ADD STZ JMP2r &pop ( zstr -- zstr ) DUP LDZk #01 SUB SWP STZ JMP2r &print ( zstr -- zstr ) LDZk ADDk NIP SWP &l INC LDZk #18 DEO NEQk ,&l JCN NIP JMP2r
Memory
To print an entire page of memory:
@pmem ( addr* -- ) STH2 #0000 &l #00 OVR STH2kr ADD2 LDA ,print/byte JSR DUP #0f AND #0f NEQ #16 MUL #0a ADD #18 DEO INC NEQk ,&l JCN POP2 POP2r JMP2r
Helpers for memory.
@mclr ( src* len* -- ) OVR2 ADD2 SWP2 &l STH2k #00 STH2r STA INC2 GTH2k ,&l JCN POP2 POP2 JMP2r @mcpy ( src* dst* len* -- ) SWP2 STH2 OVR2 ADD2 SWP2 &l LDAk STH2kr STA INC2r INC2 GTH2k ,&l JCN POP2 POP2 POP2r JMP2r @msfl ( a* b* len* -- ) STH2 SWP2 EQU2k ,&e JCN &l DUP2k STH2kr ADD2 LDA ROT ROT STA INC2 GTH2k ,&l JCN POP2 POP2 &e POP2r JMP2r @msfr ( a* b* len* -- ) STH2 EQU2k ,&e JCN &l DUP2 LDAk ROT ROT STH2kr ADD2 STA #0001 SUB2 LTH2k ,&l JCN POP2 POP2 &e POP2r JMP2r
To shift part of a string using a signed short.
@ssft ( str* len* -- ) STH2 DUP2k ;slen JSR2 ADD2 STH2r DUP2 #8000 GTH2 ,&l JCN ORAk ,&r JCN POP2 POP2 POP2 JMP2r &l #8000 SWP2 SUB2 #8000 ADD2 ;msfl JSR2 JMP2r &r ;msfr JSR2 JMP2r
Dates
To find the day of the week from a given date, Tomohiko Sakamoto's method:
@dotw ( y* m d -- dotw ) ( y -= m < 3; ) OVR STH SWP2 #00 STHr #02 LTH SUB2 STH2 ( t[m-1] + d ) #00 ROT ;&t ADD2 LDA #00 SWP ROT #00 SWP ADD2 ( y + y/4 - y/100 + y/400 ) STH2kr STH2kr #02 SFT2 ADD2 STH2kr #0064 DIV2 SUB2 STH2r #0190 DIV2 ADD2 ADD2 ( % 7 ) #0007 DIV2k MUL2 SUB2 NIP JMP2r &t 00 03 02 05 00 03 05 01 04 06 02 04
To find if a year is a leap year:
@is-leap-year ( year* -- bool ) ( leap year if perfectly divisible by 400 ) DUP2 #0190 ( MOD2 ) DIV2k MUL2 SUB2 #0000 EQU2 ,&leap JCN ( not leap year if divisible by 100 but not divisible by 400 ) DUP2 #0064 ( MOD2 ) DIV2k MUL2 SUB2 #0000 EQU2 ,¬-leap JCN ( leap year if not divisible by 100 but divisible by 4 ) DUP2 #0003 AND2 #0000 EQU2 ,&leap JCN ( all other years are not leap years ) ¬-leap POP2 #00 JMP2r &leap POP2 #01 JMP2r
Random
@prng-init ( -- ) ( seed ) #00 .DateTime/second DEI #00 .DateTime/minute DEI #60 SFT2 EOR2 #00 .DateTime/hour DEI #c0 SFT2 EOR2 ,prng/x STR2 #00 .DateTime/hour DEI #04 SFT2 #00 .DateTime/day DEI #10 SFT2 EOR2 #00 .DateTime/month DEI #60 SFT2 EOR2 .DateTime/year DEI2 #a0 SFT2 EOR2 ,prng/y STR2 JMP2r @prng ( -- number* ) LIT2 &x $2 DUP2 #50 SFT2 EOR2 DUP2 #03 SFT2 EOR2 LIT2 &y $2 DUP2 ,&x STR2 DUP2 #01 SFT2 EOR2 EOR2 ,&y STR2k POP JMP2r
Misc
To convert a signed byte to a signed short.
DUP #7f GTH #ff MUL SWP
( Arithmetic macros ) %MOD { DIVk MUL SUB } %MOD2 { DIV2k MUL2 SUB2 } %MIN2 { LTH2k JMP SWP2 POP2 } %MAX2 { GTH2k JMP SWP2 POP2 } ( Signed macros ) %LTS2 { #8000 STH2k ADD2 SWP2 STH2r ADD2 GTH2 } %GTS2 { #8000 STH2k ADD2 SWP2 STH2r ADD2 LTH2 } ( Binary macros ) %ROL { DUP #07 SFT SWP #10 SFT ADD } %ROR { DUP #70 SFT SWP #01 SFT ADD } %ROL2 { DUP2 #0f SFT2 SWP2 #10 SFT2 ADD2 } %ROR2 { DUP2 #f0 SFT2 SWP2 #01 SFT2 ADD2 } ( A clever hack ) %PC { #00 JSR STH2r }
Incoming: left dexe noodle theme gly format ufx format bifurcan catclock yufo proquints brainfuck uxn uxntal alphabet bicycle drifblim beetbug about computer oscean arvelie