Move the opcode map: nothing in 0x0X, and room for a return variant

Three blocks move and nothing else changes. Branches take 0x60, subroutines
take 0x70, and the ALU moves up into the 0x10 block the two of them used to
share. Order within each block is preserved exactly - this relocates them,
it does not rethink them.

WHAT IT BUYS IS AN EMPTY 0x00 TO 0x0F. Program Memory that was never
written, or a load that stopped part way and left zeroes in its tail, used
to read as a long run of ADDs: the machine carried on through them, arrived
somewhere unpredictable, and whatever broke there was a long way from the
byte that caused it. Now it faults where it is met:

  Fault: 0x00 at Program Address 0x0004 is not an instruction.

That is the address of the byte after the last real instruction, which is
the difference between a diagnosis and a search. Reserving the whole nibble
rather than just 0x00 means a run into blank memory faults wherever it
starts rather than only when it lands on the right byte. runOffTest records
it, and the block is left empty for whatever turns out to want it.

The other half is room: branches and subroutines had filled 0x10 to 0x1F
between them, so a service return that keeps Q and DP3 had nowhere to sit
next to its family. It has 0x76 waiting now.

Five places wrote an opcode down that the scripted remap did not reach, and
four of them were found by tests rather than by looking:

- secondPass.c lists which opcodes take an address, and firstPass.c knows
  SWI by number. Missing those made XOR read as a branch.
- Asm.asm knows SWI by number too, being the other assembler. Missing it
  made the native and host assemblers disagree byte for byte, which is
  exactly the check that exists to catch a thing known in two places.
- loaderTest.asm carries a hand written payload, and its RETI was 0x19. To
  the assembler those are numbers and to the program they are data, so
  nothing but running it could notice. It says so in a comment now.
- The Assembler Manual prints the bytes hello.asm assembles to, and two of
  them were branches.

The monitor's recorded disassembly moved by exactly the bytes it should:
18 became 72 wherever SWI appears, with SETD and INIB untouched and every
disassembled line still reading the same.
This commit is contained in:
Anachronaut
2026-08-27 18:05:54 -04:00
parent ce2a2cd7e6
commit cd5f548736
15 changed files with 213 additions and 186 deletions
+40 -26
View File
@@ -12,36 +12,50 @@ typedef struct {
} 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:
{0x00, "ADD"},
{0x01, "SUB"},
{0x02, "AND"},
{0x03, "OR"},
{0x04, "XOR"},
{0x05, "NOTA"},
{0x06, "NOTB"},
{0x07, "SHL"},
{0x08, "SHR"},
{0x10, "ADD"},
{0x11, "SUB"},
{0x12, "AND"},
{0x13, "OR"},
{0x14, "XOR"},
{0x15, "NOTA"},
{0x16, "NOTB"},
{0x17, "SHL"},
{0x18, "SHR"},
// Branch Operations:
{0x10, "BRI"},
{0x11, "BRQ"},
{0x12, "BRA"},
{0x13, "BRB"},
{0x14, "BRC"},
{0x15, "BRD"},
{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.
{0x1A, "BNQ"},
{0x1B, "BNA"},
{0x1C, "BNB"},
{0x1D, "BNC"},
{0x16, "RCAL"},
{0x17, "CALL"},
{0x18, "SWI"},
{0x19, "RETI"},
{0x1E, "RRET"},
{0x1F, "RET"},
{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"},
// Register Operations:
{0x20, "RSTA"},
{0x21, "RSTB"},
@@ -115,7 +129,7 @@ int dataPointerOperands(uint8_t opcode) {
case 0x4A: // LDD
case 0x4B: // STD
return 2;
case 0x15: // BRD
case 0x65: // BRD
case 0x33: // PSHD
case 0x36: // POPD
case 0x40: // INCD
+1 -1
View File
@@ -385,7 +385,7 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
&& (*intermediateArray)[*intermediateIndex].type == LABEL
&& *intermediateIndex > 0
&& (*intermediateArray)[*intermediateIndex - 1].type == INSTRUCTION
&& (*intermediateArray)[*intermediateIndex - 1].byteValue == 0x18) {
&& (*intermediateArray)[*intermediateIndex - 1].byteValue == 0x72) {
(*intermediateArray)[*intermediateIndex].type = VECTOR_REFERENCE;
(*intermediateArray)[*intermediateIndex].byteLength = 1;
}
+6 -6
View File
@@ -512,17 +512,17 @@ static void checkOperands(intermediateElement *intermediateArray, int arraySize,
const char *problem = NULL;
// Listed rather than matched on the high nibble, because not every instruction in
// the branch block takes an address: RET has none, and BRD gets its destination
// from a Data Pointer instead of from the program.
if (opcode == 0x10 || opcode == 0x11 || opcode == 0x12 ||
opcode == 0x13 || opcode == 0x14 || opcode == 0x17 ||
opcode == 0x1A || opcode == 0x1B || opcode == 0x1C || opcode == 0x1D) {
// these two blocks takes an address: RET has none, and BRD gets its destination from
// a Data Pointer instead of from the program.
if (opcode == 0x60 || opcode == 0x61 || opcode == 0x62 ||
opcode == 0x63 || opcode == 0x64 || opcode == 0x71 ||
opcode == 0x66 || opcode == 0x67 || opcode == 0x68 || opcode == 0x69) {
// Branches and CALL take a two byte address, which only a label can supply.
if (nextType != LABEL) problem = "Branch without label.";
} else if ((opcode & 0xF0) == 0xD0 || (opcode & 0xF0) == 0xE0) {
// The instruction is either an input or output and must be followed by a value.
if (nextType != VALUE) problem = "I/O without destination port.";
} else if (opcode == 0x18) {
} else if (opcode == 0x72) {
// SWI names a vector, either by the name it was given in the Vector Segment or,
// rarely, as a literal number. Without one it swallows whatever follows it and
// every address after that shifts.
+25 -25
View File
@@ -203,7 +203,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
uint16_t result;
switch(Instruction) {
// 0x - Arithmetic and Logic Operations.
case 0x00:
case 0x10:
// ADD - A + B + Carry -> Q
result = (uint16_t)cpu->A + (uint16_t)cpu->B + (cpu->Status & STATUS_CARRY);
if (result > 255) {
@@ -213,7 +213,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
}
cpu->Q = result & 0xFF;
break;
case 0x01:
case 0x11:
// SUB - A - B - Carry -> Q
result = (uint16_t)cpu->A - (uint16_t)cpu->B - (cpu->Status & STATUS_CARRY);
if (result > 255) {
@@ -223,34 +223,34 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
}
cpu->Q = result & 0xFF;
break;
case 0x02:
case 0x12:
// AND - A and B -> Q
cpu->Q = cpu->A&cpu->B;
break;
case 0x03:
case 0x13:
// OR - A or B -> Q
cpu->Q = cpu->A|cpu->B;
break;
case 0x04:
case 0x14:
// XOR - A xor B -> Q
cpu->Q = cpu->A^cpu->B;
break;
case 0x05:
case 0x15:
// NOTA - not A -> Q
cpu->Q = ~cpu->A;
break;
case 0x06:
case 0x16:
// NOTB - not B -> Q
cpu->Q = ~cpu->B;
break;
case 0x07:
case 0x17:
// SHL - Shift AB left.
shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
shiftRegister = (shiftRegister << 1) | (shiftRegister >> 15);
cpu->A = shiftRegister >> 8;
cpu->B = shiftRegister & 0xFF;
break;
case 0x08:
case 0x18:
// SHR - Shift AB right.
shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
shiftRegister = (shiftRegister >> 1) | (shiftRegister << 15);
@@ -260,11 +260,11 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
//
// 1x - Branch Operations:
//
case 0x10:
case 0x60:
// BRI - Branch Immediately
genericBranch(cpu);
break;
case 0x11:
case 0x61:
// BRQ - Branch if Q = 0
if(cpu->Q == 0) {
genericBranch(cpu);
@@ -273,7 +273,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2;
}
break;
case 0x12:
case 0x62:
// BRA - Branch if A = 0
if(cpu->A == 0) {
genericBranch(cpu);
@@ -282,7 +282,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2;
}
break;
case 0x13:
case 0x63:
// BRB - if B = 0
if(cpu->B == 0) {
@@ -291,7 +291,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2;
}
break;
case 0x14:
case 0x64:
// BRC - Do an immediate branch if the Carry Flag is set.
if (cpu->Status & STATUS_CARRY) {
genericBranch(cpu);
@@ -299,7 +299,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2;
}
break;
case 0x15: {
case 0x65: {
// BRD - Branch to the address held in a Data Pointer.
// This is the only branch whose destination is not written into the
// program, which is what makes a table of addresses something a program
@@ -309,7 +309,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter = destination - 1;
}
break;
case 0x16:
case 0x70:
// RCAL - Call, pushing nothing but the return address.
//
// The unsafe one, and it says so in its name. CALL puts A, B and the first
@@ -327,11 +327,11 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->StackPointer--;
genericBranch(cpu);
break;
case 0x17:
case 0x71:
// CALL - Push the Program Counter to the Stack, and perform an immediate branch.
genericCall(cpu);
break;
case 0x1A:
case 0x66:
// BNQ - Branch if Q is not 0.
if(cpu->Q != 0) {
genericBranch(cpu);
@@ -339,7 +339,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2;
}
break;
case 0x1B:
case 0x67:
// BNA - Branch if A is not 0.
if(cpu->A != 0) {
genericBranch(cpu);
@@ -347,7 +347,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2;
}
break;
case 0x1C:
case 0x68:
// BNB - Branch if B is not 0.
if(cpu->B != 0) {
genericBranch(cpu);
@@ -355,7 +355,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2;
}
break;
case 0x1D:
case 0x69:
// BNC - Branch if the Carry Flag is clear.
if (!(cpu->Status & STATUS_CARRY)) {
genericBranch(cpu);
@@ -363,7 +363,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2;
}
break;
case 0x18: {
case 0x72: {
// SWI - Software Interrupt. The byte after the opcode names the vector.
// Never masked: this is an instruction the program deliberately ran, not
// something a device asked for.
@@ -378,7 +378,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter = site - 1;
}
} break;
case 0x19: {
case 0x73: {
// RETI - Return from an interrupt. Pops the frame in the exact reverse of
// the order enterInterrupt pushed it.
cpu->StackPointer++;
@@ -405,7 +405,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// same job with its +2, for the same reason.
cpu->ProgramCounter = resumeAddress - 1;
} break;
case 0x1E:
case 0x74:
// RRET - Return from an RCAL, taking back nothing but the return address.
cpu->StackPointer++;
cpu->ProgramCounter = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
@@ -415,7 +415,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// does. Everything else RET restores, this deliberately does not.
cpu->ProgramCounter += 2;
break;
case 0x1F:
case 0x75:
// RET - Return from subroutine, restore the registers and set the Program Counter to the Return Address.
// Pop A from the Stack.
cpu->StackPointer++;