CosmOS pre-alpha and launchable application versions of old programs.

This commit is contained in:
Anachronaut
2026-08-17 15:31:49 -04:00
parent eff6902bcf
commit 91c9d49d1b
66 changed files with 5612 additions and 160 deletions
+54 -3
View File
@@ -63,15 +63,65 @@ for m in re.finditer(r'^### (.+?) Operations: (\d+) Instructions?$', pm, re.M):
problems.append("the manual says %s has %d instructions, and it has %d"
% (name, claimed, match[0]))
# ---- How many instructions carry a Data Pointer selector ----
#
# The manual says this as a word rather than a figure, and it is the sort of number that
# goes stale quietly: adding an instruction that takes a selector leaves the sentence
# looking perfectly reasonable and wrong. dataPointerOperands is the list, so it is the
# one to believe.
words = {12: "Twelve", 13: "Thirteen", 14: "Fourteen", 15: "Fifteen", 16: "Sixteen",
17: "Seventeen", 18: "Eighteen", 19: "Nineteen", 20: "Twenty"}
selectors = asmc[asmc.index("int dataPointerOperands"):asmc.index("uint8_t 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]+) instructions work through a Data Pointer\.', pm, re.M)
if not said:
problems.append("the manual no longer says how many instructions take a Data Pointer")
elif said.group(1) != words.get(taking):
problems.append("the manual says %s instructions work through a Data Pointer, and %d do"
% (said.group(1).lower(), taking))
# ---- Every device class in the header has a row in the Devices table ----
#
# The table says which ports a device answers on and what class it reports. Adding a
# device, or widening one from a single port to a block, leaves the table looking perfectly
# reasonable and describing a machine that no longer exists. The classes are the part that
# can be checked against the source without teaching this script how ports are laid out:
# every class the header defines except DEVICE_NONE is something a program can find on the
# bus, so every one of them has to be findable in the manual too.
ioh = read("Source/Emulator/io.h")
classes = {name: int(value, 16)
for name, value in re.findall(r'^#define (DEVICE_[A-Z_]+)\s+(0x[0-9A-Fa-f]{2})$',
ioh, re.M)
if name not in ("DEVICE_NONE",)}
if "## Devices:" not in pm:
problems.append("the Programming Manual has lost its Devices table")
else:
table = pm.split("## Devices:")[1].split("\n## ")[0]
listed = {int(m, 16) for m in re.findall(r'\|\s*(0x[0-9A-Fa-f]{2})\s*\|\s*$', table, re.M)}
for name, value in sorted(classes.items(), key=lambda pair: pair[1]):
if value not in listed:
problems.append("%s (0x%02X) is a device class and has no row in the Devices"
" table" % (name, value))
# ---- 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:
problems.append("%s is a directive and is not in the Assembler Manual" % directive)
# ---- Every routine the manual promises exists ----
for library, names in [("Programs/Libraries/sbfs.asm", re.findall(r'\| (sbfs[A-Za-z]+) \|', pm))]:
#
# The first column of the table in each of these sections names something the library has
# to define. A routine renamed in the source and not in the manual is caught here, which
# is what keeps the tables a description rather than a memory.
for heading, library in [("## Reading The Filesystem:", "Programs/CosmOS/Source/sbfs.asm"),
("## The Console Library:", "Programs/CosmOS/Source/console.asm")]:
if heading not in pm:
problems.append("the Programming Manual has lost its \"%s\" section"
% heading.strip("# :"))
continue
section = pm.split(heading)[1].split("\n## ")[0]
defined = set(re.findall(r'^([a-zA-Z][A-Za-z0-9]*):', read(library), re.M))
for name in names:
for name in re.findall(r'^\| ([a-z][A-Za-z0-9]*) \|', section, re.M):
if name not in defined:
problems.append("the manual lists %s, which %s does not define" % (name, library))
@@ -111,7 +161,8 @@ for heading in ["## An Example SplitBit Assembly Program:",
with tempfile.TemporaryDirectory() as work:
asm = os.path.join(work, "example.asm")
open(asm, "w").write(example)
built = subprocess.run(["./Assembler", "-I", "Programs/Libraries", asm,
built = subprocess.run(["./Assembler", "-I", "Programs/Libraries",
"-I", "Programs/CosmOS/Source", asm,
"-o", os.path.join(work, "example.bin")],
capture_output=True)
if built.returncode != 0:
+10
View File
@@ -0,0 +1,10 @@
console mode ports
at start: 01 ready
key mode: 05 ready keys
what came in:
hi
at the end: 06 ended keys
line mode: 02 ended
Execution halted after 891 cycles.
[exit 0]
+14
View File
@@ -0,0 +1,14 @@
console test
byte hex: 00 0F A5 FF
word hex: 0000 03E8 FFFF
byte dec: 0 7 42 100 255
word dec: 0 9 10 255 1000 10001 65535
spaces: || |
lines read:
[hello] 5
[] 0
[SplitBit] 8
[a line that is f] 16
end of input
Execution halted after 6064 cycles.
[exit 0]
+25
View File
@@ -0,0 +1,25 @@
CosmOS
> dir list what is on the disk
load <file> read a program off the disk
run start what was loaded
dump sixty four bytes of memory, and again for more
dump <program|data|bank> <address>
help this
exit stop
> greeting.txt 17
filler1.txt 8
filler2.txt 8
filler3.txt 8
filler4.txt 8
filler5.txt 8
filler6.txt 8
filler7.txt 8
filler8.txt 8
across.txt 700
empty.txt 0
aName22CharactersLong! 22
12 files
> > I do not know: frobnicate
> halted
Execution halted after 12376 cycles.
[exit 0]
+23
View File
@@ -0,0 +1,23 @@
CosmOS
> dump <program|data|bank> <address>
> there is no such bank
> loaded, starting at 2000
> 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 ................
> 2040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
2050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
2060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
2070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
> 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
> 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 ................
> halted
Execution halted after 23498 cycles.
[exit 0]
+9
View File
@@ -0,0 +1,9 @@
CosmOS
> loaded, starting at 2000
> Hello, World!
finished
> Hello, World!
finished
> halted
Execution halted after 2668 cycles.
[exit 0]
+873
View File
@@ -0,0 +1,873 @@
CosmOS
> loaded, starting at 2000
>  #
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
#
##

#
##

##
##
the board has settled
finished
>
halted
Execution halted after 11671206 cycles.
[exit 0]
+25
View File
@@ -0,0 +1,25 @@
CosmOS
> loaded, starting at 2000
>  #
#
###
stopped
finished
> >
halted
Execution halted after 26219 cycles.
[exit 0]
+7
View File
@@ -0,0 +1,7 @@
CosmOS
no filesystem on the disk
> no filesystem on the disk
>
halted
Execution halted after 636 cycles.
[exit 0]
+20
View File
@@ -0,0 +1,20 @@
CosmOS
> nothing is loaded
> greet.sbx 210
hello.sbx 52
Life.sbx 1411
notes.txt 21
4 files
> load what?
> no such file
> not a program
> loaded, starting at 2000
> a program, loaded off a disk, running on the system that loaded it
what should I call you? hello, Anachronaut. that is all I do.
finished
> a program, loaded off a disk, running on the system that loaded it
what should I call you? hello, Claude. that is all I do.
finished
> halted
Execution halted after 11647 cycles.
[exit 0]
+15
View File
@@ -0,0 +1,15 @@
greeting.txt 17
filler1.txt 8
filler2.txt 8
filler3.txt 8
filler4.txt 8
filler5.txt 8
filler6.txt 8
filler7.txt 8
filler8.txt 8
across.txt 700
empty.txt 0
aName22CharactersLong! 22
files: 12
Execution halted after 9498 cycles.
[exit 0]
+9
View File
@@ -0,0 +1,9 @@
stack reclaim test
at start: FFFF
spent: FFF7
reclaimed: FFFF
still good: 33
borrowed: 7 and 9
back home: FFFF
Execution halted after 1054 cycles.
[exit 0]
+8
View File
@@ -0,0 +1,8 @@
split: [dump] [program 2000]
split: [dir] []
same: yes no no no
hex: 2000 00FF 00FF BEEF FFFF 0000
0 is a fine way to begin a string
no number here
Execution halted after 2730 cycles.
[exit 0]
+1
View File
@@ -0,0 +1 @@
hi
+4
View File
@@ -0,0 +1,4 @@
hello
SplitBit
a line that is far longer than sixteen characters
+5
View File
@@ -0,0 +1,5 @@
help
dir
frobnicate
exit
+8
View File
@@ -0,0 +1,8 @@
dump nonsense
dump 9 0000
load greet.sbx
dump program 2000
dump
dump data 1000
dump 2 0000
exit
+4
View File
@@ -0,0 +1,4 @@
load hello.sbx
run
run
exit
+2
View File
@@ -0,0 +1,2 @@
load Life.sbx
run
+3
View File
@@ -0,0 +1,3 @@
load Life.sbx
run
q
+1
View File
@@ -0,0 +1 @@
dir
+11
View File
@@ -0,0 +1,11 @@
run
dir
load
load nosuch.sbx
load notes.txt
load greet.sbx
run
Anachronaut
run
Claude
exit
+30 -2
View File
@@ -45,10 +45,38 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/
# A disk with a loadable program on it. The program is assembled here rather than kept as
# bytes, so that what gets loaded is always built from the source beside it.
"$TOOL" format "$DISKS/load.img" 64 2 >/dev/null
"$ROOT/Assembler" "$ROOT/Programs/loadable/hello.asm" -o "$WORK/hello.bin" >/dev/null
python3 "$ROOT/Source/DiskTool/wrap.py" "$WORK/hello.bin" "$WORK/hello.sbx" 0x2000 0x1000 0x2000 >/dev/null
# The assembler writes a loadable program itself, because the source says where it goes.
"$ROOT/Assembler" "$ROOT/Programs/loadable/hello.asm" -o "$WORK/hello.sbx" >/dev/null
"$TOOL" put "$DISKS/load.img" "$WORK/hello.sbx" >/dev/null
# A disk for CosmOS. greet.sbx asks the system for everything it does rather than talking
# to the hardware itself, so loading and running it exercises the whole path: the loader,
# the vector table, the service handlers, and giving the machine back at the end.
#
# hello.sbx is the opposite case, and that is why it is here: it is the original
# hello.asm, written before any of this existed, and it still writes straight to port
# 0x00 rather than calling osPrintString. A program is allowed to reach past the system
# to the hardware, so something has to check that one still gives the machine back.
#
# notes.txt is there so that loading something that is not a program can be tried too.
"$TOOL" format "$DISKS/cosmos.img" 32 1 >/dev/null
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/greet.asm" -o "$WORK/greet.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/greet.sbx" >/dev/null
# Assembled to a different working name so it cannot tread on load.img's hello.sbx above,
# then put under the name the shell asks for.
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/hello.asm" -o "$WORK/appHello.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/appHello.sbx" hello.sbx >/dev/null
# Life.sbx is the one that had to be taught to stop. It runs to a still life and returns
# on its own, so the test needs no cycle limit: whether it ends is the thing being checked
# and a limit would hide the answer by supplying one.
"$ROOT/Assembler" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Life.asm" -o "$WORK/Life.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Life.sbx" >/dev/null
printf 'this is not a program' > notes.txt
"$TOOL" put "$DISKS/cosmos.img" notes.txt >/dev/null
# A disk of its own for the writing test, with one file already on it so that what it
# writes has to be placed somewhere that does not tread on what is there.
"$TOOL" format "$DISKS/write.img" 32 1 >/dev/null
+86 -1
View File
@@ -69,6 +69,10 @@ blitTest | testPrograms/blitTest.asm | run | -
# Reading a filesystem that the host tool wrote. The two are separate implementations of
# one written format, so this is where any drift between them would show.
sbfsReadTest | testPrograms/sbfsReadTest.asm | run | - | - | disks/sbfs.img
# Walking the directory rather than searching it, which is what listing a disk needs.
# Twelve files is more than the eight an entry block holds, so the walk has to cross into
# the second directory block to see them all.
sbfsWalkTest | testPrograms/sbfsWalkTest.asm | run | - | - | disks/sbfs.img
# Writing a filesystem, then reading back what was written. The disk starts with a file
# on it, so allocation has to find room rather than start at the beginning.
@@ -110,8 +114,12 @@ registryTest | testPrograms/registryTest.asm | run | -
# ---- Moving the cursor along ----
paddingTest | testPrograms/paddingTest.asm | run | - | -
# ---- Finding the Stack ----
# ---- Finding the Stack, and moving it ----
stackPointerTest | testPrograms/stackPointerTest.asm | run | - | -
# MVDS, the dangerous one: a system taking its Stack back from a program that stopped
# without unwinding. The sequence is the one the Programming Manual prints under "The
# Stack Pointer, Set By Hand", so the manual's example cannot quietly stop working.
stackReclaimTest | testPrograms/stackReclaimTest.asm | run | - | -
# ---- The Interrupt Flag ----
# Nothing reads the flag yet. This checks that setting and clearing it leaves
@@ -144,6 +152,73 @@ swiFaultTest | testPrograms/swiFaultTest.asm | run | -
# where, and exits non zero, rather than stepping over it and carrying on.
faultTest | testPrograms/faultTest.asm | run | - | -
# ---- The modern console library ----
# Every routine in console.asm, called with the cases that are easy to get wrong: a zero,
# the largest thing that fits, and a leading zero that should not print. The reading half
# is given a line longer than its buffer, so truncation and the swallowing of the rest
# are shown rather than assumed.
consoleTest | testPrograms/consoleTest.asm | run | consoleTest.in | -
# The console's status and control ports. Input here is a file rather than a terminal, so
# key mode has no terminal to change and only the mode bit moves: that is deliberate, since
# a program has to behave the same either way and a test needing a terminal could not run.
# 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 | -
# 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.
textTest | testPrograms/textTest.asm | run | - | -
# ---- CosmOS ----
# The system and its shell, driven by a script of commands. This is the first thing that
# uses the machine as a machine rather than exercising one part of it: it boots, mounts a
# disk written by the host tool, reads lines, and walks a directory to answer 'dir'.
cosmos | CosmOS/Source/cosmos.asm | run | cosmos.in | - | disks/sbfs.img
# The same with nothing attached. A shell that only works with a disk in the drive is not
# finished, so the empty machine is a case in its own right rather than an accident.
cosmosNoDisk | CosmOS/Source/cosmos.asm | run | cosmosNoDisk.in | -
# Loading a program and running it, which is the whole machine at once: the filesystem
# finds it, the controller writes it into Program Memory, the vector table carries its
# requests back to the system, and MVDS takes the Stack back when it stops. Every way
# 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. It reads Program Memory, which the instruction set cannot do at all, so it
# works only through the controller. 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.
#
# Dumping 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.
cosmosDump | CosmOS/Source/cosmos.asm | run | cosmosDump.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
# the system has to get control back anyway. run is asked for twice for the same reason
# it is in cosmosRun, and here it also says the Stack comes back from a program that never
# entered a service handler at all.
cosmosHello | CosmOS/Source/cosmos.asm | run | cosmosHello.in | - | disks/cosmos.img
# Life, which is the one program that had no end state to reach. It stops when the board
# settles, and the glider does settle: it crosses the field, hits the dead border, and
# collapses into a block at generation 54. NO CYCLE LIMIT ON PURPOSE. Whether it stops on
# its own is the whole point of the port, and a limit would answer that question for it.
#
# NOTHING IS TYPED AFTER run, and that is load bearing rather than tidy. Life polls the
# console between generations, and a byte sitting in the pipe is a byte waiting on the
# console as far as the machine is concerned - exactly as typing ahead at a terminal
# would be. An "exit" on the next line stops it at generation 1 and gets eaten. The shell
# ends the run by reaching the end of input instead, the way cosmosNoDisk does.
cosmosLife | CosmOS/Source/cosmos.asm | run | cosmosLife.in | - | disks/cosmos.img
# The other half of that: a key IS waiting, so it stops at once instead of at generation
# 54. The two together are what say the poll is reading the console rather than always
# answering the same way.
cosmosLifeKey | CosmOS/Source/cosmos.asm | run | cosmosLifeKey.in | - | disks/cosmos.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 | - | -
app-hello | CosmOS/Apps/hello.asm | assemble | - | -
app-Life | CosmOS/Apps/Life.asm | assemble | - | -
# ---- Programs driven by console input ----
inputTest | inputTest.asm | run | inputTest.in | -
inputTestOld | testPrograms/inputTest.asm | run | inputTest.in | -
@@ -161,6 +236,12 @@ replCalculator | replCalculator.asm | run | replCalcu
16x16LifeModern | gameOfLife/16x16LifeModern.asm | run | - | 3000000
# ---- Libraries: no entry point, so only check that they assemble ----
# The CosmOS libraries assemble on their own, unlike print.asm below, which cannot: it
# begins with a branch to a label only the including program defines. That difference is
# the point of the rewrite, so both halves of it are recorded here.
lib-console | CosmOS/Source/console.asm | assemble | - | -
lib-text | CosmOS/Source/text.asm | assemble | - | -
lib-sbfs | CosmOS/Source/sbfs.asm | assemble | - | -
lib-int8 | Libraries/int8.asm | assemble | - | -
lib-int16 | Libraries/int16.asm | assemble | - | -
lib-int32 | Libraries/int32.asm | assemble | - | -
@@ -177,6 +258,10 @@ diagDuplicateVector | testPrograms/diagnostics/duplicateVector.asm | xfail | -
diagBareSWI | testPrograms/diagnostics/bareSWI.asm | xfail | - | -
diagAlignOutside | testPrograms/diagnostics/alignOutside.asm | xfail | - | -
diagBareAlign | testPrograms/diagnostics/bareAlign.asm | xfail | - | -
# One segment based and the other forgotten, which is how the ported Fib-8 put its data
# on top of the console's variables. It assembled and it ran, so nothing but the assembler
# was ever going to catch it.
diagUnbasedSegment | testPrograms/diagnostics/unbasedSegment.asm | xfail | - | -
# ---- Known breakages, recorded rather than ignored ----
# print.asm branches to 'start', which only the including program defines.
+5 -3
View File
@@ -111,11 +111,13 @@ check() {
assemble() {
# assemble <name> <source>; echoes the built binary path on success.
# Everything builds from Programs/ with Libraries/ on the include path, and the
# binary goes to Tests/build, so the source tree is never written to.
# Everything builds from Programs/ with Libraries/ and CosmOS/ on the include path,
# and the binary goes to Tests/build, so the source tree is never written to.
# CosmOS is there because it owns the filesystem library and the service names, which
# test programs outside it include.
local name="$1" src="$2"
local bin="$BUILD/$name.bin"
if ( cd "$PROGRAMS" && "$ASSEMBLER" -I Libraries -o "$bin" "$src" ) >"$BUILD/.assemble.log" 2>&1; then
if ( cd "$PROGRAMS" && "$ASSEMBLER" -I Libraries -I CosmOS/Source -o "$bin" "$src" ) >"$BUILD/.assemble.log" 2>&1; then
echo "$bin"
return 0
fi