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 |
|
||||||||||||||||||||||
| $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 |
|
0 |
|
|
||||||||||||||||||||||
| $FFE8 | Column (X) | 0 | Set Column (x) | Position Column 0-127 | ||||||||||||||||||||||
| $FFE7 |
|
0 |
|
|
||||||||||||||||||||||
| $FFE6 | Display Flags | 0 | Set Display Flags |
|
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.
