XXIIVV

Uxntal syntax

Uppercased opcodes are reserved words, hexadecimal bytes and shorts are always lowercase. Comments are within parentheses, and the square brackets are ignored.

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
!jmi?jci

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.

Typed uxntal is an inference system based on routine definitions.

Type inference in Uxntal is done by checking the stack effect declarations of words before they can be run, against the cumulative stack state of each item in the definition of each word.

@routine ( a b -- c ) Ok.
	MUL
JMP2r

The simplest case is when a piece of code does not have any branches or recursion, and just 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
	DUP2 ADD2
JMP2r 

Branch Type

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

@routine ( a -- ) Ok.
	#01 ?&branch POP 
JMP2r
	&branch ( a ~- )
		POP
JMP2r

Fall-through Type

The tail-less fall-through type allows for routine to not return, and check the stack balance including that of the next routine in memory.

@falling ( a b c -+ c )
	POP
@next-routine ( a b -- res )
	ADD
JMP2r

Return-Stack Passing

Passing items to a branch through the return-stack, is done by prefixing the definition with a backtick.

@routine ( a -- b )
	STHk
	DUP #01 EQU ?&branch
	POPr
JMP2r
	&branch ( a `a ~- b )
		POP STHr
JMP2r

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

Helpers for bitwise operations.

@popcnt ( byte -- count ) LITr 00 #00 &w SFTk #01 AND STH ADDr INC SFTk ?&w POP2 STHr 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
@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 )

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

Structs allow to access individual members in a data structure.

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.

Example

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

BRK

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

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

A cons cell is a data structure which holds both data and the address to the following cell.

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.

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

JMP2r

@nil ( -- list* )

	alloc ;nil

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

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

JMP2r

( 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* )

	&w
		INC2 INC2 LDA2 LDA2k ;nil NEQ2 ?&w
	INC2 INC2

JMP2r

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

	STH2k last INC2 INC2 STA2 STH2r

JMP2r

Finding the length

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

@length ( list* -- length* )

	LIT2r 0000
	&w
		INC2 INC2 INC2r
		LDA2 LDA2k ;nil NEQ2 ?&w
	POP2 STH2r

JMP2r

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
	&w
		LDA2k INC2 INC2 LDA2 ;nil EQU2 ?&value
			( list ) LDA2k echo !&resume
			&value LDA2k LDA2 pstr #2018 DEO
		&resume
		INC2 INC2 LDA2
		LDA2k ;nil NEQ2 ?&w
	POP2
	#2918 DEO #2018 DEO

JMP2r

@pstr ( str* -- )

	&w
		LDAk #18 DEO
		INC2 LDAk ?&w
	POP2

JMP2r