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.
This commit is contained in:
Anachronaut
2026-08-27 18:18:36 -04:00
parent cd5f548736
commit c8c9f0b363
9 changed files with 275 additions and 133 deletions
+4
View File
@@ -56,6 +56,10 @@ Instruction instruction_set[] = {
{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"},
+46
View File
@@ -405,6 +405,52 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// same job with its +2, for the same reason.
cpu->ProgramCounter = resumeAddress - 1;
} break;
case 0x76: {
// SRET - Return from a handler that has an answer.
//
// THE SAME FRAME AS RETI, WITH THE CALL CONVENTION'S RULE APPLIED TO IT. 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 handler with an answer had to reach into its own frame and
// un-save two fields by hand - thirty places in CosmOS did that, each of them
// knowing the frame's layout by an offset, and all thirty would have gone
// quietly wrong the day the frame gained a field.
//
// So: RETI restores everything and is how a hardware handler says it was never
// here. SRET restores what a RET restores and is how a service says it has
// replied. The saved Q and DP3 are stepped over and dropped.
cpu->StackPointer++;
uint8_t savedStatus = readData(cpu, cpu->StackPointer);
// The Interrupt Flag, and only that. Entering a handler clears it and the
// frame is what puts it back, so dropping the whole byte would leave a service
// silently turning interrupts off. Everything else in Status - the carry
// above all - is left as the handler leaves it, because that is what a RET
// does and the point of this instruction is that there is one rule.
cpu->Status = (uint8_t)((cpu->Status & ~STATUS_INTERRUPT)
| (savedStatus & STATUS_INTERRUPT));
cpu->StackPointer++; // The saved Q, dropped: the handler's answer stands.
cpu->StackPointer++;
cpu->A = readData(cpu, cpu->StackPointer);
cpu->StackPointer++;
cpu->B = readData(cpu, cpu->StackPointer);
for (int i = DATA_POINTERS - 1; i >= 0; i--) {
cpu->StackPointer++;
uint16_t high = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
cpu->StackPointer++;
uint16_t low = (uint16_t)readData(cpu, cpu->StackPointer);
// Data Pointer 3 is stepped over for the same reason as Q. The other three
// come back, exactly as a RET brings them back.
if (i != DATA_POINTERS - 1) {
cpu->DataPointer[i] = high | low;
}
}
uint16_t resumeAddress;
cpu->StackPointer++;
resumeAddress = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
cpu->StackPointer++;
resumeAddress = resumeAddress | (uint16_t)readData(cpu, cpu->StackPointer);
cpu->ProgramCounter = resumeAddress - 1;
} break;
case 0x74:
// RRET - Return from an RCAL, taking back nothing but the return address.
cpu->StackPointer++;