Seventy becomes seventy one: a machine that can wait

HALT is terminal - stepCPU returns at once when the Halt Flag is up, so a
halted machine does not execute, service devices, or take an interrupt -
and that has to stay true, because every test ends with a halt and "halted"
is how a program says it has finished. The consequence was that SplitBit
had no way to wait at all. Every wait was a spin, and a spin is bus
traffic: 11.5% of Type over a 14K file on a disk of ten thousand cycles,
after read-ahead had already hidden three quarters of the latency.

WAIT is 0xFE, one byte, no operands, sitting under HALT where the
instruction that almost stops the machine belongs. Three decisions in it:

- A line already standing means there is nothing to wait for, so WAIT does
  nothing. That is what makes test-then-wait race-free.
- Any line ends the wait, masked or not, so a program can sleep on a device
  it has no handler for and read its status afterwards. Masking says who
  answers a request, not whether it happened.
- A line that wakes the CPU without being dispatched is taken down by the
  WAIT. Left standing it would be found by the next WAIT, which would
  return at once - the program would spin exactly as before while looking
  as though it slept.

Waiting is NOT a Status bit, and that is the trap avoided rather than a
gap: Status rides into the interrupt frame and comes back out, so a machine
interrupted mid-wait would return from its handler still waiting, and wait
again for what it had already been given. An internal field instead.

Idle cycles are counted apart from bus cycles and the halt line says so
when there are any, which is what makes the difference observable at all -
with the line-clearing removed the total moves by ONE cycle, 20,100 against
20,099, and only the idle half changes, halving to 9,976. A test on
totals could never have seen it. Tests/terminal.sh asks that question,
being the file for things a recorded output cannot see, and fails with the
clear removed while "both reads finished" still passes.

Three collisions, all found by building it:

- 0xFE was the assembler's "not an instruction" sentinel. getOpcode now
  answers a negative NOT_AN_OPCODE, which is outside the range of every
  possible answer instead of inside the unused part of it.
- 0xFE was also what faultTest and faultResumeTest executed to provoke a
  fault. They now use 0xFD and say why, because they did not fail when it
  became an instruction - they HUNG, having started sleeping instead.
- Keys.asm has had a label called "wait" for a year, and mnemonics are
  matched uppercased. What that reported was "Branch without label" at the
  BRQ thirty lines away. The assembler now refuses a label that is already
  an instruction, at the label, by name; every instruction added takes a
  word out of the space of label names, so this will happen again.
This commit is contained in:
Anachronaut
2026-08-26 11:11:25 -04:00
parent 6b41354f8f
commit c3188ed657
21 changed files with 311 additions and 23 deletions
+4 -2
View File
@@ -39,14 +39,16 @@ start:
OUTA 0x02
SIF
wait:
; Called spin rather than wait because WAIT is an instruction now, and a label may not
; be one. Which is the joke of it: this loop is exactly what WAIT exists to replace.
spin:
; This loop is the point. It never touches the console, so every character that appears
; below was put there by something that interrupted it.
SETD.3 Stopping
LDA.3
RSTB
OR
BRQ wait
BRQ spin
CALL newLine
RSTA
+2 -1
View File
@@ -37,7 +37,7 @@ AsmShapeSelectors:
0d0 0d0 0d0 0d1 0d1 0d1 0d2
AsmInstructionCount:
0d70
0d71
AsmInstructions:
0x00 0d0 "ADD "
@@ -109,4 +109,5 @@ AsmInstructions:
0xE0 0d2 "INA "
0xE1 0d2 "INB "
0xF0 0d0 "NOP "
0xFE 0d0 "WAIT"
0xFF 0d0 "HALT"
+2 -1
View File
@@ -3753,7 +3753,7 @@ ShapeLength:
0d1 0d3 0d2 0d2 0d3 0d4 0d3
InstructionCount:
0d70
0d71
; ---- The instruction table ----
;
@@ -3830,6 +3830,7 @@ Instructions:
0xE0 0d2 "INA "
0xE1 0d2 "INB "
0xF0 0d0 "NOP "
0xFE 0d0 "WAIT"
0xFF 0d0 "HALT"
DumpBytes:
#Reserve 0d16
+7 -2
View File
@@ -5,7 +5,12 @@
; the same byte again, which is why this handler moves the saved address on by one
; first. MVSD is what lets it reach the frame at all.
;
; 0xFE is not an instruction. Writing it as a literal is the only way past the
; CHOSEN AWAY FROM HALT, because the bytes next to it get used. This said 0xFE for
; a long time, and then 0xFE became WAIT - so the test stopped faulting and started
; SLEEPING. It hung instead of failing, which is the worst way for a test to notice
; that the thing it was testing had moved.
;
; 0xFD is not an instruction. Writing it as a literal is the only way past the
; assembler, which is what makes a program containing one buildable.
;
; Correct output is:
@@ -20,7 +25,7 @@ start:
INIA 0x0A
OUTA 0x00
0xFE ; Not an instruction. The handler steps over this.
0xFD ; Not an instruction. The handler steps over this.
INIA 0d75 ; 'K'. Reached only because the handler moved the address on.
OUTA 0x00
+7 -2
View File
@@ -1,6 +1,11 @@
; Tests that the CPU stops when it meets a byte it cannot decode.
;
; 0xFE is not an instruction. Placing it in the Program Segment as a literal gets
; CHOSEN AWAY FROM HALT, because the bytes next to it get used. This said 0xFE for
; a long time, and then 0xFE became WAIT - so the test stopped faulting and started
; SLEEPING. It hung instead of failing, which is the worst way for a test to notice
; that the thing it was testing had moved.
;
; 0xFD is not an instruction. Placing it in the Program Segment as a literal gets
; it past the assembler, which is the only way to build a program containing one.
;
; The CPU should raise the Fault Flag, halt, and leave the Program Counter pointing
@@ -11,5 +16,5 @@
start:
INIA 0d65 ; Something harmless first, so the fault is not at address zero.
0xFE ; Not an instruction.
0xFD ; Not an instruction.
HALT ; Never reached.
+59
View File
@@ -0,0 +1,59 @@
; waitTest.asm
; WAIT, on a disk slow enough to be worth waiting for.
;
; Two reads, waited for rather than spun on, with INTERRUPTS MASKED and no handler
; installed anywhere. That is the shape this instruction exists for: a program sleeps on a
; device it has no handler for and reads the device's status when it wakes.
;
; THE SECOND READ IS THE TEST. A line that wakes the CPU without being dispatched has to
; be taken down by the WAIT itself; left standing, it is found by the next WAIT, which
; returns at once, and the program spins for the whole of the second read while looking
; exactly as though it slept. Both reads print, so a hang fails here - but a program that
; spun would print the same two characters, and what tells them apart is the emulator's
; count of idle cycles against bus cycles. Tests/terminal.sh is where that is checked,
; because settle() strips cycle counts out of every recorded output including this one.
;
; Written by Anachronaut
#Program
start:
CIF
INIA 0d3
OUTA 0xE3
INIA 0x20
OUTA 0xE2
INIA 0x03
OUTA 0xE8
RSTA
OUTA 0x20
RSTA
OUTA 0x21
INIA 0x01
OUTA 0x22
CALL waitDisk
INIA 0x31 ; "1"
OUTA 0x00
RSTA
OUTA 0x20
INIA 0x01
OUTA 0x21 ; Block 1, a different one.
INIA 0x01
OUTA 0x22
CALL waitDisk
INIA 0x32 ; "2"
OUTA 0x00
INIA 0x0A
OUTA 0x00
HALT
waitDisk:
INA 0x23
INIB 0x01
AND
BRQ waitDone
WAIT
BRI waitDisk
waitDone:
RET
#Vectors
Boot start
+1 -1
View File
@@ -33,7 +33,7 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
## The Machine:
- **Harvard architecture.** Two 64K memories, one for instructions and one for data. An instruction can only read the second, which is why strings live there and why the memory controller exists.
- **Its own instruction set**, 70 instructions, four Data Pointers, and a Q register that holds what the ALU last worked out. Small enough that the table describing it fits in the machine's own memory, which is what lets it disassemble and assemble for itself.
- **Its own instruction set**, 71 instructions, four Data Pointers, and a Q register that holds what the ALU last worked out. Small enough that the table describing it fits in the machine's own memory, which is what lets it disassemble and assemble for itself.
- **Interrupts.** Software traps, hardware lines from devices, and faults, all arriving through one vector table with a full context save.
- **A bus programs can enumerate**, so a program can ask what a machine is made of rather than being told.
- **A memory controller** that reads and writes Program Memory, moves blocks between banks, reaches memory that devices bring with them, and guards a range against being written by accident. It is how a SplitBit machine loads a program.
+3 -3
View File
@@ -122,11 +122,11 @@ int checkIfInstruction(intermediateElement *currentElement) {
dot = strchr(dot, '.');
}
uint8_t opcode = getOpcode(token);
if (opcode == 0xFE) {
// It's not an instruction.
int found = getOpcode(token);
if (found == NOT_AN_OPCODE) {
return 0;
}
uint8_t opcode = (uint8_t)found;
int selectorsWanted = dataPointerOperands(opcode);
if (selectorsGiven > selectorsWanted) {
+8 -2
View File
@@ -92,6 +92,7 @@ Instruction instruction_set[] = {
{0xE1, "INB"},
// Special Operations:
{0xF0, "NOP"},
{0xFE, "WAIT"},
{0xFF, "HALT"}
};
@@ -139,13 +140,18 @@ int dataPointerOperands(uint8_t opcode) {
}
}
uint8_t getOpcode(char* mnemonic) {
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;
}
}
return 0xFE; // FE is an unused instruction, we'll use it to indicate an error.
// 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;
}
+5 -1
View File
@@ -119,7 +119,11 @@
const char* getMnemonic(uint8_t opcode);
uint8_t getOpcode(char* mnemonic);
// The opcode a mnemonic assembles to, or NOT_AN_OPCODE if the word is not one. The
// return is an int rather than a byte so that the answer "no" cannot be confused with any
// of the 256 answers "yes" - see the note in getOpcode.
#define NOT_AN_OPCODE (-1)
int getOpcode(char* mnemonic);
// How many Data Pointer selector bytes follow the given opcode. Never more than two.
#define MAX_DATA_POINTER_OPERANDS 2
+28
View File
@@ -81,6 +81,34 @@ void addLabel(char *labelName, uint16_t address, int type, const char *fileName,
cleanedLabel[len - 1] = '\0'; // Remove the colon
}
// A NAME THAT IS ALREADY AN INSTRUCTION IS REFUSED, and the error is here rather
// than at the branch that could not find it. Mnemonics are matched with the token
// uppercased, so a label called "wait" and the instruction WAIT are the same word
// - and when WAIT was added, Keys.asm had been using that label for a year. What
// it reported was "Branch without label" at the BRQ, thirty lines from the cause
// and naming nothing that had changed.
//
// This will happen again. Every instruction added takes a word out of the space
// of label names, so the check belongs where the name is claimed.
{
char upper[8];
int n = 0;
for (; cleanedLabel[n] != '\0' && n < (int)sizeof(upper) - 1; n++) {
upper[n] = (char)toupper((unsigned char)cleanedLabel[n]);
}
upper[n] = '\0';
// Only a name short enough to BE a mnemonic can collide with one, and the
// longest is four characters. A longer name is truncated by the loop above
// and would not match anything, which is the right answer.
if (cleanedLabel[n] == '\0' && getOpcode(upper) != NOT_AN_OPCODE) {
fprintf(stderr, RED "Error: \"%s\" is an instruction, so it cannot also be"
" a label.\n" RESET, cleanedLabel);
printf("File: %s at line %d.\n", fileName, lineNumber);
free(cleanedLabel);
exit(1);
}
}
// A name may only be defined once. Without this check a reference quietly
// resolves to whichever definition came first, so a typo or a name that two
// libraries both happen to use is very hard to track down.
+48
View File
@@ -143,6 +143,9 @@ void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemor
cpu->StackPointer = 0xFFFF;
cpu->Program = programMemory;
cpu->Data = dataMemory;
cpu->busCycles = 0;
cpu->idleCycles = 0;
cpu->Waiting = 0;
cpu->Fault = FAULT_NONE;
cpu->FaultVector = 0;
}
@@ -759,6 +762,23 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
case 0xF0:
// NOP - Do nothing.
break;
case 0xFE:
// WAIT - Stop fetching until a device asks for attention.
//
// NOT A HALT. The machine is still clocked and devices still run; what stops
// is the CPU's use of the bus. HALT is how a program says it has finished and
// must stay that way, so this is a separate instruction rather than a gentler
// HALT - and its state is a field of its own rather than a Status bit, for
// the reason cpu.h gives.
//
// A LINE ALREADY UP MEANS THERE IS NOTHING TO WAIT FOR, and that is what
// makes the ordinary idiom race-free: a program tests its device, finds it
// busy, and waits. If the device finished in between, the line is standing
// and this does nothing at all rather than sleeping through the answer.
if (nextPendingInterrupt() < 0) {
cpu->Waiting = 1;
}
break;
case 0xFF:
// HALT - Set the Halt Bit of the Status Register.
cpu->Status |= STATUS_HALT;
@@ -777,6 +797,34 @@ void stepCPU(CPURegisters *cpu) {
// willing to be interrupted about it. Masking decides when a request is answered,
// not whether the outside world is allowed to have happened.
serviceDevices();
// ---- Stopped in a WAIT ----
//
// Nothing is fetched and nothing is executed. A clock still passes, because a
// device that takes time has to be able to reach the end of it, and it is charged
// to idle rather than to the bus: the CPU is not using memory.
//
// A LINE OF ANY KIND ENDS THE WAIT, masked or not. Masking says who answers a
// request, not whether it happened - so a program can sleep on a device it has no
// handler for and simply read its status afterwards, which is the whole reason
// this is worth having and is what the filesystem does with it.
if (cpu->Waiting) {
if (nextPendingInterrupt() < 0) {
cpu->idleCycles++;
return;
}
cpu->Waiting = 0;
// Woken while masked, so nobody is going to answer this line and take it
// down. IT HAS TO BE TAKEN DOWN HERE. Left standing it would be found by the
// next WAIT, which would return at once, and by the one after that - the
// program would spin exactly as it did before while appearing to sleep.
//
// Unmasked, the dispatch below takes it down instead, and a line with no
// handler faults there the way it always has. Waiting changes what the CPU
// does between instructions; it does not change interrupt policy.
if (!(cpu->Status & STATUS_INTERRUPT)) {
clearInterrupt((uint8_t)nextPendingInterrupt());
}
}
// A device asking for attention is answered between instructions and never
// inside one, so the address that goes into the frame is always the start of an
// instruction and RETI always lands somewhere meaningful.
+13
View File
@@ -75,6 +75,19 @@ typedef struct {
// cost the same, and that a CALL moving ten bytes of Stack cost what a branch costs,
// which is not true of any machine anybody could build.
unsigned long busCycles;
// ---- And what it has cost while doing nothing ----
//
// Clocks spent inside WAIT, where the CPU is stopped and the bus is idle. They are
// counted because time still has to pass - a device that takes a while has to be able
// to finish - and they are counted SEPARATELY because they are not the same thing as
// work. A machine waiting on a disk is not using memory, and charging it as though it
// were is exactly the sort of dishonest number the bus count was built to replace.
unsigned long idleCycles;
// Whether the CPU is stopped in a WAIT, which is not a Status bit and must not become
// one: the Status register rides into the interrupt frame and comes back out of it, so
// a machine interrupted while waiting would return from the handler still waiting, and
// wait again for the thing it had already been given.
uint8_t Waiting;
// Set alongside the Fault Flag, and read only by whatever reports the stop.
uint8_t Fault; // A FaultCause.
uint8_t FaultVector; // Which vector was empty, when Fault is FAULT_NO_HANDLER.
+24 -4
View File
@@ -65,6 +65,22 @@ char *programFile = NULL;
// Memory Banks:
uint8_t Program[0x10000], Data[0x10000];
// How the run is reported. The idle half is mentioned only when there is one, so that
// every program written before WAIT existed prints exactly the line it always did.
//
// THE TWO ARE NOT THE SAME KIND OF TIME. A bus cycle is the machine using memory; an idle
// cycle is the machine stopped in a WAIT while a device catches up. Added together they
// are elapsed time, which is what a cycle limit measures; told apart they say whether a
// program was working or waiting.
static void reportCycles(const CPURegisters *cpu, unsigned long cycleCount) {
if (cpu->idleCycles > 0) {
printf("Execution halted after %lu cycles, %lu of them waiting.\n",
cycleCount, cpu->idleCycles);
} else {
printf("Execution halted after %lu cycles.\n", cycleCount);
}
}
int main (int argc, char *argv[]) {
EmulatorOptions options;
uint8_t result = parseOptions(argc, argv, &options);
@@ -133,9 +149,13 @@ int main (int argc, char *argv[]) {
// gone rather than after so many steps. In debug mode the budget is one, and any
// instruction costs at least the fetch of its own opcode, so one step still runs.
for (long spent = 0; spent < cycles; ) {
unsigned long before = cpu.busCycles;
// Both kinds of cycle, because both are time passing. A step that waits
// spends no bus at all, and a budget measured only in bus cycles would never
// be spent - the machine would sit inside one batch forever and the device it
// was waiting for would never be given a moment to finish.
unsigned long before = cpu.busCycles + cpu.idleCycles;
stepCPU(&cpu);
unsigned long took = cpu.busCycles - before;
unsigned long took = (cpu.busCycles + cpu.idleCycles) - before;
spent += (long)took;
cycleCount += took;
// Time has passed, so anything waiting on it may be finished.
@@ -159,7 +179,7 @@ int main (int argc, char *argv[]) {
printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount);
} else if (cpu.Status & STATUS_FAULT) {
// The Program Counter is still pointing at whatever the CPU could not get past.
printf("Execution halted after %lu cycles.\n", cycleCount);
reportCycles(&cpu, cycleCount);
if (cpu.Fault == FAULT_NO_HANDLER) {
fprintf(stderr, "Fault: Software vector %u, dispatched from Program Address 0x%04X, has no handler installed.\n",
cpu.FaultVector, cpu.ProgramCounter);
@@ -175,7 +195,7 @@ int main (int argc, char *argv[]) {
}
return 1;
} else {
printf("Execution halted after %lu cycles.\n", cycleCount);
reportCycles(&cpu, cycleCount);
}
return 0;
}
+45 -1
View File
@@ -46,6 +46,7 @@ It has ten registers:
- Bit 1 is the Fault Flag. It is set when the CPU cannot get past something and no handler was installed to deal with it: a byte that is not an instruction, or a dispatch through an empty vector. See Faults.
- Bit 2 is the Interrupt Flag. It is set by SIF and cleared by CIF. While it is set the CPU answers devices asking for attention; while it is clear they wait. Arriving at a handler clears it, and RETI restores it along with the rest of the Status register. See Hardware Interrupts.
- Bit 7 is the Halt Flag. It is set by the HALT instruction, and by a fault.
- There is no Wait Flag. The CPU stopped in a WAIT is stopped in a way no program can see, precisely because this register is saved and restored across an interrupt and a wait must not be. See WAIT under Special Operations.
Each condition has a branch both ways round, so a loop that carries on while something is not zero is one instruction rather than a branch over an unconditional one. Before these existed a quarter of every conditional branch in the corpus was written backwards and padded out, and each of those needed a label invented only to be jumped past.
@@ -165,12 +166,55 @@ LDD and STD are how a program follows an address it has stored, rather than one
| E0 | INA | 2 | Writes the value of an Input to A. The input port is specified by the next byte of Program Memory. |
| E1 | INB | 2 | Writes the value of an Input to B. The input port is specified by the next byte of Program Memory. |
### Special Operations: 2 Instructions
### Special Operations: 3 Instructions
| Hex Code | Mnemonic | Bytes | Description |
| -- | ---- | -- | -- |
| F0 | NOP | 1 | Perform no Operation, increment the Program Counter. |
| FE | WAIT | 1 | Stops fetching until a device asks for attention. |
| FF | HALT | 1 | Stops CPU Execution. |
WAIT is not a gentler HALT and the two are not interchangeable. HALT stops the machine and
is how a program says it has finished; WAIT stops only the CPU's use of the bus. The clock
runs, devices run, and the moment any of them raises a line the CPU carries on with the
instruction after the WAIT.
**A line that is already up means there is nothing to wait for**, and WAIT does nothing at
all. That is what makes the ordinary shape of it safe:
```asm
waitForDisk:
INA 0x23 ; The status port.
INIB 0x01
AND
BRQ ready ; Not busy, so there is nothing to wait for.
WAIT
BRI waitForDisk
ready:
```
Test the device, then wait. If the device finishes in the gap between the two, its line is
standing when WAIT runs and the wait is skipped rather than slept through.
**Any line ends the wait, whether or not the Interrupt Flag is set.** Masking decides who
answers a request, not whether it happened - so a program can sleep on a device it has no
handler for at all and simply read the device's status afterwards, which is what the loop
above does. If the flag *is* set, everything happens as it always does: the handler runs
and RETI comes back to the instruction after the WAIT.
A line that wakes the CPU without being dispatched to a handler is taken down by the WAIT
itself. Otherwise it would still be standing at the next WAIT, which would return at once,
and at the one after that - the program would spin exactly as it would have without the
instruction, while looking as though it slept.
**Waiting is not a bit in the Status register**, and that is deliberate rather than an
oversight. Status is pushed into the interrupt frame and restored by RETI, so a machine
that took an interrupt while waiting would come back from the handler still waiting, and
wait again for the thing it had already been given.
Nothing wakes a WAIT that no device will ever interrupt. That is a program saying it has
nothing to do until something happens, and if nothing can happen it waits for ever, the
same way an unconditional branch to itself loops for ever.
# Making It Do Something
## Making It Print Something:
+1 -1
View File
@@ -138,7 +138,7 @@ words = {12: "Twelve", 13: "Thirteen", 14: "Fourteen", 15: "Fifteen", 16: "Sixte
17: "Seventeen", 18: "Eighteen", 19: "Nineteen", 20: "Twenty",
21: "Twenty one", 22: "Twenty two", 23: "Twenty three", 24: "Twenty four",
25: "Twenty five", 26: "Twenty six"}
selectors = asmc[asmc.index("int dataPointerOperands"):asmc.index("uint8_t getOpcode")]
selectors = asmc[asmc.index("int dataPointerOperands"):asmc.index("int getOpcode")]
taking = len(re.findall(r'^\s*case 0x[0-9A-Fa-f]{2}:', selectors, re.M))
said = re.search(r'^([A-Z][a-z]+(?: [a-z]+)?) instructions work through a Data Pointer\.',
pm, re.M)
+1 -1
View File
@@ -1,3 +1,3 @@
Fault: 0xFE at Program Address 0x0002 is not an instruction.
Fault: 0xFD at Program Address 0x0002 is not an instruction.
Execution halted.
[exit 1]
+3
View File
@@ -0,0 +1,3 @@
12
Execution halted.
[exit 0]
+7
View File
@@ -560,6 +560,13 @@ stringKeyword | testPrograms/stringKeyword.asm | run | -
# version of RCAL that quietly did what CALL does would pass a test that only checked it
# returned to the right place.
rawCallAndOffsets | testPrograms/rawCallAndOffsets.asm | run | - | -
# WAIT, the seventy first instruction, on a disk given ten thousand cycles of latency. Two
# reads with interrupts masked and no handler anywhere: the machine sleeps through both and
# reads the status port when it wakes. What is recorded here is that it FINISHES - a WAIT
# that never woke would hang, and a hang is what this catches. Whether it slept or spun is
# a question about cycle counts, which settle() strips, so Tests/terminal.sh asks that one.
waitTest | testPrograms/waitTest.asm | run | - | 5000000 | disks/sbfs.img@10000
printDecimalTest | testPrograms/printDecimalTest.asm | xfail | - | -
printDigitTest | testPrograms/printDigitTest.asm | xfail | - | -
printHexTest | testPrograms/printHexTest.asm | xfail | - | -
+2 -1
View File
@@ -103,7 +103,8 @@ settle() {
# exists to remove was sitting inside a line rather than at the head of one, and
# survived. replCalculator is the one that does that, and it was the only test to churn
# when the machine started charging for memory instead of counting instructions.
sed -i -E 's/Execution halted after [0-9]+ cycles\./Execution halted./;
sed -i -E 's/Execution halted after [0-9]+ cycles, [0-9]+ of them waiting\./Execution halted./;
s/Execution halted after [0-9]+ cycles\./Execution halted./;
s/Execution stopped after [0-9]+ cycles\. \(cycle limit reached\)/Execution stopped. (cycle limit reached)/' "$1"
}
+41
View File
@@ -60,11 +60,21 @@ ASM
"$ROOT/Assembler" "$BUILD/prompt.asm" -o "$BUILD/prompt.bin" >/dev/null 2>&1 || {
echo "Could not assemble the terminal test programs."; exit 1; }
# And the one that waits. Assembled from the repository rather than written inline, because
# it is a real test program that run.sh also runs - there it proves the machine wakes up at
# all, and here it proves it was asleep.
"$ROOT/Assembler" "$ROOT/Programs/testPrograms/waitTest.asm" -o "$BUILD/waitTest.bin" \
>/dev/null 2>&1 || { echo "Could not assemble waitTest."; exit 1; }
"$ROOT/SplitDisk" format "$BUILD/wait.img" 32 1 >/dev/null 2>&1 || {
echo "Could not make the disk waitTest reads."; exit 1; }
python3 - "$ROOT" "$BUILD" <<'PY'
import os
import pty
import select
import signal
import re
import subprocess
import sys
import time
@@ -249,6 +259,37 @@ try:
except FileNotFoundError:
report(False, "suspending and resuming", "could not measure")
# ---- Waiting is not the same as spinning ----
#
# settle() strips cycle counts out of every recorded output, so no test in run.sh can see
# the difference between a machine that slept through a slow disk and one that spun on it.
# The two print the same characters and take the same elapsed time. What separates them is
# WHICH KIND of cycle went by, and that is only visible here.
#
# waitTest reads two blocks from a disk given ten thousand cycles of latency, with
# interrupts masked and no handler installed. Nearly all of the run should be idle. With
# the line-clearing removed from WAIT the total barely moves - it was 20,100 against
# 20,099 - and the idle count halves, because the second wait finds the first wait's line
# still standing and returns at once.
waitProgram = os.path.join(build, "waitTest.bin")
waitDisk = os.path.join(build, "wait.img")
if os.path.exists(waitProgram) and os.path.exists(waitDisk):
run = subprocess.run([emulator, waitProgram, "--fast", "--cycles", "5000000",
"--disk", waitDisk, "--disk-cycles", "10000"],
capture_output=True, text=True)
got = re.search(r"halted after (\d+) cycles, (\d+) of them waiting", run.stdout)
if not got:
report(False, "a slow disk is waited for", "no idle cycles were reported at all")
else:
total, idle = int(got.group(1)), int(got.group(2))
report("12" in run.stdout, "both reads finished", "%d cycles" % total)
# Two waits of ten thousand cycles each. Sleeping through both puts idle over
# nine tenths of the run; spinning through either drops it to about half.
report(idle > total * 0.9, "and the machine slept rather than spun",
"%d of %d idle (%.0f%%)" % (idle, total, 100.0 * idle / total))
else:
report(False, "a slow disk is waited for", "waitTest.bin or the disk is missing")
print()
if problems:
print("The terminal does not survive everything it should:")