pages/uxn_notes.gn Mineral Existence

Uxn Notes

Uxn is an 8-bit virtual stack machine, created by Devine, and programmed in uxnambly, a unique flavor of assembly.

These are my notes for it.

Opcodes
Stack
0x00 BRK break
0x01 LIT literal ++
0x02 ---
0x03 POP pop a
0x04 DUP duplicate a a a
0x05 SWP swap a b b a
0x06 OVR over a b a b a
0x07 ROT rotate a b c b c a


Logic
0x08 EQU equal a b c
0x09 NEQ not equal a b c
0x0a GTH greater than a b c
0x0b LTH less than a b c
0x0c JMP jump {a}
0x0d JCN jump conditional a {b}
0x0e JSR jump stash {a} rs
0x0f STH stash a rs


Memory
0x10 LDZ load zero page a {b}
0x11 STZ store zero page a b
0x12 LDR load relative a {b}
0x13 STR store relative {b} a
0x14 LDA load absolute a* {b}
0x15 STA store absolute {b} a*
0x16 DEI device in a {b}
0x17 DEO device out {b} a rs


Arithmetic
0x18 ADD add a b c
0x19 SUB subtract a b c
0x1a MUL multiply a b c
0x1b DIV divide a b c
0x1c AND and a b c
0x1d ORA or a b c
0x1e EOR exclusive or a b c
0x1f SFT shit a b c


Modes

Keep mode: lets operators work without consuming items from the stack. Results are simply pushed to the top.

Short mode: allows operators to operate on 2-byte data, pushing and popping extra byte from the stack. With jump opcodes, short mode jumps to absolute memory addresses.

Return mode: sets opcodes to operate on the return stack. Certain opcodes, such as STH, already operate on the stack, so applying the return mode to those opcodes let them operate on the working stack.



Runes
% macro-define
| pad absolute
$ pad relative
@ label-define
& sublabel-define
/ sublabel spacer
# literal hex
. literal addr (zero page)
, literal addr (relative)
; literal addr (absolute)
: raw addr
' raw char
{quote} raw word

Hello world, part 2!

( dev/console )

|10 @Console [ &pad $8 &char ]

( init )

|0100 ( -> )
	
	;hello-word 

	&loop
		( send ) LDAk .Console/char DEO
		( incr ) #0001 ADD2
		( loop ) LDAk ,&loop JCN
	POP2
	
BRK

@hello-word "Hello 20 "World!

This is the new version of hello world (as of commit ea0584a2), which is significantly more concise, and uses some of the new opcodes. If you want an example which is a bit longer, I'd recommend checking out the first breakdown.

The couple of lines are the same as they were before, so I won't go over them again.

To start, everything happens in one space, and isn't broken up between functions. The general overview of how the program works is the same as the older version; the newer version is just a bit more concise.

Once again, let's go through this line by line, starting at line 9.

;hello-world
└──────────┘
      A

This one's pretty simple (though it may look weird if you're not quite familiar with uxn's concepts yet). The semicolon tells the cpu to use absolute addresses, and in this case the address it's referring to is the `@hello-world` label. That absolute address is added to the stack, and the program continues to line 11.

&loop 
└───┘ 
  A

This one is also super short; part A just creates a sublabel called `loop` for later use.

( send ) LDAk .Console/char DEO
└──────┘ └──┘ └───────────────┘
   A      B           C

Part A is just a comment, but part B is where we see a new opcode! `LDA` takes an absolute address and adds the value at that address to the stack. Here it's used with the keep mode, so it doesn't consume that address (which points to the start of our `"hello 20 "world`) text. With the character at that address now on the top of the stack, part C takes that value and prints it to the console.

( incr ) #0001 ADD2
└──────┘ └────────┘
   A         B

Part B, as the comment says, just increments our address, which is now at the top of the stack. The address on the stack now points to the next character of the text.

( loop ) LDAk ,&loop JCN
└──────┘ └──┘ └────────┘
   A      B       C

Part B is the same thing as used before, it gets the value at the address, keeps the address on the stack, and adds the value to the top of the stack. This value is then used by part C, which passes two arguments, (that value, and the relative address of the `loop` sublabel which was defined earlier) to the `JCN` opcode. `JCN` takes a boolean value and an address, and if the boolean is true (not a zero), it jumps to that address. Here, it looks at that value, which is normally a character, and repeats. However, if the address has been incremented past where we have characters, the value will be #00 (I think, but the point is it's a zero), and the loop stops.

The final opcode, `POP2`, removes the top two-byte item on the stack, which is our address. With everything done and "hello world" printed to the console, we break.

Breakdown of Hello world

( dev/console )

%RTN { JMP2r }

( devices )

|10 @Console    [ &pad $8 &char $1 ]

( init )

|0100 ( -> )
	
	;hello-world ;print JSR2
	
BRK

@print ( addr -- )
	
	&loop
		( send ) DUP2 GET .Console/char DEO
		( incr ) #0001 ADD2
		( loop ) DUP2 GET #00 NEQ ,&loop JNZ
	POP2

RTN

@hello-world [ "Hello 20 "World! ]

In uxn, everything is a stack of data. This often means that commands seem backward, as data is added to that stack before operations can use it.

The second line (`%RTN { JMP2r }`) simply declares a macro labeled "RTN" which returns the two bytes at the top of the stack.

The next line is very interesting, and definitely confused me for a while!

|10 @Console    [ &pad $8 &char $1 ]
└──────────┘      └─────┘ └──────┘
      A              B       C

Part A starts the line by making sense. It defines a label: "Console", which is an address to the console device (at 0110).

Part B is where things start to get a bit confusing, and where Devine's [devices](https://wiki.xxiivv.com/site/uxnemu.html) table is very helpful. The first part, `&pad`, defines a sublabel (of `Console`), which is assigned by the `$8` part to an address one byte further than the `Console` address. If you look at the previously mentioned table, you'll see that this puts us right up to the output section of the console's memory space. This sublabel is never actually called.

Part C then uses the same method as part B to assign the sublabel `char` to one bit past the `pad` label, which is the start of the "char" section of the console device!

The main part of the program starts with the line "|0100 ( -> )", which moves us into the "system" device's memory space.

;hello-world ;print JSR2
└──────────┘ └────┘ └──┘
      A        B     C

Part A gets the address of the `hello-world` label and adds to the top of the stack. Part B then gets the address of the `print` label and pushes that to the stack, moving the first label down (really down four bytes, because each label is two bytes). The `JSR` opcode jumps the program to an address popped from the top of the stack, and the `2` modifier tells it to use two byte addresses. Because the top address is to `print`, we now move there.

The main part of the `print` section is in the `( send )` line, where the program outputs to the terminal.

( send ) DUP2 GET .Console/char DEO
└──────┘ └──┘ └─┘ └───────────┘ └─┘
   A      B    C        D        E

Part A is just a comment.

Part B duplicates the top two bytes of the stack, which here is the address to the `print` section.

Part C consumes and adds the top item to the stack, which is that address to `print`

Part D adds the device address for the console's char byte to the stack.

Part E consumes both the device address and the address of the `print` label, which is essentially the first byte of the `"hello 20 "world` string.

The next two lines make a bit more sense, but can be a bit unintuitive if you aren't used to working with memory address (like I am).

( incr ) #0001 ADD2

This is relatively straight-forward, it adds one byte to the item at the top of the stack, which is... the `print` address! Thus, the program now has an address on the top of the stack which points to the second byte in the `"hello 20 "world` string.

While a bit scary, I promise this next line isn't too complicated!

( loop ) DUP2 GET #00 NEQ ,&loop JNZ
         └──────┘ └─────┘ └────────┘
            A        B         C

At part A we duplicate and `GET` the two bytes at the top of the stack (remember, that's now the address to the next byte of our string), and the value at that address is compared to `#00`, the ascii null char, at part B. The `NEQ` opcode, as the name suggests, checks if its inputs are not equal. If they aren't equal, it returns a 1, and if they are equal it returns a 0.

Part C starts by adding the literal relative address of the `&loop` sublabel to the stack, and then the conditional jump operator, `JNZ`, uses the previous boolean added by part B to decide if it should jump to that address.

In this way, the loop repeats, printing each byte, until it hits a null byte, at which point it stops and returns.

And there we have it: Hello world!