Sixty four instructions becomes seventy

The six settled back on the twenty fourth, built now.

RCAL and RRET are a call that puts nothing back. CALL restores A, B and Data
Pointers 0 through 2, which costs ten bytes of Stack and is why a subroutine
here can only hand anything back through Q, DP3 or memory. RCAL costs two and
restores nothing, which is what a short leaf routine wants and is unsafe in
exactly the way the name says.

They are a pair because the frames are different sizes: returning from one
through the other walks the Stack to somewhere that was never a return address.
That was the user's correction to the original proposal, which had a raw call
and no raw return.

DPUA and DPDA offset a Data Pointer by A; DPUW and DPDW by A and B together,
most significant first. DPUP and DPDN take a byte written into the program, so
moving a pointer by something just worked out meant storing it and loading it
back. Down as well as up on symmetry grounds, which was also the user's call -
the argument against it came from counting uses in a corpus written under the
constraint.

The opcodes sit where they belong: 0x16 and 0x1E immediately below CALL and RET,
and 0x4E through 0x51 at the end of the Data Pointer family. All six fit shapes
that already existed, so instructiontable.py needed only set membership and both
machine side copies of the table regenerated from it unchanged.

Checked at every level it exists at: the emulator runs them, the host assembler
encodes them, the monitor disassembles all six with the right lengths, and the
assembler that runs on the machine builds a program using them byte for byte
identically to the host - and that program runs.

The recorded test measures what the two calls COST as well as what they put
back, because an RCAL that quietly did what CALL does would still return to the
right place. It does not survive that: returned through RRET, it hangs.

docs.sh can read a two word number now. The count of instructions taking a Data
Pointer went past twenty, and the pattern only allowed one word, so the check
would have reported that the manual had stopped saying it rather than that the
number was wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-08-25 17:29:43 -04:00
co-authored by Claude Opus 5
parent 00d896e3e7
commit af0360128b
11 changed files with 247 additions and 11 deletions
+58
View File
@@ -266,6 +266,24 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter = destination - 1;
}
break;
case 0x16:
// 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
// three Data Pointers back the way it found them, which costs ten bytes of
// Stack and means a subroutine can only hand anything back through Q, DP3 or
// memory. RCAL costs two bytes and puts nothing back at all: everything the
// callee touches, the caller has lost.
//
// It must be returned from with RRET. The two frames are different sizes, so
// returning from one through the other walks the Stack to somewhere that was
// never a return address.
cpu->Data[cpu->StackPointer] = cpu->ProgramCounter & 0xFF;
cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = (cpu->ProgramCounter >> 8) & 0xFF;
cpu->StackPointer--;
genericBranch(cpu);
break;
case 0x17:
// CALL - Push the Program Counter to the Stack, and perform an immediate branch.
genericCall(cpu);
@@ -344,6 +362,16 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// same job with its +2, for the same reason.
cpu->ProgramCounter = resumeAddress - 1;
} break;
case 0x1E:
// RRET - Return from an RCAL, taking back nothing but the return address.
cpu->StackPointer++;
cpu->ProgramCounter = (uint16_t)cpu->Data[cpu->StackPointer] << 8;
cpu->StackPointer++;
cpu->ProgramCounter = cpu->ProgramCounter | (uint16_t)cpu->Data[cpu->StackPointer];
// Two on, to step over the address the RCAL branched through, exactly as RET
// does. Everything else RET restores, this deliberately does not.
cpu->ProgramCounter += 2;
break;
case 0x1F:
// RET - Return from subroutine, restore the registers and set the Program Counter to the Return Address.
// Pop A from the Stack.
@@ -558,6 +586,36 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
*target -= cpu->Program[cpu->ProgramCounter];
}
break;
case 0x4E: {
// DPUA - Offset the selected Data Pointer up by A.
//
// A rather than Q, because Q is what the ALU last worked out and would be
// gone by the time anything had been added to it. Working a step out and then
// moving a pointer by it took a store and a reload before this existed.
uint16_t *target = selectDataPointer(cpu);
*target += cpu->A;
}
break;
case 0x4F: {
// DPDA - Offset the selected Data Pointer down by A.
uint16_t *target = selectDataPointer(cpu);
*target -= cpu->A;
}
break;
case 0x50: {
// DPUW - Offset the selected Data Pointer up by A and B together, A being the
// most significant, which is how every sixteen bit value on this machine is
// carried between a pair of registers.
uint16_t *target = selectDataPointer(cpu);
*target += ((uint16_t)cpu->A << 8) | (uint16_t)cpu->B;
}
break;
case 0x51: {
// DPDW - Offset the selected Data Pointer down by A and B together.
uint16_t *target = selectDataPointer(cpu);
*target -= ((uint16_t)cpu->A << 8) | (uint16_t)cpu->B;
}
break;
case 0x4A: {
// LDD - Load the first Data Pointer from the two bytes of Data Memory
// addressed by the second. Byte order matches everywhere else an address