Fixed assembler bug that caused crash on IR array resize. Added line editor app.

This commit is contained in:
Anachronaut
2026-08-17 23:26:21 -04:00
parent 1d1a14318c
commit e3100b4718
87 changed files with 2201 additions and 74 deletions
+829
View File
@@ -0,0 +1,829 @@
; 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 sbfsMount
BNQ noDisk
CALL loadFile
SETD.0 FileName
CALL printString
SETD.0 CommaText
CALL printString
CALL countLines
MVQA
CALL printByteDecimal
SETD.0 LinesText
CALL printString
CALL newLine
commandLoop:
SETD.0 PromptText
CALL printString
SETD.0 Command
INIB 0d40
CALL readLine
; Running out of typing ends it, the same way it ends the shell.
SETD.0 ConsoleEndOfInput
LDA.0
BNA 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
CALL printString
CALL newLine
BRI commandLoop
quit:
SWI osExit
noName:
SETD.0 NoNameText
CALL printString
CALL newLine
SWI osExit
noDisk:
SETD.0 NoDiskText
CALL printString
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
CALL printString
SETD.0 Entry
INIB 0d80
CALL readLine
SETD.0 ConsoleEndOfInput
LDA.0
BNA 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
CALL printString
CALL newLine
BRI commandLoop
doChange:
SETD.0 Wanted
LDA.0
BRA insertNeedsLine
CALL findLine
BNQ noSuchLine
SETD.0 EnteringText
CALL printString
SETD.0 Entry
INIB 0d80
CALL readLine
SETD.0 ConsoleEndOfInput
LDA.0
BNA 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
CALL printString
CALL newLine
BRI commandLoop
doWrite:
CALL writeFile
BNQ writeFailed
SETD.0 WrittenText
CALL printString
SETD.0 WroteSize
CALL printWordDecimal
SETD.0 BytesText
CALL printString
CALL newLine
BRI commandLoop
writeFailed:
SETD.0 NoWriteText
CALL printString
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 printByteDecimal
SETD.0 ColonText
CALL printString
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
CALL sbfsFind
BNQ loadNothing
SETD.1 0x40 0x00
CALL sbfsRead
BNQ loadNothing
; How many bytes came back: the block count is the high byte of the length and the tail
; is the low one, which is how a size is put together everywhere on this disk.
SETD.0 SbfsFileBlocks
DPUP.0 0d01
LDA.0
SETD.1 ReadLeft
STA.1
SETD.0 SbfsFileTail
LDA.0
SETD.1 ReadLeft
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
SETD.0 WroteSize
LDA.0
INIB 0x40
CCF
SUB
MVQA
SETD.0 SbfsFileBlocks
RSTB
STB.0
INCD.0
STA.0
SETD.0 WroteSize
INCD.0
LDA.0
SETD.0 SbfsFileTail
STA.0
; The size the file is about to be, said in bytes, before saving changes what these mean.
SETD.0 WroteSize
LDA.0
INIB 0x40
CCF
SUB
MVQA
SETD.0 WroteSize
STA.0
SETD.0 FileName
SETD.1 0x40 0x00
CALL sbfsSaveFile
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
PromptText:
"> "
EnteringText:
": "
ColonText:
": "
CommaText:
", "
LinesText:
" lines"
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>"
NoDiskText:
"there is no disk"
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 sbfs.asm
#Include text.asm
#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
+169 -1
View File
@@ -109,6 +109,16 @@ prompt:
CALL textSame
BRQ doDump
SETD.0 CommandLine
SETD.1 DeleteName
CALL textSame
BRQ doDelete
SETD.0 CommandLine
SETD.1 RenameName
CALL textSame
BRQ doRename
SETD.0 CommandLine
SETD.1 HelpName
CALL textSame
@@ -195,7 +205,16 @@ dirDone:
SETD.0 DirSeen
LDA.0
CALL printByteDecimal
; One file is not one files. Cheap to get right and it reads as carelessness otherwise.
SETD.0 DirSeen
LDA.0
DECA
BRA dirOne
SETD.0 FilesText
BRI dirCount
dirOne:
SETD.0 FileText
dirCount:
CALL printString
CALL newLine
BRI prompt
@@ -470,6 +489,84 @@ loadComplain:
CALL newLine
BRI prompt
; ---- delete and rename ----
;
; The two things a disk needs that reading and writing do not provide, and the two that
; anything editing a document will want from the shell as well as from a program. Deleting
; frees an entry and its blocks; renaming changes twenty two bytes and moves nothing.
doDelete:
SETD.0 DiskReady
LDA.0
BRA fileNoDisk
SETD.1 TextRest
LDD.0.1
LDA.0
BRA deleteWhat
CALL sbfsDelete
BNQ deleteFailed
SETD.0 Deleted
CALL printString
CALL newLine
BRI prompt
deleteWhat:
SETD.0 DeleteWhat
BRI fileComplain
deleteFailed:
SETD.0 NoSuchFile
BRI fileComplain
doRename:
SETD.0 DiskReady
LDA.0
BRA fileNoDisk
SETD.1 TextRest
LDD.0.1
LDA.0
BRA renameWhat
; Two names, so the rest of the line is split again. textSplit writes a zero over the
; space it cuts at, so what was one string becomes two without anything being copied.
SETD.1 TextRest
LDD.0.1
SETD.1 RenameFrom
STD.0.1
CALL textSplit
SETD.1 TextRest
LDD.1.1
LDA.1
BRA renameWhat ; Only one name was given, and this needs both.
SETD.2 RenameFrom
LDD.0.2
CALL sbfsRename
BNQ renameFailed
SETD.0 Renamed
CALL printString
CALL newLine
BRI prompt
renameWhat:
SETD.0 RenameWhat
BRI fileComplain
renameFailed:
; Either there is no such file or the new name is already taken. Which of the two is not
; worth another message: both mean the disk does not have room for that name to move.
SETD.0 RenameNo
BRI fileComplain
fileNoDisk:
SETD.0 NoDisk
fileComplain:
CALL printString
CALL newLine
BRI prompt
; ---- run ----
;
; Hands the machine to whatever was loaded. Where the Stack is now is written down first,
@@ -485,6 +582,15 @@ doRun:
SETD.1 SystemStack
STD.0.1
; Whatever followed the word "run" is kept where the program can ask for it. Copied
; rather than pointed at, because what it is pointing at is the line the shell typed
; into, and a program is entitled to outlive the shell's opinion of that.
SETD.1 TextRest
LDD.0.1
SETD.1 RunArgument
INIB 0d64
CALL copyText
CALL installVectors
; The entry address is a number until BRD makes it a place. DP3 is the one to build it
@@ -499,6 +605,26 @@ runNothing:
CALL newLine
BRI prompt
; DP0 is a string, DP1 is where it should go, and B is how much room there is counting
; the zero on the end. What does not fit is left behind, and what is written is a string
; either way.
copyText:
BRB copyTextDone ; No room at all, so nothing is written, not even the zero.
copyTextLoop:
DECB
BRB copyTextEnd ; Only room for the terminator now.
LDA.0
STA.1
BRA copyTextDone
INCD.0
INCD.1
BRI copyTextLoop
copyTextEnd:
RSTA
STA.1
copyTextDone:
RET
; ---- Putting a program's vectors in, and taking them out again ----
;
; The vector table lives in Program Memory, which no instruction can write, so both of
@@ -635,6 +761,20 @@ handleReadLine:
CALL readLine
RETI
; What the program was asked to work on. DP0 says where to put it and B how much room
; there is, counting the zero on the end, which is the same bargain readLine offers.
;
; Being asked for rather than left at an agreed address is deliberate. The two sides of
; this already have to agree on a vector number and nothing else, and that number is
; written down once in services.asm; an address would be a second thing to agree about, in
; a memory map that is a convention rather than anything enforced.
handleArgument:
PSHD.0
POPD.1
SETD.0 RunArgument
CALL copyText
RETI
; Giving the machine back. This is the one place MVDS earns its keep. The program's Stack,
; and the frame this very interrupt arrived on, are both abandoned where they lie, because
; nothing is going to return through either of them.
@@ -893,13 +1033,17 @@ Farewell:
"halted"
FilesText:
" files"
FileText:
" file"
; Two strings rather than one, because a string literal stops at 255 characters and each
; one carries its own zero byte, so they are printed in turn rather than joined.
HelpText:
"dir list what is on the disk
load <file> read a program off the disk
run start what was loaded"
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"
HelpMoreText:
"dump sixty four bytes of memory, and again for more
dump <program|data|bank> <address>
@@ -923,6 +1067,16 @@ LoadWhat:
"load what?"
NoSuchFile:
"no such file"
Deleted:
"gone"
Renamed:
"renamed"
DeleteWhat:
"delete what?"
RenameWhat:
"rename what to what?"
RenameNo:
"there is no such file, or that name is taken"
TooManyVectors:
"that program wants more vectors than there is room for"
Unreadable:
@@ -944,6 +1098,10 @@ LoadName:
"load"
RunName:
"run"
DeleteName:
"delete"
RenameName:
"rename"
HelpName:
"help"
ExitName:
@@ -960,6 +1118,15 @@ LoadCount:
LoadVersion:
0x00
; Where the first of rename's two names is, kept while the second is picked out of the
; line, since finding that needs the pointers for itself.
RenameFrom:
0x00 0x00
; What followed "run", kept for the program to ask for.
RunArgument:
#Reserve 0d64
; ---- The vectors a loaded program brought with it ----
;
; Six bytes each: where it goes, what goes there, and what was there before. The last two
@@ -1013,4 +1180,5 @@ CommandLine:
osPrintString handlePrintString
osReadLine handleReadLine
osExit handleExit
osArgument handleArgument
Device 0x20 diskDone
+226
View File
@@ -991,6 +991,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:
@@ -1050,5 +1252,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
+1
View File
@@ -24,3 +24,4 @@
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.
+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.