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.
176 lines
5.0 KiB
C
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;
|
|
}
|
|
|
|
|