Compare commits

..
10 Commits
Author SHA1 Message Date
AnachronautandClaude Opus 5 b36d438132 Add the CosmOS README, brought up to date with today's changes
Written by Anachronaut and not previously committed. Three things in it had
gone stale between writing and now, and are corrected here rather than
committed knowingly wrong:

  - the shell command table still described dump, which the monitor
    supersedes, so it now describes monitor mode and its commands
  - the service table listed four services, and there are nine
  - the application list did not mention Files

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:20:20 -04:00
AnachronautandClaude Opus 5 3e87a67809 Edit: use the system's services instead of carrying the filesystem
The editor included sbfs.asm and console.asm, which was 3235 of its 4941
bytes. Both come out: reading and writing a file, printing and reading a
line are services now, and sbfsMount goes entirely because the system has
already mounted the disk. Only text.asm remains, at 357 bytes, because
textSame and textNumber own nothing and touch no device - sharing those is
a linker's job rather than an operating system's.

4941 bytes to 1983, a little better than predicted because dropping the
mount and the blocks-and-tail arithmetic went with the library rather than
being translated.

ConsoleEndOfInput went with console.asm, so the end of input is now read
from the console status port. That is better than what it replaced: the
port is there whether or not the system is.

Nothing it does changed, and that is checked rather than asserted. The
recorded output of the cosmosEdit test does not move by a single byte
across this commit - a test written days before the rewrite, checking a
property rather than an output, and therefore able to verify a change it
knew nothing about.

One thing is lost and worth naming: there is no way to ask whether a disk
is present, so the editor's "there is no disk" message is gone. On a
diskless machine it now opens an empty document and fails at w instead. The
shell already refuses to load without a disk, so the editor cannot normally
be reached that way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:19:51 -04:00
AnachronautandClaude Opus 5 0b6d2be43f 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>
2026-08-19 18:19:15 -04:00
AnachronautandClaude Opus 5 301716e869 sbfs: give sbfsMatchEntry a counter of its own
sbfsFind counts a block's eight entries in SbfsCount and calls
sbfsMatchEntry for each one, and sbfsMatchEntry used the same variable for
its own twenty two byte comparison. A name differing at its first byte
returned without decrementing, leaving 22 in the entry counter, so the
search then walked twenty two entries through a buffer holding eight: off
the end of it, into whatever data happened to follow, matching against
rubbish and reporting a file that is not there as found.

It had always been there. What decided whether it looked like it worked
was the bytes after SbfsBuffer, so it stayed hidden until unrelated data
was added and changed them. It showed up as "dir" followed by any failed
"load" reporting that the file could not be read rather than that it does
not exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:18:12 -04:00
AnachronautandClaude Opus 5 be402cc9be Assembler: a string that spells an instruction is no longer assembled as one
A token is classified after its quotes have been stripped, so a string
literal reading "ADD" looked exactly like the ADD instruction and was
assembled as one. It failed with "attempting to assemble outside the
Program Segment", a message about a mistake nobody had made.

The literal and label checks were already guarded against strings and the
instruction check was not. Mnemonics match without regard to case, so
"or" and "and" were caught by this too, and those are ordinary enough
words to want in a message.

Third of its family, after a string beginning with '0' being read as a
malformed number and a string in the Program Segment being silently
discarded. All three have the same root.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:17:39 -04:00
AnachronautandClaude Opus 5 ca34e077ad Add sbex.h, which was never committed
The loadable program format's header has been in the working tree since
the loader was written and has never been in the repository: the gitignore
rules matched Source/Assembler as a directory, so it was silently
untracked. Anything cloning this repository could not build it, because
secondPass.c includes this file.

This is the failure the previous commit describes, having already happened
once without being noticed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:17:28 -04:00
AnachronautandClaude Opus 5 51601545ae Anchor the gitignore patterns to the repository root
Without a leading slash a pattern matches at any depth, so the rules for
the three built binaries also matched directories: "Assembler" ignored
Source/Assembler. The files already in it stayed tracked, which is why
nothing had gone wrong yet, but a new source file added there would have
been silently untracked and left out of a commit.

Found by git refusing to stage a change to Source/Assembler/firstPass.c.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:16:06 -04:00
Anachronaut c4b59acc68 Infrastructure for system services through software interrupts. 2026-08-19 15:16:30 -04:00
Anachronaut e3100b4718 Fixed assembler bug that caused crash on IR array resize. Added line editor app. 2026-08-17 23:26:21 -04:00
Anachronaut 1d1a14318c Programs can now list and share vectors. 2026-08-17 21:39:27 -04:00
106 changed files with 4725 additions and 171 deletions
+15 -9
View File
@@ -1,9 +1,15 @@
Tests/build/
Programs/build/
Object/
Assembler
SplitBit
SplitDisk
CLAUDE.md
claudeResume.sh
codexResume.sh
# Anchored to the repository root, all of them. Without the leading slash a pattern
# matches at any depth, so "Assembler" also ignored the Source/Assembler DIRECTORY - and a
# new source file added there would have been silently untracked.
/Tests/build/
/Programs/build/
/Object/
/Assembler
/SplitBit
/SplitDisk
/CLAUDE.md
/claudeResume.sh
/codexResume.sh
# Python leaves these beside the test scripts.
__pycache__/
+846
View File
@@ -0,0 +1,846 @@
; Edit, a line editor for CosmOS.
;
; The first program on this machine that makes a file a person typed. Everything on every
; disk before this one was put there by the host tool.
;
; It is line oriented, in the manner of ed, and that is a deliberate choice rather than a
; limitation of the machine - Snake already draws a whole screen and steers with single
; keys. A full screen editor wants scrolling, a redraw model and cursor arithmetic, none of
; which teaches anything about files, and files are what this exists to exercise. So it
; stays in line mode and reads whole lines, which is what the console does without being
; asked for anything.
;
; l list the whole thing, numbered
; a add lines at the end, until a line that is just a dot
; i <n> put lines in before line n, the same way
; c <n> change line n
; d <n> delete line n
; w write it back
; q stop without writing
;
; ---- How the text is kept ----
;
; A LINKED LIST OF LINES, not one buffer with newlines in it. Each line is a node holding
; where the next one is, how long it is, and its bytes:
;
; 0 2 where the next line is, or zero
; 2 1 how many bytes this line has
; 3 the bytes
;
; Inserting is then two pointers changed and nothing moved, and so is deleting. With one
; flat buffer both of them would mean shifting everything after the edit, which on a
; machine with no memcpy is a loop over every byte of the rest of the document, run for
; every keystroke's worth of editing.
;
; The price is that DELETED LINES ARE NOT REUSED. A new line always goes at the end of the
; arena, and an unlinked one just sits there. A session that edits heavily uses more room
; than the document needs, and writing the file out and reading it back is what tidies it
; up. That is an honest trade for a program this size, and it is written down here rather
; than left as a surprise.
;
; Two regions are used by arrangement rather than reserved, because reserving them would
; put tens of kilobytes of zeroes into the file for no reason:
;
; 0x4000 the file, on its way in or out
; 0x8000 the arena the lines live in
;
; Nothing is running but this, so both are ours. It is the same arrangement CosmOS makes
; with 0x8000 while it is loading something, for the same reason.
#Include services.asm
#Program
#Base 0x2000
start:
SETD.0 FileName
INIB 0d23
SWI osArgument
SETD.0 FileName
LDA.0
BRA noName
; Everything is set here rather than trusted to be zero, since running a program a second
; time does not load it again.
RSTA
SETD.0 TextHead
STA.0
INCD.0
STA.0
SETD.0 ArenaFree
INIA 0x80
STA.0
INCD.0
RSTA
STA.0
CALL loadFile
SETD.0 FileName
SWI osPrintString
SETD.0 CommaText
SWI osPrintString
CALL countLines
MVQA
CALL printByte
; One line is not one lines. The same care the shell's file listing takes, for the same
; reason: it costs four instructions and reads as carelessness without them.
DECA
BRA oneLine
SETD.0 LinesText
BRI sayLines
oneLine:
SETD.0 LineText
sayLines:
SWI osPrintString
CALL newLine
commandLoop:
SETD.0 PromptText
SWI osPrintString
SETD.0 Command
INIB 0d40
SWI osReadLine
; Running out of typing ends it, the same way it ends the shell.
INA 0x01
INIB 0x02 ; ENDED
AND
BNQ quit
SETD.0 Command
LDA.0
BRA commandLoop ; An empty line asks for nothing.
; Whatever number follows the letter, if there is one. The spaces between the two are
; stepped over first: a number is what somebody typed after "d ", not after "d".
SETD.0 Command
INCD.0
commandSpaces:
LDA.0
INIB 0x20
XOR
BNQ commandArgument
INCD.0
BRI commandSpaces
commandArgument:
CALL textNumber
MVQA
SETD.0 Wanted
STA.0
SETD.0 Command
LDA.0
INIB 0d108 ; l
XOR
BRQ doList
INIB 0d97 ; a
XOR
BRQ doAppend
INIB 0d105 ; i
XOR
BRQ doInsert
INIB 0d99 ; c
XOR
BRQ doChange
INIB 0d100 ; d
XOR
BRQ doDelete
INIB 0d119 ; w
XOR
BRQ doWrite
INIB 0d113 ; q
XOR
BRQ quit
SETD.0 WhatText
SWI osPrintString
CALL newLine
BRI commandLoop
quit:
SWI osExit
noName:
SETD.0 NoNameText
SWI osPrintString
CALL newLine
SWI osExit
; ---- The commands ----
doList:
CALL listLines
BRI commandLoop
doAppend:
CALL countLines
MVQA
INCA
SETD.0 Wanted
STA.0 ; Adding at the end is inserting before the line after it.
BRI insertLoop
doInsert:
SETD.0 Wanted
LDA.0
BRA insertNeedsLine
insertLoop:
SETD.0 EnteringText
SWI osPrintString
SETD.0 Entry
INIB 0d80
SWI osReadLine
INA 0x01
INIB 0x02 ; ENDED
AND
BNQ commandLoop
; A line that is just a dot ends it, which is the oldest convention there is for this.
SETD.0 Entry
SETD.1 DotText
CALL textSame
BRQ commandLoop
SETD.0 Entry
CALL makeNode
SETD.0 Wanted
LDA.0
CALL linkBefore
SETD.0 Wanted
LDA.0
INCA
STA.0 ; The next one goes after the one just put in.
BRI insertLoop
insertNeedsLine:
SETD.0 NeedsLineText
SWI osPrintString
CALL newLine
BRI commandLoop
doChange:
SETD.0 Wanted
LDA.0
BRA insertNeedsLine
CALL findLine
BNQ noSuchLine
SETD.0 EnteringText
SWI osPrintString
SETD.0 Entry
INIB 0d80
SWI osReadLine
INA 0x01
INIB 0x02 ; ENDED
AND
BNQ commandLoop
SETD.0 Entry
CALL makeNode
SETD.0 Wanted
LDA.0
CALL linkBefore ; The new one goes in front of the old one,
SETD.0 Wanted
LDA.0
INCA
CALL unlinkLine ; and the old one, now one further along, comes out.
BRI commandLoop
doDelete:
SETD.0 Wanted
LDA.0
BRA insertNeedsLine
CALL unlinkLine
BNQ noSuchLine
BRI commandLoop
noSuchLine:
SETD.0 NoLineText
SWI osPrintString
CALL newLine
BRI commandLoop
doWrite:
CALL writeFile
BNQ writeFailed
SETD.0 WrittenText
SWI osPrintString
SETD.0 WroteSize
CALL printWord
SETD.0 BytesText
SWI osPrintString
CALL newLine
BRI commandLoop
writeFailed:
SETD.0 NoWriteText
SWI osPrintString
CALL newLine
BRI commandLoop
; ---- The list of lines ----
; DP0 is a string. Puts a node holding it at the end of the arena, and leaves DP3 on it.
makeNode:
SETD.1 ArenaFree
LDD.3.1
PSHD.3
POPD.1
RSTA
STA.1 ; Nothing follows it yet.
INCD.1
STA.1
INCD.1
PSHD.1 ; Where the length goes, once it is known.
INCD.1
RSTB
makeNodeLoop:
LDA.0
BRA makeNodeEnd
STA.1
INCD.0
INCD.1
INCB
BRI makeNodeLoop
makeNodeEnd:
POPD.0
PSHB
POPA
STA.0 ; How long it turned out to be.
INIB 0d3
CCF
ADD
MVQA
SETD.0 ArenaFree
CALL addByteToWord
RET
; A is a line number. Leaves DP3 on that line and PrevLine on the one before it, which is
; zero when it is the first. Q is zero if there is such a line.
findLine:
SETD.1 Wanted2
STA.1
INIA 0d1
SETD.1 Counted
STA.1
RSTA
SETD.1 PrevLine
STA.1
INCD.1
STA.1
SETD.1 TextHead
LDD.3.1
findLineStep:
PSHD.3
POPA
POPB
OR
BRQ findLineMissing
SETD.1 Counted
LDA.1
SETD.1 Wanted2
LDB.1
XOR
BRQ findLineFound
PSHD.3
SETD.1 PrevLine
POPD.0
STD.0.1
PSHD.3
POPD.0
LDD.3.0 ; On to whatever follows it.
SETD.1 Counted
LDA.1
INCA
STA.1
BRI findLineStep
findLineFound:
RSTA
RSTB
CCF
ADD
RET
findLineMissing:
RSTA
INIB 0d1
CCF
ADD
RET
; DP3 is a new node and A is the line number it should become. Puts it there.
linkBefore:
PSHD.3
SETD.1 NewLine
POPD.0
STD.0.1 ; The new node, while the old ones are looked through.
CALL findLine ; Which may miss, and missing means putting it at the end.
; What the new node should point at is whatever was there, or nothing.
SETD.1 NewLine
LDD.0.1
BNQ linkBeforeAtEnd
PSHD.3
POPD.1
STD.1.0 ; new.next = the line that was there
BRI linkBeforeAttach
linkBeforeAtEnd:
; Nothing was there, so the new one ends the list and goes after whatever was last.
RSTA
STA.0
INCD.0
STA.0
SETD.1 NewLine
LDD.0.1
linkBeforeAttach:
; And whatever came before now points at the new one. Before the first line, that is
; the head of the list rather than a node.
SETD.1 PrevLine
LDD.2.1
PSHD.2
POPA
POPB
OR
BRQ linkBeforeHead
SETD.1 NewLine
LDD.0.1
SETD.1 PrevLine
LDD.1.1
STD.0.1
RET
linkBeforeHead:
SETD.1 NewLine
LDD.0.1
SETD.1 TextHead
STD.0.1
RET
; A is a line number. Takes it out of the list. Q is zero if there was such a line.
unlinkLine:
CALL findLine
BNQ unlinkMissing
; What follows the one being taken out.
PSHD.3
POPD.0
LDD.0.0
SETD.1 PrevLine
LDD.2.1
PSHD.2
POPA
POPB
OR
BRQ unlinkHead
SETD.1 PrevLine
LDD.1.1
STD.0.1
BRI unlinkDone
unlinkHead:
SETD.1 TextHead
STD.0.1
unlinkDone:
RSTA
RSTB
CCF
ADD
RET
unlinkMissing:
RSTA
INIB 0d1
CCF
ADD
RET
; Q is how many lines there are.
countLines:
RSTA
SETD.1 Counted
STA.1
SETD.1 TextHead
LDD.3.1
countStep:
PSHD.3
POPA
POPB
OR
BRQ countDone
SETD.1 Counted
LDA.1
INCA
STA.1
PSHD.3
POPD.0
LDD.3.0
BRI countStep
countDone:
SETD.1 Counted
LDA.1
RSTB
CCF
ADD
RET
listLines:
INIA 0d1
SETD.1 Counted
STA.1
SETD.1 TextHead
LDD.3.1
listStep:
PSHD.3
POPA
POPB
OR
BRQ listDone
SETD.0 Counted
LDA.0
CALL printByte
SETD.0 ColonText
SWI osPrintString
PSHD.3
POPD.1
DPUP.1 0d02
LDA.1
SETD.1 Leftover
STA.1
PSHD.3
POPD.0
DPUP.0 0d03
SETD.1 Leftover
LDA.1
BRA listEmpty
listChars:
LDA.0
OUTA 0x00
INCD.0
SETD.1 Leftover
LDA.1
DECA
STA.1
BNA listChars
listEmpty:
CALL newLine
SETD.1 Counted
LDA.1
INCA
STA.1
PSHD.3
POPD.0
LDD.3.0
BRI listStep
listDone:
RET
; ---- The file ----
; Reads the file into lines, if there is one. A name that is not on the disk is a new
; document rather than a mistake, which is what makes this the way to start one.
loadFile:
SETD.0 FileName
SETD.1 0x40 0x00
SWI osFileRead
BNQ loadNothing
; How many bytes came back. The service says so in DP3, which is one of the two things a
; service is allowed to answer in, and a file that fits in memory has a length that fits
; in a pointer. A name that is not on the disk fails here, and that is a new document
; rather than a mistake.
PSHD.3
POPA ; The low byte is on top, the way a pointer is pushed.
POPB
SETD.1 ReadLeft
STB.1
INCD.1
STA.1
SETD.0 0x40 0x00
SETD.1 Entry
RSTA
SETD.2 EntryLength
STA.2
splitStep:
; Anything left?
SETD.2 ReadLeft
LDA.2
INCD.2
LDB.2
OR
BRQ splitLast
; How long the line is so far is kept in memory rather than in B, because comparing
; against a newline needs B and would quietly count the comparison instead of the line.
LDA.0
INIB 0d10
XOR
BRQ splitLine
STA.1 ; A is still the character; an ALU operation does not touch it.
INCD.1
SETD.2 EntryLength
LDA.2
INCA
STA.2
BRI splitOn
splitLine:
RSTA
STA.1 ; The line ends here, so it becomes a string.
PSHD.0 ; How far through the file we are.
SETD.0 Entry
CALL makeNode
CALL appendNode
POPD.0
SETD.1 Entry
RSTA
SETD.2 EntryLength
STA.2
splitOn:
INCD.0
SETD.2 ReadLeft
CALL takeOneOff
BRI splitStep
splitLast:
; A file that does not end in a newline still has a last line in it.
SETD.2 EntryLength
LDA.2
BRA loadNothing
RSTA
STA.1
SETD.0 Entry
CALL makeNode
CALL appendNode
loadNothing:
RET
; DP3 is a node. Puts it on the end of the list.
appendNode:
; The node has to be put somewhere safe first: counting the lines walks the list in DP3,
; which is where the node being added is being held.
PSHD.3
CALL countLines
MVQA
INCA
POPD.3
CALL linkBefore
RET
; DP2 is a two byte count. Takes one off it.
takeOneOff:
DPUP.2 0d01
LDA.2
DECA
STA.2
BNC takeOneDone ; No borrow, so the high half is untouched.
DPDN.2 0d01
LDA.2
DECA
STA.2
RET
takeOneDone:
RET
; Builds the whole document at 0x4000 and saves it. Q is zero if it worked.
writeFile:
SETD.1 0x40 0x00
SETD.2 TextHead
LDD.3.2
writeStep:
PSHD.3
POPA
POPB
OR
BRQ writeOut
PSHD.3
POPD.0
DPUP.0 0d02
LDA.0
SETD.2 Leftover
STA.2
INCD.0
LDA.2
BRA writeBreak
writeChars:
LDA.0
STA.1
INCD.0
INCD.1
SETD.2 Leftover
LDA.2
DECA
STA.2
BNA writeChars
writeBreak:
INIA 0d10
STA.1
INCD.1
PSHD.3
POPD.0
LDD.3.0
BRI writeStep
writeOut:
; Where the building stopped says how big it is, with no arithmetic worth the name: the
; buffer starts on a page boundary at 0x4000, so the high byte less 0x40 is the number of
; whole blocks and the low byte is the tail.
SETD.2 WroteSize
STD.1.2
; Turn where it stopped into how big it is, which is one subtraction: the high byte less
; 0x40 and the low byte as it stands.
SETD.0 WroteSize
LDA.0
INIB 0x40
CCF
SUB
MVQA
SETD.0 WroteSize
STA.0 ; WroteSize is now a count of bytes, which is what gets printed.
; And into the two registers the service takes a size in.
SETD.0 WroteSize
LDA.0
INCD.0
LDB.0
SETD.0 FileName
SETD.1 0x40 0x00
SWI osFileSave
RET
; ---- All that is left of a console library ----
;
; Printing a string and reading a line are services now. These three are only here because
; what a service takes is not quite what the call sites have: a number arrives in two
; registers rather than one or in memory, and a line feed is a string like any other.
newLine:
SETD.0 Break
SWI osPrintString
RET
; A is a byte.
printByte:
PSHA
POPB
RSTA
SWI osPrintNumber
RET
; DP0 is a two byte number, most significant first.
printWord:
LDA.0
INCD.0
LDB.0
SWI osPrintNumber
RET
; DP0 is a two byte number, A is a byte. Adds the one to the other.
addByteToWord:
DPUP.0 0d01
LDB.0
CCF
ADD
STQ.0
DPDN.0 0d01
LDA.0
RSTB
ADD
STQ.0
RET
#Data
#Base 0x1000
Break:
0x0A 0x00
PromptText:
"> "
EnteringText:
": "
ColonText:
": "
CommaText:
", "
LinesText:
" lines"
LineText:
" line"
WrittenText:
"written, "
BytesText:
" bytes"
DotText:
"."
WhatText:
"l list, a add, i insert, c change, d delete, w write, q quit"
NoNameText:
"edit what? try: run edit <file>"
NoLineText:
"there is no such line"
NeedsLineText:
"which line?"
NoWriteText:
"it would not write"
FileName:
#Reserve 0d24
Command:
#Reserve 0d41
Entry:
#Reserve 0d81
TextHead:
0x00 0x00
ArenaFree:
0x00 0x00
PrevLine:
0x00 0x00
NewLine:
0x00 0x00
Wanted:
0x00
Wanted2:
0x00
Counted:
0x00
Leftover:
0x00
EntryLength:
0x00
ReadLeft:
0x00 0x00
WroteSize:
0x00 0x00
#Include text.asm
+176
View File
@@ -0,0 +1,176 @@
; A program that keeps a file without knowing how a filesystem works.
;
; INCLUDES NOTHING BUT THE SERVICE NAMES. No sbfs.asm, no console.asm - the system has both
; of those running already, and this asks it rather than carrying a second copy. That is the
; whole point of the program: if it works, a tool that edits documents does not need two and
; a half kilobytes of filesystem bound into it.
;
; It writes a file, reads it back, says how big it was, renames it, and takes it away again,
; which is every file service there is.
;
; ---- What you will see before the handlers are written ----
;
; The numbers are pinned but nothing answers to them yet, so the first call dispatches
; through an empty vector and the machine stops:
;
; Fault: Software vector 21, dispatched from Program Address 0x200B, has no handler
; installed.
;
; That is the fault working properly rather than the program being broken. A vector with
; nothing in it is not a jump to address zero; it is a stop, with the vector named and the
; place it was called from named, which is as much as the machine can know.
#Include services.asm
#Program
#Base 0x2000
start:
; ---- Write it ----
;
; A and B together are how many bytes there are, most significant first, which is the
; same sixteen bits a length always is on this machine.
SETD.0 Name
SETD.1 Body
RSTA
INIB 0d22 ; The text and its newline. NOT the zero the assembler put
SWI osFileSave ; after it: a text file ends where the text ends.
BNQ noSave
SETD.0 SavedText
SWI osPrintString
; ---- Read it back ----
;
; DP3 comes back holding how many bytes there were, because that is one of the two things
; a service is allowed to answer in and a file that fits in memory has a length that fits
; in a pointer.
SETD.0 Name
SETD.1 Landing
SWI osFileRead
BNQ noRead
SETD.0 ReadText
SWI osPrintString
PSHD.3
POPB ; The low byte is on top, the way a pointer is pushed.
POPA
SWI osPrintNumber
SETD.0 BytesText
SWI osPrintString
; And now the length is needed for something rather than just reported. What came back is
; a file, not a string: nothing on the disk ends in a zero byte, because the entry says
; where it stops instead. So a zero goes on the end before it can be printed as one.
SETD.1 Landing
PSHD.3
POPB
POPA
walkToEnd:
BRB atEnd
INCD.1
DECB
BRI walkToEnd
atEnd:
RSTA
STA.1
SETD.0 Landing
SWI osPrintString
; ---- Call it something else ----
SETD.0 Name
SETD.1 OtherName
SWI osFileRename
BNQ noRename
SETD.0 RenamedText
SWI osPrintString
; ---- And take it away ----
SETD.0 OtherName
SWI osFileDelete
BNQ noDelete
SETD.0 DeletedText
SWI osPrintString
; Reading it now should fail, and a service saying no is not the same as one that is not
; there: this comes back with an answer rather than stopping the machine.
SETD.0 OtherName
SETD.1 Landing
SWI osFileRead
BRQ stillThere
SETD.0 GoneText
SWI osPrintString
SWI osExit
stillThere:
SETD.0 StillText
SWI osPrintString
SWI osExit
noSave:
SETD.0 NoSaveText
SWI osPrintString
SWI osExit
noRead:
SETD.0 NoReadText
SWI osPrintString
SWI osExit
noRename:
SETD.0 NoRenameText
SWI osPrintString
SWI osExit
noDelete:
SETD.0 NoDeleteText
SWI osPrintString
SWI osExit
#Data
#Base 0x1000
Name:
"kept.txt"
OtherName:
"moved.txt"
Body:
"a file kept by asking
"
SavedText:
"saved it
"
ReadText:
"read it back, "
BytesText:
" bytes:
"
RenamedText:
"renamed it
"
DeletedText:
"deleted it
"
GoneText:
"and it is gone
"
StillText:
"but it is still there
"
NoSaveText:
"it would not save
"
NoReadText:
"it would not read
"
NoRenameText:
"it would not rename
"
NoDeleteText:
"it would not delete
"
Landing:
#Reserve 0d256
+101
View File
@@ -0,0 +1,101 @@
; A loaded program that is interrupted by the console rather than asking it for anything.
;
; Until the loadable program format could carry vectors, this could not be written. A
; handler has to be an address in the vector table, and a program that is not the one the
; machine booted from had no way to say what its vectors were, so interrupts belonged to
; boot images and every loaded program had to poll. Snake polls for exactly that reason.
;
; What this shows is the whole path: the assembler writes the vectors into the file, the
; system installs them when the program is run, the console raises its line, the handler
; runs, and the system takes the vectors back out again when the program gives the machine
; back. Nothing in the waiting loop below looks at the console at all.
;
; The vector is named by port, because a device interrupts on the port it is plugged into
; and the console is on port 0x00.
#Include services.asm
#Program
#Base 0x2000
start:
CIF ; Nothing arrives until there is something to catch it.
; Set rather than trusted to be zero. Running a program a second time does not load it
; again, so its Data Segment is exactly as the last run left it - and the last thing the
; last run did was set this.
RSTA
SETD.0 Stopping
STA.0
SETD.0 Banner
CALL printString
CALL newLine
; Key mode and interrupt on input, in one write, since the two control bits are
; independent of each other.
INIA 0x03
OUTA 0x02
SIF
wait:
; 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
CALL newLine
RSTA
OUTA 0x02 ; Line mode and no interrupts, the way it was found.
SETD.0 DoneText
CALL printString
CALL newLine
SWI osExit
; Entered because the console had something to say. Never called.
keyHandler:
INA 0x01
INIB 0x01 ; READY: is there a byte, as opposed to the end of input?
AND
BRQ keyNoByte
INA 0x00
INIB 0x71 ; q, which is how this program is stopped.
XOR
BRQ keyStop
OUTA 0x00 ; Nothing echoes in key mode, so the handler does it.
RETI
keyNoByte:
; The end of input raises the line once as well, so a program driven entirely by
; interrupts is told when nothing more is coming instead of waiting for ever.
keyStop:
SETD.3 Stopping
INIA 0x01
STA.3
RETI
#Data
#Base 0x1000
Banner:
"keys, by interrupt. q stops."
DoneText:
"the console has been handed back"
; The only thing the handler and the loop it interrupts have to say to each other.
Stopping:
0x00
#Vectors
Boot start
Device 0x00 keyHandler
#Include console.asm
+57
View File
@@ -0,0 +1,57 @@
; A program that is told what to work on.
;
; Everything loaded before this one did the same thing however it was started, because
; there was no way to tell one anything. A tool that edits a document needs to know which
; document, and that is the same need a dozen other things will have, so it is a service
; rather than something an editor arranges for itself.
;
; run says it was given nothing
; run whatever else says "whatever else"
;
; The whole rest of the line arrives, spaces and all, rather than a list of words. What
; counts as an argument is the program's business; the system's business is handing over
; what was typed.
#Include services.asm
#Program
#Base 0x2000
start:
SETD.0 Given
INIB 0d64
SWI osArgument
SETD.0 Given
LDA.0
BRA sayNothing
SETD.0 SaidText
SWI osPrintString
SETD.0 Given
SWI osPrintString
BRI sayEnd
sayNothing:
SETD.0 NothingText
SWI osPrintString
sayEnd:
SETD.0 NewLine
SWI osPrintString
SWI osExit
#Data
#Base 0x1000
SaidText:
"it says: "
NothingText:
"nothing was said"
NewLine:
0x0A 0x00
Given:
#Reserve 0d64
+9 -7
View File
@@ -9,13 +9,15 @@
; console holds the next key until it is asked, so nothing typed between frames is lost,
; and a script of moves plays back one move to a frame.
;
; WHY IT POLLS RATHER THAN INTERRUPTS. The console can raise an interrupt line when a byte
; arrives, which is the better shape for a game: the loop would never look at the console
; at all. A loaded program cannot use it. Installing a handler means putting an address in
; the vector table, and the loadable program format carries only code and data - a program
; that is not the one the machine booted from has no way to say what its vectors are. So
; interrupts belong to boot images for now, and this asks once a frame, which is what the
; machines this one is pretending to be did anyway.
; WHY IT POLLS RATHER THAN INTERRUPTS. When this was written a loaded program could not be
; interrupted at all: installing a handler means putting an address in the vector table,
; and the loadable format carried only code and data, so a program that was not the one the
; machine booted from had no way to say what its vectors were. That is no longer true - the
; format carries them now, and Keys.asm is the program that shows it.
;
; This still polls, and now by choice. Asking once a frame is what the machines this one is
; pretending to be actually did, it is the shape a game with a frame loop wants anyway, and
; having one of each in the same Apps directory is worth more than having two the same.
;
; THE BOARD IS A PAGE, and that is the whole trick this program turns on. Sixteen by
; sixteen is 256 squares, so a square number is a byte, and the board is aligned so that
+270
View File
@@ -0,0 +1,270 @@
# CosmOS
## Overview:
CosmOS is a small, single-tasking disk operating environment for the SplitBit 8-bit
computer. It boots the machine, finds and mounts an SBFS filesystem, provides a command
line and memory monitor, loads applications from disk, and takes control back when they
finish.
CosmOS is written entirely in SplitBit assembly. It is closer in scale and purpose to a
resident monitor or an early disk operating system than to a modern multitasking OS: one
program owns the machine at a time, there is no privilege boundary, and applications are
assembled for fixed regions of memory. What it provides is a stable home from which those
programs can be found, run, and given services without each one having to boot the machine
for itself.
### Features:
- Interactive Shell: Read commands from the SplitBit console and continue until `exit` or
the end of input.
- SBFS Filesystem: Mount, list, read, write, delete, and rename files on a SplitBit disk.
- Loadable Applications: Validate SBEX files, copy their Program and Data segments into
the addresses for which they were assembled, and start them at their declared entry
point.
- Resident Services: Applications can print strings and numbers, read lines, receive their
command arguments, read and write files, and return to the shell through named software
interrupts.
- Application Vectors: Install interrupt vectors carried by a loadable program and
restore whatever they replaced when the program exits.
- Stack Reclamation: Save the system Stack before launching an application and take it
back on exit, so an application need not unwind itself before returning.
- Memory Monitor: Inspect and modify Program Memory, Data Memory, and registered
device-memory banks through the SplitBit memory controller, disassemble instructions,
and begin execution at an address.
- Hardware Discovery: Mount the disk through the device registry rather than assuming
that one is present at a particular controller bank.
- Native Applications: Includes demonstrations, mathematical programs, interactive
programs, a game, and a line-oriented text editor.
- Reproducible Disk Image: The makefile assembles the system and every application, then
constructs a fresh SBFS image containing the resulting executables.
## Building and Running:
CosmOS currently lives inside the SplitBit Emulator repository and uses its assembler,
emulator, and disk-image tool. From the repository root, build those tools first:
```sh
make
```
Then build CosmOS and all of its applications:
```sh
cd Programs
make cosmos
```
Build a fresh SBFS application disk as well:
```sh
make cosmos-disk
```
To boot CosmOS with that disk attached:
```sh
make run-cosmos
```
The generated files are kept under `Programs/build/`:
- `CosmOS/Source/cosmos.bin` is the bootable CosmOS image.
- `CosmOS/Apps/*.sbx` are loadable application images.
- `cosmos.img` is the SBFS disk containing those applications.
The disk is rebuilt from scratch when its applications change, so its contents describe
the current source tree rather than accumulating files left by older builds.
## Shell Commands:
CosmOS currently provides these built-in commands:
| Command | Description |
| -- | -- |
| `dir` | List the files on the mounted disk and their sizes. |
| `load <file>` | Read and validate an SBEX application, then place its code and data where its header requests. |
| `run [words]` | Start the loaded application and make the rest of the line available to it as an argument. |
| `delete <file>` | Remove a file from the filesystem and release its blocks. |
| `rename <file> <to>` | Give a file a different name without moving its contents. |
| `monitor` | Enter monitor mode, in which the prompt becomes `*` and the commands below are also available. |
| `help` | Show the built-in command summary. |
| `exit` | Leave monitor mode if in it, and otherwise halt the machine. |
Monitor mode adds the following. It is a mode rather than a separate program because a
loaded application occupies the one region a loaded application is given, so a monitor
which was itself an application could never examine another one. The mode persists: an
application started with `g` which returns through `osExit` arrives back at the monitor
prompt rather than at the shell.
| Command | Description |
| -- | -- |
| `x [address]` | Display 64 bytes as hexadecimal and as characters. |
| `d [address]` | Disassemble eight instructions. |
| `s <address> <byte>...` | Write bytes into the bank being examined, including Program Memory. |
| `b <program\|data\|bank>` | Select a memory space or a registered bank number. |
| `g <address>` | Begin execution at an address. |
`x` and `d` share a position, and each leaves it after what it displayed, so either may be
given without an address to continue from where the last one stopped.
For example:
```text
> dir
> load Snake.sbx
> run
```
Loading and running are separate operations for now. A loaded program may be run again
without being read from disk again, which is useful both as a monitor facility and as a
test that CosmOS correctly restores its Stack and vector table after every run.
CosmOS also boots without a disk. It reports that no filesystem was found, leaves the
shell and memory monitor available, and refuses commands that require a mounted disk
without stopping the machine.
## Included Applications:
The application disk is populated from every assembly file in `CosmOS/Apps/`. At present
it includes:
- `Edit`: A line-oriented text editor that can create, load, modify, and save files from
inside CosmOS.
- `Snake`: A playable terminal game using nonblocking single-key input.
- `Life`: A 16 by 16 Conway's Game of Life simulation that returns when it settles or
reaches its generation limit.
- `Keys`: An interrupt-driven console demonstration carrying its own hardware vector.
- `Say`: Demonstrates receiving the argument supplied to `run`.
- `Files`: Writes, reads, renames, and deletes a file using only the system's services,
including no filesystem code of its own.
- `greet` and `hello`: Small examples of, respectively, using CosmOS services and talking
directly to SplitBit hardware.
- `Fib-8`, `Fib-16`, and `Fib-32`: Fibonacci demonstrations at three integer widths.
- `Sieve-8` and `Sieve-16`: Prime sieves covering the 8-bit and 16-bit ranges.
These programs are ordinary SBEX files on SBFS. They are not built into the operating
system, and the host-side `SplitDisk` tool can add or remove other files from an image.
## The Application Model:
CosmOS divides the two SplitBit address spaces by convention:
| Memory | CosmOS | Loaded application |
| -- | -- | -- |
| Program Memory | `0x0000` through `0x1FFF` | `0x2000` and above |
| Data Memory | `0x0000` through `0x0FFF` | `0x1000` and above |
Applications state their actual Program and Data addresses with `#Base`. The SplitBit
assembler then writes an SBEX loadable image containing those addresses, the entry point,
the segment lengths, and any vectors the application needs. CosmOS does not relocate
code: the addresses in the file must be the addresses for which it was assembled.
This division is an ABI convention rather than protection. An application owns the
machine while it runs and may address hardware or CosmOS memory directly. The convention
keeps independently assembled software out of the system's way; it is not a security
boundary.
### System Services:
Applications include `Source/services.asm` to obtain stable names and vector numbers for
the services CosmOS provides. The currently installed services are:
| Service | Interface |
| -- | -- |
| `osPrintString` | DP0 names a zero-terminated string to print. |
| `osReadLine` | DP0 names a destination and B is its capacity; Q returns the line length. |
| `osArgument` | DP0 names a destination and B is its capacity; receives the text following `run`. |
| `osExit` | Abandon the application's Stack, restore the CosmOS environment, and return to the shell. |
| `osFileRead` | DP0 names a file and DP1 a destination; Q reports success and DP3 returns its length in bytes. |
| `osFileSave` | DP0 names a file, DP1 supplies its contents, and A with B give the length; Q reports success. |
| `osFileDelete` | DP0 names a file to remove; Q reports success. |
| `osFileRename` | DP0 names an existing file and DP1 its new name; Q reports success. |
| `osPrintNumber` | A with B give a number to print in decimal without leading zeroes. |
The filesystem services exist so that an application need not contain a second copy of the
filesystem in order to keep a file. There is deliberately no service to mount a disk: the
system mounts one before its first prompt, and an application mounting it again was only
ever a consequence of owning a private copy of the library.
A minimal CosmOS application therefore looks like this:
```asm
#Include services.asm
#Program
#Base 0x2000
start:
SETD.0 Message
SWI osPrintString
SWI osExit
#Data
#Base 0x1000
Message:
"Hello from CosmOS."
```
An application may also include its own libraries or access hardware ports directly.
The services are an interface offered by the system, not the only way software is allowed
to use the computer.
## Source Layout:
- `Source/cosmos.asm`: Boot process, shell, loader, monitor, system services, and
application lifecycle.
- `Source/console.asm`: Console input, strings, hexadecimal and decimal output, and line
handling.
- `Source/text.asm`: String comparison, splitting, and hexadecimal text conversion used
by the shell.
- `Source/sbfs.asm`: Target-side implementation of the SplitBit filesystem.
- `Source/services.asm`: The shared names and stable vector numbers used by CosmOS and
separately assembled applications.
- `Apps/`: Loadable programs packaged onto the CosmOS disk image.
## Tests:
CosmOS is exercised as part of the SplitBit repository's normal test suite:
```sh
make test
```
The tests boot the system with and without a disk and drive the shell through recorded
console input. They cover directory traversal, every loader refusal, repeated application
runs, memory inspection, vector installation and restoration, command arguments,
filesystem deletion and renaming, interactive applications, and editing a file followed
by reading the saved result back in a second editor session.
Individual CosmOS tests can be run from the repository root, for example:
```sh
./Tests/run.sh cosmos cosmosRun cosmosEdit
```
Test disks are constructed with the host-side `SplitDisk` tool. CosmOS is therefore
reading filesystems written by an independent implementation of the same format rather
than merely checking its filesystem code against itself.
## Current Scope:
CosmOS is early software for an experimental computer. It runs one application at a
time, has no privilege levels or process isolation, does not relocate applications, and
does not yet provide a native assembler or linker. Its present purpose is to make
SplitBit usable from inside the machine: inspect it, manage persistent files, load
programs, provide common services, and return reliably to a command prompt.
The intended long-term milestone is self-hosting: editing SplitBit assembly source under
CosmOS, assembling and linking it natively, and eventually rebuilding CosmOS and its own
development tools on the machine.
## Additional Information:
The SplitBit Programming Manual describes the CPU, devices, memory controller, SBFS,
SBEX format, interrupt model, and CosmOS service interface. The SplitBit Assembler Manual
documents the assembly language, loadable-program bases, and vector declarations.
## License:
CosmOS is part of the SplitBit Emulator project and is licensed under the Apache License,
Version 2.0. See the repository's top-level `LICENSE` file for the full license text.
File diff suppressed because it is too large Load Diff
+236 -1
View File
@@ -350,13 +350,20 @@ sbfsWalkName:
; Compares the name in the entry at DP2 with the one kept in SbfsWanted. Q is zero if
; they are the same. Names are padded with zeroes rather than terminated, so a name that
; fills the field has no terminator to look for, which is why the count is what stops it.
; A COUNTER OF ITS OWN, and that matters more than it looks. sbfsFind counts the eight
; entries of a block in SbfsCount and calls this for each one; when this used SbfsCount too,
; a failed comparison left the ENTRY count holding however far the NAME comparison had got.
; A name differing at its first byte left 22 there, so the search walked twenty two entries
; through a buffer holding eight - off the end of it, into whatever data happened to follow,
; and reported a match against rubbish. What followed the buffer decided whether it looked
; like it worked, which is why it went unnoticed until unrelated data was added.
sbfsMatchEntry:
PSHD.2
POPD.0
DPUP.0 0d06 ; The name inside the entry.
SETD.2 SbfsWanted
INIA 0d22
SETD.1 SbfsCount
SETD.1 SbfsMatchLeft
STA.1
sbfsMatchLoop:
@@ -991,6 +998,208 @@ sbfsSameByte:
XOR
RET
; ---- Deleting ----
;
; Frees a file. DP0 names it, and Q is zero if it went.
;
; A deleted entry and one that was never used are the same thing, which is the whole of
; what deleting is here: the entry is zeroed and its blocks stop being spoken for. THE
; BLOCKS THEMSELVES ARE LEFT EXACTLY AS THEY WERE, so what was in a file is still on the
; disk until something is put over the top of it. Worth knowing if anything is ever meant
; to be private. The host tool does the same, so the two agree about what a deleted disk
; looks like.
;
; Nothing is compacted. Deleting leaves a hole, and because files are contiguous a hole is
; only usable by something that fits inside it. That is the price of the directory being
; the whole allocation map, and tidying it up is an ordinary program somebody can write
; rather than anything the format has to say.
sbfsDelete:
CALL sbfsFind
BNQ sbfsDeleteFailed
; How much room it was taking, worked out before the entry that says so is thrown away.
CALL sbfsFileExtent
; sbfsFind leaves DP3 on the entry and SbfsBlock on the directory block it came out of,
; which is everything needed to change it and put it back.
PSHD.3
POPD.0
INIB 0d32
sbfsDeleteWipe:
RSTA
STA.0
INCD.0
DECB
BNB sbfsDeleteWipe
SETD.1 SbfsBuffer
CALL sbfsBufferIn
CALL sbfsWriteBlock
BNQ sbfsDeleteFailed
; And the free count goes back up. It is a note rather than the truth, but a note worth
; keeping right.
RSTA
SETD.0 SbfsBlock
STA.0
INCD.0
STA.0
CALL sbfsReadBlock
BNQ sbfsDeleteFailed
SETD.1 SbfsBuffer
CALL sbfsBufferOut
SETD.0 SbfsBuffer
DPUP.0 0d12
SETD.2 SbfsWantBlocks
CALL sbfsAddWord
SETD.1 SbfsBuffer
CALL sbfsBufferIn
CALL sbfsWriteBlock
RET
sbfsDeleteFailed:
RSTA
INIB 0d1
CCF
ADD
RET
; ---- Renaming ----
;
; DP0 is the name a file has, DP1 is the name it should have. Q is zero if it was renamed.
;
; Only the twenty two bytes of the name change, so no data moves and no block is touched
; but the one holding the entry. THAT IS WHAT MAKES A SAFE SAVE POSSIBLE. Writing a file
; that has grown means putting it somewhere else, and the obvious order - throw the old one
; away, then write the new one - loses the lot if there turns out to be nowhere to put it.
; Renaming is the cheapest thing this filesystem can do and the only one that can be left
; until last, so it is what the order is built around.
sbfsRename:
; Where the old name is, kept somewhere that finding things will not tread on.
SETD.2 SbfsSavedName
STD.0.2
PSHD.1
POPD.0
SETD.1 SbfsNewName
CALL sbfsKeepName ; Twenty two bytes, padded, the way an entry holds one.
; Refused if something already answers to the new name. Two entries with one name is a
; disk that cannot be searched sensibly: a search answers with whichever it meets first,
; so the other becomes unreachable without ever having been deleted.
SETD.0 SbfsNewName
CALL sbfsFind
BRQ sbfsRenameFailed
SETD.2 SbfsSavedName
LDD.0.2
CALL sbfsFind
BNQ sbfsRenameFailed
PSHD.3
POPD.1
DPUP.1 0d06 ; Past the flags, the start, the block count and the tail.
SETD.0 SbfsNewName
INIA 0d22
SETD.2 SbfsCount
STA.2
sbfsRenameName:
LDA.0
STA.1
INCD.0
INCD.1
LDA.2
DECA
STA.2
BNA sbfsRenameName
SETD.1 SbfsBuffer
CALL sbfsBufferIn
CALL sbfsWriteBlock
RET
sbfsRenameFailed:
RSTA
INIB 0d1
CCF
ADD
RET
; ---- Saving over something that is already there ----
;
; DP0 names the file, DP1 is the data, and SbfsFileBlocks with SbfsFileTail say how big it
; now is. Q is zero if it was saved.
;
; This is the routine every tool that edits a document wants, and the reason it is here
; rather than in each of them is that the careful order is not obvious and getting it wrong
; destroys somebody's work:
;
; make a temporary nothing is lost if there is nowhere to put it
; write it
; delete the original only once the new one is safely down
; rename the temporary
;
; The obvious order - delete, create, write - looks fine and is a trap. Files here are
; contiguous, so a file that has grown may not fit where it was, and a create can be
; refused for want of a run long enough even on a disk with plenty of free blocks. Do it
; that way round and the original is already gone when that happens.
sbfsSaveFile:
SETD.2 SbfsSaveName
STD.0.2
SETD.2 SbfsSaveData
STD.1.2
; How big it is, kept aside: finding and deleting things both overwrite the place the
; size is normally said, because both of them describe whatever they last looked at.
SETD.0 SbfsSaveBlocks
SETD.2 SbfsFileBlocks
CALL sbfsSetWord
SETD.0 SbfsFileTail
LDA.0
SETD.1 SbfsSaveTail
STA.1
; A temporary left behind by a save that did not finish would be in the way. Whether
; there was one is not worth asking about, since either answer leads here.
SETD.0 SbfsTempName
CALL sbfsDelete
SETD.0 SbfsFileBlocks
SETD.2 SbfsSaveBlocks
CALL sbfsSetWord
SETD.0 SbfsSaveTail
LDA.0
SETD.1 SbfsFileTail
STA.1
SETD.0 SbfsTempName
CALL sbfsCreate
BNQ sbfsSaveFailed
SETD.2 SbfsSaveData
LDD.1.2
CALL sbfsWriteFile
BNQ sbfsSaveFailed
; Now, and not before, the old one goes. It may not exist, which is what saving something
; for the first time looks like from here.
SETD.2 SbfsSaveName
LDD.0.2
CALL sbfsDelete
SETD.0 SbfsTempName
SETD.2 SbfsSaveName
LDD.1.2
CALL sbfsRename
RET
sbfsSaveFailed:
RSTA
INIB 0d1
CCF
ADD
RET
#Data
SbfsMagic:
@@ -1026,6 +1235,8 @@ SbfsOne:
0x00 0x01
SbfsCount:
0x00
SbfsMatchLeft:
0x00
SbfsLeft:
0x00
@@ -1050,5 +1261,29 @@ SbfsName:
SbfsWanted:
#Reserve 0d22
; ---- What renaming and saving keep ----
; A name on its way into an entry, padded to the twenty two bytes an entry holds.
SbfsNewName:
#Reserve 0d22
; Where a name lives, kept across a search, which needs the pointers for itself.
SbfsSavedName:
0x00 0x00
SbfsSaveName:
0x00 0x00
SbfsSaveData:
0x00 0x00
SbfsSaveBlocks:
0x00 0x00
SbfsSaveTail:
0x00
; What a document is called while it is being written and is not yet the real thing. A
; name nothing else is likely to want, and short enough to leave room for a long one.
SbfsTempName:
"sbfs.part"
SbfsBuffer:
#Reserve 0d256
+27
View File
@@ -24,3 +24,30 @@
osPrintString 0d16 ; DP0 names a string. Prints it.
osReadLine 0d17 ; DP0 names somewhere to put a line read from the console.
osExit 0d18 ; Give the machine back to the system.
osArgument 0d19 ; DP0 names somewhere to put the rest of the run command.
; ---- What the system does with the disk on a program's behalf ----
;
; A loaded program that wanted a file used to include the whole filesystem, which is two
; and a half kilobytes of it carrying a private copy of code the system already has
; running. These are that code, reachable.
;
; NOTHING HERE MOUNTS ANYTHING. The system mounted the disk before it read the prompt, and
; there is one disk with one buffer registered as one bank; a program mounting it again was
; only ever an artefact of having its own copy of the library.
;
; Sizes are in bytes and fit the registers exactly. 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 that
; both sides have to agree on the shape of.
osFileRead 0d20 ; DP0 names it, DP1 says where. Q is zero if it read, DP3 is how many bytes.
osFileSave 0d21 ; DP0 names it, DP1 is the bytes, A and B are how many. Q is zero if it saved.
osFileDelete 0d22 ; DP0 names it. Q is zero if it went.
osFileRename 0d23 ; DP0 is the name it has, DP1 the name it should have. Q is zero if it moved.
; ---- And with the console ----
;
; printString is already up there. This is the other half of what a program prints: a
; number, in decimal, without leading zeroes. A and B together, so one service covers both
; a line number and a byte count and there is no need for two.
osPrintNumber 0d24
+76
View File
@@ -215,6 +215,82 @@ textHexNo:
ADD
RET
; ---- A number written in decimal ----
;
; DP0 names it. Q is the value, and TextDigits says how many digits were read, which is
; zero when there was no number there at all. Stops at the first thing that is not a digit.
;
; Decimal rather than hex, and one byte rather than two, because this is for the numbers a
; person types at a program: a line number, a count, a how many. Nobody counts lines in
; hex, and nobody types a line number above 255 on a machine this size. textHexWord is
; still the one for an address, where hex is what everybody means.
;
; Ten times the running total is worked out as eight of it plus two of it, because nothing
; on this machine multiplies. Anything past 255 wraps, which is what the same sum does
; everywhere else here.
textNumber:
RSTA
SETD.1 TextValue
STA.1
SETD.1 TextDigits
STA.1
textNumberLoop:
LDA.0
BRA textNumberDone
; Below '0' or above '9' ends it.
INIB 0d48
CCF
SUB
BRC textNumberDone ; It borrowed, so the character was below '0'.
MVQA
INIB 0d10
CCF
SUB
BNC textNumberDone ; It did not borrow, so it was ten or more past '0'.
PSHA ; The digit, while the total is multiplied.
SETD.1 TextValue
LDA.1
LDB.1
CCF
ADD ; Twice.
MVQA
MVQB
PSHA ; Twice, kept: ten is eight and two.
CCF
ADD ; Four times.
MVQA
MVQB
CCF
ADD ; Eight times.
MVQA
POPB
CCF
ADD ; Ten times.
MVQA
POPB
CCF
ADD ; And the digit.
SETD.1 TextValue
STQ.1
SETD.1 TextDigits
LDA.1
INCA
STA.1
INCD.0
BRI textNumberLoop
textNumberDone:
SETD.1 TextValue
LDA.1
RSTB
CCF
ADD ; Q is the value, the way a routine hands a byte back.
RET
#Data
; Where the rest of the line begins, after textSplit has taken a word off the front.
+5 -2
View File
@@ -76,10 +76,13 @@ cosmos: $(COSMOS) $(APPS)
$(COSMOS_DISK): $(APPS)
@mkdir -p $(@D)
rm -f $@
$(DISKTOOL) format $@ 64 2
$(DISKTOOL) format $@ 256 2
@for app in $(APPS); do $(DISKTOOL) put $@ $$app; done
cosmos-disk: $(COSMOS_DISK)
# The system as well as the disk. Building only the image leaves whatever cosmos.bin was
# there before, or none at all, and then the disk is booted with a system that does not
# match the programs on it.
cosmos-disk: $(COSMOS) $(COSMOS_DISK)
run-cosmos: $(COSMOS) $(COSMOS_DISK)
$(EMU) --disk $(COSMOS_DISK) $(COSMOS)
+233
View File
@@ -0,0 +1,233 @@
; Deleting, renaming, and saving over something that is already there.
;
; Reading and writing were built first because a program had to be got onto a disk and off
; it again. What a document needs is different and it is all about the second time: a file
; that is written once is easy, and a file that is written again having grown is where this
; format's bargain shows. Files are contiguous and do not grow, so saving a longer version
; means putting it somewhere else and letting go of where it was.
;
; THE ORDER MATTERS AND THE OBVIOUS ONE IS WRONG. Delete the old, create the new, write it:
; that loses the lot when the create is refused for want of a run long enough, which is a
; thing that happens on a disk with plenty of free blocks once it is in pieces. sbfsSaveFile
; does it the other way round, and the rename at the end of that is why renaming exists.
;
; Correct output is:
; here.txt 0002 already here
; doc.txt 0003 first draft
; doc.txt 0004 a second draft, which is longer than the first
; notes.txt 0004 a second draft, which is longer than the first
; doc.txt gone
; notes.txt gone
;
; The start block moving from 0003 to 0004 is the file being put somewhere else, which is
; what saving a longer one has to do. The last two lines are a rename and a delete having
; actually happened rather than having been reported.
#Include print.asm
#Include sbfs.asm
#Program
start:
CALL sbfsMount
BNQ failed
SETD.0 Existing
CALL report
BNQ failed
; Written once, the ordinary way.
SETD.0 DocName
SETD.1 ShortText
INIA 0d11
CALL makeAndWrite
BNQ failed
SETD.0 DocName
CALL report
BNQ failed
; And again, longer. Nothing here says where it goes; that is sbfsSaveFile's business.
SETD.0 SbfsFileBlocks
RSTA
STA.0
INCD.0
STA.0
SETD.0 SbfsFileTail
INIA 0d46
STA.0
SETD.0 DocName
SETD.1 LongText
CALL sbfsSaveFile
BNQ failed
SETD.0 DocName
CALL report
BNQ failed
; Renaming moves nothing: the same blocks answer to a different name.
SETD.0 DocName
SETD.1 NewName
CALL sbfsRename
BNQ failed
SETD.0 NewName
CALL report
BNQ failed
; And the old name is not there any more, which is the half of renaming that could have
; quietly not happened.
SETD.0 DocName
CALL expectGone
SETD.0 NewName
CALL sbfsDelete
BNQ failed
SETD.0 NewName
CALL expectGone
HALT
failed:
SETD.0 Failed
CALL printString
CALL lineFeed
HALT
; DP0 names the file, DP1 is its text, A is how long it is. Everything here fits in a
; block, so the whole length is the tail.
makeAndWrite:
PSHD.1
SETD.1 SbfsFileTail
STA.1
RSTA
SETD.1 SbfsFileBlocks
STA.1
INCD.1
STA.1
CALL sbfsCreate
POPD.1
BNQ makeFailed
CALL sbfsWriteFile
RET
makeFailed:
RET
; DP0 names a file that should not be there. Says so either way.
expectGone:
PSHD.0
POPD.3
CALL printString
CALL padName
PSHD.3
POPD.0
CALL sbfsFind
BRQ goneStillThere
SETD.0 GoneText
CALL printString
CALL lineFeed
RET
goneStillThere:
SETD.0 StillText
CALL printString
CALL lineFeed
RET
; Prints a file's name, where it begins, and what is in it.
report:
PSHD.0
POPD.3
CALL printString
CALL padName
PSHD.3
POPD.0
CALL sbfsFind
BNQ reportFailed
SETD.0 SbfsFileStart
LDA.0
CALL printByteHex
INCD.0
LDA.0
CALL printByteHex
CALL blankSpace
SETD.1 Landing
CALL sbfsRead
BNQ reportFailed
SETD.0 Landing
SETD.1 SbfsFileTail
LDA.1
SETD.1 LeftOver
STA.1
BRA reportEnd
reportLoop:
LDA.0
OUTA 0x00
INCD.0
SETD.1 LeftOver
LDA.1
DECA
STA.1
BNA reportLoop
reportEnd:
CALL lineFeed
RSTA
RSTB
CCF
ADD
RET
reportFailed:
RSTA
INIB 0d1
CCF
ADD
RET
; Names are different lengths and the columns should not be, so this pads out to eleven.
; DP3 holds the name, which is where the caller left it.
padName:
PSHD.3
POPD.0
INIB 0d11
padCount:
LDA.0
BRA padOut
INCD.0
DECB
BNB padCount
padOut:
RSTA
padLoop:
BRB padDone
INIA 0x20
OUTA 0x00
DECB
BRI padLoop
padDone:
RET
#Data
Existing:
"here.txt"
DocName:
"doc.txt"
NewName:
"notes.txt"
ShortText:
"first draft"
LongText:
"a second draft, which is longer than the first"
GoneText:
"gone"
StillText:
"STILL THERE"
Failed:
"failed"
LeftOver:
0x00
Landing:
#Reserve 0d256
+107
View File
@@ -0,0 +1,107 @@
; A software interrupt handing something back.
;
; RETI restores every register from the frame, which is what makes an interrupt safe to
; arrive at an arbitrary moment: the interrupted code cannot tell it happened. A SERVICE is
; not arbitrary - it was asked for - and the same rule means it has no way to answer.
;
; So a service that has something to say writes it INTO ITS OWN FRAME, over the saved
; register, and lets RETI put it back. MVSD is what makes the frame reachable: it copies
; the Stack Pointer into a Data Pointer, and the frame sits just above it.
;
; +1 Status +5 DP3 high +9 DP1 high +13 resume high
; +2 Q +6 DP3 low +10 DP1 low +14 resume low
; +3 A +7 DP2 high +11 DP0 high
; +4 B +8 DP2 low +12 DP0 low
;
; WHICH REGISTERS A SERVICE MAY ANSWER IN is a convention rather than a rule, and it is the
; same one CALL already has: Q and DP3. A subroutine cannot hand back A, B or DP0 to DP2
; because RET puts them back; a service could write over any of them, and should not, for
; exactly the reason the first list exists. A caller expects what it kept to still be there.
;
; ONLY THE HANDLER ITSELF CAN DO THIS. The offsets are from where the Stack Pointer is, and
; a CALL moves it by ten. A routine called by a handler that tried this would be writing
; into its own return address.
;
; Correct output is:
; quiet: 7 a service that says nothing leaves Q as it found it
; answer: 42 one that does, does not
; pointer: ABC and DP3 comes back the same way
#Include console.asm
#Program
start:
; Something recognisable in Q, so that a service leaving it alone is visible.
INIA 0d7
RSTB
CCF
ADD
SETD.0 QuietText
CALL printString
SWI quiet
MVQA
CALL printByteDecimal
CALL newLine
SETD.0 AnswerText
CALL printString
SWI answer
MVQA
CALL printByteDecimal
CALL newLine
SETD.0 PointerText
CALL printString
SWI pointer
PSHD.3
POPD.0
CALL printString
CALL newLine
HALT
; Says nothing, so whatever the caller had in Q is still there afterwards.
quiet:
INIA 0d99
RSTB
CCF
ADD ; Q is 99 in here, and nobody outside will ever know.
RETI
answer:
INIA 0d42
MVSD.1
DPUP.1 0d02 ; The saved Q.
STA.1
RETI
pointer:
SETD.0 Letters
MVSD.1
DPUP.1 0d05 ; The saved DP3, high byte first the way everything is stored.
PSHD.0
POPA ; The low half comes off the Stack first.
POPB
STB.1
INCD.1
STA.1
RETI
#Data
QuietText:
"quiet: "
AnswerText:
"answer: "
PointerText:
"pointer: "
Letters:
"ABC"
#Vectors
Boot start
quiet quiet
answer answer
pointer pointer
+49
View File
@@ -0,0 +1,49 @@
; A string whose text spells an instruction.
;
; The quotes are gone by the time the assembler looks at a token, so a string reading "ADD"
; used to be assembled as the ADD instruction - which failed with "attempting to assemble
; outside the Program Segment", a message about a mistake nobody had made. Mnemonics match
; without regard to case, so "or" and "and" were caught by it too, and those are ordinary
; enough words to want in a message.
;
; Correct output is:
; ADD OR and NOP
#Include console.asm
#Program
start:
SETD.0 First
CALL printString
CALL blank
SETD.0 Second
CALL printString
CALL blank
SETD.0 Third
CALL printString
CALL blank
SETD.0 Fourth
CALL printString
CALL newLine
HALT
blank:
INIA 0x20
OUTA 0x00
RET
#Data
First:
"ADD"
Second:
"OR"
Third:
"and"
Fourth:
"NOP"
#Vectors
Boot start
+7 -1
View File
@@ -14,6 +14,8 @@ SplitBit is a custom 8 bit system designed for hobbyist projects and experimenta
- Devices: A bus registry that says what a machine is made of, so a program can ask rather than being told.
- Filesystem: SBFS, read and written by SplitBit itself, and by a host tool that speaks the same format so an image can be moved either way.
- Loadable Programs: A program that was not booted from carries a header saying where it belongs, and Programs/loader.asm reads one off a disk, puts it there, and runs it.
- An Operating System: CosmOS boots the machine, mounts a disk, lists what is on it, loads a program and runs it, and takes the machine back when it finishes. It comes with a library of programs to run, including a game and a line editor that writes files a person typed.
- System Services: A loaded program reaches the console and the disk through numbered software interrupts rather than carrying a copy of the code that drives them. The numbers are written down in one file that both sides include, so neither ever types one. It took the editor from 4941 bytes to 1983 without changing a line of what it does.
- Storage: A block device with 256 byte blocks and 16 megabytes of them, backed by an image file on the host. It knows blocks and not files, because a filesystem is meant to be software SplitBit runs.
- Memory Controller: Reads and writes Program Memory, moves blocks between memory 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.
- Assembler: Assemble human readable assembly language files directly into SplitBit compatible binary files. Supports including external files, handling labels, alignment and reservation, and defining Program, Data and Vector segments.
@@ -66,7 +68,7 @@ The sources are ISO C, and build clean under -std=c11 -pedantic with -Wall -Wext
- delete \<image\> \<name\>: Remove one.
#### Notes:
- SplitDisk speaks the same on disk format SplitBit does, so an image it makes is one the machine can read, and one the machine writes is one it can read back. Until SplitBit can write its own filesystem this is the only way to get a program onto a disk.
- SplitDisk speaks the same on disk format SplitBit does, so an image it makes is one the machine can read, and one the machine writes is one it can read back. SplitBit writes its own filesystem now, so this is not the only way to get something onto a disk; it is still the only way to get a program onto one, since nothing running on the machine assembles anything yet.
- Files are laid down contiguously, so a disk can have free blocks without having them in one piece. When that happens put says so rather than putting part of a file on.
### Usage:
@@ -110,6 +112,10 @@ Disk images that tests read from are built by Tests/makedisks.sh before the run,
The disk tool is checked separately by Tests/disk.sh, which make test runs afterwards: it puts files of every awkward size onto an image and takes them off again, and checks that the things the format says cannot happen are refused.
Tests/terminal.sh checks the things a recorded output cannot see. Every other test pipes input in and output to a file, which answers what a program prints and is blind to two whole classes of behaviour: **when** something is printed, since 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 **what happens to the terminal**, since key mode only touches one 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 the questions 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.
A cycle count is deliberately **not** part of a recorded result. The last line of the emulator's output has the number taken out of it 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 arrive in a crowd of meaningless ones. Anything that wants to measure cycles should say so in a test of its own.
Tests/docs.sh then 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 routine the manuals promise exists, and that the worked examples still assemble to the bytes printed beside them. Documentation goes stale quietly, and this is what stops it.
Tests are defined in Tests/manifest, one line per program. To record the current output as the expected result, after you have checked that it is correct:
+23 -5
View File
@@ -166,8 +166,18 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
exit(1);
}
// Read off tokens.
while (readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
if ((size_t)*intermediateIndex >= *arraySize - 1) {
//
// ROOM IS MADE BEFORE THE TOKEN IS READ, not after. readToken writes into the element
// at the current index, so a check that came afterwards was checking whether the write
// that had already happened was allowed to. It survived for a long time because the
// margin usually covered it, and stopped surviving when a file grew past a doubling:
// several paths below take a SECOND element for one token - an #Include takes one for
// the file name, #Align and #Reserve take one for the count - so the index can move by
// two in an iteration and step straight over a margin of one.
//
// The margin is two for that reason, which is the most any one iteration uses.
while (1) {
if ((size_t)*intermediateIndex + 2 >= *arraySize) {
size_t grownSize = *arraySize * 2; // Double the size of the array.
// Into a temporary, so that the old allocation is still ours to free if
// this fails, rather than being lost the moment realloc returns NULL.
@@ -184,7 +194,9 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
*intermediateArray = grown;
*arraySize = grownSize;
}
//printf("Token number %d\n", intermediateIndex);
if (!readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
break;
}
// Go ahead and mark what we already know about this token.
(*intermediateArray)[*intermediateIndex].fileName = fileName;
(*intermediateArray)[*intermediateIndex].lineNumber = lineNumber;
@@ -311,8 +323,14 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
status = VECTORS;
break;
}
// Next, check to see if it's an instruction.
} else if (checkIfInstruction(&(*intermediateArray)[*intermediateIndex])) {
// Next, check to see if it's an instruction. A string is never one, however it is
// spelled: the quotes are gone by the time anything looks at a token, so a string
// whose text happens to be a mnemonic looked exactly like that instruction and was
// assembled as one. Mnemonics are matched without regard to case, so this was not
// only a problem for a program with "ADD" in its data - "or" and "and" are ordinary
// enough words to find in a message.
} else if ((*intermediateArray)[*intermediateIndex].type != STRING
&& checkIfInstruction(&(*intermediateArray)[*intermediateIndex])) {
// We should check if we're set up to mark this for the Program Segment.
if (status != PROGRAM) {
fprintf(stderr, RED "Error: Attempting to assemble outside the Program Segment.\n Did you forget to use the #Program keyword?\n" RESET);
+70
View File
@@ -0,0 +1,70 @@
// sbex.h
// The SplitBit loadable program format, version one.
//
// A program that is not the one the machine booted from has to say where it wants to
// live, because nothing relocates it. This is a header saying that, in front of the
// bytes themselves. It is the same idea as the load address on the front of a C64 .PRG,
// with room for the machine to ask a few more questions later.
//
// Two things read this: whatever builds one on the host, and the loader running on
// SplitBit. As with the filesystem, nothing is shared between them but the specification.
//
// All multi byte numbers are most significant byte first.
//
// 0 4 "SBEX"
// 4 1 Version
// 5 1 How many vectors follow the data, zero in a version 1 file
// 6 2 Where the code goes in Program Memory
// 8 2 Where to start running, an address in Program Memory
// 10 2 How many bytes of code there are
// 12 2 Where the data goes in Data Memory
// 14 2 How many bytes of data there are
// 16 The code, then the data, then the vectors
//
// ---- Vectors, added in version two ----
//
// Four bytes each, the same shape a boot image uses: the address of the vector table slot,
// then the address to put in it. Both most significant byte first. Saying the slot outright
// rather than the vector number means the loader does no arithmetic and does not need to
// know where either vector table begins, and one entry can name a software or a hardware
// vector without saying which.
//
// A PROGRAM CARRYING VECTORS SAYS VERSION TWO, and one carrying none stays version one and
// loads anywhere. The version is not decided by the format's age but by whether the file
// needs something of its loader: an older loader meeting a version two file refuses it and
// says so, which is the right answer, because a program whose handlers were quietly dropped
// is not the program somebody asked for. It would run, and then fail later at a place with
// nothing to connect it back to loading.
//
// INSTALLING THEM IS THE LOADER'S JOB, AND SO IS TAKING THEM BACK. A vector points into the
// program that supplied it, so leaving one installed after that program is gone aims an
// interrupt at whatever occupies those addresses next. A loader restores what it found.
#define SBEX_VECTOR_ENTRY_BYTES 4
//
// Sixteen bytes, so the code begins at a round offset and finding it is one step rather
// than an arithmetic. Nothing here relocates anything: the addresses are where the
// program was built to live, and putting it anywhere else would leave every branch and
// every SETD inside it pointing at the wrong place.
//
// Written by Anachronaut
#ifndef SBEX_H
#define SBEX_H
#define SBEX_MAGIC "SBEX"
#define SBEX_MAGIC_BYTES 4
// What a program says when it asks nothing of its loader beyond code and data, and what
// it says when it also brings vectors that have to be installed.
#define SBEX_VERSION 1
#define SBEX_VERSION_VECTORS 2
#define SBEX_HEADER_BYTES 16
#define SBEX_VERSION_AT 4
#define SBEX_VECTORS_AT 5
#define SBEX_CODE_AT 6
#define SBEX_ENTRY_AT 8
#define SBEX_CODE_LEN_AT 10
#define SBEX_DATA_AT 12
#define SBEX_DATA_LEN_AT 14
#endif // SBEX_H
+64 -8
View File
@@ -57,7 +57,10 @@ void addLabel(char *labelName, uint16_t address, int type, const char *fileName,
if (debugSecondPass) printf("Added label %s with address %04X\n", labelName, labelArray[labelCount].address);
labelCount++;
} else {
fprintf(stderr, "Error: Too many labels defined.\n");
fprintf(stderr, RED "Error: Too many labels. This program and everything it\n"
" includes may define %d between them, and \"%s\" is one too many.\n" RESET,
MAX_LABELS, labelName);
printf("File: %s at line %d.\n", fileName, lineNumber);
exit(1);
}
}
@@ -564,16 +567,43 @@ static void writeLoadable(const char *outputFileName, uint8_t *Program, int prog
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, outputFileName);
exit(1);
}
// Boot says where a program begins, which is what the entry field holds, so in a
// loadable program that line fills it in. It is NOT installed as vector 0: that entry
// is where the whole machine starts, and a program being loaded into a running system
// has no business saying anything about that.
//
// Without a Boot line the entry is the first byte of the code, which is where a
// program begins if it does not say otherwise.
uint16_t entry = codeBase;
int installed = 0;
for (int i = 0; i < vectorArrayCount; i++) {
if (vectorArray[i].declaredOnly) {
continue;
}
if (vectorArray[i].base == SOFTWARE_VECTOR_BASE
&& vectorArray[i].index == VECTOR_BOOT) {
entry = vectorArray[i].handler;
continue;
}
installed++;
}
if (installed > 255) {
fprintf(stderr, RED "Error: A loadable program may bring at most 255 vectors.\n" RESET);
exit(1);
}
uint8_t header[SBEX_HEADER_BYTES];
memset(header, 0, sizeof(header));
memcpy(header, SBEX_MAGIC, SBEX_MAGIC_BYTES);
header[SBEX_VERSION_AT] = SBEX_VERSION;
// A program that brings vectors needs something of its loader that a version one
// loader does not know how to give, so it says so, and an older one refuses it rather
// than running it without them.
header[SBEX_VERSION_AT] = installed > 0 ? SBEX_VERSION_VECTORS : SBEX_VERSION;
header[SBEX_VECTORS_AT] = (uint8_t)installed;
header[SBEX_CODE_AT] = (uint8_t)(codeBase >> 8);
header[SBEX_CODE_AT + 1] = (uint8_t)(codeBase & 0xFF);
// Where it starts is where it begins. A program that wants otherwise puts a branch
// at its first instruction, which costs three bytes and needs no format for it.
header[SBEX_ENTRY_AT] = (uint8_t)(codeBase >> 8);
header[SBEX_ENTRY_AT + 1] = (uint8_t)(codeBase & 0xFF);
header[SBEX_ENTRY_AT] = (uint8_t)(entry >> 8);
header[SBEX_ENTRY_AT + 1] = (uint8_t)(entry & 0xFF);
header[SBEX_CODE_LEN_AT] = (uint8_t)(codeLength >> 8);
header[SBEX_CODE_LEN_AT + 1] = (uint8_t)(codeLength & 0xFF);
header[SBEX_DATA_AT] = (uint8_t)(dataBase >> 8);
@@ -583,10 +613,36 @@ static void writeLoadable(const char *outputFileName, uint8_t *Program, int prog
fwrite(header, 1, sizeof(header), outputFile);
fwrite(Program + codeBase, 1, (size_t)codeLength, outputFile);
fwrite(Data + dataBase, 1, (size_t)dataLength, outputFile);
// The vectors, last, so that everything before them sits where a version one loader
// already expects to find it.
for (int i = 0; i < vectorArrayCount; i++) {
if (vectorArray[i].declaredOnly) {
continue;
}
if (vectorArray[i].base == SOFTWARE_VECTOR_BASE
&& vectorArray[i].index == VECTOR_BOOT) {
continue;
}
uint16_t slot = vectorArray[i].base + (uint16_t)vectorArray[i].index * VECTOR_ENTRY_BYTES;
fputc((slot >> 8) & 0xFF, outputFile);
fputc(slot & 0xFF, outputFile);
fputc((vectorArray[i].handler >> 8) & 0xFF, outputFile);
fputc(vectorArray[i].handler & 0xFF, outputFile);
}
fclose(outputFile);
printf("Successfully wrote SplitBit loadable program to \"%s\".\n", outputFileName);
printf(GREEN " Code: %d bytes at 0x%04X.\n Data: %d bytes at 0x%04X.\n Total size: %d bytes.\n" RESET,
codeLength, codeBase, dataLength, dataBase, SBEX_HEADER_BYTES + codeLength + dataLength);
printf(GREEN " Code: %d bytes at 0x%04X.\n Data: %d bytes at 0x%04X.\n" RESET,
codeLength, codeBase, dataLength, dataBase);
if (entry != codeBase) {
printf(GREEN " Starts at 0x%04X.\n" RESET, entry);
}
if (installed > 0) {
printf(GREEN " Vectors: %d. Version 2, so a loader that cannot install them will say so.\n" RESET,
installed);
}
printf(GREEN " Total size: %d bytes.\n" RESET,
SBEX_HEADER_BYTES + codeLength + dataLength + installed * SBEX_VECTOR_ENTRY_BYTES);
}
void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCount, uint8_t *Data, int dataCount) {
+5 -1
View File
@@ -11,7 +11,11 @@
#include <ctype.h>
#include "Assm-util.h"
#define MAX_LABELS 256
// Every label in a program and in everything it includes shares one table, because they
// share one namespace: a name may only be defined once across the whole assembly. So this
// is not the size of one file but the size of a program and its libraries together, and
// CosmOS with its four libraries went past 256 while still being a small system.
#define MAX_LABELS 1024
#define MAX_VECTORS 256
typedef struct {
+35 -1
View File
@@ -146,7 +146,41 @@ Message:
Every label inside is then already the address it will have once the program is loaded, so a branch or a SETD written in it points at the right place. Giving either segment a base makes the whole program a loadable one, and the assembler writes it out with a header saying where its two pieces go, followed by the pieces themselves. The space below each base is not in the file: the header says where the bytes belong and the loader puts them there.
A program starts at its code base. One that wants to begin somewhere else puts a branch at its first instruction, which costs three bytes and needs nothing from the format.
A program starts at its code base unless it says otherwise, and `Boot` in its Vector Segment is how it says otherwise:
```
#Program
#Base 0x2000
helpers:
...
start:
...
#Vectors
Boot start ; Which is where this program begins.
```
That fills in the entry point in the header. It is not installed as vector 0 of the machine, which is where everything begins at power on and no business of a program being loaded into a system that is already running.
### Vectors In A Loadable Program:
Everything else in a Vector Segment is carried in the file and installed by whatever loads the program. That is what lets a loaded program be interrupted: a handler is an address in the vector table, and until the format could carry one, a program that was not the one the machine booted from had no way to ask for it.
```
#Vectors
Boot start
Device 0x00 keyHandler ; The console, which now interrupts this program.
```
A program carrying vectors is written out as version two of the format, and the assembler says so:
```
Vectors: 1. Version 2, so a loader that cannot install them will say so.
```
A program carrying none stays version one and loads anywhere. The difference matters because a loader that does not understand version two refuses the file rather than running a program with its handlers missing, which would work until the moment it was supposed to be interrupted and then fail somewhere with nothing pointing back at the cause.
Whoever loads the program is expected to take the vectors out again when it finishes. See the Programming Manual.
The address a program is assembled for has to be the address it is loaded at. Nothing checks that, and nothing can fix it: a program put anywhere else has every branch and every SETD inside it pointing somewhere wrong.
+190 -5
View File
@@ -454,9 +454,9 @@ Status bit 1 says the last operation failed: there is no disk, or the block aske
A disk error is not a fault. Faults on this machine mean it cannot continue, and a read that fails is an ordinary thing that happens to working programs on failing media. It is reported so that a program can cope with it, rather than stopping the machine and taking the choice away.
## Reading The Filesystem:
## Reading And Writing The Filesystem:
The disk knows blocks and nothing else, so a filesystem is software. Programs/CosmOS/Source/sbfs.asm reads one.
The disk knows blocks and nothing else, so a filesystem is software. Programs/CosmOS/Source/sbfs.asm is one.
| Routine | Does |
| --- | --- |
@@ -467,11 +467,35 @@ The disk knows blocks and nothing else, so a filesystem is software. Programs/Co
| sbfsNext | Steps the walk to the next entry in use. Q is zero if there was one. |
| sbfsCreate | Makes a file. DP0 names it, and SbfsFileBlocks with SbfsFileTail say how big it is. Q is zero if it was made, and then SbfsFileStart says where it went. |
| sbfsWriteFile | Writes the file that was made, from Data Memory at DP1. |
| sbfsDelete | DP0 names a file. Frees its entry and its blocks. Q is zero if it went. |
| sbfsRename | DP0 is the name a file has, DP1 the name it should have. Q is zero if it was renamed. Refused if something already answers to the new name. |
| sbfsSaveFile | DP0 names the file, DP1 is the data, and SbfsFileBlocks with SbfsFileTail say how big it now is. Writes it whether or not it was there before, and whatever size it used to be. |
Finding a file and listing what is there are different jobs. sbfsFind searches for one name; sbfsFirst and sbfsNext walk the whole directory, stopping on each entry that is in use and stepping over the free ones. A walk keeps a directory block in SbfsBuffer between calls, so anything else that goes to the disk in the middle of one ends it: take what is wanted out of an entry before asking the disk for anything else.
A file's size is settled when it is made, because nothing can grow one afterwards. Files are laid down contiguously, so the block after a file usually belongs to somebody else. A program that does not know how much it will write has to guess high and accept the slack, or build its output elsewhere and make the file once the size is known.
### Saving Something Twice:
Which is why saving a document is not the same as writing a file, and why sbfsSaveFile exists rather than each tool doing it. A file that has grown will usually not fit where it was, so saving it means putting it somewhere else and letting go of where it was — and **the obvious order is a trap**:
```
delete the old one
make a new one <- refused, and the old one is already gone
write it
```
A create can be refused for want of a run long enough even on a disk with plenty of free blocks, because free blocks are only useful to a contiguous file when they are next to each other. Done in that order, the first fragmented disk somebody meets eats their work. sbfsSaveFile does it the other way round:
```
make a temporary nothing is lost if there is nowhere to put it
write it
delete the original only now, once the new one is safely down
rename the temporary
```
**That is what renaming is for.** It looks like a convenience and it is the safety mechanism: it is the only one of the three operations that moves no data — a name lives in the directory entry, so renaming writes twenty two bytes into one block — which makes it the only one that can be left until last and relied on not to fail.
Finding room is a walk through the directory rather than a lookup, because there is no allocation table. With files laid down contiguously the directory already says which blocks are spoken for, and a second copy of that would be a second thing to keep right. The free count in the superblock is kept up to date but it is a note rather than the truth: it can be worked out again from the directory, and the directory is the one to believe.
A file's length is its block count times 256 plus its tail, which is the same as putting the block count in the high byte and the tail in the low one. Nothing pads a file out, so the bytes after the end of one are whatever else happened to be in that block, and it is the reading program's business to stop where the tail says.
@@ -519,6 +543,68 @@ Every routine names the Data Pointer it works through rather than assuming there
readLine cuts a line short if it is longer than the buffer, and then reads the rest of it and throws it away, so that what is left over does not turn up as the next line. ConsoleEndOfInput is set if the console ran out instead of ending a line, and it is cleared at the start of every call, so it always describes the last line read. That is a different thing from an empty line, and a program reading until there is no more has to be able to tell the two apart.
## What A Program May Ask The System For:
A loaded program is on its own hardware and can do anything the machine can do — it is a fence, not a wall. But the things it usually wants are things the system is already doing, and asking is both shorter and the only way to reach code that was assembled separately. `CALL` needs a label, and a label has to be in the same assembly; `SWI` needs only a number both sides agree on.
Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, which both the system and the program include. Neither side ever types a number.
| Service | Does |
| --- | --- |
| osPrintString | DP0 names a string ending in a zero byte. Prints it. |
| osReadLine | DP0 names somewhere to put a line, B says how much room there is. Reads one from the console. Q comes back holding how long it was. |
| osExit | Gives the machine back. Does not return. |
| osArgument | DP0 names somewhere to put whatever followed the run command, B says how much room there is. |
| osFileRead | DP0 names a file, DP1 says where to put it. Q is zero if it read, and DP3 comes back holding how many bytes there were. |
| osFileSave | DP0 names a file, DP1 is the bytes, A and B together are how many. Q is zero if it saved, whether or not it was there before. |
| osFileDelete | DP0 names a file. Q is zero if it went. |
| osFileRename | DP0 is the name a file has, DP1 the name it should have. Q is zero if it moved. |
| osPrintNumber | A and B together are a number. Prints it in decimal, without leading zeroes. |
```
#Include services.asm
...
SETD.0 Message
SWI osPrintString
```
### The Disk Without A Filesystem:
A program that wants a file does not need to know what a filesystem is. Before these existed it had to include the whole of `sbfs.asm` — 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.
There is no service to mount one, and that is not an omission. The system mounts the disk before it reads its first prompt, and there is one disk with one buffer registered as one bank; a program mounting it again was only ever an artefact of owning a second copy of the library. That call disappears rather than moving.
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, and going out it is A and B together. Neither direction needs a record in memory whose shape both sides have to agree on.
A file of 256 blocks or more is refused by `osFileRead` rather than partly read, because 64K will not fit in Data Memory and its length will not fit in the pointer that reports it. A length that lies would be worse than a file that will not open.
`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.
`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.
A handler is entered with the caller's registers exactly as they were, because an interrupt frame is pushed rather than cleared. That is why a service can be given a pointer in DP0 and a count in B without any of it being copied anywhere first.
### How A Service Answers:
The same thing that makes an interrupt safe makes a service mute. RETI restores every register from the frame, so whatever a handler worked out is thrown away on the way out — which is exactly right for a device interrupting at a moment nobody chose, and useless for a service that was asked a question.
A service answers by **writing into its own frame**, over the saved register, and letting RETI put it back. MVSD copies the Stack Pointer into a Data Pointer and the frame sits just above it, so returning a byte in Q is three instructions:
```
answer:
INIA 0d42
MVSD.1
DPUP.1 0d02 ; The saved Q. See the frame table under Interrupts.
STA.1
RETI
```
**Which registers a service may answer in is the convention CALL already has: Q and DP3.** A subroutine cannot hand back A, B or Data Pointers 0 to 2 because RET puts them back; a service *could* write over any of them and should not, for exactly the reason that list exists. A caller is entitled to find what it kept still there.
**Only the handler itself can do this.** The offsets are from wherever the Stack Pointer is, and a CALL moves it by ten — so a routine called by a handler that tried the same thing would be writing into its own return address. The poke belongs inline, next to the RETI.
A service that has nothing to say does nothing, and the caller's registers arrive back untouched. That is worth knowing from the other side too: a service cannot corrupt a register by accident, only by deciding to.
## Loading A Program From A Disk:
A program that was not the one the machine booted from carries sixteen bytes in front of it saying where it belongs.
@@ -526,19 +612,31 @@ A program that was not the one the machine booted from carries sixteen bytes in
| Offset | Size | Holds |
| --- | --- | --- |
| 0 | 4 | SBEX |
| 4 | 1 | Version. One. |
| 5 | 1 | Reserved. |
| 4 | 1 | Version. One, or two if it brings vectors. |
| 5 | 1 | How many vectors follow the data. Zero in a version one file. |
| 6 | 2 | Where the code goes in Program Memory. |
| 8 | 2 | Where to start running. |
| 10 | 2 | How many bytes of code there are. |
| 12 | 2 | Where the data goes in Data Memory. |
| 14 | 2 | How many bytes of data there are. |
| 16 | | The code, and then the data. |
| 16 | | The code, then the data, then the vectors. |
Programs/loader.asm reads one off a disk, puts the two pieces where the header asks, and jumps to the entry with BRD. Every part of that already existed: the filesystem finds the file, the memory controller writes Program Memory, and BRD turns an address worked out at run time into somewhere to go. The header is the only new thing. Programs/CosmOS does the same as one of its commands, and then takes the machine back afterwards, which the standalone loader has no way to do.
The magic matters for the same reason it does everywhere else on this machine. Without it, loading a text file would put nonsense into Program Memory and then jump into it.
### Bringing Vectors:
A program that only wants to be run needs nothing here and says version one. A program that wants a handler installed needs something of whoever loads it, and says version two.
Each vector is four bytes: the address of the slot in the vector table, then the address to put in it, both most significant byte first. Naming the slot rather than the vector number means the loader does no arithmetic and does not have to know where either vector table begins, and one entry can be a software or a hardware vector without saying which it is.
**A version two file is refused by a loader that cannot install them.** That is the point of the version rather than an inconvenience of it. A program whose handlers were quietly dropped would load, run, and then go wrong somewhere with nothing to connect the failure back to loading — a game waiting for keys that no longer arrive. Failing once, at load, with a reason, is worth more than running.
**Whoever installs them takes them back.** A vector points into the program that supplied it, so one left in the table after that program has gone aims an interrupt at whatever occupies those addresses next. CosmOS keeps its own copy of what a program brought, puts them in when the program is run and not when it is loaded, and restores what was underneath them when the program gives the machine back. Restoring, rather than clearing: a program is allowed to install a handler over one the system was already using, and when it goes, what it covered up has to come back rather than become a hole.
`Boot` in a loadable program fills in the entry field, since that is what it means, and is not installed as vector 0 — where the machine starts is not a loaded program's business. Without one, a program begins at the first byte of its code.
### Where A Program Says It Lives:
**Nothing relocates anything.** A program is put exactly where its header asks, and that has to be the address it was assembled for, or every branch and every SETD inside it points somewhere wrong.
@@ -559,6 +657,93 @@ That is not general placement: a base applies to a whole segment, and only the f
Whoever does the loading keeps its own code and data below the addresses the loaded program claims. That is an arrangement between the two of them rather than anything the machine enforces. Programs/CosmOS is where that arrangement is written down as a memory map and kept to.
## Programs That Come With The System:
`Programs/CosmOS/Apps` holds what the shell can load. Several are old programs written for the bare machine that needed five edits each to become loadable ones — the Fibonacci and sieve programs, `greet`, and `hello`. The rest were written for the system as it is now, and each of those exists to show one thing working:
| Program | What it is for |
| --- | --- |
| Life | Conway's Game of Life, which had to be taught to stop, since a program that never ends takes the shell with it. Polls the console between generations. |
| 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. |
| Edit | A line editor. |
### The Monitor:
The monitor is **part of the shell**, not a program the shell loads, and that is the whole reason it works. A loaded program occupies the one place a loaded program goes, so a monitor that was an application could never look at any other application: loading the thing you wanted to inspect would replace the thing doing the inspecting.
`monitor` turns it on and the prompt changes from `>` to `*`. It is **a mode, not a detour** — the shell's own commands still work, and the mode persists until you say otherwise:
```
> load Snake.sbx
> monitor
* d 2000
2000 47 00 11 00 SETD.0 1100
* b data
bank 01
* x 1000
* exit
>
```
**A program giving the machine back lands at the prompt it was started from**, so `g` into something, letting it run, and having it exit puts you back at `*` rather than at the shell. That falls out of the mode being a variable the prompt reads rather than a second loop: every way back to the prompt goes through one place, including `osExit`. Looking at a program and running it therefore do not interrupt each other, which is the thing a monitor is for.
`exit` leaves whatever you are in — the monitor if you are in it, the machine if you are not.
| | |
| --- | --- |
| `x [addr]` | Sixty-four bytes, as hex and as characters |
| `d [addr]` | Eight instructions, disassembled |
| `s addr b b …` | Put those bytes there |
| `b program\|data\|n` | Which bank to look at |
| `g addr` | Go there |
`x` and `d` share one cursor and each leaves it past what it showed, so without an address either carries on — reading through memory is one letter at a time, and you can switch between bytes and instructions without retyping where you are. `s` deliberately does not move it.
Everything else here does something; the monitor looks at what the others did. It shows memory as hex and as characters, disassembles it, writes bytes into it, and jumps to an address — all through the memory controller, which is the only thing that can reach Program Memory.
That is why a monitor is worth more on this machine than on most. Data Memory a program can already read for itself with a Data Pointer. The half it cannot see is Program Memory, and that is the half its bugs are in.
**Its instruction table is generated from the assembler's**, by `Tests/instructiontable.py`, and checked against it by `Tests/docs.sh` — along with a second check that the lengths that table implies are the ones the manual's own Bytes column prints. Both matter for the same reason: a disassembler that disagreed about how long an instruction is would not print one line wrong, it would lose its place and print everything after it wrong. Which is what a disassembler does anyway when it starts in the middle of an instruction, and is worth seeing once so it is recognised later.
**Where to put something you typed in yourself** is a question the monitor answers, because the answer moves every time the monitor is rebuilt. `m` says where its own two segments end, and those are the first free addresses:
```
> m
code from 2000, free from 2607
data from 1000, free from 1367 up to the stack
```
Which is what makes the monitor's real trick possible — a program that no assembler ever saw:
```
> s 8000 26 48 D1 00 26 49 D1 00 26 0A D1 00 18 12
> d 8000
8000 26 48 INIA 48
8002 D1 00 OUTA 00
8004 26 49 INIA 49
...
800C 18 12 SWI 12
> g 8000
HI
```
Typed in as bytes, checked by disassembling it back, and run. It ends with `SWI osExit`, which is how it gives the machine to the shell rather than to nothing.
There are no breakpoints yet, and `g` does not come back. The machinery for both already exists and nothing has used it: SplitBit has 192 undecodable bytes, and an invalid opcode dispatches through the `BadOpcode` vector carrying **the address of the offending byte**. A breakpoint is a spare byte written over an instruction and a handler waiting for it.
### The Editor:
`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 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.
Saving goes through `sbfsSaveFile`, so a document that has grown is written somewhere else and the original is only let go of once the new one is safely down. That is the whole reason the editor was written: not because the machine needed an editor, but because every tool that produces a file needs the same four operations, and building them for one imaginary tool is how they end up wrong.
## Refusing:
A device can refuse what it was asked to do. This is not the same as interrupting. An interrupt is a device asking for attention later, answered between instructions once the CPU is ready. A refusal is a device saying no to the instruction happening now, so the machine stops where it stands rather than carrying on as though the access had worked.
+128 -1
View File
@@ -131,6 +131,35 @@ else:
problems.append("the Assembler Manual does not say the automatic vectors start"
" at %d" % autoFrom)
# ---- The loadable header table matches the offsets the assembler writes ----
#
# The Programming Manual prints the header field by field, which is the description two
# implementations work from. sbex.h is where the offsets actually are, so a field moved
# there and not here would leave the manual describing a format nobody writes.
sbex = read("Source/Assembler/sbex.h")
offsets = {name: int(value)
for name, value in re.findall(r'^#define (SBEX_[A-Z_]+_AT)\s+(\d+)$', sbex, re.M)}
if "## Loading A Program From A Disk:" not in pm:
problems.append("the Programming Manual has lost its loadable program section")
else:
loading = pm.split("## Loading A Program From A Disk:")[1].split("\n## ")[0]
listed = [int(m) for m in re.findall(r'^\| (\d+) \| \d* \|', loading, re.M)]
for name, offset in sorted(offsets.items(), key=lambda pair: pair[1]):
if offset not in listed:
problems.append("%s is at offset %d and the header table has no row for it"
% (name, offset))
# Spelled as a word, the way these manuals write small numbers in prose.
asWord = {1: "one", 2: "two", 3: "three", 4: "four", 5: "five"}
for version in ("SBEX_VERSION", "SBEX_VERSION_VECTORS"):
number = re.search(r'^#define %s\s+(\d+)$' % version, sbex, re.M)
if not number:
problems.append("%s is gone from sbex.h" % version)
continue
said = asWord.get(int(number.group(1)))
if said is None or said not in loading.lower():
problems.append("the loadable program section does not mention version %s (%s)"
% (number.group(1), said))
# ---- 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
@@ -150,6 +179,104 @@ else:
problems.append("%s is bit %d of the console status port and The Console does"
" not mention it" % (name, bit))
# ---- Every service the system offers has a row ----
#
# 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")
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]
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))):
if directive not in am:
@@ -160,7 +287,7 @@ for directive in sorted(set(re.findall(r'"(#[A-Za-z]+)"', util))):
# 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"),
for heading, library in [("## Reading And Writing 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"
+1 -1
View File
@@ -1,3 +1,3 @@
0000 0001 0001 0002 0003 0005 0008 000D 0015 0022 0037 0059 0090 00E9 0179 0262 03DB 063D 0A18 1055 1A6D 2AC2 452F 6FF1
Execution halted after 2534 cycles.
Execution halted.
[exit 0]
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -222,5 +222,5 @@
Execution stopped after 3000000 cycles. (cycle limit reached)
Execution stopped. (cycle limit reached)
[exit 0]
+1 -1
View File
@@ -222,5 +222,5 @@
Execution stopped after 3000000 cycles. (cycle limit reached)
Execution stopped. (cycle limit reached)
[exit 0]
+1 -1
View File
@@ -1,3 +1,3 @@
00000000 00000001 00000001 00000002 00000003 00000005 00000008 0000000D 00000015 00000022 00000037 00000059 00000090 000000E9 00000179 00000262 000003DB 0000063D 00000A18 00001055 00001A6D 00002AC2 0000452F 00006FF1 0000B520 00012511 0001DA31 0002FF42 0004D973 0007D8B5 000CB228 00148ADD 00213D05 0035C7E2 005704E7 008CCCC9 00E3D1B0 01709E79 02547029 03C50EA2 06197ECB 09DE8D6D 0FF80C38 19D699A5 29CEA5DD 43A53F82 6D73E55F
Execution halted after 10610 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,3 +1,3 @@
0 1 1 2 3 5 8 13 21 34 55 89 144 233
Execution halted after 1506 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,3 +1,3 @@
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251
Execution halted after 54061 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,4 +1,4 @@
Fault: The device on port 233 refused the access at Program Address 0x001D, and nothing is installed to deal with it.
O
Execution halted after 16 cycles.
Execution halted.
[exit 1]
+1 -1
View File
@@ -3,5 +3,5 @@ blitted
DE AD
untouched
ABABCDEFGH
Execution halted after 464 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,4 +1,4 @@
QAB C qab c
9876543210
Execution halted after 176 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -3,5 +3,5 @@ asked for: 0D ready keys interrupts
what arrived:
keys
at the end: 02 ended
Execution halted after 1597 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -6,5 +6,5 @@ hi
at the end: 06 ended keys
line mode: 02 ended
Execution halted after 891 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -10,5 +10,5 @@ lines read:
[SplitBit] 8
[a line that is f] 16
end of input
Execution halted after 6064 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,5 +1,5 @@
DE AD BE EF
01 FF
00 00
Execution halted after 315 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -2,5 +2,5 @@ AA BB
readonly
nobank
done
Execution halted after 245 cycles.
Execution halted.
[exit 0]
+6 -5
View File
@@ -1,11 +1,12 @@
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>
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
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
@@ -21,5 +22,5 @@ aName22CharactersLong! 22
12 files
> > I do not know: frobnicate
> halted
Execution halted after 12376 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -19,5 +19,5 @@ CosmOS
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.
Execution halted.
[exit 0]
+21
View File
@@ -0,0 +1,21 @@
CosmOS
> loaded, starting at 2000
> poem.txt, 0 lines
> : : : : > : : > 1: alpha
2: INSERTED
3: beta
4: gamma
> : > > 1: CHANGED
2: INSERTED
3: beta
> written, 22 bytes
> finished
> poem.txt, 3 lines
> 1: CHANGED
2: INSERTED
3: beta
> there is no such line
> finished
> halted
Execution halted.
[exit 0]
+17
View File
@@ -0,0 +1,17 @@
CosmOS
> one.txt 13
two.txt 14
2 files
> renamed
> first.txt 13
two.txt 14
2 files
> gone
> two.txt 14
1 file
> no such file
> rename what to what?
> there is no such file, or that name is taken
> halted
Execution halted.
[exit 0]
+1 -1
View File
@@ -5,5 +5,5 @@ finished
> Hello, World!
finished
> halted
Execution halted after 2668 cycles.
Execution halted.
[exit 0]
+19
View File
@@ -0,0 +1,19 @@
CosmOS
> loaded, starting at 2000
> keys, by interrupt. q stops.
ab
the console has been handed back
finished
> > keys, by interrupt. q stops.
cd
the console has been handed back
finished
> > 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
Execution halted.
[exit 0]
+1 -1
View File
@@ -869,5 +869,5 @@ the board has settled
finished
>
halted
Execution halted after 11671206 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -21,5 +21,5 @@ stopped
finished
> >
halted
Execution halted after 26219 cycles.
Execution halted.
[exit 0]
+31
View File
@@ -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]
+1 -1
View File
@@ -3,5 +3,5 @@ no filesystem on the disk
> no filesystem on the disk
>
halted
Execution halted after 636 cycles.
Execution halted.
[exit 0]
+4 -2
View File
@@ -4,8 +4,10 @@ CosmOS
hello.sbx 52
Life.sbx 1411
Snake.sbx 2175
Keys.sbx 663
Say.sbx 155
notes.txt 21
5 files
7 files
> load what?
> no such file
> not a program
@@ -17,5 +19,5 @@ finished
what should I call you? hello, Claude. that is all I do.
finished
> halted
Execution halted after 12778 cycles.
Execution halted.
[exit 0]
+11
View File
@@ -0,0 +1,11 @@
CosmOS
> loaded, starting at 2000
> nothing was said
finished
> it says: notes.txt
finished
> it says: a longer thing with spaces
finished
> halted
Execution halted.
[exit 0]
+12
View File
@@ -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]
+1 -1
View File
@@ -289,5 +289,5 @@ you ran into something
finished
>
halted
Execution halted after 1910669 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,3 +1,3 @@
ABCZ
Execution halted after 21 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,5 +1,5 @@
O
K
!
Execution halted after 19 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,5 +1,5 @@
04
04
06
Execution halted after 114 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,5 +1,5 @@
00
from the disk
02
Execution halted after 245 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -2,5 +2,5 @@ one
two
three
done
Execution halted after 126 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,4 +1,4 @@
O
K
Execution halted after 17 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,3 +1,3 @@
Fault: 0xFE at Program Address 0x0002 is not an instruction.
Execution halted after 2 cycles.
Execution halted.
[exit 1]
+1 -1
View File
@@ -4,5 +4,5 @@ caught 01FC
read ok
lowered ok
fenced 05
Execution halted after 535 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,3 +1,3 @@
Hello, World!
Execution halted after 70 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,4 +1,4 @@
Input Test: Will echo anything you put in.
Hello SplitBit
Execution halted after 406 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,4 +1,4 @@
Input Test: Will echo anything you put in.
Hello SplitBit
Execution halted after 406 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,4 +1,4 @@
00 00 00 00 01
1
Execution halted after 4707 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,5 +1,5 @@
ready
trap
device
Execution halted after 105 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,3 +1,3 @@
OKC
Execution halted after 19 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,4 +1,4 @@
loader
loaded off a disk, with a string and a loop of its own
Execution halted after 1098 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,5 +1,5 @@
before
loaded
back
Execution halted after 132 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,5 +1,5 @@
Fault: The device on port 16 interrupted at Program Address 0x0019, and hardware vector 16 has no handler installed.
M
S
Execution halted after 16 cycles.
Execution halted.
[exit 1]
+1 -1
View File
@@ -1,3 +1,3 @@
0000 0032
Execution halted after 1134 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,3 +1,3 @@
AAA
Execution halted after 24 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,3 +1,3 @@
00 31
Execution halted after 78 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -3,5 +3,5 @@ twenty
sixty three
automatic
done
Execution halted after 272 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,5 +1,5 @@
Hello
World
H
Execution halted after 79 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,4 +1,4 @@
Hello, World!
42 is the great answer.
Execution halted after 295 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -9,5 +9,5 @@ Testing printByteHex...
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F 80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F 90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF
Testing complete!
Execution halted after 71185 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,4 +1,4 @@
Fault: The device on port 17 refused the access at Program Address 0x000A, and nothing is installed to deal with it.
O
Execution halted after 6 cycles.
Execution halted.
[exit 1]
+1 -1
View File
@@ -1,5 +1,5 @@
caught
caught
done
Execution halted after 136 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -3,5 +3,5 @@ flags 01
5A 5A
refused
refused
Execution halted after 443 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -2,5 +2,5 @@
10 10 00
FF 01 00
05 00 00
Execution halted after 440 cycles.
Execution halted.
[exit 0]
+8
View File
@@ -0,0 +1,8 @@
here.txt 0002 already here
doc.txt 0003 first draft
doc.txt 0004 a second draft, which is longer than the first
notes.txt 0004 a second draft, which is longer than the first
doc.txt gone
notes.txt gone
Execution halted.
[exit 0]
+1 -1
View File
@@ -3,5 +3,5 @@ across.txt 0002 BC ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHI
aName22CharactersLong! 0000 16 exactly twenty two!!!!
empty.txt 0000 00
absent.txt missing
Execution halted after 19230 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -11,5 +11,5 @@ across.txt 700
empty.txt 0
aName22CharactersLong! 22
files: 12
Execution halted after 9498 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,5 +1,5 @@
here.txt 0002 already here
first.txt 0003 written by SplitBit itself
second.txt 0004 and a second one after it
Execution halted after 5888 cycles.
Execution halted.
[exit 0]
+5
View File
@@ -0,0 +1,5 @@
quiet: 7
answer: 42
pointer: ABC
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,3 +1,3 @@
OK
Execution halted after 14 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -5,5 +5,5 @@ reclaimed: FFFF
still good: 33
borrowed: 7 and 9
back home: FFFF
Execution halted after 1054 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -2,5 +2,5 @@ one
two
three
AFTER
Execution halted after 122 cycles.
Execution halted.
[exit 0]
+3
View File
@@ -0,0 +1,3 @@
ADD OR and NOP
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,4 +1,4 @@
Fault: Software vector 20, dispatched from Program Address 0x0008, has no handler installed.
O
Execution halted after 5 cycles.
Execution halted.
[exit 1]
+1 -1
View File
@@ -4,5 +4,5 @@ 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.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,3 +1,3 @@
Two pointers, no stack shenanigans.
Execution halted after 395 cycles.
Execution halted.
[exit 0]
+1 -1
View File
@@ -1,5 +1,5 @@
OK!
good
AFTER
Execution halted after 77 cycles.
Execution halted.
[exit 0]
+22
View File
@@ -0,0 +1,22 @@
load Edit.sbx
run poem.txt
a
alpha
beta
gamma
.
i 2
INSERTED
.
l
c 1
CHANGED
d 4
l
w
q
run poem.txt
l
d 99
q
exit
+9
View File
@@ -0,0 +1,9 @@
dir
rename one.txt first.txt
dir
delete first.txt
dir
delete first.txt
rename two.txt
rename two.txt two.txt
exit
+3
View File
@@ -0,0 +1,3 @@
load Files.sbx
run
exit
+10
View File
@@ -0,0 +1,10 @@
load Keys.sbx
run
abq
run
cdq
monitor
b program
x fe00
exit
exit
+18
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
load Say.sbx
run
run notes.txt
run a longer thing with spaces
exit

Some files were not shown because too many files have changed in this diff Show More