Notice
This article was translated by AI.
See the original article here (in Korean).
Uxntal is the language of Uxn.
Uxntal is an Assembly language that compiles into ROM to run on the Uxn CPU.
Note
Like any other programming language, Uxntal has an interconnected structure, making it difficult to learn sequentially. Here, we have organized the concepts from most independent to least.
Read Only Memory (ROM)
ROM
is assembled from Uxntal. Despite being called ROM (Read Only Memory), once it is loaded into Uxn, it can be modified using STA and similar opcodes.
Most of Uxntal’s data, including opcodes , is converted directly into bytes.
Uxn loads ROM starting at ROM 0x0100
by default, and writing begins at 0x0100
. However, you can start writing at a specific address if desired.
Warning
Zero-page
Addresses below0x0100are called zero-page, and they are used by the Uxn/Varvara system. If you try to write there, you will get an error, and the actual ROM also starts writing from0x0100. Therefore, you typically only attach labels to those addresses.|00 #00 ( ASSEMBLY ERROR Writing zero-page: #00 in @on-reset, test.tal:1. )
Random Access Memory (RAM)
Uxn provides RAM
consisting of two 256-byte stacks.
RAM consists of the WST
(Working STack), which is the default workspace, and the RST
(Return STack), which can be used as additional storage for operations.
Memory:
- WST (Working Stack)
- RST (Return Stack)
Data Structure
Data is categorized into byte
(integers in the range 0~255 (28-1)) and short
(integers in the range 0~65535 (216-1)), but the only difference is the amount of space they occupy on the stack.
Byte data can be inserted onto the stack as two-digit hex (0x00
), and short data as four-digit hex (0x0000
), but there is no distinction within the stack itself.
#00 #ab #1234
( RESULT
WST: 00 ab 12 34
)
Caution
This may be confusing, but
#00by itself is the code that inserts the00byte onto the top of WST.
Literal(LIT)
Data:
Byte
Short = Byte × 2
Comments
Strings wrapped in ( and ) are treated as comments.
(Spaces are included.)
( This is comment. )
(
This is comment too )
( ( You can also nest ) )
Runes
Uxntal has several special characters that provide various features.
Labels
Labels serve to assign a name to a specific ROM address .
Labels are divided into scopes and sublabels. A scope is created with a string starting with @. A sublabel is created with a string starting with &, or in the form @scope/name. As you can see, sublabels are nested within a scope.
In other words, the following two code snippets produce the same result:
@banana &peel
@banana @banana/peel
Note
Uxntal supports jumping to labels.
Warning
Uxntal treats hex as regular data, so a label cannot consist solely of characters
a-f.#00 bed INC @bed INC ( ASSEMBLY ERROR Hexadecimal invalid: bed in @on-reset, hello.tal:2. )
Labels can be used for the following purposes.
Function is literally a function — it is executed at a certain position and then returns to that position.
@func ( -- )
( ... )
JMP2r
Constant — since uxntal has no variable syntax, data is placed in ROM and its address is referenced and used as a variable.
@text
"Hello 20 "World 0a
Vector is a concept similar to an event listener.
It is almost exclusively used in Varvara, so we will cover it in more detail when we discuss Varvara later.
Note
In fact, at
0x0100— the starting point of the Uxntal ROM — a vector called@on-resetis assigned by default.
Padding
You can start writing at a specific ROM memory
address using |. However, since Uxn will execute from ROM ROM 0x0100
, you must write your code at 0x0100
if you want it to run. Also, you cannot write to memory that has already been passed, so you must write in ascending order. (Attaching labels alone is possible.)
|100 #00 INC routine INC
|200 @routine
( RESULT
WST: 01
RST: 01 06
)
|100 #00 INC r-one INC
|200 @r-one r-two
|150 @r-two INC
( ASSEMBLY ERROR
Writing rewind: INC in @r-two, test.tal:3.
)
You can move relative to the current ROM address
using $.
By combining it with labels, you can create a struct at a specific position in ROM .
|d0 @player &x $2 &y $2 &health $1
(
The player struct has x and y as shorts, and health as a byte.
You can reference them in code as player/x, etc.
)
Note
As a result of padding, the ROM file looks like this:
Details
( test.tal ) |100 "A |150 "B $4f "C$ ./uxnasm test.tal test.rom $ xxd test.rom 00000000: 4100 0000 0000 0000 0000 0000 0000 0000 A............... 00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000050: 4200 0000 0000 0000 0000 0000 0000 0000 B............... 00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000090: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 000000a0: 43 C
Number, ASCII Runes
Runes for inserting data onto the stack
You can insert a hex value onto the stack using
#. Literal (LIT)You can insert each character of an ASCII string as a byte into ROM using
".Warning
This is not inserting onto the stack during program execution. When the assembler encounters
", it converts the following string into ASCII hex and inserts it directly into ROM .( test.rom ) "banana$ ./uxnasm test.tal test.rom $ xxd hello.rom 00000000: 6261 6e61 6e61 banana
Addressing
Uxntal has 6 runes for handling ROM addresses . Each rune serves to retrieve a label’s address .
Literal- fetches the address at runtime and inserts it onto the stack .
- Literal Relative(
,) inserts the distance from the current PC to the label’s address onto the stack. - Literal Zero Page(
.) inserts the address (byte) of a zero-page label onto the stack. - Literal Absolute(
;) inserts the address (short) of a label onto the stack.
Note
( test.rom ) ;label @labelResult & Analyze
$ ./uxnasm test.tal test.rom $ xxd test.rom 00000000: a001 03 ... ^^~~~~~ (1)---+ +--(2)
- (1)
0x0a: The opcode for LIT2 , which inserts the following short onto the stack- (2)
0x0103: The ROM address of@label
Raw- fetches the address at assembly time and inserts it into ROM .
- Raw Relative(
_) inserts the distance from the current PC to the label’s address into ROM. - Raw Zero Page(
-) inserts the address (byte) of a zero-page label into ROM. - Raw Absolute(
=) inserts the address (short) of a label into ROM.
Note
( test.rom ) =label @labelResult & Analyze
$ ./uxnasm test.tal test.rom $ xxd test.rom 00000000: 0102 .. ^^^^ (1)---+
- (1)
0x0102: The ROM address of@labelAs you can see from the result, Raw Labeling inserts the label’s ROM address at assembly time, so the label’s address (
0x0102) is one byte before Literal Labeling (0x0103).
Wrappings
(,)parentheses are comments.[,]square brackets are themselves ignored, unlike(,)where the content inside is ignored. Square brackets are used as notation for grouping opcodes for readability.{,}curly braces are used as anonymous, i.e., nameless routines.
Macro
You can create macros with %name { }.
%modulo ( num denum -- res ) {
DIVk MUL SUB }
@routine ( -- c* )
#18 #03 modulo JMP2r
Opcodes
Uxntal has no operator precedence; operations are simply executed on the stack in the order they are programmed.
The PC (Program Counter) is the ROM address
of the next opcode, and it increases by 21 after each opcode execution.2
#0f #06 #04 ADD MUL
( RESULT
WST: 96
)
0x06+0x04=0x0a(WST:0f 0a)0x0f×0x0a=0x96(WST:96)
Notation
Uxntal follows the notation of the Forth programming language.
Each item before -- represents the stack state before execution, and each item after represents the stack state after execution.
#12 #34 ADD
( RESULT
WST: 46
)
ADD ( a b -- c )
For operations targeting shorts
, the item is annotated with * after it.
#1234 #abcd ADD2
( RESULT
WST: be 01
)
ADD2 ( a* b* -- c* )
For operations that affect the RST
, the RST state is annotated after ..
#12 STH
( RESULT
WST:
RST: 12
)
STH ( a -- . a)
( BEFORE RST: 12 )
STHr
( RESULT
WST: 12
)
STHr ( . a -- a )
Note
Stash
Stash(
STH) consumes the top byte of the stack(WST) and inserts it into the RST.STHrconsumes the top byte of the RST and inserts it into the WST.
Mode
Uxntal has 3 modes.
| Mode | Description |
|---|---|
Short mode 2 | Performs operation on shorts |
Keep mode k | Preserves the operand values |
Return mode r | Performs operation on the RST |
Short mode consumes shorts (two bytes) instead of bytes for operations.
For jump-related opcodes3, it uses absolute positions.
#0123 #abcd ADD2
( RESULT
WST: ac f0
)
ADD2 ( a* b* -- c* )
Keep mode preserves the values used in the operation instead of consuming them.
#01 #23 ADDk
( RESULT
WST: 01 23 24
)
ADD2 ( a b -- a b c )
Return mode performs operations on the RST instead of the WST.
( BEFORE RST: 01 23 )
ADDr
( RESULT
RST: 24
)
ADDr ( . a b -- . c )
Note
Stash
Stash(
STH) consumes the top byte of the stack and inserts it into the RST.STHrconsumes the top byte of the RST and inserts it into the WST.
These modes can be combined. Each opcode has a total of 8 variants.4
ADD ( a b -- c )
ADD2 ( a* b* -- c* )
ADDk ( a b -- a b c )
ADDr ( . a b -- . c )
ADD2k ( a* b* -- a* b* c* )
ADDkr ( . a b -- . a b c )
ADD2r ( . a* b* -- . c* )
ADD2kr ( . a* b* -- . a* b* c* )
Therefore, each of Uxntal’s opcodes occupies 2 bytes.
| OPCODE | |||||||
|---|---|---|---|---|---|---|---|
| 2 | k | r | opcode id | ||||
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Immediate Opcodes
Immediate Opcodes do not consume data from the top of the stack; instead, they immediately fetch and use the value written in ROM
after the opcode.
Since they only work in a predetermined way, immediate opcodes do not have modes.
| Name | Opcode | Syntax |
|---|---|---|
| Literal | LIT | # |
| Jump Immediate | JMI | !routine |
| Jump Conditional Immediate | JCI | ?routine |
| Jump Stash Return Immediate | JSI | routine |
Literal(LIT) uses # to insert the next data onto the stack.
#12 #abcd
( RESULT
WST: 12 ab cd
)
Note
Literal(
LIT)Literal(
LIT) can actually be used beyond just#; it is an opcode that inserts the next data from ROM onto the stack as-is.( test.tal ) LIT "A ( RESULT WST: 41 )$ ./uxnasm test.tal test.rom $ xxd test.rom 00000000: 8041 .A ^^~~ (1)---+ +--(2)
- (1)
0x80: The opcode forLIT, which inserts the next byte (0x41) from ROM onto the stack.- (2)
0x41: The ASCII value ofA
LIT2of course consumes two bytes from ROM .
Jump Immediate(JMI) jumps to a routine by writing its name after !.
#00 !routine INC
@routine INC INC
( RESULT
WST: 02
)
Without the jump opcode, the result would have been 03, but since it jumps to the @routine routine (address), the result is 02.
Jump Conditional Immediate(JCI) conditionally jumps by writing the routine’s name after ?.
When executed, it takes the top byte from the stack
and jumps to the routine based on its value. In other words, a conditional jump consumes a byte.
When ? is followed by a routine, it consumes the top byte of the stack
, and jumps to the routine if it is not 0x00. If it is 0x00, execution continues as normal. (The official documentation also describes this as incrementing the PC by 2.)
#00 ?routine INC @routine INC INC
( RESULT
WST: 03
)
Top byte is 00, so no jump
#01 ?routine INC @routine INC INC
( RESULT
WST: 02
)
Top byte is 01, so jump occurs
If a {, } block follows ?, the opposite applies — the inside is executed if the top byte of the stack
is 00.
#10 #00 ?{ INC }
( RESULT
WST: 11
)
#10 #01 ?{ INC }
( RESULT
WST: 10
)
This is one of the confusing parts of Uxntal, but it helps to think of } as the routine definition and { as the routine label.
#10 #00 ?{ INC }
->
#10 #00 ?end INC @end
Jump Stash Return Immediate(JSI) simply executes a routine at the current position.
The difference from JMI is that it pushes the current PC+2 value onto the RST and then jumps to the routine’s address.
1#00
2routine
3INC
4@routine
5( RESULT
6WST: 00
7RST: 01 05
8 ^^^^^ PC of Line 3
9)
Note
Jump Stash Return
There is another opcode called Jump Stash Return(
JSR), which dynamically jumps to the address at the top of the stack while simultaneously pushing the current PC+2 value onto the RST.
Therefore, the following code produces exactly the same result as theJSIexample above.1#00 2;routine JSR 3INC 4@routine 5( RESULT 6WST: 00 7RST: 01 05 8 ^^^^^ PC of Line 3 9)The reason
JSRexists despiteJSIis that, unlikeJSIwhich jumps to a fixed routine,JSRcan jump to a dynamic routine/address.
Since it saves the ROM address (PC) of the current position, it is used like a function — execute a routine and return to the original position. And since it can be made to return, it is also called a subroutine when used at any point internally.
#07 #04 modulo BRK
( If we continue as-is, we would execute modulo's opcodes, so we break before that. )
( Forth notation is mainly used to annotate a routine's side effects within code. )
@modulo ( a mod -- res )
DIVk MUL SUB JMP2r
( RESULT
WST: 03
)
Note
Break
Break(
BRK) terminates the operation of the current vector. This opcode has no modes.
Further More…
This article has only covered the basic syntax and ecosystem of Uxntal. Please refer to the various opcodes[↗] on your own.
Related
References
The reason it increases by 2 is that each opcode occupies 2 bytes . ↩︎
As an exception, immediate opcodes advance by the length of the data stored immediately after the opcode. ↩︎
JMP,JCN,JSR↩︎Immediate opcodes do not have modes. ↩︎
