CosmOS: a service interface for the disk and console, and the monitor in the shell
Two changes that arrived together because both live in cosmos.asm. THE SERVICES. A loaded program that wanted a file had to include the whole filesystem, carrying two and a half kilobytes of a private copy of code the system already had running, and then mount a disk that was already mounted. Five services are added at pinned numbers 20 to 24: osFileRead, osFileSave, osFileDelete, osFileRename and osPrintNumber. The sizes fit the registers exactly in both directions. A file that can be read into Data Memory is under 64K by definition, so its length is sixteen bits: coming back it is DP3, going out it is A and B together, and neither direction needs a record in memory whose shape both sides must agree on. There is deliberately no service to mount a disk. The system mounts one before its first prompt, and a program mounting it again was only ever a consequence of owning a second copy of the library, so that call disappears rather than moving. Apps/Files.asm writes, reads, renames and deletes a file in 645 bytes and includes nothing but the service names. THE MONITOR. Previously an application, now part of the shell, because an application occupies the one region a loaded application is given: a monitor that was an application could never examine another one, since loading the thing to be inspected would replace the thing doing the inspecting. "monitor" turns it on and the prompt becomes "*". It is a mode rather than a sub-prompt, and it persists: because the mode is a variable the prompt reads rather than a second loop, and every path back to the prompt goes through one place including osExit, a program started with "g" that gives the machine back arrives at the monitor prompt it was started from. Examining a program and running it therefore do not interrupt each other. "exit" leaves whatever you are in. It supersedes dump, and adds disassembly, writing bytes, and jumping to an address. Its instruction table is generated from the assembler's own list by Tests/instructiontable.py rather than typed again, and Tests/docs.sh checks both that the system's copy matches the generator and that the lengths that table implies are the ones the manual's Bytes column prints. A disassembler that disagreed about a length would not print one line wrong, it would lose its place and print everything after it wrong. Also here: b refuses a bank that is not registered, since asking the controller for one is refused and a refusal nobody catches stops the machine; g records the Stack the way run does, without which a program returning through osExit restored whatever the last run had left; and make cosmos-disk now depends on the system as well as the image. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
301716e869
commit
0b6d2be43f
+86
-5
@@ -184,17 +184,98 @@ else:
|
||||
# services.asm is the one place the numbers are written, and both the system and every
|
||||
# program include it. A service added there and not here is one nothing can find out about
|
||||
# except by reading the source of the operating system.
|
||||
# DECLARING A SERVICE AND IMPLEMENTING ONE ARE DIFFERENT THINGS, and the manual should
|
||||
# describe the second. services.asm names them and fixes their numbers, which is what lets a
|
||||
# number be pinned before anything answers to it; cosmos.asm is where a name gets a handler.
|
||||
# A row for a service nothing implements would be describing a call that faults, and a
|
||||
# missing row for one that works is a service nobody can find out about.
|
||||
services = read("Programs/CosmOS/Source/services.asm")
|
||||
offered = re.findall(r'^\s{2}(os[A-Za-z]+)\s+0d\d+', services, re.M)
|
||||
if not offered:
|
||||
named = set(re.findall(r'^\s{2}(os[A-Za-z]+)\s+0d\d+', services, re.M))
|
||||
system = read("Programs/CosmOS/Source/cosmos.asm")
|
||||
vectors = system.split("#Vectors")[-1] if "#Vectors" in system else ""
|
||||
implemented = {name for name in re.findall(r'^\s{2}(os[A-Za-z]+)\s+[a-zA-Z]', vectors, re.M)
|
||||
if name in named}
|
||||
if not named:
|
||||
problems.append("no services could be found in services.asm")
|
||||
elif "## What A Program May Ask The System For:" not in pm:
|
||||
problems.append("the Programming Manual has lost its services section")
|
||||
else:
|
||||
section = pm.split("## What A Program May Ask The System For:")[1].split("\n## ")[0]
|
||||
for name in offered:
|
||||
if ("| %s |" % name) not in section:
|
||||
problems.append("%s is a service and has no row in the services table" % name)
|
||||
documented = set(re.findall(r'^\| (os[A-Za-z]+) \|', section, re.M))
|
||||
for name in sorted(implemented - documented):
|
||||
problems.append("%s is a service the system implements and has no row in the"
|
||||
" services table" % name)
|
||||
for name in sorted(documented - implemented):
|
||||
problems.append("the services table describes %s, which nothing implements: calling"
|
||||
" it would dispatch through an empty vector and fault" % name)
|
||||
|
||||
# ---- Every program the manual describes is really there ----
|
||||
#
|
||||
# The table names what the shell can load. A program renamed or removed leaves a row
|
||||
# describing something nobody can run, which is the same kind of quiet wrongness as a
|
||||
# routine that no longer exists. The other direction is deliberately not checked: the ported
|
||||
# programs are covered in the prose rather than given a row each.
|
||||
import os
|
||||
if "## Programs That Come With The System:" not in pm:
|
||||
problems.append("the Programming Manual has lost its list of programs")
|
||||
else:
|
||||
listed = pm.split("## Programs That Come With The System:")[1].split("\n### ")[0]
|
||||
# After the separator, so the table's own heading row is not mistaken for a program.
|
||||
listed = listed.split("| --- |")[-1]
|
||||
for name in re.findall(r'^\| ([A-Z][A-Za-z0-9-]*) \|', listed, re.M):
|
||||
if not os.path.exists("Programs/CosmOS/Apps/%s.asm" % name):
|
||||
problems.append("the manual describes a program called %s, and there is no"
|
||||
" Programs/CosmOS/Apps/%s.asm" % (name, name))
|
||||
|
||||
# ---- The monitor's instruction table is the assembler's ----
|
||||
#
|
||||
# The monitor disassembles, so it needs the same 64 instructions with the same names and the
|
||||
# same lengths. A disassembler that disagreed about a length would not print one line wrong,
|
||||
# it would lose its place and print everything after it wrong, which is the worst way for a
|
||||
# tool like that to fail: confidently. So the table is generated from assembly.c by
|
||||
# Tests/instructiontable.py, and what is in the monitor is checked against it here.
|
||||
import subprocess
|
||||
generated = subprocess.run([sys.executable, "Tests/instructiontable.py"],
|
||||
capture_output=True, text=True)
|
||||
if generated.returncode != 0:
|
||||
problems.append("the instruction table generator would not run")
|
||||
else:
|
||||
wanted = [line.rstrip() for line in generated.stdout.splitlines() if line.strip()]
|
||||
monitor = read("Programs/CosmOS/Source/cosmos.asm")
|
||||
if "\nInstructions:\n" not in monitor:
|
||||
problems.append("the system has lost its instruction table")
|
||||
else:
|
||||
block = monitor.split("\nInstructions:\n")[1]
|
||||
have = []
|
||||
for line in block.splitlines():
|
||||
if not line.strip() or not line.startswith(" 0x"):
|
||||
break
|
||||
have.append(line.rstrip())
|
||||
if have != wanted:
|
||||
problems.append("the system's instruction table is not what the assembler's"
|
||||
" instruction set generates: %d entries against %d, first"
|
||||
" difference at %s"
|
||||
% (len(have), len(wanted),
|
||||
next((a or b for a, b in zip(have + [None] * len(wanted),
|
||||
wanted + [None] * len(have))
|
||||
if a != b), "the end")))
|
||||
|
||||
# ---- And the lengths that table implies are the ones the manual prints ----
|
||||
#
|
||||
# The generator works out how long each instruction is from rules written in it; the manual
|
||||
# says so in a column somebody typed. They are independent accounts of the same fact, which
|
||||
# is exactly the pair worth checking against each other.
|
||||
sys.path.insert(0, "Tests")
|
||||
import instructiontable
|
||||
lengthOf = {0: 1, 1: 3, 2: 2, 3: 2, 4: 3, 5: 4, 6: 3}
|
||||
printed = {}
|
||||
for m in re.finditer(r'^\|\s*[0-9A-F]{2}\s*\|\s*([A-Z][A-Z0-9]*)\s*\|\s*(\d+)\s*\|', pm, re.M):
|
||||
printed[m.group(1)] = int(m.group(2))
|
||||
for opcode, name in instructiontable.table():
|
||||
implied = lengthOf[instructiontable.shapeOf(opcode)]
|
||||
if name in printed and printed[name] != implied:
|
||||
problems.append("the manual says %s is %d bytes and the disassembler will read it"
|
||||
" as %d" % (name, printed[name], implied))
|
||||
|
||||
# ---- Every directive the assembler knows is written down ----
|
||||
for directive in sorted(set(re.findall(r'"(#[A-Za-z]+)"', util))):
|
||||
|
||||
@@ -4,10 +4,9 @@ load <file> read a program off the disk
|
||||
run [words] start what was loaded, and tell it those words
|
||||
delete <file> take it off the disk
|
||||
rename <file> <to> call it something else
|
||||
dump sixty four bytes of memory, and again for more
|
||||
dump <program|data|bank> <address>
|
||||
monitor look at memory, change it, and jump into it
|
||||
help this
|
||||
exit stop
|
||||
exit stop, or leave the monitor if you are in it
|
||||
> greeting.txt 17
|
||||
filler1.txt 8
|
||||
filler2.txt 8
|
||||
|
||||
@@ -8,10 +8,12 @@ finished
|
||||
cd
|
||||
the console has been handed back
|
||||
finished
|
||||
> > FE00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||
> > x examine, d disassemble, s set, b bank, g go, exit leaves
|
||||
* bank 00
|
||||
* FE00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||
FE10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||
FE20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||
FE30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||
> halted
|
||||
* > halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
CosmOS
|
||||
> x examine, d disassemble, s set, b bank, g go, exit leaves
|
||||
* b <program|data|number>
|
||||
* there is no such bank
|
||||
* loaded, starting at 2000
|
||||
* bank 00
|
||||
* 2000 47 00 10 00 SETD.0 1000
|
||||
2004 18 10 SWI 10
|
||||
2006 47 00 10 44 SETD.0 1044
|
||||
200A 18 10 SWI 10
|
||||
200C 47 00 10 7A SETD.0 107A
|
||||
2010 27 1F INIB 1F
|
||||
2012 18 11 SWI 11
|
||||
2014 47 00 10 5D SETD.0 105D
|
||||
* 2000 47 00 10 00 18 10 47 00 10 44 18 10 47 00 10 7A G.....G..D..G..z
|
||||
2010 27 1F 18 11 47 00 10 5D 18 10 47 00 10 7A 18 10 '...G..]..G..z..
|
||||
2020 47 00 10 65 18 10 18 12 00 00 00 00 00 00 00 00 G..e............
|
||||
2030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||
* bank 01
|
||||
* 1000 61 20 70 72 6F 67 72 61 6D 2C 20 6C 6F 61 64 65 a program, loade
|
||||
1010 64 20 6F 66 66 20 61 20 64 69 73 6B 2C 20 72 75 d off a disk, ru
|
||||
1020 6E 6E 69 6E 67 20 6F 6E 20 74 68 65 20 73 79 73 nning on the sys
|
||||
1030 74 65 6D 20 74 68 61 74 20 6C 6F 61 64 65 64 20 tem that loaded
|
||||
* bank 02
|
||||
* 0000 01 FF 00 00 00 00 00 00 01 FF 00 00 00 00 00 00 ................
|
||||
0010 03 FF 08 00 00 00 00 00 01 20 01 00 00 00 00 00 ......... ......
|
||||
0020 00 FF 00 00 00 00 00 00 00 FF 00 00 00 00 00 00 ................
|
||||
0030 00 FF 00 00 00 00 00 00 00 FF 00 00 00 00 00 00 ................
|
||||
* Fault: The device on port 233 refused the access at Program Address 0x1359, and nothing is installed to deal with it.
|
||||
Execution halted.
|
||||
[exit 1]
|
||||
@@ -0,0 +1,12 @@
|
||||
CosmOS
|
||||
> loaded, starting at 2000
|
||||
> saved it
|
||||
read it back, 22 bytes:
|
||||
a file kept by asking
|
||||
renamed it
|
||||
deleted it
|
||||
and it is gone
|
||||
finished
|
||||
> halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -0,0 +1,3 @@
|
||||
load Files.sbx
|
||||
run
|
||||
exit
|
||||
@@ -3,5 +3,8 @@ run
|
||||
abq
|
||||
run
|
||||
cdq
|
||||
dump program fe00
|
||||
monitor
|
||||
b program
|
||||
x fe00
|
||||
exit
|
||||
exit
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
monitor
|
||||
b nonsense
|
||||
b 9
|
||||
load greet.sbx
|
||||
b program
|
||||
d 2000
|
||||
x 2000
|
||||
b data
|
||||
x 1000
|
||||
b 2
|
||||
x 0
|
||||
s 8000 26 48 D1 00 26 0A D1 00 18 12
|
||||
d 8000
|
||||
g 8000
|
||||
d 8000
|
||||
exit
|
||||
dir
|
||||
exit
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The instruction table, as the assembler has it.
|
||||
|
||||
The monitor needs the same 64 instructions the assembler does, with the same names and the
|
||||
same lengths, and a disassembler that disagreed with the assembler about how long an
|
||||
instruction is would not merely print one thing wrong - it would lose its place and print
|
||||
everything after it wrong too. So the table is generated from assembly.c rather than typed
|
||||
out again, and Tests/docs.sh checks the generated form against what is in the monitor.
|
||||
|
||||
Shapes are what follows the opcode:
|
||||
0 nothing 1 an address 2 one byte 3 a Data Pointer selector
|
||||
4 selector, byte 5 selector, address 6 two selectors
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
ADDRESS = {0x10, 0x11, 0x12, 0x13, 0x14, 0x17, 0x1A, 0x1B, 0x1C, 0x1D}
|
||||
ONE_BYTE = {0x18, 0x26, 0x27}
|
||||
TWO_SELECTORS = {0x4A, 0x4B}
|
||||
SELECTOR = {0x15, 0x33, 0x36, 0x40, 0x41, 0x42, 0x43, 0x44,
|
||||
0x45, 0x46, 0x47, 0x48, 0x49, 0x4C, 0x4D}
|
||||
|
||||
|
||||
def shapeOf(opcode):
|
||||
if opcode in TWO_SELECTORS:
|
||||
return 6
|
||||
if opcode == 0x47: # SETD, a selector and then an address
|
||||
return 5
|
||||
if opcode in (0x48, 0x49): # DPUP and DPDN, a selector and then a byte
|
||||
return 4
|
||||
if opcode in SELECTOR:
|
||||
return 3
|
||||
if opcode in ADDRESS:
|
||||
return 1
|
||||
if opcode in ONE_BYTE or (opcode & 0xF0) in (0xD0, 0xE0):
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def table(path="Source/Assembler/assembly.c"):
|
||||
source = open(path).read()
|
||||
found = re.findall(r'\{0x([0-9A-Fa-f]{2}),\s*"([A-Z0-9]+)"\}', source)
|
||||
return [(int(code, 16), name) for code, name in found]
|
||||
|
||||
|
||||
def asAssembly(entries):
|
||||
lines = []
|
||||
for opcode, name in entries:
|
||||
padded = (name + " ")[:4]
|
||||
lines.append(' 0x%02X 0d%d "%s"' % (opcode, shapeOf(opcode), padded))
|
||||
return lines
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
entries = table()
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--count":
|
||||
print(len(entries))
|
||||
else:
|
||||
for line in asAssembly(entries):
|
||||
print(line)
|
||||
@@ -125,3 +125,11 @@ printf 'the second one' > two.txt
|
||||
"$ROOT/Assembler" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \
|
||||
"$ROOT/Programs/CosmOS/Apps/Edit.asm" -o "$WORK/Edit.sbx" >/dev/null
|
||||
"$TOOL" put "$DISKS/editor.img" "$WORK/Edit.sbx" >/dev/null
|
||||
|
||||
# A disk for the file services, holding nothing but the program that exercises them. Its
|
||||
# own, because that program writes: it tidies up after itself, but a run that stopped part
|
||||
# way would leave a document behind on a fixture every later test reads.
|
||||
"$TOOL" format "$DISKS/services.img" 256 2 >/dev/null
|
||||
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
||||
"$ROOT/Programs/CosmOS/Apps/Files.asm" -o "$WORK/Files.sbx" >/dev/null
|
||||
"$TOOL" put "$DISKS/services.img" "$WORK/Files.sbx" >/dev/null
|
||||
|
||||
@@ -211,6 +211,17 @@ cosmosNoDisk | CosmOS/Source/cosmos.asm | run | cosmosNoD
|
||||
# load can refuse is tried first, and run is asked for twice, so the Stack being reclaimed
|
||||
# rather than merely abandoned is what makes the second one work.
|
||||
cosmosRun | CosmOS/Source/cosmos.asm | run | cosmosRun.in | - | disks/cosmos.img
|
||||
# The monitor, which is part of the shell rather than a program: a mode you go into and stay
|
||||
# in. The targets are chosen to be stable - a loaded program's code and data, and the bank
|
||||
# table - rather than the system's own code, which would churn whenever any library changed.
|
||||
#
|
||||
# Looking at the bank table is worth having on its own. It is the machine describing itself,
|
||||
# and it shows the disk buffer that sbfsMount registered as bank 3 at boot.
|
||||
#
|
||||
# The last part is what the mode is FOR: a program typed in as bytes, run with g, and the
|
||||
# prompt that comes back is the monitor's own. A program giving the machine back lands where
|
||||
# it was started from, so looking at something and running it do not interrupt each other.
|
||||
cosmosMonitor | CosmOS/Source/cosmos.asm | run | cosmosMonitor.in | - | disks/cosmos.img
|
||||
# The original hello.asm, brought over as an application. It is not much of a program,
|
||||
# but it is the one that talks to the hardware directly: it writes to port 0x00 instead
|
||||
# of calling osPrintString, so it is the case where a program reaches past the system and
|
||||
@@ -266,6 +277,12 @@ cosmosSay | CosmOS/Source/cosmos.asm | run | cosmosSay
|
||||
# split a file into lines, edit them, build a file back out of them, and save it over
|
||||
# something that was already there and is now a different size.
|
||||
cosmosEdit | CosmOS/Source/cosmos.asm | run | cosmosEdit.in | - | disks/editor.img
|
||||
# The file services, exercised by a program that includes NOTHING but the service names: no
|
||||
# filesystem library, no console library. It writes a file, reads it back, says how long it
|
||||
# was, renames it and deletes it, in 645 bytes - against the editor's 4941, which does less
|
||||
# with files and carries the filesystem inside it. That difference is the whole case for the
|
||||
# service layer, and this is where it is checked rather than argued.
|
||||
cosmosServices | CosmOS/Source/cosmos.asm | run | cosmosFiles2.in | - | disks/services.img
|
||||
# The programs CosmOS loads, checked on their own so that a failure here reads as "the app
|
||||
# does not assemble" rather than as a broken disk image.
|
||||
app-greet | CosmOS/Apps/greet.asm | assemble | - | -
|
||||
@@ -275,6 +292,7 @@ app-Snake | CosmOS/Apps/Snake.asm | assemble | -
|
||||
app-Keys | CosmOS/Apps/Keys.asm | assemble | - | -
|
||||
app-Say | CosmOS/Apps/Say.asm | assemble | - | -
|
||||
app-Edit | CosmOS/Apps/Edit.asm | assemble | - | -
|
||||
app-Files | CosmOS/Apps/Files.asm | assemble | - | -
|
||||
|
||||
# ---- Programs driven by console input ----
|
||||
inputTest | inputTest.asm | run | inputTest.in | -
|
||||
|
||||
Reference in New Issue
Block a user