Files
SplitBit-Emulator/Source/Assembler/assembly.c
T
Anachronaut c8c9f0b363 SRET: a handler answers the way a subroutine does
CALL saves A, B and Data Pointers 0 to 2 and nothing else, which is exactly
why Q and DP3 are how a subroutine hands something back. An interrupt saves
all of it, so a service with an answer had to reach into its own frame and
un-save two fields by hand:

  MVSD.2
  DPUP.2 0d02           ; the saved Q, by an offset it had to know
  STA.2
  RETI

Thirty places in CosmOS did that. Every one knew the frame's layout by
heart, and all thirty would have gone quietly wrong the day the frame
gained a field - the same duplicated fact this project keeps being bitten
by, except duplicated into thirty places AND into the CPU.

SRET is 0x76, in the seat the block split left for it. It is RETI's frame
with RET's rule applied: A, B and DP0 to DP2 come back, the saved Q and DP3
are dropped, and the Interrupt Flag is restored from the frame - only that
bit, so carry survives a service the way it survives a call, and there is
one rule rather than two. RETI stays exactly as it was: a hardware handler
has nothing to say and must leave no trace.

CosmOS is 10,969 bytes against 11,122, and no handler knows a frame offset.

TWO MISTAKES WORTH RECORDING, both mine, both caught by tests.

The first conversion matched STA.2 with a regular expression that did not
allow a trailing comment, so it ran past the end of one handler and into
the next. The second understood the pattern and still got it wrong: the old
frame write carried the answer from A into the saved Q slot, so simply
deleting the write left Q holding whatever it happened to hold. Services
that answer by calling something were fine - Q already had it - and
services that set A directly silently reported success for every failure.
cosmosCwd is what noticed, by saying "cannot go there" about a directory
that was there. Sixteen handlers move the answer into Q now.

Seven MVQA went with it. They copied Q into A so the frame write could
carry it; SRET puts A back, so they moved a value nobody would ever read.
2026-08-27 18:18:36 -04:00

176 lines
5.0 KiB
C

// assembly.c
// These are functions useful for translating assembly mnemonics to hex and vice-versa for the SplitBit CPU.
// Written by Anachronaut
// 10/18/2024
#include "assembly.h"
#include <string.h>
typedef struct {
uint8_t opcode;
const char* mnemonic;
} Instruction;
Instruction instruction_set[] = {
// ---- Nothing at all in 0x00 to 0x0F ----
//
// Kept empty on purpose. Program Memory that has never been written, or a load that
// stopped part way and left zeroes in its tail, used to read as a long run of
// additions and then do something unpredictable a long way from the cause. An
// unassigned byte faults where it is met, with the address, which is the difference
// between a diagnosis and a search.
//
// Arithmetic and Logic Operations:
{0x10, "ADD"},
{0x11, "SUB"},
{0x12, "AND"},
{0x13, "OR"},
{0x14, "XOR"},
{0x15, "NOTA"},
{0x16, "NOTB"},
{0x17, "SHL"},
{0x18, "SHR"},
// Branch Operations:
{0x60, "BRI"},
{0x61, "BRQ"},
{0x62, "BRA"},
{0x63, "BRB"},
{0x64, "BRC"},
{0x65, "BRD"},
// The same four conditions the other way round. A quarter of the conditional
// branches in the corpus were a branch over an unconditional one before these
// existed, each of them needing a label invented only to be jumped past.
{0x66, "BNQ"},
{0x67, "BNA"},
{0x68, "BNB"},
{0x69, "BNC"},
// Subroutine Operations:
//
// A block of their own since the branches and these outgrew one nibble between them.
// Each raw form sits immediately below the ordinary one it cannot be mixed with: RCAL
// under CALL, RRET under RET, because the frames differ and returning through the
// wrong one takes the machine somewhere nobody named.
{0x70, "RCAL"},
{0x71, "CALL"},
{0x72, "SWI"},
{0x73, "RETI"},
{0x74, "RRET"},
{0x75, "RET"},
// A handler with an answer. RETI restores everything and is how a hardware handler
// says it was never here; this restores what a RET restores, and is how a service
// says it has replied. See the note in cpu.c.
{0x76, "SRET"},
// Register Operations:
{0x20, "RSTA"},
{0x21, "RSTB"},
{0x22, "INCA"},
{0x23, "INCB"},
{0x24, "DECA"},
{0x25, "DECB"},
{0x26, "INIA"},
{0x27, "INIB"},
{0x28, "CCF"},
{0x29, "MVQA"},
{0x2A, "MVQB"},
{0x2B, "SIF"},
{0x2C, "CIF"},
// Stack Operations:
{0x30, "PSHQ"},
{0x31, "PSHA"},
{0x32, "PSHB"},
{0x33, "PSHD"},
{0x34, "POPA"},
{0x35, "POPB"},
{0x36, "POPD"},
// Data Operations:
{0x40, "INCD"},
{0x41, "DECD"},
{0x42, "LDA"},
{0x43, "LDB"},
{0x44, "STQ"},
{0x45, "STA"},
{0x46, "STB"},
{0x47, "SETD"},
{0x48, "DPUP"},
{0x49, "DPDN"},
{0x4A, "LDD"},
{0x4B, "STD"},
{0x4C, "MVSD"},
{0x4D, "MVDS"},
{0x4E, "DPUA"},
{0x4F, "DPDA"},
{0x50, "DPUW"},
{0x51, "DPDW"},
// Output Operations:
{0xD0, "OUTQ"},
{0xD1, "OUTA"},
{0xD2, "OUTB"},
// Input Operations:
{0xE0, "INA"},
{0xE1, "INB"},
// Special Operations:
{0xF0, "NOP"},
{0xFE, "WAIT"},
{0xFF, "HALT"}
};
int num_instructions = sizeof(instruction_set) / sizeof(Instruction);
const char* getMnemonic(uint8_t opcode) {
for (int i = 0; i < num_instructions; i++) {
if (instruction_set[i].opcode == opcode) {
return instruction_set[i].mnemonic;
}
}
return "---";
}
int dataPointerOperands(uint8_t opcode) {
// How many Data Pointer selector bytes follow this opcode. Most instructions
// have none. The ones that work through a pointer have one naming which pointer.
// LDD and STD move a pointer through a pointer, so they name two.
switch (opcode) {
case 0x4A: // LDD
case 0x4B: // STD
return 2;
case 0x65: // BRD
case 0x33: // PSHD
case 0x36: // POPD
case 0x40: // INCD
case 0x41: // DECD
case 0x42: // LDA
case 0x43: // LDB
case 0x44: // STQ
case 0x45: // STA
case 0x46: // STB
case 0x47: // SETD
case 0x48: // DPUP
case 0x49: // DPDN
case 0x4C: // MVSD
case 0x4D: // MVDS
case 0x4E: // DPUA
case 0x4F: // DPDA
case 0x50: // DPUW
case 0x51: // DPDW
return 1;
default:
return 0;
}
}
int getOpcode(char* mnemonic) {
for (int i = 0; i < num_instructions; i++) {
if (strcmp(instruction_set[i].mnemonic, mnemonic) == 0) {
return instruction_set[i].opcode;
}
}
// NOT_AN_OPCODE, and it is negative on purpose. This used to answer 0xFE on the
// grounds that 0xFE was unused - which was true until WAIT was given that opcode, at
// which point the assembler would have read WAIT as a word it did not recognise. A
// sentinel picked from the unused half of a range stops being a sentinel the moment
// somebody uses the range, so this one is outside the range altogether.
return NOT_AN_OPCODE;
}