Uxntal syntax

Uppercased opcodes are reserved words, hexadecimal bytes and shorts are always lowercase. Parentheses are comments, curlies are lambdas, and square brackets are used for organization.

The first line begins with a padding of |10 to the Console device, followed by the enumeration of the device's ports. This enum will allow us to refer to the console by name, as opposed to using the port numbers directly.

The second line pads to |0100, which is where the first page of memory ends, and where all Uxn programs begin. Next is a comment, the arrow symbol indicates that the following operation is a vector, and will terminate with BRK.

We push the absolute address, made of two bytes, of the label @hello-world to the stack, which points to a series of characters in memory. A hexadecimal number or label pushed to the stack in this fashion is called a literal, as opposed to a value stored in memory. Next, we jump to the @print-text subroutine, and leave 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 RunesLiteral Hex Rune
|absolute$relative#literal hex
Label RunesAscii Runes
@parent&child"raw ascii
Addressing RunesPre-processor Runes
,literal relative_raw relative%macro-define~include
.literal zero-page-raw zero-page
;literal absolute=raw absolute
Immediate Runes

Next, the LDAk opcode takes the absolute address on the stack, and loads the byte 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 device port #18, defined by our Console enum, 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, next we do a conditional jump with ?&while for as long as the item on the stack is not zero. We use POP2 to remove the address on the stack and keep the stack clean at the end of the subroutine.

Lastly, we encounter JMP2r which jumps to the absolute address that we left on the return stack when we entered the @print-text subroutine.

Using and defining struct data structures in Uxntal.

Structs are defined by rolling back the program address with |00, similarly to how you would define an enum.

|00 @person &name $2 &age $1 &height $2 &length

The length member of the struct holds the total size of the struct, and allows to jump to a specific item in a datastructure.


The idea here is that you define padded labels, for example the ;person/age label holds the value of $0002, naming that offset allows to access specific members of a data structure by applying the sublabels of the enum to a pointer.

|0100 @reset

	( get second person ) ;family #0001 ;person/length MUL2 ADD2
		( get name* ) DUP2 ;person/name ADD2 LDA2 print-string
		( get age ) DUP2 ;person/age ADD2 LDA print-byte
		( get height* ) ;person/height ADD2 LDA2 print-short


@family ( name* age height* )
	=dict/melany 2a 008c
	=dict/emily 14 0073
	=dict/avery 09 0091

	&melany "Melany $1
	&emily "Emily $1
	&avery "Avery $1

Using and operating on negative numbers in Uxntal.

Uxn doesn't have built-in support for negative integers. However, you can emulate signed numbers by treating some unsigned values as negative. For example, treating unsigned bytes as signed results in the following:

hex 000102 7e7f808182 fdfeff
unsigned 012 126127128129130 253254255
signed 012 126127-128-127-126 -3-2-1

The first 128 integers (0-127) are represented the same as unsigned and signed, but the latter 128 are different. The basic idea here is that for values greater than #7f (127) we subtract 256 to get their signed value:

signed = n < 128 ? n : n - 256

It turns out that many unsigned operations "work" even when treating the values as signed. (In other words, you get the same result as you would have using a language with signed integer types.) The following arithmetic instructions work correctly with "signed" values:

#13 #ff ADD returns #12
#02 #03 SUB returns #ff
#02 #ff MUL returns #fe

Be careful! The smallest negative value (-128 for bytes, -32768 for shorts) has no corresponding positive value. This means that some operations will not work as expected:

#80 #ff MUL returns #80 (-128 * -1 = -128)
#00 #80 SUB returns #80 (0 - (-128) = -128)

Also, negative and positive values will "wrap around" in the usual way when dealing with two's-complement representations:

#7f #01 ADD returns #80 (127 + 1 = -128)
#80 #01 SUB returns #7f (-128 - 1 = 127)
#80 #80 ADD returns #00 (-128 + (-128) = 0)

Other instructions will not handle "negative" integers correctly. These routines will safely compare "signed" bytes:

@signed-lth ( x y -- x<y )
	DUP2 #8080 AND2 EQU ?{ LTH JMP2r } LTH #00 NEQ

@signed-gth ( x y -- x>y )
	DUP2 #8080 AND2 EQU ?{ GTH JMP2r } GTH #00 NEQ

Similarly, division will not correctly handle signed values. The simplest way to handle this is to make both values non-negative, do unsigned division (i.e. DIV) and then set the correct sign at the end.

@signed-div ( x y -- x/y )
	DUP2 #8080 AND2 EQU STH DIV STHr ?{ #ff MUL }

The unsigned shift operator treats the sign bit like any other. This means shifting left will lose the sign bit (reversing the sign) and that shifting right will convert the sign bit into a value bit. Signed numbers will also need their own routines for decimal input and output, if those are required by your program.

@signed-print ( num -- )
	( - ) DUP #80 LTH ?{ LIT "- #18 DEO #7f AND #80 SWP SUB }
	( 100 ) DUP #64 DIV signed-print/emit
	( 10 ) DUP #0a DIV signed-print/base
	&base ( digit -- ) #0a DIVk MUL SUB
	&emit ( digit -- ) LIT "0 ADD #18 DEO JMP2r

If you need a sign-aware shift you'll likely want to convert negatives to positive values, perform a shift, and then restore the sign. Keep in mind that -128 cannot be converted to a positive value, and may require special treatment.

Using anonymous functions in Uxntal

In the context of Uxntal, lambdas are managed routine labels designated by curlies, not unlike Postscript's anonymous procedures. Under the hood, the opening bracket is a way to aquire the address at the location in memory of its closing bracket, this allows the destination address to be used like any other label such as a JCI ?{, a JMI, !{ or a plain literal ;{. Lambdas can also be nested into each other.

.button LDZ #00 EQU ?{ will-not-eval }

A lambda block that has no rune will be parsed as a subroutine jump, and so, will be jumped over, and a pointer to the start of the lambda will be pushed to the top of the return stack. The body of the lambda can be unquoted with the STH2r and JSR2 opcodes.

#12 #34 { ADD JMP2r } STH2r JSR2

A lambda can be used to store data inline, and ensure that the content is not inadvertenly run.

{ "hello } STH2r print-lambda

Under the hood, the lambda block writes the length to jump by, and that length can be read to get the length of the lambda block.

@print-lambda ( {str}* -- )
	DUP2k #0002 SUB2 LDA2 ADD2 SWP2
	&l ( -- )
		LDAk .Console/write DEO
		INC2 GTH2k ?&l

A higher-order function is a function that takes a function as an argument or returns one as a result. In the following example, the foreach routine is expecting a pointer to a series of bytes, and a pointer to a function to apply on each byte-long item in memory.

{ 01 02 03 04 05 } STH2r ;double foreach

The body of the double function reads the value of a cell in memory and writes a result equal to twice its value, and the body of the foreach function is merely applying a function to each cell in memory.

@double ( addr* -- addr* )

@foreach ( {bytes}* fn* -- bytes* )
	,&t STR2
	DUP2k #0002 SUB2 LDA2 ADD2 SWP2
	&l ( -- )
		[ LIT2 &t $2 ] JSR2 INC2 GTH2k ?&l

Consider that the whole double function can equally be inlined:

{ 01 02 03 04 05 } STH2r { STH2k LDAk DUP ADD STH2r STA JMP2r } STH2r foreach

As another demonstration of the same concept, we can inline a list of items, here's an implementation of Lisp's member function that returns the member in a list, or nil.

{ =cat =dog =bat } STH2r ;rat member
@member ( {items}* target* -- res/-1* )
	,&t STR2
	DUP2k #0002 SUB2 LDA2 ADD2 SWP2
	&l ( -- )
		LDA2k [ LIT2 &t $2 ] EQU2 ?&found
		INC2 INC2 GTH2k ?&l
	POP2 ;nil &found NIP2 JMP2r

Lambdas can also be nested into one another, but remember, only the outermost layer of a nested lambda is evaluated at a time:

#01 { { "foo $1 } STH2r !print-lambda } STH2r JCN2

Using cons cells and linked lists in Uxntal.

In functional programming languages, a list is the most versatile data type that can be used to store a collection of similar data items. Uxn uses singular opcodes operating on words of equal length, one might come across a problem that is better addressed with routines that operate on ordered lists of items and nested stacks. This 300 bytes implementation of cons cells gives your project that power.

A cons cell is data structure which holds both data and an address to the next cell.

The data slot is known as the CAR, and the address slot is known as the CDR. The purpose is so that ordered lists can be traversed by going from one cell to the next, it also allows one to change the order of cells without manually moving any of the cells' data.

Let us consider the following cons list:

(list cat dog owl)

Also equivalent to the expression:

(cons owl (cons dog (cons cat nil)))

A translation to uxntal assembly would be something like the following, in which POP2 is to get rid of the dangling pointer. This can obviously be made prettier with macros, but we'll keep things straightforward.

nil ;cat cons ;dog cons ;owl cons POP2

Creating a new list

The @alloc routine needs a @memory label to an address with enough space to host the lists, it returns the address of the newly created cell. A list begins with a cons cell whose CAR is a pointer to the cell's data and whose CDR is the next cell. A nil list begins with the nil function pointer.

The @cons routine creates a new cons cell, making the fn its CAR, and the next cell its CDR. It returns the address of the newly created cons cell. @cons is often used to add a single element to the front of a list. This is called consing the element onto the list.

@alloc ( -- cell* )

	[ LIT2 &next :memory ] DUP2 #0004 ADD2 ,&next STR2


@nil ( -- list* )

	alloc ;nil

@cons ( list* fn* -- list* )

	( car ) alloc STH2k STA2
	( cdr ) STH2kr INC2 INC2 STA2


( values ) [
	@cat =dict/cat :nil @dog =dict/dog :nil @bat =dict/bat :nil
	@ant =dict/ant :nil @owl =dict/owl :nil @cow =dict/cow :nil ]

@dict [
	&cat "cat $1 &dog "dog $1 &bat "bat $1
	&ant "ant $1 &owl "owl $1 &cow "cow $1 ]

@memory $4000

Nesting two lists

You can make a list of lists, or nested lists, using the @cons routine with two cons cells from different lists.

nil ;cat cons ;dog cons ;owl cons
nil ;ant cons ;bat cons ;cow cons
nil SWP2 cons SWP2 cons
	echo ( ( owl dog cat ) ( cow bat ant ) )

Joining two lists

Two lists of cons cells can be joined together into one by modifying the last pointer of the second list, and pointing it to the first one.

@last ( list* -- cell* )

		INC2 INC2 LDA2 LDA2k ;nil NEQ2 ?&w


@join ( list-a* list-b* -- list-b* )

	STH2k last INC2 INC2 STA2 STH2r


Finding the length

This routine will run through a list and return its length.

@length ( list* -- length* )

	LIT2r 0000
		LDA2 LDA2k ;nil NEQ2 ?&w


Printing a list

Running through a list and its nested lists is a matter of itterating through each cell's CDR until a ;nil pointer is found. The following routine will run recursively. If the list is a series of function pointers, you can modify this into an evaluation routine.

@echo ( list* -- )

	#2818 DEO #2018 DEO
		LDA2k INC2 INC2 LDA2 ;nil EQU2 ?&value
			( list ) LDA2k echo !&resume
			&value LDA2k LDA2 pstr #2018 DEO
		LDA2k ;nil NEQ2 ?&w
	#2918 DEO #2018 DEO


@pstr ( str* -- )

		LDAk #18 DEO
		INC2 LDAk ?&w


Using stack effect definitions in Uxntal.

Type inference in Uxntal is done by checking the stack effect declarations of words, against the sum of stack changes predicted to occur based on the items in their bodies.

Words that do not pass the stack-checker are generating a warning, and so essentially this defines a very basic and permissive type system that nevertheless catches some invalid programs and enables compiler optimizations.

@routine ( a b -: c ) Ok.

The simplest case is when a piece of code does not have any branches or recursion, and merely pushes literals and calls words. Pushing a literal has stack effect ( -- x ). The stack effect of most words is always known statically from the declaration.

@add ( a* b* -- c* ) Warning: Imbalance in @add of +2

Branch Validation

A word calling a subroutine that would force an exit of the parent routine with an imbalanced stack also triggers an error.

@routine ( a b -: c ) Warning: Imbalance in @routine of +1
	EQUk ?&sub-routine
	POP2 #0a JMP2r
	&sub-routine ( a b -: c* )
		POP2 #000b JMP2r

Return-Stack Passing

The following example shows some extra syntax where stack elements passed through the return stack are prefixed with a backtick:

@wcpy ( src* des* -: cap* ) Ok.
	&w ( src* `des* -: cap* )
		INC2r INC2 LDAk #20 GTH ?&w
	( cap ) #00 STH2kr STA
	INC2r STH2r JMP2r

Each jump label must be defined for a routine's body to validate with the arity checker, but labels can also be used anywhere in a routine to test against a specific arity in time, like you would a breakpoint.

Fall-through Type

The fall-through comment allows to validate a tail-less routine(or, that does not return or break), by including the stack effect of the following routine in memory.

@falling ( a b c -: c ) Ok.
	( >> )
@next-routine ( a b -: res )

More research has to be done in this space, but it would be interesting to check a routine's purity by making sure that no ST/LD/DE opcodes can be reached.

Runtime Validation

Lastly, a runtime specific solution to validate the stack state at any one point during the execution of a program, is to read the System/wst port and compare it against a given stack pointer byte value. Note: that the value in the wst and rst ports include the stack pointer byte.

@on-reset ( -> )
	#abcd DUP2 
	.System/wst DEI #05 EQU ?{
		#01 .System/debug DEO

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.

Hexadecimal Numbers

To print an hexadecimal number:

@phex ( short* -- )
	SWP phex/b
	&b ( -- )
		DUP #04 SFT phex/c
	&c ( -- )
		#0f AND DUP #09 GTH #27 MUL ADD [ LIT "0 ] ADD #18 DEO

To convert an hexadecimal string to a value:

@shex ( str* -- val* )
	[ LIT2r 0000 ]
	&w ( -- )
		( validate ) LDAk chex INC #00 EQU ?&end
		( accumulate ) [ LITr 40 ] SFT2r
		( combine ) LDAk chex [ LITr 00 ] STH ADD2r
		( continue ) INC2 LDAk ?&w
	&end POP2 STH2r JMP2r

To convert an hexadecimal character to a nibble:

@chex ( c -- val? )
	( dec ) [ LIT "0 ] SUB DUP #09 GTH ?{ JMP2r }
	( hex ) #27 SUB DUP #0f GTH ?{ JMP2r }
	( err ) POP #ff JMP2r

Decimal Numbers

To convert a decimal string to a value:

@pdec ( short* -- )
	#2710 [ LIT2r 00fb ]
	&w ( -- )
		DIV2k #000a DIV2k MUL2 SUB2 SWPr EQUk OVR STHkr EQU AND ?{
			DUP [ LIT "0 ] ADD #19 DEO
			INCr }
		POP2 #000a DIV2 SWPr INCr STHkr ?&w

To convert a decimal string to a hexadecimal value.

@sdec ( str* -- val* )
	[ LIT2r 0000 ]
	&w ( -- )
		( validate ) LDAk [ LIT "0 ] SUB #09 GTH ?&end
		( accumulate ) [ LIT2r 000a ] MUL2r
		( combine ) LDAk [ LIT "0 ] SUB [ LITr 00 ] STH ADD2r
		( continue ) INC2 LDAk ?&w
	&end POP2 STH2r JMP2r


To print a string.

@pstr ( str* -- )
	&w ( -- )
		LDAk #18 DEO
		INC2 & LDAk ?&w

Helpers for strings:



To print an entire page of memory:

@pmem ( addr* -- )
	&l ( -- )
		ADD2k LDA phex/b
		DUP #0f AND #0f NEQ #16 MUL #0a ADD #18 DEO
		INC NEQk ?&l

Helpers for memory.


Helpers for bitwise operations.

@popcnt ( byte -- count ) LITr 00 #00 &w SFTk #01 AND STH ADDr INC SFTk ?&w POP2 STHr 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
	( not a leap year if divisible by 100 )
	( but not divisible by 400 )
	DUP2 #0064 ( MOD2 ) DIV2k MUL2 SUB2 #0000 EQU2 ?¬-leap
	( leap year if not divisible by 100 )
	( but divisible by 4 )
	DUP2 #0003 AND2 #0000 EQU2 ?&leap
	( 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.

@smax ( x* y* -> smax* ) EOR2k POP #80 AND ?min !max
@min ( x* y* -> min* ) LTH2k JMP SWP2 POP2 JMP2r
@max ( x* y* -> max* ) LTH2k JMP SWP2 NIP2 JMP2r

( 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 }