Rostiger's Uxn Zine
Rostiger's Uxn Zine

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.

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:

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:

POPa bDiscard top item.
NIPa cDiscard second item.
SWPa c bMove second item to top.
ROTb c aMove third item to top.
DUPa b c cCopy top item.
OVRa b c bCopy 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.


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.

|absolute$relative#literal hex
@parent&child"raw ascii
,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 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:

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.


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


To print the binary value from a short:

@pbin ( v* -- )

	,&t STR2
		#0f OVR SUB [ LIT2 &t $2 ] ROT SFT2
		NIP #01 AND LIT "0 ADD #18 DEO
		INC GTHk ?&l


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
		DUP [ LIT &z $1 ] EQU ,&skip JCN
			#ff ,&z STR DUP #30 ADD #18 DEO

		DIV2k DUP ,&emit JSR MUL2 SUB2

To convert a decimal string to a hexadecimal value.

@sdec ( str* -- val* )

	LIT2r 0000
		LIT2r 000a MUL2r
		LITr 00
		LDAk #30 SUB STH ADD2r
		INC2 LDAk ,&w JCN


To convert a hexadecimal string to a hexadecimal value.

@shex ( str* -- val* )

	LIT2r 0000
		LITr 40 SFT2r
		LITr 00
		LDAk ,chex JSR STH ADD2r
		INC2 LDAk ,&w JCN


To convert a hexadecimal character to a nibble.

@chex ( c -- val|ff )

	#27 SUB DUP #0f GTH JMP JMP2r
	POP #ff



To print a string.


	;string ,pstr JSR


@string "Hello 20 "World $1

@pstr ( str* -- )

		LDAk #18 DEO
		INC2 LDAk ,&w JCN


It was also discovered by Yeti that inline strings can be printed, the following routine was submitted by Kira:


	,pinl JSR "hello 20 "World $1


@pinl ( -- )

	INC2r ,pinl JCN


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


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


To print an entire page of memory:

@pmem ( addr* -- )

		#00 OVR STH2kr ADD2 LDA ,print/byte JSR
		DUP #0f AND #0f NEQ #16 MUL #0a ADD #18 DEO
		INC NEQk ,&l JCN


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

	&l #8000 SWP2 SUB2 #8000 ADD2 ;msfl JSR2 JMP2r
	&r ;msfr JSR2 JMP2r


To find the day of the week from a given date, Tomohiko Sakamoto's method:

@dotw ( y* m d -- dotw )

	( y -= m < 3; )
	( t[m-1] + d )
	#00 ROT ;&t ADD2 LDA #00 SWP
	( y  + y/4 - y/100 + y/400 )
	STH2kr #02 SFT2 ADD2
	STH2kr #0064 DIV2 SUB2
	STH2r #0190 DIV2 ADD2
	( % 7 )
	#0007 DIV2k MUL2 SUB2 NIP

	&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 )
	POP2 #00

	&leap POP2 #01 JMP2r


@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


@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



To convert a signed byte to a signed short.

( Arithmetic macros )

%MOD2 { DIV2k MUL2 SUB2 }

( 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