Uxntal Syntax

In concatenative 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.

#01 DUP ADD #03 MUL ( result: 06 )

Parentheses indicate comments, lowercase #numbers are hexadecimal literals to be pushed onto the stack.

Example Program

The first line of our example begins with the padding token |10, followed by definitions for @labels, &sublabels and $length that aligns with the various ports of the Console device so that we can reference it by name in our program. To learn more, see Uxntal Structs.

The following program is not the most efficient way of printing a string, merely a length of code that covers most basic functionalities of the language.

The second segment moves the program location to the address 0x0100, which is where the first page of memory ends, and where all Uxn programs begin. Next, inside parentheses, is a comment with the arrow symbol indicating that the following operation is a vector. To learn more, see Uxntal Notation.

The ;my-string token pushes an absolute address, made of two bytes, pointing to a series of letters in memory. Values pushed to the stack in this fashion are called a literals, as opposed to values stored in memory which are called immediates. Next, we call the @print-text routine.

Both &while and @while are ways to define labels, but using the ampersand rune will prefix our new label with the name of the last parent label, creating @print-text/while. To learn more, see Uxntal Scope.

Next, the LDAk opcode loads the byte in memory found at that address; the ASCII letter H, to the top of the stack. The k-mode indicates that the operation will not consume the address. That value is sent to the device port #18, defined by our Console label and its sublabels, with DEO which prints that character to the terminal. To learn more, see Uxntal Devices.

We then increment the absolute address found on top of the stack with INC2, we use the 2-mode because the address is made of two bytes. We load the byte at the incremented value and do a conditional immediate 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.

To summarize, uppercased opcodes are reserved words, lowercase hexadecimal numbers are bytes and shorts, parentheses are comments, curlies are lambdas, and square brackets are used for organization. Runes are special characters at the start of a word that define its meaning, here is the full list:

Padding RunesLiteral Hex Rune
|absolute$relative#literal hex
Label RunesAscii Runes
@parent&child"raw ascii
Addressing RunesPre-processor Runes
,literal relative_raw relative%macro~include
.literal zero-page-raw zero-page
;literal absolute=raw absolute
Immediate Runes

Using inline functions in Uxntal.

A macro is a way of defining inline routines, it allows to create new words that will be replaced by the body of the macro, as opposed to a jump where the program counter will move to a routine and back, therefore it needs to be defined before its usage, as follow:

%modulo ( num denum -- res ) {

@routine ( -- c* )
	#18 #03 modulo

In the previous example, the token modulo will get replaced by the body of the macro during assembly:

@routine ( -- c* )
	#18 #03 DIVk MUL SUB

Note: A macro does not have a scope, so it may not contain sublabels if the macro is to be used multiple time within a single parent label, lambdas are immune to this limitation.

Using and defining data structures in Uxntal.

Enums are labels with unique values that can be used as constants in a program, they begin by rolling back the program address with |00:

|00 @Suit &clubs $1 &diamonds $1 &hearts $1 &spades

@is-diamond ( suit -- f )
	.Suit/clubs EQU

Structs define padded labels, for example the ;person/age label holds a value of 2, using that offset allows to access specific members of a data structure by applying the sublabels to a pointer:

|00 @Person &name $2 &age $1 &height $2
	=dict/melanye 2a 008c
	=dict/alexane 2c 009a

@get-height ( member* -- height* )
	;Person/height ADD2 LDA2

Constants are labels that hold a specific value through the entire execution of the program. They allow to create label that can be used in place of a number, making the code more readable.

|1400 @limit

@within-limit ( value* -- f )
	;limit LTH2

Pro Tip: Labels can also be used with the padding runes to define a global length. For example, if one needs to specify a length of 0x30 for multiple members of a struct, a value can be specified as follow:

|30 @length
|00 @struct &field $length

Using and defining scope in Uxntal.

Uxntal objects are defined statically using a @label token, it allows for the enclosed methods to access local &labels. The example below creates an object with the method get-x, accessible from outside the scope as Object/get-x. By capitalizing the object name, we communicate to the assembler that the label will not be called and should not raise a warning.


&x $1 &y $1

&get-x ( -- x )
	,&x LDR

New methods and members can be appended to an existing scope by creating a label with the scope name followed by a slash and the name of the extension. The &labels declared within the extension have the same permissions for accessing local labels as during the object definition. To learn more, see symbols.

@Object/get-y ( -- y )
	,&y LDR

When calling local routines the scope's name can be omitted. To see a complete example in that pseudo object-oriented style, see timer.tal.

&get-both ( -- x y )

Using stack effect definitions in Uxntal.

The Uxntal notation follows that of the Forth programming language, where each item on the left of the -- spacer is the state of the stack before, and to the right, the state of the stack after:

@routine ( a b -- a b c )

By default, single items are a byte long, and shorts are indicated with a * suffix, the order in which they appear is the order of the stack with the top item to the right:

@routine ( a b* -- b* a )

In some rare cases, an item is passed to a routine via the return stack, in this case it is prefixed with the ` character indicates items in the return stack to its right. The return address is omitted from the stack effect notation.

@routine ( a* `b* -- )

If a specific item on the stack needs to be explicit about it being a pointer that needs to be unquoted, the concatenative notation is written within square brackets.

@routine ( {fn}* -- )

Further notation is available for program verification.

Using anonymous functions in Uxntal

In the context of Uxntal, lambdas is a label designated by a curly bracket that points to its associated closing bracket, and can be nested. Under the hood, the opening bracket assembles to the address of the closing bracket which allows the destination address to be used like any other label such as a JCI ?{, a JMI, !{ or a plain literal ;{.

Counted Strings

Similarly to counted strings, lambdas can encode strings in memory by preceeding their content by the address of the end of the string, so the reading of that string data is not looking for a null byte, but running until reaching the bounds. The advantage is that the address of the next character to append is readily available.

@on-reset ( -> )
    ;cstr print-counted

@cstr ={ "foo 20 "bar }

@print-counted ( cstr* -- )
    LDA2k SWP2 INC2 INC2
    &l ( -- )
        LDAk #18 DEO
        INC2 GTH2k ?&l
    POP2 POP2 JMP2r

Data Structures

We can inline a list of items, here's an implementation a function that returns the member in a list, or nil. Notice how the lambda jump requires the list address to be moved from the return stack.

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

Unless Blocks

It is important to notice that a in the case of a conditional jump, the lambda's content is jumped over when the flag byte is true.

.button LDZ ?{ skipped-on-button }

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

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

Higher-Order Functions

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

Uxntal Doors

The ability to treat instructions as data makes programs that write programs possible. Self-modifying code(SMC) is generally considered harmful, and is therefore not permitted in most modern computer architectures today.

Action at a distance is an anti-pattern in computer science in which behavior in one part of a program modifies operations in another part of the program. This anti-pattern should be avoided whenever possible, but if wielded carefully SMC can become a practical ally when writing Uxntal.

A door is an allocation of local memory that can store state across vectors.

@routine ( -- i )
	[ LIT &door $1 ] INCk ,&door STR

Caching Doors

In most cases, SMC is used to cache data that would otherwise be difficult or slow to retrieve, like when writing a responsive application that would make frequent requests to a device.

In the following door, we are comparing the state of the mouse device between vector events, we could store the previous state in a zero-page variable, but keeping the value locally allows to reserve a byte from within the context where it is needed, and is faster by being inlined.

@on-mouse ( -> )
	[ LIT2 &last $1 -Mouse/state ] DEI 
		DUP ,&last STR
		EORk ?&changed 

Callback Doors

To chain operations across vectors, one might try passing the next operation pointer on the stack, but since we cannot be certain which vector will happen next, we can't expect a specific stack state between events. A safer way is to write the next operation directly into a door where it will be needed, ideally preserving the label scope.

@set-animation ( callback* -- )
	,&callback STR2
	;&run .Screen/vector DEO2

&run ( -> )
	[ LIT &time f0 ] 
		INCk ,&time STR
		#00 EQU ?&done

&done ( -> )
	[ LIT2 &callback $2 ] JSR2

Depth-Punching Doors

Routines should try and avoid accessing stack values that are further than 2 or 3 shorts deep on either stacks, but sometimes it cannot be helped. In the following example, we want to run a function over each value of a 2d array. Instead of juggling the stacks on each iteration to bring out the function pointer, it is often more efficient to write the function pointer across the nested loop.

@each-pixel ( fn* -- )
	,&fn STR2
			DUP STHkr [ LIT2 &fn $2 ] JSR2
			INC GTHk ?&x
		INC GTHk ?&h

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 -- res )
	DUP2 #8080 AND2 EQU ?&diff LTH JMP2r &diff LTH #00 NEQ JMP2r

@signed-gth ( x y -- res )
	DUP2 #8080 AND2 EQU ?&diff GTH JMP2r &diff GTH #00 NEQ JMP2r

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.

@abs ( x -- abs-x sign )

@signed-div ( x y -- x/y )

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.

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 /b
	&b ( byte -: )
		DUP #04 SFT /c
	&c ( byte -: )
		#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 ( str* `acc* -: val* )
	LDAk chex INC #00 EQU ?{
		[ LITr 40 ] SFT2r LDAk chex [ LITr 00 ] STH
		ADD2r INC2 LDAk ?&w }

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 print a decimal short to decimal:

@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 print a decimal byte to decimal:

@print-dec ( dec -- )
	DUP #64 DIV print-num/try
	DUP #0a DIV print-num/try
	( >> )

@print-num ( num -- )
	#0a DIVk MUL SUB [ LIT "0 ] ADD #18 DEO
	&try ( num -- )
		DUP ?print-num

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* -: )
	LDAk #18 DEO
	INC2 & LDAk ?<pstr>

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


@msfl ( b* a* len* -- )
	SWP2 EQU2k ?&end
	&l ( -- )
		INC2 GTH2k ?&l
	POP2 POP2 &end POP2r JMP2r

@msfr ( b* a* len* -- )
	EQU2k ?&end
	&l ( -- )
		#0001 SUB2 LTH2k ?&l
	POP2 POP2 &end POP2r JMP2r


@prng-init ( -- )
	[ LIT2 00 -DateTime/second ] DEI
		[ LIT2 00 -DateTime/minute ] DEI #60 SFT2 EOR2
		[ LIT2 00 -DateTime/hour ] DEI #c0 SFT2 EOR2 ,prng/x STR2
	[ LIT2 00 -DateTime/hour ] DEI #04 SFT2
		[ LIT2 00 -DateTime/day ] DEI #10 SFT2 EOR2
		[ LIT2 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
@mod ( x y -- z ) DIVk MUL SUB JMP2r
@mod2 ( x* y* -- z* ) DIV2k MUL2 SUB2 JMP2r

( Signed macros )

@abs ( a -- b ) DUP #80 AND #00 EQU ?{ #00 SWP SUB } JMP2r
@abs2 ( a* -- b* ) DUP2k #1f SFT2 MUL2 SUB2 JMP2r
@lts2 ( a* b* -- f ) #8000 STH2k ADD2 SWP2 STH2r ADD2 GTH2 JMP2r
@gts2 ( a* b* -- f ) #8000 STH2k ADD2 SWP2 STH2r ADD2 LTH2 JMP2r

( Binary macros )

@rol ( x y -- z ) DUP #07 SFT SWP #10 SFT ADD JMP2r
@ror ( x y -- z ) DUP #70 SFT SWP #01 SFT ADD JMP2r
@rol2 ( x* y* -- z* ) DUP2 #0f SFT2 SWP2 #10 SFT2 ADD2 JMP2r
@ror2 ( x* y* -- z* ) DUP2 #f0 SFT2 SWP2 #01 SFT2 ADD2 JMP2r

incoming uxn