header logo

eSubleq Emulator

A simple enhanced Subleq (eSubleq) emulator with assembler.

This enhancement provides memory-mapped peripherals for simplification of writing programs for Subleq.

Learn more about Subleq from Esolangs and Wikipedia.


Assembly Input

     

Debug & Serial IO



Storage Panel

     

Storage size: $000000 (0)

Seek pointer: $000000 (0)

Debug Panel

       

A: $0000 → $00 (0)

B: $0000 → $00 (0)

C: $0000

PC: $0000

Cycles: 0

Status: Not Halted


Memory Monitor - Page



Bitmap Display


Memory-Mapped Peripheral Addresses

Address Read A (Data In) Read B Write B (Data Out) Notes / Behavior
$FFFF Serial In 0 Serial Out Standard I/O port
$FFFE ALU Register A 0 Set Register A Writing this immediately updates all ALU outputs
$FFFD ALU Register B 0 Set Register B Writing this immediately updates all ALU outputs
$FFFC A XOR B 0 NOP Bitwise XOR
$FFFB A AND B 0 NOP Bitwise AND
$FFFA A OR B 0 NOP Bitwise OR
$FFF9 Shift Result 0 NOP Main 8-bit shift result (B<0 ? A>>|B| : A<<B)
$FFF8 Shift Residual 0 NOP Overflow/underflow bits pushed out of $FFF9
$FFF7 A + B 0 NOP Addition with carry out
$FFF6 A - B 0 NOP Subtraction with carry out
$FFF5 (A * B) >> 8 0 NOP Product high byte
$FFF4 (A * B) & 0xFF 0 NOP Product low byte
$FFF3 A / B 0 NOP Quotient (integer division)
$FFF2 A % B 0 NOP Modulo (remainder)
$FFF1 Flags 0 NOP
7 6 5 4 3 2 1 0
0 subtraction
carry
addition
carry
signed
<
signed
>
< > =
$FFF0 Cycle Counter 0 NOP Low byte of cycle counter
$FFEF Block Cap High 0 NOP Block storage capacity high byte
(No storage if high and low are 0)
$FFEE Block Cap Low 0 NOP Block storage capacity low byte
$FFED Seek Ptr High 0 Set Seek Ptr High Storage seek pointer high byte
$FFEC Seek Ptr Mid 0 Set Seek Ptr Mid Storage seek pointer mid byte
$FFEB Seek Ptr Low 0 Set Seek Ptr Low Storage seek pointer low byte
$FFEA Read Storage Byte 0 Write Storage Byte Read or write byte at seek ptr
(Automatically increments seek ptr)
$FFE9
Page (Y)
Row (Y)
0
Set Page (Y)
Set Row (y)
Page Position (0-7)
Row Position (0-63)
$FFE8 Column (X) 0 Set Column (x) Position Column 0-127
$FFE7
VRAM Byte
Pixel Value
0
Write VRAM Byte
Pixel Command
SSD1306 style where byte is a vertical 8 pixels with bit 0 at the top, increments column then row position
7 6 5 4 3 2 1 0
INC X INC Y 0 0 0 0 XOR Pixel value
$FFE6 Display Flags 0 Set Display Flags
7 6 5 4 3 2 1 0
VSYNC (R/O) Display enable (R/W) 0 0 0 Pixel mode (R/W) Live mode (R/W) Flush buffer (W/O)

Legend: Serial, ALU, System, Block Storage, Display (Byte), Display (Pixel)


Assembler Usage

The assembler provided with this emulator is eSASM v1.1, made in-house at KH Labs. It features a macro system and multiple label addressing modes to simplify the implementation of subleq programs.


Instructions

There is only one instruction: SUBLEQ a, b, c

Commas and capitalization are optional. If the c argument is omitted, it will automatically be set to the address of the next instruction. This has the effect of not doing any conditional branching based on the subtraction result.


The following are acceptable syntaxes:

SUBLEQ 1, 2, 3
subleq 1, 2, 3
SLQ 1, 2, 3
SLQ 1, 2
1, 2, 3
slq 1 2 3
1 2


Comments

A comment is any text after the ; character.

Do not use these in macros as they will break them.

; Comment on its own line
SLQ 1, 2, 3  ; Comment after a line
;SLQ 4, 5, 6  ; Commenting out a line

Data

There are two pseudo-instructions used for inserting data into a program: BYTES and CHARS


BYTES takes a list of 8 bit values and inserts them into the code at that position.


CHARS takes a list of strings and inserts their ASCII character codes into the code at that position. CHARS also has the feature of being able to negate the character codes at compile time by prefixing the string with a -.


BYTES 1, 2, 3, -1 -2 -3  ; Data: 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD
CHARS "hi\n" -"bye"  ; Data: 0x68, 0x69, 0x0A, 0x9E, 0x87, 0x9B

Labels

Labels are names used to mark an address in the program.

Z: SLQ 0, 0, start  ; Z is set to $0000, jump to 'start:'
SLQ 1, 2  ; This is skipped over
start:
SLQ Z, Z, Z  ; Clear byte at Z and jump to Z

They can also be applied before any data and don't need spaces after the colon.

D: SLQ A: 1, B:w, C: 3  ; A and D are set to $0000, B is set to $0002, C is set to $0004
data: BYTES E:1, F:2, G:3  ; data and E are set to $0006, F is set to $0007, G is set to $0008
CHARS h:"hello " w:-"world"  ; h is set to $0009, w is set to $000E

The ? label is a special label that stores the address where it is.

SLQ ?, ?, ?  ; Equivalent to 'SLQ $0000, $0002, $0004'

Label Modifiers

As a convenience, compile-time label modifiers are provided to aid with either shifting an address or getting a fragment of an address.


Shifting an address is done with the + or - modifiers after an address, followed by a number.

For working with addresses from within subleq, the +L and +H modifiers are available to address the lower and higher bytes of a 16 bit address in memory.

Z: 0 Y:0 start  ; Z is $0000, Y is $0002
data: BYTES 0, 1, 2, 3
start:
data+1 Z+1 ?+2  ; Set byte at $0001 to -1, 
data+3 Z+2  ; Set byte at $0002 to -3
Z Z ?+2  ; Equivalent to just 'Z Z'
Z+H Y-1  ; Clear byte at $0001

Getting an address fragment (the lower or higher byte) is possible with the @ modifier after an address, followed by L or H (0 or 1, respectively) to get either the lower or higher byte of an address as a value.

This is typically used for adding an address into a BYTES list since a 16 bit address is not a valid value for that.

This modifier must come after the shifting modifier if they are both used together.

Z: 0 0  ; Z is $0000
A: Z Z  ; A is $0006
BYTES A@L, A@H, A@0, A@1, Z+2@L, A+2@L, ?@L  ; Data: 0x06, 0x00, 0x06, 0x00, 0x02, 0x08, 0x18

Directives

The following directives are available: .ORG, .DEFINE, .MACRO, and .DEBUG

Directives don't compile down into bytes directly, instead they modify the program in some way.


The .ORG directive takes an address and resumes the program from that point.

It also supports using a label address with shifting, but the label address must be known ahead of time.

Z: 0 0 A  ; Z is $0000
Y: 0 0 B  ; Y is $0006
.ORG $1000
A: 0 0 Y  ; A is $1000
B: 0 0 Z  ; B is $1006

The .DEFINE directive works similarly to the simple usage of #define in C.

It is specifically used for simple replacement cases in which the word after the .DEFINE will be replaced by whatever follows it in the definition.

.DEFINE HALTADDR -1
.DEFINE HALT Z Z HALTADDR
Z: 0 0
HALT  ; Resolves to 'Z Z -1'

The .MACRO directive works similarly to the more advanced usage of #define in C where it's basically a .DEFINE but with parameters.

Both .DEFINE and .MACRO support multi-line definitions by escaping the newline with a \ character.

The .MACRO is given a special # parameter that instantiates as a value unique to that instance of the macro. This is used to define labels that don't collide with labels generated from calling the macro multiple times.

Z: 0 0

.MACRO SPLIT(addr) {addr}@0, {addr}@1
.MACRO JMP(addr) Z Z {addr}
; Multiline macro can't have comments otherwise it'll break the escape
.MACRO CLR16(addr) {addr} {addr} \
                   {addr}+1 {addr}+1
.MACRO TEST(a, b) START{#}: \
                  CLR16({a}) \
                  CLR16({b}) \
                  JMP(START{#})

BYTES SPLIT($1234), SPLIT($5678)  ; Data: 0x34, 0x12, 0x78, 0x56
; Resolves to 'BYTES $1234@0, $1234@1, $5678@0, $5678@1'

TEST(Z, ?)  ; Resolves to:
; START1_:
; Z Z
; Z+1 Z+1
; ? ?
; ?+1 ?+1
; Z Z START1_

The .DEBUG modifier simply prints the values that it is given.

It will attempt to resolve labels, however if a label isn't defined yet when .DEBUG gets run, it will just return the label name.

Z: 0 0
.DEBUG -3 ? test: start_loop Z $ff00@H "hi\"!"
; Prints the following:
;   DEBUG:2: -3 $0006 test start_loop 0 $FF00H hi"!

Sample Program
; 8 bit data
;16 bit address (little endian)
1, 2, 3
1, 2
1 2 3
1 2
SUBLEQ 1, 2, 3
SUBLEQ 1, 2
SLQ 1 2 3
SLQ 1 2
3 -1 g
Z: Z1:0 Z2:0 ?+2

Z1 Z2 Z
f: .ORG $1000 g:  ; Start near the middle
JMP(end)

.DEFINE HALTADDR -1
.MACRO CLR (addr) {addr} {addr}
.MACRO CLR16( addr) CLR({addr}) \
                   CLR({addr}+1)
CLR16(f)
.DEFINE SERIAL $FFFF
.MACRO SPLIT  (addr) {addr}@0, {addr}@1
.DEFINE HALT CLR(Z) HALTADDR
.DEFINE NOP Z Z
.MACRO JMP(addr) CLR(Z) {addr}
.MACRO PRT(addr) {addr} Z \
                 Z SERIAL \
                 Z Z
.MACRO INF( a,b ) START{#}: \
                 PRT({a}) \
                 PRT({b}) \
                 JMP(START{#})
INF(SERIAL , HALTADDR)
Z Z+1
Z Z+H

.ORG $2000
BYTES 0, SPLIT(HALTADDR-1), -10, -$10, SERIAL@L, B:SERIAL@H, 2, 4, SERIAL-1@0, SERIAL@1, ?@L, B2: ?+2@1

C1: CHARS C2:-"hello, world!\n" C3: "today"
CHARS "hello\
world"
chars "H\x61llo\nworld"

B B2
C1 C2 C3
HALT
end:

Processor Information

The eSubleq emulator is a one-instruction-set computer that has an 8 bit data bus with a 16 bit address bus.

This emulator equiped with 65536 (64K) bytes of memory.

Addresses are stored in memory as 2-byte little endian values.


Since instructions are comprised of 3 addresses, each instruction is 6 bytes in size.

The maximum number of instructions is 10920 when accounting for memory mapped peripherals.

The program counter starts at address 0, but the processor will be halted if it's equal to 0xFFFF.

The official recommendation is to allow the first instruction to be Z: 0 0 so that the Z label can refer to address $0000.


eSubleq Basics

A subleq instruction looks like this: SUBLEQ A, B, C, where A, B, and C are each 16 bit addresses.

If the C parameter is omitted, it will default to the address of the following instruction.

The following pseudocode describes how subleq traditionally works:

Mem[B] -= Mem[A]

if Mem[B] <= 0:
    PC = C
else
    PC += 6

The enhancement provided by eSubleq is to split up the "SUB" stage of the processor so that the different read types can be decoupled.

tmp = ReadB(B) - ReadA(A)
WriteB(B, tmp)

if tmp <= 0:
    PC = C
else
    PC += 6

The ReadA, ReadB, and WriteB functions can then defer the action to memory mapped peripherals, and depending on if the address is found in the A or B parameter, can act differently when accessed.


For example, take the serial device at address $FFFF.

Reading from it and storing the negated value to address $0000 would look like this: SUBLEQ FFFF, 0000. That breaks down to tmp = ReadB($0000) - ReadA($FFFF) and WriteB($0000, tmp), where ReadA returns the next available character from the serial input buffer, ReadB returns the value stored in memory, and WriteB writes the value to memory.

Then writing that character to serial would look like this: SUBLEQ 0000, FFFF. That breaks down to tmp = ReadB($FFFF) - ReadA($0000) and WriteB($FFFF, tmp), where ReadA returns the negated character from memory, ReadB always returns 0 from address $FFFF, and WriteB writes the specified character to the serial output.

The asymmetry between ReadA and ReadB is what enables the same address to work as both input and output depending on whether or not it will be modified.