diff --git a/Programs/testPrograms/noDeviceTest.asm b/Programs/testPrograms/noDeviceTest.asm new file mode 100644 index 0000000..b6208f7 --- /dev/null +++ b/Programs/testPrograms/noDeviceTest.asm @@ -0,0 +1,51 @@ +; A device raising its line with nothing installed to answer it, CAUGHT. +; +; The other half of the fault that could not be handed over. A program that asks a device to +; interrupt it and then forgets the handler used to stop the machine dead; now it goes to a +; vector of its own with the port in Q, so the program can say which device it was that +; nobody was listening to. +; +; NOTHING IS TYPED AT THIS. The console raises its line once at the end of input as well as +; for an arriving byte - which is exactly so that a program driven by interrupts is told when +; nothing more is coming - and the test is run with no input at all, so that end is what +; arrives. +; +; Correct output is: +; nobody listening on port 00 +; and a clean halt. + +#Include console.asm + +#Program + +start: + INIA 0x02 ; Interrupt when the console has something to say. + OUTA 0x02 + SIF +spin: + ; Never touches the console. Whatever happens below is something that interrupted this. + BRI spin + +; Q holds the port whose entry was empty. The console is port zero. +nobody: + SETD.0 NobodyText + CALL printString + MVQA + CALL printByteHex + CALL newLine + + ; The console back to how it was found, so nothing else is asked for, and stop. There is + ; nothing to resume to: the loop above only exists to be interrupted. + RSTA + OUTA 0x02 + HALT + +#Data + +NobodyText: +"nobody listening on port " + +#Vectors + + Boot start + NoDevice nobody diff --git a/Programs/testPrograms/noHandlerTest.asm b/Programs/testPrograms/noHandlerTest.asm new file mode 100644 index 0000000..3784e43 --- /dev/null +++ b/Programs/testPrograms/noHandlerTest.asm @@ -0,0 +1,56 @@ +; A software interrupt that names a vector with nothing in it, CAUGHT. +; +; That fault used to be the one the machine had no way of handing over, because the thing +; that would hand it over is the thing that has just found nothing to hand it to. It stopped +; the machine, and no program could do anything about it - which made calling a service the +; system does not implement fatal, and that is an ordinary mistake to make. +; +; It goes to a vector of its own now, with the number of the empty entry in Q. This catches +; it, says which one it was, steps over the SWI and its operand, and carries on - so the +; output says both that the fault arrived and that a program can survive it. +; +; Correct output is: +; empty vector 28 +; carried on +; and a clean halt. + +#Include console.asm + +#Program + +start: + SWI 0d40 ; Forty is 0x28, and nothing is installed there. + + SETD.0 Carried + CALL printString + CALL newLine + HALT + +; Entered because there was nowhere to go. Q holds which entry was empty, and it is the only +; thing on this machine a handler is handed in a register. +missing: + SETD.0 EmptyText + CALL printString + MVQA + CALL printByteHex + CALL newLine + + ; ---- Carrying on past it ---- + ; + ; The frame holds the address after the SWI and the byte naming its vector, so a bare RETI + ; already lands past the instruction that faulted. That is not true of every fault here - + ; a refused port or a byte that does not decode both resume ON the thing that failed - so + ; it is worth saying out loud which kind this one is. + RETI + +#Data + +EmptyText: +"empty vector " +Carried: +"carried on" + +#Vectors + + Boot start + NoHandler missing diff --git a/Source/Assembler/assembly.h b/Source/Assembler/assembly.h index 50141de..4d6bf55 100644 --- a/Source/Assembler/assembly.h +++ b/Source/Assembler/assembly.h @@ -66,7 +66,31 @@ #define VECTOR_GUARD_VIOLATION 3 // A bank was named that has nothing registered in it, or an access ran past its end. #define VECTOR_BANK_FAULT 4 -// Vectors 5 to 15 are held back for faults that do not exist yet, so that each cause + +// ---- The two faults that used to be uncatchable ---- +// +// A vector was dispatched through and had nothing in it. That is discovered by the very +// thing that would dispatch, so for a long time it could only stop the machine: there was +// no way to hand a program a fault about there being nowhere to hand it. +// +// These two are where it goes instead, and they are separate for the same reason every +// other cause is - a missing software vector and a device nobody is listening to are +// different mistakes with different fixes, and a handler should not have to work out which +// it was. +// +// WHICH ENTRY WAS EMPTY ARRIVES IN Q, and that is the only thing on this machine a handler +// is given in a register. It is not a cause register by the back door: the vector still +// says what happened, and Q says which of the 256 entries it happened about, which is a +// parameter rather than a cause. It costs no new state, because the frame already saved the +// Q the interrupted program had and RETI puts it back. +// +// A vector THESE dispatch through and find empty stops the machine, and has to: a machine +// that cannot report a fault about a missing handler by any route has run out of places to +// go, and looping there would be worse than stopping. +#define VECTOR_NO_HANDLER 5 +#define VECTOR_NO_DEVICE 6 + +// Vectors 7 to 15 are held back for faults that do not exist yet, so that each cause // can have an entry of its own rather than sharing one and needing a cause register to // tell them apart. Everything from 16 up belongs to programs, in two halves. // diff --git a/Source/Assembler/secondPass.c b/Source/Assembler/secondPass.c index fcd68da..cf14417 100644 --- a/Source/Assembler/secondPass.c +++ b/Source/Assembler/secondPass.c @@ -226,6 +226,8 @@ static const struct { { "BadOpcode", VECTOR_INVALID_OPCODE }, { "GuardViolation", VECTOR_GUARD_VIOLATION }, { "BankFault", VECTOR_BANK_FAULT }, + { "NoHandler", VECTOR_NO_HANDLER }, + { "NoDevice", VECTOR_NO_DEVICE }, }; static const int reservedVectorCount = (int)(sizeof(reservedVectors) / sizeof(reservedVectors[0])); diff --git a/Source/Emulator/cpu.c b/Source/Emulator/cpu.c index 31e63e0..030a1af 100644 --- a/Source/Emulator/cpu.c +++ b/Source/Emulator/cpu.c @@ -69,9 +69,31 @@ static uint16_t readVector(const uint8_t *programMemory, uint16_t base, uint8_t // Note that a zero entry means "no handler" to everything that dispatches, including the // two entries the CPU treats as start addresses when it reads them at reset. The // exemption belongs to that one read, not to the entries themselves. -static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index, uint16_t resumeAddress) { +static uint8_t dispatch(CPURegisters *cpu, uint16_t base, uint8_t index, + uint16_t resumeAddress, int mayEscalate) { uint16_t handler = readVector(cpu->Program, base, index); if (handler == 0x0000) { + // ---- Nowhere to go is itself something to report ---- + // + // This is the one fault the machine used to have no way of handing over, because + // the thing that would hand it over is the thing that has just found nothing to + // hand it to. So it goes to a vector of its own instead, with the number of the + // empty entry in Q - and a missing software vector and a device nobody is + // listening to are separate entries, because they are separate mistakes. + // + // NOT WHEN ALREADY ESCALATING. If the fault vector for this is itself empty then + // the machine really has run out of places to go, and stopping is the only honest + // answer left. + if (mayEscalate) { + const uint8_t escalation = (base == HARDWARE_VECTOR_BASE) + ? VECTOR_NO_DEVICE : VECTOR_NO_HANDLER; + if (!dispatch(cpu, SOFTWARE_VECTOR_BASE, escalation, resumeAddress, 0)) { + // After the frame, so the Q the interrupted program had is safely in it and + // RETI will put it back. What the handler sees is which entry was empty. + cpu->Q = index; + return 0; + } + } cpu->Fault = FAULT_NO_HANDLER; cpu->FaultVector = index; cpu->Status |= STATUS_FAULT; @@ -109,6 +131,13 @@ static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index, u return 0; } +// Dispatching the ordinary way: through the vector asked for, and through the fault vector +// for a missing one if that is what it turns out to be. +static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index, + uint16_t resumeAddress) { + return dispatch(cpu, base, index, resumeAddress, 1); +} + // A device that refused what it was asked stops the machine where it stands, rather than // raising a line and letting execution carry on past the mistake. The frame carries the // address of the instruction that asked, so a handler can see which one it was, and so a diff --git a/SplitBit Assembler Manual.md b/SplitBit Assembler Manual.md index 4367753..be2da3b 100644 --- a/SplitBit Assembler Manual.md +++ b/SplitBit Assembler Manual.md @@ -633,7 +633,7 @@ A `#Vectors` line that names a **handler** says this program implements that vec `Boot` in a loadable program fills the **entry** field rather than being installed. Vector zero is where the whole machine starts, and a program being loaded into a running system has no business saying anything about that. A boot image is the one thing that does, so there it is installed like any other. -`Device` is named by the port it is plugged into, because that is what decides which vector it arrives through. `Device`, `Boot`, `SoftReset`, `BadOpcode`, `GuardViolation` and `BankFault` are matched **without regard to case**, the way mnemonics are: they are part of the language rather than names the programmer chose. +`Device` is named by the port it is plugged into, because that is what decides which vector it arrives through. `Device`, `Boot`, `SoftReset`, `BadOpcode`, `GuardViolation`, `BankFault`, `NoHandler` and `NoDevice` are matched **without regard to case**, the way mnemonics are: they are part of the language rather than names the programmer chose. **A declaration and an implementation are the same entry.** `services.asm` says a service is called `osPrintString` and has number 16; `cosmos.asm` says `osPrintString` is handled by `handlePrintString`. Both sides include the first file, so the name is met twice and the second time fills in the handler. That is what lets one shared file serve both a program that calls a service and the system that implements it - and it is why the first pass declares and the second implements, a handler being an address and no address being known until every label has been placed. diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index 495298c..9fc2c4b 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -547,7 +547,17 @@ If a device interrupts and its vector is empty, that is a fault: the machine sto If the CPU reads a byte from Program Memory that does not decode to an instruction, it dispatches through Software Vector 2. -Faults get a vector each rather than sharing one. Vector 2 is the only cause defined so far, and vectors 3 through 15 are held back for the ones that come later, so that a handler always knows what happened from the entry it arrived through. That is why the machine has no fault cause register to read. +Faults get a vector each rather than sharing one, so that a handler always knows what happened from the entry it arrived through. That is why the machine has no fault cause register to read. + +| Vector | What happened | +| --- | --- | +| 2 | A byte in Program Memory does not decode to an instruction. | +| 3 | A device refused a write that landed inside a raised fence. | +| 4 | A bank was named that has nothing in it, or an access ran past its end. | +| 5 | A software vector was dispatched through and had no handler. | +| 6 | A device interrupted and its hardware vector had no handler. | + +Vectors 7 through 15 are held back for the causes that come later. The address in the frame is the address of the offending byte itself, not the one after it. A handler can therefore read the byte that failed and say what it was. It also means a handler that returns with a bare RETI will meet the same byte again, because resuming past a fault means deciding where to resume, and only the handler knows that. @@ -555,6 +565,18 @@ If nothing is installed at Vector 2, the CPU sets the Fault Flag and the Halt Fl Stopping matters because the alternative is worse. A byte that means nothing is almost always a sign that execution has wandered into data, or that a program was built for a machine with instructions this one does not have. Stepping over it and carrying on turns a clear failure into a program that appears to run and quietly does the wrong thing. +### Nowhere To Go: + +Vectors 5 and 6 are the fault of dispatching through an empty entry, and they were a long time coming, because the thing that would hand that fault over is the thing which has just found nothing to hand it to. Until they existed, a `SWI` naming a service the system does not implement stopped the machine and no program could do anything about it - and calling a service that is not there is an ordinary mistake. + +**Which entry was empty arrives in Q**, and it is the only thing on this machine a handler is given in a register. That is not a fault cause register by another route: the vector still says *what* happened, and Q says which of the 256 entries it happened about, which is a parameter and not a cause. It costs nothing, because the frame already saved the Q the interrupted program had and RETI puts it back. + +The two are separate entries because they are separate mistakes with separate fixes. A missing software vector is usually a program calling something that is not there; a device with nobody listening is usually a program that asked to be interrupted and forgot the handler. + +**The frame's address is past the SWI**, unlike every other fault here, because the instruction did dispatch - it was the entry that was empty. A bare RETI therefore carries straight on, where a bad opcode or a refused port would meet the same instruction again. + +If Vector 5 or 6 is *itself* empty, the machine stops the way it always did. It has run out of places to go, and looping there would be worse than stopping. A handler that commits the same fault it was called about recurses like any other, which is the same bargain as a Vector 2 handler containing a byte that does not decode. + ## Refusing: A device can refuse what it was asked to do. This is not the same as interrupting. An interrupt is a device asking for attention later, answered between instructions once the CPU is ready. A refusal is a device saying no to the instruction happening now, so the machine stops where it stands rather than carrying on as though the access had worked. diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index 992a8c5..e458407 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -79,7 +79,7 @@ from `make`, not from here. ### 1. Recorded output `Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares -everything it printed against a file in `Tests/expected`. 192 tests, of which 130 run, 35 +everything it printed against a file in `Tests/expected`. 194 tests, of which 132 run, 35 only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image given at all. diff --git a/Tests/expected/noDeviceTest.out b/Tests/expected/noDeviceTest.out new file mode 100644 index 0000000..3ed325f --- /dev/null +++ b/Tests/expected/noDeviceTest.out @@ -0,0 +1,3 @@ +nobody listening on port 00 +Execution halted. +[exit 0] diff --git a/Tests/expected/noHandlerTest.out b/Tests/expected/noHandlerTest.out new file mode 100644 index 0000000..4a5fbc8 --- /dev/null +++ b/Tests/expected/noHandlerTest.out @@ -0,0 +1,4 @@ +empty vector 28 +carried on +Execution halted. +[exit 0] diff --git a/Tests/manifest b/Tests/manifest index 76e5393..ba3949c 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -190,6 +190,16 @@ maskTest | testPrograms/maskTest.asm | run | - # reach on its own. The round trip through a handler is covered by hand built # binaries until #Vectors arrives. swiFaultTest | testPrograms/swiFaultTest.asm | run | - | - +# And the same fault CAUGHT, which nothing could do until the machine had somewhere to send +# it. Calling a service the system does not implement used to be fatal, and it is an ordinary +# mistake. The number of the empty entry arrives in Q, and the handler steps over the SWI and +# carries on - so the recording says both that the fault was delivered and that a program can +# live through one. +noHandlerTest | testPrograms/noHandlerTest.asm | run | - | - +# The other half: a device raising its line with nothing installed to answer it. Nothing is +# typed at this on purpose - the console raises its line once when input ENDS as well as when +# a byte arrives, and with no input at all that end is what turns up. +noDeviceTest | testPrograms/noDeviceTest.asm | run | - | - # ---- Meeting a byte that is not an instruction ---- # This one is meant to fault. It checks the CPU stops, says what it found and