diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index 500bfdf..d2f191c 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -441,11 +441,13 @@ handleExit: LDD.0.1 MVDS.0 - ; The console goes back to line mode whatever the program left it in. A program that - ; wanted keys is expected to put it back itself, but one that stopped early, or forgot, - ; would otherwise hand back a shell with no echo and no backspace, and the shell has no - ; way to find out that happened. Writing line mode when it is already in line mode costs - ; a byte out of a port and does nothing, which is the right price for not having to know. + ; The console goes back to how the shell wants it, whatever the program left it in: line + ; mode, and not interrupting. A program that wanted either is expected to put it back + ; itself, but one that stopped early, or forgot, would otherwise hand back a shell with + ; no echo and no backspace, or one being interrupted about keys it is reading anyway. + ; Zero is both bits, so this undoes everything the control port can be asked for, and + ; asking for what is already the case costs a byte out of a port and does nothing. That + ; is the right price for not having to know. RSTA OUTA 0x02 diff --git a/Programs/testPrograms/consoleInterruptTest.asm b/Programs/testPrograms/consoleInterruptTest.asm new file mode 100644 index 0000000..ab6b460 --- /dev/null +++ b/Programs/testPrograms/consoleInterruptTest.asm @@ -0,0 +1,161 @@ +; Tests interrupt on input: the console asking for attention instead of being asked. +; +; The control port has two bits and they are independent of one another. Bit 0 puts the +; console in key mode; bit 1 says to put the interrupt line up when a byte arrives. The +; console is on port 0x00, so that is the vector a key comes through - a device raises its +; line on its base port, and the status and control ports belong to the same device as the +; data port. +; +; The main program does nothing at all but wait to be told it is finished. It never looks +; at the console, which is the whole point: every byte is dealt with by the handler. +; +; What the status bits mean: +; bit 0 READY reading the data port will not have to wait +; bit 1 ENDED input has run out for good +; bit 2 KEYMODE the console is in key mode +; bit 3 INTERRUPTS the console is set to raise its line when a byte arrives +; +; This runs with input from a file rather than a terminal, so key mode has no terminal to +; put into another state and nothing here depends on one. A pipe with bytes in it is a +; console with keys waiting as far as the console is concerned. + +#Include console.asm + +#Program + +start: + CIF ; Nothing gets through until there is something to catch it. + + SETD.0 Banner + CALL printString + CALL newLine + + ; Key mode and interrupt on input, asked for in one write. Neither bit depends on the + ; other, so there is no order to get wrong. + INIA 0x03 + OUTA 0x02 + SETD.0 AskedLabel + CALL printString + CALL showStatus + + SETD.0 ArrivedLabel + CALL printString + CALL newLine + + SIF ; From here a byte arriving runs keyHandler. + +waitLoop: + ; Waiting without looking. Nothing in this loop touches the console, so every byte that + ; comes out below was put there by something that interrupted this. + SETD.3 Finished + LDA.3 + RSTB + OR ; Q is the flag, so this is a test for zero. + BRQ waitLoop + + CALL newLine + RSTA + OUTA 0x02 ; Line mode and no interrupts, the way it was found. + SETD.0 DoneLabel + CALL printString + CALL showStatus + HALT + +; Entered because the console had something to say. It is never called. +; +; DP3 is used freely here: an interrupt saves all four Data Pointers and RETI puts them +; back, so a handler cannot disturb what it interrupted no matter what it touches. +keyHandler: + INA 0x01 + INIB 0x02 ; ENDED + AND + BNQ keyEnded + INA 0x00 ; The byte this interrupt was about. + OUTA 0x00 ; Nothing echoes in key mode, so the program does it. + RETI + +keyEnded: + ; The end of input raises the line once, so a program driven entirely by interrupts + ; still finds out that nothing more is coming. Without it this would wait forever for a + ; key that cannot arrive. + SETD.3 Finished + INIA 0x01 + STA.3 + RETI + +; Prints the status byte as hex and then names the bits that are up, so a change in the +; output says which bit moved rather than only that the number is different. +showStatus: + INA 0x01 + PSHA + CALL printByteHex + INIA 0x20 + OUTA 0x00 + POPA + PSHA + + INIB 0x01 + AND + BRQ showNotReady + SETD.0 ReadyWord + CALL printString +showNotReady: + POPA + PSHA + + INIB 0x02 + AND + BRQ showNotEnded + SETD.0 EndedWord + CALL printString +showNotEnded: + POPA + PSHA + + INIB 0x04 + AND + BRQ showNotKeys + SETD.0 KeysWord + CALL printString +showNotKeys: + POPA + + INIB 0x08 + AND + BRQ showNotInterrupts + SETD.0 InterruptsWord + CALL printString +showNotInterrupts: + CALL newLine + RET + +#Data + +Banner: +"console input interrupts" + +AskedLabel: +"asked for: " +ArrivedLabel: +"what arrived:" +DoneLabel: +"at the end: " + +ReadyWord: +"ready " +EndedWord: +"ended " +KeysWord: +"keys " +InterruptsWord: +"interrupts " + +; Set by the handler when the console says there will be no more bytes. It is the only +; thing the handler and the program it interrupts have to say to each other. +Finished: + 0x00 + +#Vectors + + Boot start + Device 0x00 keyHandler diff --git a/Programs/testPrograms/consoleModeTest.asm b/Programs/testPrograms/consoleModeTest.asm index fd285fc..ced4d32 100644 --- a/Programs/testPrograms/consoleModeTest.asm +++ b/Programs/testPrograms/consoleModeTest.asm @@ -10,6 +10,10 @@ ; bit 1 ENDED input has run out for good ; bit 2 KEYMODE the console is in key mode ; +; There is a fourth bit, for whether the console interrupts on input, and nothing here +; sets it. Polling and interrupting are the two ways to get a byte and this is the one +; about polling; consoleInterruptTest.asm is the other. +; ; This runs with input from a file rather than a terminal, so key mode has no terminal to ; put into another state and the mode bit is the only thing that changes. That is on ; purpose: the same program has to work either way, and a test that needed a terminal @@ -60,8 +64,9 @@ readDone: ; ---- The status at the end of input ---- ; - ; READY is set as well as ENDED, because a read does answer at once. It just answers - ; 0xFF forever. ENDED is what says so. + ; ENDED is set and READY is clear, although a read would answer at once here: what it + ; answers is 0xFF standing in for nothing. READY means there is a byte to be had, so the + ; loop above stops on its own rather than taking imaginary bytes forever. SETD.0 AtEndLabel CALL printString CALL showStatus diff --git a/Source/Emulator/cpu.c b/Source/Emulator/cpu.c index e94924e..65fd2d5 100644 --- a/Source/Emulator/cpu.c +++ b/Source/Emulator/cpu.c @@ -674,6 +674,11 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { void stepCPU(CPURegisters *cpu) { if (!(cpu->Status & STATUS_HALT)) { + // Devices get their moment before the lines are read, and unconditionally: a + // device is entitled to notice something whether or not the CPU is currently + // willing to be interrupted about it. Masking decides when a request is answered, + // not whether the outside world is allowed to have happened. + serviceDevices(); // 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. diff --git a/Source/Emulator/io.c b/Source/Emulator/io.c index 17289e0..16fe14b 100644 --- a/Source/Emulator/io.c +++ b/Source/Emulator/io.c @@ -26,6 +26,7 @@ static int consoleKeyMode = 0; static int consoleEnded = 0; static int consolePushback = -1; // A byte already taken from the host, or -1. +static int consoleInterrupts = 0; // Whether an arriving byte puts the line up. static struct termios consoleSavedTerminal; static int consoleTerminalSaved = 0; @@ -35,6 +36,11 @@ void consoleRestore(void) { consoleTerminalSaved = 0; } consoleKeyMode = 0; + // Whatever the console was in the middle of asking for is withdrawn along with the + // mode. A line left standing here would be answered by whatever ran next, which had + // nothing to do with it and never asked to be interrupted. + consoleInterrupts = 0; + clearInterrupt(PORT_CONSOLE); } // Restores the terminal and then dies the way it would have died anyway, so that the @@ -45,8 +51,7 @@ static void consoleSignalHandler(int signalNumber) { raise(signalNumber); } -static void consoleSetMode(uint8_t mode) { - int wantKeys = (mode & CONSOLE_MODE_KEY) != 0; +static void consoleSetMode(int wantKeys) { if (wantKeys == consoleKeyMode) { return; } @@ -79,6 +84,35 @@ static void consoleSetMode(uint8_t mode) { tcsetattr(STDIN_FILENO, TCSANOW, &raw); } +// Puts the line up if the console has something to say and has been asked to say it. +// Called wherever news arrives and wherever a program declares it wants to hear news, so +// that enabling interrupts while a byte is already waiting is not a way to miss it. +static void consoleAnnounce(void) { + if (consoleInterrupts && (consolePushback >= 0 || consoleEnded)) { + raiseInterrupt(PORT_CONSOLE); + } +} + +// The whole control port in one write. The two bits are independent, so both are read out +// of the byte and applied, and neither is inferred from the other. +static void consoleSetControl(uint8_t control) { + // The mode goes first because turning key mode off restores the terminal, and that + // withdraws any standing request along with it. Setting the interrupt bit afterwards + // means one write can ask for line mode and interrupts together, which is an ordinary + // thing to want and would otherwise be undone in the same breath as it was asked for. + consoleSetMode((control & CONSOLE_MODE_KEY) != 0); + + int wantInterrupts = (control & CONSOLE_CONTROL_INTERRUPT) != 0; + if (!wantInterrupts) { + // Asking to stop being interrupted takes down whatever was already asked for. A + // request that outlived the setting that made it would arrive at a program that + // had just said it did not want it. + clearInterrupt(PORT_CONSOLE); + } + consoleInterrupts = wantInterrupts; + consoleAnnounce(); +} + // Everything already written is put where it can be seen before the machine asks the host // anything. Standard output is line buffered on a terminal, so a prompt with no newline // after it - "> " is exactly that, and exactly why this matters - would sit in the buffer @@ -92,6 +126,11 @@ static void consoleShowWhatIsWritten(void) { } uint8_t consoleReadByte(void) { + // Taking the byte answers whatever the console was asking about, so the line comes + // down here as well as when the CPU acknowledges it. Otherwise a program that reads + // the data port with the Interrupt Flag down would be interrupted afterwards on + // behalf of a byte it already has, and find nothing waiting when it looked. + clearInterrupt(PORT_CONSOLE); if (consolePushback >= 0) { uint8_t byte = (uint8_t)consolePushback; consolePushback = -1; @@ -150,10 +189,47 @@ static void consoleFetch(void) { } // A read that failed for any other reason is left alone: the next attempt asks again, // and an interrupted poll is not news. + // + // Anything that was news puts the line up. This is the only place a byte arrives from + // the outside world, so it is the only place that has to, and it raises AT MOST ONCE + // PER BYTE for free: the pushback holds one, and while it is full there is nothing to + // fetch and so nothing to announce. A handler that does not read what it was called + // about is simply not called again, the way a receive register with one byte in it + // stops asking. The end of input announces itself once for the same reason - it is + // discovered once, and every later look leaves before it gets here. + consoleAnnounce(); +} + +// How many instructions the machine runs between glances at the console. Nothing here +// happens alongside the CPU, so noticing a keystroke costs a system call, and asking on +// every instruction costs more than executing one: a poll is about 150ns against roughly +// 9ns for an instruction at full tilt, so it would slow the machine by nearly twenty +// times. At the emulated clock this stride is a quarter of a millisecond between glances, +// which no one typing has ever been able to tell from immediately. +#define CONSOLE_SERVICE_STRIDE 256 + +void serviceDevices(void) { + // The common case is a machine nobody is interrupting, and it costs one test. + if (!consoleInterrupts) { + return; + } + static unsigned int untilNextGlance = 0; + if (untilNextGlance > 0) { + untilNextGlance--; + return; + } + untilNextGlance = CONSOLE_SERVICE_STRIDE - 1; + consoleFetch(); } static uint8_t consoleStatus(void) { - uint8_t status = consoleKeyMode ? CONSOLE_STATUS_KEYMODE : 0; + uint8_t status = 0; + if (consoleKeyMode) { + status |= CONSOLE_STATUS_KEYMODE; + } + if (consoleInterrupts) { + status |= CONSOLE_STATUS_INTERRUPT; + } consoleFetch(); if (consoleEnded) { // READY IS NOT SET HERE, although a read would answer immediately. The bit means @@ -395,7 +471,7 @@ static const DeviceRecord *deviceOnPort(uint8_t port) { } if (port > PORT_CONSOLE && port <= PORT_CONSOLE_TOP) { // The status and control ports are the same device as the data port, which is the - // one in the table and the one that would raise a line if the console ever did. + // one in the table and the one the console raises its line on. return deviceOnPort(PORT_CONSOLE); } if (port > PORT_DISK && port <= PORT_DISK_TOP) { @@ -440,7 +516,7 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) { // Later, I'll want to use a buffer for this for performance, probably. putchar(DataByte); break; - case CONSOLE_CONTROL: consoleSetMode(DataByte); break; + case CONSOLE_CONTROL: consoleSetControl(DataByte); break; case CONSOLE_STATUS: // Read only. A device saying how it is does not take instructions through the // same hole, so a write here is ignored rather than meaning something. @@ -495,8 +571,9 @@ uint8_t InputHandler(uint8_t Address) { break; case CONSOLE_STATUS: return consoleStatus(); case CONSOLE_CONTROL: - // Write only. Reading it gives zero rather than the mode, because the mode is - // a bit in the status port and one fact wants one place to live. + // Write only. Reading it gives zero rather than what was last written, because + // everything it sets is reported by the status port and one fact wants one + // place to live. return 0; break; case DISK_BLOCK_HIGH: return (uint8_t)(diskBlock >> 8); diff --git a/Source/Emulator/io.h b/Source/Emulator/io.h index b475de7..0a4ab16 100644 --- a/Source/Emulator/io.h +++ b/Source/Emulator/io.h @@ -58,10 +58,27 @@ // KEY MODE ONLY REACHES THE TERMINAL when there is one. With input coming from a pipe // there is nothing to put into another mode, and the status port answers by asking the // operating system whether anything is waiting, which is true of a pipe with bytes in it. +// +// A PROGRAM THAT INTERRUPTS ON INPUT MUST NOT BLOCK ON THE DATA PORT. Reading it waits, +// and the machine executes no instructions while it is waiting, so nothing is serviced +// and the line the console is about to raise goes nowhere until the read it was meant to +// replace has already finished. Interrupting and blocking are two answers to the same +// question and a program wants one of them. +// The control port's bits, which are independent of one another. Writing zero asks for +// line mode with no interrupts, which is how the console starts and what a program that +// knows nothing of any of this leaves behind it. #define CONSOLE_MODE_LINE 0x00 #define CONSOLE_MODE_KEY 0x01 +// Asks the console to put its line up when a byte arrives, instead of the program having +// to come and look. It composes with the mode rather than depending on it: in line mode +// the terminal still holds what is typed until Return, and then a whole line's worth of +// bytes arrive at once, each raising the line in turn as the one before it is taken. +// That is not especially useful, but a control bit that quietly did nothing depending on +// another control bit would be worse than a burst of interrupts somebody asked for. +#define CONSOLE_CONTROL_INTERRUPT 0x02 + // Set when there is a byte to be had. NOT set at the end of input, although a read would // answer at once there: what it answers is 0xFF standing in for nothing, and calling that // ready would make a loop that reads while READY spin on imaginary bytes forever. A loop @@ -74,6 +91,9 @@ // Which mode the console is in, so that a program can put it back the way it found it // rather than assuming it knows. #define CONSOLE_STATUS_KEYMODE 0x04 +// Whether the console is set to interrupt, for the same reason: everything a program can +// ask the console to be, it can also ask the console what it currently is. +#define CONSOLE_STATUS_INTERRUPT 0x08 // Puts the terminal back the way it was found. Registered with atexit and called from the // signal handlers, because a machine that stops in key mode and does not undo it leaves @@ -161,6 +181,16 @@ uint8_t InputHandler(uint8_t Address); // These belong to the bus rather than to the CPU. Nothing here is saved in a frame, and // a program cannot read them except by being interrupted. +// Gives every device a moment to notice something the machine did not ask it about. +// Nothing here runs alongside the CPU: a device that waits on the outside world - the +// console is the only one so far - is never going to see a keystroke unless something +// asks it to look, and the CPU calling this between instructions is that something. +// +// It is called on every step and gets out of the way immediately when there is nothing to +// do, because there usually is not. A device that wants attention rarely is a device that +// must cost nothing when it does not. +void serviceDevices(void); + void raiseInterrupt(uint8_t port); void clearInterrupt(uint8_t port); diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index 5295fc3..2109977 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -130,7 +130,7 @@ If a device interrupts and its vector is empty, that is a fault: the machine sto | Port | Device | Class | | --- | --- | --- | -| 0x00 - 0x02 | The console. See The Console below. Writing to 0x00 sends a byte to standard output, reading takes one from standard input. | 0x02 | +| 0x00 - 0x02 | The console. See The Console below. Writing to 0x00 sends a byte to standard output, reading takes one from standard input. It interrupts on 0x00, its base port, when asked to. | 0x02 | | 0x10 | A test device. Writing anything to it puts its own line up, so that interrupt handling can be exercised without waiting on anything. The byte written is ignored. | 0x10 | | 0x11 | A device that refuses everything, in both directions, so that refusal can be exercised without the memory controller. | 0x11 | | 0x20 - 0x23 | The disk. See Storage below. It interrupts on 0x20, its base port. | 0x13 | @@ -340,13 +340,15 @@ Both arrive at the instruction that asked, so a handler sees which one it was. A ## The Console: -Port 0x00 is the oldest thing on this machine and it has not changed: writing sends a byte out, reading takes one in and waits until there is one. Every program ever written for SplitBit uses it that way and still does. What is new is that a program can now ask whether a read would have to wait, and can say what it wants a keypress to mean. +Port 0x00 is the oldest thing on this machine and it has not changed: writing sends a byte out, reading takes one in and waits until there is one. Every program ever written for SplitBit uses it that way and still does. What is new is that a program can say what it wants a keypress to mean, can ask whether a read would have to wait, and can arrange to be told when a byte arrives instead of having to ask at all. | Port | Register | | --- | --- | | 0x00 | Data. Writing sends a byte out, reading takes one in and waits for it. | -| 0x01 | Status. Bit 0 a byte is waiting, bit 1 input has ended, bit 2 the console is in key mode. | -| 0x02 | Mode. Writing 0x00 asks for line mode, 0x01 for key mode. | +| 0x01 | Status. Bit 0 a byte is waiting, bit 1 input has ended, bit 2 the console is in key mode, bit 3 the console is set to interrupt. | +| 0x02 | Control. Bit 0 asks for key mode, bit 1 asks the console to interrupt when a byte arrives. Writing 0x00 asks for neither, which is how the console starts. | + +The control port's two bits are independent, and one write sets both. Everything the control port can ask for, the status port reports, so a program can put the console back the way it found it instead of assuming it knows. ### Two Kinds Of Input: @@ -376,6 +378,38 @@ Reading the data port when input has run out gives 0xFF, which is what it has al Bit 0 is **not** set once input has ended, even though a read would answer immediately. The bit means a byte is there to be had, and at the end of input there is not. That way a loop that reads while bit 0 is set stops when the input does, instead of taking imaginary bytes forever. +### Being Told Instead Of Asking: + +Bit 1 of the control port asks the console to put its interrupt line up when a byte arrives, so a program can get on with something else and be told. The console is on port 0x00, so that is the vector a key comes through, named the way every device is: + +``` +#Vectors + Device 0x00 keyHandler +``` + +The handler is entered because the console had something to say, and asks the status port what. There are two possible answers, and the second is why a program can rely on this instead of also polling: + +``` +keyHandler: + INA 0x01 + INIB 0x02 ; Bit 1: has input ended? + AND + BNQ noMoreKeys ; Nothing more is ever coming. + INA 0x00 ; The byte this interrupt was about. + OUTA 0x00 ; Nothing echoes in key mode, so send it back out. + RETI +``` + +**The end of input raises the line once**, as well as an arriving byte. A program driven entirely by interrupts would otherwise sit forever waiting to be told about a key that cannot arrive. + +**The line goes up at most once per byte.** The console holds one byte, so while that byte is still there, nothing new can arrive to ask about, and a handler that returns without reading it is simply not called again. This is what a receive register holding one byte does, and it means a handler cannot interrupt-storm the machine by forgetting something. The cost is the other half of the same fact: bytes arriving while that one is unread are lost, exactly as they would be on hardware. + +The two control bits do not depend on each other, so a program may ask to be interrupted in line mode. The terminal still holds what is typed until Return, and then the whole line arrives at once as a run of interrupts, one per byte. That is rarely what anyone wants, but a control bit that quietly did nothing because of another control bit would be worse. + +**A program that interrupts on input must not also block on the data port.** Reading it waits, and nothing else in the machine runs while it is waiting, so a program that does both has chosen to wait after asking not to. Interrupting and blocking are two answers to the same question, and a program wants one of them. + +The machine notices an arriving key between instructions, and does not look on every single one. The delay is about a quarter of a millisecond, which is shorter than the gap between two keystrokes by a wide margin and shorter than anything a person can perceive at all. + ## Storage: The disk is a block device. It knows numbered blocks of 256 bytes and has never heard of a file. A filesystem is software this machine runs, not something done on its behalf: a disk that understood filenames would be the emulator doing the work while the machine pretended it had. diff --git a/Tests/docs.sh b/Tests/docs.sh index a46821c..b22b440 100755 --- a/Tests/docs.sh +++ b/Tests/docs.sh @@ -103,6 +103,25 @@ else: problems.append("%s (0x%02X) is a device class and has no row in the Devices" " table" % (name, value)) +# ---- Every console status bit is described ---- +# +# The status port is read by writing a mask and testing it, so a program can only use a bit +# it has been told the number of. Adding one and forgetting to write it down leaves a bit +# that works and that nobody can discover. The section names them as "bit N", so that is +# what is looked for. +status = {name: int(value, 16) + for name, value in re.findall(r'^#define (CONSOLE_STATUS_[A-Z]+)\s+(0x[0-9A-Fa-f]{2})$', + ioh, re.M)} +if "## The Console:" not in pm: + problems.append("the Programming Manual has lost its \"The Console\" section") +else: + console = pm.split("## The Console:")[1].split("\n## ")[0] + for name, value in sorted(status.items(), key=lambda pair: pair[1]): + bit = value.bit_length() - 1 + if "bit %d" % bit not in console: + problems.append("%s is bit %d of the console status port and The Console does" + " not mention it" % (name, bit)) + # ---- Every directive the assembler knows is written down ---- for directive in sorted(set(re.findall(r'"(#[A-Za-z]+)"', util))): if directive not in am: diff --git a/Tests/expected/consoleInterruptTest.out b/Tests/expected/consoleInterruptTest.out new file mode 100644 index 0000000..4b7d27d --- /dev/null +++ b/Tests/expected/consoleInterruptTest.out @@ -0,0 +1,7 @@ +console input interrupts +asked for: 0D ready keys interrupts +what arrived: +keys +at the end: 02 ended +Execution halted after 1597 cycles. +[exit 0] diff --git a/Tests/input/consoleInterruptTest.in b/Tests/input/consoleInterruptTest.in new file mode 100644 index 0000000..38156aa --- /dev/null +++ b/Tests/input/consoleInterruptTest.in @@ -0,0 +1 @@ +keys \ No newline at end of file diff --git a/Tests/manifest b/Tests/manifest index 056b1ab..23a32e6 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -164,6 +164,12 @@ consoleTest | testPrograms/consoleTest.asm | run | consoleTe # What it pins down is that READY is clear at the end of input while ENDED is set, so a # loop reading while READY stops on its own instead of taking imaginary bytes forever. consoleModeTest | testPrograms/consoleModeTest.asm | run | consoleModeTest.in | - +# The console interrupting instead of being asked. The main program never touches the +# console at all, so every byte in that output was delivered by a handler. It also pins +# down the two things that make the feature usable rather than merely present: the end of +# input raises the line once, so an interrupt-driven program is told when to stop, and a +# handler that does not read the byte is not called again, so nothing storms. +consoleInterruptTest | testPrograms/consoleInterruptTest.asm | run | consoleInterruptTest.in | - # Picking a typed line apart, which is how the shell understands anything. Includes a # string beginning with a zero: the assembler strips the quotes before deciding what a # token is, so such a string looked like a malformed literal and was refused.