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