Bedrock: Differences from Uxn

This article describes the differences between the Bedrock computer system and the Uxn and Varvara computing stack.

Bedrock originated as a fork of Uxn and Varvara, with the aim of improving performance on extremely resource-constrained systems. It has since diverged in many significant ways, but remains similar in others. To summarise the changes in a sentence: the instruction set has been stripped back, the component interfaces have been tightened, and the device capabilities have been expanded.

Architecture

No addressing modes

Bedrock doesn’t have relative or zero-page addressing modes; all addresses are absolute.

The advantage of using relative and zero-page addresses is that they’re only one byte long, whereas absolute addresses take up two bytes each. This is mitigated by the use of immediate mode in Bedrock, which allows a jump instruction to consume the address directly from memory instead of first pushing it to a stack:

#00   JMP   ( relative jump in Uxn:     2 instructions, 3 bytes )
#0000 JMP2  ( absolute jump in Uxn:     2 instructions, 4 bytes )
JMP:0000    ( absolute jump in Bedrock: 1 instruction,  3 bytes )

The main disadvantage of relative addresses is that they’re less efficient under the hood. A relative address has to be converted to an absolute address internally before it can be used, by adding it as an 8-bit signed offset to the 16-bit program counter, whereas an absolute address can be used directly. Relative addresses can jump as far as 128 bytes in either direction which makes them most suitable for use in tight loops, but tight loops are where the code should be most efficient.

For zero-page addresses, I found that I’d treat the zero page as a finite resource and then be reluctant to use it, because there are only 256 bytes available. There’s also the restriction that library functions are unable to store values on the zero page, because the library has no way of figuring out which addresses are used by which other functions. Because of this, not every variable can be stored on the zero page, and so you have to remember whether a value is stored on the zero page or elsewhere in order to use the correct rune for the label.

Removing the relative and zero-page addressing modes frees up a few instructions in the instruction set (LDZ, STZ, LDR, STR) and also greatly simplifies the design of the assembler. By removing the zero page, the program is able to start from address 0000 instead of 0100, saving the assembler from having to deal with two different address spaces (address in the ROM vs. address at runtime). The assembler also no longer needs to use different runes for the relative, zero-page, and absolute variants of a label; labels will always assemble to an absolute address.

Allow implementations to determine behaviour in edge cases

Bedrock allows each implementation to choose how best to handle a select list of edge cases, including stack underflows, stack overflows, and program counter overflows.

The reasoning here is that no program should be triggering these edge cases anyway. Overflowing or underflowing the stack will at best mask a stack balancing issue elsewhere in the program, and overflowing the program counter could be instead performed by placing a jump instruction at the end of program memory.

The best way to handle each of these edge cases depends on the language or host system used for the implementation. Stack pointers in C or Rust would be unsigned 8-bit integers with wrapping semantics, whereas stack pointers in JavaScript would be signed floating-point numbers. By allowing each implementation to choose how best to handle each edge case, the overall performance of each implementation can be improved at no real cost.

Restrict device interfaces

Devices in Bedrock can only send and receive data through the device ports.

Uxn uses a block of memory to store device port values, with device communications performed by reading from or writing to this port memory. However, devices are also able to reach into the implementation to read and write values from program memory, stacks, and other devices. The screen device can reach into the system device to fetch colour information, the system device can reach into the stacks to get and set the stack pointers, and the file device can reach into program memory to write file data. This makes it difficult to implement aspects of Uxn on systems that enforce isolation between components.

Instruction set

Replace keep mode with immediate mode

Bedrock replaces the keep mode k with immediate mode :.

I found keep mode to only really be useful for instructions that read a single value from a stack, like LDA, or for the relational instructions LTH/GTH/EQU/NEQ. For other instructions, I found myself wanting to keep one value but not the other, such as when storing a byte to memory with the sequence DUP #0000 STA.

New values must be pushed to the stack frequently, because almost every instruction results in fewer bytes being on the stack than before. The LIT instruction is used the most often in Uxn, because it’s the only instruction that can push new values to the stack for instructions to operate on.

The immediate mode was added to Bedrock to allow any instruction to act like the LIT instruction, pushing a new value to the stack before performing the instruction. This make the LIT instruction obsolete, as it could now be replaced by the immediate-mode variant of the PSH instruction (called STH in Uxn). The keep mode was removed, living on in spirit as the NQK (not-equal-keep) instruction.

#0002 #0003 ADD2  ( Uxn:     3 instructions, 7 bytes )
*:0002 ADD*:0003  ( Bedrock: 2 instructions, 6 bytes )

Replace BRK instruction with HLT

Bedrock uses the 00 instruction to halt the program.

The 00 instruction is special, because it will be performed when evaluating any byte of uninitialised program memory. In Uxn the 00 instruction is BRK, which pauses the program until a device vector triggers. In Bedrock it is HLT, which immediately terminates the program.

The most likely reason for an uninitialised memory address to be evaluated is from some kind of program error: by running past the end of the program, making an uncontrolled jump into deep memory, or entering a data segment and finding a null byte. In all of these cases the program is out of control and should be terminated.

The BRK approach is to recover the program if possible. A device vector will generally expect the stacks to be empty; if there is junk data left on a stack after a program error, it will be ignored, with the current stack position now acting as the stack base. This works well alongside circular stacks. The disadvantage to this approach is that it can mask program errors, because the program is now able to recover from significant errors.

No multiply or divide instructions

Bedrock doesn’t have instructions for multiplying or dividing numbers; instead, this functionality is exposed by the math device.

I found that, when programming for Uxn, it was easier to reach for #02 MUL than #10 SFT to multiply or divide by a power of two, despite the latter being more efficient to implement under the hood. This can be a bit of a trap: both methods run in the same number of cycles and assemble to the same number of bytes, but on systems without a hardware multiplier the MUL approach will take a good deal longer to run.

Add the conditional call instruction JCS

Bedrock adds the instruction JCS for conditionally calling a function.

This instruction acts as a combination of JCN and JMS (called JSR in Uxn). I had previously emulated this functionality as a higher-order function in Uxn.

Conditional calls are only useful for calling functions that don’t add or remove values from the stack, because if the function isn’t called then any values that would have been passed to the function are not consumed. It turns out that there are still a lot of good use-cases for calling these functions, most notably in callback-driven event loops.

Replace bit shifting SFT with SHL and SHR.

Bedrock splits the bi-directional bit shifting instruction SFT into the separate left-shifting and right-shifting instructions SHL and SHR.

The SFT instruction in Uxn performs both left and right bit shifting in a single operation. The shift distances are determined by a single byte, with the left digit determining left shift and the right digit determining right. This saves space in the instruction set, because a single instruction can be used for both jobs, but it is less efficient under the hood because the instruction always has to perform two shift operations. Performing a left-shift operation with dynamic distance is also inefficient, because the distance value must first be shifted left by four before it can be used for left-shifting:

[  #0001 #03 ] #40 SFT SFT2  ( Uxn:     3 instructions, 4 bytes )
[ *:0001 :03 ] SHL*          ( Bedrock: 1 instruction,  1 byte  )

Add bit rotation instructions ROL and ROR

Bedrock adds the instructions ROL and ROR for performing bit rotation.

Bit rotation is a fundamental operation that is inefficient to emulate with bit shifting instructions. It can be used for offsetting sprites and calculating checksums.

Add the decrement instruction DEC

Bedrock adds the instruction DEC for decrementing a value. This is to compliment the INC instruction from Uxn, and improves the efficiency of decrementing loops.

Add debug instructions DB1 through DB6

The debug instructions DB1 through DB6 were added as variants of the HLT instruction.

These instructions normally have no effect, acting as no-operation instructions. This fits with the behaviour of HLT, which, other than halting the system, has no effect on the memory, stacks, or program counter.

They are intended to act as debugging hooks or breakpoints for the private use of each implementation, but will do nothing if the implementation is not running in a debug mode. They replace the debug port of the Varvara system device.

Assembler

Bedrock string literals are surrounded with quote characters on both ends, like in "string". This is different to Uxn, where each word of a string must be prefixed by a quote character and spaces entered as interstitial byte literals:

"This 20 "is 20 "a 20 "string. 00  ( Uxn string literal     )
"This is a string."                ( Bedrock string literal )

In Bedrock, double-quoted strings are null-terminated, and single-quoted strings are not. Single-quoted strings are often used to represent individual characters, like in PSH:'B'. The Bedrock string syntax should be no more difficult to parse than the comment syntax, because both strings and comments are a sequence of characters delimited at both ends.

No literal runes

In Uxn, the literal rune # is used as syntactic sugar for LIT, with the syntax #00 being equivalent to the long form LIT 00. No matching rune exists for the return stack though, so the long form LITr 00 must be used instead. This asymmetry is even more pronounced when pushing a label address to the return stack. The literal absolute rune ; is used to push an absolute address to the working stack, as in ;label. No matching rune exists for the return stack though, so the long form LIT2r =label must be used instead.

Bedrock doesn’t use runes to push values to the stacks; instead, the PSH instruction is used in all cases. Pushing a label to the working stack is PSH*:label, and to the return stack is PSHr*:label. To make this even more concise, the immediate-mode PSH instructions can be used without the PSH prefix, as *:label and r*:label.

No absolute padding rune

Bedrock programs begin from address 0000 instead of 0100, so no absolute pad rune is needed.

This simplifies the design of the assembler, because it doesn’t need to guard against an absolute pad rune rewinding to a previous address.

Allow uppercase hexadecimal literals

Bedrock allows hexadecimal literals to be written with both lowercase and uppercase characters.

There wasn’t a technical reason for disallowing this, other than that it would make the ADD2 instruction a valid hexadecimal literal. This was mitigated by changing the 2 mode flag to * in change wide mode character.

This change allows code to be more readable for people who are used to reading hexadecimal values in uppercase.

Change wide mode character

Bedrock uses a * character instead of a 2 to represent wide mode (called short mode in Uxn). This better matches how double/short values are written in stack notation, as ( a* b* c* -- ).

I find this makes code easier to read, although I admit this is subjective. I would sometimes confuse myself when using the MUL2, DIV2, ADD2, SUB2 instructions, because they appear at first glance to multiply by 2 or subtract 2 instead of acting on wider values.

Change macro definition syntax

Bedrock uses a different macro definition syntax that is slightly easier to parse. The assembler doesn’t have to guard against a macro definition missing an opening {.

%MACRO { 01 02 03 }  ( Uxn macro definition     )
%MACRO   01 02 03 ;  ( Bedrock macro definition )

Devices

Screen device

The screen device uses a sixteen colour palette instead of a four colour palette. It’s true that four colours are more than enough for a user interface, but the kinds of programs and games that I wanted to make required just a few more than that, and sixteen colours works out to a tidy 4 bits per colour, or 8 bits per screen pixel (each pixel having both a foreground and a background colour).

As well as pixels and sprites, the screen can perform line and rectangle drawing operations. Rectangles are important for efficient user interfaces, and lines are intended to be used for efficient wireframe graphics.