diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index ec1aef7..20f6149 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -361,7 +361,7 @@ from every assembly file in it. Several are old programs written for the bare ma | Snake | A game. Draws a whole screen with cursor addressing and steers with single keys, asking the console once a frame and never waiting. | | Keys | The console interrupting rather than being asked. The only one that brings a vector of its own, which is what the version two format exists for. | | Say | Prints whatever it was told, which is the shortest thing that shows osArgument working. | -| Files | Writes a file, reads it back, renames it and deletes it, in 645 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. | +| Files | Writes a file, reads it back, renames it and deletes it, in 665 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. | | Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. | | Edit | A line editor. | | Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. | @@ -462,7 +462,7 @@ Typed in as bytes, checked by disassembling it back, and run. It ends with `SWI `Edit` is the first program on this machine that makes a file a person typed - every byte on every disk before it was put there by the host tool. It is line oriented in the manner of `ed`: `l` lists, `a` adds at the end, `i` and `c` and `d` take a line number, `w` writes and `q` stops. -It includes nothing but `services.asm` and `text.asm`: the filesystem and the console are the system's, asked for rather than carried. That is what took it from 4,941 bytes to 1,983 without a line of its own logic changing - and the way that was checked is worth knowing, because the recorded output of the `cosmosEdit` test did not move by a single byte across the rewrite. +It includes nothing but `services.asm` and `text.asm`: the filesystem and the console are the system's, asked for rather than carried. That is what took it from 4,941 bytes to 1,996 without a line of its own logic changing - and the way that was checked is worth knowing, because the recorded output of the `cosmosEdit` test did not move by a single byte across the rewrite. It keeps the document as a **linked list of lines** rather than one buffer with newlines in it. Each line says where the next one is, how long it is, and then its bytes. Inserting is two pointers changed and nothing moved; with a flat buffer it would mean shifting every byte after the edit, on a machine whose only block move is a device asked politely. The price is that deleted lines are not reused, so a heavy session uses more room than the document needs and writing it out is what tidies up. @@ -646,7 +646,7 @@ Running off the end is how a reader finds out it has finished, so it gets an ans `Programs/CosmOS/Apps/Stream.asm` reads an 84,000 byte file through a 256 byte buffer, then reads a small file both ways - whole with `osFileRead` and streamed - and checks that the two agree. -`Programs/CosmOS/Apps/Files.asm` does the whole round trip - write, read, report, rename, delete - in 645 bytes, and includes nothing but the service names. +`Programs/CosmOS/Apps/Files.asm` does the whole round trip - write, read, report, rename, delete - in 665 bytes, and includes nothing but the service names. `osArgument` is how a program is told what it is for. Everything written before it did the same thing however it was started, which is fine for a program that greets you and no use to one that edits a named document. What arrives is the whole rest of the line, spaces and all, rather than a list of words: what counts as an argument is the program's business, and handing over what was typed is the system's. diff --git a/README.md b/README.md index 369331a..1539fe9 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,11 @@ Then `dir` to see what is there, `load Snake.sbx` and `run` to play something, o | --- | --- | | `-d`, `--debug` | Single step through cycles. Each key press advances one instruction. | | `-c`, `--cycles N` | Stop after N cycles rather than running until the program halts. Useful for programs that never halt, and for getting the same output from a run every time. | +| `-f`, `--fast` | Run as fast as the host allows, ignoring the emulated cycle rate. | +| `-D`, `--disk ` | Attach a disk image, creating a 128K one if the file is not there. | +| `-L`, `--disk-cycles N` | How many cycles a block read or write takes. Zero, the default, finishes before the next instruction starts. | +| `-W`, `--write-protect` | Attach the disk read only. A disk whose image the host will not let you write is read only whether you ask for this or not. | +| `-h`, `--help` | Show help and usage information. | **A cycle is one access to memory**, not one instruction. Fetching an opcode is a cycle, fetching each byte after it is another, reading or writing Data Memory is one, every byte a @@ -124,15 +129,26 @@ trusting the answer to be there. Zero is the default and is how the machine has The waiting is one small routine, and it is reached with `RCAL` rather than `CALL` because what it hands back is the settled status in A, and an ordinary call would put A back the way -it found it. Whether real hardware would overlap a fetch with the end of the -previous instruction is left open, and deliberately: this is the conservative model, and -pipelining is a decision to make while drawing the hardware rather than one to inherit from -an emulator. -| `-f`, `--fast` | Run as fast as the host allows, ignoring the emulated cycle rate. | -| `-D`, `--disk ` | Attach a disk image, creating a 128K one if the file is not there. | -| `-L`, `--disk-cycles N` | How many cycles a block read or write takes. Zero, the default, finishes before the next instruction starts. | -| `-W`, `--write-protect` | Attach the disk read only. A disk whose image the host will not let you write is read only whether you ask for this or not. | -| `-h`, `--help` | Show help and usage information. | +it found it. + +**And waiting is not the same kind of cycle as working.** A machine stopped in a `WAIT` is +clocked but is not using the bus, so those cycles are counted apart from the rest and the +halt line says so when there are any: + +``` +Execution halted after 1042474 cycles, 119772 of them waiting. +``` + +Added together they are elapsed time, which is what `--cycles` measures. Told apart they +say whether a program was working or waiting - and that distinction is the only thing that +separates a machine which slept through a slow disk from one which spun on it. The two take +the same wall clock time and print the same characters. When the filesystem's wait was +first written, taking the line-clearing out of `WAIT` moved the total by a single cycle, +20,100 against 20,099, while the idle half halved. + +Whether real hardware would overlap a fetch with the end of the previous instruction is +left open, and deliberately: this is the conservative model, and pipelining is a decision +to make while drawing the hardware rather than one to inherit from an emulator. If the CPU reads a byte that is not an instruction, it goes to the fault handler the program installed. If it installed none, it raises the Fault Flag and halts, and the emulator reports the byte and the address it was found at and exits with a non zero status. The same happens if a program or a device asks for a handler that was never installed. @@ -266,18 +282,19 @@ The disk images tests read from are built first by `Tests/makedisks.sh`, using S test that reads one is therefore checked against a filesystem written by different code from the same written specification, rather than against itself. -`Tests/run.sh` drives that comparison. Five more scripts run alongside it, and each exists +`Tests/run.sh` drives that comparison. Six more scripts run alongside it, and each exists because a recorded file cannot answer its question: - **`Tests/disk.sh`** checks the disk tool on its own: files of every awkward size onto an image and off again, and the things the format says cannot happen refused rather than half done. -- **`Tests/terminal.sh`** checks what a recorded file cannot see. Piped output is buffered and flushed at exit, so a prompt shown before its answer is asked for and one shown an hour late produce identical files; and key mode only touches a terminal when there is one. Both have gone wrong here, and both were found by a person whose terminal stopped working rather than by anything in this suite. So it runs the emulator under a pseudo-terminal and asks directly: that a prompt arrives before input is read, that a keystroke arrives without Return, that the terminal is handed back however the machine dies, and that suspending and resuming leave it as they found it. +- **`Tests/terminal.sh`** checks what a recorded file cannot see. Piped output is buffered and flushed at exit, so a prompt shown before its answer is asked for and one shown an hour late produce identical files; and key mode only touches a terminal when there is one. Both have gone wrong here, and both were found by a person whose terminal stopped working rather than by anything in this suite. So it runs the emulator under a pseudo-terminal and asks directly: that a prompt arrives before input is read, that a keystroke arrives without Return, that the terminal is handed back however the machine dies, and that suspending and resuming leave it as they found it. It also asks the one question about cycles that a recorded file cannot, since the count is stripped from every one: whether a program on a slow disk slept through the wait or spun on it. Both print the same characters and take the same elapsed time, and only the split between idle and bus cycles tells them apart. - **`Tests/native.sh`** checks the assembler that runs on SplitBit against the one that runs on the host, byte for byte, on a boot image and four loadable programs, and then on CosmOS and on itself, and then on the CosmOS that CosmOS built. - **`Tests/agree.sh`** checks the two implementations of SBFS against each other rather than each against itself, by building the same disk with SplitDisk and with CosmOS and comparing the images byte for byte. Every field one of them writes and the other only reads is checked there and nowhere else. +- **`Tests/lint.sh`** checks SplitLint against a fixture written so that every line of it trips exactly one rule. It compares which warning came out and at which line rather than how many came out in total: a count stays right while the thing behind it goes wrong, and breaking one rule's message left the total untouched at twenty three. - **`Tests/docs.sh`** checks the manuals against the code: that every instruction has a row and every row is an instruction, that the counts in the headings are right, that every directive is written down, that every service the system implements is described and every service described is implemented, that every routine the manuals promise exists, that CosmOS still fits in the half of the machine its memory map gives it, and that the worked examples still assemble to the bytes printed beside them. A cycle count is deliberately **not** part of a recorded result. The last line of the emulator's output has the number taken out before anything is compared, keeping only whether the program stopped on its own or ran into its limit, which is behaviour. Two instructions added to CosmOS used to move that number in six unrelated files at once, so a real difference would have arrived in a crowd of meaningless ones. Anything that wants to measure cycles should say so in a test of its own. -To rebuild all three tools with the address and undefined behaviour sanitizers and run the suite under them: +To rebuild all four tools with the address and undefined behaviour sanitizers and run the suite under them: ``` make sanitize diff --git a/Tests/docs.sh b/Tests/docs.sh index 894089b..9558721 100755 --- a/Tests/docs.sh +++ b/Tests/docs.sh @@ -13,6 +13,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" || exit 1 python3 - <<'PY' +import glob import re import sys @@ -580,6 +581,75 @@ else: " 0x%s - the buffers are on top of the variables" % (ends - 1, first.group(1).upper())) +# ---- Every test script the suite runs has a bullet saying why it exists ---- +# +# Two claims in the README went stale at once and neither was noticed: it said FIVE more +# scripts run alongside run.sh when there were six, and "all three tools" when there were +# four. Both are the kind of number that is written once, is true for months, and is then +# quietly wrong - which is the entire subject of this file. +# +# run.sh is the driver rather than one of the others, and makedisks.sh makes the images +# rather than checking anything, so neither is counted. +# The repository README rather than CosmOS's, which is what `readme` above holds. +rootReadme = open("README.md").read() +scripts = sorted(os.path.basename(p) for p in glob.glob("Tests/*.sh") + if os.path.basename(p) not in ("run.sh", "makedisks.sh")) +words = {"three": 3, "four": 4, "five": 5, "six": 6, "seven": 7, "eight": 8, "nine": 9} +said = re.search(r"([A-Za-z]+) more scripts run alongside it", rootReadme) +if not said: + problems.append("the README no longer says how many scripts run alongside run.sh") +elif words.get(said.group(1).lower()) != len(scripts): + problems.append("the README says %s scripts run alongside run.sh, and there are %d: %s" + % (said.group(1), len(scripts), ", ".join(scripts))) +for name in scripts: + if ("`Tests/%s`" % name) not in rootReadme: + problems.append("Tests/%s runs in the suite and the README does not say what it is" + " for" % name) + +# ---- And the tool count is the number of things the makefile builds ---- +makefile = open("makefile").read() +built = re.search(r"^all:(.*)$", makefile, re.M) +if not built: + problems.append("the makefile no longer has an all target this can count") +else: + tools = len(built.group(1).split()) + for said in re.findall(r"(?:build|rebuild) (?:all )?(?:the )?([a-z]+) tools", rootReadme): + if words.get(said) != tools: + problems.append("the README says the %s tools and the makefile builds %d" + % (said, tools)) + +# ---- The sizes the CosmOS README quotes for its own programs ---- +# +# It said Files was 645 bytes in two places and Edit 1,983. They were 665 and 1,996: the +# programs kept being improved and the sentences about how small they are did not move. +# These are the most quotable numbers in the document and the least likely to be rechecked +# by hand, so they are measured. +# +# A claim is " ... in N bytes" on one line. Only lines that name +# an app are looked at, so ordinary prose about bytes is left alone. +apps = {} +for source in glob.glob("Programs/CosmOS/Apps/*.asm"): + apps[os.path.basename(source)[:-4]] = source + +for line in readme.split("\n"): + said = re.search(r"\b(?:in|to) ([\d,]+) bytes", line) + if not said: + continue + named = [name for name in apps + if re.search(r"(?:`|\| )%s(?:\.asm)?\b" % re.escape(name), line)] + if len(named) != 1: + continue + built = subprocess.run(["./Assembler", "-I", "Programs/CosmOS/Source", + apps[named[0]], "-o", os.devnull], + capture_output=True, text=True) + real = re.search(r"Total size: (\d+) bytes", built.stdout) + if not real: + problems.append("could not measure %s, which the CosmOS README quotes a size for" + % named[0]) + elif int(said.group(1).replace(",", "")) != int(real.group(1)): + problems.append("the CosmOS README says %s is %s bytes and it is %s" + % (named[0], said.group(1), real.group(1))) + if problems: print("The manuals and the code disagree:") for p in problems: