Compare commits
58
Commits
12cb489268
...
79727044b7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79727044b7 | ||
|
|
7b28f48f52 | ||
|
|
89c667848b | ||
|
|
87d819847e | ||
|
|
b4206673a6 | ||
|
|
f1cc2e56b2 | ||
|
|
61a80ae13d | ||
|
|
c8c9f0b363 | ||
|
|
cd5f548736 | ||
|
|
ce2a2cd7e6 | ||
|
|
dc74149321 | ||
|
|
546f336823 | ||
|
|
54ff7196c9 | ||
|
|
c312853f8e | ||
|
|
82adeeb193 | ||
|
|
d07b23f90b | ||
|
|
612bd1b97c | ||
|
|
0a2965bc63 | ||
|
|
9c144469b4 | ||
|
|
e1273337c4 | ||
|
|
c146d98588 | ||
|
|
8f4cc5878d | ||
|
|
2b079324ae | ||
|
|
c216c83e12 | ||
|
|
c3188ed657 | ||
|
|
6b41354f8f | ||
|
|
c74075dc51 | ||
|
|
ce0f18f4ef | ||
|
|
634650cab9 | ||
|
|
2b5506ee70 | ||
|
|
0c240f7ad3 | ||
|
|
ce8fb721fe | ||
|
|
d6cbbb5034 | ||
|
|
9e2aa0122e | ||
|
|
aa7bdc6acd | ||
|
|
db3d349da8 | ||
|
|
d4cba36c5e | ||
|
|
e0cf0a9a25 | ||
|
|
f1e5cc46f6 | ||
|
|
54f5cfe8a4 | ||
|
|
a7d3e09d94 | ||
|
|
19ab36a201 | ||
|
|
af0360128b | ||
|
|
00d896e3e7 | ||
|
|
fb7b224bbb | ||
|
|
9f7dffdeca | ||
|
|
7cd5e34347 | ||
|
|
ec56439d9a | ||
|
|
06bdbf7728 | ||
|
|
da91a36d92 | ||
|
|
36ce9f6ccf | ||
|
|
588e02aff5 | ||
|
|
36a1b07b5b | ||
|
|
78e9eef472 | ||
|
|
f4fb56606e | ||
|
|
2b0aeeefd4 | ||
|
|
dbe58db660 | ||
|
|
4fd8bf7b3f |
@@ -7,6 +7,7 @@
|
|||||||
/Assembler
|
/Assembler
|
||||||
/SplitBit
|
/SplitBit
|
||||||
/SplitDisk
|
/SplitDisk
|
||||||
|
/SplitLint
|
||||||
/CLAUDE.md
|
/CLAUDE.md
|
||||||
/claudeResume.sh
|
/claudeResume.sh
|
||||||
/codexResume.sh
|
/codexResume.sh
|
||||||
@@ -26,3 +27,4 @@ __pycache__/
|
|||||||
|
|
||||||
# Kate leaves these beside a file it has open.
|
# Kate leaves these beside a file it has open.
|
||||||
.*.kate-swp
|
.*.kate-swp
|
||||||
|
Source/Emulator/rom.c
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
; bare.asm
|
||||||
|
; A machine with no operating system on it at all.
|
||||||
|
;
|
||||||
|
; THIS IS THE POINT OF LOADING AN ORDINARY IMAGE. The second stage does not know what
|
||||||
|
; CosmOS is; it knows what a boot image is, and this is one. So a program that wants the
|
||||||
|
; whole machine to itself - no shell, no filesystem, no services - is not a special case
|
||||||
|
; needing a special path. It is a file, written under CosmOS like any other, and started by
|
||||||
|
; naming it in /System/Boot/boot.cfg.
|
||||||
|
;
|
||||||
|
; It owns everything from address zero upward, and the only thing it can be sure of is the
|
||||||
|
; console, because that is all it asked for.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
SETD.0 Said
|
||||||
|
sayLoop:
|
||||||
|
LDA.0
|
||||||
|
BRA sayDone
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.0
|
||||||
|
BRI sayLoop
|
||||||
|
sayDone:
|
||||||
|
HALT
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
Said:
|
||||||
|
"bare metal: no system, just this
|
||||||
|
"
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot start
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
; slotData.asm
|
||||||
|
; A boot slot payload that arranges its own Data Segment.
|
||||||
|
;
|
||||||
|
; Stage one places Program Memory and nothing else, because knowing where a payload's data
|
||||||
|
; ends and its code begins would mean knowing a format, and knowing formats is the thing
|
||||||
|
; ROM must do as little of as possible. So a payload that wants initialised data arranges
|
||||||
|
; it: a loadable image is written into the slot as code followed by data, which leaves the
|
||||||
|
; data image sitting in Program Memory just past the code, and the first thing the payload
|
||||||
|
; does is blit it down to where it was assembled for.
|
||||||
|
;
|
||||||
|
; This is how the real second stage will get the Data Segment that sbfs.asm needs.
|
||||||
|
;
|
||||||
|
; THE PADDING IS NOT DECORATION. The blit needs a length, the assembler will not work out
|
||||||
|
; a difference between two labels, so the segment is padded to a round number and that
|
||||||
|
; number is what gets copied. Reserve one byte too many and the last byte does not arrive:
|
||||||
|
; the first draft of this padded to 257 and copied 256.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
#Program
|
||||||
|
#Base 0xC000
|
||||||
|
|
||||||
|
start:
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE0 ; SourceBank: Program Memory, where stage one put everything.
|
||||||
|
SETD.0 codeEnd
|
||||||
|
PSHD.0
|
||||||
|
POPA
|
||||||
|
POPB
|
||||||
|
OUTB 0xE1
|
||||||
|
OUTA 0xE2
|
||||||
|
INIA 0d1
|
||||||
|
OUTA 0xE3 ; DestBank: Data Memory.
|
||||||
|
SETD.0 Greeting
|
||||||
|
PSHD.0
|
||||||
|
POPA
|
||||||
|
POPB
|
||||||
|
OUTB 0xE4
|
||||||
|
OUTA 0xE5
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0xE6
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE7 ; A round 256 bytes, which the segment is padded to.
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0xE8
|
||||||
|
|
||||||
|
SETD.0 Greeting
|
||||||
|
sayLoop:
|
||||||
|
LDA.0
|
||||||
|
BRA sayDone
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.0
|
||||||
|
BRI sayLoop
|
||||||
|
sayDone:
|
||||||
|
HALT
|
||||||
|
|
||||||
|
codeEnd:
|
||||||
|
|
||||||
|
#Data
|
||||||
|
#Base 0x3000
|
||||||
|
|
||||||
|
Greeting:
|
||||||
|
"a payload with data of its own
|
||||||
|
"
|
||||||
|
Padding:
|
||||||
|
#Reserve 0d224
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
; slotTest.asm
|
||||||
|
; Something small to put in a boot slot, so that the chain can be proved end to end.
|
||||||
|
;
|
||||||
|
; NO DATA SEGMENT, and that is not tidiness - it is the shape of what stage one can do.
|
||||||
|
; Stage one reads raw blocks into Program Memory and jumps to the first byte. It places no
|
||||||
|
; data, because it has no way to know where a payload's data ends and its code begins
|
||||||
|
; without knowing a format, and knowing a format is the thing ROM must do as little of as
|
||||||
|
; possible. So a payload either carries no initialised data or arranges its own.
|
||||||
|
;
|
||||||
|
; Which is why this prints with INIA and OUTA rather than from a string. The real stage two
|
||||||
|
; needs a Data Segment, and how it gets one is an open question written up beside this.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Program
|
||||||
|
#Base 0xC000
|
||||||
|
|
||||||
|
here:
|
||||||
|
INIA 0x62 ; "b"
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x6F ; "o"
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x6F ; splitlint[redundant-assignment]: a second "o", spelled out like the rest
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x74 ; "t"
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x65 ; "e"
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x64 ; "d"
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
; stage1.asm
|
||||||
|
; The first thing the machine runs. Reads the boot area off the disk and jumps into it.
|
||||||
|
;
|
||||||
|
; THIS IS THE PART THAT ONE DAY CANNOT BE CHANGED. It is written to go in ROM, so
|
||||||
|
; everything it knows has to be a thing that is true forever: which port the disk is on,
|
||||||
|
; that a SplitBit disk begins with its own name, and where two numbers sit in the block
|
||||||
|
; that name is in. It does not know what a file is, what a directory is, or that SBFS has
|
||||||
|
; versions. All of that lives in the boot area, on the disk, where it can be replaced.
|
||||||
|
;
|
||||||
|
; The test to apply to any line added here is the only test that matters for ROM: am I
|
||||||
|
; certain this is right forever? A loader that could find /System/cosmos.bin by name would
|
||||||
|
; be friendlier and would freeze the filesystem format in silicon.
|
||||||
|
;
|
||||||
|
; It is an ordinary boot image for now, so it can be run and tested with everything that
|
||||||
|
; already exists. Nothing about it changes when it moves into ROM except who puts it in
|
||||||
|
; memory.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
; ---- What is known forever ----
|
||||||
|
;
|
||||||
|
; Disk: 0x20 block high, 0x21 block low, 0x22 command, 0x23 status
|
||||||
|
; Controller: 0xE0 source bank, 0xE1/0xE2 source, 0xE3 dest bank, 0xE4/0xE5 dest,
|
||||||
|
; 0xE6/0xE7 count, 0xE8 command
|
||||||
|
; Banks: 0 Program Memory, 1 Data Memory, 3 the disk's buffer once registered
|
||||||
|
|
||||||
|
start:
|
||||||
|
; The disk's buffer becomes bank 3. Memory a device brings is reachable only through the
|
||||||
|
; controller, so this is what makes the block readable at all.
|
||||||
|
INIA 0d3
|
||||||
|
OUTA 0xE3
|
||||||
|
INIA 0x20
|
||||||
|
OUTA 0xE2
|
||||||
|
INIA 0x03
|
||||||
|
OUTA 0xE8
|
||||||
|
|
||||||
|
; Block 0, the superblock.
|
||||||
|
RSTA
|
||||||
|
SETD.0 BlockHigh
|
||||||
|
STA.0
|
||||||
|
SETD.0 BlockLow
|
||||||
|
STA.0
|
||||||
|
RCAL readBlock
|
||||||
|
BNQ bootFailed
|
||||||
|
|
||||||
|
; "SBFS", or there is nothing here to boot from. Four bytes, compared where they landed.
|
||||||
|
SETD.2 ScratchAt
|
||||||
|
LDD.0.2
|
||||||
|
SETD.2 DiskMagic
|
||||||
|
INIA 0d4
|
||||||
|
SETD.1 Counter
|
||||||
|
STA.1
|
||||||
|
magicLoop:
|
||||||
|
LDA.0
|
||||||
|
LDB.2
|
||||||
|
XOR
|
||||||
|
BNQ bootFailed
|
||||||
|
INCD.0
|
||||||
|
INCD.2
|
||||||
|
SETD.1 Counter
|
||||||
|
LDA.1
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
BNA magicLoop
|
||||||
|
|
||||||
|
; Two numbers, at fixed offsets in the block whose name has just been checked: how many
|
||||||
|
; blocks a boot slot holds, and which of the two slots to start from.
|
||||||
|
SETD.2 ScratchAt
|
||||||
|
LDD.0.2
|
||||||
|
DPUP.0 0d14
|
||||||
|
LDA.0
|
||||||
|
BNA bootFailed ; A high byte means a slot larger than this will ever read.
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
BRA bootFailed ; No boot area at all, so this disk cannot be started.
|
||||||
|
SETD.1 SlotBlocks
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
SETD.2 ScratchAt
|
||||||
|
LDD.0.2
|
||||||
|
DPUP.0 0d16
|
||||||
|
LDA.0
|
||||||
|
|
||||||
|
; The first block of the live slot. Slot 0 begins at block 1 and slot 1 begins a whole
|
||||||
|
; slot later, so the only arithmetic is an addition and there is nothing to multiply.
|
||||||
|
BRA slotZero
|
||||||
|
SETD.0 SlotBlocks
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
BRI slotFound
|
||||||
|
slotZero:
|
||||||
|
INIA 0d1
|
||||||
|
slotFound:
|
||||||
|
SETD.0 BlockLow
|
||||||
|
STA.0
|
||||||
|
RSTA
|
||||||
|
SETD.0 BlockHigh
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
; And where it goes. Stage two lives above everything the system will occupy, so that
|
||||||
|
; loading the system does not walk over the loader while it is still running.
|
||||||
|
SETD.0 StageHigh
|
||||||
|
INIA 0xC0
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
readLoop:
|
||||||
|
RCAL readBlock
|
||||||
|
BNQ bootFailed
|
||||||
|
|
||||||
|
; The block, out of the disk's buffer and into Program Memory where it will be run.
|
||||||
|
INIA 0d3
|
||||||
|
OUTA 0xE0
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE1
|
||||||
|
OUTA 0xE2
|
||||||
|
RSTA ; splitlint[redundant-assignment]: a bank number, not the address above
|
||||||
|
OUTA 0xE3 ; DestBank: Program Memory.
|
||||||
|
SETD.0 StageHigh
|
||||||
|
LDA.0
|
||||||
|
OUTA 0xE4
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE5
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0xE6
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE7 ; A whole block.
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0xE8
|
||||||
|
|
||||||
|
; On to the next block, and the next page of Program Memory to put it in.
|
||||||
|
SETD.0 StageHigh
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
SETD.0 BlockLow
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
BNA blockStepped
|
||||||
|
SETD.0 BlockHigh
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
blockStepped:
|
||||||
|
|
||||||
|
SETD.0 SlotBlocks
|
||||||
|
LDA.0
|
||||||
|
DECA
|
||||||
|
STA.0
|
||||||
|
BNA readLoop
|
||||||
|
|
||||||
|
; Into it. Nothing is checked about what was read, because there is nothing here that
|
||||||
|
; could check it: what a valid stage two looks like is stage two's business, and a ROM
|
||||||
|
; that knew would be a ROM that could be wrong about it later.
|
||||||
|
SETD.0 StageStart
|
||||||
|
LDD.1.0
|
||||||
|
BRD.1
|
||||||
|
|
||||||
|
; ---- Reading the block the two block registers name ----
|
||||||
|
;
|
||||||
|
; RCAL rather than CALL because what it hands back is Q, and an ordinary call would put
|
||||||
|
; back the registers this leaves its answer in.
|
||||||
|
readBlock:
|
||||||
|
SETD.0 BlockHigh
|
||||||
|
LDA.0
|
||||||
|
OUTA 0x20
|
||||||
|
SETD.0 BlockLow
|
||||||
|
LDA.0
|
||||||
|
OUTA 0x21
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x22 ; Read.
|
||||||
|
|
||||||
|
waitDisk:
|
||||||
|
INA 0x23
|
||||||
|
INIB 0x01
|
||||||
|
AND
|
||||||
|
BRQ readDone ; The busy bit is down, so there is nothing to wait for.
|
||||||
|
WAIT
|
||||||
|
BRI waitDisk
|
||||||
|
readDone:
|
||||||
|
; The error bit, which is the whole of what can go wrong down here.
|
||||||
|
INIB 0x02
|
||||||
|
AND
|
||||||
|
BNQ readBad
|
||||||
|
|
||||||
|
; And into Data Memory, where the CPU can look at it.
|
||||||
|
INIA 0d3
|
||||||
|
OUTA 0xE0
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE1
|
||||||
|
OUTA 0xE2
|
||||||
|
INIA 0d1
|
||||||
|
OUTA 0xE3 ; DestBank: Data Memory.
|
||||||
|
INIA 0x80
|
||||||
|
OUTA 0xE4
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE5
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0xE6
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE7
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0xE8
|
||||||
|
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD ; Q is zero: the block is in Scratch.
|
||||||
|
RRET
|
||||||
|
readBad:
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RRET
|
||||||
|
|
||||||
|
; ---- When there is nothing to start ----
|
||||||
|
;
|
||||||
|
; One character and a stop. A ROM has no room for an explanation and nowhere to put one:
|
||||||
|
; the console is the only thing it can be sure of, and even that only in the sense that
|
||||||
|
; writing to a port nobody is listening to costs nothing.
|
||||||
|
bootFailed:
|
||||||
|
INIA 0x3F ; "?"
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
DiskMagic:
|
||||||
|
"SBFS"
|
||||||
|
|
||||||
|
BlockHigh:
|
||||||
|
0x00
|
||||||
|
BlockLow:
|
||||||
|
0x00
|
||||||
|
SlotBlocks:
|
||||||
|
0x00
|
||||||
|
Counter:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
; Where stage two is being written, a page at a time, and where it begins. The high byte is
|
||||||
|
; stepped as the blocks go by; the low byte is always zero, because a block is a page.
|
||||||
|
StageHigh:
|
||||||
|
0xC0
|
||||||
|
StageStart:
|
||||||
|
0xC0 0x00
|
||||||
|
|
||||||
|
; Where the superblock, and then each block of the boot area, lands on its way past.
|
||||||
|
;
|
||||||
|
; AN ADDRESS RATHER THAN STORAGE. Reserving it, or aligning to it, would put thirty two
|
||||||
|
; kilobytes of zeroes into a file that is going to be a ROM - which is what the assembler's
|
||||||
|
; own scratch map exists to avoid, for the same reason. Two bytes here say where; nothing
|
||||||
|
; carries what.
|
||||||
|
ScratchAt:
|
||||||
|
0x80 0x00
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot start
|
||||||
@@ -0,0 +1,698 @@
|
|||||||
|
; stage2.asm
|
||||||
|
; The second stage. Finds the system on the disk and starts it.
|
||||||
|
;
|
||||||
|
; Stage one knows nothing about filesystems and never will, because it is going into a ROM
|
||||||
|
; and a ROM that knew SBFS would freeze the format in silicon. This is where that knowledge
|
||||||
|
; lives instead: on the disk, in a boot slot, replaceable by writing blocks.
|
||||||
|
;
|
||||||
|
; ---- What it loads ----
|
||||||
|
;
|
||||||
|
; An ordinary SplitBit boot image, the same SPBT file the emulator has always been handed.
|
||||||
|
; That is the whole trick and it was the user's: a second stage that loads the machine's
|
||||||
|
; NORMAL image format is not a boot-specific mechanism at all. Bare metal SplitBit stops
|
||||||
|
; being a special case - a program that wants no operating system is just an image, written
|
||||||
|
; under CosmOS like any other, and startable because it is a file.
|
||||||
|
;
|
||||||
|
; ---- How it gets its own data ----
|
||||||
|
;
|
||||||
|
; Stage one places Program Memory and nothing else. A loadable image is written into the
|
||||||
|
; slot as code followed by data, so this program's Data Segment is sitting in Program Memory
|
||||||
|
; just past its own code when it starts, and the first thing it does is blit it down. See
|
||||||
|
; slotData.asm, which proves the mechanism on its own.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
; Above everything the system will occupy, so that loading the system does not walk over
|
||||||
|
; the loader while it is still running.
|
||||||
|
#Base 0xC000
|
||||||
|
|
||||||
|
start:
|
||||||
|
; ---- The Data Segment, fetched from just past the code ----
|
||||||
|
;
|
||||||
|
; A ROUND FOUR KILOBYTES rather than the exact size. The blit needs a length, the
|
||||||
|
; assembler will not work out the difference between two labels, and a hand kept number
|
||||||
|
; is a number that goes wrong quietly - slotData.asm padded to 257 and copied 256, and
|
||||||
|
; the byte that never arrived happened to be padding. Copying more than there is costs
|
||||||
|
; nothing: the source runs into slot padding, and the destination is nobody's memory.
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE0 ; SourceBank: Program Memory, where stage one put everything.
|
||||||
|
SETD.0 codeEnd
|
||||||
|
PSHD.0
|
||||||
|
POPA
|
||||||
|
POPB
|
||||||
|
OUTB 0xE1
|
||||||
|
OUTA 0xE2
|
||||||
|
INIA 0d1
|
||||||
|
OUTA 0xE3 ; DestBank: Data Memory.
|
||||||
|
SETD.0 StageDataBase
|
||||||
|
PSHD.0
|
||||||
|
POPA
|
||||||
|
POPB
|
||||||
|
OUTB 0xE4
|
||||||
|
OUTA 0xE5
|
||||||
|
INIA 0x10
|
||||||
|
OUTA 0xE6
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE7 ; Four kilobytes.
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0xE8
|
||||||
|
|
||||||
|
; ---- Now there is data, so there can be words ----
|
||||||
|
SETD.0 StageName
|
||||||
|
RCAL say
|
||||||
|
|
||||||
|
CALL sbfsMount
|
||||||
|
BNQ noFilesystem
|
||||||
|
|
||||||
|
; ---- What to start ----
|
||||||
|
;
|
||||||
|
; Read before the image is, because the configuration is staged where the image will go:
|
||||||
|
; there is one large free area down here and no reason to have two.
|
||||||
|
; ---- Something to start just this once ----
|
||||||
|
;
|
||||||
|
; A file naming an image to run instead of the configured system, and then to forget
|
||||||
|
; about. It is the same format as boot.cfg and is read the same way, because a second
|
||||||
|
; format for one setting would be a second format.
|
||||||
|
;
|
||||||
|
; CONSUMED BY BEING READ, not by working. It is deleted before the jump, which is the
|
||||||
|
; only moment there is: after the jump this program does not exist. That also means a
|
||||||
|
; one shot that hangs cannot hang twice - the request is already gone, and the mark on
|
||||||
|
; the disk brings the ordinary system back.
|
||||||
|
SETD.0 OncePath
|
||||||
|
SETD.2 StageAt
|
||||||
|
LDD.1.2
|
||||||
|
INIA 0d4
|
||||||
|
CALL cfgLoad
|
||||||
|
|
||||||
|
SETD.0 KeySystem
|
||||||
|
CALL cfgGet
|
||||||
|
BNQ noOnce
|
||||||
|
|
||||||
|
SETD.1 BootName
|
||||||
|
SETD.2 CfgValue
|
||||||
|
LDD.0.2
|
||||||
|
RCAL copyString
|
||||||
|
|
||||||
|
; Gone before it is used, so that whatever happens next happens only once.
|
||||||
|
SETD.0 OncePath
|
||||||
|
CALL sbfsDelete
|
||||||
|
|
||||||
|
SETD.0 OnceText
|
||||||
|
RCAL say
|
||||||
|
SETD.0 BootName
|
||||||
|
RCAL say
|
||||||
|
RCAL newLine
|
||||||
|
|
||||||
|
; ---- AND THE MARK IS NOT TOUCHED ----
|
||||||
|
;
|
||||||
|
; A one shot is already self limiting: the request was deleted a moment ago, so whatever
|
||||||
|
; happens now, the next start reads boot.cfg like any other. Marking it as well would
|
||||||
|
; report every successful bare metal boot as a start that never arrived - which is what
|
||||||
|
; the first version did, because a program that owns the whole machine has no filesystem
|
||||||
|
; to clear a mark with and is not doing anything wrong by not having one.
|
||||||
|
SETD.0 BootName
|
||||||
|
RCAL tryImage
|
||||||
|
|
||||||
|
; It did not start, and there is nothing to fall back to that was asked for. Whatever
|
||||||
|
; boot.cfg says is the thing to try, so carry on into it.
|
||||||
|
SETD.0 OnceFailedText
|
||||||
|
RCAL say
|
||||||
|
|
||||||
|
noOnce:
|
||||||
|
SETD.0 ConfigPath
|
||||||
|
SETD.2 StageAt
|
||||||
|
LDD.1.2
|
||||||
|
INIA 0d4
|
||||||
|
CALL cfgLoad
|
||||||
|
|
||||||
|
; Said before anything is started, so that a setting somebody meant which did not happen
|
||||||
|
; is on the screen above whatever happened instead.
|
||||||
|
SETD.0 KnownKeys
|
||||||
|
CALL cfgCheck
|
||||||
|
|
||||||
|
SETD.0 KeySystem
|
||||||
|
CALL cfgGet
|
||||||
|
SETD.1 BootName
|
||||||
|
BNQ systemDefault
|
||||||
|
SETD.2 CfgValue
|
||||||
|
LDD.0.2
|
||||||
|
RCAL copyString
|
||||||
|
BRI systemChosen
|
||||||
|
systemDefault:
|
||||||
|
SETD.0 SystemName
|
||||||
|
RCAL copyString
|
||||||
|
systemChosen:
|
||||||
|
|
||||||
|
; And what to fall back to, kept now because the configuration is about to be written
|
||||||
|
; over by the image itself.
|
||||||
|
RSTA
|
||||||
|
SETD.0 HaveFallback
|
||||||
|
STA.0
|
||||||
|
SETD.0 KeyFallback
|
||||||
|
CALL cfgGet
|
||||||
|
BNQ noFallbackSet
|
||||||
|
SETD.1 FallbackName
|
||||||
|
SETD.2 CfgValue
|
||||||
|
LDD.0.2
|
||||||
|
RCAL copyString
|
||||||
|
INIA 0x01
|
||||||
|
SETD.0 HaveFallback
|
||||||
|
STA.0
|
||||||
|
noFallbackSet:
|
||||||
|
|
||||||
|
; ---- How the last start went ----
|
||||||
|
;
|
||||||
|
; Settled means the last one arrived, so try what the configuration asks for. Trying
|
||||||
|
; means the loader handed over last time and nothing came back to say it got there, so
|
||||||
|
; the thing named is what broke the machine and the fallback is the way out. Fell back
|
||||||
|
; means that already happened and nobody has settled it since, so do the same again
|
||||||
|
; rather than retrying a system that is known not to start.
|
||||||
|
CALL sbfsBootState
|
||||||
|
SETD.0 SbfsStateWas
|
||||||
|
LDA.0
|
||||||
|
BRA stateSettled
|
||||||
|
INIB 0d1
|
||||||
|
XOR
|
||||||
|
BRQ stateFailedLast
|
||||||
|
SETD.0 StillBackText
|
||||||
|
RCAL say
|
||||||
|
BRI useFallback
|
||||||
|
|
||||||
|
stateFailedLast:
|
||||||
|
SETD.0 DidNotArriveText
|
||||||
|
RCAL say
|
||||||
|
INIA 0d2
|
||||||
|
CALL sbfsSetBootState
|
||||||
|
BRI useFallback
|
||||||
|
|
||||||
|
stateSettled:
|
||||||
|
; The mark goes down BEFORE the jump, because after it there is nothing here to do it.
|
||||||
|
; What clears it is the system reaching its prompt, which is not a claim that the system
|
||||||
|
; works - it is the moment a person can tell it otherwise.
|
||||||
|
INIA 0d1
|
||||||
|
CALL sbfsSetBootState
|
||||||
|
|
||||||
|
SETD.0 BootName
|
||||||
|
RCAL tryImage
|
||||||
|
|
||||||
|
; It did not start. Whatever went wrong has already said so, and if there is something
|
||||||
|
; else to try then trying it is the whole reason for having said it rather than stopping.
|
||||||
|
SETD.0 FallbackText
|
||||||
|
RCAL say
|
||||||
|
|
||||||
|
useFallback:
|
||||||
|
SETD.0 HaveFallback
|
||||||
|
LDA.0
|
||||||
|
BRA noFallbackTryAgain
|
||||||
|
SETD.0 FallbackName
|
||||||
|
RCAL tryImage
|
||||||
|
BRI noSystem
|
||||||
|
|
||||||
|
noFallbackTryAgain:
|
||||||
|
; NOTHING TO FALL BACK TO, so try what was asked for anyway rather than stopping. With no
|
||||||
|
; second name the mark is the only thing standing between the machine and its own
|
||||||
|
; configuration, and refusing on the strength of it would turn "the last start failed"
|
||||||
|
; into "no start is permitted", which is worse than the problem it was added to solve.
|
||||||
|
;
|
||||||
|
; A failure that was passing recovers here. One that is not leaves the machine exactly
|
||||||
|
; where it would have been without any of this, which is the most that can be promised
|
||||||
|
; when there is only one thing to start.
|
||||||
|
SETD.0 NoFallbackText
|
||||||
|
RCAL say
|
||||||
|
SETD.0 BootName
|
||||||
|
RCAL tryImage
|
||||||
|
BRI noSystem
|
||||||
|
|
||||||
|
; ---- Starting one particular image ----
|
||||||
|
;
|
||||||
|
; DP0 names it. Returns only if it could not be started, having said why; everything that
|
||||||
|
; works ends in the jump at the bottom and never comes back.
|
||||||
|
tryImage:
|
||||||
|
SETD.1 TryName
|
||||||
|
STD.0.1
|
||||||
|
CALL sbfsFind
|
||||||
|
BNQ tryMissing
|
||||||
|
|
||||||
|
; ---- The image, read whole into somewhere nothing else is using ----
|
||||||
|
SETD.2 StageAt
|
||||||
|
LDD.1.2
|
||||||
|
CALL sbfsRead
|
||||||
|
BNQ unreadable
|
||||||
|
|
||||||
|
; "SPBT", or this is a file rather than something to start.
|
||||||
|
SETD.2 StageAt
|
||||||
|
LDD.0.2
|
||||||
|
SETD.2 BootMagic
|
||||||
|
INIA 0d4
|
||||||
|
RCAL matchTag
|
||||||
|
BNQ notAnImage
|
||||||
|
|
||||||
|
; The format version, and then four bytes of feature flags. A flag set is an image
|
||||||
|
; asking for something this machine may not have, and the honest answer to a request
|
||||||
|
; that cannot be understood is to refuse rather than to run it anyway.
|
||||||
|
LDA.0
|
||||||
|
INIB 0d1
|
||||||
|
XOR
|
||||||
|
BNQ wrongVersion
|
||||||
|
INCD.0
|
||||||
|
INIA 0d4
|
||||||
|
RCAL allZero
|
||||||
|
BNQ wantsMore
|
||||||
|
|
||||||
|
; "PRG", its length, and then the code itself straight into Program Memory.
|
||||||
|
SETD.2 ProgTag
|
||||||
|
INIA 0d3
|
||||||
|
RCAL matchTag
|
||||||
|
BNQ notAnImage
|
||||||
|
RCAL takeCount
|
||||||
|
RSTA
|
||||||
|
RCAL placeSegment
|
||||||
|
|
||||||
|
; "DAT", the same again into Data Memory.
|
||||||
|
SETD.2 DataTag
|
||||||
|
INIA 0d3
|
||||||
|
RCAL matchTag
|
||||||
|
BNQ notAnImage
|
||||||
|
RCAL takeCount
|
||||||
|
INIA 0d1
|
||||||
|
RCAL placeSegment
|
||||||
|
|
||||||
|
; ---- "VEC", which an image written before vectors existed simply does not have ----
|
||||||
|
;
|
||||||
|
; So running out of file here is the ordinary case rather than a fault: what follows is
|
||||||
|
; the end of a perfectly good image, and the entry point is then the address zero.
|
||||||
|
SETD.2 VecTag
|
||||||
|
INIA 0d3
|
||||||
|
RCAL matchTag
|
||||||
|
BNQ noVectors
|
||||||
|
RCAL takeCount
|
||||||
|
|
||||||
|
vectorLoop:
|
||||||
|
SETD.2 CountHigh
|
||||||
|
LDA.2
|
||||||
|
INCD.2
|
||||||
|
LDB.2
|
||||||
|
OR
|
||||||
|
BRQ vectorsDone
|
||||||
|
|
||||||
|
; Four bytes: where in Program Memory the vector sits, then what to put there. The
|
||||||
|
; controller writes it, because nothing else can write Program Memory.
|
||||||
|
LDA.0
|
||||||
|
SETD.2 VecWhere
|
||||||
|
STA.2
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
INCD.2
|
||||||
|
STA.2
|
||||||
|
INCD.0
|
||||||
|
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE3 ; DestBank: Program Memory.
|
||||||
|
SETD.2 VecWhere
|
||||||
|
LDA.2
|
||||||
|
OUTA 0xE4
|
||||||
|
INCD.2
|
||||||
|
LDA.2
|
||||||
|
OUTA 0xE5
|
||||||
|
LDA.0
|
||||||
|
OUTA 0xE9 ; A byte straight in, rather than a blit of one.
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
OUTA 0xE9
|
||||||
|
INCD.0
|
||||||
|
|
||||||
|
; THE BOOT VECTOR IS THE ONE WORTH KEEPING. Program Memory cannot be read back, so the
|
||||||
|
; address to start at has to be noticed on its way past rather than looked up after.
|
||||||
|
SETD.2 VecWhere
|
||||||
|
LDA.2
|
||||||
|
INIB 0xFC
|
||||||
|
XOR
|
||||||
|
BNQ vectorStepped
|
||||||
|
INCD.2
|
||||||
|
LDA.2
|
||||||
|
BNA vectorStepped
|
||||||
|
SETD.2 EntryHigh
|
||||||
|
DECD.0
|
||||||
|
DECD.0
|
||||||
|
LDA.0
|
||||||
|
STA.2
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
INCD.2
|
||||||
|
STA.2
|
||||||
|
INCD.0
|
||||||
|
|
||||||
|
vectorStepped:
|
||||||
|
SETD.2 CountLow
|
||||||
|
LDA.2
|
||||||
|
INIB 0d4
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
MVQA
|
||||||
|
STA.2
|
||||||
|
BNC vectorLoop
|
||||||
|
SETD.2 CountHigh
|
||||||
|
LDA.2
|
||||||
|
DECA
|
||||||
|
STA.2
|
||||||
|
BRI vectorLoop
|
||||||
|
|
||||||
|
noVectors:
|
||||||
|
vectorsDone:
|
||||||
|
; And into it. Everything above has been arranging memory; this is the only instruction
|
||||||
|
; that hands the machine over.
|
||||||
|
SETD.0 EntryHigh
|
||||||
|
LDD.1.0
|
||||||
|
BRD.1
|
||||||
|
|
||||||
|
unreadable:
|
||||||
|
SETD.0 UnreadableText
|
||||||
|
RCAL say
|
||||||
|
RCAL sayName
|
||||||
|
RRET
|
||||||
|
notAnImage:
|
||||||
|
SETD.0 NotImageText
|
||||||
|
RCAL say
|
||||||
|
RCAL sayName
|
||||||
|
RRET
|
||||||
|
wrongVersion:
|
||||||
|
SETD.0 VersionText
|
||||||
|
RCAL say
|
||||||
|
RCAL sayName
|
||||||
|
RRET
|
||||||
|
wantsMore:
|
||||||
|
SETD.0 FeatureText
|
||||||
|
RCAL say
|
||||||
|
RCAL sayName
|
||||||
|
RRET
|
||||||
|
|
||||||
|
noFilesystem:
|
||||||
|
SETD.0 NoDiskText
|
||||||
|
RCAL say
|
||||||
|
HALT
|
||||||
|
|
||||||
|
tryMissing:
|
||||||
|
SETD.0 NoSystemText
|
||||||
|
RCAL say
|
||||||
|
RCAL sayName
|
||||||
|
RRET
|
||||||
|
|
||||||
|
; Whichever image was being tried, said after the reason it did not work.
|
||||||
|
sayName:
|
||||||
|
SETD.0 TryName
|
||||||
|
LDD.0.0
|
||||||
|
RCAL say
|
||||||
|
RCAL newLine
|
||||||
|
RRET
|
||||||
|
|
||||||
|
noSystem:
|
||||||
|
SETD.0 NothingText
|
||||||
|
RCAL say
|
||||||
|
HALT
|
||||||
|
|
||||||
|
; DP0 names a string and DP1 where to put it, terminator and all.
|
||||||
|
copyString:
|
||||||
|
LDA.0
|
||||||
|
STA.1
|
||||||
|
BRA copyDone
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
BRI copyString
|
||||||
|
copyDone:
|
||||||
|
RRET
|
||||||
|
|
||||||
|
; ---- Comparing a marker and stepping past it ----
|
||||||
|
;
|
||||||
|
; DP0 is in the image, DP2 names what it should say, A is how many bytes. DP0 is left past
|
||||||
|
; them either way, which is what lets a caller try one marker and carry on.
|
||||||
|
matchTag:
|
||||||
|
SETD.1 TagLeft
|
||||||
|
STA.1
|
||||||
|
tagLoop:
|
||||||
|
SETD.1 TagLeft
|
||||||
|
LDA.1
|
||||||
|
BRA tagSame
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
LDA.0
|
||||||
|
LDB.2
|
||||||
|
XOR
|
||||||
|
BNQ tagDiffers
|
||||||
|
INCD.0
|
||||||
|
INCD.2
|
||||||
|
BRI tagLoop
|
||||||
|
tagSame:
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RRET
|
||||||
|
tagDiffers:
|
||||||
|
; Past the rest of it regardless, so that what DP0 points at does not depend on which
|
||||||
|
; byte happened to differ.
|
||||||
|
SETD.1 TagLeft
|
||||||
|
LDA.1
|
||||||
|
BRA tagDiffersDone
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
INCD.0
|
||||||
|
BRI tagDiffers
|
||||||
|
tagDiffersDone:
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RRET
|
||||||
|
|
||||||
|
; A bytes at DP0, all of which have to be zero. Used for the feature flags, where anything
|
||||||
|
; set is the image asking for something this machine may not be able to give it.
|
||||||
|
allZero:
|
||||||
|
SETD.1 TagLeft
|
||||||
|
STA.1
|
||||||
|
zeroLoop:
|
||||||
|
SETD.1 TagLeft
|
||||||
|
LDA.1
|
||||||
|
BRA zeroAllClear
|
||||||
|
DECA
|
||||||
|
STA.1
|
||||||
|
LDA.0
|
||||||
|
INCD.0
|
||||||
|
BNA zeroNotClear
|
||||||
|
BRI zeroLoop
|
||||||
|
zeroAllClear:
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RRET
|
||||||
|
zeroNotClear:
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RRET
|
||||||
|
|
||||||
|
; Two bytes at DP0, most significant first, into the count that placeSegment and the vector
|
||||||
|
; loop both work through. DP0 is left on the byte after them.
|
||||||
|
takeCount:
|
||||||
|
LDA.0
|
||||||
|
SETD.1 CountHigh
|
||||||
|
STA.1
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
SETD.1 CountLow
|
||||||
|
STA.1
|
||||||
|
INCD.0
|
||||||
|
RRET
|
||||||
|
|
||||||
|
; ---- A segment, out of the staged image and into the memory it belongs in ----
|
||||||
|
;
|
||||||
|
; A is the bank: 0 for Program Memory, 1 for Data. Both segments go to address zero, which
|
||||||
|
; is what a boot image means - it is the format for something that owns the machine.
|
||||||
|
;
|
||||||
|
; DP0 is left past the segment, ready for whatever marker comes next.
|
||||||
|
placeSegment:
|
||||||
|
SETD.1 WantBank
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
INIA 0d1
|
||||||
|
OUTA 0xE0 ; SourceBank: Data Memory, where the image was staged.
|
||||||
|
PSHD.0
|
||||||
|
POPA
|
||||||
|
POPB
|
||||||
|
OUTB 0xE1
|
||||||
|
OUTA 0xE2
|
||||||
|
SETD.1 WantBank
|
||||||
|
LDA.1
|
||||||
|
OUTA 0xE3
|
||||||
|
RSTA
|
||||||
|
OUTA 0xE4
|
||||||
|
OUTA 0xE5 ; To the bottom of it.
|
||||||
|
SETD.1 CountHigh
|
||||||
|
LDA.1
|
||||||
|
OUTA 0xE6
|
||||||
|
INCD.1
|
||||||
|
LDA.1
|
||||||
|
OUTA 0xE7
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0xE8
|
||||||
|
|
||||||
|
; And past it. DPUW steps a pointer by A and B together, which is exactly the shape the
|
||||||
|
; two halves of a length are already in.
|
||||||
|
SETD.1 CountHigh
|
||||||
|
LDA.1
|
||||||
|
INCD.1
|
||||||
|
LDB.1
|
||||||
|
DPUW.0
|
||||||
|
RRET
|
||||||
|
|
||||||
|
; ---- Saying something ----
|
||||||
|
;
|
||||||
|
; Its own rather than console.asm's, because everything this includes has to fit in a boot
|
||||||
|
; slot and the console library brings a great deal this will never use. DP0 names a string.
|
||||||
|
say:
|
||||||
|
LDA.0
|
||||||
|
BRA sayDone
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.0
|
||||||
|
BRI say
|
||||||
|
sayDone:
|
||||||
|
RRET
|
||||||
|
|
||||||
|
newLine:
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
RRET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
; Out of the way of everything: the system's data goes to 0x0000, and whatever image is
|
||||||
|
; being loaded is staged below this.
|
||||||
|
#Base 0xE000
|
||||||
|
|
||||||
|
; The first thing in the segment, so that its address is where the blit above puts the
|
||||||
|
; whole of it.
|
||||||
|
StageDataBase:
|
||||||
|
|
||||||
|
StageName:
|
||||||
|
"stage two
|
||||||
|
"
|
||||||
|
FoundText:
|
||||||
|
"found "
|
||||||
|
NoDiskText:
|
||||||
|
"no filesystem
|
||||||
|
"
|
||||||
|
NoSystemText:
|
||||||
|
"no "
|
||||||
|
|
||||||
|
; What to start. A name for now; the plan is for this to be read out of a file so that any
|
||||||
|
; number of systems can sit on one disk and the choice is an ordinary safe write.
|
||||||
|
; Where the configuration lives, and what to start when it does not say.
|
||||||
|
ConfigPath:
|
||||||
|
"/System/Boot/boot.cfg"
|
||||||
|
OncePath:
|
||||||
|
"/System/Boot/once.cfg"
|
||||||
|
SystemName:
|
||||||
|
"/System/Boot/cosmos.bin"
|
||||||
|
KeySystem:
|
||||||
|
"system"
|
||||||
|
KeyFallback:
|
||||||
|
"fallback"
|
||||||
|
|
||||||
|
; The keys this knows. Anything else in the file is a setting nothing asked for, and
|
||||||
|
; cfgCheck says so rather than letting it look as though it worked.
|
||||||
|
KnownKeys:
|
||||||
|
"system"
|
||||||
|
"fallback"
|
||||||
|
""
|
||||||
|
|
||||||
|
FallbackText:
|
||||||
|
"trying the fallback
|
||||||
|
"
|
||||||
|
DidNotArriveText:
|
||||||
|
"the last start did not arrive
|
||||||
|
"
|
||||||
|
StillBackText:
|
||||||
|
"still on the fallback: settle it to try again
|
||||||
|
"
|
||||||
|
NoFallbackText:
|
||||||
|
"no fallback, so trying it again
|
||||||
|
"
|
||||||
|
NothingText:
|
||||||
|
"nothing to start
|
||||||
|
"
|
||||||
|
OnceText:
|
||||||
|
"just this once: "
|
||||||
|
OnceFailedText:
|
||||||
|
"it did not start, so carrying on
|
||||||
|
"
|
||||||
|
|
||||||
|
BootName:
|
||||||
|
#Reserve 0d128
|
||||||
|
FallbackName:
|
||||||
|
#Reserve 0d128
|
||||||
|
HaveFallback:
|
||||||
|
0x00
|
||||||
|
TryName:
|
||||||
|
0x00 0x00
|
||||||
|
|
||||||
|
UnreadableText:
|
||||||
|
"could not read it
|
||||||
|
"
|
||||||
|
NotImageText:
|
||||||
|
"not a boot image
|
||||||
|
"
|
||||||
|
VersionText:
|
||||||
|
"an image this cannot start
|
||||||
|
"
|
||||||
|
FeatureText:
|
||||||
|
"the image wants more machine
|
||||||
|
"
|
||||||
|
|
||||||
|
BootMagic:
|
||||||
|
"SPBT"
|
||||||
|
ProgTag:
|
||||||
|
"PRG"
|
||||||
|
DataTag:
|
||||||
|
"DAT"
|
||||||
|
VecTag:
|
||||||
|
"VEC"
|
||||||
|
|
||||||
|
TagLeft:
|
||||||
|
0x00
|
||||||
|
WantBank:
|
||||||
|
0x00
|
||||||
|
CountHigh:
|
||||||
|
0x00
|
||||||
|
CountLow:
|
||||||
|
0x00
|
||||||
|
VecWhere:
|
||||||
|
0x00 0x00
|
||||||
|
|
||||||
|
; Where to start once memory is arranged. Zero unless the image says otherwise, which is
|
||||||
|
; what a boot image with no vector table means and what every program written before the
|
||||||
|
; table existed relies on.
|
||||||
|
EntryHigh:
|
||||||
|
0x00
|
||||||
|
EntryLow:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
; Where the image is put while it is being taken apart. Below this program's own data and
|
||||||
|
; above everything the system will occupy. An address rather than storage: reserving it
|
||||||
|
; would put its zeroes in the boot slot.
|
||||||
|
StageAt:
|
||||||
|
0x20 0x00
|
||||||
|
|
||||||
|
#Include sbfs.asm
|
||||||
|
#Include text.asm
|
||||||
|
#Include config.asm
|
||||||
|
|
||||||
|
; ---- The end of everything ----
|
||||||
|
;
|
||||||
|
; Back to the Program Segment so that this label lands past the included code as well as
|
||||||
|
; past the code above. It is where the Data Segment's image begins once stage one has put
|
||||||
|
; the whole slot into Program Memory.
|
||||||
|
#Program
|
||||||
|
codeEnd:
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
; wedged.asm
|
||||||
|
; A system that starts and never reaches a prompt.
|
||||||
|
;
|
||||||
|
; Not a broken program so much as a stand in for one: what matters is that it is handed the
|
||||||
|
; machine and never clears the mark the loader put on the disk, which is what every real
|
||||||
|
; way of failing before the shell has in common.
|
||||||
|
;
|
||||||
|
; Without that mark, pointing /System/Boot/boot.cfg at something like this would be a
|
||||||
|
; machine that could not be told anything ever again - the shell is the only way to change
|
||||||
|
; the file, and the file is what stops the shell from starting.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
SETD.0 Dying
|
||||||
|
sayLoop:
|
||||||
|
LDA.0
|
||||||
|
BRA gone
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.0
|
||||||
|
BRI sayLoop
|
||||||
|
gone:
|
||||||
|
HALT
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
Dying:
|
||||||
|
"a system that never reaches a prompt
|
||||||
|
"
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot start
|
||||||
@@ -17,12 +17,29 @@
|
|||||||
; Correct output is two stops. A and B differ between them, the addresses differ, and the
|
; Correct output is two stops. A and B differ between them, the addresses differ, and the
|
||||||
; Stack Pointer differs too, because the second one is inside a subroutine and a call has
|
; Stack Pointer differs too, because the second one is inside a subroutine and a call has
|
||||||
; put ten bytes down by then.
|
; put ten bytes down by then.
|
||||||
|
;
|
||||||
|
; EVERY POINTER IS SET BEFORE EACH STOP, and the three this program owns are rotated
|
||||||
|
; between the two so that all of them visibly change.
|
||||||
|
;
|
||||||
|
; This program used to leave DP1 and DP2 alone, and what a stop then showed for them was
|
||||||
|
; whatever the shell happened to have left there. That is a real thing about the machine -
|
||||||
|
; a program is handed the pointers as it finds them - but it is not this program's to
|
||||||
|
; demonstrate, and it made what this program prints depend on where CosmOS's code happens
|
||||||
|
; to sit. The recorded output had to be taken again four times in one day's work, every
|
||||||
|
; time for a value nothing should ever depend on.
|
||||||
|
;
|
||||||
|
; A demonstration of what the registers were should show registers somebody chose. Then
|
||||||
|
; every line of it is being asserted rather than merely observed.
|
||||||
|
;
|
||||||
|
; DP3 is deliberately still the system's. It is where the program was entered, which is the
|
||||||
|
; one thing here worth seeing that this program did not choose, and it is steady because it
|
||||||
|
; is this program's own base.
|
||||||
|
|
||||||
#Include services.asm
|
#Include services.asm
|
||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
SETD.0 Banner
|
SETD.0 Banner
|
||||||
@@ -31,6 +48,8 @@ start:
|
|||||||
INIA 0d17
|
INIA 0d17
|
||||||
INIB 0d34
|
INIB 0d34
|
||||||
SETD.0 Marker
|
SETD.0 Marker
|
||||||
|
SETD.1 Banner
|
||||||
|
SETD.2 DoneText
|
||||||
SWI osBreak
|
SWI osBreak
|
||||||
|
|
||||||
; The second stop is inside a subroutine, so that the Stack Pointer is visibly not where
|
; The second stop is inside a subroutine, so that the Stack Pointer is visibly not where
|
||||||
@@ -39,18 +58,21 @@ start:
|
|||||||
|
|
||||||
SETD.0 DoneText
|
SETD.0 DoneText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
|
RSTA
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
deeper:
|
deeper:
|
||||||
INIA 0d68
|
INIA 0d68
|
||||||
INIB 0d85
|
INIB 0d85
|
||||||
SETD.0 Banner
|
SETD.0 Banner
|
||||||
|
SETD.1 DoneText
|
||||||
|
SETD.2 Marker
|
||||||
SWI osBreak
|
SWI osBreak
|
||||||
RET
|
RET
|
||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
Banner:
|
Banner:
|
||||||
"two stops, and what the registers were at each
|
"two stops, and what the registers were at each
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
; Tries to commit a file bigger than the room it reserved.
|
||||||
|
;
|
||||||
|
; osFileStart sets aside an extent and osFileWrite refuses a block index outside it, so the
|
||||||
|
; obvious way to reach a neighbouring file - writing off the end - is already barred. This
|
||||||
|
; is the other way to the same place: reserve one block, write the one block, and then tell
|
||||||
|
; osFileDone the file came to two.
|
||||||
|
;
|
||||||
|
; NOTHING WOULD SAY SO IF THAT WERE ALLOWED. A directory entry is the only record of what a
|
||||||
|
; file owns, so an entry claiming a block it was never given simply owns it, and whatever
|
||||||
|
; owned it before owns it too. Both files then look perfectly well formed.
|
||||||
|
;
|
||||||
|
; Correct behaviour is a refusal, and the file left as it was. The reservation is one block
|
||||||
|
; and a tail of ten, so:
|
||||||
|
;
|
||||||
|
; two blocks and no tail more than was reserved refused
|
||||||
|
; one block and a tail exactly what was reserved allowed
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
#Program
|
||||||
|
#Base 0x4000
|
||||||
|
|
||||||
|
start:
|
||||||
|
; One block, and ten bytes after it.
|
||||||
|
SETD.0 Name
|
||||||
|
SETD.3 0x00 0x01
|
||||||
|
INIA 0d10
|
||||||
|
SWI osFileStart
|
||||||
|
BNQ noStart
|
||||||
|
|
||||||
|
SETD.1 Block
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
SWI osFileWrite
|
||||||
|
BNQ noWrite
|
||||||
|
SETD.1 Block
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
SWI osFileWrite
|
||||||
|
BNQ noWrite
|
||||||
|
|
||||||
|
; Two whole blocks, which is more than one block and a tail.
|
||||||
|
SETD.3 0x00 0x02
|
||||||
|
RSTA
|
||||||
|
SWI osFileDone
|
||||||
|
BNQ refused
|
||||||
|
SETD.0 Allowed
|
||||||
|
SWI osPrintString
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
refused:
|
||||||
|
SETD.0 Refused
|
||||||
|
SWI osPrintString
|
||||||
|
|
||||||
|
; And the honest size, which is what was reserved.
|
||||||
|
SETD.3 0x00 0x01
|
||||||
|
INIA 0d10
|
||||||
|
SWI osFileDone
|
||||||
|
BNQ noHonest
|
||||||
|
SETD.0 Honest
|
||||||
|
SWI osPrintString
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
noHonest:
|
||||||
|
SETD.0 NoHonest
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
noStart:
|
||||||
|
SETD.0 NoStart
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
noWrite:
|
||||||
|
SETD.0 NoWrite
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
#Data
|
||||||
|
#Base 0x2000
|
||||||
|
|
||||||
|
Name:
|
||||||
|
"claim.dat"
|
||||||
|
Block:
|
||||||
|
#Reserve 0d256
|
||||||
|
Allowed:
|
||||||
|
"claiming more than was reserved was ALLOWED
|
||||||
|
"
|
||||||
|
Refused:
|
||||||
|
"claiming more than was reserved was refused
|
||||||
|
"
|
||||||
|
Honest:
|
||||||
|
"and the size it really came to was taken
|
||||||
|
"
|
||||||
|
NoHonest:
|
||||||
|
"the honest size was refused too
|
||||||
|
"
|
||||||
|
NoStart:
|
||||||
|
"no start
|
||||||
|
"
|
||||||
|
NoWrite:
|
||||||
|
"no write
|
||||||
|
"
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
; Compare two files without asking either one to fit in Data Memory.
|
||||||
|
;
|
||||||
|
; Each occupied block is read into its own 256-byte buffer. The byte count returned for
|
||||||
|
; the block is compared before its contents, and only those bytes are examined: bytes
|
||||||
|
; after the end of a short final block belong to neither file and are allowed to differ.
|
||||||
|
;
|
||||||
|
; Written by ChatGPT for Anachronaut's SplitBit
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
#Base 0x4000
|
||||||
|
|
||||||
|
start:
|
||||||
|
SETD.0 Arguments
|
||||||
|
INIB 0xFF
|
||||||
|
SWI osArgument
|
||||||
|
SETD.0 Arguments
|
||||||
|
LDA.0
|
||||||
|
BRA usage
|
||||||
|
CALL textSplit
|
||||||
|
SETD.0 TextRest
|
||||||
|
LDD.0.0
|
||||||
|
LDA.0
|
||||||
|
BRA usage
|
||||||
|
|
||||||
|
SETD.0 Arguments
|
||||||
|
SWI osFileInfo
|
||||||
|
BNQ firstFailed
|
||||||
|
SETD.0 FirstBlocks
|
||||||
|
STD.3.0
|
||||||
|
|
||||||
|
SETD.0 TextRest
|
||||||
|
LDD.0.0
|
||||||
|
SWI osFileInfo
|
||||||
|
BNQ secondFailed
|
||||||
|
SETD.0 SecondBlocks
|
||||||
|
STD.3.0
|
||||||
|
|
||||||
|
; A different number of occupied blocks is immediately a different file.
|
||||||
|
SETD.0 FirstBlocks
|
||||||
|
LDA.0
|
||||||
|
SETD.1 SecondBlocks
|
||||||
|
LDB.1
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ different
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
LDA.0
|
||||||
|
LDB.1
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ different
|
||||||
|
|
||||||
|
SETD.0 FirstBlocks
|
||||||
|
SETD.1 RemainingBlocks
|
||||||
|
CALL copyWord
|
||||||
|
RSTA
|
||||||
|
SETD.0 Index
|
||||||
|
STA.0
|
||||||
|
INCD.0
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
nextBlock:
|
||||||
|
SETD.0 RemainingBlocks
|
||||||
|
LDA.0
|
||||||
|
INCD.0
|
||||||
|
LDB.0
|
||||||
|
OR
|
||||||
|
BRQ alike
|
||||||
|
|
||||||
|
SETD.0 Arguments
|
||||||
|
SETD.1 FirstBlock
|
||||||
|
CALL readAtIndex
|
||||||
|
BNQ firstReadFailed
|
||||||
|
SETD.0 FirstCount
|
||||||
|
STD.3.0
|
||||||
|
|
||||||
|
SETD.0 TextRest
|
||||||
|
LDD.0.0
|
||||||
|
SETD.1 SecondBlock
|
||||||
|
CALL readAtIndex
|
||||||
|
BNQ secondReadFailed
|
||||||
|
SETD.0 SecondCount
|
||||||
|
STD.3.0
|
||||||
|
|
||||||
|
; Equal occupied-block counts do not imply equal tails, so compare the valid byte count
|
||||||
|
; returned for this block as well.
|
||||||
|
SETD.0 FirstCount
|
||||||
|
LDA.0
|
||||||
|
SETD.1 SecondCount
|
||||||
|
LDB.1
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ different
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
LDA.0
|
||||||
|
LDB.1
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ different
|
||||||
|
|
||||||
|
SETD.0 FirstCount
|
||||||
|
SETD.1 BytesLeft
|
||||||
|
CALL copyWord
|
||||||
|
SETD.0 FirstBlock
|
||||||
|
SETD.1 SecondBlock
|
||||||
|
|
||||||
|
compareByte:
|
||||||
|
SETD.2 BytesLeft
|
||||||
|
LDA.2
|
||||||
|
INCD.2
|
||||||
|
LDB.2
|
||||||
|
OR
|
||||||
|
BRQ blockSame
|
||||||
|
LDA.0
|
||||||
|
LDB.1
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNQ different
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
SETD.2 BytesLeft
|
||||||
|
CALL takeByte
|
||||||
|
BRI compareByte
|
||||||
|
|
||||||
|
blockSame:
|
||||||
|
CALL stepIndex
|
||||||
|
CALL takeBlock
|
||||||
|
BRI nextBlock
|
||||||
|
|
||||||
|
; DP0 is the path and DP1 the destination buffer. Q and DP3 are the service answers.
|
||||||
|
readAtIndex:
|
||||||
|
SETD.2 Index
|
||||||
|
LDA.2
|
||||||
|
INCD.2
|
||||||
|
LDB.2
|
||||||
|
SWI osFileBlock
|
||||||
|
RET
|
||||||
|
|
||||||
|
; DP0 names the source word and DP1 the destination word.
|
||||||
|
copyWord:
|
||||||
|
LDA.0
|
||||||
|
STA.1
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
LDA.0
|
||||||
|
STA.1
|
||||||
|
RET
|
||||||
|
|
||||||
|
stepIndex:
|
||||||
|
SETD.0 Index
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
BNC stepDone
|
||||||
|
DECD.0
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
stepDone:
|
||||||
|
RET
|
||||||
|
|
||||||
|
takeBlock:
|
||||||
|
SETD.0 RemainingBlocks
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
BRA blockBorrow
|
||||||
|
DECA
|
||||||
|
STA.0
|
||||||
|
RET
|
||||||
|
blockBorrow:
|
||||||
|
INIA 0xFF
|
||||||
|
STA.0
|
||||||
|
DECD.0
|
||||||
|
LDA.0
|
||||||
|
DECA
|
||||||
|
STA.0
|
||||||
|
RET
|
||||||
|
|
||||||
|
; Decrement the big-endian sixteen-bit BytesLeft. A full block arrives as 0x0100, so an
|
||||||
|
; eight-bit counter would compare none of it and call many different files equal.
|
||||||
|
takeByte:
|
||||||
|
INCD.2
|
||||||
|
LDA.2
|
||||||
|
BRA byteBorrow
|
||||||
|
DECA
|
||||||
|
STA.2
|
||||||
|
RET
|
||||||
|
byteBorrow:
|
||||||
|
INIA 0xFF
|
||||||
|
STA.2
|
||||||
|
DECD.2
|
||||||
|
LDA.2
|
||||||
|
DECA
|
||||||
|
STA.2
|
||||||
|
RET
|
||||||
|
|
||||||
|
alike:
|
||||||
|
SETD.0 SameText
|
||||||
|
SWI osPrintString
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
different:
|
||||||
|
SETD.0 DifferentText
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
usage:
|
||||||
|
SETD.0 Usage
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d2
|
||||||
|
SWI osExit
|
||||||
|
firstFailed:
|
||||||
|
SETD.0 FirstError
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d2
|
||||||
|
SWI osExit
|
||||||
|
secondFailed:
|
||||||
|
SETD.0 SecondError
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d2
|
||||||
|
SWI osExit
|
||||||
|
firstReadFailed:
|
||||||
|
SETD.0 FirstReadError
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d2
|
||||||
|
SWI osExit
|
||||||
|
secondReadFailed:
|
||||||
|
SETD.0 SecondReadError
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d2
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
#Data
|
||||||
|
#Base 0x2000
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
#Reserve 0d256
|
||||||
|
FirstBlocks:
|
||||||
|
0x00 0x00
|
||||||
|
SecondBlocks:
|
||||||
|
0x00 0x00
|
||||||
|
RemainingBlocks:
|
||||||
|
0x00 0x00
|
||||||
|
Index:
|
||||||
|
0x00 0x00
|
||||||
|
FirstCount:
|
||||||
|
0x00 0x00
|
||||||
|
SecondCount:
|
||||||
|
0x00 0x00
|
||||||
|
BytesLeft:
|
||||||
|
0x00 0x00
|
||||||
|
FirstBlock:
|
||||||
|
#Reserve 0d256
|
||||||
|
SecondBlock:
|
||||||
|
#Reserve 0d256
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
"compare: give me two files
|
||||||
|
"
|
||||||
|
FirstError:
|
||||||
|
"compare: cannot find the first file
|
||||||
|
"
|
||||||
|
SecondError:
|
||||||
|
"compare: cannot find the second file
|
||||||
|
"
|
||||||
|
FirstReadError:
|
||||||
|
"compare: cannot read the first file
|
||||||
|
"
|
||||||
|
SecondReadError:
|
||||||
|
"compare: cannot read the second file
|
||||||
|
"
|
||||||
|
SameText:
|
||||||
|
"the same
|
||||||
|
"
|
||||||
|
DifferentText:
|
||||||
|
"different
|
||||||
|
"
|
||||||
|
|
||||||
|
#Include text.asm
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
; Copy one file to another without asking either file to fit in Data Memory.
|
||||||
|
;
|
||||||
|
; The two paths are the two words in the argument. Filesystem names can contain spaces,
|
||||||
|
; but the CosmOS command line has no quoting yet, so this deliberately has the same rule
|
||||||
|
; as the shell: a space separates things.
|
||||||
|
;
|
||||||
|
; osFileInfo reports how many disk blocks the source occupies, while osFileStart wants the
|
||||||
|
; shape stored in an SBFS entry: whole blocks and a possible tail. Reading the last block
|
||||||
|
; first recovers that distinction. The ordinary copy then walks forward one block at a
|
||||||
|
; time, through one 256-byte buffer.
|
||||||
|
;
|
||||||
|
; Written by ChatGPT for Anachronaut's SplitBit
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
#Base 0x4000
|
||||||
|
|
||||||
|
start:
|
||||||
|
SETD.0 Arguments
|
||||||
|
INIB 0xFF
|
||||||
|
SWI osArgument
|
||||||
|
SETD.0 Arguments
|
||||||
|
LDA.0
|
||||||
|
BRA usage
|
||||||
|
CALL textSplit
|
||||||
|
SETD.0 TextRest
|
||||||
|
LDD.0.0
|
||||||
|
LDA.0
|
||||||
|
BRA usage
|
||||||
|
|
||||||
|
; How many occupied blocks the source has. Keep it as both the loop bound and the
|
||||||
|
; distinction between an empty file and one whose last block must be inspected.
|
||||||
|
SETD.0 Arguments
|
||||||
|
SWI osFileInfo
|
||||||
|
BNQ sourceFailed
|
||||||
|
SETD.0 SourceBlocks
|
||||||
|
STD.3.0
|
||||||
|
PSHD.3
|
||||||
|
POPB
|
||||||
|
POPA
|
||||||
|
OR
|
||||||
|
BRQ emptySource
|
||||||
|
|
||||||
|
; Read the final occupied block. DP3 says how many bytes of it belong to the file:
|
||||||
|
; 0x0100 for a whole block, or 0x0001 through 0x00FF for a tail.
|
||||||
|
SETD.0 SourceBlocks
|
||||||
|
LDD.3.0
|
||||||
|
DECD.3
|
||||||
|
SETD.0 Index
|
||||||
|
STD.3.0
|
||||||
|
SETD.0 Arguments
|
||||||
|
SETD.1 Block
|
||||||
|
SETD.2 Index
|
||||||
|
LDA.2
|
||||||
|
INCD.2
|
||||||
|
LDB.2
|
||||||
|
SWI osFileBlock
|
||||||
|
BNQ readFailed
|
||||||
|
SETD.0 LastCount
|
||||||
|
STD.3.0
|
||||||
|
|
||||||
|
; A full final block means every occupied block is whole. A short final block means
|
||||||
|
; there is one fewer whole block and its low-byte count is the tail.
|
||||||
|
SETD.0 LastCount
|
||||||
|
LDA.0
|
||||||
|
BNA wholeEnding
|
||||||
|
SETD.0 Index
|
||||||
|
SETD.1 OutputWhole
|
||||||
|
CALL copyWord
|
||||||
|
SETD.0 LastCount
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
SETD.1 OutputTail
|
||||||
|
STA.1
|
||||||
|
BRI sizeKnown
|
||||||
|
|
||||||
|
wholeEnding:
|
||||||
|
SETD.0 SourceBlocks
|
||||||
|
SETD.1 OutputWhole
|
||||||
|
CALL copyWord
|
||||||
|
RSTA
|
||||||
|
SETD.0 OutputTail
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
sizeKnown:
|
||||||
|
BRI startOutput
|
||||||
|
|
||||||
|
emptySource:
|
||||||
|
RSTA
|
||||||
|
SETD.0 OutputWhole
|
||||||
|
STA.0
|
||||||
|
INCD.0
|
||||||
|
STA.0
|
||||||
|
SETD.0 OutputTail
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
startOutput:
|
||||||
|
SETD.0 TextRest
|
||||||
|
LDD.0.0
|
||||||
|
SETD.2 OutputWhole
|
||||||
|
LDD.3.2
|
||||||
|
SETD.2 OutputTail
|
||||||
|
LDA.2
|
||||||
|
SWI osFileStart
|
||||||
|
BNQ startFailed
|
||||||
|
|
||||||
|
; Empty files have no blocks to transfer, but still need committing so that an existing
|
||||||
|
; destination becomes an empty file.
|
||||||
|
SETD.0 SourceBlocks
|
||||||
|
SETD.1 RemainingBlocks
|
||||||
|
CALL copyWord
|
||||||
|
RSTA
|
||||||
|
SETD.0 Index
|
||||||
|
STA.0
|
||||||
|
INCD.0
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
copyNext:
|
||||||
|
SETD.0 RemainingBlocks
|
||||||
|
LDA.0
|
||||||
|
INCD.0
|
||||||
|
LDB.0
|
||||||
|
OR
|
||||||
|
BRQ copyDone
|
||||||
|
|
||||||
|
SETD.0 Arguments
|
||||||
|
SETD.1 Block
|
||||||
|
SETD.2 Index
|
||||||
|
LDA.2
|
||||||
|
INCD.2
|
||||||
|
LDB.2
|
||||||
|
SWI osFileBlock
|
||||||
|
BNQ readFailed
|
||||||
|
|
||||||
|
SETD.1 Block
|
||||||
|
SETD.2 Index
|
||||||
|
LDA.2
|
||||||
|
INCD.2
|
||||||
|
LDB.2
|
||||||
|
SWI osFileWrite
|
||||||
|
BNQ writeFailed
|
||||||
|
|
||||||
|
CALL stepIndex
|
||||||
|
CALL takeBlock
|
||||||
|
BRI copyNext
|
||||||
|
|
||||||
|
copyDone:
|
||||||
|
SETD.2 OutputWhole
|
||||||
|
LDD.3.2
|
||||||
|
SETD.2 OutputTail
|
||||||
|
LDA.2
|
||||||
|
SWI osFileDone
|
||||||
|
BNQ doneFailed
|
||||||
|
SETD.0 Copied
|
||||||
|
SWI osPrintString
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
; DP0 names the source word and DP1 the destination word.
|
||||||
|
copyWord:
|
||||||
|
LDA.0
|
||||||
|
STA.1
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
LDA.0
|
||||||
|
STA.1
|
||||||
|
RET
|
||||||
|
|
||||||
|
; Increment the big-endian sixteen-bit Index.
|
||||||
|
stepIndex:
|
||||||
|
SETD.0 Index
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
BNC stepDone
|
||||||
|
DECD.0
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
stepDone:
|
||||||
|
RET
|
||||||
|
|
||||||
|
; Decrement the big-endian sixteen-bit RemainingBlocks.
|
||||||
|
takeBlock:
|
||||||
|
SETD.0 RemainingBlocks
|
||||||
|
INCD.0
|
||||||
|
LDA.0
|
||||||
|
BRA takeBorrow
|
||||||
|
DECA
|
||||||
|
STA.0
|
||||||
|
RET
|
||||||
|
takeBorrow:
|
||||||
|
INIA 0xFF
|
||||||
|
STA.0
|
||||||
|
DECD.0
|
||||||
|
LDA.0
|
||||||
|
DECA
|
||||||
|
STA.0
|
||||||
|
RET
|
||||||
|
|
||||||
|
usage:
|
||||||
|
SETD.0 Usage
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d2
|
||||||
|
SWI osExit
|
||||||
|
sourceFailed:
|
||||||
|
SETD.0 SourceError
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
readFailed:
|
||||||
|
SETD.0 ReadError
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
startFailed:
|
||||||
|
SETD.0 StartError
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
writeFailed:
|
||||||
|
SETD.0 WriteError
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
doneFailed:
|
||||||
|
SETD.0 DoneError
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
#Data
|
||||||
|
#Base 0x2000
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
#Reserve 0d256
|
||||||
|
SourceBlocks:
|
||||||
|
0x00 0x00
|
||||||
|
RemainingBlocks:
|
||||||
|
0x00 0x00
|
||||||
|
Index:
|
||||||
|
0x00 0x00
|
||||||
|
LastCount:
|
||||||
|
0x00 0x00
|
||||||
|
OutputWhole:
|
||||||
|
0x00 0x00
|
||||||
|
OutputTail:
|
||||||
|
0x00
|
||||||
|
Block:
|
||||||
|
#Reserve 0d256
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
"copy: give me a source and destination
|
||||||
|
"
|
||||||
|
SourceError:
|
||||||
|
"copy: cannot find the source
|
||||||
|
"
|
||||||
|
ReadError:
|
||||||
|
"copy: cannot read the source
|
||||||
|
"
|
||||||
|
StartError:
|
||||||
|
"copy: cannot create the destination
|
||||||
|
"
|
||||||
|
WriteError:
|
||||||
|
"copy: cannot write the destination
|
||||||
|
"
|
||||||
|
DoneError:
|
||||||
|
"copy: cannot finish the destination
|
||||||
|
"
|
||||||
|
Copied:
|
||||||
|
"copied
|
||||||
|
"
|
||||||
|
|
||||||
|
#Include text.asm
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
SETD.0 FileName
|
SETD.0 FileName
|
||||||
@@ -75,8 +75,15 @@ start:
|
|||||||
RSTA
|
RSTA
|
||||||
STA.0
|
STA.0
|
||||||
|
|
||||||
|
RSTA
|
||||||
|
SETD.0 TooLong
|
||||||
|
STA.0
|
||||||
CALL loadFile
|
CALL loadFile
|
||||||
|
|
||||||
|
SETD.0 TooLong
|
||||||
|
LDA.0
|
||||||
|
BNA tooLongToEdit
|
||||||
|
|
||||||
SETD.0 FileName
|
SETD.0 FileName
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
SETD.0 CommaText
|
SETD.0 CommaText
|
||||||
@@ -161,12 +168,21 @@ commandArgument:
|
|||||||
BRI commandLoop
|
BRI commandLoop
|
||||||
|
|
||||||
quit:
|
quit:
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
tooLongToEdit:
|
||||||
|
SETD.0 TooLongText
|
||||||
|
SWI osPrintString
|
||||||
|
CALL newLine
|
||||||
|
INIA 0d1
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
noName:
|
noName:
|
||||||
SETD.0 NoNameText
|
SETD.0 NoNameText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
CALL newLine
|
CALL newLine
|
||||||
|
INIA 0d2
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
|
|
||||||
@@ -192,7 +208,7 @@ insertLoop:
|
|||||||
SETD.0 EnteringText
|
SETD.0 EnteringText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
SETD.0 Entry
|
SETD.0 Entry
|
||||||
INIB 0d80
|
INIB 0d128
|
||||||
SWI osReadLine
|
SWI osReadLine
|
||||||
INA 0x01
|
INA 0x01
|
||||||
INIB 0x02 ; ENDED
|
INIB 0x02 ; ENDED
|
||||||
@@ -232,7 +248,7 @@ doChange:
|
|||||||
SETD.0 EnteringText
|
SETD.0 EnteringText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
SETD.0 Entry
|
SETD.0 Entry
|
||||||
INIB 0d80
|
INIB 0d128
|
||||||
SWI osReadLine
|
SWI osReadLine
|
||||||
INA 0x01
|
INA 0x01
|
||||||
INIB 0x02 ; ENDED
|
INIB 0x02 ; ENDED
|
||||||
@@ -529,7 +545,6 @@ listStep:
|
|||||||
PSHD.3
|
PSHD.3
|
||||||
POPD.0
|
POPD.0
|
||||||
DPUP.0 0d03
|
DPUP.0 0d03
|
||||||
SETD.1 Leftover
|
|
||||||
LDA.1
|
LDA.1
|
||||||
BRA listEmpty
|
BRA listEmpty
|
||||||
listChars:
|
listChars:
|
||||||
@@ -599,6 +614,29 @@ splitStep:
|
|||||||
XOR
|
XOR
|
||||||
BRQ splitLine
|
BRQ splitLine
|
||||||
|
|
||||||
|
; ---- Room for it ----
|
||||||
|
;
|
||||||
|
; THERE WAS NO CHECK HERE AT ALL, and Entry is followed in memory by TextHead and
|
||||||
|
; ArenaFree - the head of the document and the pointer the line allocator hands out. A
|
||||||
|
; line longer than the buffer wrote characters over both, so the list head pointed into
|
||||||
|
; the middle of the text and the allocator handed out an address inside the file.
|
||||||
|
;
|
||||||
|
; What that looked like: a thirty one line file opened as three, one of them cut short.
|
||||||
|
; Then, opening it a second time, a list that led back into itself and a machine that
|
||||||
|
; walked it for ever - the emulator still running and the machine never answering again.
|
||||||
|
;
|
||||||
|
; Typing a long line was always safe, because osReadLine is told how much room there is.
|
||||||
|
; Only the file being read went unchecked, which is why a new document behaved and a
|
||||||
|
; source file did not.
|
||||||
|
PSHA
|
||||||
|
SETD.2 EntryLength
|
||||||
|
LDA.2
|
||||||
|
INIB 0d128
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
POPA ; The character back, and Q still says whether there is room.
|
||||||
|
BRQ splitTooLong
|
||||||
|
|
||||||
STA.1 ; A is still the character; an ALU operation does not touch it.
|
STA.1 ; A is still the character; an ALU operation does not touch it.
|
||||||
INCD.1
|
INCD.1
|
||||||
SETD.2 EntryLength
|
SETD.2 EntryLength
|
||||||
@@ -626,6 +664,15 @@ splitOn:
|
|||||||
CALL takeOneOff
|
CALL takeOneOff
|
||||||
BRI splitStep
|
BRI splitStep
|
||||||
|
|
||||||
|
splitTooLong:
|
||||||
|
; NOT TRUNCATED. This is an editor: a line it shortened here would be written back
|
||||||
|
; shortened, and the file would be damaged by having been looked at. Refusing leaves it
|
||||||
|
; exactly as it was.
|
||||||
|
INIA 0x01
|
||||||
|
SETD.2 TooLong
|
||||||
|
STA.2
|
||||||
|
RET
|
||||||
|
|
||||||
splitLast:
|
splitLast:
|
||||||
; A file that does not end in a newline still has a last line in it.
|
; A file that does not end in a newline still has a last line in it.
|
||||||
SETD.2 EntryLength
|
SETD.2 EntryLength
|
||||||
@@ -653,12 +700,12 @@ appendNode:
|
|||||||
|
|
||||||
; DP2 is a two byte count. Takes one off it.
|
; DP2 is a two byte count. Takes one off it.
|
||||||
takeOneOff:
|
takeOneOff:
|
||||||
DPUP.2 0d01
|
INCD.2
|
||||||
LDA.2
|
LDA.2
|
||||||
DECA
|
DECA
|
||||||
STA.2
|
STA.2
|
||||||
BNC takeOneDone ; No borrow, so the high half is untouched.
|
BNC takeOneDone ; No borrow, so the high half is untouched.
|
||||||
DPDN.2 0d01
|
DECD.2
|
||||||
LDA.2
|
LDA.2
|
||||||
DECA
|
DECA
|
||||||
STA.2
|
STA.2
|
||||||
@@ -723,7 +770,6 @@ writeOut:
|
|||||||
CCF
|
CCF
|
||||||
SUB
|
SUB
|
||||||
MVQA
|
MVQA
|
||||||
SETD.0 WroteSize
|
|
||||||
STA.0 ; WroteSize is now a count of bytes, which is what gets printed.
|
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.
|
; And into the two registers the service takes a size in.
|
||||||
@@ -766,12 +812,12 @@ printWord:
|
|||||||
|
|
||||||
; DP0 is a two byte number, A is a byte. Adds the one to the other.
|
; DP0 is a two byte number, A is a byte. Adds the one to the other.
|
||||||
addByteToWord:
|
addByteToWord:
|
||||||
DPUP.0 0d01
|
INCD.0
|
||||||
LDB.0
|
LDB.0
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
STQ.0
|
STQ.0
|
||||||
DPDN.0 0d01
|
DECD.0
|
||||||
LDA.0
|
LDA.0
|
||||||
RSTB
|
RSTB
|
||||||
ADD
|
ADD
|
||||||
@@ -780,7 +826,7 @@ addByteToWord:
|
|||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
Break:
|
Break:
|
||||||
0x0A 0x00
|
0x0A 0x00
|
||||||
@@ -812,13 +858,20 @@ NeedsLineText:
|
|||||||
"which line?"
|
"which line?"
|
||||||
NoWriteText:
|
NoWriteText:
|
||||||
"it would not write"
|
"it would not write"
|
||||||
|
TooLongText:
|
||||||
|
"a line in it is longer than this can edit, so it has not been opened"
|
||||||
|
|
||||||
FileName:
|
FileName:
|
||||||
#Reserve 0d24
|
#Reserve 0d24
|
||||||
Command:
|
Command:
|
||||||
#Reserve 0d41
|
#Reserve 0d41
|
||||||
|
; A hundred and twenty eight and the zero that ends it, which is what a line is everywhere
|
||||||
|
; else on this machine - the same number configuration files use, rather than a second
|
||||||
|
; answer to a question already answered.
|
||||||
Entry:
|
Entry:
|
||||||
#Reserve 0d81
|
#Reserve 0d129
|
||||||
|
TooLong:
|
||||||
|
0x00
|
||||||
|
|
||||||
TextHead:
|
TextHead:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
; Swap ValueB and ValueA.
|
; Swap ValueB and ValueA.
|
||||||
@@ -67,11 +67,12 @@ start:
|
|||||||
BRI start
|
BRI start
|
||||||
end:
|
end:
|
||||||
CALL lineFeed
|
CALL lineFeed
|
||||||
|
RSTA
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
ValueA:
|
ValueA:
|
||||||
; Low byte, high byte.
|
; Low byte, high byte.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
; Swap ValueB and ValueA.
|
; Swap ValueB and ValueA.
|
||||||
@@ -123,11 +123,12 @@ start:
|
|||||||
end:
|
end:
|
||||||
CALL lineFeed
|
CALL lineFeed
|
||||||
;HALT
|
;HALT
|
||||||
|
RSTA
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
ValueA:
|
ValueA:
|
||||||
; Lowest byte ... Highest byte.
|
; Lowest byte ... Highest byte.
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
; Load our initial values into A and B.
|
; Load our initial values into A and B.
|
||||||
INIA 0x00
|
RSTA
|
||||||
CALL printByteDecimal
|
CALL printByteDecimal
|
||||||
CALL blankSpace
|
CALL blankSpace
|
||||||
; Move the value into B.
|
; Move the value into B.
|
||||||
@@ -25,8 +25,7 @@ start:
|
|||||||
PSHA
|
PSHA
|
||||||
POPB
|
POPB
|
||||||
; Copy Q into A
|
; Copy Q into A
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
; Print A.
|
; Print A.
|
||||||
CALL printByteDecimal
|
CALL printByteDecimal
|
||||||
CALL blankSpace
|
CALL blankSpace
|
||||||
@@ -34,10 +33,11 @@ start:
|
|||||||
end:
|
end:
|
||||||
CALL lineFeed
|
CALL lineFeed
|
||||||
;HALT
|
;HALT
|
||||||
|
RSTA
|
||||||
SWI osExit ; Return to CosmOS.
|
SWI osExit ; Return to CosmOS.
|
||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
#Include print.asm
|
#Include print.asm
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
; ---- Write it ----
|
; ---- Write it ----
|
||||||
@@ -101,33 +101,39 @@ atEnd:
|
|||||||
BRQ stillThere
|
BRQ stillThere
|
||||||
SETD.0 GoneText
|
SETD.0 GoneText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
|
RSTA
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
stillThere:
|
stillThere:
|
||||||
SETD.0 StillText
|
SETD.0 StillText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
|
RSTA
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
noSave:
|
noSave:
|
||||||
SETD.0 NoSaveText
|
SETD.0 NoSaveText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
SWI osExit
|
SWI osExit
|
||||||
noRead:
|
noRead:
|
||||||
SETD.0 NoReadText
|
SETD.0 NoReadText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
SWI osExit
|
SWI osExit
|
||||||
noRename:
|
noRename:
|
||||||
SETD.0 NoRenameText
|
SETD.0 NoRenameText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
SWI osExit
|
SWI osExit
|
||||||
noDelete:
|
noDelete:
|
||||||
SETD.0 NoDeleteText
|
SETD.0 NoDeleteText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
Name:
|
Name:
|
||||||
"kept.txt"
|
"kept.txt"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
CIF ; Nothing arrives until there is something to catch it.
|
CIF ; Nothing arrives until there is something to catch it.
|
||||||
@@ -39,14 +39,16 @@ start:
|
|||||||
OUTA 0x02
|
OUTA 0x02
|
||||||
SIF
|
SIF
|
||||||
|
|
||||||
wait:
|
; Called spin rather than wait because WAIT is an instruction now, and a label may not
|
||||||
|
; be one. Which is the joke of it: this loop is exactly what WAIT exists to replace.
|
||||||
|
spin:
|
||||||
; This loop is the point. It never touches the console, so every character that appears
|
; This loop is the point. It never touches the console, so every character that appears
|
||||||
; below was put there by something that interrupted it.
|
; below was put there by something that interrupted it.
|
||||||
SETD.3 Stopping
|
SETD.3 Stopping
|
||||||
LDA.3
|
LDA.3
|
||||||
RSTB
|
RSTB
|
||||||
OR
|
OR
|
||||||
BRQ wait
|
BRQ spin
|
||||||
|
|
||||||
CALL newLine
|
CALL newLine
|
||||||
RSTA
|
RSTA
|
||||||
@@ -54,6 +56,7 @@ wait:
|
|||||||
SETD.0 DoneText
|
SETD.0 DoneText
|
||||||
CALL printString
|
CALL printString
|
||||||
CALL newLine
|
CALL newLine
|
||||||
|
RSTA
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
; Entered because the console had something to say. Never called.
|
; Entered because the console had something to say. Never called.
|
||||||
@@ -82,7 +85,7 @@ keyStop:
|
|||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
Banner:
|
Banner:
|
||||||
"keys, by interrupt. q stops."
|
"keys, by interrupt. q stops."
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
CALL seedGlider
|
CALL seedGlider
|
||||||
@@ -105,6 +105,7 @@ lifeEnd:
|
|||||||
CALL lineFeed
|
CALL lineFeed
|
||||||
CALL printString ; DP0 still holds the words: CALL puts DP0 back.
|
CALL printString ; DP0 still holds the words: CALL puts DP0 back.
|
||||||
CALL lineFeed
|
CALL lineFeed
|
||||||
|
RSTA
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
seedGlider:
|
seedGlider:
|
||||||
@@ -348,7 +349,7 @@ delayDone:
|
|||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
RowCount:
|
RowCount:
|
||||||
0x00
|
0x00
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
; Read a text file one screen at a time.
|
||||||
|
;
|
||||||
|
; Twenty two lines are shown before a prompt. Space advances another screen, Return one
|
||||||
|
; line, and q gives the machine back to CosmOS. This is forward-only on purpose: the file
|
||||||
|
; stream holds one block and never asks the whole document to fit in memory.
|
||||||
|
;
|
||||||
|
; Written by ChatGPT for Anachronaut's SplitBit
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
#Program
|
||||||
|
#Base 0x4000
|
||||||
|
|
||||||
|
start:
|
||||||
|
SETD.0 Name
|
||||||
|
INIB 0d29
|
||||||
|
SWI osArgument
|
||||||
|
SETD.0 Name
|
||||||
|
LDA.0
|
||||||
|
BRA noName
|
||||||
|
CALL fileStreamOpen
|
||||||
|
BNQ openFailed
|
||||||
|
CALL fullPage
|
||||||
|
|
||||||
|
nextBlock:
|
||||||
|
CALL fileStreamNext
|
||||||
|
BNQ readFailed
|
||||||
|
PSHD.3
|
||||||
|
POPB
|
||||||
|
POPA
|
||||||
|
SETD.2 Remaining
|
||||||
|
STA.2
|
||||||
|
INCD.2
|
||||||
|
STB.2
|
||||||
|
OR
|
||||||
|
BRQ finished
|
||||||
|
SETD.1 FileStreamBlock
|
||||||
|
SETD.2 Remaining
|
||||||
|
|
||||||
|
printLoop:
|
||||||
|
LDA.1
|
||||||
|
OUTA 0x00
|
||||||
|
INIB 0x0A
|
||||||
|
XOR
|
||||||
|
BNQ bytePrinted
|
||||||
|
SETD.3 LinesLeft
|
||||||
|
LDA.3
|
||||||
|
DECA
|
||||||
|
STA.3
|
||||||
|
BNA bytePrinted
|
||||||
|
CALL pause
|
||||||
|
BNQ finished
|
||||||
|
bytePrinted:
|
||||||
|
INCD.1
|
||||||
|
CALL fileStreamTakeRemaining
|
||||||
|
BRQ nextBlock
|
||||||
|
BRI printLoop
|
||||||
|
|
||||||
|
; Ask for one key without asking for Return. The console is put back immediately,
|
||||||
|
; including on q and end of redirected input.
|
||||||
|
pause:
|
||||||
|
SETD.0 MorePrompt
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x02
|
||||||
|
INA 0x00
|
||||||
|
PSHA ; Keep the key while A is used to restore line mode.
|
||||||
|
RSTA
|
||||||
|
OUTA 0x02
|
||||||
|
POPA
|
||||||
|
SETD.0 ClearPrompt
|
||||||
|
SWI osPrintString
|
||||||
|
|
||||||
|
INIB 0x71 ; q
|
||||||
|
XOR
|
||||||
|
BRQ pauseQuit
|
||||||
|
INIB 0xFF ; end of redirected input
|
||||||
|
XOR
|
||||||
|
BRQ pauseQuit
|
||||||
|
INIB 0x0A ; Return: one more line.
|
||||||
|
XOR
|
||||||
|
BRQ oneLine
|
||||||
|
INIB 0x20 ; Space: one more screen.
|
||||||
|
XOR
|
||||||
|
BNQ pause ; Ignore every other key.
|
||||||
|
CALL fullPage
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
OR
|
||||||
|
RET
|
||||||
|
oneLine:
|
||||||
|
SETD.3 LinesLeft
|
||||||
|
INIA 0x01
|
||||||
|
STA.3
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
OR
|
||||||
|
RET
|
||||||
|
pauseQuit:
|
||||||
|
RSTA
|
||||||
|
INIB 0x01
|
||||||
|
OR
|
||||||
|
RET
|
||||||
|
fullPage:
|
||||||
|
SETD.3 LinesLeft
|
||||||
|
INIA 0d22
|
||||||
|
STA.3
|
||||||
|
RET
|
||||||
|
|
||||||
|
noName:
|
||||||
|
SETD.0 Usage
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d2
|
||||||
|
SWI osExit
|
||||||
|
openFailed:
|
||||||
|
SETD.0 OpenError
|
||||||
|
SWI osPrintString
|
||||||
|
BRI printError
|
||||||
|
readFailed:
|
||||||
|
SETD.0 ReadError
|
||||||
|
SWI osPrintString
|
||||||
|
printError:
|
||||||
|
RSTA
|
||||||
|
MVQB
|
||||||
|
SWI osPrintNumber
|
||||||
|
SETD.0 NewLine
|
||||||
|
SWI osPrintString
|
||||||
|
; ITS OWN EXIT, and it did not have one. This fell through into finished and reported
|
||||||
|
; that everything was fine, having just printed the reason it was not - which nobody
|
||||||
|
; noticed while the only reader was a person, who could see both.
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
finished:
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
#Data
|
||||||
|
#Base 0x2000
|
||||||
|
Usage:
|
||||||
|
"more: give me a file name
|
||||||
|
"
|
||||||
|
OpenError:
|
||||||
|
"more: cannot find the file, error "
|
||||||
|
ReadError:
|
||||||
|
"more: cannot read the file, error "
|
||||||
|
NewLine:
|
||||||
|
0x0A 0x00
|
||||||
|
MorePrompt:
|
||||||
|
"-- more --"
|
||||||
|
ClearPrompt:
|
||||||
|
0x0A 0x00
|
||||||
|
Name:
|
||||||
|
#Reserve 0d29
|
||||||
|
Remaining:
|
||||||
|
0x00 0x00
|
||||||
|
LinesLeft:
|
||||||
|
0x00
|
||||||
|
|
||||||
|
#Include fileStream.asm
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
; Once.asm
|
||||||
|
; Starts something else next time, and only next time.
|
||||||
|
;
|
||||||
|
; > Once /System/Boot/mine.bin
|
||||||
|
; next start: /System/Boot/mine.bin, once
|
||||||
|
; > reboot
|
||||||
|
;
|
||||||
|
; Writes /System/Boot/once.cfg, which the loader reads before boot.cfg and DELETES BEFORE
|
||||||
|
; IT JUMPS. So the image runs on the next start and on no other, whatever happens to it -
|
||||||
|
; a one shot that hangs cannot hang twice, because the request is gone before it ran.
|
||||||
|
;
|
||||||
|
; ---- What this is for ----
|
||||||
|
;
|
||||||
|
; A program that owns the whole machine has nowhere to run. It cannot be started from the
|
||||||
|
; shell, because starting it means there is no shell; and pointing boot.cfg at it means a
|
||||||
|
; machine that keeps starting it, which is a poor place to find a mistake. This is the
|
||||||
|
; missing step: write it, ask for it once, and the system comes back by itself.
|
||||||
|
;
|
||||||
|
; The file is the same format as boot.cfg because a second format for one setting would be
|
||||||
|
; a second format. It says `system` for the same reason.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
#Base 0x4000
|
||||||
|
|
||||||
|
start:
|
||||||
|
SETD.0 Wanted
|
||||||
|
INIB 0d128
|
||||||
|
SWI osArgument
|
||||||
|
MVQA
|
||||||
|
BNA noName
|
||||||
|
|
||||||
|
SETD.0 Wanted
|
||||||
|
LDA.0
|
||||||
|
BRA noName
|
||||||
|
|
||||||
|
; The line, built as "system " and then the name. One write, because the file is the
|
||||||
|
; whole of the request and half of it would be a request for half a thing.
|
||||||
|
SETD.0 Prefix
|
||||||
|
SETD.1 Line
|
||||||
|
RCAL copyString
|
||||||
|
SETD.0 Wanted
|
||||||
|
RCAL copyString
|
||||||
|
INIA 0x0A
|
||||||
|
STA.1
|
||||||
|
INCD.1
|
||||||
|
RSTA
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
; How long it came to, which is what the write is told.
|
||||||
|
SETD.0 Line
|
||||||
|
RCAL measure
|
||||||
|
|
||||||
|
SETD.0 OncePath
|
||||||
|
SETD.1 Line
|
||||||
|
SWI osFileSave
|
||||||
|
MVQA
|
||||||
|
BNA noWrite
|
||||||
|
|
||||||
|
SETD.0 DoneText
|
||||||
|
SWI osPrintString
|
||||||
|
SETD.0 Wanted
|
||||||
|
SWI osPrintString
|
||||||
|
SETD.0 OnceText
|
||||||
|
SWI osPrintString
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
noName:
|
||||||
|
SETD.0 Usage
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d2
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
noWrite:
|
||||||
|
SETD.0 NoWriteText
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
; DP0 names a string and DP1 where it goes. DP1 is left on the zero at the end, so one
|
||||||
|
; string can be written straight after another.
|
||||||
|
copyString:
|
||||||
|
LDA.0
|
||||||
|
BRA copyDone
|
||||||
|
STA.1
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
BRI copyString
|
||||||
|
copyDone:
|
||||||
|
RRET
|
||||||
|
|
||||||
|
; DP0 names the line. osFileSave wants a size, and a file of whole blocks and a tail is
|
||||||
|
; that count with the blocks in A and the tail in B - one block is never full here.
|
||||||
|
measure:
|
||||||
|
RSTB
|
||||||
|
measureLoop:
|
||||||
|
LDA.0
|
||||||
|
BRA measureDone
|
||||||
|
INCD.0
|
||||||
|
MVQB
|
||||||
|
INIA 0d1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQB
|
||||||
|
BRI measureLoop
|
||||||
|
measureDone:
|
||||||
|
RSTA
|
||||||
|
RRET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
#Base 0x2000
|
||||||
|
|
||||||
|
Prefix:
|
||||||
|
"system "
|
||||||
|
Usage:
|
||||||
|
"once what? try: Once /System/Boot/something.bin
|
||||||
|
"
|
||||||
|
DoneText:
|
||||||
|
"next start: "
|
||||||
|
OnceText:
|
||||||
|
", once
|
||||||
|
"
|
||||||
|
NoWriteText:
|
||||||
|
"it would not write
|
||||||
|
"
|
||||||
|
OncePath:
|
||||||
|
"/System/Boot/once.cfg"
|
||||||
|
|
||||||
|
Wanted:
|
||||||
|
#Reserve 0d129
|
||||||
|
Line:
|
||||||
|
#Reserve 0d160
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
; Writes a file one block at a time, without ever holding the whole of it.
|
||||||
|
;
|
||||||
|
; This is the write side of what Stream demonstrates for reading: a file bigger than the
|
||||||
|
; memory building it. It writes as many blocks as it is asked for, each one filled with a
|
||||||
|
; pattern that says which block it is, so that what comes off the disk afterwards can be
|
||||||
|
; checked against what should have gone on rather than merely being the right length.
|
||||||
|
;
|
||||||
|
; The argument is how many blocks, in decimal. The last one is deliberately a part block,
|
||||||
|
; because a tail is the case every off-by-one in a filesystem hides in.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
#Program
|
||||||
|
#Base 0x4000
|
||||||
|
|
||||||
|
start:
|
||||||
|
SETD.0 Argument
|
||||||
|
INIB 0d8
|
||||||
|
SWI osArgument
|
||||||
|
SETD.0 Argument
|
||||||
|
LDA.0
|
||||||
|
BRA useDefault
|
||||||
|
CALL readCount
|
||||||
|
BRI counted
|
||||||
|
useDefault:
|
||||||
|
INIA 0d4
|
||||||
|
SETD.0 Blocks
|
||||||
|
STA.0
|
||||||
|
counted:
|
||||||
|
|
||||||
|
; Nothing may be zero blocks: the tail below would then be the whole file.
|
||||||
|
SETD.0 Blocks
|
||||||
|
LDA.0
|
||||||
|
BNA haveCount
|
||||||
|
INIA 0d1
|
||||||
|
STA.0
|
||||||
|
haveCount:
|
||||||
|
|
||||||
|
; Whole blocks, and a tail of forty bytes on the end of them.
|
||||||
|
SETD.0 Name
|
||||||
|
SETD.3 0x00 0x00
|
||||||
|
SETD.0 Blocks
|
||||||
|
LDA.0
|
||||||
|
PSHA
|
||||||
|
POPB
|
||||||
|
RSTA
|
||||||
|
PSHA
|
||||||
|
PSHB
|
||||||
|
POPD.3
|
||||||
|
SETD.0 Name
|
||||||
|
INIA 0d40
|
||||||
|
SWI osFileStart
|
||||||
|
BNQ startFailed
|
||||||
|
|
||||||
|
; Each whole block, filled with its own number.
|
||||||
|
RSTA
|
||||||
|
SETD.0 Which
|
||||||
|
STA.0
|
||||||
|
nextBlock:
|
||||||
|
SETD.0 Which
|
||||||
|
LDA.0
|
||||||
|
SETD.2 Blocks
|
||||||
|
LDB.2
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ theTail
|
||||||
|
|
||||||
|
CALL fillBlock
|
||||||
|
SETD.1 Block
|
||||||
|
SETD.0 Which
|
||||||
|
LDA.0
|
||||||
|
RSTB
|
||||||
|
PSHA
|
||||||
|
POPB
|
||||||
|
RSTA
|
||||||
|
SWI osFileWrite
|
||||||
|
BNQ writeFailed
|
||||||
|
|
||||||
|
SETD.0 Which
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
BRI nextBlock
|
||||||
|
|
||||||
|
theTail:
|
||||||
|
; And the part block at the end, which is the same fill cut short by the size given at
|
||||||
|
; the start. Only the first forty bytes of it will belong to the file.
|
||||||
|
CALL fillBlock
|
||||||
|
SETD.1 Block
|
||||||
|
SETD.0 Which
|
||||||
|
LDA.0
|
||||||
|
RSTB
|
||||||
|
PSHA
|
||||||
|
POPB
|
||||||
|
RSTA
|
||||||
|
SWI osFileWrite
|
||||||
|
BNQ writeFailed
|
||||||
|
|
||||||
|
; And how big it turned out to be, which here is what was asked for: this one knows its
|
||||||
|
; size from the start. Something that did not - an assembler, say - would ask for more
|
||||||
|
; than it needed and say the truth here.
|
||||||
|
RSTA
|
||||||
|
PSHA
|
||||||
|
SETD.0 Blocks
|
||||||
|
LDA.0
|
||||||
|
PSHA
|
||||||
|
POPD.3
|
||||||
|
INIA 0d40
|
||||||
|
SWI osFileDone
|
||||||
|
BNQ doneFailed
|
||||||
|
|
||||||
|
SETD.0 Wrote
|
||||||
|
SWI osPrintString
|
||||||
|
SETD.0 Blocks
|
||||||
|
LDB.0
|
||||||
|
RSTA ; A and B together are the number, so the count is the low half.
|
||||||
|
SWI osPrintNumber
|
||||||
|
SETD.0 AndTail
|
||||||
|
SWI osPrintString
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
; The block becomes 256 copies of the block number plus a fixed byte, so that a block
|
||||||
|
; written into the wrong place is visible rather than merely being bytes.
|
||||||
|
fillBlock:
|
||||||
|
SETD.0 Block
|
||||||
|
SETD.1 Which
|
||||||
|
LDA.1
|
||||||
|
INIB 0x41
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQA
|
||||||
|
RSTB
|
||||||
|
fillLoop:
|
||||||
|
STA.0
|
||||||
|
INCD.0
|
||||||
|
DECB
|
||||||
|
BNB fillLoop
|
||||||
|
RET
|
||||||
|
|
||||||
|
; The argument, in decimal, into Blocks. Anything that is not a digit ends it.
|
||||||
|
readCount:
|
||||||
|
RSTA
|
||||||
|
SETD.1 Blocks
|
||||||
|
STA.1
|
||||||
|
SETD.0 Argument
|
||||||
|
countLoop:
|
||||||
|
LDA.0
|
||||||
|
BRA countDone
|
||||||
|
INIB 0x30
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
MVQA
|
||||||
|
INIB 0d10
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNC countDone ; Not a digit, so the number ended.
|
||||||
|
SETD.1 Blocks
|
||||||
|
LDB.1
|
||||||
|
PSHA
|
||||||
|
INIA 0d10
|
||||||
|
CALL timesTen
|
||||||
|
POPA
|
||||||
|
SETD.1 Scratch
|
||||||
|
LDB.1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQA
|
||||||
|
SETD.1 Blocks
|
||||||
|
STA.1
|
||||||
|
INCD.0
|
||||||
|
BRI countLoop
|
||||||
|
countDone:
|
||||||
|
RET
|
||||||
|
|
||||||
|
; B times ten into Scratch, by adding it up. Nothing here is bigger than a byte.
|
||||||
|
timesTen:
|
||||||
|
RSTA
|
||||||
|
SETD.0 Scratch
|
||||||
|
STA.0
|
||||||
|
INIA 0d10
|
||||||
|
tenLoop:
|
||||||
|
PSHA
|
||||||
|
SETD.0 Scratch
|
||||||
|
LDA.0
|
||||||
|
SETD.2 TenHold
|
||||||
|
STB.2
|
||||||
|
LDB.2
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQA
|
||||||
|
STA.0
|
||||||
|
POPA
|
||||||
|
DECA
|
||||||
|
BNA tenLoop
|
||||||
|
RET
|
||||||
|
|
||||||
|
startFailed:
|
||||||
|
SETD.0 NoStart
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
writeFailed:
|
||||||
|
SETD.0 NoWrite
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
doneFailed:
|
||||||
|
SETD.0 NoDone
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
#Data
|
||||||
|
#Base 0x2000
|
||||||
|
|
||||||
|
Name:
|
||||||
|
"poured.dat"
|
||||||
|
Argument:
|
||||||
|
#Reserve 0d9
|
||||||
|
Blocks:
|
||||||
|
0x00
|
||||||
|
Which:
|
||||||
|
0x00
|
||||||
|
Scratch:
|
||||||
|
0x00
|
||||||
|
TenHold:
|
||||||
|
0x00
|
||||||
|
Block:
|
||||||
|
#Reserve 0d256
|
||||||
|
Wrote:
|
||||||
|
"poured "
|
||||||
|
AndTail:
|
||||||
|
" blocks and a tail of 40
|
||||||
|
"
|
||||||
|
NoStart:
|
||||||
|
"could not start it
|
||||||
|
"
|
||||||
|
NoWrite:
|
||||||
|
"could not write a block
|
||||||
|
"
|
||||||
|
NoDone:
|
||||||
|
"could not finish it
|
||||||
|
"
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
; Reboot.asm
|
||||||
|
; Starts the machine again.
|
||||||
|
;
|
||||||
|
; Whatever put the first instruction in memory does it again, and everything after that
|
||||||
|
; follows: the boot slot is read, the loader runs, and whatever the configuration names -
|
||||||
|
; or whatever Once asked for - is what starts.
|
||||||
|
;
|
||||||
|
; ---- Why this writes a port ----
|
||||||
|
;
|
||||||
|
; A reset has to work when the system does not. Something that could only be asked for
|
||||||
|
; through SWI would be unavailable in exactly the case that wants it most, and a program
|
||||||
|
; that owns the whole machine has no system to ask. So the machine takes it directly, the
|
||||||
|
; way it takes everything else: a byte out of a port.
|
||||||
|
;
|
||||||
|
; It is still a program rather than a shell word, because it is not the shell's business
|
||||||
|
; and because a word built into the shell is not callable by anything else.
|
||||||
|
;
|
||||||
|
; ---- What survives ----
|
||||||
|
;
|
||||||
|
; The disk, and everything written to it. That is what warm means: the machine starts
|
||||||
|
; again, the world it starts into does not. Nothing here flushes anything, because nothing
|
||||||
|
; on this machine is held back - a file is on the disk when the write returns.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
#Base 0x4000
|
||||||
|
|
||||||
|
start:
|
||||||
|
SETD.0 Saying
|
||||||
|
SWI osPrintString
|
||||||
|
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x13
|
||||||
|
|
||||||
|
; Not reached. The machine has started over by the time the next instruction would run,
|
||||||
|
; so anything here is a statement about a machine that no longer exists - but a program
|
||||||
|
; whose last instruction is an output is one byte from running into whatever follows it,
|
||||||
|
; and that is not a habit worth keeping.
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
#Base 0x2000
|
||||||
|
|
||||||
|
Saying:
|
||||||
|
"starting again
|
||||||
|
"
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
SETD.0 Given
|
SETD.0 Given
|
||||||
@@ -40,11 +40,12 @@ sayNothing:
|
|||||||
sayEnd:
|
sayEnd:
|
||||||
SETD.0 NewLine
|
SETD.0 NewLine
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
|
RSTA
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
SaidText:
|
SaidText:
|
||||||
"it says: "
|
"it says: "
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
; Settle.asm
|
||||||
|
; Says how the last start went, and tells the machine to stop falling back.
|
||||||
|
;
|
||||||
|
; A program rather than a shell command, because the shell is for the things you cannot do
|
||||||
|
; without it and this is not one of them. It reaches the system through SWI like anything
|
||||||
|
; else, which means it can be replaced, left off a disk, or called by whatever comes to
|
||||||
|
; call programs in sequence - and none of that is true of a word built into the shell.
|
||||||
|
;
|
||||||
|
; ---- What settling means ----
|
||||||
|
;
|
||||||
|
; The loader marks the disk before handing over and the system clears the mark on reaching
|
||||||
|
; its prompt, so a mark still set is a start that never arrived. After that the loader uses
|
||||||
|
; the fallback and KEEPS USING IT, because a system known not to start should not be tried
|
||||||
|
; every other boot for ever.
|
||||||
|
;
|
||||||
|
; Settling is how it is told that has changed. It does not fix anything and it does not
|
||||||
|
; check anything: it says "the situation is different now, try again". Which is why it is a
|
||||||
|
; deliberate act by somebody who has just changed something, rather than anything automatic.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
#Base 0x4000
|
||||||
|
|
||||||
|
start:
|
||||||
|
SWI osBootState
|
||||||
|
MVQA
|
||||||
|
SETD.0 State
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
BRA alreadySettled
|
||||||
|
|
||||||
|
INIB 0d2
|
||||||
|
XOR
|
||||||
|
BRQ fellBack
|
||||||
|
|
||||||
|
; Trying. Nothing has gone wrong yet - this is what the disk looks like while a start is
|
||||||
|
; still in progress, which from in here means the system that is running has not reached
|
||||||
|
; its prompt, which it plainly has. So the mark is stale.
|
||||||
|
SETD.0 WasTrying
|
||||||
|
SWI osPrintString
|
||||||
|
BRI doSettle
|
||||||
|
|
||||||
|
fellBack:
|
||||||
|
SETD.0 WasFallen
|
||||||
|
SWI osPrintString
|
||||||
|
|
||||||
|
doSettle:
|
||||||
|
SWI osBootSettle
|
||||||
|
MVQA
|
||||||
|
BNA settleFailed
|
||||||
|
SETD.0 Settled
|
||||||
|
SWI osPrintString
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
settleFailed:
|
||||||
|
SETD.0 NoDisk
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
alreadySettled:
|
||||||
|
SETD.0 Already
|
||||||
|
SWI osPrintString
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
#Base 0x2000
|
||||||
|
|
||||||
|
WasTrying:
|
||||||
|
"the disk says a start is still in progress
|
||||||
|
"
|
||||||
|
WasFallen:
|
||||||
|
"the disk says the last start did not arrive, so this is the fallback
|
||||||
|
"
|
||||||
|
Settled:
|
||||||
|
"settled: the next start will use the configuration again
|
||||||
|
"
|
||||||
|
Already:
|
||||||
|
"already settled: the next start will use the configuration
|
||||||
|
"
|
||||||
|
NoDisk:
|
||||||
|
"nothing to settle: no disk answered
|
||||||
|
"
|
||||||
|
|
||||||
|
State:
|
||||||
|
0x00
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
RSTA
|
RSTA
|
||||||
@@ -74,6 +74,7 @@ advancePage:
|
|||||||
|
|
||||||
finished:
|
finished:
|
||||||
CALL lineFeed
|
CALL lineFeed
|
||||||
|
RSTA
|
||||||
SWI osExit ; Return to CosmOS.
|
SWI osExit ; Return to CosmOS.
|
||||||
|
|
||||||
; DP0 points at a PrimeStates entry. CALL restores it on return.
|
; DP0 points at a PrimeStates entry. CALL restores it on return.
|
||||||
@@ -139,7 +140,7 @@ printCandidateHex:
|
|||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
; Segment has to begin on a page boundary, and now says so itself rather than relying on
|
; Segment has to begin on a page boundary, and now says so itself rather than relying on
|
||||||
; whatever happens to have been assembled before it. The marking loop adds the prime to
|
; whatever happens to have been assembled before it. The marking loop adds the prime to
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
; Search the list until we find a prime.
|
; Search the list until we find a prime.
|
||||||
@@ -38,6 +38,7 @@ start:
|
|||||||
BRI markMultiples ; Otherwise, loop again to mark the next multiple as nonprime.
|
BRI markMultiples ; Otherwise, loop again to mark the next multiple as nonprime.
|
||||||
end:
|
end:
|
||||||
CALL lineFeed ; Print a linefeed to make it look nice.
|
CALL lineFeed ; Print a linefeed to make it look nice.
|
||||||
|
RSTA
|
||||||
SWI osExit ; The program is done, we found all the primes!
|
SWI osExit ; The program is done, we found all the primes!
|
||||||
|
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ start:
|
|||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
; The table of our prime candidates. It has to begin on a page boundary: marking walks
|
; The table of our prime candidates. It has to begin on a page boundary: marking walks
|
||||||
; the pointer's low byte and treats the carry out as running off the end of the table,
|
; the pointer's low byte and treats the carry out as running off the end of the table,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
; The two pointers whose low byte is a square number. Both regions are page aligned, so
|
; The two pointers whose low byte is a square number. Both regions are page aligned, so
|
||||||
@@ -105,6 +105,7 @@ gameOverSay:
|
|||||||
CALL newLine
|
CALL newLine
|
||||||
RSTA
|
RSTA
|
||||||
OUTA 0x02 ; Line mode, the way it was found.
|
OUTA 0x02 ; Line mode, the way it was found.
|
||||||
|
RSTA ; splitlint[redundant-assignment]: an exit status, not a console mode
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
; ---- Reaching a square ----
|
; ---- Reaching a square ----
|
||||||
@@ -334,7 +335,6 @@ takeKeyTurn:
|
|||||||
XOR
|
XOR
|
||||||
POPA
|
POPA
|
||||||
BRQ takeKeyDone ; Opposite, so it is not a turn anybody can make.
|
BRQ takeKeyDone ; Opposite, so it is not a turn anybody can make.
|
||||||
SETD.0 Direction
|
|
||||||
STA.0
|
STA.0
|
||||||
RET
|
RET
|
||||||
|
|
||||||
@@ -430,7 +430,6 @@ stepDown:
|
|||||||
INIB 0xF0
|
INIB 0xF0
|
||||||
AND
|
AND
|
||||||
MVQA
|
MVQA
|
||||||
INIB 0xF0
|
|
||||||
XOR
|
XOR
|
||||||
POPA
|
POPA
|
||||||
BRQ stepWall ; The bottom row is where the high nibble is fifteen.
|
BRQ stepWall ; The bottom row is where the high nibble is fifteen.
|
||||||
@@ -453,7 +452,6 @@ stepRight:
|
|||||||
INIB 0x0F
|
INIB 0x0F
|
||||||
AND
|
AND
|
||||||
MVQA
|
MVQA
|
||||||
INIB 0x0F
|
|
||||||
XOR
|
XOR
|
||||||
POPA
|
POPA
|
||||||
BRQ stepWall
|
BRQ stepWall
|
||||||
@@ -605,7 +603,7 @@ pauseInner:
|
|||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
HeadCell:
|
HeadCell:
|
||||||
0x00
|
0x00
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
; Status.asm
|
||||||
|
; Says what the last program made of what it was asked to do.
|
||||||
|
;
|
||||||
|
; The shell keeps the number and does not print it, because a program that failed has
|
||||||
|
; already said so in words and a number beside that would be noise. But a number nobody can
|
||||||
|
; see is a number nobody can trust, so this is how a person looks.
|
||||||
|
;
|
||||||
|
; 0 it did what it was asked
|
||||||
|
; 1 it did not
|
||||||
|
; 2 it was asked wrongly
|
||||||
|
;
|
||||||
|
; A program may give its own meanings if it says so, and Compare does: one there means the
|
||||||
|
; files differ, which is a result rather than a failure.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
#Base 0x4000
|
||||||
|
|
||||||
|
start:
|
||||||
|
SWI osLastStatus
|
||||||
|
MVQA
|
||||||
|
SETD.0 Was
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
SETD.0 Prefix
|
||||||
|
SWI osPrintString
|
||||||
|
|
||||||
|
; A and B together, most significant first - so the status is the LOW half. Put in A the
|
||||||
|
; first time this was written, which printed a status of two as five hundred and twelve.
|
||||||
|
RSTA
|
||||||
|
SETD.0 Was
|
||||||
|
LDB.0
|
||||||
|
SWI osPrintNumber
|
||||||
|
|
||||||
|
; And in words, for the three the system itself uses.
|
||||||
|
SETD.0 Was
|
||||||
|
LDA.0
|
||||||
|
BRA sayWorked
|
||||||
|
INIB 0d1
|
||||||
|
XOR
|
||||||
|
BRQ sayFailed
|
||||||
|
SETD.0 Was
|
||||||
|
LDA.0
|
||||||
|
INIB 0d2
|
||||||
|
XOR
|
||||||
|
BRQ sayAsked
|
||||||
|
BRI done
|
||||||
|
|
||||||
|
sayWorked:
|
||||||
|
SETD.0 WorkedText
|
||||||
|
SWI osPrintString
|
||||||
|
BRI done
|
||||||
|
sayFailed:
|
||||||
|
SETD.0 FailedText
|
||||||
|
SWI osPrintString
|
||||||
|
BRI done
|
||||||
|
sayAsked:
|
||||||
|
SETD.0 AskedText
|
||||||
|
SWI osPrintString
|
||||||
|
|
||||||
|
done:
|
||||||
|
SETD.0 NewLine
|
||||||
|
SWI osPrintString
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
#Base 0x2000
|
||||||
|
|
||||||
|
Prefix:
|
||||||
|
"the last program left "
|
||||||
|
WorkedText:
|
||||||
|
", which is: it did what it was asked"
|
||||||
|
FailedText:
|
||||||
|
", which is: it did not"
|
||||||
|
AskedText:
|
||||||
|
", which is: it was asked wrongly"
|
||||||
|
NewLine:
|
||||||
|
"
|
||||||
|
"
|
||||||
|
Was:
|
||||||
|
0x00
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
; ---- 1. How big is something that will not fit ----
|
; ---- 1. How big is something that will not fit ----
|
||||||
@@ -216,6 +216,7 @@ missing:
|
|||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
CALL printWhy
|
CALL printWhy
|
||||||
|
|
||||||
|
INIA 0d1
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
noBig:
|
noBig:
|
||||||
@@ -223,14 +224,17 @@ noBig:
|
|||||||
SETD.0 NoBigText
|
SETD.0 NoBigText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
CALL printWhy
|
CALL printWhy
|
||||||
|
INIA 0d1
|
||||||
SWI osExit
|
SWI osExit
|
||||||
noSmall:
|
noSmall:
|
||||||
SETD.0 NoSmallText
|
SETD.0 NoSmallText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
SWI osExit
|
SWI osExit
|
||||||
noRename:
|
noRename:
|
||||||
SETD.0 NoRenameText
|
SETD.0 NoRenameText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
; ---- Routines ----
|
; ---- Routines ----
|
||||||
@@ -290,11 +294,11 @@ checksumLoop:
|
|||||||
INCD.2
|
INCD.2
|
||||||
LDA.2
|
LDA.2
|
||||||
BNA checksumLow
|
BNA checksumLow
|
||||||
DPDN.2 0d01
|
DECD.2
|
||||||
LDA.2
|
LDA.2
|
||||||
DECA
|
DECA
|
||||||
STA.2 ; Borrow out of the high byte.
|
STA.2 ; Borrow out of the high byte.
|
||||||
DPUP.2 0d01
|
INCD.2
|
||||||
INIA 0xFF
|
INIA 0xFF
|
||||||
STA.2
|
STA.2
|
||||||
BRI checksumTest
|
BRI checksumTest
|
||||||
@@ -333,7 +337,7 @@ stepIndex:
|
|||||||
INCA
|
INCA
|
||||||
STA.2
|
STA.2
|
||||||
BNC stepIndexDone
|
BNC stepIndexDone
|
||||||
DPDN.2 0d01
|
DECD.2
|
||||||
LDA.2
|
LDA.2
|
||||||
INCA
|
INCA
|
||||||
STA.2
|
STA.2
|
||||||
@@ -404,7 +408,7 @@ printWhy:
|
|||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
BigName:
|
BigName:
|
||||||
"big.txt"
|
"big.txt"
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
; Print a text file without asking it to fit in Data Memory.
|
||||||
|
; fileStream.asm owns finding the file and walking its blocks. Type owns only what makes
|
||||||
|
; it Type: send each byte to the console until the stream says there are no more.
|
||||||
|
;
|
||||||
|
; Written by ChatGPT for Anachronaut's SplitBit
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
#Program
|
||||||
|
#Base 0x4000
|
||||||
|
|
||||||
|
start:
|
||||||
|
SETD.0 Name
|
||||||
|
INIB 0d29
|
||||||
|
SWI osArgument
|
||||||
|
SETD.0 Name
|
||||||
|
LDA.0
|
||||||
|
BRA noName
|
||||||
|
CALL fileStreamOpen
|
||||||
|
BNQ openFailed
|
||||||
|
|
||||||
|
nextBlock:
|
||||||
|
CALL fileStreamNext
|
||||||
|
BNQ readFailed
|
||||||
|
PSHD.3
|
||||||
|
POPB
|
||||||
|
POPA
|
||||||
|
SETD.2 Remaining
|
||||||
|
STA.2
|
||||||
|
INCD.2
|
||||||
|
STB.2
|
||||||
|
OR
|
||||||
|
BRQ finished
|
||||||
|
SETD.1 FileStreamBlock
|
||||||
|
SETD.2 Remaining
|
||||||
|
|
||||||
|
printLoop:
|
||||||
|
LDA.1
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.1
|
||||||
|
CALL fileStreamTakeRemaining
|
||||||
|
BRQ nextBlock
|
||||||
|
BRI printLoop
|
||||||
|
|
||||||
|
noName:
|
||||||
|
SETD.0 Usage
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d2
|
||||||
|
SWI osExit
|
||||||
|
openFailed:
|
||||||
|
SETD.0 OpenError
|
||||||
|
SWI osPrintString
|
||||||
|
BRI printError
|
||||||
|
readFailed:
|
||||||
|
SETD.0 ReadError
|
||||||
|
SWI osPrintString
|
||||||
|
printError:
|
||||||
|
RSTA
|
||||||
|
MVQB
|
||||||
|
SWI osPrintNumber
|
||||||
|
SETD.0 NewLine
|
||||||
|
SWI osPrintString
|
||||||
|
; ITS OWN EXIT, and it did not have one. This fell through into finished and reported
|
||||||
|
; that everything was fine, having just printed the reason it was not - which nobody
|
||||||
|
; noticed while the only reader was a person, who could see both.
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
finished:
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
#Data
|
||||||
|
#Base 0x2000
|
||||||
|
Usage:
|
||||||
|
"type: give me a file name
|
||||||
|
"
|
||||||
|
OpenError:
|
||||||
|
"type: cannot find the file, error "
|
||||||
|
ReadError:
|
||||||
|
"type: cannot read the file, error "
|
||||||
|
NewLine:
|
||||||
|
0x0A 0x00
|
||||||
|
Name:
|
||||||
|
#Reserve 0d29
|
||||||
|
Remaining:
|
||||||
|
0x00 0x00
|
||||||
|
|
||||||
|
#Include fileStream.asm
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
; Goes somewhere else and reads a file by a bare name once it is there.
|
||||||
|
;
|
||||||
|
; This exists to prove two things the shell promises and nothing else could test. First
|
||||||
|
; that osChangeDir works: the file it prints is named with no path at all, so the only way
|
||||||
|
; to reach it is to be standing in the right place. Second that the shell puts the working
|
||||||
|
; directory back afterwards: the prompt after this returns says where the shell was, not
|
||||||
|
; where this went.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Include services.asm
|
||||||
|
#Program
|
||||||
|
#Base 0x4000
|
||||||
|
|
||||||
|
start:
|
||||||
|
; Where to go is the argument. Nothing else about this program says a directory name, so
|
||||||
|
; running it anywhere else moves it anywhere else.
|
||||||
|
SETD.0 Where
|
||||||
|
INIB 0d40
|
||||||
|
SWI osArgument
|
||||||
|
SETD.0 Where
|
||||||
|
LDA.0
|
||||||
|
BRA noWhere
|
||||||
|
|
||||||
|
SETD.0 Where
|
||||||
|
SWI osChangeDir
|
||||||
|
BNQ noSuchPlace
|
||||||
|
|
||||||
|
SETD.0 Went
|
||||||
|
SWI osPrintString
|
||||||
|
|
||||||
|
; And now a bare name, which means nothing until you are somewhere.
|
||||||
|
SETD.0 Bare
|
||||||
|
CALL fileStreamOpen
|
||||||
|
BNQ noFile
|
||||||
|
|
||||||
|
wanderBlock:
|
||||||
|
CALL fileStreamNext
|
||||||
|
BNQ noFile
|
||||||
|
PSHD.3
|
||||||
|
POPB
|
||||||
|
POPA
|
||||||
|
SETD.2 Left
|
||||||
|
STA.2
|
||||||
|
INCD.2
|
||||||
|
STB.2
|
||||||
|
|
||||||
|
SETD.2 Left
|
||||||
|
LDA.2
|
||||||
|
INCD.2
|
||||||
|
LDB.2
|
||||||
|
OR
|
||||||
|
BRQ wanderDone
|
||||||
|
|
||||||
|
SETD.0 FileStreamBlock
|
||||||
|
SETD.2 Left
|
||||||
|
INCD.2
|
||||||
|
LDB.2
|
||||||
|
wanderByte:
|
||||||
|
LDA.0
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.0
|
||||||
|
DECB
|
||||||
|
BNB wanderByte
|
||||||
|
BRI wanderBlock
|
||||||
|
|
||||||
|
wanderDone:
|
||||||
|
RSTA
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
noWhere:
|
||||||
|
SETD.0 NoWhereText
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
noSuchPlace:
|
||||||
|
SETD.0 NoPlaceText
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
noFile:
|
||||||
|
SETD.0 NoFileText
|
||||||
|
SWI osPrintString
|
||||||
|
INIA 0d1
|
||||||
|
SWI osExit
|
||||||
|
|
||||||
|
#Data
|
||||||
|
#Base 0x2000
|
||||||
|
|
||||||
|
Where:
|
||||||
|
#Reserve 0d40
|
||||||
|
Left:
|
||||||
|
0x00 0x00
|
||||||
|
Went:
|
||||||
|
"moved, and reading a bare name from there:
|
||||||
|
"
|
||||||
|
NoWhereText:
|
||||||
|
"wander where?
|
||||||
|
"
|
||||||
|
NoPlaceText:
|
||||||
|
"cannot go there
|
||||||
|
"
|
||||||
|
NoFileText:
|
||||||
|
"nothing of that name here
|
||||||
|
"
|
||||||
|
Bare:
|
||||||
|
"notes.txt"
|
||||||
|
|
||||||
|
#Include fileStream.asm
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000 ; Above the system, which keeps below here.
|
#Base 0x4000 ; Above the system, which keeps below here.
|
||||||
|
|
||||||
greet:
|
greet:
|
||||||
SETD.0 Opening
|
SETD.0 Opening
|
||||||
@@ -39,11 +39,12 @@ greet:
|
|||||||
|
|
||||||
; Give the machine back. The system takes its Stack back at this point, so everything
|
; Give the machine back. The system takes its Stack back at this point, so everything
|
||||||
; this program pushed goes with it.
|
; this program pushed goes with it.
|
||||||
|
RSTA
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000 ; And its data above the system's.
|
#Base 0x2000 ; And its data above the system's.
|
||||||
|
|
||||||
Opening:
|
Opening:
|
||||||
"a program, loaded off a disk, running on the system that loaded it
|
"a program, loaded off a disk, running on the system that loaded it
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000 ; Change two:
|
#Base 0x4000 ; Change two:
|
||||||
|
|
||||||
SETD hello ; Change three
|
SETD hello ; Change three
|
||||||
Start:
|
Start:
|
||||||
@@ -21,10 +21,11 @@ End:
|
|||||||
OUTA 0x00 ; Output it to the text console.
|
OUTA 0x00 ; Output it to the text console.
|
||||||
;HALT ; Terminate the program.
|
;HALT ; Terminate the program.
|
||||||
; Instead, let's call osExit to return the system nicely. Fourth change.
|
; Instead, let's call osExit to return the system nicely. Fourth change.
|
||||||
|
RSTA
|
||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000 ; Five, adjust the base of the data segment.
|
#Base 0x2000 ; Five, adjust the base of the data segment.
|
||||||
hello: ; Throw a label here so we can explicitly point at this data. Six, actually.
|
hello: ; Throw a label here so we can explicitly point at this data. Six, actually.
|
||||||
"Hello, World!"
|
"Hello, World!"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
SETD.0 Argument
|
SETD.0 Argument
|
||||||
@@ -57,6 +57,7 @@ start:
|
|||||||
CALL settleFormat
|
CALL settleFormat
|
||||||
CALL deriveName ; After the first pass: what it is called depends on whether a
|
CALL deriveName ; After the first pass: what it is called depends on whether a
|
||||||
; #Base turned up, and that is not known until then.
|
; #Base turned up, and that is not known until then.
|
||||||
|
|
||||||
CALL layOutImage
|
CALL layOutImage
|
||||||
BNQ stopped
|
BNQ stopped
|
||||||
CALL passTwo
|
CALL passTwo
|
||||||
@@ -202,7 +203,7 @@ passNotVectorName:
|
|||||||
BNQ passNotSwi
|
BNQ passNotSwi
|
||||||
SETD.0 ClsOpcode
|
SETD.0 ClsOpcode
|
||||||
LDA.0
|
LDA.0
|
||||||
INIB 0x18
|
INIB 0x72 ; SWI. Moved with the rest of the subroutine block.
|
||||||
XOR
|
XOR
|
||||||
BNQ passNotSwi
|
BNQ passNotSwi
|
||||||
INIA 0d1
|
INIA 0d1
|
||||||
@@ -735,7 +736,7 @@ emitStringLoop:
|
|||||||
CALL numStep
|
CALL numStep
|
||||||
SETD.1 EmitWalk
|
SETD.1 EmitWalk
|
||||||
LDD.0.1
|
LDD.0.1
|
||||||
DPDN.0 0d01
|
DECD.0
|
||||||
LDA.0
|
LDA.0
|
||||||
BNA emitStringLoop ; The zero goes out with the rest and then stops the loop.
|
BNA emitStringLoop ; The zero goes out with the rest and then stops the loop.
|
||||||
BRI emitDone
|
BRI emitDone
|
||||||
@@ -1276,7 +1277,6 @@ dropColon:
|
|||||||
LDA.0
|
LDA.0
|
||||||
BRA dropColonDone
|
BRA dropColonDone
|
||||||
DECA
|
DECA
|
||||||
SETD.0 TokLength
|
|
||||||
STA.0
|
STA.0
|
||||||
SETD.0 TokText
|
SETD.0 TokText
|
||||||
SETD.1 DropWalk
|
SETD.1 DropWalk
|
||||||
@@ -1360,8 +1360,7 @@ layOutImage:
|
|||||||
BNQ layOutNo
|
BNQ layOutNo
|
||||||
|
|
||||||
SETD.0 ImgWalk
|
SETD.0 ImgWalk
|
||||||
SETD.2 ScratchImage
|
CALL numZero ; The front of the file, not the front of a buffer.
|
||||||
CALL numSet
|
|
||||||
SETD.0 MagicSPBT
|
SETD.0 MagicSPBT
|
||||||
INIA 0d4
|
INIA 0d4
|
||||||
CALL putBytes
|
CALL putBytes
|
||||||
@@ -1414,8 +1413,7 @@ layOutLoadable:
|
|||||||
BNQ layOutNo
|
BNQ layOutNo
|
||||||
|
|
||||||
SETD.0 ImgWalk
|
SETD.0 ImgWalk
|
||||||
SETD.2 ScratchImage
|
CALL numZero ; The front of the file, not the front of a buffer.
|
||||||
CALL numSet
|
|
||||||
SETD.0 MagicSBEX
|
SETD.0 MagicSBEX
|
||||||
INIA 0d4
|
INIA 0d4
|
||||||
CALL putBytes
|
CALL putBytes
|
||||||
@@ -1460,17 +1458,113 @@ layOutNo:
|
|||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
|
|
||||||
|
; ---- Room for the file ----
|
||||||
|
;
|
||||||
|
; Nothing is held in memory any more, so what is checked here is not a buffer. It is that
|
||||||
|
; the SIZE CAN STILL BE DESCRIBED: every number in this assembler is two bytes, so a file
|
||||||
|
; of more than 65,535 would have wrapped round while it was being added up and come out
|
||||||
|
; smaller than one of its own segments. A wrapped total would allocate a short file and
|
||||||
|
; write off the end of it.
|
||||||
|
;
|
||||||
|
; Then the file is started, which is where running out of DISK is found - and that happens
|
||||||
|
; before a single byte is written, so a refusal costs nothing that was already there.
|
||||||
checkImageRoom:
|
checkImageRoom:
|
||||||
SETD.0 ImgRoom
|
SETD.0 ImgTotal
|
||||||
SETD.2 ImgTotal
|
SETD.2 ImgProgLen
|
||||||
|
CALL numCompare
|
||||||
|
BRC imageTooBig ; The total is smaller than a part of it: it went round.
|
||||||
|
SETD.0 ImgTotal
|
||||||
|
SETD.2 ImgDataLen
|
||||||
CALL numCompare
|
CALL numCompare
|
||||||
BRC imageTooBig
|
BRC imageTooBig
|
||||||
|
|
||||||
|
; ---- How much room to ask for ----
|
||||||
|
;
|
||||||
|
; MORE THAN THE FILE WILL COME TO, on purpose. How many vectors are actually installed is
|
||||||
|
; not known until the second pass has resolved every handler, and by then the file has to
|
||||||
|
; exist to be written into. So the room asked for is the whole file plus four bytes for
|
||||||
|
; every vector the table can hold, and five for a marker.
|
||||||
|
;
|
||||||
|
; THE LIMIT RATHER THAN THE COUNT SO FAR, and that distinction cost a real bug. Asking
|
||||||
|
; for four bytes per vector DECLARED looks like a safe bound and is not: a device is
|
||||||
|
; declared during the SECOND pass, in the line that implements it, so a program with one
|
||||||
|
; installs a vector that was not counted when the room was measured. CosmOS reserved
|
||||||
|
; 14,163 bytes and committed 14,167, writing four bytes past what it had been given -
|
||||||
|
; which happened to land inside the last block it owned, and would not have if the
|
||||||
|
; boundary had fallen four bytes earlier.
|
||||||
|
;
|
||||||
|
; The limit cannot go stale that way. It is what the vector table holds, so no assembly
|
||||||
|
; can install more, whenever they are counted.
|
||||||
|
;
|
||||||
|
; Asking for too much costs nothing but a moment: osFileDone is told what it really came
|
||||||
|
; to and the difference goes back. Asking for too little would have meant writing off the
|
||||||
|
; end of the file, and the first sign of it was Keys coming out four bytes short.
|
||||||
|
SETD.0 ImgAsk
|
||||||
|
SETD.2 ImgTotal
|
||||||
|
CALL numSet
|
||||||
|
SETD.0 ImgVecMost
|
||||||
|
SETD.2 VecLimit
|
||||||
|
CALL numSet
|
||||||
|
SETD.0 ImgVecMost
|
||||||
|
SETD.2 ImgVecMost
|
||||||
|
CALL numAdd
|
||||||
|
SETD.0 ImgVecMost
|
||||||
|
SETD.2 ImgVecMost
|
||||||
|
CALL numAdd ; Four bytes an entry.
|
||||||
|
SETD.0 ImgAsk
|
||||||
|
SETD.2 ImgVecMost
|
||||||
|
CALL numAdd
|
||||||
|
INIA 0d5
|
||||||
|
SETD.0 ImgAsk
|
||||||
|
CALL numAddByte
|
||||||
|
|
||||||
|
; Blocks are the high half of the size and the tail is the low half, which is how an
|
||||||
|
; entry holds one and why nothing here has to divide.
|
||||||
|
SETD.1 ImgAsk
|
||||||
|
LDA.1
|
||||||
|
SETD.0 ImgOutBlocks
|
||||||
|
STA.0 ; The high half is whole blocks.
|
||||||
|
INCD.1
|
||||||
|
LDA.1
|
||||||
|
SETD.0 ImgOutTail
|
||||||
|
STA.0 ; And the low half is what is left over.
|
||||||
|
|
||||||
|
; DP3 wants the block count and A the tail. The Stack takes the high byte first, which is
|
||||||
|
; the way every reader in the system takes a count back out of DP3.
|
||||||
|
RSTA
|
||||||
|
PSHA
|
||||||
|
SETD.0 ImgOutBlocks
|
||||||
|
LDA.0
|
||||||
|
PSHA
|
||||||
|
POPD.3
|
||||||
|
SETD.0 ImgOutTail
|
||||||
|
LDA.0
|
||||||
|
SETD.0 OutName
|
||||||
|
SWI osFileStart
|
||||||
|
BNQ imageNoRoom
|
||||||
|
|
||||||
|
; Nothing in the window yet.
|
||||||
|
RSTA
|
||||||
|
SETD.0 OutHeld
|
||||||
|
STA.0
|
||||||
|
SETD.0 OutDirty
|
||||||
|
STA.0
|
||||||
|
|
||||||
RSTA
|
RSTA
|
||||||
RSTB
|
RSTB
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
|
|
||||||
|
imageNoRoom:
|
||||||
|
SETD.0 NoRoomText
|
||||||
|
SWI osPrintString
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
imageTooBig:
|
imageTooBig:
|
||||||
SETD.0 TooBigText
|
SETD.0 TooBigText
|
||||||
SWI osPrintString
|
SWI osPrintString
|
||||||
@@ -1481,16 +1575,171 @@ imageTooBig:
|
|||||||
RET
|
RET
|
||||||
|
|
||||||
; Puts A down at ImgWalk and steps it.
|
; Puts A down at ImgWalk and steps it.
|
||||||
putByte:
|
; ---- Putting a byte into the file ----
|
||||||
|
;
|
||||||
|
; THE FILE IS WRITTEN AS IT IS MADE, through one block of window, rather than being built
|
||||||
|
; whole in memory and handed over at the end. Everything below works in FILE OFFSETS: a
|
||||||
|
; cursor is a two byte number counting from the front of the file, and because a block is
|
||||||
|
; two hundred and fifty six bytes, the block it lands in is the offset's HIGH byte and the
|
||||||
|
; place within that block is its LOW one. No division anywhere, which is just as well.
|
||||||
|
;
|
||||||
|
; There are two cursors and they move independently, because #Program and #Data alternate
|
||||||
|
; in source and the two segments are consecutive in the file. Whenever a byte lands in a
|
||||||
|
; block other than the one in hand, the one in hand goes back to the disk and the new one
|
||||||
|
; is fetched - so a run of bytes in one segment costs nothing extra, and a switch between
|
||||||
|
; segments costs two block operations. There are a few dozen switches in a source file and
|
||||||
|
; several thousand bytes.
|
||||||
|
;
|
||||||
|
; The block is FETCHED rather than assumed blank, and that is what makes the whole thing
|
||||||
|
; work with one window instead of three. The header is patched after every byte is out; the
|
||||||
|
; block where the program ends is the same block the data begins in; both are simply
|
||||||
|
; revisits, and a revisit is what fetching handles.
|
||||||
|
|
||||||
|
; A at the file offset in the two byte number DP2 names, which is then stepped on.
|
||||||
|
putAt:
|
||||||
SETD.0 ImgHold
|
SETD.0 ImgHold
|
||||||
STA.0
|
STA.0
|
||||||
SETD.1 ImgWalk
|
|
||||||
LDD.0.1
|
; WHICH CURSOR THIS IS, KEPT IN MEMORY. It arrives in DP2 and everything below wants DP2
|
||||||
SETD.2 ImgHold
|
; for something of its own - and a call puts DP2 back the way it was AT THE CALL, not the
|
||||||
LDA.2
|
; way it was on the way in. Leaving it there meant the step at the end moved whatever the
|
||||||
STA.0
|
; last call had left in DP2, which was the address of the window, and the window walked
|
||||||
|
; off across memory while the cursor stood still.
|
||||||
|
SETD.0 ImgCursor
|
||||||
|
STD.2.0
|
||||||
|
|
||||||
|
; Which block, and where in it: a block is two hundred and fifty six bytes, so the two
|
||||||
|
; halves of the offset are exactly those two things.
|
||||||
|
PSHD.2
|
||||||
|
POPD.0
|
||||||
|
LDA.0
|
||||||
|
SETD.1 ImgBlockWant
|
||||||
|
STA.1
|
||||||
INCD.0
|
INCD.0
|
||||||
STD.0.1
|
LDA.0
|
||||||
|
SETD.1 ImgSlot
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
CALL outHold
|
||||||
|
BNQ putAtFailed
|
||||||
|
|
||||||
|
SETD.0 OutAt
|
||||||
|
SETD.2 ScratchWindow
|
||||||
|
CALL numSet
|
||||||
|
SETD.0 OutAt
|
||||||
|
SETD.1 ImgSlot
|
||||||
|
LDA.1
|
||||||
|
CALL numAddByte
|
||||||
|
SETD.1 OutAt
|
||||||
|
LDD.0.1
|
||||||
|
SETD.1 ImgHold
|
||||||
|
LDA.1
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
INIA 0x01
|
||||||
|
SETD.0 OutDirty
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
SETD.1 ImgCursor
|
||||||
|
LDD.0.1
|
||||||
|
CALL numStep
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
putAtFailed:
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; Makes sure the block ImgBlockWant names is the one in the window, writing back whatever
|
||||||
|
; was there and fetching the new one.
|
||||||
|
outHold:
|
||||||
|
SETD.0 OutHeld
|
||||||
|
LDA.0
|
||||||
|
BRA outFetch ; Nothing in hand at all.
|
||||||
|
SETD.0 OutBlock
|
||||||
|
LDA.0
|
||||||
|
SETD.2 ImgBlockWant
|
||||||
|
LDB.2
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ outHeldAlready
|
||||||
|
|
||||||
|
CALL outFlush
|
||||||
|
BNQ outHoldNo
|
||||||
|
|
||||||
|
outFetch:
|
||||||
|
SETD.0 ImgBlockWant
|
||||||
|
LDA.0
|
||||||
|
SETD.1 OutBlock
|
||||||
|
STA.1
|
||||||
|
|
||||||
|
SETD.1 ScratchWindow
|
||||||
|
LDD.1.1
|
||||||
|
SETD.0 OutBlock
|
||||||
|
LDB.0
|
||||||
|
RSTA
|
||||||
|
SWI osFileFetch
|
||||||
|
BNQ outHoldNo
|
||||||
|
|
||||||
|
INIA 0x01
|
||||||
|
SETD.0 OutHeld
|
||||||
|
STA.0
|
||||||
|
RSTA
|
||||||
|
SETD.0 OutDirty
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
outHeldAlready:
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
outHoldNo:
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; Whatever is in the window goes back to the file, if anything has been put in it.
|
||||||
|
outFlush:
|
||||||
|
SETD.0 OutHeld
|
||||||
|
LDA.0
|
||||||
|
BRA outFlushNothing
|
||||||
|
SETD.0 OutDirty
|
||||||
|
LDA.0
|
||||||
|
BRA outFlushNothing
|
||||||
|
|
||||||
|
SETD.1 ScratchWindow
|
||||||
|
LDD.1.1
|
||||||
|
SETD.0 OutBlock
|
||||||
|
LDB.0
|
||||||
|
RSTA
|
||||||
|
SWI osFileWrite
|
||||||
|
BNQ outHoldNo
|
||||||
|
|
||||||
|
RSTA
|
||||||
|
SETD.0 OutDirty
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
outFlushNothing:
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; The header and everything else that is written in order goes through ImgWalk.
|
||||||
|
putByte:
|
||||||
|
SETD.2 ImgWalk
|
||||||
|
CALL putAt
|
||||||
RET
|
RET
|
||||||
|
|
||||||
; Puts A bytes from DP0 down at ImgWalk.
|
; Puts A bytes from DP0 down at ImgWalk.
|
||||||
@@ -1543,12 +1792,11 @@ emitByte:
|
|||||||
emitToProgram:
|
emitToProgram:
|
||||||
SETD.1 ProgPut
|
SETD.1 ProgPut
|
||||||
emitPut:
|
emitPut:
|
||||||
LDD.0.1
|
PSHD.1
|
||||||
SETD.2 EmitHold
|
POPD.2 ; ProgPut or DataPut, whichever this byte belongs to.
|
||||||
LDA.2
|
SETD.0 EmitHold
|
||||||
STA.0
|
LDA.0
|
||||||
INCD.0
|
CALL putAt
|
||||||
STD.0.1
|
|
||||||
RET
|
RET
|
||||||
|
|
||||||
; The byte at DP0 offset by A, into ClsByte. The classifier has one of these; this is the
|
; The byte at DP0 offset by A, into ClsByte. The classifier has one of these; this is the
|
||||||
@@ -1676,8 +1924,8 @@ writeVectorsHeader:
|
|||||||
LDA.0
|
LDA.0
|
||||||
BRA writeVectorsNone
|
BRA writeVectorsNone
|
||||||
SETD.0 ImgWalk
|
SETD.0 ImgWalk
|
||||||
SETD.2 ScratchImage
|
CALL numZero ; Back to the front of the file, which is a revisit like
|
||||||
CALL numSet
|
; any other: the window fetches block nought again.
|
||||||
SETD.0 ImgWalk
|
SETD.0 ImgWalk
|
||||||
INIA 0d4
|
INIA 0d4
|
||||||
CALL numAddByte
|
CALL numAddByte
|
||||||
@@ -1695,8 +1943,8 @@ writeVectorsNone:
|
|||||||
LDA.0
|
LDA.0
|
||||||
BRA writeVectorsOut
|
BRA writeVectorsOut
|
||||||
SETD.0 ImgWalk
|
SETD.0 ImgWalk
|
||||||
SETD.2 ScratchImage
|
CALL numZero ; Back to the front of the file, which is a revisit like
|
||||||
CALL numSet
|
; any other: the window fetches block nought again.
|
||||||
SETD.0 ImgWalk
|
SETD.0 ImgWalk
|
||||||
INIA 0d8
|
INIA 0d8
|
||||||
CALL numAddByte
|
CALL numAddByte
|
||||||
@@ -1772,15 +2020,32 @@ vectorNotEntry:
|
|||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
|
|
||||||
|
; Whatever is still in the window, and then the file takes its name. Nothing that was
|
||||||
|
; already on the disk has been touched until this last step.
|
||||||
writeImage:
|
writeImage:
|
||||||
SETD.0 OutName
|
CALL outFlush
|
||||||
SETD.1 ScratchImage
|
BNQ writeFailed
|
||||||
LDD.1.1
|
|
||||||
SETD.2 ImgTotal
|
; What it really came to, which is what the room was asked for less whatever the vectors
|
||||||
LDA.2
|
; did not need.
|
||||||
INCD.2
|
SETD.1 ImgTotal
|
||||||
LDB.2
|
LDA.1
|
||||||
SWI osFileSave
|
SETD.0 ImgOutBlocks
|
||||||
|
STA.0
|
||||||
|
INCD.1
|
||||||
|
LDA.1
|
||||||
|
SETD.0 ImgOutTail
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
RSTA
|
||||||
|
PSHA
|
||||||
|
SETD.0 ImgOutBlocks
|
||||||
|
LDA.0
|
||||||
|
PSHA
|
||||||
|
POPD.3
|
||||||
|
SETD.0 ImgOutTail
|
||||||
|
LDA.0
|
||||||
|
SWI osFileDone
|
||||||
BNQ writeFailed
|
BNQ writeFailed
|
||||||
RSTA
|
RSTA
|
||||||
RSTB
|
RSTB
|
||||||
@@ -1915,7 +2180,7 @@ report:
|
|||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
Argument:
|
Argument:
|
||||||
#Reserve 0d23
|
#Reserve 0d23
|
||||||
@@ -2047,11 +2312,46 @@ ImgHold:
|
|||||||
DropWalk:
|
DropWalk:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
|
|
||||||
; How big an output file this can build. Everything the assembler makes has to fit here at once,
|
; ---- The output, which goes out as it is made ----
|
||||||
; because a file is written in one call and there is nowhere to put half of one. CosmOS
|
;
|
||||||
; itself comes to 9,564 bytes.
|
; THERE IS NO LIMIT ON HOW BIG A FILE THIS CAN BUILD, and that is the point of the window
|
||||||
ImgRoom:
|
; below. The assembler used to hold the whole output in an eighteen kilobyte buffer and
|
||||||
0x34 0x00
|
; write it in one call at the end, which made the largest program it could assemble a
|
||||||
|
; property of ITS OWN memory rather than of the disk - and cosmos.bin reached 13,245 bytes
|
||||||
|
; of the 13,312 there were, sixty seven short of the system being unable to build itself.
|
||||||
|
;
|
||||||
|
; Now one block at a time goes through OutBlock, which lives in scratch like every other
|
||||||
|
; buffer here: a #Reserve is written into the file as zeroes and copied at load, so a
|
||||||
|
; program that carries its own scratch pays for it twice. What bounds the output is the
|
||||||
|
; contiguous run the disk can find for it.
|
||||||
|
;
|
||||||
|
; OutAt is the cursor, OutBlock which block of the file the window holds, OutHeld whether
|
||||||
|
; it holds one at all, and OutDirty whether anything has been put in it since it arrived.
|
||||||
|
OutAt:
|
||||||
|
0x00 0x00
|
||||||
|
OutBlock:
|
||||||
|
0x00
|
||||||
|
OutHeld:
|
||||||
|
0x00
|
||||||
|
OutDirty:
|
||||||
|
0x00
|
||||||
|
ImgBlockWant:
|
||||||
|
0x00
|
||||||
|
ImgSlot:
|
||||||
|
0x00
|
||||||
|
ImgOutBlocks:
|
||||||
|
0x00
|
||||||
|
ImgOutTail:
|
||||||
|
0x00
|
||||||
|
ImgCursor:
|
||||||
|
0x00 0x00
|
||||||
|
ImgAsk:
|
||||||
|
0x00 0x00
|
||||||
|
ImgVecMost:
|
||||||
|
0x00 0x00
|
||||||
|
NoRoomText:
|
||||||
|
"no room on the disk for the output
|
||||||
|
"
|
||||||
|
|
||||||
MagicSPBT:
|
MagicSPBT:
|
||||||
"SPBT"
|
"SPBT"
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ clsToken:
|
|||||||
INIB 0x23 ; '#'
|
INIB 0x23 ; '#'
|
||||||
XOR
|
XOR
|
||||||
BNQ clsTryInstruction
|
BNQ clsTryInstruction
|
||||||
INIA 0d0
|
RSTA
|
||||||
SETD.0 ClsType
|
SETD.0 ClsType
|
||||||
STA.0
|
STA.0
|
||||||
BRI clsYes
|
BRI clsYes
|
||||||
|
|||||||
@@ -297,13 +297,18 @@ LabEnd:
|
|||||||
; How many labels there may be, and how many bytes of name between them.
|
; How many labels there may be, and how many bytes of name between them.
|
||||||
;
|
;
|
||||||
; SIZED FOR THE ASSEMBLER ITSELF, which turns out to be the largest thing it is asked to
|
; SIZED FOR THE ASSEMBLER ITSELF, which turns out to be the largest thing it is asked to
|
||||||
; build: 555 labels and about 6,800 bytes of name, against CosmOS's 475 and 5,881. Running into
|
; build: 555 labels and about 6,800 bytes of name, against CosmOS's 650 and 8,081 - CosmOS is
|
||||||
; either limit says so rather than writing past the end of the table. The buffers live
|
; the bigger of the two now, and was the smaller when this was written. Running into either
|
||||||
|
; limit says so rather than writing past the end of the table, and that is what it did. The buffers live
|
||||||
; above the program rather than inside it - see the scratch map in Asm.asm.
|
; above the program rather than inside it - see the scratch map in Asm.asm.
|
||||||
|
; Fifteen hundred and thirty six names, and sixteen kilobytes to hold them in. Both were
|
||||||
|
; half that, and the names ran out first: 8,081 bytes of 8,192, which is a hundred and
|
||||||
|
; eleven - and the thing that ran into it was one ordinary piece of work adding eighteen
|
||||||
|
; labels. THESE TWO MUST AGREE WITH THE SCRATCH MAP, which says where the room actually is.
|
||||||
LabLimit:
|
LabLimit:
|
||||||
0x03 0x00
|
0x06 0x00
|
||||||
LabRoom:
|
LabRoom:
|
||||||
0x20 0x00
|
0x40 0x00
|
||||||
|
|
||||||
LabNamed:
|
LabNamed:
|
||||||
": "
|
": "
|
||||||
|
|||||||
@@ -28,16 +28,16 @@ numSet:
|
|||||||
|
|
||||||
; The two byte number at DP0 becomes itself plus the one at DP2.
|
; The two byte number at DP0 becomes itself plus the one at DP2.
|
||||||
numAdd:
|
numAdd:
|
||||||
DPUP.0 0d01
|
INCD.0
|
||||||
DPUP.2 0d01
|
INCD.2
|
||||||
LDA.0
|
LDA.0
|
||||||
LDB.2
|
LDB.2
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
MVQA
|
MVQA
|
||||||
STA.0
|
STA.0
|
||||||
DPDN.0 0d01
|
DECD.0
|
||||||
DPDN.2 0d01
|
DECD.2
|
||||||
LDA.0
|
LDA.0
|
||||||
LDB.2
|
LDB.2
|
||||||
ADD ; Carries in from the low half. Nothing between touches it.
|
ADD ; Carries in from the low half. Nothing between touches it.
|
||||||
@@ -47,16 +47,16 @@ numAdd:
|
|||||||
|
|
||||||
; The two byte number at DP0 becomes itself less the one at DP2.
|
; The two byte number at DP0 becomes itself less the one at DP2.
|
||||||
numTake:
|
numTake:
|
||||||
DPUP.0 0d01
|
INCD.0
|
||||||
DPUP.2 0d01
|
INCD.2
|
||||||
LDA.0
|
LDA.0
|
||||||
LDB.2
|
LDB.2
|
||||||
CCF
|
CCF
|
||||||
SUB
|
SUB
|
||||||
MVQA
|
MVQA
|
||||||
STA.0
|
STA.0
|
||||||
DPDN.0 0d01
|
DECD.0
|
||||||
DPDN.2 0d01
|
DECD.2
|
||||||
LDA.0
|
LDA.0
|
||||||
LDB.2
|
LDB.2
|
||||||
SUB ; Borrows in from the low half.
|
SUB ; Borrows in from the low half.
|
||||||
@@ -66,14 +66,14 @@ numTake:
|
|||||||
|
|
||||||
; Adds the byte in A to the two byte number at DP0.
|
; Adds the byte in A to the two byte number at DP0.
|
||||||
numAddByte:
|
numAddByte:
|
||||||
DPUP.0 0d01
|
INCD.0
|
||||||
LDB.0
|
LDB.0
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
MVQA
|
MVQA
|
||||||
STA.0
|
STA.0
|
||||||
BNC numAddByteDone
|
BNC numAddByteDone
|
||||||
DPDN.0 0d01
|
DECD.0
|
||||||
LDA.0
|
LDA.0
|
||||||
INCA
|
INCA
|
||||||
STA.0
|
STA.0
|
||||||
@@ -82,12 +82,12 @@ numAddByteDone:
|
|||||||
|
|
||||||
; Adds one to the two byte number at DP0.
|
; Adds one to the two byte number at DP0.
|
||||||
numStep:
|
numStep:
|
||||||
DPUP.0 0d01
|
INCD.0
|
||||||
LDA.0
|
LDA.0
|
||||||
INCA
|
INCA
|
||||||
STA.0
|
STA.0
|
||||||
BNC numStepDone ; It did not wrap, so the high byte is untouched.
|
BNC numStepDone ; It did not wrap, so the high byte is untouched.
|
||||||
DPDN.0 0d01
|
DECD.0
|
||||||
LDA.0
|
LDA.0
|
||||||
INCA
|
INCA
|
||||||
STA.0
|
STA.0
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
SETD.0 Wanted
|
SETD.0 Wanted
|
||||||
@@ -60,7 +60,7 @@ noFile:
|
|||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
Wanted:
|
Wanted:
|
||||||
#Reserve 0d23
|
#Reserve 0d23
|
||||||
|
|||||||
@@ -12,33 +12,79 @@
|
|||||||
;
|
;
|
||||||
; None of this is initialised data. It is scratch, wanted only while the assembler is
|
; None of this is initialised data. It is scratch, wanted only while the assembler is
|
||||||
; running, and while it is running everything above its own data is free: the system keeps
|
; running, and while it is running everything above its own data is free: the system keeps
|
||||||
; below 0x1000, the staging area is only in use during a load, and the Stack comes down
|
; below 0x1FFF, the staging area is only in use during a load, and the Stack comes down
|
||||||
; from the top. So the addresses are written down here and the file carries none of it.
|
; from the top. So the addresses are written down here and the file carries none of it.
|
||||||
;
|
;
|
||||||
; 0x8000 3072 the label index, 768 entries of four
|
; That sentence said 0x1000 for a while after the system's half of Data Memory was
|
||||||
; 0x8C00 8192 the label names, packed end to end
|
; doubled, twelve lines above the paragraph that explains the doubling. A stale number is
|
||||||
; 0xAC00 13312 the output file being built
|
; bad enough; a stale number sitting next to the correction is worse, because whichever
|
||||||
; 0xE000 1792 the vector names, 64 entries of twenty eight
|
; one a reader takes is a coin toss.
|
||||||
; 0xE700 1758 the reader's stack, six levels of 293
|
|
||||||
; 0xEE00 368 which files have been included, sixteen names of 23
|
|
||||||
;
|
;
|
||||||
; That ends at 0xEF70, with the Stack coming down from 0xFFFF above it - about four
|
; 0x4000 6144 the label index, 1536 entries of four
|
||||||
|
; 0x5800 16384 the label names, packed end to end
|
||||||
|
; 0x9800 256 one block of the output file, on its way to the disk
|
||||||
|
; 0x9900 18176 free
|
||||||
|
; 0xE000 1792 the vector names, 64 entries of twenty eight
|
||||||
|
; 0xE700 2048 the reader's stack, six levels of 301
|
||||||
|
; 0xEF00 368 which files have been included, sixteen names of 23
|
||||||
|
;
|
||||||
|
; IT USED TO START AT 0x8000, and the reason given was that everything above the
|
||||||
|
; assembler's own data is free. That was true when it was written and stopped being true
|
||||||
|
; without anything noticing: the system kept below 0x1000 then, and its data now reaches
|
||||||
|
; 0x1FFF, and the assembler's own moved from 0x1000 to 0x2000 with it. The floor came up
|
||||||
|
; and the map stayed where it was, leaving sixteen kilobytes between the two that nothing
|
||||||
|
; touched.
|
||||||
|
;
|
||||||
|
; Starting at 0x4000 takes that back. The assembler's data is a little over four kilobytes
|
||||||
|
; from 0x2000, so there is still nearly four kilobytes of slack in front of this - and room
|
||||||
|
; for its data to double before the two would meet. `make test` measures that gap now
|
||||||
|
; rather than trusting this paragraph, and measures the floor above as well, because both
|
||||||
|
; of those numbers describe the machine AROUND this file and neither is enforced by a line
|
||||||
|
; of code anywhere.
|
||||||
|
;
|
||||||
|
; THE ROOM WENT TO ALL THREE OF THE BUFFERS THAT WERE FULL, and there turned out to be
|
||||||
|
; three rather than one. The output was the obvious wall - cosmos.bin was 13,245 bytes
|
||||||
|
; against 13,312, which is sixty seven - so it was given the lot, and the very next thing
|
||||||
|
; added to the system ran out of LABEL NAMES instead, at 8,081 of 8,192. Two ceilings a
|
||||||
|
; hundred bytes apart look like one ceiling until the first is lifted.
|
||||||
|
;
|
||||||
|
; The index was a hundred and eighteen entries from the same place. So: names doubled,
|
||||||
|
; index doubled, and the output given what is left, which is still four and a half thousand
|
||||||
|
; bytes more than CosmOS needs today.
|
||||||
|
;
|
||||||
|
; THE OUTPUT IS NO LONGER HELD AT ALL. It used to be built whole in memory and handed over
|
||||||
|
; at the end, which is what made a buffer of eighteen kilobytes the largest thing this
|
||||||
|
; machine could assemble. The file is produced in order, so it is written as it is made,
|
||||||
|
; through one block of window - and the eighteen kilobytes that were its share are free.
|
||||||
|
;
|
||||||
|
; What to do with them is not obvious and does not have to be decided today. Nothing here
|
||||||
|
; is close to full: the names are at half, the index at a third, and the output has no
|
||||||
|
; ceiling of its own any more. Leaving the room unclaimed is better than sharing it out
|
||||||
|
; among buffers that do not need it, because an unclaimed page is available to whichever
|
||||||
|
; one turns out to want it.
|
||||||
|
;
|
||||||
|
; That ends at 0xF070, with the Stack coming down from 0xFFFF above it - nearly four
|
||||||
; kilobytes, against the tens of bytes of CALL frames this ever nests.
|
; kilobytes, against the tens of bytes of CALL frames this ever nests.
|
||||||
;
|
;
|
||||||
|
; The reader's levels went from 293 to 301 when an include gained somewhere to be looked
|
||||||
|
; for: the name in each level is a PATH now, and "/Lib/" is five characters of it. Six
|
||||||
|
; levels of 301 is 1806, so the room here has to stay above that - which is why the include
|
||||||
|
; list moved up rather than the stack simply being asked to fit.
|
||||||
|
;
|
||||||
; THE TWO THINGS THAT DECIDE THESE SIZES are the largest program it will be asked to build
|
; THE TWO THINGS THAT DECIDE THESE SIZES are the largest program it will be asked to build
|
||||||
; and the largest one it will be asked to read. CosmOS is 475 labels and 9,564 bytes of
|
; and the largest one it will be asked to read. CosmOS is 475 labels and 9,564 bytes of
|
||||||
; output; the assembler itself is 555 labels, about 6,800 bytes of name and 11,648 of output. The
|
; output; the assembler itself is 555 labels, about 6,800 bytes of name and 11,648 of output. The
|
||||||
; second is bigger than the first, which is worth knowing: the hardest thing this assembles
|
; second is bigger than the first, which is worth knowing: the hardest thing this assembles
|
||||||
; is not the operating system, it is itself.
|
; is not the operating system, it is itself.
|
||||||
ScratchLabIndex:
|
ScratchLabIndex:
|
||||||
0x80 0x00
|
0x40 0x00
|
||||||
ScratchLabArena:
|
ScratchLabArena:
|
||||||
0x8C 0x00
|
0x58 0x00
|
||||||
ScratchImage:
|
ScratchWindow:
|
||||||
0xAC 0x00
|
0x98 0x00
|
||||||
ScratchVecNames:
|
ScratchVecNames:
|
||||||
0xE0 0x00
|
0xE0 0x00
|
||||||
ScratchSrcStack:
|
ScratchSrcStack:
|
||||||
0xE7 0x00
|
0xE7 0x00
|
||||||
ScratchIncNames:
|
ScratchIncNames:
|
||||||
0xEE 0x00
|
0xEF 0x00
|
||||||
|
|||||||
@@ -189,13 +189,53 @@ srcInclude:
|
|||||||
|
|
||||||
CALL srcRemember
|
CALL srcRemember
|
||||||
CALL srcPush
|
CALL srcPush
|
||||||
|
|
||||||
|
; TWO PLACES, TRIED IN ORDER: where you are, and then /Lib. The same rule the shell uses
|
||||||
|
; for a program it does not recognise, which is where it came from - a name means the
|
||||||
|
; one beside you if there is one, and the system's otherwise.
|
||||||
|
;
|
||||||
|
; It is what a search path is for, and the host assembler has had one since before there
|
||||||
|
; was a machine to run this on. Without it every source that calls a service has to sit
|
||||||
|
; in the same directory as services.asm, and the disk cannot be organised at all.
|
||||||
SETD.0 IncWanted
|
SETD.0 IncWanted
|
||||||
SETD.1 SrcName
|
SETD.1 SrcName
|
||||||
CALL srcKeepName
|
CALL srcKeepName
|
||||||
CALL srcRewind
|
CALL srcRewind
|
||||||
|
BRQ srcIncludeIn
|
||||||
|
|
||||||
|
CALL srcInLibrary
|
||||||
|
CALL srcRewind
|
||||||
BNQ srcIncludeGone
|
BNQ srcIncludeGone
|
||||||
|
srcIncludeIn:
|
||||||
RET ; Q is zero, out of srcRewind.
|
RET ; Q is zero, out of srcRewind.
|
||||||
|
|
||||||
|
; SrcName becomes the same name inside the library directory. Built here rather than kept
|
||||||
|
; as a second buffer, because what has to survive is the name the file was ASKED for -
|
||||||
|
; that is what the include-once list holds, and a file found in the library on one line
|
||||||
|
; and beside you on another is still the same include.
|
||||||
|
srcInLibrary:
|
||||||
|
SETD.0 SrcLibrary
|
||||||
|
SETD.1 SrcName
|
||||||
|
srcLibraryPrefix:
|
||||||
|
LDA.0
|
||||||
|
BRA srcLibraryName
|
||||||
|
STA.1
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
BRI srcLibraryPrefix
|
||||||
|
|
||||||
|
srcLibraryName:
|
||||||
|
SETD.0 IncWanted
|
||||||
|
srcLibraryCopy:
|
||||||
|
LDA.0
|
||||||
|
STA.1
|
||||||
|
BRA srcLibraryDone
|
||||||
|
INCD.0
|
||||||
|
INCD.1
|
||||||
|
BRI srcLibraryCopy
|
||||||
|
srcLibraryDone:
|
||||||
|
RET
|
||||||
|
|
||||||
srcIncludeSkip:
|
srcIncludeSkip:
|
||||||
RSTA
|
RSTA
|
||||||
RSTB
|
RSTB
|
||||||
@@ -483,8 +523,12 @@ srcKeepEnd:
|
|||||||
; ---- The current file, as one block so that it can be put aside in one piece ----
|
; ---- The current file, as one block so that it can be put aside in one piece ----
|
||||||
;
|
;
|
||||||
SrcState:
|
SrcState:
|
||||||
|
; THIRTY TWO RATHER THAN THE TWENTY THREE A NAME NEEDS, because what goes here is a PATH:
|
||||||
|
; an include not found beside you is looked for in the library, and "/Lib/" plus a name of
|
||||||
|
; twenty two plus the zero that ends it is twenty eight. Every block of the file is asked
|
||||||
|
; for by this name, so it has to be the one that resolves, not the one that was typed.
|
||||||
SrcName:
|
SrcName:
|
||||||
#Reserve 0d23
|
#Reserve 0d32
|
||||||
SrcBlocks:
|
SrcBlocks:
|
||||||
0x00 0x00
|
0x00 0x00
|
||||||
SrcIndex:
|
SrcIndex:
|
||||||
@@ -504,12 +548,19 @@ SrcEnded:
|
|||||||
SrcBuffer:
|
SrcBuffer:
|
||||||
#Reserve 0d256
|
#Reserve 0d256
|
||||||
|
|
||||||
; 292 bytes: a name of 23, six numbers of two, one single byte, and the buffer. NOTHING MAY
|
; 301 bytes: a name of 32, six numbers of two, one single byte, and the buffer. NOTHING MAY
|
||||||
; BE ADDED IN THE MIDDLE OF THE BLOCK ABOVE without changing this to match.
|
; BE ADDED IN THE MIDDLE OF THE BLOCK ABOVE without changing this to match, and the room
|
||||||
|
; set aside for six of them in scratch.asm has to be at least six times it.
|
||||||
SrcStateBytes:
|
SrcStateBytes:
|
||||||
0x01 0x24
|
0x01 0x2D
|
||||||
SrcDepthLimit:
|
SrcDepthLimit:
|
||||||
0d6
|
0d6
|
||||||
|
|
||||||
|
; Where an include is looked for when it is not beside you. One fixed place rather than a
|
||||||
|
; list somebody sets, for the same reason the shell has one fixed place for programs: a
|
||||||
|
; list would need somewhere to live between one boot and the next.
|
||||||
|
SrcLibrary:
|
||||||
|
"/Lib/"
|
||||||
SrcOne:
|
SrcOne:
|
||||||
0x00 0x01
|
0x00 0x01
|
||||||
|
|
||||||
|
|||||||
@@ -37,32 +37,35 @@ AsmShapeSelectors:
|
|||||||
0d0 0d0 0d0 0d1 0d1 0d1 0d2
|
0d0 0d0 0d0 0d1 0d1 0d1 0d2
|
||||||
|
|
||||||
AsmInstructionCount:
|
AsmInstructionCount:
|
||||||
0d64
|
0d72
|
||||||
|
|
||||||
AsmInstructions:
|
AsmInstructions:
|
||||||
0x00 0d0 "ADD "
|
0x10 0d0 "ADD "
|
||||||
0x01 0d0 "SUB "
|
0x11 0d0 "SUB "
|
||||||
0x02 0d0 "AND "
|
0x12 0d0 "AND "
|
||||||
0x03 0d0 "OR "
|
0x13 0d0 "OR "
|
||||||
0x04 0d0 "XOR "
|
0x14 0d0 "XOR "
|
||||||
0x05 0d0 "NOTA"
|
0x15 0d0 "NOTA"
|
||||||
0x06 0d0 "NOTB"
|
0x16 0d0 "NOTB"
|
||||||
0x07 0d0 "SHL "
|
0x17 0d0 "SHL "
|
||||||
0x08 0d0 "SHR "
|
0x18 0d0 "SHR "
|
||||||
0x10 0d1 "BRI "
|
0x60 0d1 "BRI "
|
||||||
0x11 0d1 "BRQ "
|
0x61 0d1 "BRQ "
|
||||||
0x12 0d1 "BRA "
|
0x62 0d1 "BRA "
|
||||||
0x13 0d1 "BRB "
|
0x63 0d1 "BRB "
|
||||||
0x14 0d1 "BRC "
|
0x64 0d1 "BRC "
|
||||||
0x15 0d3 "BRD "
|
0x65 0d3 "BRD "
|
||||||
0x1A 0d1 "BNQ "
|
0x66 0d1 "BNQ "
|
||||||
0x1B 0d1 "BNA "
|
0x67 0d1 "BNA "
|
||||||
0x1C 0d1 "BNB "
|
0x68 0d1 "BNB "
|
||||||
0x1D 0d1 "BNC "
|
0x69 0d1 "BNC "
|
||||||
0x17 0d1 "CALL"
|
0x70 0d1 "RCAL"
|
||||||
0x18 0d2 "SWI "
|
0x71 0d1 "CALL"
|
||||||
0x19 0d0 "RETI"
|
0x72 0d2 "SWI "
|
||||||
0x1F 0d0 "RET "
|
0x73 0d0 "RETI"
|
||||||
|
0x74 0d0 "RRET"
|
||||||
|
0x75 0d0 "RET "
|
||||||
|
0x76 0d0 "SRET"
|
||||||
0x20 0d0 "RSTA"
|
0x20 0d0 "RSTA"
|
||||||
0x21 0d0 "RSTB"
|
0x21 0d0 "RSTB"
|
||||||
0x22 0d0 "INCA"
|
0x22 0d0 "INCA"
|
||||||
@@ -97,10 +100,15 @@ AsmInstructions:
|
|||||||
0x4B 0d6 "STD "
|
0x4B 0d6 "STD "
|
||||||
0x4C 0d3 "MVSD"
|
0x4C 0d3 "MVSD"
|
||||||
0x4D 0d3 "MVDS"
|
0x4D 0d3 "MVDS"
|
||||||
|
0x4E 0d3 "DPUA"
|
||||||
|
0x4F 0d3 "DPDA"
|
||||||
|
0x50 0d3 "DPUW"
|
||||||
|
0x51 0d3 "DPDW"
|
||||||
0xD0 0d2 "OUTQ"
|
0xD0 0d2 "OUTQ"
|
||||||
0xD1 0d2 "OUTA"
|
0xD1 0d2 "OUTA"
|
||||||
0xD2 0d2 "OUTB"
|
0xD2 0d2 "OUTB"
|
||||||
0xE0 0d2 "INA "
|
0xE0 0d2 "INA "
|
||||||
0xE1 0d2 "INB "
|
0xE1 0d2 "INB "
|
||||||
0xF0 0d0 "NOP "
|
0xF0 0d0 "NOP "
|
||||||
|
0xFE 0d0 "WAIT"
|
||||||
0xFF 0d0 "HALT"
|
0xFF 0d0 "HALT"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
SETD.0 Wanted
|
SETD.0 Wanted
|
||||||
@@ -128,7 +128,7 @@ noFile:
|
|||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
Wanted:
|
Wanted:
|
||||||
#Reserve 0d23
|
#Reserve 0d23
|
||||||
|
|||||||
@@ -189,7 +189,6 @@ vecReadFields:
|
|||||||
STA.1
|
STA.1
|
||||||
INCD.0
|
INCD.0
|
||||||
LDA.0
|
LDA.0
|
||||||
SETD.1 VecHandler
|
|
||||||
INCD.1
|
INCD.1
|
||||||
STA.1
|
STA.1
|
||||||
INCD.0
|
INCD.0
|
||||||
|
|||||||
+492
-32
@@ -17,9 +17,19 @@ for itself.
|
|||||||
- Interactive Shell: Read commands from the SplitBit console and continue until `exit` or
|
- Interactive Shell: Read commands from the SplitBit console and continue until `exit` or
|
||||||
the end of input.
|
the end of input.
|
||||||
- SBFS Filesystem: Mount, list, read, write, delete, and rename files on a SplitBit disk.
|
- SBFS Filesystem: Mount, list, read, write, delete, and rename files on a SplitBit disk.
|
||||||
|
- Paths: Anywhere a filename is taken, a path may be given instead - names with `/`
|
||||||
|
between them, with `.` and `..`. Directories are read but not yet made; the host tool
|
||||||
|
makes them.
|
||||||
|
- Working Directory: `cd` moves the machine, `dir` lists where it is, and the prompt says
|
||||||
|
where that is once it is not the root. A program may move too, and the shell puts the
|
||||||
|
working directory back when the program stops.
|
||||||
|
- Making Directories: `mkdir` and `rmdir` on the machine, and files written where their
|
||||||
|
path says, so a disk can be organised without the host tool.
|
||||||
- Loadable Applications: Validate SBEX files, copy their Program and Data segments into
|
- 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
|
the addresses for which they were assembled, and start them at their declared entry
|
||||||
point.
|
point.
|
||||||
|
- Invocation By Name: A word the shell has no command for is looked for on the disk as
|
||||||
|
`<name>.sbx`, and loaded and started if it is there. Built-in commands are tried first.
|
||||||
- Resident Services: Applications can print strings and numbers, read lines, receive their
|
- 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
|
command arguments, read and write files, and return to the shell through named software
|
||||||
interrupts.
|
interrupts.
|
||||||
@@ -81,8 +91,12 @@ CosmOS currently provides these built-in commands:
|
|||||||
| Command | Description |
|
| Command | Description |
|
||||||
| -- | -- |
|
| -- | -- |
|
||||||
| `dir` | List the files on the mounted disk and their sizes. |
|
| `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. |
|
| `load <path>` | 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. |
|
| `run [words]` | Start the loaded application and make the rest of the line available to it as an argument. |
|
||||||
|
| `cd [path]` | Go to a directory, or to the root with nothing after it. |
|
||||||
|
| `mkdir <path>` | Make a directory. |
|
||||||
|
| `rmdir <path>` | Remove one, if it is empty. |
|
||||||
|
| `<name> [words]` | Any word the shell does not recognise is looked for on the disk as `<name>.sbx`, and loaded and started if it is there. |
|
||||||
| `delete <file>` | Remove a file from the filesystem and release its blocks. |
|
| `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. |
|
| `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. |
|
| `monitor` | Enter monitor mode, in which the prompt becomes `*` and the commands below are also available. |
|
||||||
@@ -115,14 +129,227 @@ For example:
|
|||||||
> run
|
> run
|
||||||
```
|
```
|
||||||
|
|
||||||
Loading and running are separate operations for now. A loaded program may be run again
|
Or, equivalently:
|
||||||
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.
|
```text
|
||||||
|
> Snake
|
||||||
|
```
|
||||||
|
|
||||||
|
Loading and running remain separate operations, and both of them remain. 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; and `load` is how the monitor puts an arbitrary file in front of itself, which
|
||||||
|
is a thing typing a name deliberately cannot do.
|
||||||
|
|
||||||
|
### Paths:
|
||||||
|
|
||||||
|
Everywhere CosmOS takes a filename it will take a path: names with `/` between them,
|
||||||
|
walked from the root, with `.` meaning where you are and `..` meaning the directory above.
|
||||||
|
`..` from the root is the root. A bare name is a path of one name, so nothing written
|
||||||
|
before directories existed had to change.
|
||||||
|
|
||||||
|
```text
|
||||||
|
> load /Apps/Snake.sbx
|
||||||
|
> Type /Notes/today.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Programs did not have to be taught any of this.** Path resolution lives inside
|
||||||
|
`sbfsFind`, below the services, so `osFileInfo`, `osFileBlock`, `osFileSave`,
|
||||||
|
`osFileDelete` and `osFileRename` all still take a pointer to a name - and a path is
|
||||||
|
simply a longer name. `Type`, `More`, `Edit` and the assembler gained subdirectories
|
||||||
|
without a line being changed in any of them.
|
||||||
|
|
||||||
|
Each *name* along a path is still the 22 characters a directory entry holds, and a longer
|
||||||
|
one is refused rather than cut short, because a name cut to 22 characters is a different
|
||||||
|
name that might well be some other file's.
|
||||||
|
|
||||||
|
### The Working Directory:
|
||||||
|
|
||||||
|
`cd` moves the machine. A path beginning with `/` is measured from the root and anything
|
||||||
|
else from where you are, so a bare name means a file in the current directory - which is
|
||||||
|
the whole of what a working directory is, and no program had to be told.
|
||||||
|
|
||||||
|
```text
|
||||||
|
> cd /Apps
|
||||||
|
/Apps> dir
|
||||||
|
/Apps> cd Deep
|
||||||
|
/Apps/Deep> cd ..
|
||||||
|
/Apps> cd
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
`cd` with nothing after it goes to the root, which is the only place always there.
|
||||||
|
|
||||||
|
**The prompt says where you are, but only when that is not the root**, so a machine nobody
|
||||||
|
has moved about on looks exactly as it always did. Nothing stores the path: the working
|
||||||
|
directory is an entry index and two bytes, and the text on the prompt is worked out again
|
||||||
|
each time by walking the chain of parents upward.
|
||||||
|
|
||||||
|
That walk goes from where you are up to the root, so the names arrive deepest first and
|
||||||
|
are written into the buffer **backwards, from its end**. When they do not all fit, what is
|
||||||
|
already down is the deep end of the path, which is the end worth keeping - so the prompt
|
||||||
|
is cut at the front and says so:
|
||||||
|
|
||||||
|
```
|
||||||
|
...opqrst03/abcdefghijklmnopqrst04/.../abcdefghijklmnopqrst08>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Three limits, and the smallest is not the one you would guess.** A path handed to any
|
||||||
|
one operation is capped at 95 characters up to the last separator plus a name of 22; the
|
||||||
|
host tool carries 512, which only means it can build a tree CosmOS cannot name in one
|
||||||
|
piece. Neither of those binds anything: the longest path on a full install is 21
|
||||||
|
characters. What binds is the prompt's 127 bytes - and **nothing caps how deep the
|
||||||
|
directories go**, because `mkdir a` and `cd a` are each well inside every limit and can be
|
||||||
|
typed all day.
|
||||||
|
|
||||||
|
Before the walk was bounded it wrote past the front of that buffer and into whatever the
|
||||||
|
assembler had put below it, which was the shell's own command names. Six directories of 22
|
||||||
|
characters was enough. The first five bytes to go were the word `exit`, so the shell
|
||||||
|
stopped recognising the command for leaving - a fault with no plausible connection to the
|
||||||
|
directory you happened to be standing in.
|
||||||
|
|
||||||
|
`dir` lists the directory you are in rather than the whole disk.
|
||||||
|
|
||||||
|
A program can move too, with `osChangeDir`, and **the shell puts the working directory
|
||||||
|
back when the program stops** - the same discipline it already applies to the Stack and to
|
||||||
|
the vector table, and for the same reason. A program is entitled to move about; the shell
|
||||||
|
is entitled to find itself where it left off.
|
||||||
|
|
||||||
|
Whenever what a relative path means changes - a `cd`, a program calling `osChangeDir`, a
|
||||||
|
program exiting - CosmOS forgets the file it was remembering. That cache is keyed on the
|
||||||
|
path as it was typed, so `notes.txt` is the same key in two directories and nothing about
|
||||||
|
the entry it holds would look wrong. It is the kind of stale that gets believed rather
|
||||||
|
than noticed.
|
||||||
|
|
||||||
|
### Making And Removing Directories:
|
||||||
|
|
||||||
|
`mkdir` and `rmdir` are the machine's own, so a disk can be organised without the host
|
||||||
|
tool. A file a program writes goes where its path says, and a bare name means the
|
||||||
|
directory you are in.
|
||||||
|
|
||||||
|
**A directory costs one entry and no blocks at all.** Its start, block count and tail are
|
||||||
|
all zero, which is what keeps the flat array of entries the whole allocation map - with
|
||||||
|
files laid down contiguously, every block is inside some entry's range or it is not, and
|
||||||
|
an entry with no range is in nobody's way.
|
||||||
|
|
||||||
|
Making the first directory on a disk is what raises it from version one to version two,
|
||||||
|
because it is the only thing that makes the difference between them real. A disk stays
|
||||||
|
readable by anything that has never heard of a directory right up until it actually has
|
||||||
|
one.
|
||||||
|
|
||||||
|
**A disk may have at most 8,191 directory blocks**, which is 65,528 entries, and that
|
||||||
|
number comes from the parent field rather than from anything about size. A parent is an
|
||||||
|
index *plus one* in two bytes, so entry 65,535 has no parent number at all: adding one
|
||||||
|
wraps to zero, and zero means the root.
|
||||||
|
|
||||||
|
The failure is worth describing, because it is the shape of failure this format has to
|
||||||
|
watch for. Such an entry does not refuse what is put inside it. It writes a parent of zero
|
||||||
|
and the thing lands in **the root**, while whatever asked is told it went where it asked
|
||||||
|
for. Looking in that directory afterwards finds nothing, because the search is for a
|
||||||
|
parent the entry does not carry - so the same create succeeds again, and again, filling
|
||||||
|
the root with entries of one name. Two entries of one name in one directory is precisely
|
||||||
|
what `rename` refuses on the grounds that a search answers with whichever it meets first
|
||||||
|
and the rest can never be reached again; this made them by the handful, one per attempt.
|
||||||
|
|
||||||
|
Both implementations refuse to format past the bound, and refuse to read a disk that
|
||||||
|
claims it - because a disk claiming it was made by something that never checked.
|
||||||
|
|
||||||
|
Four things are refused, and each refusal is the reason a separate command exists:
|
||||||
|
|
||||||
|
**`rmdir` will not take a file and `delete` will not take a directory.** Neither can be
|
||||||
|
the one that removed more than was asked for.
|
||||||
|
|
||||||
|
**A directory with anything in it is refused.** This is not politeness. A parent is an
|
||||||
|
entry *index*, and a freed index is handed to the next thing created - so the children of
|
||||||
|
a directory removed from under them would turn up inside whatever took its place. Nothing
|
||||||
|
points downward, so there would be no way to find them afterwards and no way to notice.
|
||||||
|
|
||||||
|
**A name already used in that directory is refused.** Two entries with one name in one
|
||||||
|
place is a directory that cannot be searched sensibly: a search answers with whichever it
|
||||||
|
meets first, and the other becomes unreachable without ever having been deleted. The same
|
||||||
|
name in a *different* directory is fine, and is the point of the exercise.
|
||||||
|
|
||||||
|
**`rename` will not move anything.** Only the twenty two bytes of the name change and the
|
||||||
|
parent is not among them, so `rename a/x b/y` would be a lie the disk went along with.
|
||||||
|
|
||||||
|
`Tests/agree.sh` builds the same disk twice, once with SplitDisk and once with CosmOS, and
|
||||||
|
compares the images byte for byte. Every field one writes and the other only reads is
|
||||||
|
checked there and nowhere else: which entry a thing lands in, which block, what a
|
||||||
|
directory's unused fields hold, the version, the free count.
|
||||||
|
|
||||||
|
### Starting An Application By Name:
|
||||||
|
|
||||||
|
A word the shell has no command for is not immediately an error. Before saying so, the
|
||||||
|
shell adds `.sbx` to it unless it is already there, looks for a file of that name, and if
|
||||||
|
one is there loads it and starts it exactly as `load` and `run` would. Whatever followed
|
||||||
|
the word reaches the program through `osArgument`, the same way and by the same route as
|
||||||
|
whatever follows `run`.
|
||||||
|
|
||||||
|
Three properties of this are deliberate:
|
||||||
|
|
||||||
|
**Built-in commands are tried first and always win.** The search happens only after the
|
||||||
|
whole dispatch chain has failed to match, so a file named `dir.sbx` cannot become `dir`.
|
||||||
|
The commands that are worth trusting when the disk is the thing being doubted stay
|
||||||
|
trustworthy.
|
||||||
|
|
||||||
|
**The extension is what makes a file reachable by name.** Typing `notes` looks for
|
||||||
|
`notes.sbx`, and typing `notes.txt` looks for `notes.txt.sbx`. A text file therefore
|
||||||
|
cannot be started by typing what it is called, whatever happens to be inside it. Only
|
||||||
|
`load` reaches a file by its literal name.
|
||||||
|
|
||||||
|
A path works here too, so `/Apps/Say hello` starts `/Apps/Say.sbx` and gives it `hello`.
|
||||||
|
|
||||||
|
**Two places are tried, in order: where you are, and then `/Apps`.** The first is what makes
|
||||||
|
a program you are working on the one that runs; the second is what lets `Snake` work from
|
||||||
|
anywhere without a copy of it in every directory. A word that already begins with `/` has
|
||||||
|
said where to look, so only that place is tried. Neither is stored anywhere, so there is
|
||||||
|
nothing to configure and nothing to go stale - a search path somebody could set would need
|
||||||
|
somewhere to live between one boot and the next, and there is no such place yet.
|
||||||
|
|
||||||
|
**A file that is found but is broken says so.** If `notes.sbx` exists and is not an SBEX
|
||||||
|
program, typing `notes` reports `not a program` rather than `I do not know: notes`.
|
||||||
|
Reporting an unknown command about a file that is sitting on the disk would send somebody
|
||||||
|
looking in the wrong place.
|
||||||
|
|
||||||
|
Names are matched exactly, including case, because every other name on the filesystem is.
|
||||||
|
What limits the typed word is the buffer it is built in rather than the format: each name
|
||||||
|
along a path is still twenty two characters, and the path walker refuses a longer one
|
||||||
|
rather than cutting it down. A word that will not fit is reported as unknown, which is the
|
||||||
|
truth, since nothing the shell can reach is called that.
|
||||||
|
|
||||||
CosmOS also boots without a disk. It reports that no filesystem was found, leaves the
|
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
|
shell and memory monitor available, and refuses commands that require a mounted disk
|
||||||
without stopping the machine.
|
without stopping the machine.
|
||||||
|
|
||||||
|
## What Is On The Disk:
|
||||||
|
|
||||||
|
`make -C Programs cosmos-disk` builds the disk this system is meant to be met on, and it is
|
||||||
|
laid out in three directories:
|
||||||
|
|
||||||
|
| Where | What |
|
||||||
|
| -- | -- |
|
||||||
|
| `/Apps` | The programs. The second place the shell looks for a word it does not recognise, so anything here starts by name from anywhere on the disk. |
|
||||||
|
| `/Source` | The things you name to the assembler: CosmOS itself, the assembler itself, and small programs to read. |
|
||||||
|
| `/Lib` | The things those include. Everything here is named by an `#Include` somewhere and by nothing else, which is what makes it a library rather than a source. |
|
||||||
|
|
||||||
|
The split is by role rather than by which directory the host keeps a file in, and it only
|
||||||
|
works because **an include is looked for where you are and then in `/Lib`** - the same rule
|
||||||
|
the shell uses for programs, applied to the assembler. Without that search every source
|
||||||
|
that calls a service would have to sit beside `services.asm`, and there would be nothing to
|
||||||
|
organise.
|
||||||
|
|
||||||
|
So the machine rebuilds itself from its own disk:
|
||||||
|
|
||||||
|
```text
|
||||||
|
> cd /Source
|
||||||
|
/Source> Asm cosmos.asm
|
||||||
|
wrote cosmos.bin: program 9778, data 3346, labels 648
|
||||||
|
/Source> Asm Asm.asm
|
||||||
|
wrote Asm.sbx: program 7570, data 4114, labels 562
|
||||||
|
```
|
||||||
|
|
||||||
|
Both come out byte for byte what the host assembler makes from the same source.
|
||||||
|
|
||||||
## Included Applications:
|
## Included Applications:
|
||||||
|
|
||||||
`Programs/CosmOS/Apps` holds what the shell can load, and the application disk is built
|
`Programs/CosmOS/Apps` holds what the shell can load, and the application disk is built
|
||||||
@@ -134,10 +361,20 @@ from every assembly file in it. Several are old programs written for the bare ma
|
|||||||
| Snake | A game. Draws a whole screen with cursor addressing and steers with single keys, asking the console once a frame and never waiting. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| Reboot | Starts the machine again, in 45 bytes. Writes a port rather than asking the system, because a reset has to work when the system does not. |
|
||||||
|
| Once | Asks the loader to start something else on the next start, and only that one, in 569 bytes. |
|
||||||
|
| Status | Says what the last program made of what it was asked to do, in 222 bytes. The shell keeps the number and does not print it; this is how a person looks. |
|
||||||
|
| Settle | Says how the last start went and tells the machine to stop falling back, in 353 bytes. A program rather than a shell word, because the shell is for what cannot be done without it. |
|
||||||
|
| Files | Writes a file, reads it back, renames it and deletes it, in 675 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. |
|
||||||
| Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. |
|
| Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. |
|
||||||
| Edit | A line editor. |
|
| Edit | A line editor. |
|
||||||
| Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. |
|
| Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. |
|
||||||
|
| Type | Prints a named text file a block at a time, including one too large to fit in Data Memory. |
|
||||||
|
| Pour | Writes a file a block at a time, never holding more than one block of it. Each block is filled with a byte naming itself, so a block written to the wrong place shows up as content rather than as a length. |
|
||||||
|
| Copy | Copies one path to another a block at a time, including an empty file or one larger than Data Memory. |
|
||||||
|
| Compare | Compares two files a block at a time, stopping at their real tails rather than comparing unused bytes in the final disk blocks. |
|
||||||
|
| Wander | Goes to the directory it is given and reads a file there by a bare name. The only thing that moves the machine from inside a program, and so the only thing that can check the shell puts the working directory back afterwards. |
|
||||||
|
| More | A forward-only pager. Space advances a screen, Return one line, and q stops. |
|
||||||
|
|
||||||
### The Monitor:
|
### The Monitor:
|
||||||
|
|
||||||
@@ -229,7 +466,20 @@ Typed in as bytes, checked by disassembling it back, and run. It ends with `SWI
|
|||||||
|
|
||||||
`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.
|
`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 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 brought `Edit` down from 4,941 bytes to 2,157 bytes 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.
|
||||||
|
|
||||||
|
**A line it reads in is at most 128 characters**, the same length a line is everywhere else
|
||||||
|
on this machine, and a file with a longer one is refused rather than opened. Refused rather
|
||||||
|
than shortened, because this is an editor: a line cut on the way in would be written back
|
||||||
|
cut, and the file damaged by having been looked at.
|
||||||
|
|
||||||
|
That limit was not there at all until a source file found it. The buffer is followed in
|
||||||
|
memory by the head of the document and the pointer the line allocator hands out, so a 94
|
||||||
|
character line wrote characters over both - a 31 line file opened as 3, and opening it a
|
||||||
|
second time walked a list that led back into itself for ever, with the emulator still
|
||||||
|
running and the machine never answering again. Typing a long line was always safe, because
|
||||||
|
`osReadLine` is told how much room there is; only the file being read went unchecked, which
|
||||||
|
is why a new document behaved and a source file did not.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -239,14 +489,53 @@ These are ordinary SBEX files on SBFS. None of them is built into the operating
|
|||||||
a disk can be filled from either side: the host tool puts files on, and so does the machine,
|
a disk can be filled from either side: the host tool puts files on, and so does the machine,
|
||||||
which assembles its own now.
|
which assembles its own now.
|
||||||
|
|
||||||
|
### A Clean Install:
|
||||||
|
|
||||||
|
`make cosmos-disk` in `Programs/` lays down a disk the machine can start itself from, and
|
||||||
|
`make run-cosmos` starts it - with no boot image named, so the emulator shadows its ROM and
|
||||||
|
reads the disk for everything else.
|
||||||
|
|
||||||
|
```
|
||||||
|
/ Apps Source Lib System
|
||||||
|
/Apps what you run, and the second place the shell looks for a word
|
||||||
|
/Source what you assemble, including stage1.asm and stage2.asm
|
||||||
|
/Lib what those include
|
||||||
|
/System/Boot cosmos.bin, and the slots the loader lives in
|
||||||
|
```
|
||||||
|
|
||||||
|
**The loader's own source is on the disk**, which means the machine can rebuild what starts
|
||||||
|
it: `Asm stage2.asm` produces the bytes that go in a boot slot, and everything stage two
|
||||||
|
includes is already in `/Lib`. Stage one is the exception and always will be - it is the
|
||||||
|
ROM, and the one part of this a disk cannot replace.
|
||||||
|
|
||||||
|
No `boot.cfg` is written. Stage two falls back to `/System/Boot/cosmos.bin` when there is
|
||||||
|
none, and a clean install having nothing to configure is the right default.
|
||||||
|
|
||||||
|
`make run-cosmos-direct` hands the system over the old way instead, memory placed from
|
||||||
|
outside with nothing on the disk consulted. That is what a debugger does, and it is what to
|
||||||
|
use when the thing being debugged is the boot chain, since it skips the boot chain.
|
||||||
|
|
||||||
## The Application Model:
|
## The Application Model:
|
||||||
|
|
||||||
CosmOS divides the two SplitBit address spaces by convention:
|
CosmOS divides the two SplitBit address spaces by convention:
|
||||||
|
|
||||||
| Memory | CosmOS | Loaded application |
|
| Memory | CosmOS | Loaded application |
|
||||||
| -- | -- | -- |
|
| -- | -- | -- |
|
||||||
| Program Memory | `0x0000` through `0x1FFF` | `0x2000` and above |
|
| Program Memory | `0x0000` through `0x3FFF` | `0x4000` and above |
|
||||||
| Data Memory | `0x0000` through `0x0FFF` | `0x1000` and above |
|
| Data Memory | `0x0000` through `0x1FFF` | `0x2000` and above |
|
||||||
|
|
||||||
|
Both of CosmOS's halves were doubled once it outgrew the first ones. **The division is a
|
||||||
|
convention and nothing enforced it**, so CosmOS quietly grew past `0x1FFF` and the next
|
||||||
|
program loaded landed on top of its own code - which does not fail where it happens, it
|
||||||
|
fails later, in whatever part of the shell the program happened to cover. `make test` now
|
||||||
|
measures both segments against the numbers in this table, so the table is checked rather
|
||||||
|
than merely written down.
|
||||||
|
|
||||||
|
**The table is checked against itself as well.** The first version of that check read only
|
||||||
|
the CosmOS column, and so it passed a table whose Data row gave the system `0x3FFF` and an
|
||||||
|
application `0x2000` - two columns that cannot both be true, sitting next to each other.
|
||||||
|
Measuring one number against the code and never against the number beside it is how a
|
||||||
|
specification contradicts itself in public.
|
||||||
|
|
||||||
Applications state their actual Program and Data addresses with `#Base`. The SplitBit
|
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,
|
assembler then writes an SBEX loadable image containing those addresses, the entry point,
|
||||||
@@ -275,7 +564,7 @@ A minimal CosmOS application therefore looks like this:
|
|||||||
#Include services.asm
|
#Include services.asm
|
||||||
|
|
||||||
#Program
|
#Program
|
||||||
#Base 0x2000
|
#Base 0x4000
|
||||||
|
|
||||||
start:
|
start:
|
||||||
SETD.0 Message
|
SETD.0 Message
|
||||||
@@ -283,7 +572,7 @@ start:
|
|||||||
SWI osExit
|
SWI osExit
|
||||||
|
|
||||||
#Data
|
#Data
|
||||||
#Base 0x1000
|
#Base 0x2000
|
||||||
|
|
||||||
Message:
|
Message:
|
||||||
"Hello from CosmOS."
|
"Hello from CosmOS."
|
||||||
@@ -311,8 +600,16 @@ Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, w
|
|||||||
| osFileRename | DP0 is the name a file has, DP1 the name it should have. Q is zero if it moved. |
|
| osFileRename | DP0 is the name a file has, DP1 the name it should have. Q is zero if it moved. |
|
||||||
| osFileInfo | DP0 names a file. Q is zero if it is there, and DP3 comes back holding how many blocks it occupies. |
|
| osFileInfo | DP0 names a file. Q is zero if it is there, and DP3 comes back holding how many blocks it occupies. |
|
||||||
| osFileBlock | DP0 names a file, DP1 says where to put a block of it, A and B together are which block counting from zero. Q is zero if it read, and DP3 comes back holding how many of the block's bytes belong to the file. |
|
| osFileBlock | DP0 names a file, DP1 says where to put a block of it, A and B together are which block counting from zero. Q is zero if it read, and DP3 comes back holding how many of the block's bytes belong to the file. |
|
||||||
|
| osChangeDir | DP0 names a directory. Q is zero if the machine is now in it. What a program changes here, the shell puts back when the program stops. |
|
||||||
|
| osFileStart | DP0 names a file, DP3 is how many whole blocks and A is the bytes left over in the last one. Q is zero if a write is now open. Nothing already on the disk is touched. |
|
||||||
|
| osFileWrite | DP1 is a block, A and B together are which block of the file it is, counting from zero. Q is zero if it was written. An index past the end of the file is refused. |
|
||||||
|
| osFileDone | DP3 is how many whole blocks it came to and A the bytes left over. The old file goes and what was written takes its name, at that size. Q is zero if it was committed. |
|
||||||
|
| osFileFetch | DP1 is where a block should go, A and B together are which block. Reads back a block of the file being written. |
|
||||||
| osPrintNumber | A and B together are a number. Prints it in decimal, without leading zeroes. |
|
| osPrintNumber | A and B together are a number. Prints it in decimal, without leading zeroes. |
|
||||||
| osBreak | Stops the program, shows every register as it had them, waits for a key, and carries on. |
|
| osBreak | Stops the program, shows every register as it had them, waits for a key, and carries on. |
|
||||||
|
| osLastStatus | Q answers what the last program exited with: 0 it did what it was asked, 1 it did not, 2 it was asked wrongly. A program may give its own meanings if it says so. |
|
||||||
|
| osBootState | Q answers how the last start went: 0 settled, 1 trying, 2 fell back. A machine with no disk answers settled, because there is nothing there to be unsettled about. |
|
||||||
|
| osBootSettle | Puts it back to settled, which is how a machine that fell back is told the situation has changed. Q is zero if the disk took it. **Settling is the only write a program gets** - marking a start as trying or fallen back is the loader's business, and a service that let a program claim either would let it lie about something the loader cannot check. |
|
||||||
|
|
||||||
```
|
```
|
||||||
#Include services.asm
|
#Include services.asm
|
||||||
@@ -395,33 +692,12 @@ Running off the end is how a reader finds out it has finished, so it gets an ans
|
|||||||
|
|
||||||
`Programs/CosmOS/Apps/Stream.asm` reads an 84,000 byte file through a 256 byte buffer, then reads a small file both ways - whole with `osFileRead` and streamed - and checks that the two agree.
|
`Programs/CosmOS/Apps/Stream.asm` reads an 84,000 byte file through a 256 byte buffer, then reads a small file both ways - whole with `osFileRead` and streamed - and checks that the two agree.
|
||||||
|
|
||||||
`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.
|
`Programs/CosmOS/Apps/Files.asm` does the whole round trip - write, read, report, rename, delete - in 675 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.
|
`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.
|
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.
|
|
||||||
|
|
||||||
## The Filesystem Library:
|
## The Filesystem Library:
|
||||||
|
|
||||||
The disk knows blocks and nothing else, so a filesystem is software. Programs/CosmOS/Source/sbfs.asm is one.
|
The disk knows blocks and nothing else, so a filesystem is software. Programs/CosmOS/Source/sbfs.asm is one.
|
||||||
@@ -443,6 +719,115 @@ Finding a file and listing what is there are different jobs. sbfsFind searches f
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
### Writing A File Too Big To Hold:
|
||||||
|
|
||||||
|
`osFileSave` is handed a whole document at once, which is what a text editor has. A program
|
||||||
|
that produces its output a piece at a time - an assembler, say - would have to hold all of
|
||||||
|
it first, and the largest thing on this machine would then be limited by memory rather than
|
||||||
|
by the disk.
|
||||||
|
|
||||||
|
So there is the other half of the streaming pair. `osFileInfo` and `osFileBlock` read a
|
||||||
|
file a block at a time; `osFileStart`, `osFileWrite` and `osFileDone` write one.
|
||||||
|
|
||||||
|
```text
|
||||||
|
SETD.0 Name
|
||||||
|
SETD.3 0x00 0x06 ; six whole blocks
|
||||||
|
INIA 0d40 ; and forty bytes after them
|
||||||
|
SWI osFileStart
|
||||||
|
|
||||||
|
...for each block: DP1 the bytes, A and B which block...
|
||||||
|
SWI osFileWrite
|
||||||
|
|
||||||
|
SETD.3 0x00 0x06 ; and what it came to, which need not be
|
||||||
|
INIA 0d40 ; what was asked for
|
||||||
|
SWI osFileDone
|
||||||
|
```
|
||||||
|
|
||||||
|
**One write is open at a time, and the system holds it rather than the program.** Reading
|
||||||
|
needs no state - a name and an index are the whole question - but writing safely does,
|
||||||
|
because the new file has to exist before the old one is thrown away and something has to
|
||||||
|
remember which temporary belongs to which name. Keeping that here means the careful order
|
||||||
|
is written once instead of in every program that streams.
|
||||||
|
|
||||||
|
**Nothing already on the disk is touched until `osFileDone`.** The room for the whole file
|
||||||
|
is taken at the start, so a disk that cannot hold it says so while the old one is still
|
||||||
|
there. That is stronger than `osFileSave` can manage, where the size is only known once the
|
||||||
|
caller already has every byte in hand.
|
||||||
|
|
||||||
|
**The size asked for need not be the size it comes to.** Some sizes are not knowable until
|
||||||
|
the last byte is out - the assembler cannot say how many vectors a program installs until
|
||||||
|
it has resolved them, and by then the file it is writing into has to exist. So the room is
|
||||||
|
taken generously at the start, where running out costs nothing, and `osFileDone` is told
|
||||||
|
the truth. The blocks that were asked for and not used go back.
|
||||||
|
|
||||||
|
`osFileFetch` reads a block of the file back, which is what lets a program keep only one
|
||||||
|
block of it in hand. Anything producing two parts of a file at once - source that says
|
||||||
|
`#Program` and `#Data` in whatever order it likes - has to be able to put a block down, go
|
||||||
|
and write somewhere else, and pick it up again where it left off.
|
||||||
|
|
||||||
|
Two limits differ between the two. `osFileSave` is handed a byte count in two registers and
|
||||||
|
so cannot write more than 65,535 bytes; `osFileStart` is told blocks and a tail, the way an
|
||||||
|
entry holds a size, and reaches the whole disk. And `osFileWrite` refuses an index past the
|
||||||
|
end of the file - files are contiguous, so block nine of a three block file is a real block
|
||||||
|
belonging to something else, and writing it would put one file's bytes inside another with
|
||||||
|
nothing anywhere saying so.
|
||||||
|
|
||||||
|
### Reading Ahead:
|
||||||
|
|
||||||
|
A file is read front to back, so when a program asks for a block, the one after it is
|
||||||
|
almost certainly wanted next. `sbfsReadOne` asks the disk for it straight away and hands
|
||||||
|
the caller the block it wanted - so the transfer happens while the program is busy with
|
||||||
|
what it already has, and the wait is mostly gone by the time it comes back.
|
||||||
|
|
||||||
|
Nothing is done differently and nothing is done out of order. The machine simply stops
|
||||||
|
standing still.
|
||||||
|
|
||||||
|
**It is not done for directory searches, and that is not an oversight.** A scan stops the
|
||||||
|
moment it matches, so the next block is one nobody will ever look at: it costs a transfer to
|
||||||
|
fetch and another wait to throw away. Tried there, it was nineteen per cent *slower*. Read
|
||||||
|
ahead is a bet that the next block is wanted, and a search is exactly the case that hopes it
|
||||||
|
is not.
|
||||||
|
|
||||||
|
What it is worth, printing a fourteen kilobyte file:
|
||||||
|
|
||||||
|
| Cycles a block | Without | With |
|
||||||
|
| -- | -- | -- |
|
||||||
|
| 0 | 936,626 | 962,959 |
|
||||||
|
| 2,000 | 1,064,498 | 976,882 |
|
||||||
|
| 10,000 | 1,576,562 | 1,032,889 |
|
||||||
|
|
||||||
|
The second column barely moves. From an instant disk to a slow one the cost rises seven per
|
||||||
|
cent, where without it the same change costs sixty eight - which is the point: **a machine
|
||||||
|
that reads ahead stops caring very much how fast its disk is.** The three per cent it costs
|
||||||
|
at zero is the bookkeeping, paid when there is nothing to hide behind it.
|
||||||
|
|
||||||
|
### And Waiting For What Is Left:
|
||||||
|
|
||||||
|
Read ahead hides most of the wait and cannot hide all of it. What remained was a loop
|
||||||
|
asking the disk's status port over and over, which is work the machine is doing and memory
|
||||||
|
it is touching to find out that nothing has happened yet.
|
||||||
|
|
||||||
|
`sbfsWaitDisk` uses `WAIT` now. It tests the status port, and only if the disk is still
|
||||||
|
busy does it stop - the CPU is put down until a device raises a line, and the disk raises
|
||||||
|
one when it finishes. **Asking first is what makes it safe:** if the disk finished in the
|
||||||
|
gap between the test and the `WAIT`, its line is already standing and the `WAIT` does
|
||||||
|
nothing rather than sleeping through the answer.
|
||||||
|
|
||||||
|
There is no handler and no vector. The shell keeps the Interrupt Flag down, and a `WAIT`
|
||||||
|
wakes on a line whether or not anybody intends to answer it, taking it down on the way
|
||||||
|
past. Printing the same fourteen kilobyte file:
|
||||||
|
|
||||||
|
| Cycles a block | Total | Of that, the bus | Waiting |
|
||||||
|
| -- | -- | -- | -- |
|
||||||
|
| 0 | 922,570 | 922,570 | 0 |
|
||||||
|
| 2,000 | 946,474 | 922,702 | 23,772 |
|
||||||
|
| 10,000 | 1,042,474 | 922,702 | 119,772 |
|
||||||
|
|
||||||
|
**The middle column stops moving.** What the program costs in memory is now the same
|
||||||
|
whatever the disk does, and the difference is time spent with the bus quiet. On this
|
||||||
|
emulator that changes nothing anybody can see; on hardware it is the difference between a
|
||||||
|
CPU contending for memory with everything else and a CPU standing out of the way.
|
||||||
|
|
||||||
### Saving Something Twice:
|
### 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**:
|
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**:
|
||||||
@@ -464,12 +849,87 @@ A create can be refused for want of a run long enough even on a disk with plenty
|
|||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
|
#### Exactly what that promises:
|
||||||
|
|
||||||
|
The ordering protects the original against **every way a save can fail while it is running**, and it is worth naming those, because they are the ones that actually happen: there is no run of free blocks long enough, or none at all; the disk refuses a block write; the name turns out to belong to a directory; the writer gives up part way through. In all of them the file that was already there is untouched, and what is lost is the temporary, which nothing had come to depend on yet.
|
||||||
|
|
||||||
|
It is **not** power-loss atomic, and nothing about SBFS claims it is. The commit is two block writes - delete the old entry, then give the temporary its name - and a machine that stops between them leaves the old file gone and the new one under the temporary's name. Both writes are to the directory, so `sbfs.part` or `sbfs.out` is sitting there holding every byte of the work; the data survives and the name does not, and putting it right is one `rename` typed by hand. `dir` marks it `<unfinished>` so that it can be found, which is the whole of the recovery this format offers.
|
||||||
|
|
||||||
|
Closing that window means a journal or a second copy of the directory, and both are a great deal of machinery to buy back a two-write gap on a machine with no power failures to speak of. The honest description is the one to write down: **safe against the failures of ordinary operation, not against the machine stopping.**
|
||||||
|
|
||||||
|
#### What tells a temporary from a file:
|
||||||
|
|
||||||
|
While the save runs the temporary is an ordinary entry in every way that matters - it holds real blocks and answers to a name - and the only thing that makes it different is that nobody has committed it yet. That is **not a property of its contents.** The same bytes become the finished file the instant the rename lands, so there is nothing to put inside it that would be true. It belongs in the entry, which is the thing the commit changes, and it is **flag bit `0x04`**.
|
||||||
|
|
||||||
|
It used to be told apart by being *called* `sbfs.part` or `sbfs.out`, and those are names anybody is entitled to give a file of their own. Starting a save deleted whatever answered to one, as stale scratch - so saving anything at all in a directory destroyed your file of that name there, silently, and the first you would know of it is going to look for it. A file that does not carry the flag now belongs to somebody, and the save is **refused** rather than helping itself to the name.
|
||||||
|
|
||||||
|
The same bit is what makes an interrupted save recoverable. Both listings show an unfinished write rather than sizing it, because the size in the entry is the room it asked for and not what was written into it:
|
||||||
|
|
||||||
|
```
|
||||||
|
> dir
|
||||||
|
stranded.txt <unfinished>
|
||||||
|
```
|
||||||
|
|
||||||
|
Rename it to keep the data, delete it to give the blocks back. Nothing reclaims it on its own; a boot-time consistency check could, and this is the field it would read.
|
||||||
|
|
||||||
|
**A committed file never carries the bit**, so a disk this writes is byte for byte the disk the older code wrote - the agreement tests compare whole images and say so. Only the wreckage of a save that stopped looks different, and code that has never heard of the flag reads that as an ordinary file, which is exactly what it did before.
|
||||||
|
|
||||||
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.
|
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.
|
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.
|
||||||
|
|
||||||
The other implementation of this format is SplitDisk, on the host. Nothing is shared between the two but the specification, so a change to either has to be a change to both.
|
The other implementation of this format is SplitDisk, on the host. Nothing is shared between the two but the specification, so a change to either has to be a change to both.
|
||||||
|
|
||||||
|
### What A Program Made Of It:
|
||||||
|
|
||||||
|
`SWI osExit` takes a status **in A**: zero if the program did what it was asked, one if it
|
||||||
|
did not, two if it was asked wrongly. A program may give its own meanings if it says so, and
|
||||||
|
`Compare` does - one there means the files differ, which is a result rather than a failure.
|
||||||
|
|
||||||
|
**In A rather than Q**, which is not a departure from the rule that a service answers in Q.
|
||||||
|
This one *takes an argument*, the way `osPrintNumber` takes A and B, and it never returns to
|
||||||
|
answer anything. A is free precisely because a return would have put it back - and Q is the
|
||||||
|
ALU's output, so setting it to a small number costs four instructions where A costs one.
|
||||||
|
|
||||||
|
**The shell keeps the number and does not print it.** A program that failed has already said
|
||||||
|
so in words, and a number beside that would be noise. `osLastStatus` hands it back and
|
||||||
|
`Status` is the program that shows it. The indirection is the point: this number is for the
|
||||||
|
thing that cannot read words - whatever comes to run programs in sequence and has to decide
|
||||||
|
whether to run the next one.
|
||||||
|
|
||||||
|
Marking all fifty eight exits found a defect on its first run. `Type` and `More` printed why
|
||||||
|
they had failed and then **fell through into the success exit**, reporting that all was
|
||||||
|
well. Nobody had noticed, because while the only reader was a person, the person could see
|
||||||
|
both.
|
||||||
|
|
||||||
|
### How A Service Answers:
|
||||||
|
|
||||||
|
A handler arrives with the caller's registers pushed rather than cleared, and **`RETI`
|
||||||
|
restores every one of them** - which is what makes an interrupt safe to arrive at an
|
||||||
|
arbitrary moment, since the interrupted code cannot tell it happened. A service is not
|
||||||
|
arbitrary. It was asked for, and it has something to say.
|
||||||
|
|
||||||
|
It says it with `SRET`, which is `RET` adapted to an interrupt frame: **A, B and Data
|
||||||
|
Pointers 0 through 2 come back, the saved Q and Data Pointer 3 are dropped, and the
|
||||||
|
Interrupt Flag is put back from the frame.** So a service answers in exactly the registers
|
||||||
|
a subroutine answers in, and there is one rule on this machine rather than two.
|
||||||
|
|
||||||
|
Before it existed, a handler with an answer wrote into its own frame:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
MVSD.2
|
||||||
|
DPUP.2 0d02 ; the saved Q, by an offset it had to know
|
||||||
|
STA.2
|
||||||
|
RETI
|
||||||
|
```
|
||||||
|
|
||||||
|
Thirty places did that, each knowing the frame's layout by heart, and all thirty would have
|
||||||
|
gone quietly wrong the day the frame gained a field. None of them knows it now.
|
||||||
|
|
||||||
|
`RETI` is still right for a **hardware** handler, which has nothing to say and must leave
|
||||||
|
no trace. The two returns are not a choice of style: one says *I was never here* and the
|
||||||
|
other says *here is your answer*.
|
||||||
|
|
||||||
### What A Subroutine Can And Cannot Hand Back:
|
### What A Subroutine Can And Cannot Hand Back:
|
||||||
|
|
||||||
This is the thing that catches people, including whoever wrote the last three pieces of system code, so it is worth stating once and plainly.
|
This is the thing that catches people, including whoever wrote the last three pieces of system code, so it is worth stating once and plainly.
|
||||||
|
|||||||
@@ -0,0 +1,469 @@
|
|||||||
|
; config.asm
|
||||||
|
; Reading a configuration file.
|
||||||
|
;
|
||||||
|
; One setting to a line: a key, a space, and the rest of the line is the value. A semicolon
|
||||||
|
; starts a comment and a blank line is nothing. That format was not designed so much as
|
||||||
|
; noticed - textSplit already cuts the first word off a line and leaves the rest, and
|
||||||
|
; textSame already compares two strings and insists they end together, so reading a setting
|
||||||
|
; is those two routines and a loop. It is also the shape the shell reads, which means a
|
||||||
|
; configuration line is a command line the machine reads instead of a person typing one.
|
||||||
|
;
|
||||||
|
; ---- Configuration is advice ----
|
||||||
|
;
|
||||||
|
; A file that is not there, a key that is not in it, and a value that makes no sense are
|
||||||
|
; all the same answer: use the default. Nothing here reports a failure for any of them,
|
||||||
|
; because a program that cannot run without its configuration has turned its configuration
|
||||||
|
; into a single point of failure - and for the thing that starts the machine, that would
|
||||||
|
; mean a mistyped file is a machine that does not start.
|
||||||
|
;
|
||||||
|
; ---- But quiet is not the same as silent ----
|
||||||
|
;
|
||||||
|
; A setting that was meant and did not take effect should say so, or the only symptom is
|
||||||
|
; that the machine did not do what somebody asked it to. So there are TWO routines rather
|
||||||
|
; than one: cfgGet reads, and never says anything; cfgCheck reads the whole file once and
|
||||||
|
; reports what it did not understand.
|
||||||
|
;
|
||||||
|
; They are separate because they know different things. A malformed line is malformed to
|
||||||
|
; anybody, so cfgGet's own scan could spot one - but reading three settings would then
|
||||||
|
; report the same bad line three times. And an unknown KEY is not something this can judge
|
||||||
|
; at all: only the caller knows which keys mean anything to it, which is why cfgCheck is
|
||||||
|
; given a table of them.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
; ---- Reading the file in ----
|
||||||
|
;
|
||||||
|
; DP0 names a path, DP1 names a buffer, and A is how many blocks the buffer holds.
|
||||||
|
;
|
||||||
|
; Q is zero if there is something to read, INCLUDING WHEN THERE IS NO FILE. A missing
|
||||||
|
; configuration file is a file with no settings in it, which is a perfectly ordinary thing
|
||||||
|
; for a disk to have, and the caller wanting the defaults gets them either way.
|
||||||
|
cfgLoad:
|
||||||
|
SETD.2 CfgRoom
|
||||||
|
STA.2
|
||||||
|
SETD.2 CfgBufferAt
|
||||||
|
STD.1.2
|
||||||
|
|
||||||
|
; Nothing loaded until something is.
|
||||||
|
RSTA
|
||||||
|
SETD.2 CfgLength
|
||||||
|
STA.2
|
||||||
|
INCD.2
|
||||||
|
STA.2
|
||||||
|
|
||||||
|
CALL sbfsFind
|
||||||
|
BNQ cfgNoFile
|
||||||
|
|
||||||
|
; A file too big for the buffer is read as far as it fits rather than refused: the
|
||||||
|
; settings at the top still work, and the ones past the end are missing keys, which is
|
||||||
|
; a case every caller already handles.
|
||||||
|
SETD.2 SbfsFileBlocks
|
||||||
|
INCD.2
|
||||||
|
LDA.2
|
||||||
|
SETD.2 CfgRoom
|
||||||
|
LDB.2
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BNC cfgTooBig
|
||||||
|
|
||||||
|
SETD.2 CfgBufferAt
|
||||||
|
LDD.1.2
|
||||||
|
CALL sbfsRead
|
||||||
|
BNQ cfgNoFile
|
||||||
|
|
||||||
|
; How many bytes of it are real. A file is whole blocks and then a tail, which is the
|
||||||
|
; block count in the high byte and the tail in the low one.
|
||||||
|
SETD.2 SbfsFileBlocks
|
||||||
|
INCD.2
|
||||||
|
LDA.2
|
||||||
|
SETD.2 CfgLength
|
||||||
|
STA.2
|
||||||
|
SETD.2 SbfsFileTail
|
||||||
|
LDA.2
|
||||||
|
SETD.2 CfgLength
|
||||||
|
INCD.2
|
||||||
|
STA.2
|
||||||
|
|
||||||
|
cfgReady:
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
cfgTooBig:
|
||||||
|
; As much of it as there is room for, which is every whole block of the buffer.
|
||||||
|
SETD.2 CfgRoom
|
||||||
|
LDA.2
|
||||||
|
SETD.2 SbfsFileBlocks
|
||||||
|
INCD.2
|
||||||
|
STA.2
|
||||||
|
RSTA
|
||||||
|
SETD.2 SbfsFileTail
|
||||||
|
STA.2
|
||||||
|
SETD.2 CfgBufferAt
|
||||||
|
LDD.1.2
|
||||||
|
CALL sbfsRead
|
||||||
|
BNQ cfgNoFile
|
||||||
|
SETD.2 CfgRoom
|
||||||
|
LDA.2
|
||||||
|
SETD.2 CfgLength
|
||||||
|
STA.2
|
||||||
|
RSTA
|
||||||
|
INCD.2
|
||||||
|
STA.2
|
||||||
|
BRI cfgReady
|
||||||
|
|
||||||
|
cfgNoFile:
|
||||||
|
; No file, or a disk that would not give it up. Neither is a failure: it is a file with
|
||||||
|
; nothing in it, and every key will be missing, which is what a default is for.
|
||||||
|
RSTA
|
||||||
|
SETD.2 CfgLength
|
||||||
|
STA.2
|
||||||
|
INCD.2
|
||||||
|
STA.2
|
||||||
|
BRI cfgReady
|
||||||
|
|
||||||
|
; ---- Walking it a line at a time ----
|
||||||
|
;
|
||||||
|
; cfgRewind puts the walk back at the top. cfgLine copies the next line into CfgLine and
|
||||||
|
; leaves Q zero if there was one.
|
||||||
|
;
|
||||||
|
; A line longer than the buffer holds is copied as far as it goes and the rest of it is
|
||||||
|
; skipped, with CfgLong set to say so. Ignoring it is what the format asks for; saying that
|
||||||
|
; it was ignored is what cfgCheck is for.
|
||||||
|
cfgRewind:
|
||||||
|
SETD.0 CfgBufferAt
|
||||||
|
SETD.1 CfgWalk
|
||||||
|
CALL sbfsCopyWord
|
||||||
|
SETD.0 CfgLength
|
||||||
|
SETD.1 CfgLeft
|
||||||
|
CALL sbfsCopyWord
|
||||||
|
RET
|
||||||
|
|
||||||
|
cfgLine:
|
||||||
|
RSTA
|
||||||
|
SETD.0 CfgLong
|
||||||
|
STA.0
|
||||||
|
SETD.0 CfgFill
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
SETD.0 CfgLeft
|
||||||
|
LDA.0
|
||||||
|
INCD.0
|
||||||
|
LDB.0
|
||||||
|
OR
|
||||||
|
BRQ cfgLineNone
|
||||||
|
|
||||||
|
SETD.2 CfgWalk
|
||||||
|
LDD.0.2
|
||||||
|
SETD.1 CfgLine
|
||||||
|
|
||||||
|
cfgLineLoop:
|
||||||
|
SETD.2 CfgLeft
|
||||||
|
LDA.2
|
||||||
|
INCD.2
|
||||||
|
LDB.2
|
||||||
|
OR
|
||||||
|
BRQ cfgLineEnd
|
||||||
|
|
||||||
|
LDA.0
|
||||||
|
INIB 0x0A
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ cfgLineBreak
|
||||||
|
|
||||||
|
; Room for it, or the line is one of the long ones and only its beginning is kept.
|
||||||
|
SETD.2 CfgFill
|
||||||
|
LDB.2
|
||||||
|
INIA 0d128
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ cfgLineOverflow
|
||||||
|
LDA.0
|
||||||
|
STA.1
|
||||||
|
INCD.1
|
||||||
|
LDA.2
|
||||||
|
INCA
|
||||||
|
STA.2
|
||||||
|
|
||||||
|
cfgLineStep:
|
||||||
|
INCD.0
|
||||||
|
RCAL cfgSpent
|
||||||
|
BRI cfgLineLoop
|
||||||
|
|
||||||
|
cfgLineOverflow:
|
||||||
|
INIA 0x01
|
||||||
|
SETD.2 CfgLong
|
||||||
|
STA.2
|
||||||
|
BRI cfgLineStep
|
||||||
|
|
||||||
|
cfgLineBreak:
|
||||||
|
INCD.0
|
||||||
|
RCAL cfgSpent
|
||||||
|
cfgLineEnd:
|
||||||
|
RSTA
|
||||||
|
STA.1 ; The zero that makes what was copied a string.
|
||||||
|
SETD.2 CfgWalk
|
||||||
|
STD.0.2
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
cfgLineNone:
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; One byte of the file accounted for.
|
||||||
|
cfgSpent:
|
||||||
|
SETD.2 CfgLeft
|
||||||
|
INCD.2
|
||||||
|
LDA.2
|
||||||
|
BNA cfgSpentLow
|
||||||
|
SETD.2 CfgLeft
|
||||||
|
LDA.2
|
||||||
|
DECA
|
||||||
|
STA.2
|
||||||
|
INCD.2
|
||||||
|
INIA 0xFF
|
||||||
|
STA.2
|
||||||
|
RRET
|
||||||
|
cfgSpentLow:
|
||||||
|
DECA
|
||||||
|
STA.2
|
||||||
|
RRET
|
||||||
|
|
||||||
|
; ---- What a line turns out to be ----
|
||||||
|
;
|
||||||
|
; Q is zero if there is a setting on it, and then CfgLine names the key and CfgValue names
|
||||||
|
; the value. Q is not zero for a blank line or a comment, which are not settings and are
|
||||||
|
; not mistakes either.
|
||||||
|
cfgParse:
|
||||||
|
RSTA
|
||||||
|
SETD.0 CfgNoValue
|
||||||
|
STA.0
|
||||||
|
|
||||||
|
; A LINE THAT WAS CUT SHORT IS NOT A SETTING. Its key may look perfectly good and its
|
||||||
|
; value is whatever fitted, so treating it as one would hand back an answer that is wrong
|
||||||
|
; rather than missing - and a missing setting gets the default, which is the safe thing.
|
||||||
|
; Reported by cfgCheck, ignored by everything.
|
||||||
|
SETD.0 CfgLong
|
||||||
|
LDA.0
|
||||||
|
BNA cfgParseNothing
|
||||||
|
|
||||||
|
SETD.0 CfgLine
|
||||||
|
LDA.0
|
||||||
|
BRA cfgParseNothing
|
||||||
|
INIB 0x3B ; A semicolon starts a comment.
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRQ cfgParseNothing
|
||||||
|
|
||||||
|
CALL textSplit
|
||||||
|
SETD.0 TextRest
|
||||||
|
SETD.1 CfgValue
|
||||||
|
CALL sbfsCopyWord
|
||||||
|
|
||||||
|
; A KEY WITH NOTHING AFTER IT IS NOT A SETTING, and finding that out here rather than in
|
||||||
|
; each caller is what makes first-match-wins safe. A file with
|
||||||
|
;
|
||||||
|
; system
|
||||||
|
; system /System/Boot/cosmos.bin
|
||||||
|
;
|
||||||
|
; in it would otherwise match the first line, hand back an empty value, and the machine
|
||||||
|
; would try to start a file with no name while a perfectly good setting sat underneath.
|
||||||
|
; An unusable value is an absent one, which is what C3 says and where it earns its keep.
|
||||||
|
SETD.2 CfgValue
|
||||||
|
LDD.0.2
|
||||||
|
LDA.0
|
||||||
|
BRA cfgParseNoValue
|
||||||
|
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
cfgParseNoValue:
|
||||||
|
; Told apart from a blank line, because one of them is a mistake and the other is not.
|
||||||
|
INIA 0x01
|
||||||
|
SETD.0 CfgNoValue
|
||||||
|
STA.0
|
||||||
|
cfgParseNothing:
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- Asking for one setting ----
|
||||||
|
;
|
||||||
|
; DP0 names the key. Q is zero if the file had it, and CfgValue then names the value.
|
||||||
|
; Says nothing about anything, ever.
|
||||||
|
cfgGet:
|
||||||
|
SETD.1 CfgWanted
|
||||||
|
STD.0.1
|
||||||
|
CALL cfgRewind
|
||||||
|
|
||||||
|
cfgGetLoop:
|
||||||
|
CALL cfgLine
|
||||||
|
BNQ cfgGetMissing
|
||||||
|
CALL cfgParse
|
||||||
|
BNQ cfgGetLoop
|
||||||
|
|
||||||
|
SETD.0 CfgLine
|
||||||
|
SETD.2 CfgWanted
|
||||||
|
LDD.1.2
|
||||||
|
CALL textSame
|
||||||
|
BNQ cfgGetLoop
|
||||||
|
|
||||||
|
RSTA
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
cfgGetMissing:
|
||||||
|
RSTA
|
||||||
|
INIB 0d1
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- Saying what was not understood ----
|
||||||
|
;
|
||||||
|
; DP0 names a table of the keys the caller knows: strings one after another, ended by an
|
||||||
|
; empty one. Every line is looked at once, and anything that will not take effect is
|
||||||
|
; reported with enough of itself to be found and fixed.
|
||||||
|
;
|
||||||
|
; Nothing here changes what any setting does. The defaults have already been used by the
|
||||||
|
; time this runs, or will be; this exists so that a setting somebody meant, which did not
|
||||||
|
; happen, does not do so in silence.
|
||||||
|
cfgCheck:
|
||||||
|
SETD.1 CfgKnown
|
||||||
|
STD.0.1
|
||||||
|
CALL cfgRewind
|
||||||
|
|
||||||
|
cfgCheckLoop:
|
||||||
|
CALL cfgLine
|
||||||
|
BNQ cfgCheckDone
|
||||||
|
|
||||||
|
SETD.0 CfgLong
|
||||||
|
LDA.0
|
||||||
|
BRA cfgCheckParse
|
||||||
|
SETD.0 CfgLongText
|
||||||
|
RCAL cfgSay
|
||||||
|
SETD.0 CfgLine
|
||||||
|
RCAL cfgSay
|
||||||
|
RCAL cfgNewLine
|
||||||
|
BRI cfgCheckLoop ; Said once. What its key looks like is not worth a second remark.
|
||||||
|
|
||||||
|
cfgCheckParse:
|
||||||
|
CALL cfgParse
|
||||||
|
BRQ cfgCheckKnown
|
||||||
|
|
||||||
|
; Not a setting. A blank line or a comment is not a mistake and gets no remark; a key
|
||||||
|
; somebody started and did not finish is, and gets one.
|
||||||
|
SETD.0 CfgNoValue
|
||||||
|
LDA.0
|
||||||
|
BRA cfgCheckLoop
|
||||||
|
SETD.0 CfgEmptyText
|
||||||
|
RCAL cfgSay
|
||||||
|
SETD.0 CfgLine
|
||||||
|
RCAL cfgSay
|
||||||
|
RCAL cfgNewLine
|
||||||
|
BRI cfgCheckLoop
|
||||||
|
|
||||||
|
cfgCheckKnown:
|
||||||
|
SETD.2 CfgKnown
|
||||||
|
LDD.1.2
|
||||||
|
|
||||||
|
cfgCheckNext:
|
||||||
|
LDA.1
|
||||||
|
BRA cfgCheckUnknown ; The empty name that ends the table.
|
||||||
|
SETD.0 CfgLine
|
||||||
|
CALL textSame
|
||||||
|
BRQ cfgCheckLoop ; A key this caller knows, so there is nothing to say.
|
||||||
|
|
||||||
|
; Past this name and on to the next one.
|
||||||
|
cfgCheckSkip:
|
||||||
|
LDA.1
|
||||||
|
BRA cfgCheckSkipped
|
||||||
|
INCD.1
|
||||||
|
BRI cfgCheckSkip
|
||||||
|
cfgCheckSkipped:
|
||||||
|
INCD.1
|
||||||
|
BRI cfgCheckNext
|
||||||
|
|
||||||
|
cfgCheckUnknown:
|
||||||
|
SETD.0 CfgUnknownText
|
||||||
|
RCAL cfgSay
|
||||||
|
SETD.0 CfgLine
|
||||||
|
RCAL cfgSay
|
||||||
|
RCAL cfgNewLine
|
||||||
|
BRI cfgCheckLoop
|
||||||
|
|
||||||
|
cfgCheckDone:
|
||||||
|
RET
|
||||||
|
|
||||||
|
; ---- Saying things ----
|
||||||
|
;
|
||||||
|
; Its own rather than the console's, because the first thing to read a configuration file
|
||||||
|
; is the boot loader and everything it uses has to fit in a boot slot.
|
||||||
|
cfgSay:
|
||||||
|
LDA.0
|
||||||
|
BRA cfgSaid
|
||||||
|
OUTA 0x00
|
||||||
|
INCD.0
|
||||||
|
BRI cfgSay
|
||||||
|
cfgSaid:
|
||||||
|
RRET
|
||||||
|
|
||||||
|
cfgNewLine:
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
RRET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
CfgLongText:
|
||||||
|
"a line too long to read: "
|
||||||
|
CfgEmptyText:
|
||||||
|
"a setting with no value: "
|
||||||
|
CfgUnknownText:
|
||||||
|
"a setting nothing asked for: "
|
||||||
|
|
||||||
|
CfgBufferAt:
|
||||||
|
0x00 0x00
|
||||||
|
CfgRoom:
|
||||||
|
0x00
|
||||||
|
CfgLength:
|
||||||
|
0x00 0x00
|
||||||
|
CfgWalk:
|
||||||
|
0x00 0x00
|
||||||
|
CfgLeft:
|
||||||
|
0x00 0x00
|
||||||
|
CfgFill:
|
||||||
|
0x00
|
||||||
|
CfgLong:
|
||||||
|
0x00
|
||||||
|
CfgNoValue:
|
||||||
|
0x00
|
||||||
|
CfgWanted:
|
||||||
|
0x00 0x00
|
||||||
|
CfgKnown:
|
||||||
|
0x00 0x00
|
||||||
|
CfgValue:
|
||||||
|
0x00 0x00
|
||||||
|
|
||||||
|
; One line, and the number is the one the format says: a key of twenty two and a path of
|
||||||
|
; the length a path is allowed to be leaves room to spare in a hundred and twenty eight.
|
||||||
|
CfgLine:
|
||||||
|
#Reserve 0d129
|
||||||
+1213
-167
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,141 @@
|
|||||||
|
; Read a named file from beginning to end, one block at a time.
|
||||||
|
;
|
||||||
|
; DP0 = filename
|
||||||
|
; CALL fileStreamOpen Q = 0, or the osFileInfo error
|
||||||
|
; CALL fileStreamNext Q = 0, or the osFileBlock error
|
||||||
|
; DP3 = bytes in FileStreamBlock; zero means EOF
|
||||||
|
;
|
||||||
|
; This is application-side machinery built on CosmOS services, not a filesystem and not
|
||||||
|
; yet an OS stream. Open remembers the address of the caller's filename, which must remain
|
||||||
|
; valid until the one stream is finished.
|
||||||
|
;
|
||||||
|
; Written by ChatGPT for Anachronaut's SplitBit
|
||||||
|
|
||||||
|
#Program
|
||||||
|
fileStreamOpen:
|
||||||
|
SETD.3 FileStreamName
|
||||||
|
STD.0.3
|
||||||
|
SWI osFileInfo
|
||||||
|
BNQ fileStreamReturn
|
||||||
|
PSHD.3
|
||||||
|
POPB
|
||||||
|
POPA
|
||||||
|
SETD.3 FileStreamBlocks
|
||||||
|
STA.3
|
||||||
|
INCD.3
|
||||||
|
STB.3
|
||||||
|
SETD.3 FileStreamIndex
|
||||||
|
RSTA
|
||||||
|
STA.3
|
||||||
|
INCD.3
|
||||||
|
STA.3
|
||||||
|
fileStreamReturn:
|
||||||
|
RET
|
||||||
|
|
||||||
|
fileStreamNext:
|
||||||
|
SETD.3 FileStreamBlocks
|
||||||
|
LDA.3
|
||||||
|
INCD.3
|
||||||
|
LDB.3
|
||||||
|
OR
|
||||||
|
BRQ fileStreamEnd
|
||||||
|
SETD.3 FileStreamName
|
||||||
|
LDD.0.3
|
||||||
|
SETD.1 FileStreamBlock
|
||||||
|
SETD.3 FileStreamIndex
|
||||||
|
LDA.3
|
||||||
|
INCD.3
|
||||||
|
LDB.3
|
||||||
|
SWI osFileBlock
|
||||||
|
BNQ fileStreamReturn
|
||||||
|
|
||||||
|
; DP3 is the service's return value and also the pointer this routine uses for all of
|
||||||
|
; its bookkeeping. Keep the value before touching the pointer, then restore it at the
|
||||||
|
; one successful return below.
|
||||||
|
PSHD.3
|
||||||
|
POPB
|
||||||
|
POPA
|
||||||
|
SETD.3 FileStreamCount
|
||||||
|
STA.3
|
||||||
|
INCD.3
|
||||||
|
STB.3
|
||||||
|
|
||||||
|
; Index++.
|
||||||
|
SETD.3 FileStreamIndex
|
||||||
|
INCD.3
|
||||||
|
LDA.3
|
||||||
|
INCA
|
||||||
|
STA.3
|
||||||
|
BNC fileStreamTakeBlock
|
||||||
|
DECD.3
|
||||||
|
LDA.3
|
||||||
|
INCA
|
||||||
|
STA.3
|
||||||
|
fileStreamTakeBlock:
|
||||||
|
; Blocks--.
|
||||||
|
SETD.3 FileStreamBlocks
|
||||||
|
INCD.3
|
||||||
|
LDA.3
|
||||||
|
BRA fileStreamBlocksBorrow
|
||||||
|
DECA
|
||||||
|
STA.3
|
||||||
|
BRI fileStreamReadReturn
|
||||||
|
fileStreamBlocksBorrow:
|
||||||
|
INIA 0xFF
|
||||||
|
STA.3
|
||||||
|
DECD.3
|
||||||
|
LDA.3
|
||||||
|
DECA
|
||||||
|
STA.3
|
||||||
|
fileStreamReadReturn:
|
||||||
|
SETD.3 FileStreamCount
|
||||||
|
LDD.3.3
|
||||||
|
RET
|
||||||
|
fileStreamEnd:
|
||||||
|
; There is no reset-pointer instruction. Two zero bytes through the Stack are the
|
||||||
|
; literal construction of a zero Data Pointer.
|
||||||
|
RSTA
|
||||||
|
PSHA
|
||||||
|
PSHA
|
||||||
|
POPD.3
|
||||||
|
RET
|
||||||
|
|
||||||
|
; DP2 names a big-endian sixteen-bit byte count. Decrement it, returning Q = 0 when it
|
||||||
|
; reached zero and nonzero while bytes remain. This is shared because correctly counting
|
||||||
|
; a full 0x0100-byte block is the least interesting part of both applications to duplicate.
|
||||||
|
fileStreamTakeRemaining:
|
||||||
|
INCD.2
|
||||||
|
LDA.2
|
||||||
|
BRA fileStreamRemainingBorrow
|
||||||
|
DECA
|
||||||
|
STA.2
|
||||||
|
DECD.2
|
||||||
|
LDB.2
|
||||||
|
INCD.2
|
||||||
|
LDA.2
|
||||||
|
OR
|
||||||
|
RET
|
||||||
|
fileStreamRemainingBorrow:
|
||||||
|
INIA 0xFF
|
||||||
|
STA.2
|
||||||
|
DECD.2
|
||||||
|
LDA.2
|
||||||
|
DECA
|
||||||
|
STA.2
|
||||||
|
LDB.2
|
||||||
|
INCD.2
|
||||||
|
LDA.2
|
||||||
|
OR
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
FileStreamName:
|
||||||
|
0x00 0x00
|
||||||
|
FileStreamBlocks:
|
||||||
|
0x00 0x00
|
||||||
|
FileStreamIndex:
|
||||||
|
0x00 0x00
|
||||||
|
FileStreamCount:
|
||||||
|
0x00 0x00
|
||||||
|
FileStreamBlock:
|
||||||
|
#Reserve 0d256
|
||||||
+2028
-105
File diff suppressed because it is too large
Load Diff
@@ -68,6 +68,47 @@
|
|||||||
; 4 the disk would not read it (osFileBlock only)
|
; 4 the disk would not read it (osFileBlock only)
|
||||||
osFileInfo 0d26 ; DP0 names it. Q is zero if it is there, DP3 is how many blocks.
|
osFileInfo 0d26 ; DP0 names it. Q is zero if it is there, DP3 is how many blocks.
|
||||||
osFileBlock 0d27 ; DP0 names it, DP1 says where, A and B are which block from zero.
|
osFileBlock 0d27 ; DP0 names it, DP1 says where, A and B are which block from zero.
|
||||||
|
|
||||||
|
; ---- Moving about ----
|
||||||
|
;
|
||||||
|
; DP0 names a directory. Q is zero if the machine is now in it.
|
||||||
|
;
|
||||||
|
; WHAT A PROGRAM CHANGES HERE, THE SHELL PUTS BACK when the program stops - the same
|
||||||
|
; discipline the Stack and the vector table are held to, and for the same reason. A program
|
||||||
|
; is entitled to move about; the shell is entitled to find itself where it left off.
|
||||||
|
;
|
||||||
|
; This is what makes a bare name mean something to a program: everything a program opens is
|
||||||
|
; relative to here, so a program given a directory to work in can say "notes.txt" and mean
|
||||||
|
; the one in it.
|
||||||
|
osChangeDir 0d28
|
||||||
|
|
||||||
|
; ---- Writing a file a block at a time ----
|
||||||
|
;
|
||||||
|
; The mirror of osFileInfo and osFileBlock, and the way to write something too big to hold
|
||||||
|
; in memory. osFileSave stays for a whole document handed over at once, which is what a
|
||||||
|
; text editor has and what most programs want.
|
||||||
|
;
|
||||||
|
; ONE WRITE IS OPEN AT A TIME AND THE SYSTEM HOLDS IT. Reading needs no state - a name and
|
||||||
|
; an index are the whole question - but writing safely does, because the new file has to
|
||||||
|
; exist before the old one is thrown away and something must remember which temporary
|
||||||
|
; belongs to which name. Keeping that here means the careful order is written once instead
|
||||||
|
; of in every program that streams.
|
||||||
|
;
|
||||||
|
; Nothing that already exists is touched until osFileDone, so a disk without room says so
|
||||||
|
; while the old file is still there.
|
||||||
|
;
|
||||||
|
; osFileStart is told the size the way an entry holds one, blocks and a tail, rather than a
|
||||||
|
; count of bytes - so it reaches the whole disk. osFileSave is handed a byte count in two
|
||||||
|
; registers and cannot write more than 65,535.
|
||||||
|
osFileStart 0d29 ; DP0 names it, DP3 is whole blocks, A is bytes in the tail.
|
||||||
|
osFileWrite 0d30 ; DP1 is the block, A and B together are which one, from zero.
|
||||||
|
osFileDone 0d31 ; DP3 is whole blocks and A the tail: how big it turned out to be.
|
||||||
|
osFileFetch 0d32 ; DP1 is where it goes, A and B are which block. Reads one back.
|
||||||
|
|
||||||
|
; osFileFetch is what lets a program keep only ONE block of a file in hand while writing
|
||||||
|
; it. Anything producing two parts of a file at once - an assembler, whose source says
|
||||||
|
; #Program and #Data in whatever order it likes - has to be able to put a block down, go
|
||||||
|
; and write somewhere else, and pick it up again where it left off.
|
||||||
; Q is zero if it read, DP3 is how many of its bytes are the file's:
|
; Q is zero if it read, DP3 is how many of its bytes are the file's:
|
||||||
; a whole 0d256 except in a last block that is short. That count is
|
; a whole 0d256 except in a last block that is short. That count is
|
||||||
; why DP3 answers and not a register - 0d256 does not fit in a byte,
|
; why DP3 answers and not a register - 0d256 does not fit in a byte,
|
||||||
@@ -94,3 +135,35 @@
|
|||||||
; restore. The price is that it is part of the program: a build with breakpoints in it has
|
; restore. The price is that it is part of the program: a build with breakpoints in it has
|
||||||
; different addresses from one without.
|
; different addresses from one without.
|
||||||
osBreak 0d25
|
osBreak 0d25
|
||||||
|
|
||||||
|
; ---- How the last start went ----
|
||||||
|
;
|
||||||
|
; The loader marks the disk before it hands over and the system clears the mark on reaching
|
||||||
|
; its prompt, so a mark still set is a start that never arrived. See the boot state in
|
||||||
|
; sbfs.h for what the numbers mean.
|
||||||
|
;
|
||||||
|
; osBootState answers in Q: 0 settled, 1 trying, 2 fell back. A machine with no disk answers
|
||||||
|
; settled, because there is nothing there to be unsettled about.
|
||||||
|
;
|
||||||
|
; osBootSettle puts it back to settled, which is how a machine that fell back is told the
|
||||||
|
; situation has changed. Q is zero if the disk took it.
|
||||||
|
;
|
||||||
|
; THE ONLY WRITE A PROGRAM GETS IS SETTLING. Marking a start as trying or fallen back is
|
||||||
|
; the loader's business, and a service that let a program claim either would let it lie
|
||||||
|
; about something the loader has no way to check.
|
||||||
|
osBootState 0d33
|
||||||
|
osBootSettle 0d34
|
||||||
|
|
||||||
|
; ---- What a program made of it ----
|
||||||
|
;
|
||||||
|
; osExit takes a status in A: zero if the program did what it was asked, and a number of
|
||||||
|
; its own choosing if it did not. IN A RATHER THAN Q, which is not a departure from the
|
||||||
|
; rule that a service answers in Q - this one takes an argument, the way osPrintNumber
|
||||||
|
; does, and never returns to answer anything. A is free precisely because a return would
|
||||||
|
; have put it back, and Q is the ALU's output, so a small number costs four instructions
|
||||||
|
; there and one in A.
|
||||||
|
;
|
||||||
|
; osLastStatus answers in Q with what the last program exited with. The shell does not
|
||||||
|
; print it: a program that failed has already said so in words, and a number beside that
|
||||||
|
; would be noise. This is for the thing that cannot read words.
|
||||||
|
osLastStatus 0d35
|
||||||
|
|||||||
@@ -94,8 +94,7 @@ textHexWord:
|
|||||||
textHexLoop:
|
textHexLoop:
|
||||||
LDA.0
|
LDA.0
|
||||||
CALL textHexDigit
|
CALL textHexDigit
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
INIB 0xFF
|
INIB 0xFF
|
||||||
CCF
|
CCF
|
||||||
SUB
|
SUB
|
||||||
@@ -273,7 +272,6 @@ textNumberLoop:
|
|||||||
POPB
|
POPB
|
||||||
CCF
|
CCF
|
||||||
ADD ; And the digit.
|
ADD ; And the digit.
|
||||||
SETD.1 TextValue
|
|
||||||
STQ.1
|
STQ.1
|
||||||
|
|
||||||
SETD.1 TextDigits
|
SETD.1 TextDigits
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#Program
|
#Program
|
||||||
start:
|
start:
|
||||||
; Load our initial values into A and B.
|
; Load our initial values into A and B.
|
||||||
INIA 0x00
|
RSTA
|
||||||
CALL printByteDecimal
|
CALL printByteDecimal
|
||||||
CALL blankSpace
|
CALL blankSpace
|
||||||
; Move the value into B.
|
; Move the value into B.
|
||||||
@@ -22,8 +22,7 @@ start:
|
|||||||
PSHA
|
PSHA
|
||||||
POPB
|
POPB
|
||||||
; Copy Q into A
|
; Copy Q into A
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
; Print A.
|
; Print A.
|
||||||
CALL printByteDecimal
|
CALL printByteDecimal
|
||||||
CALL blankSpace
|
CALL blankSpace
|
||||||
|
|||||||
@@ -193,50 +193,43 @@ countNeighbors:
|
|||||||
LDB
|
LDB
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
DPUP 0d02
|
DPUP 0d02
|
||||||
|
|
||||||
LDB
|
LDB
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
DPUP 0d02
|
DPUP 0d02
|
||||||
|
|
||||||
LDB
|
LDB
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
DPUP 0d32
|
DPUP 0d32
|
||||||
|
|
||||||
LDB
|
LDB
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
DPUP 0d04
|
DPUP 0d04
|
||||||
|
|
||||||
LDB
|
LDB
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
DPUP 0d32
|
DPUP 0d32
|
||||||
|
|
||||||
LDB
|
LDB
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
DPUP 0d02
|
DPUP 0d02
|
||||||
|
|
||||||
LDB
|
LDB
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
DPUP 0d02
|
DPUP 0d02
|
||||||
|
|
||||||
LDB
|
LDB
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ start:
|
|||||||
INCD ; Increment to the next spot in the buffer.
|
INCD ; Increment to the next spot in the buffer.
|
||||||
BRI inputLoop ; Loop again to grab more string.
|
BRI inputLoop ; Loop again to grab more string.
|
||||||
inputEnd:
|
inputEnd:
|
||||||
INIA 0x00 ; Set A to 0.
|
RSTA ; Set A to 0.
|
||||||
STA ; Store it in the buffer.
|
STA ; Store it in the buffer.
|
||||||
SETD Buffer ; Set the Data Pointer to the start of the buffer again.
|
SETD Buffer ; Set the Data Pointer to the start of the buffer again.
|
||||||
CALL printString ; Print it out.
|
CALL printString ; Print it out.
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ repl:
|
|||||||
CALL printString
|
CALL printString
|
||||||
|
|
||||||
CALL readNonSpace
|
CALL readNonSpace
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
|
|
||||||
; Q, q, or end-of-file exits.
|
; Q, q, or end-of-file exits.
|
||||||
INIB 0xFF
|
INIB 0xFF
|
||||||
@@ -39,14 +38,12 @@ repl:
|
|||||||
BRQ inputError
|
BRQ inputError
|
||||||
|
|
||||||
CALL readNonSpace
|
CALL readNonSpace
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
SETD Operator
|
SETD Operator
|
||||||
STA
|
STA
|
||||||
|
|
||||||
CALL readNonSpace
|
CALL readNonSpace
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
CALL readHexByteFirst
|
CALL readHexByteFirst
|
||||||
SETD RightOperand
|
SETD RightOperand
|
||||||
STQ
|
STQ
|
||||||
@@ -98,7 +95,7 @@ readNonSpaceLoop:
|
|||||||
INIB 0x0D
|
INIB 0x0D
|
||||||
XOR
|
XOR
|
||||||
BRQ readNonSpaceLoop
|
BRQ readNonSpaceLoop
|
||||||
INIB 0x00
|
RSTB
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
@@ -108,12 +105,11 @@ readNonSpaceLoop:
|
|||||||
readHexByteFirst:
|
readHexByteFirst:
|
||||||
CALL clearParseStatus
|
CALL clearParseStatus
|
||||||
CALL hexNibble
|
CALL hexNibble
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
INIB 0xFF
|
INIB 0xFF
|
||||||
XOR
|
XOR
|
||||||
BRQ invalidByte
|
BRQ invalidByte
|
||||||
INIB 0x00
|
RSTB
|
||||||
SHL
|
SHL
|
||||||
SHL
|
SHL
|
||||||
SHL
|
SHL
|
||||||
@@ -121,11 +117,9 @@ readHexByteFirst:
|
|||||||
PSHA
|
PSHA
|
||||||
|
|
||||||
CALL readNonSpace
|
CALL readNonSpace
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
CALL hexNibble
|
CALL hexNibble
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
INIB 0xFF
|
INIB 0xFF
|
||||||
XOR
|
XOR
|
||||||
BRQ invalidLowNibble
|
BRQ invalidLowNibble
|
||||||
@@ -142,7 +136,7 @@ invalidByte:
|
|||||||
STA
|
STA
|
||||||
POPD
|
POPD
|
||||||
INIA 0xFF
|
INIA 0xFF
|
||||||
INIB 0x00
|
RSTB
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
@@ -157,8 +151,7 @@ hexNibble:
|
|||||||
CCF
|
CCF
|
||||||
SUB
|
SUB
|
||||||
BRC tryUpperHex
|
BRC tryUpperHex
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
INIB 0d10
|
INIB 0d10
|
||||||
CCF
|
CCF
|
||||||
SUB
|
SUB
|
||||||
@@ -171,8 +164,7 @@ tryUpperHex:
|
|||||||
CCF
|
CCF
|
||||||
SUB
|
SUB
|
||||||
BRC tryLowerHex
|
BRC tryLowerHex
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
INIB 0d06
|
INIB 0d06
|
||||||
CCF
|
CCF
|
||||||
SUB
|
SUB
|
||||||
@@ -185,8 +177,7 @@ tryLowerHex:
|
|||||||
CCF
|
CCF
|
||||||
SUB
|
SUB
|
||||||
BRC badNibble
|
BRC badNibble
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
INIB 0d06
|
INIB 0d06
|
||||||
CCF
|
CCF
|
||||||
SUB
|
SUB
|
||||||
@@ -195,14 +186,14 @@ tryLowerHex:
|
|||||||
badNibble:
|
badNibble:
|
||||||
POPA
|
POPA
|
||||||
INIA 0xFF
|
INIA 0xFF
|
||||||
INIB 0x00
|
RSTB
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
|
|
||||||
decimalNibble:
|
decimalNibble:
|
||||||
POPB
|
POPB
|
||||||
INIB 0x00
|
RSTB
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
@@ -223,8 +214,7 @@ lowerNibble:
|
|||||||
|
|
||||||
; Return Q=0 if Q was 0xFF, otherwise return a nonzero value.
|
; Return Q=0 if Q was 0xFF, otherwise return a nonzero value.
|
||||||
resultIsInvalid:
|
resultIsInvalid:
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
INIB 0xFF
|
INIB 0xFF
|
||||||
XOR
|
XOR
|
||||||
RET
|
RET
|
||||||
@@ -288,7 +278,7 @@ evaluate:
|
|||||||
INIA 0x01
|
INIA 0x01
|
||||||
STA
|
STA
|
||||||
RSTA
|
RSTA
|
||||||
INIB 0x00
|
RSTB
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
@@ -360,7 +350,7 @@ multiplyLoop:
|
|||||||
multiplyDone:
|
multiplyDone:
|
||||||
SETD Product
|
SETD Product
|
||||||
LDA
|
LDA
|
||||||
INIB 0x00
|
RSTB
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ int8mult:
|
|||||||
DECA ; Subtract one from the multiplier.
|
DECA ; Subtract one from the multiplier.
|
||||||
BRA int8multDone ; If the multiplier becomes zero, we're done.
|
BRA int8multDone ; If the multiplier becomes zero, we're done.
|
||||||
PSHA ; Push the multiplier back to the stack.
|
PSHA ; Push the multiplier back to the stack.
|
||||||
PSHQ ; Push the running total to the stack.
|
MVQA ; Push the running total to the stack. Pop the running total into A.
|
||||||
POPA ; Pop the running total into A.
|
|
||||||
BRI int8multLoop
|
BRI int8multLoop
|
||||||
return0:
|
return0:
|
||||||
RSTA
|
RSTA
|
||||||
@@ -58,8 +57,7 @@ int8div:
|
|||||||
POPA ; Pop the quotient counter from the stack.
|
POPA ; Pop the quotient counter from the stack.
|
||||||
INCA ; Increment it.
|
INCA ; Increment it.
|
||||||
PSHA ; Push it back onto the stack.
|
PSHA ; Push it back onto the stack.
|
||||||
PSHQ ; Push the running total onto the stack.
|
MVQA ; Push the running total onto the stack. Pop the runing total into A.
|
||||||
POPA ; Pop the runing total into A.
|
|
||||||
BRI int8divLoop ; Branch back to the loop.
|
BRI int8divLoop ; Branch back to the loop.
|
||||||
int8divDone:
|
int8divDone:
|
||||||
CCF ; Clear the Carry Flag.
|
CCF ; Clear the Carry Flag.
|
||||||
@@ -76,13 +74,11 @@ int8mod:
|
|||||||
int8modLoop:
|
int8modLoop:
|
||||||
SUB ; Subtract B from A.
|
SUB ; Subtract B from A.
|
||||||
BRC int8modDone
|
BRC int8modDone
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
BRI int8modLoop
|
BRI int8modLoop
|
||||||
int8modDone:
|
int8modDone:
|
||||||
; Add the divisor to Q to get the remainder.
|
; Add the divisor to Q to get the remainder.
|
||||||
CCF
|
CCF
|
||||||
PSHQ
|
MVQA
|
||||||
POPA
|
|
||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
|
|||||||
@@ -97,10 +97,10 @@ printByteDecimal:
|
|||||||
|
|
||||||
; Expects A to contain the byte you want to print as a hexadecimal value.
|
; Expects A to contain the byte you want to print as a hexadecimal value.
|
||||||
printByteHex:
|
printByteHex:
|
||||||
INIB 0x00 ; Set B to 0.
|
RSTB ; Set B to 0.
|
||||||
SHR SHR SHR SHR ; Shift Right four times to move the high nybble into the lower half of A.
|
SHR SHR SHR SHR ; Shift Right four times to move the high nybble into the lower half of A.
|
||||||
CALL printHexDigit ; Print the nybble.
|
CALL printHexDigit ; Print the nybble.
|
||||||
INIA 0x00 ; Set A to 0.
|
RSTA ; Set A to 0.
|
||||||
SHL SHL SHL SHL ; Shift left four times to move the low nybble back into the lower half of A.
|
SHL SHL SHL SHL ; Shift left four times to move the low nybble back into the lower half of A.
|
||||||
CALL printHexDigit ; Print the nybble.
|
CALL printHexDigit ; Print the nybble.
|
||||||
RET ; Return to the caller.
|
RET ; Return to the caller.
|
||||||
|
|||||||
+106
-34
@@ -79,6 +79,23 @@ DISKTOOL ?= ../SplitDisk
|
|||||||
COSMOS = $(BUILD)/CosmOS/Source/cosmos.bin
|
COSMOS = $(BUILD)/CosmOS/Source/cosmos.bin
|
||||||
APPS = $(patsubst CosmOS/Apps/%.asm,$(BUILD)/CosmOS/Apps/%.sbx,$(wildcard CosmOS/Apps/*.asm))
|
APPS = $(patsubst CosmOS/Apps/%.asm,$(BUILD)/CosmOS/Apps/%.sbx,$(wildcard CosmOS/Apps/*.asm))
|
||||||
COSMOS_DISK = $(BUILD)/cosmos.img
|
COSMOS_DISK = $(BUILD)/cosmos.img
|
||||||
|
|
||||||
|
# ---- What starts the machine ----
|
||||||
|
#
|
||||||
|
# Stage two goes into a boot slot as RAW BYTES: stage one reads blocks into Program Memory
|
||||||
|
# and jumps to the first one, so a sixteen byte header would be sixteen bytes of nonsense
|
||||||
|
# executed first. Its Data Segment travels with it and it copies that down itself.
|
||||||
|
#
|
||||||
|
# Stage one is not here at all. It is the ROM, built into the emulator by the top level
|
||||||
|
# makefile from the same source, which is what makes it the one part of this that a disk
|
||||||
|
# cannot replace.
|
||||||
|
STAGE2 = $(BUILD)/Boot/stage2.raw
|
||||||
|
DEPENDENCIES += $(BUILD)/Boot/stage2.d
|
||||||
|
|
||||||
|
$(STAGE2): Boot/stage2.asm
|
||||||
|
@mkdir -p $(@D)
|
||||||
|
$(ASM) $(INCLUDES) -M $(BUILD)/Boot/stage2.d -o $(BUILD)/Boot/stage2.sbx $<
|
||||||
|
tail -c +17 $(BUILD)/Boot/stage2.sbx > $@
|
||||||
DEPENDENCIES += $(APPS:.sbx=.d)
|
DEPENDENCIES += $(APPS:.sbx=.d)
|
||||||
|
|
||||||
$(BUILD)/CosmOS/Apps/%.sbx: CosmOS/Apps/%.asm
|
$(BUILD)/CosmOS/Apps/%.sbx: CosmOS/Apps/%.asm
|
||||||
@@ -100,52 +117,107 @@ cosmos: $(COSMOS) $(APPS) $(NATIVE_ASM)
|
|||||||
|
|
||||||
# Made from scratch every time, so that what is on it is what is in Apps/ now and not
|
# Made from scratch every time, so that what is on it is what is in Apps/ now and not
|
||||||
# also whatever used to be.
|
# also whatever used to be.
|
||||||
$(COSMOS_DISK): $(APPS) $(NATIVE_ASM) testPrograms/stringKeyword.asm \
|
#
|
||||||
CosmOS/Apps/hello.asm CosmOS/Apps/Say.asm CosmOS/Apps/Keys.asm \
|
# TWENTY FOUR DIRECTORY BLOCKS, WHICH IS ONE HUNDRED AND NINETY TWO NAMES. It was eight,
|
||||||
CosmOS/Source/services.asm CosmOS/Source/console.asm \
|
# and that is sixty four, of which thirty nine were already spoken for. The two ceilings a
|
||||||
|
# disk has were nowhere near each other: at the average file on here, twenty six blocks,
|
||||||
|
# sixty four names run out with the disk forty one per cent full. Names were going to be
|
||||||
|
# gone long before space was.
|
||||||
|
#
|
||||||
|
# A directory block is 256 bytes and holds eight entries, so the difference costs sixteen
|
||||||
|
# blocks of four thousand and ninety six - three tenths of one per cent - to buy a hundred
|
||||||
|
# and twenty eight more names. The superblock has carried this number per disk since the
|
||||||
|
# format was written, so nothing but this line knows what it is.
|
||||||
|
$(COSMOS_DISK): $(APPS) $(NATIVE_ASM) $(COSMOS) $(STAGE2) testPrograms/stringKeyword.asm \
|
||||||
|
$(wildcard CosmOS/Apps/*.asm) \
|
||||||
$(wildcard CosmOS/Source/*.asm) $(wildcard CosmOS/Assembler/*.asm)
|
$(wildcard CosmOS/Source/*.asm) $(wildcard CosmOS/Assembler/*.asm)
|
||||||
@mkdir -p $(@D)
|
@mkdir -p $(@D)
|
||||||
rm -f $@
|
rm -f $@
|
||||||
$(DISKTOOL) format $@ 4096 8
|
@# A BOOT AREA, so that this is a disk the machine can start itself from rather than
|
||||||
@for app in $(APPS); do $(DISKTOOL) put $@ $$app; done
|
@# one it has to be handed. Forty blocks a slot and two slots: stage two is about
|
||||||
$(DISKTOOL) put $@ $(NATIVE_ASM)
|
@# eight thousand bytes, and the second slot is what makes replacing it survivable,
|
||||||
@# SOURCE goes on as well, because an assembler with nothing to assemble is a
|
@# since raw blocks have no name and so nothing to rename.
|
||||||
@# demonstration of nothing.
|
$(DISKTOOL) format $@ 4096 24 40
|
||||||
|
$(DISKTOOL) boot $@ $(STAGE2) 0
|
||||||
|
@# THREE DIRECTORIES, WHICH IS WHAT A CLEAN INSTALL LOOKS LIKE: what you run, what you
|
||||||
|
@# assemble, and what those include. It was thirty nine files in one list with
|
||||||
|
@# cosmos.asm sitting between fileStream.asm and sbfs.asm.
|
||||||
@#
|
@#
|
||||||
@# hello.asm and Say.asm are the APPLICATION versions, so each assembles to a .sbx
|
@# The split is by ROLE rather than by which directory the host keeps them in. /Source
|
||||||
@# written straight over the one the host tool put there - which means the next
|
@# holds the things you name to the assembler and /Lib the things they pull in, which is
|
||||||
@# thing loaded is a program the machine built itself, in the same breath.
|
@# a distinction the host makes with -I and the machine now makes with a search path of
|
||||||
|
@# its own: an include is looked for beside you and then in /Lib. Without that, every
|
||||||
|
@# source that calls a service would have to sit in the same directory as services.asm
|
||||||
|
@# and there would be nothing to organise.
|
||||||
|
$(DISKTOOL) mkdir $@ /Apps
|
||||||
|
$(DISKTOOL) mkdir $@ /Source
|
||||||
|
$(DISKTOOL) mkdir $@ /Lib
|
||||||
|
$(DISKTOOL) mkdir $@ /System
|
||||||
|
$(DISKTOOL) mkdir $@ /System/Boot
|
||||||
|
@# The system itself, as a file, which is the whole of what boot.cfg chooses between.
|
||||||
|
@# No boot.cfg is written: stage two falls back to this name when there is none, and a
|
||||||
|
@# clean install having nothing to configure is the right default.
|
||||||
|
$(DISKTOOL) put $@ $(COSMOS) /System/Boot/cosmos.bin
|
||||||
|
@# What you run. /Apps is the second place the shell looks when a word it does not know
|
||||||
|
@# turns out to be a program, so anything in here starts by name from anywhere.
|
||||||
|
@for app in $(APPS); do \
|
||||||
|
$(DISKTOOL) put $@ $$app /Apps/`basename $$app` >/dev/null || exit 1; done
|
||||||
|
$(DISKTOOL) put $@ $(NATIVE_ASM) /Apps/Asm.sbx
|
||||||
|
@# What you assemble. All of it, because an assembler with nothing to assemble is a
|
||||||
|
@# demonstration of nothing:
|
||||||
@#
|
@#
|
||||||
@# Keys.asm is the one that brings a vector of its own, so assembling it exercises the
|
@# > cd /Source
|
||||||
@# version two header and the Vector Segment: the loader installs its handler, the
|
@# /Source> Asm cosmos.asm the system it is running on
|
||||||
@# console interrupts into it, and the shell takes the vector back at exit.
|
@# /Source> Asm Asm.asm and the thing that built it
|
||||||
@#
|
|
||||||
@# strings.asm is the odd one out on purpose: it has no #Include and no #Base, so it
|
|
||||||
@# comes out as a boot image rather than a loadable program, and the difference
|
|
||||||
@# between the two is visible on one disk.
|
|
||||||
$(DISKTOOL) put $@ CosmOS/Apps/hello.asm
|
|
||||||
$(DISKTOOL) put $@ CosmOS/Apps/Say.asm
|
|
||||||
$(DISKTOOL) put $@ CosmOS/Apps/Keys.asm
|
|
||||||
$(DISKTOOL) put $@ CosmOS/Source/services.asm
|
|
||||||
$(DISKTOOL) put $@ CosmOS/Source/console.asm
|
|
||||||
$(DISKTOOL) put $@ testPrograms/stringKeyword.asm strings.asm
|
|
||||||
@# And the whole of CosmOS, and the whole of the assembler, so that the machine can
|
|
||||||
@# build the system it is running on and then build the thing that built it:
|
|
||||||
@#
|
|
||||||
@# > load Asm.sbx
|
|
||||||
@# > run cosmos.asm
|
|
||||||
@# > run Asm.asm
|
|
||||||
@#
|
@#
|
||||||
@# Both come out byte for byte what the host tool makes from the same source.
|
@# Both come out byte for byte what the host tool makes from the same source.
|
||||||
@for f in CosmOS/Source/*.asm CosmOS/Assembler/*.asm; do \
|
@#
|
||||||
$(DISKTOOL) put $@ $$f >/dev/null; done
|
@# Keys.asm brings a vector of its own, so assembling it exercises the version two
|
||||||
|
@# header and the Vector Segment: the loader installs its handler, the console
|
||||||
|
@# interrupts into it, and the shell takes the vector back at exit.
|
||||||
|
@#
|
||||||
|
@# strings.asm is the odd one out on purpose. It has no #Include and no #Base, so it
|
||||||
|
@# comes out as a boot image rather than a loadable program, and the difference between
|
||||||
|
@# the two is visible on one disk.
|
||||||
|
$(DISKTOOL) put $@ CosmOS/Source/cosmos.asm /Source/cosmos.asm
|
||||||
|
$(DISKTOOL) put $@ CosmOS/Assembler/Asm.asm /Source/Asm.asm
|
||||||
|
$(DISKTOOL) put $@ CosmOS/Assembler/readTest.asm /Source/readTest.asm
|
||||||
|
$(DISKTOOL) put $@ CosmOS/Assembler/tokenTest.asm /Source/tokenTest.asm
|
||||||
|
$(DISKTOOL) put $@ CosmOS/Apps/hello.asm /Source/hello.asm
|
||||||
|
$(DISKTOOL) put $@ CosmOS/Apps/Say.asm /Source/Say.asm
|
||||||
|
$(DISKTOOL) put $@ CosmOS/Apps/Keys.asm /Source/Keys.asm
|
||||||
|
$(DISKTOOL) put $@ testPrograms/stringKeyword.asm /Source/strings.asm
|
||||||
|
@# And the loader, so the machine can rebuild what starts it. Assembling stage2.asm on
|
||||||
|
@# the machine and writing the result into the other boot slot is the whole of a
|
||||||
|
@# self-hosted boot chain, and everything it includes is already in /Lib.
|
||||||
|
$(DISKTOOL) put $@ Boot/stage1.asm /Source/stage1.asm
|
||||||
|
$(DISKTOOL) put $@ Boot/stage2.asm /Source/stage2.asm
|
||||||
|
@# And what those include. Everything here is named by an #Include somewhere and by
|
||||||
|
@# nothing else, which is exactly what makes it a library rather than a source.
|
||||||
|
@for f in CosmOS/Source/console.asm CosmOS/Source/fileStream.asm \
|
||||||
|
CosmOS/Source/sbfs.asm CosmOS/Source/services.asm CosmOS/Source/text.asm \
|
||||||
|
CosmOS/Source/config.asm \
|
||||||
|
CosmOS/Assembler/classify.asm CosmOS/Assembler/labels.asm \
|
||||||
|
CosmOS/Assembler/numbers.asm CosmOS/Assembler/scratch.asm \
|
||||||
|
CosmOS/Assembler/source.asm CosmOS/Assembler/table.asm \
|
||||||
|
CosmOS/Assembler/token.asm CosmOS/Assembler/vectors.asm; do \
|
||||||
|
$(DISKTOOL) put $@ $$f /Lib/`basename $$f` >/dev/null || exit 1; done
|
||||||
|
|
||||||
# The system as well as the disk. Building only the image leaves whatever cosmos.bin was
|
# 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
|
# there before, or none at all, and then the disk is booted with a system that does not
|
||||||
# match the programs on it.
|
# match the programs on it.
|
||||||
cosmos-disk: $(COSMOS) $(COSMOS_DISK)
|
cosmos-disk: $(COSMOS) $(COSMOS_DISK)
|
||||||
|
|
||||||
run-cosmos: $(COSMOS) $(COSMOS_DISK)
|
# THE MACHINE STARTS ITSELF. No image is named, so the emulator shadows its ROM into
|
||||||
|
# Program Memory and that reads the disk for everything else - a boot slot, then a loader,
|
||||||
|
# then whatever /System/Boot/boot.cfg names, or cosmos.bin when it names nothing.
|
||||||
|
run-cosmos: $(COSMOS_DISK)
|
||||||
|
$(EMU) --disk $(COSMOS_DISK)
|
||||||
|
|
||||||
|
# The same disk with the system handed over directly instead, which is what a debugger
|
||||||
|
# does: memory is placed from outside and nothing on the disk is consulted about it. Useful
|
||||||
|
# when the thing being debugged is the boot chain itself, since it skips the boot chain.
|
||||||
|
run-cosmos-direct: $(COSMOS) $(COSMOS_DISK)
|
||||||
$(EMU) --disk $(COSMOS_DISK) $(COSMOS)
|
$(EMU) --disk $(COSMOS_DISK) $(COSMOS)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@@ -155,4 +227,4 @@ clean:
|
|||||||
# reassembles every program that includes it.
|
# reassembles every program that includes it.
|
||||||
-include $(DEPENDENCIES)
|
-include $(DEPENDENCIES)
|
||||||
|
|
||||||
.PHONY: all clean cosmos cosmos-disk run-cosmos
|
.PHONY: all clean cosmos cosmos-disk run-cosmos run-cosmos-direct
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ refusedHandler:
|
|||||||
BRC refusedCarried
|
BRC refusedCarried
|
||||||
RETI
|
RETI
|
||||||
refusedCarried:
|
refusedCarried:
|
||||||
DPDN.0 0d01
|
DECD.0
|
||||||
LDA.0
|
LDA.0
|
||||||
INCA
|
INCA
|
||||||
STA.0
|
STA.0
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ bTaken:
|
|||||||
OUTA 0x00
|
OUTA 0x00
|
||||||
|
|
||||||
CCF
|
CCF
|
||||||
BNC cTaken
|
BNC cTaken ; splitlint: this test exists to check a branch whose carry is known
|
||||||
BRI wrong
|
BRI wrong
|
||||||
cTaken:
|
cTaken:
|
||||||
CALL blankSpace
|
CALL blankSpace
|
||||||
@@ -78,7 +78,7 @@ cTaken:
|
|||||||
INIB 0x01
|
INIB 0x01
|
||||||
CCF
|
CCF
|
||||||
ADD
|
ADD
|
||||||
BNC wrong
|
BNC wrong ; splitlint: the point is that a set carry does NOT take this
|
||||||
CALL blankSpace
|
CALL blankSpace
|
||||||
INIA 0d99 ; 'c'
|
INIA 0d99 ; 'c'
|
||||||
OUTA 0x00
|
OUTA 0x00
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ stepPastRefusal:
|
|||||||
BRC carried
|
BRC carried
|
||||||
RETI
|
RETI
|
||||||
carried:
|
carried:
|
||||||
DPDN.0 0d01
|
DECD.0
|
||||||
LDA.0
|
LDA.0
|
||||||
INCA
|
INCA
|
||||||
STA.0
|
STA.0
|
||||||
|
|||||||
@@ -5,7 +5,12 @@
|
|||||||
; the same byte again, which is why this handler moves the saved address on by one
|
; the same byte again, which is why this handler moves the saved address on by one
|
||||||
; first. MVSD is what lets it reach the frame at all.
|
; first. MVSD is what lets it reach the frame at all.
|
||||||
;
|
;
|
||||||
; 0xFE is not an instruction. Writing it as a literal is the only way past the
|
; CHOSEN AWAY FROM HALT, because the bytes next to it get used. This said 0xFE for
|
||||||
|
; a long time, and then 0xFE became WAIT - so the test stopped faulting and started
|
||||||
|
; SLEEPING. It hung instead of failing, which is the worst way for a test to notice
|
||||||
|
; that the thing it was testing had moved.
|
||||||
|
;
|
||||||
|
; 0xFD is not an instruction. Writing it as a literal is the only way past the
|
||||||
; assembler, which is what makes a program containing one buildable.
|
; assembler, which is what makes a program containing one buildable.
|
||||||
;
|
;
|
||||||
; Correct output is:
|
; Correct output is:
|
||||||
@@ -20,7 +25,7 @@ start:
|
|||||||
INIA 0x0A
|
INIA 0x0A
|
||||||
OUTA 0x00
|
OUTA 0x00
|
||||||
|
|
||||||
0xFE ; Not an instruction. The handler steps over this.
|
0xFD ; Not an instruction. The handler steps over this.
|
||||||
|
|
||||||
INIA 0d75 ; 'K'. Reached only because the handler moved the address on.
|
INIA 0d75 ; 'K'. Reached only because the handler moved the address on.
|
||||||
OUTA 0x00
|
OUTA 0x00
|
||||||
@@ -41,7 +46,7 @@ faultHandler:
|
|||||||
RETI
|
RETI
|
||||||
|
|
||||||
carried:
|
carried:
|
||||||
DPDN.0 0d01
|
DECD.0
|
||||||
LDA.0
|
LDA.0
|
||||||
INCA
|
INCA
|
||||||
STA.0
|
STA.0
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
; Tests that the CPU stops when it meets a byte it cannot decode.
|
; Tests that the CPU stops when it meets a byte it cannot decode.
|
||||||
;
|
;
|
||||||
; 0xFE is not an instruction. Placing it in the Program Segment as a literal gets
|
; CHOSEN AWAY FROM HALT, because the bytes next to it get used. This said 0xFE for
|
||||||
|
; a long time, and then 0xFE became WAIT - so the test stopped faulting and started
|
||||||
|
; SLEEPING. It hung instead of failing, which is the worst way for a test to notice
|
||||||
|
; that the thing it was testing had moved.
|
||||||
|
;
|
||||||
|
; 0xFD is not an instruction. Placing it in the Program Segment as a literal gets
|
||||||
; it past the assembler, which is the only way to build a program containing one.
|
; it past the assembler, which is the only way to build a program containing one.
|
||||||
;
|
;
|
||||||
; The CPU should raise the Fault Flag, halt, and leave the Program Counter pointing
|
; The CPU should raise the Fault Flag, halt, and leave the Program Counter pointing
|
||||||
@@ -11,5 +16,5 @@
|
|||||||
|
|
||||||
start:
|
start:
|
||||||
INIA 0d65 ; Something harmless first, so the fault is not at address zero.
|
INIA 0d65 ; Something harmless first, so the fault is not at address zero.
|
||||||
0xFE ; Not an instruction.
|
0xFD ; Not an instruction.
|
||||||
HALT ; Never reached.
|
HALT ; Never reached.
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ guardHandler:
|
|||||||
BRC stepCarried
|
BRC stepCarried
|
||||||
RETI
|
RETI
|
||||||
stepCarried:
|
stepCarried:
|
||||||
DPDN.0 0d01
|
DECD.0
|
||||||
LDA.0
|
LDA.0
|
||||||
INCA
|
INCA
|
||||||
STA.0
|
STA.0
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ start:
|
|||||||
INCD ; Increment to the next spot in the buffer.
|
INCD ; Increment to the next spot in the buffer.
|
||||||
BRI inputLoop ; Loop again to grab more string.
|
BRI inputLoop ; Loop again to grab more string.
|
||||||
inputEnd:
|
inputEnd:
|
||||||
INIA 0x00 ; Set A to 0.
|
RSTA ; Set A to 0.
|
||||||
STA ; Store it in the buffer.
|
STA ; Store it in the buffer.
|
||||||
SETD Buffer ; Set the Data Pointer to the start of the buffer again.
|
SETD Buffer ; Set the Data Pointer to the start of the buffer again.
|
||||||
CALL printString ; Print it out.
|
CALL printString ; Print it out.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ start:
|
|||||||
ADD ; Q = 0, and the Carry Flag is set.
|
ADD ; Q = 0, and the Carry Flag is set.
|
||||||
SIF
|
SIF
|
||||||
CIF
|
CIF
|
||||||
BRC carryHeld
|
BRC carryHeld ; splitlint: whether carry survived SIF and CIF is what is being tested
|
||||||
|
|
||||||
; Falling through here means one flag trampled the other.
|
; Falling through here means one flag trampled the other.
|
||||||
INIA 0d88 ; 'X'
|
INIA 0d88 ; 'X'
|
||||||
|
|||||||
@@ -114,6 +114,12 @@ Back:
|
|||||||
; by hand because SplitBit has no linker: to this program it is just so much data.
|
; by hand because SplitBit has no linker: to this program it is just so much data.
|
||||||
;
|
;
|
||||||
; INIA 'l' OUTA 0 INIA 'o' OUTA 0 ... and so on, then RETI
|
; INIA 'l' OUTA 0 INIA 'o' OUTA 0 ... and so on, then RETI
|
||||||
|
;
|
||||||
|
; THESE ARE OPCODES AND THEY MOVE WHEN THE MAP MOVES. Nothing checks them - to the
|
||||||
|
; assembler they are numbers, and to this program they are data - so an opcode
|
||||||
|
; reorganisation has to come here by hand. It has happened once: RETI was 0x19 before the
|
||||||
|
; branch and subroutine blocks were split apart, and this was the only place in the corpus
|
||||||
|
; that noticed, by faulting on a byte that had stopped being an instruction.
|
||||||
Payload:
|
Payload:
|
||||||
0x26 0x6C 0xD1 0x00
|
0x26 0x6C 0xD1 0x00
|
||||||
0x26 0x6F 0xD1 0x00
|
0x26 0x6F 0xD1 0x00
|
||||||
@@ -122,4 +128,4 @@ Payload:
|
|||||||
0x26 0x65 0xD1 0x00
|
0x26 0x65 0xD1 0x00
|
||||||
0x26 0x64 0xD1 0x00
|
0x26 0x64 0xD1 0x00
|
||||||
0x26 0x0A 0xD1 0x00
|
0x26 0x0A 0xD1 0x00
|
||||||
0x19
|
0x73 ; RETI
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
#Program
|
#Program
|
||||||
|
|
||||||
clearRegisters:
|
clearRegisters:
|
||||||
INIA 0x00;
|
RSTA
|
||||||
INIB 0x00;
|
RSTB
|
||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
; The six instructions added after the first sixty four, and what each is for.
|
||||||
|
;
|
||||||
|
; Two of them are a call that puts nothing back, and four move a Data Pointer by a value
|
||||||
|
; worked out while the program is running rather than one written into it.
|
||||||
|
;
|
||||||
|
; WHY A SECOND KIND OF CALL. CALL puts A, B and Data Pointers 0 through 2 back the way it
|
||||||
|
; found them, which costs ten bytes of Stack and means a subroutine can only hand anything
|
||||||
|
; back through Q, DP3 or memory. That is the right default and it is what almost everything
|
||||||
|
; here uses. RCAL costs two bytes and puts nothing back at all, which is what a short leaf
|
||||||
|
; routine wants - and it is unsafe in exactly the way its name says, because everything the
|
||||||
|
; callee touches, the caller has lost.
|
||||||
|
;
|
||||||
|
; The two frames are different sizes, so RCAL must be returned from with RRET and CALL with
|
||||||
|
; RET. Crossing them walks the Stack to somewhere that was never a return address.
|
||||||
|
;
|
||||||
|
; WHY OFFSET BY A REGISTER. DPUP and DPDN take a byte written into the program, so moving a
|
||||||
|
; pointer by something just worked out meant storing it and loading it back. DPUA and DPDA
|
||||||
|
; take A; DPUW and DPDW take A and B together, which is how every sixteen bit value on this
|
||||||
|
; machine is carried between registers.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
; ---- Offsetting by a byte, up and then back down ----
|
||||||
|
SETD.0 Text
|
||||||
|
INIA 0d7
|
||||||
|
DPUA.0
|
||||||
|
LDA.0
|
||||||
|
OUTA 0x00 ; the eighth character
|
||||||
|
INIA 0d4
|
||||||
|
DPDA.0
|
||||||
|
LDA.0
|
||||||
|
OUTA 0x00 ; and four before it
|
||||||
|
|
||||||
|
; ---- Offsetting by a whole sixteen bit value ----
|
||||||
|
SETD.1 Text
|
||||||
|
RSTA
|
||||||
|
INIB 0d13
|
||||||
|
DPUW.1
|
||||||
|
LDA.1
|
||||||
|
OUTA 0x00
|
||||||
|
RSTA
|
||||||
|
INIB 0d13
|
||||||
|
DPDW.1
|
||||||
|
LDA.1
|
||||||
|
OUTA 0x00 ; back where it started
|
||||||
|
|
||||||
|
; ---- What each kind of call puts back ----
|
||||||
|
;
|
||||||
|
; The same callee is reached both ways and sets A to something else. After the raw call
|
||||||
|
; that is what A holds; after the safe one it is not.
|
||||||
|
INIA 0x41
|
||||||
|
RCAL wrecker
|
||||||
|
OUTA 0x00 ; Z - the callee's
|
||||||
|
INIA 0x41
|
||||||
|
CALL polite
|
||||||
|
OUTA 0x00 ; A - put back
|
||||||
|
|
||||||
|
; ---- And what each costs ----
|
||||||
|
;
|
||||||
|
; Printed as a digit added to '0', so two bytes reads as "2" and ten reads as the
|
||||||
|
; character ten along from it.
|
||||||
|
MVSD.0
|
||||||
|
PSHD.0
|
||||||
|
POPB
|
||||||
|
POPA
|
||||||
|
SETD.2 Before
|
||||||
|
STB.2
|
||||||
|
|
||||||
|
RCAL rawCost
|
||||||
|
CALL safeCost
|
||||||
|
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
|
|
||||||
|
wrecker:
|
||||||
|
INIA 0x5A
|
||||||
|
RRET
|
||||||
|
|
||||||
|
polite:
|
||||||
|
INIA 0x5A
|
||||||
|
RET
|
||||||
|
|
||||||
|
; WRITTEN OUT TWICE RATHER THAN CALLED, because a call would put its own frame down on
|
||||||
|
; top of the one being measured and each of these would report ten bytes more than it
|
||||||
|
; costs. The first version of this did exactly that and printed twelve and twenty.
|
||||||
|
rawCost:
|
||||||
|
MVSD.1
|
||||||
|
PSHD.1
|
||||||
|
POPB
|
||||||
|
POPA
|
||||||
|
SETD.2 Before
|
||||||
|
LDA.2
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
MVQA
|
||||||
|
INIB 0x30
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQA
|
||||||
|
OUTA 0x00
|
||||||
|
RRET
|
||||||
|
|
||||||
|
safeCost:
|
||||||
|
MVSD.1
|
||||||
|
PSHD.1
|
||||||
|
POPB
|
||||||
|
POPA
|
||||||
|
SETD.2 Before
|
||||||
|
LDA.2
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
MVQA
|
||||||
|
INIB 0x30
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
MVQA
|
||||||
|
OUTA 0x00
|
||||||
|
RET
|
||||||
|
|
||||||
|
#Data
|
||||||
|
|
||||||
|
Text:
|
||||||
|
"0123456789abcdefg"
|
||||||
|
Before:
|
||||||
|
0x00
|
||||||
@@ -50,7 +50,7 @@ refusalHandler:
|
|||||||
RETI
|
RETI
|
||||||
|
|
||||||
carried:
|
carried:
|
||||||
DPDN.0 0d01
|
DECD.0
|
||||||
LDA.0
|
LDA.0
|
||||||
INCA
|
INCA
|
||||||
STA.0
|
STA.0
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ bankHandler:
|
|||||||
BRC bankCarried
|
BRC bankCarried
|
||||||
RETI
|
RETI
|
||||||
bankCarried:
|
bankCarried:
|
||||||
DPDN.0 0d01
|
DECD.0
|
||||||
LDA.0
|
LDA.0
|
||||||
INCA
|
INCA
|
||||||
STA.0
|
STA.0
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
#Program
|
#Program
|
||||||
|
|
||||||
start:
|
start:
|
||||||
INIA 0x00
|
RSTA
|
||||||
CALL reportPort
|
CALL reportPort
|
||||||
INIA 0x10
|
INIA 0x10
|
||||||
CALL reportPort
|
CALL reportPort
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
; runOffTest.asm
|
||||||
|
; A program that runs off the end of itself.
|
||||||
|
;
|
||||||
|
; THIS IS WHY 0x00 IS NOT AN INSTRUCTION. Program Memory that has never been written, or a
|
||||||
|
; load that stopped part way and left zeroes in its tail, used to read as a long run of
|
||||||
|
; additions - the machine would carry on through them, arrive somewhere unpredictable, and
|
||||||
|
; whatever went wrong there would be a long way from the byte that caused it.
|
||||||
|
;
|
||||||
|
; Nothing in 0x00 to 0x0F is an instruction now, so a run into blank memory faults where it
|
||||||
|
; is met and says the address. That is the difference between a diagnosis and a search, and
|
||||||
|
; it costs an opcode block nobody was using.
|
||||||
|
;
|
||||||
|
; The program does not halt. It is not supposed to: falling off the end IS the test.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
INIA 0x2E ; "."
|
||||||
|
OUTA 0x00
|
||||||
@@ -89,7 +89,7 @@ showFound:
|
|||||||
showContents:
|
showContents:
|
||||||
SETD.0 Landing
|
SETD.0 Landing
|
||||||
SETD.1 SbfsFileBlocks
|
SETD.1 SbfsFileBlocks
|
||||||
DPUP.1 0d01
|
INCD.1
|
||||||
LDA.1
|
LDA.1
|
||||||
SETD.1 LeftHigh
|
SETD.1 LeftHigh
|
||||||
STA.1
|
STA.1
|
||||||
|
|||||||
@@ -60,13 +60,12 @@ walkCheck:
|
|||||||
; A file is its blocks times 256 plus its tail, which is the block count in the high
|
; A file is its blocks times 256 plus its tail, which is the block count in the high
|
||||||
; byte and the tail in the low one. Nothing has to multiply anything.
|
; byte and the tail in the low one. Nothing has to multiply anything.
|
||||||
SETD.0 SbfsFileBlocks
|
SETD.0 SbfsFileBlocks
|
||||||
DPUP.0 0d01
|
INCD.0
|
||||||
LDA.0
|
LDA.0
|
||||||
SETD.1 Size
|
SETD.1 Size
|
||||||
STA.1
|
STA.1
|
||||||
SETD.0 SbfsFileTail
|
SETD.0 SbfsFileTail
|
||||||
LDA.0
|
LDA.0
|
||||||
SETD.1 Size
|
|
||||||
INCD.1
|
INCD.1
|
||||||
STA.1
|
STA.1
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,21 @@
|
|||||||
; quiet: 7 a service that says nothing leaves Q as it found it
|
; quiet: 7 a service that says nothing leaves Q as it found it
|
||||||
; answer: 42 one that does, does not
|
; answer: 42 one that does, does not
|
||||||
; pointer: ABC and DP3 comes back the same way
|
; pointer: ABC and DP3 comes back the same way
|
||||||
|
; sret: 42 and SRET says the same thing without touching the frame
|
||||||
|
; sretptr: ABC for a pointer too
|
||||||
|
; sretkept: 7 while everything a RET would restore still comes back
|
||||||
|
;
|
||||||
|
; ---- The frame editing above is what SRET exists to replace ----
|
||||||
|
;
|
||||||
|
; Everything between MVSD and RETI in "answer" and "pointer" is a routine reaching into its
|
||||||
|
; own frame to un-save two fields, using offsets it has to know by heart. Thirty places in
|
||||||
|
; CosmOS did that, and all thirty would have gone quietly wrong the day the frame gained a
|
||||||
|
; field. SRET is the same instruction sequence as RETI with the saved Q and DP3 stepped
|
||||||
|
; over instead of restored, so a handler answers the way a subroutine does and nothing
|
||||||
|
; below has to know what a frame looks like.
|
||||||
|
;
|
||||||
|
; Both are kept here on purpose. RETI is still how a HARDWARE handler says it was never
|
||||||
|
; there, and a service that has nothing to say should still use it.
|
||||||
|
|
||||||
#Include console.asm
|
#Include console.asm
|
||||||
|
|
||||||
@@ -59,6 +74,39 @@ start:
|
|||||||
POPD.0
|
POPD.0
|
||||||
CALL printString
|
CALL printString
|
||||||
CALL newLine
|
CALL newLine
|
||||||
|
|
||||||
|
; ---- And the same three answers, given with SRET instead ----
|
||||||
|
SETD.0 SretText
|
||||||
|
CALL printString
|
||||||
|
SWI sretAnswer
|
||||||
|
MVQA
|
||||||
|
CALL printByteDecimal
|
||||||
|
CALL newLine
|
||||||
|
|
||||||
|
SETD.0 SretPtrText
|
||||||
|
CALL printString
|
||||||
|
SWI sretPointer
|
||||||
|
PSHD.3
|
||||||
|
POPD.0
|
||||||
|
CALL printString
|
||||||
|
CALL newLine
|
||||||
|
|
||||||
|
; What SRET must still put back. A handler that trampled A, B and DP0 to DP2 would break
|
||||||
|
; every caller, which is exactly why RET restores them - so SRET does too, and the number
|
||||||
|
; printed here is the caller's own, loaded before the service was asked for anything.
|
||||||
|
INIA 0d7
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD ; Q is the caller's seven,
|
||||||
|
SETD.0 SretKeptText ; and DP0 names the label, BEFORE the service is asked.
|
||||||
|
SWI sretTrample
|
||||||
|
; Printed after the call rather than before it, which is the whole of the check: the
|
||||||
|
; handler pointed DP0 somewhere else, so this says "sretkept" only if SRET brought the
|
||||||
|
; caller's own pointer back. Printing first would have tested nothing at all.
|
||||||
|
CALL printString
|
||||||
|
MVQA
|
||||||
|
CALL printByteDecimal
|
||||||
|
CALL newLine
|
||||||
HALT
|
HALT
|
||||||
|
|
||||||
; Says nothing, so whatever the caller had in Q is still there afterwards.
|
; Says nothing, so whatever the caller had in Q is still there afterwards.
|
||||||
@@ -88,6 +136,31 @@ pointer:
|
|||||||
STA.1
|
STA.1
|
||||||
RETI
|
RETI
|
||||||
|
|
||||||
|
; ---- The same answers, without the frame ----
|
||||||
|
;
|
||||||
|
; Q and DP3 are set the way any subroutine sets them, and SRET leaves them alone. There is
|
||||||
|
; no MVSD, no offset, and nothing here that would need revisiting if the frame changed.
|
||||||
|
sretAnswer:
|
||||||
|
INIA 0d42
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
SRET
|
||||||
|
|
||||||
|
sretPointer:
|
||||||
|
SETD.3 Letters
|
||||||
|
SRET
|
||||||
|
|
||||||
|
; Sets everything a RET would restore to something wrong, to show that SRET restores it.
|
||||||
|
; If any of these came back, the caller's 7 would not.
|
||||||
|
sretTrample:
|
||||||
|
INIA 0d99
|
||||||
|
INIB 0d98
|
||||||
|
SETD.0 Letters
|
||||||
|
SETD.1 Letters
|
||||||
|
SETD.2 Letters
|
||||||
|
SRET
|
||||||
|
|
||||||
#Data
|
#Data
|
||||||
|
|
||||||
QuietText:
|
QuietText:
|
||||||
@@ -96,6 +169,12 @@ AnswerText:
|
|||||||
"answer: "
|
"answer: "
|
||||||
PointerText:
|
PointerText:
|
||||||
"pointer: "
|
"pointer: "
|
||||||
|
SretText:
|
||||||
|
"sret: "
|
||||||
|
SretPtrText:
|
||||||
|
"sretptr: "
|
||||||
|
SretKeptText:
|
||||||
|
"sretkept: "
|
||||||
Letters:
|
Letters:
|
||||||
"ABC"
|
"ABC"
|
||||||
|
|
||||||
@@ -105,3 +184,6 @@ Letters:
|
|||||||
quiet quiet
|
quiet quiet
|
||||||
answer answer
|
answer answer
|
||||||
pointer pointer
|
pointer pointer
|
||||||
|
sretAnswer sretAnswer
|
||||||
|
sretPointer sretPointer
|
||||||
|
sretTrample sretTrample
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ start:
|
|||||||
; The Stack Pointer points at the next free slot, so the byte pushed last sits one
|
; The Stack Pointer points at the next free slot, so the byte pushed last sits one
|
||||||
; above it, and the one before that sits two above.
|
; above it, and the one before that sits two above.
|
||||||
MVSD.0
|
MVSD.0
|
||||||
DPUP.0 0d01
|
INCD.0
|
||||||
LDA.0 ; 'K', the last one pushed.
|
LDA.0 ; 'K', the last one pushed.
|
||||||
INCD.0
|
INCD.0
|
||||||
LDB.0 ; 'O', the one before it.
|
LDB.0 ; 'O', the one before it.
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
; waitTest.asm
|
||||||
|
; WAIT, on a disk slow enough to be worth waiting for.
|
||||||
|
;
|
||||||
|
; Two reads, waited for rather than spun on, with INTERRUPTS MASKED and no handler
|
||||||
|
; installed anywhere. That is the shape this instruction exists for: a program sleeps on a
|
||||||
|
; device it has no handler for and reads the device's status when it wakes.
|
||||||
|
;
|
||||||
|
; THE SECOND READ IS THE TEST. A line that wakes the CPU without being dispatched has to
|
||||||
|
; be taken down by the WAIT itself; left standing, it is found by the next WAIT, which
|
||||||
|
; returns at once, and the program spins for the whole of the second read while looking
|
||||||
|
; exactly as though it slept. Both reads print, so a hang fails here - but a program that
|
||||||
|
; spun would print the same two characters, and what tells them apart is the emulator's
|
||||||
|
; count of idle cycles against bus cycles. Tests/terminal.sh is where that is checked,
|
||||||
|
; because settle() strips cycle counts out of every recorded output including this one.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Program
|
||||||
|
start:
|
||||||
|
CIF
|
||||||
|
INIA 0d3
|
||||||
|
OUTA 0xE3
|
||||||
|
INIA 0x20
|
||||||
|
OUTA 0xE2
|
||||||
|
INIA 0x03
|
||||||
|
OUTA 0xE8
|
||||||
|
RSTA
|
||||||
|
OUTA 0x20
|
||||||
|
OUTA 0x21
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x22
|
||||||
|
CALL waitDisk
|
||||||
|
INIA 0x31 ; "1"
|
||||||
|
OUTA 0x00
|
||||||
|
RSTA
|
||||||
|
OUTA 0x20
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x21 ; Block 1, a different one.
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x22
|
||||||
|
CALL waitDisk
|
||||||
|
INIA 0x32 ; "2"
|
||||||
|
OUTA 0x00
|
||||||
|
INIA 0x0A
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
|
|
||||||
|
waitDisk:
|
||||||
|
INA 0x23
|
||||||
|
INIB 0x01
|
||||||
|
AND
|
||||||
|
BRQ waitDone
|
||||||
|
WAIT
|
||||||
|
BRI waitDisk
|
||||||
|
waitDone:
|
||||||
|
RET
|
||||||
|
#Vectors
|
||||||
|
Boot start
|
||||||
@@ -23,6 +23,7 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
|
|||||||
| [`Source/Emulator`](Source/Emulator) | The machine: CPU, memory controller, devices, console, disk |
|
| [`Source/Emulator`](Source/Emulator) | The machine: CPU, memory controller, devices, console, disk |
|
||||||
| [`Source/Assembler`](Source/Assembler) | The assembler that runs on a host |
|
| [`Source/Assembler`](Source/Assembler) | The assembler that runs on a host |
|
||||||
| [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem |
|
| [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem |
|
||||||
|
| [`Source/Linter`](Source/Linter) | SplitLint, which points out needlessly long assembly forms |
|
||||||
| [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life |
|
| [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life |
|
||||||
| [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker |
|
| [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker |
|
||||||
| [`Programs/Loader`](Programs/Loader) | The standalone loader CosmOS grew out of |
|
| [`Programs/Loader`](Programs/Loader) | The standalone loader CosmOS grew out of |
|
||||||
@@ -33,7 +34,7 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
|
|||||||
## The Machine:
|
## The Machine:
|
||||||
|
|
||||||
- **Harvard architecture.** Two 64K memories, one for instructions and one for data. An instruction can only read the second, which is why strings live there and why the memory controller exists.
|
- **Harvard architecture.** Two 64K memories, one for instructions and one for data. An instruction can only read the second, which is why strings live there and why the memory controller exists.
|
||||||
- **Its own instruction set**, 64 instructions, four Data Pointers, and a Q register that holds what the ALU last worked out. Small enough that the table describing it fits in the machine's own memory, which is what lets it disassemble and assemble for itself.
|
- **Its own instruction set**, 72 instructions, four Data Pointers, and a Q register that holds what the ALU last worked out. Small enough that the table describing it fits in the machine's own memory, which is what lets it disassemble and assemble for itself.
|
||||||
- **Interrupts.** Software traps, hardware lines from devices, and faults, all arriving through one vector table with a full context save.
|
- **Interrupts.** Software traps, hardware lines from devices, and faults, all arriving through one vector table with a full context save.
|
||||||
- **A bus programs can enumerate**, so a program can ask what a machine is made of rather than being told.
|
- **A bus programs can enumerate**, so a program can ask what a machine is made of rather than being told.
|
||||||
- **A memory controller** that reads and writes Program Memory, moves blocks between 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.
|
- **A memory controller** that reads and writes Program Memory, moves blocks between 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.
|
||||||
@@ -49,7 +50,7 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
|
|||||||
|
|
||||||
## Getting Started:
|
## Getting Started:
|
||||||
|
|
||||||
Clone it and build the three tools. You need gcc and make, or similar:
|
Clone it and build the four tools. You need gcc and make, or similar:
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git
|
git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git
|
||||||
@@ -89,9 +90,89 @@ Then `dir` to see what is there, `load Snake.sbx` and `run` to play something, o
|
|||||||
| `-c`, `--cycles N` | Stop after N cycles rather than running until the program halts. Useful for programs that never halt, and for getting the same output from a run every time. |
|
| `-c`, `--cycles N` | Stop after N cycles rather than running until the program halts. Useful for programs that never halt, and for getting the same output from a run every time. |
|
||||||
| `-f`, `--fast` | Run as fast as the host allows, ignoring the emulated cycle rate. |
|
| `-f`, `--fast` | Run as fast as the host allows, ignoring the emulated cycle rate. |
|
||||||
| `-D`, `--disk <file>` | Attach a disk image, creating a 128K one if the file is not there. |
|
| `-D`, `--disk <file>` | Attach a disk image, creating a 128K one if the file is not there. |
|
||||||
|
| `-L`, `--disk-cycles N` | How many cycles a block read or write takes. Zero, the default, finishes before the next instruction starts. |
|
||||||
| `-W`, `--write-protect` | Attach the disk read only. A disk whose image the host will not let you write is read only whether you ask for this or not. |
|
| `-W`, `--write-protect` | Attach the disk read only. A disk whose image the host will not let you write is read only whether you ask for this or not. |
|
||||||
| `-h`, `--help` | Show help and usage information. |
|
| `-h`, `--help` | Show help and usage information. |
|
||||||
|
|
||||||
|
**The boot image is optional now.** Named one, the emulator places it into memory and
|
||||||
|
starts it, which is what a debugger does and how every test here runs - a real thing real
|
||||||
|
machines allow, not a shortcut to apologise for. Given only a disk, the machine starts the
|
||||||
|
way hardware would:
|
||||||
|
|
||||||
|
```
|
||||||
|
./SplitBit --disk system.img
|
||||||
|
```
|
||||||
|
|
||||||
|
The emulator carries `Programs/Boot/stage1.asm` as a **shadowed ROM**: at reset its bytes
|
||||||
|
are copied into Program Memory, boot vector included, and the CPU then does exactly what it
|
||||||
|
has always done - reads the boot vector and starts where it points. Nothing about the CPU
|
||||||
|
changed to make a machine that starts itself.
|
||||||
|
|
||||||
|
Because it is a copy rather than a mapping, those bytes are ordinary Program Memory once
|
||||||
|
stage one has jumped away. The system may write over them, and a reset puts them back,
|
||||||
|
which is why rebooting has to be a reset rather than a jump - and `SWI SoftReset` is the
|
||||||
|
instruction for it.
|
||||||
|
|
||||||
|
The ROM is generated from the assembly by the makefile rather than kept beside it, because
|
||||||
|
a copy of a program stored next to the program is a copy that goes stale.
|
||||||
|
|
||||||
|
|
||||||
|
**A cycle is one access to memory**, not one instruction. Fetching an opcode is a cycle,
|
||||||
|
fetching each byte after it is another, reading or writing Data Memory is one, every byte a
|
||||||
|
CALL pushes or a RET pops is one, and reaching a device port is one. Nothing overlaps -
|
||||||
|
there is no fetching the next instruction while this one finishes - so the count is simply
|
||||||
|
how many times the machine used the bus.
|
||||||
|
|
||||||
|
That makes the numbers describe something buildable. `RSTA` costs 1 and `SETD` costs 4,
|
||||||
|
because one is a byte and the other is four. `CALL` and `RET` together cost 24 and `RCAL`
|
||||||
|
and `RRET` cost 8, because the first pair moves twenty bytes of Stack and the second moves
|
||||||
|
four. Counting instructions said those were the same, which is not true of any machine
|
||||||
|
anybody could build - and it is the emulator's job to be the thing the hardware is designed
|
||||||
|
against.
|
||||||
|
|
||||||
|
The average SplitBit instruction costs 3.72 cycles, measured over the native assembler
|
||||||
|
assembling a program.
|
||||||
|
|
||||||
|
**The memory controller is charged for what it moves**, on the same terms. Banks are
|
||||||
|
separate memories, and that is what sets the rate: a move between two of them can overlap
|
||||||
|
its read and its write, so it settles at a byte a cycle, while a move within one bank cannot
|
||||||
|
and costs two. A fill has nothing to read and costs one. So a 256 byte block is 257 cycles
|
||||||
|
between banks and 513 within one, against the ten it used to cost - which was the five port
|
||||||
|
writes that set it up and nothing for the quarter of a kilobyte that moved.
|
||||||
|
|
||||||
|
The transfer stalls the program that asked for it. Whether hardware would let the two run at
|
||||||
|
once is left open, the same way pipelining is: the memories are separate, so it plausibly
|
||||||
|
could, and the measurements say it would buy less than it sounds like.
|
||||||
|
|
||||||
|
**The disk can be given a latency** with `--disk-cycles`, and then it really does take that
|
||||||
|
long: it says busy, finishes when the machine has run that far, and a program that does not
|
||||||
|
wait reads the block *before* the one it asked for. That is not an error anywhere - just
|
||||||
|
quietly the wrong bytes - which is why the filesystem now watches the busy bit rather than
|
||||||
|
trusting the answer to be there. Zero is the default and is how the machine has always run.
|
||||||
|
|
||||||
|
The waiting is one small routine, and it is reached with `RCAL` rather than `CALL` because
|
||||||
|
what it hands back is the settled status in A, and an ordinary call would put A back the way
|
||||||
|
it found it.
|
||||||
|
|
||||||
|
**And waiting is not the same kind of cycle as working.** A machine stopped in a `WAIT` is
|
||||||
|
clocked but is not using the bus, so those cycles are counted apart from the rest and the
|
||||||
|
halt line says so when there are any:
|
||||||
|
|
||||||
|
```
|
||||||
|
Execution halted after 1042474 cycles, 119772 of them waiting.
|
||||||
|
```
|
||||||
|
|
||||||
|
Added together they are elapsed time, which is what `--cycles` measures. Told apart they
|
||||||
|
say whether a program was working or waiting - and that distinction is the only thing that
|
||||||
|
separates a machine which slept through a slow disk from one which spun on it. The two take
|
||||||
|
the same wall clock time and print the same characters. When the filesystem's wait was
|
||||||
|
first written, taking the line-clearing out of `WAIT` moved the total by a single cycle,
|
||||||
|
20,100 against 20,099, while the idle half halved.
|
||||||
|
|
||||||
|
Whether real hardware would overlap a fetch with the end of the previous instruction is
|
||||||
|
left open, and deliberately: this is the conservative model, and pipelining is a decision
|
||||||
|
to make while drawing the hardware rather than one to inherit from an emulator.
|
||||||
|
|
||||||
If the CPU reads a byte that is not an instruction, it goes to the fault handler the program installed. If it installed none, it raises the Fault Flag and halts, and the emulator reports the byte and the address it was found at and exits with a non zero status. The same happens if a program or a device asks for a handler that was never installed.
|
If the CPU reads a byte that is not an instruction, it goes to the fault handler the program installed. If it installed none, it raises the Fault Flag and halts, and the emulator reports the byte and the address it was found at and exits with a non zero status. The same happens if a program or a device asks for a handler that was never installed.
|
||||||
|
|
||||||
## Assembling: Assembler
|
## Assembling: Assembler
|
||||||
@@ -105,10 +186,79 @@ If the CPU reads a byte that is not an instruction, it goes to the fault handler
|
|||||||
| `-o <file>` | Write the output to this path. |
|
| `-o <file>` | Write the output to this path. |
|
||||||
| `-I <dir>` | Look in this directory for included files. May be given more than once. |
|
| `-I <dir>` | Look in this directory for included files. May be given more than once. |
|
||||||
| `-M <file>` | Write out which source files the output depends on, as a make rule. |
|
| `-M <file>` | Write out which source files the output depends on, as a make rule. |
|
||||||
|
| `-S <file>` | Write every label and the address it was given, in address order. |
|
||||||
| `-h`, `--help` | Show help and usage information. |
|
| `-h`, `--help` | Show help and usage information. |
|
||||||
|
|
||||||
|
`-S` is the only thing that knows what a program's addresses are called. A program on the
|
||||||
|
disk is bytes; the machine's own monitor can disassemble it but has no idea what any of it
|
||||||
|
is named. So a count of which addresses get called says a great deal and names nothing, and
|
||||||
|
this is what turns such a count into a list of routine names.
|
||||||
|
|
||||||
Without `-o` the output takes the source file's name, in the directory you called the assembler from, with the extension the format asks for: `.bin` for a boot image and `.sbx` for a loadable program. Included files are looked for beside the file that includes them, and then along the directories given with `-I`.
|
Without `-o` the output takes the source file's name, in the directory you called the assembler from, with the extension the format asks for: `.bin` for a boot image and `.sbx` for a loadable program. Included files are looked for beside the file that includes them, and then along the directories given with `-I`.
|
||||||
|
|
||||||
|
## Checking Assembly: SplitLint
|
||||||
|
|
||||||
|
```
|
||||||
|
./SplitLint [--fatal-warnings] [--machine] <sourcefile> [sourcefile ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Every warning names the rule that produced it**, in brackets the way a compiler names a
|
||||||
|
flag, and `--machine` prints one tab-separated line per warning and nothing else - file,
|
||||||
|
line, rule, message, help - so that nothing downstream has to read prose. A run that finds
|
||||||
|
nothing says so rather than exiting silently, because a tool that says nothing has not told
|
||||||
|
you it found nothing; it has told you nothing at all, and from outside those look the same:
|
||||||
|
|
||||||
|
```
|
||||||
|
No style warnings: 121 files checked against 12 rules.
|
||||||
|
```
|
||||||
|
|
||||||
|
SplitLint reports valid assembly that has a shorter direct expression, beginning with
|
||||||
|
zero loads that can use `RSTA` or `RSTB`, Q-to-register transfers that need not pass
|
||||||
|
through the stack, self-push/pop pairs, register assignments overwritten by the next
|
||||||
|
instruction, instructions with no ordinary fallthrough path, and one-byte Data Pointer
|
||||||
|
changes that can use `INCD` or `DECD`, including direct branches whose target is already
|
||||||
|
the next labeled address. It also tracks symbolic Data Pointer bases and known offsets to
|
||||||
|
find a `SETD` that reloads an address the pointer already holds. Device input and stack
|
||||||
|
pops are excluded from dead-assignment checks because consuming their input is itself an effect.
|
||||||
|
|
||||||
|
**Nothing it knows survives a call.** `CALL` really does restore A, B and Data Pointers 0
|
||||||
|
to 2, so a pointer set before one is genuinely still set after it - and saying so produced
|
||||||
|
advice that was correct today and unsafe to take. 122 of the 178 redundant `SETD`s it first
|
||||||
|
found were redundant *only* because of that restore, and removing them would have become a
|
||||||
|
wrong-pointer bug the moment the callee was reached with `RCAL` instead, which is what
|
||||||
|
`RCAL` was added to this machine for. Worse, the linter would have gone quiet rather than
|
||||||
|
complained, because it forgets everything across an `RCAL`. Fifty four recommendations that
|
||||||
|
stay true are worth more than a hundred and seventy eight that are conditional on a change
|
||||||
|
the project intends to make.
|
||||||
|
|
||||||
|
SplitLint's control-flow knowledge is otherwise deliberately local: labels begin reachable regions, while an unparsed
|
||||||
|
directive or data item ends the current claim. It does not yet expand includes or build a
|
||||||
|
complete control-flow graph. Warnings normally leave a successful exit status, while
|
||||||
|
`--fatal-warnings` makes any warning fail the command for use in automated checks.
|
||||||
|
|
||||||
|
**A line whose comment says `splitlint: <reason>` is not reported on, and the reason is
|
||||||
|
required.** A suppression with no explanation is a way to make a tool quiet rather than a
|
||||||
|
way to say something, and a bare marker is refused rather than honoured. The corpus has
|
||||||
|
three of them, all in test programs: `branchTest.asm` exists to check that a branch whose
|
||||||
|
carry is known behaves correctly, so a diagnostic saying the outcome is known is exactly
|
||||||
|
right and exactly unwanted. `splitlint[rule]: <reason>` silences one rule and leaves the
|
||||||
|
line honest about the others.
|
||||||
|
|
||||||
|
Suppressions are counted and reported at the end of a run, and **a marker that no longer
|
||||||
|
silences anything is itself reported** - an exception that outlived its reason is the thing
|
||||||
|
the required reason was meant to prevent.
|
||||||
|
|
||||||
|
**The corpus is held to a baseline.** Sixty one warnings are left in it on purpose, and
|
||||||
|
`Tests/lint-baseline.txt` records how many of each rule each file is expected to produce,
|
||||||
|
so a sixty second fails `make test` while the sixty one stay quiet. It counts rather than
|
||||||
|
recording line numbers, because recording lines would churn the whole file whenever
|
||||||
|
anything was inserted above a warning. `./Tests/lint.sh --bless` records it again once
|
||||||
|
warnings have been deliberately fixed or deliberately accepted.
|
||||||
|
|
||||||
|
The same local model tracks whether carry is known set or clear. It reports a redundant
|
||||||
|
`CCF`, a `BRC` or `BNC` whose outcome is already determined, and computes carry through
|
||||||
|
increment, decrement, addition, and subtraction when their inputs are known.
|
||||||
|
|
||||||
## Managing Disks: SplitDisk
|
## Managing Disks: SplitDisk
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -117,16 +267,216 @@ Without `-o` the output takes the source file's name, in the directory you calle
|
|||||||
|
|
||||||
| Command | What it does |
|
| Command | What it does |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `format <image> [blocks] [dirblocks]` | Lay down a fresh filesystem. 512 blocks and 8 of directory by default, which is 128K and room for 64 files. |
|
| `boot <image> <file> [slot]` | Write a file into a boot slot, padding the rest of it with zeroes. Slot 0 unless told otherwise. |
|
||||||
| `list <image>` | Show what is on the disk. |
|
| `bootstate <image> [0\|1\|2]` | Show how the last start went, or set it. Setting it to 0 is how a disk that fell back is told to try again. |
|
||||||
| `put <image> <file> [name]` | Put a host file onto it. Without a name it uses the file's own, which is often longer than the 22 characters a name may be. |
|
| `bootslot <image> <slot>` | Choose which slot the machine starts from. One byte, on its own, so writing a slot and committing to it stay separate decisions. |
|
||||||
| `get <image> <name> [file]` | Take one off it. |
|
| `format <image> [blocks] [dirblocks] [bootblocks]` | Lay down a fresh filesystem. 512 blocks and 8 of directory by default, which is 128K and room for 64 entries. A fourth number reserves a boot area of two slots that size. |
|
||||||
| `delete <image> <name>` | Remove one. |
|
| `list <image> [path]` | Show the whole disk, or one directory of it. |
|
||||||
|
| `put <image> <file> [path]` | Put a host file onto it. Without a path it uses the file's own name, which is often longer than the 22 characters a name may be. |
|
||||||
|
| `get <image> <path> [file]` | Take one off it. |
|
||||||
|
| `delete <image> <path>` | Remove a file. |
|
||||||
|
| `mkdir <image> <path>` | Make a directory. |
|
||||||
|
| `rmdir <image> <path>` | Remove an empty one. |
|
||||||
|
|
||||||
|
### The Boot Area:
|
||||||
|
|
||||||
|
Blocks between the superblock and the directory, which the filesystem never allocates and
|
||||||
|
never sees. **Nothing was added to reserve them.** Both implementations work out the first
|
||||||
|
usable block as `directoryStart + directoryBlocks`, and `directoryStart` has always been a
|
||||||
|
field in the superblock rather than a constant, so moving the directory up reserves
|
||||||
|
everything below it by arithmetic that was already there. Neither allocator changed.
|
||||||
|
|
||||||
|
A disk made before any of this has `directoryStart` of 1 and a boot area of zero, which
|
||||||
|
reads as *not bootable* - true, and the same shape as the version two parent field, where
|
||||||
|
the value an older disk already held turned out to be the right answer.
|
||||||
|
|
||||||
|
**There are always two slots**, because a boot slot is raw blocks. A file being rewritten
|
||||||
|
is protected by writing a temporary and renaming it, and there is no name here to rename -
|
||||||
|
so a machine interrupted while updating its only boot slot would not boot at all, which is
|
||||||
|
the one failure on this disk with no way back. Writing the slot that is *not* live and then
|
||||||
|
moving one byte in the superblock turns that into a machine that boots what it had before.
|
||||||
|
|
||||||
|
### Starting Again:
|
||||||
|
|
||||||
|
Writing `1` to port `0x13` asks the machine to start over, and `Reboot` is the program that
|
||||||
|
does it - forty five bytes, most of them the word it prints.
|
||||||
|
|
||||||
|
**A port rather than a service**, because a reset has to work when the system does not.
|
||||||
|
Something that could only be asked for through `SWI` would be unavailable in exactly the
|
||||||
|
case that wants it most, and a program that owns the whole machine has no system to ask.
|
||||||
|
|
||||||
|
**What a reset repeats is how the machine started.** Named an image, the emulator places it
|
||||||
|
again; named none, the ROM is shadowed again and reads the disk for the rest. Anything else
|
||||||
|
would mean a reset changed what the machine *is*, which is the one thing a reset must not
|
||||||
|
do. It is taken between instructions, because a device cannot restart the machine from
|
||||||
|
inside the instruction that asked.
|
||||||
|
|
||||||
|
The disk is not unplugged and keeps everything written to it. That is what warm means: the
|
||||||
|
machine starts again, the world it starts into does not. **The vector table is cleared**,
|
||||||
|
which is the one deliberate departure from leaving memory alone - a vector points into
|
||||||
|
whatever installed it, and after a reset that program is not running, so a handler left
|
||||||
|
behind would aim an interrupt at an address belonging to something gone. It is the argument
|
||||||
|
CosmOS already makes when it takes a program's vectors back at exit.
|
||||||
|
|
||||||
|
### Starting Something Else Just This Once:
|
||||||
|
|
||||||
|
A program that owns the whole machine has nowhere to run. It cannot be started from the
|
||||||
|
shell, because starting it means there is no shell; and pointing `boot.cfg` at it means a
|
||||||
|
machine that keeps starting it, which is a poor place to find a mistake.
|
||||||
|
|
||||||
|
```
|
||||||
|
> Once /System/Boot/mine.bin
|
||||||
|
next start: /System/Boot/mine.bin, once
|
||||||
|
```
|
||||||
|
|
||||||
|
That writes `/System/Boot/once.cfg`, in the same format as `boot.cfg` and read with the same
|
||||||
|
routines, because a second format for one setting would be a second format. The loader reads
|
||||||
|
it before `boot.cfg` and **deletes it before it jumps** - the only moment there is, since
|
||||||
|
after the jump the loader does not exist.
|
||||||
|
|
||||||
|
**Consumed by being read, not by working.** A one shot that hangs cannot hang twice: the
|
||||||
|
request is gone before the image ran, so the next start reads `boot.cfg` like any other.
|
||||||
|
|
||||||
|
And **the boot state is not touched** by a one shot, which the first version got wrong. A
|
||||||
|
program with the whole machine has no filesystem to clear a mark with, and is doing nothing
|
||||||
|
wrong by not having one - so marking it reported every successful bare metal boot as a start
|
||||||
|
that never arrived.
|
||||||
|
|
||||||
|
Which closes the loop on the machine itself: write a bare metal program in `Edit`, assemble
|
||||||
|
it with `Asm`, ask for it with `Once`, restart, watch it run, and the system comes back
|
||||||
|
without being asked.
|
||||||
|
|
||||||
|
### Knowing Whether The Last Start Arrived:
|
||||||
|
|
||||||
|
The loader marks the disk before it hands over, and the system clears the mark when it
|
||||||
|
reaches its prompt. **A system that crashes on the way there leaves the mark**, and the
|
||||||
|
loader finding it still set next time is how a machine that will not start says so to the
|
||||||
|
only thing in a position to do anything about it.
|
||||||
|
|
||||||
|
| State | Means |
|
||||||
|
| -- | -- |
|
||||||
|
| 0 | Settled. The last start arrived, so start what the configuration says. |
|
||||||
|
| 1 | Trying. The loader handed over and nothing came back to say it got there. |
|
||||||
|
| 2 | Fell back. A try failed and the fallback was used, and will be until this is settled. |
|
||||||
|
|
||||||
|
Three starts of a machine whose configuration names something broken:
|
||||||
|
|
||||||
|
```
|
||||||
|
stage two
|
||||||
|
a system that never reaches a prompt <- marks the disk, dies
|
||||||
|
|
||||||
|
stage two
|
||||||
|
the last start did not arrive <- finds the mark, uses the fallback
|
||||||
|
CosmOS
|
||||||
|
this is the fallback: what boot.cfg asks for did not start
|
||||||
|
|
||||||
|
stage two
|
||||||
|
still on the fallback: settle it to try again <- does NOT retry
|
||||||
|
```
|
||||||
|
|
||||||
|
That third one is the part worth having. A system known not to start is not tried every
|
||||||
|
other boot for ever; it waits to be told the situation has changed.
|
||||||
|
|
||||||
|
**Reaching the prompt is a deliberate choice of threshold.** It is not a claim that the
|
||||||
|
system works - a shell can be reached by something broken in every other way. It is the
|
||||||
|
point where somebody can type, which is exactly what the fallback exists to give back:
|
||||||
|
anything wrong past there can be fixed from the prompt, and nothing wrong before it can be
|
||||||
|
fixed at all.
|
||||||
|
|
||||||
|
`bootBlocks` and `directoryStart` describe the same fact from two sides, so a disk where
|
||||||
|
they disagree is refused rather than guessed at.
|
||||||
|
|
||||||
|
`Programs/Boot/stage1.asm` is what starts a machine from one, and is written to end up in
|
||||||
|
ROM: **330 bytes**, and everything it knows is a thing that will be true forever. Which
|
||||||
|
port the disk is on, that a SplitBit disk begins with its own name, and where two numbers
|
||||||
|
sit in that first block. It does not know what a file is, what a directory is, or that SBFS
|
||||||
|
has versions - all of that lives in the boot area, on the disk, where it can be replaced.
|
||||||
|
It reads the live slot into Program Memory, jumps to the first byte, and prints one
|
||||||
|
character and stops if there is nothing to start.
|
||||||
|
|
||||||
|
`Programs/Boot/stage2.asm` is what sits in the slot. It mounts the filesystem, reads
|
||||||
|
`/System/Boot/boot.cfg` to find out what to start, and takes the image apart: code into Program Memory, data into Data
|
||||||
|
Memory, and each vector written through the controller, which is the only thing that can
|
||||||
|
write Program Memory at all. The entry point is noticed on the boot vector's way past,
|
||||||
|
because Program Memory cannot be read back to look it up afterwards.
|
||||||
|
|
||||||
|
**What it loads is an ordinary boot image** - the same SPBT file the emulator has always
|
||||||
|
been handed directly. That is the whole trick: a second stage that loads the machine's
|
||||||
|
normal image format is not a boot-specific mechanism, so **bare metal SplitBit stops being
|
||||||
|
a special case.** A program that wants no operating system under it is just an image,
|
||||||
|
written under CosmOS like any other, and startable because it is a file.
|
||||||
|
|
||||||
|
### Configuration:
|
||||||
|
|
||||||
|
One setting to a line: a key, a space, and the rest of the line is the value. A semicolon
|
||||||
|
starts a comment and a blank line is nothing.
|
||||||
|
|
||||||
|
```
|
||||||
|
; /System/Boot/boot.cfg
|
||||||
|
system /System/Boot/cosmos.bin
|
||||||
|
fallback /System/Boot/cosmos-previous.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
That format was noticed rather than designed. `textSplit` already cuts the first word off a
|
||||||
|
line and leaves the rest; `textSame` already compares two strings and insists they end
|
||||||
|
together, so `system` and `systemd` are different words. Reading a setting is those two
|
||||||
|
routines and a loop - and it is the shape the shell already reads, so **a configuration
|
||||||
|
line is a command line the machine reads instead of a person typing one.**
|
||||||
|
|
||||||
|
**Configuration is advice.** A missing file, a missing key, a value that makes no sense, a
|
||||||
|
line too long to read: all of them mean *use the default*, and none of them is a failure. A
|
||||||
|
program that cannot run without its configuration has turned its configuration into a
|
||||||
|
single point of failure, and for the thing that starts the machine that would mean a
|
||||||
|
mistyped file is a machine that will not start.
|
||||||
|
|
||||||
|
**But quiet is not silent.** A setting somebody meant, which did not take effect, says so:
|
||||||
|
|
||||||
|
```
|
||||||
|
a setting nothing asked for: fallbcak
|
||||||
|
a setting with no value: system
|
||||||
|
a line too long to read: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
So `config.asm` has two routines rather than one. `cfgGet` reads and never says anything -
|
||||||
|
reading three settings should not report the same bad line three times. `cfgCheck` reads
|
||||||
|
the file once and reports what it did not understand, and is given the caller's list of
|
||||||
|
keys, because whether a key means anything is the only part of this the reader cannot
|
||||||
|
judge for itself.
|
||||||
|
|
||||||
|
Keys are matched exactly and case sensitively, the first line that matches wins, and
|
||||||
|
unknown keys are ignored rather than refused - so a file written by a newer system loses
|
||||||
|
the settings an older one never had and keeps the ones it did. A line is at most 128 bytes
|
||||||
|
and a key at most 22, which is what a name is everywhere else on this machine.
|
||||||
|
|
||||||
|
Deliberately absent: sections, nesting, types, includes, substitution, conditions. Each is
|
||||||
|
a step from reading a file toward running one, and the boot loader is the worst place on
|
||||||
|
this machine to put an interpreter. The test to apply when the pressure arrives is: **if
|
||||||
|
this file cannot be read, can the machine still start?**
|
||||||
|
|
||||||
|
Stage two arranges its own Data Segment. Stage one places Program Memory and nothing else,
|
||||||
|
since knowing where a payload's data ended would mean knowing a format, so the image is
|
||||||
|
written into the slot as code followed by data and stage two's first act is to blit its
|
||||||
|
data down from just past its own code.
|
||||||
|
|
||||||
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. It is a convenience rather than a necessity: SplitBit writes its own filesystem, and now assembles its own programs, so a disk can be filled without leaving the machine.
|
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. It is a convenience rather than a necessity: SplitBit writes its own filesystem, and now assembles its own programs, so a disk can be filled without leaving the machine.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
A path is names with `/` between them, always from the root, since a command line tool has nowhere to keep a working directory between one run and the next. `.` and `..` mean what they usually do, and `..` from the root is the root.
|
||||||
|
|
||||||
|
A disk has two ceilings and it is usually the less obvious one that bites: blocks, and **entries**. Every file and every directory costs one entry, and `list` says how many of them are gone as well as how many blocks are. On a disk of small files the entries run out long before the space does, which is a matter of how the disk was formatted rather than a limit of the format - `dirblocks` is carried per disk, and each one is 256 bytes and holds eight entries.
|
||||||
|
|
||||||
|
### Two Versions:
|
||||||
|
|
||||||
|
| Version | What it means |
|
||||||
|
| --- | --- |
|
||||||
|
| 1 | Flat. Every file is in the root, because there is nowhere else. |
|
||||||
|
| 2 | Directories. Each entry says which directory it is in. |
|
||||||
|
|
||||||
|
**A version one disk is already a valid version two disk.** The parent is stored as an entry index *plus one*, so the zeroes a version one disk has in those bytes read as "in the root" - which is exactly where all of its files are. There is nothing to convert.
|
||||||
|
|
||||||
|
A disk is at the lowest version that describes what is on it, so `format` makes a version one disk and `mkdir` is what raises it. That is deliberate: a disk stays readable by anything that has never heard of a directory right up until it actually has one. Compatibility runs one way, which is the ordinary shape of it - version one code reading a version two disk would list directories as strange empty files.
|
||||||
|
|
||||||
## Building Programs With Make:
|
## Building Programs With Make:
|
||||||
|
|
||||||
The assembler is built to work with make. `-o` puts the output where the build system wants it, and `-M` writes out which libraries went into it, so that editing a library reassembles everything that includes it.
|
The assembler is built to work with make. `-o` puts the output where the build system wants it, and `-M` writes out which libraries went into it, so that editing a library reassembles everything that includes it.
|
||||||
@@ -170,17 +520,19 @@ The disk images tests read from are built first by `Tests/makedisks.sh`, using S
|
|||||||
test that reads one is therefore checked against a filesystem written by different code from
|
test that reads one is therefore checked against a filesystem written by different code from
|
||||||
the same written specification, rather than against itself.
|
the same written specification, rather than against itself.
|
||||||
|
|
||||||
`Tests/run.sh` drives that comparison. Four more scripts run alongside it, and each exists
|
`Tests/run.sh` drives that comparison. Six more scripts run alongside it, and each exists
|
||||||
because a recorded file cannot answer its question:
|
because a recorded file cannot answer its question:
|
||||||
|
|
||||||
- **`Tests/disk.sh`** checks the disk tool on its own: files of every awkward size onto an image and off again, and the things the format says cannot happen refused rather than half done.
|
- **`Tests/disk.sh`** checks the disk tool on its own: files of every awkward size onto an image and off again, and the things the format says cannot happen refused rather than half done.
|
||||||
- **`Tests/terminal.sh`** checks what a recorded file cannot see. 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 key mode only touches a terminal 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 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.
|
- **`Tests/terminal.sh`** checks what a recorded file cannot see. 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 key mode only touches a terminal 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 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. It also asks the one question about cycles that a recorded file cannot, since the count is stripped from every one: whether a program on a slow disk slept through the wait or spun on it. Both print the same characters and take the same elapsed time, and only the split between idle and bus cycles tells them apart.
|
||||||
- **`Tests/native.sh`** checks the assembler that runs on SplitBit against the one that runs on the host, byte for byte, on a boot image and four loadable programs, and then on CosmOS and on itself, and then on the CosmOS that CosmOS built.
|
- **`Tests/native.sh`** checks the assembler that runs on SplitBit against the one that runs on the host, byte for byte, on a boot image and four loadable programs, and then on CosmOS and on itself, and then on the CosmOS that CosmOS built.
|
||||||
- **`Tests/docs.sh`** 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 service the system implements is described and every service described is implemented, that every routine the manuals promise exists, and that the worked examples still assemble to the bytes printed beside them.
|
- **`Tests/agree.sh`** checks the two implementations of SBFS against each other rather than each against itself, by building the same disk with SplitDisk and with CosmOS and comparing the images byte for byte. Every field one of them writes and the other only reads is checked there and nowhere else.
|
||||||
|
- **`Tests/lint.sh`** checks SplitLint against a fixture written so that every line of it trips exactly one rule. It compares which warning came out and at which line rather than how many came out in total: a count stays right while the thing behind it goes wrong, and breaking one rule's message left the total untouched at twenty three.
|
||||||
|
- **`Tests/docs.sh`** 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 service the system implements is described and every service described is implemented, that every routine the manuals promise exists, that CosmOS still fits in the half of the machine its memory map gives it, and that the worked examples still assemble to the bytes printed beside them.
|
||||||
|
|
||||||
A cycle count is deliberately **not** part of a recorded result. The last line of the emulator's output has the number taken out 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 have arrived in a crowd of meaningless ones. Anything that wants to measure cycles should say so in a test of its own.
|
A cycle count is deliberately **not** part of a recorded result. The last line of the emulator's output has the number taken out 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 have arrived in a crowd of meaningless ones. Anything that wants to measure cycles should say so in a test of its own.
|
||||||
|
|
||||||
To rebuild all three tools with the address and undefined behaviour sanitizers and run the suite under them:
|
To rebuild all four tools with the address and undefined behaviour sanitizers and run the suite under them:
|
||||||
|
|
||||||
```
|
```
|
||||||
make sanitize
|
make sanitize
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ void printUsage(const char *programName) {
|
|||||||
printf(" -o <file> Write the output to this path instead of alongside the source.\n");
|
printf(" -o <file> Write the output to this path instead of alongside the source.\n");
|
||||||
printf(" -I <dir> Look in this directory for included files. May be given more than once.\n");
|
printf(" -I <dir> Look in this directory for included files. May be given more than once.\n");
|
||||||
printf(" -M <file> Write the source files this output depends on, as a make rule.\n");
|
printf(" -M <file> Write the source files this output depends on, as a make rule.\n");
|
||||||
|
printf(" -S <file> Write every label and the address it was given, in address order.\n");
|
||||||
printf(" -h, --help Display this help message.\n");
|
printf(" -h, --help Display this help message.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,15 +172,17 @@ int main(int argc, char *argv[]) {
|
|||||||
{"output", required_argument, 0, 'o'},
|
{"output", required_argument, 0, 'o'},
|
||||||
{"include", required_argument, 0, 'I'},
|
{"include", required_argument, 0, 'I'},
|
||||||
{"depend", required_argument, 0, 'M'},
|
{"depend", required_argument, 0, 'M'},
|
||||||
|
{"symbols", required_argument, 0, 'S'},
|
||||||
{"help", no_argument, 0, 'h'},
|
{"help", no_argument, 0, 'h'},
|
||||||
{0, 0, 0, 0 }
|
{0, 0, 0, 0 }
|
||||||
};
|
};
|
||||||
char *outputFileName = NULL;
|
char *outputFileName = NULL;
|
||||||
char *dependencyFileName = NULL;
|
char *dependencyFileName = NULL;
|
||||||
|
char *symbolFileName = NULL;
|
||||||
int option_index = 0;
|
int option_index = 0;
|
||||||
int opt;
|
int opt;
|
||||||
|
|
||||||
while ((opt = getopt_long(argc, argv, "o:I:M:h", long_options, &option_index)) != -1) {
|
while ((opt = getopt_long(argc, argv, "o:I:M:S:h", long_options, &option_index)) != -1) {
|
||||||
switch (opt) {
|
switch (opt) {
|
||||||
case 'o':
|
case 'o':
|
||||||
outputFileName = strdup(optarg);
|
outputFileName = strdup(optarg);
|
||||||
@@ -190,6 +193,9 @@ int main(int argc, char *argv[]) {
|
|||||||
case 'M':
|
case 'M':
|
||||||
dependencyFileName = strdup(optarg);
|
dependencyFileName = strdup(optarg);
|
||||||
break;
|
break;
|
||||||
|
case 'S':
|
||||||
|
symbolFileName = strdup(optarg);
|
||||||
|
break;
|
||||||
case 'h':
|
case 'h':
|
||||||
printUsage(argv[0]);
|
printUsage(argv[0]);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -249,6 +255,11 @@ int main(int argc, char *argv[]) {
|
|||||||
writeDependencyFile(dependencyFileName, outputFileName);
|
writeDependencyFile(dependencyFileName, outputFileName);
|
||||||
free(dependencyFileName);
|
free(dependencyFileName);
|
||||||
}
|
}
|
||||||
|
// Before the cleanup, which is what frees the label table this reads.
|
||||||
|
if (symbolFileName) {
|
||||||
|
writeSymbolFile(symbolFileName);
|
||||||
|
free(symbolFileName);
|
||||||
|
}
|
||||||
assemblerCleanup(intermediateArray, index, outputFileName);
|
assemblerCleanup(intermediateArray, index, outputFileName);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,11 +122,11 @@ int checkIfInstruction(intermediateElement *currentElement) {
|
|||||||
dot = strchr(dot, '.');
|
dot = strchr(dot, '.');
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t opcode = getOpcode(token);
|
int found = getOpcode(token);
|
||||||
if (opcode == 0xFE) {
|
if (found == NOT_AN_OPCODE) {
|
||||||
// It's not an instruction.
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
uint8_t opcode = (uint8_t)found;
|
||||||
|
|
||||||
int selectorsWanted = dataPointerOperands(opcode);
|
int selectorsWanted = dataPointerOperands(opcode);
|
||||||
if (selectorsGiven > selectorsWanted) {
|
if (selectorsGiven > selectorsWanted) {
|
||||||
|
|||||||
+60
-26
@@ -12,34 +12,54 @@ typedef struct {
|
|||||||
} Instruction;
|
} Instruction;
|
||||||
|
|
||||||
Instruction instruction_set[] = {
|
Instruction instruction_set[] = {
|
||||||
|
// ---- Nothing at all in 0x00 to 0x0F ----
|
||||||
|
//
|
||||||
|
// Kept empty on purpose. Program Memory that has never been written, or a load that
|
||||||
|
// stopped part way and left zeroes in its tail, used to read as a long run of
|
||||||
|
// additions and then do something unpredictable a long way from the cause. An
|
||||||
|
// unassigned byte faults where it is met, with the address, which is the difference
|
||||||
|
// between a diagnosis and a search.
|
||||||
|
//
|
||||||
// Arithmetic and Logic Operations:
|
// Arithmetic and Logic Operations:
|
||||||
{0x00, "ADD"},
|
{0x10, "ADD"},
|
||||||
{0x01, "SUB"},
|
{0x11, "SUB"},
|
||||||
{0x02, "AND"},
|
{0x12, "AND"},
|
||||||
{0x03, "OR"},
|
{0x13, "OR"},
|
||||||
{0x04, "XOR"},
|
{0x14, "XOR"},
|
||||||
{0x05, "NOTA"},
|
{0x15, "NOTA"},
|
||||||
{0x06, "NOTB"},
|
{0x16, "NOTB"},
|
||||||
{0x07, "SHL"},
|
{0x17, "SHL"},
|
||||||
{0x08, "SHR"},
|
{0x18, "SHR"},
|
||||||
// Branch Operations:
|
// Branch Operations:
|
||||||
{0x10, "BRI"},
|
{0x60, "BRI"},
|
||||||
{0x11, "BRQ"},
|
{0x61, "BRQ"},
|
||||||
{0x12, "BRA"},
|
{0x62, "BRA"},
|
||||||
{0x13, "BRB"},
|
{0x63, "BRB"},
|
||||||
{0x14, "BRC"},
|
{0x64, "BRC"},
|
||||||
{0x15, "BRD"},
|
{0x65, "BRD"},
|
||||||
// The same four conditions the other way round. A quarter of the conditional
|
// The same four conditions the other way round. A quarter of the conditional
|
||||||
// branches in the corpus were a branch over an unconditional one before these
|
// branches in the corpus were a branch over an unconditional one before these
|
||||||
// existed, each of them needing a label invented only to be jumped past.
|
// existed, each of them needing a label invented only to be jumped past.
|
||||||
{0x1A, "BNQ"},
|
{0x66, "BNQ"},
|
||||||
{0x1B, "BNA"},
|
{0x67, "BNA"},
|
||||||
{0x1C, "BNB"},
|
{0x68, "BNB"},
|
||||||
{0x1D, "BNC"},
|
{0x69, "BNC"},
|
||||||
{0x17, "CALL"},
|
// Subroutine Operations:
|
||||||
{0x18, "SWI"},
|
//
|
||||||
{0x19, "RETI"},
|
// A block of their own since the branches and these outgrew one nibble between them.
|
||||||
{0x1F, "RET"},
|
// Each raw form sits immediately below the ordinary one it cannot be mixed with: RCAL
|
||||||
|
// under CALL, RRET under RET, because the frames differ and returning through the
|
||||||
|
// wrong one takes the machine somewhere nobody named.
|
||||||
|
{0x70, "RCAL"},
|
||||||
|
{0x71, "CALL"},
|
||||||
|
{0x72, "SWI"},
|
||||||
|
{0x73, "RETI"},
|
||||||
|
{0x74, "RRET"},
|
||||||
|
{0x75, "RET"},
|
||||||
|
// A handler with an answer. RETI restores everything and is how a hardware handler
|
||||||
|
// says it was never here; this restores what a RET restores, and is how a service
|
||||||
|
// says it has replied. See the note in cpu.c.
|
||||||
|
{0x76, "SRET"},
|
||||||
// Register Operations:
|
// Register Operations:
|
||||||
{0x20, "RSTA"},
|
{0x20, "RSTA"},
|
||||||
{0x21, "RSTB"},
|
{0x21, "RSTB"},
|
||||||
@@ -77,6 +97,10 @@ Instruction instruction_set[] = {
|
|||||||
{0x4B, "STD"},
|
{0x4B, "STD"},
|
||||||
{0x4C, "MVSD"},
|
{0x4C, "MVSD"},
|
||||||
{0x4D, "MVDS"},
|
{0x4D, "MVDS"},
|
||||||
|
{0x4E, "DPUA"},
|
||||||
|
{0x4F, "DPDA"},
|
||||||
|
{0x50, "DPUW"},
|
||||||
|
{0x51, "DPDW"},
|
||||||
// Output Operations:
|
// Output Operations:
|
||||||
{0xD0, "OUTQ"},
|
{0xD0, "OUTQ"},
|
||||||
{0xD1, "OUTA"},
|
{0xD1, "OUTA"},
|
||||||
@@ -86,6 +110,7 @@ Instruction instruction_set[] = {
|
|||||||
{0xE1, "INB"},
|
{0xE1, "INB"},
|
||||||
// Special Operations:
|
// Special Operations:
|
||||||
{0xF0, "NOP"},
|
{0xF0, "NOP"},
|
||||||
|
{0xFE, "WAIT"},
|
||||||
{0xFF, "HALT"}
|
{0xFF, "HALT"}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,7 +133,7 @@ int dataPointerOperands(uint8_t opcode) {
|
|||||||
case 0x4A: // LDD
|
case 0x4A: // LDD
|
||||||
case 0x4B: // STD
|
case 0x4B: // STD
|
||||||
return 2;
|
return 2;
|
||||||
case 0x15: // BRD
|
case 0x65: // BRD
|
||||||
case 0x33: // PSHD
|
case 0x33: // PSHD
|
||||||
case 0x36: // POPD
|
case 0x36: // POPD
|
||||||
case 0x40: // INCD
|
case 0x40: // INCD
|
||||||
@@ -123,19 +148,28 @@ int dataPointerOperands(uint8_t opcode) {
|
|||||||
case 0x49: // DPDN
|
case 0x49: // DPDN
|
||||||
case 0x4C: // MVSD
|
case 0x4C: // MVSD
|
||||||
case 0x4D: // MVDS
|
case 0x4D: // MVDS
|
||||||
|
case 0x4E: // DPUA
|
||||||
|
case 0x4F: // DPDA
|
||||||
|
case 0x50: // DPUW
|
||||||
|
case 0x51: // DPDW
|
||||||
return 1;
|
return 1;
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t getOpcode(char* mnemonic) {
|
int getOpcode(char* mnemonic) {
|
||||||
for (int i = 0; i < num_instructions; i++) {
|
for (int i = 0; i < num_instructions; i++) {
|
||||||
if (strcmp(instruction_set[i].mnemonic, mnemonic) == 0) {
|
if (strcmp(instruction_set[i].mnemonic, mnemonic) == 0) {
|
||||||
return instruction_set[i].opcode;
|
return instruction_set[i].opcode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0xFE; // FE is an unused instruction, we'll use it to indicate an error.
|
// NOT_AN_OPCODE, and it is negative on purpose. This used to answer 0xFE on the
|
||||||
|
// grounds that 0xFE was unused - which was true until WAIT was given that opcode, at
|
||||||
|
// which point the assembler would have read WAIT as a word it did not recognise. A
|
||||||
|
// sentinel picked from the unused half of a range stops being a sentinel the moment
|
||||||
|
// somebody uses the range, so this one is outside the range altogether.
|
||||||
|
return NOT_AN_OPCODE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,11 @@
|
|||||||
|
|
||||||
const char* getMnemonic(uint8_t opcode);
|
const char* getMnemonic(uint8_t opcode);
|
||||||
|
|
||||||
uint8_t getOpcode(char* mnemonic);
|
// The opcode a mnemonic assembles to, or NOT_AN_OPCODE if the word is not one. The
|
||||||
|
// return is an int rather than a byte so that the answer "no" cannot be confused with any
|
||||||
|
// of the 256 answers "yes" - see the note in getOpcode.
|
||||||
|
#define NOT_AN_OPCODE (-1)
|
||||||
|
int getOpcode(char* mnemonic);
|
||||||
|
|
||||||
// How many Data Pointer selector bytes follow the given opcode. Never more than two.
|
// How many Data Pointer selector bytes follow the given opcode. Never more than two.
|
||||||
#define MAX_DATA_POINTER_OPERANDS 2
|
#define MAX_DATA_POINTER_OPERANDS 2
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
|
|||||||
&& (*intermediateArray)[*intermediateIndex].type == LABEL
|
&& (*intermediateArray)[*intermediateIndex].type == LABEL
|
||||||
&& *intermediateIndex > 0
|
&& *intermediateIndex > 0
|
||||||
&& (*intermediateArray)[*intermediateIndex - 1].type == INSTRUCTION
|
&& (*intermediateArray)[*intermediateIndex - 1].type == INSTRUCTION
|
||||||
&& (*intermediateArray)[*intermediateIndex - 1].byteValue == 0x18) {
|
&& (*intermediateArray)[*intermediateIndex - 1].byteValue == 0x72) {
|
||||||
(*intermediateArray)[*intermediateIndex].type = VECTOR_REFERENCE;
|
(*intermediateArray)[*intermediateIndex].type = VECTOR_REFERENCE;
|
||||||
(*intermediateArray)[*intermediateIndex].byteLength = 1;
|
(*intermediateArray)[*intermediateIndex].byteLength = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,48 @@ int debugSecondPass = 0;
|
|||||||
Label labelArray[MAX_LABELS];
|
Label labelArray[MAX_LABELS];
|
||||||
int labelCount = 0;
|
int labelCount = 0;
|
||||||
|
|
||||||
|
// ---- Where everything ended up ----
|
||||||
|
//
|
||||||
|
// Every label and the address it was given, in address order. The assembler knows this and
|
||||||
|
// nothing else does: a program on the disk is bytes, and the machine's own monitor can
|
||||||
|
// disassemble it but has no idea what any of it is called.
|
||||||
|
//
|
||||||
|
// WHAT IT IS FOR is telling where a program spends its time. Counting which addresses get
|
||||||
|
// called says a great deal and names nothing, so the answer arrives as a list of numbers
|
||||||
|
// and somebody has to work out by hand which routine each one is inside. With this, a
|
||||||
|
// tally of call targets becomes a list of routine names.
|
||||||
|
//
|
||||||
|
// Sorted by address rather than by name, because the question asked of it is always "what
|
||||||
|
// is at this address", and a label table is small enough that sorting it is free.
|
||||||
|
static int byAddress(const void *left, const void *right) {
|
||||||
|
const Label *a = left, *b = right;
|
||||||
|
if (a->address != b->address) {
|
||||||
|
return a->address < b->address ? -1 : 1;
|
||||||
|
}
|
||||||
|
return strcmp(a->label, b->label);
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeSymbolFile(const char *path) {
|
||||||
|
FILE *file = fopen(path, "w");
|
||||||
|
if (!file) {
|
||||||
|
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, path);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
Label *sorted = malloc((size_t)labelCount * sizeof(Label));
|
||||||
|
if (!sorted) {
|
||||||
|
fprintf(stderr, RED "Error: Out of memory writing the symbol file.\n" RESET);
|
||||||
|
fclose(file);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
memcpy(sorted, labelArray, (size_t)labelCount * sizeof(Label));
|
||||||
|
qsort(sorted, (size_t)labelCount, sizeof(Label), byAddress);
|
||||||
|
for (int i = 0; i < labelCount; i++) {
|
||||||
|
fprintf(file, "%04X %s\n", sorted[i].address, sorted[i].label);
|
||||||
|
}
|
||||||
|
free(sorted);
|
||||||
|
fclose(file);
|
||||||
|
}
|
||||||
|
|
||||||
void freeLabelList() {
|
void freeLabelList() {
|
||||||
for (int i = 0; i < labelCount; i++) {
|
for (int i = 0; i < labelCount; i++) {
|
||||||
if (labelArray[i].label) {
|
if (labelArray[i].label) {
|
||||||
@@ -39,6 +81,34 @@ void addLabel(char *labelName, uint16_t address, int type, const char *fileName,
|
|||||||
cleanedLabel[len - 1] = '\0'; // Remove the colon
|
cleanedLabel[len - 1] = '\0'; // Remove the colon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A NAME THAT IS ALREADY AN INSTRUCTION IS REFUSED, and the error is here rather
|
||||||
|
// than at the branch that could not find it. Mnemonics are matched with the token
|
||||||
|
// uppercased, so a label called "wait" and the instruction WAIT are the same word
|
||||||
|
// - and when WAIT was added, Keys.asm had been using that label for a year. What
|
||||||
|
// it reported was "Branch without label" at the BRQ, thirty lines from the cause
|
||||||
|
// and naming nothing that had changed.
|
||||||
|
//
|
||||||
|
// This will happen again. Every instruction added takes a word out of the space
|
||||||
|
// of label names, so the check belongs where the name is claimed.
|
||||||
|
{
|
||||||
|
char upper[8];
|
||||||
|
int n = 0;
|
||||||
|
for (; cleanedLabel[n] != '\0' && n < (int)sizeof(upper) - 1; n++) {
|
||||||
|
upper[n] = (char)toupper((unsigned char)cleanedLabel[n]);
|
||||||
|
}
|
||||||
|
upper[n] = '\0';
|
||||||
|
// Only a name short enough to BE a mnemonic can collide with one, and the
|
||||||
|
// longest is four characters. A longer name is truncated by the loop above
|
||||||
|
// and would not match anything, which is the right answer.
|
||||||
|
if (cleanedLabel[n] == '\0' && getOpcode(upper) != NOT_AN_OPCODE) {
|
||||||
|
fprintf(stderr, RED "Error: \"%s\" is an instruction, so it cannot also be"
|
||||||
|
" a label.\n" RESET, cleanedLabel);
|
||||||
|
printf("File: %s at line %d.\n", fileName, lineNumber);
|
||||||
|
free(cleanedLabel);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A name may only be defined once. Without this check a reference quietly
|
// A name may only be defined once. Without this check a reference quietly
|
||||||
// resolves to whichever definition came first, so a typo or a name that two
|
// resolves to whichever definition came first, so a typo or a name that two
|
||||||
// libraries both happen to use is very hard to track down.
|
// libraries both happen to use is very hard to track down.
|
||||||
@@ -442,17 +512,17 @@ static void checkOperands(intermediateElement *intermediateArray, int arraySize,
|
|||||||
const char *problem = NULL;
|
const char *problem = NULL;
|
||||||
|
|
||||||
// Listed rather than matched on the high nibble, because not every instruction in
|
// Listed rather than matched on the high nibble, because not every instruction in
|
||||||
// the branch block takes an address: RET has none, and BRD gets its destination
|
// these two blocks takes an address: RET has none, and BRD gets its destination from
|
||||||
// from a Data Pointer instead of from the program.
|
// a Data Pointer instead of from the program.
|
||||||
if (opcode == 0x10 || opcode == 0x11 || opcode == 0x12 ||
|
if (opcode == 0x60 || opcode == 0x61 || opcode == 0x62 ||
|
||||||
opcode == 0x13 || opcode == 0x14 || opcode == 0x17 ||
|
opcode == 0x63 || opcode == 0x64 || opcode == 0x71 ||
|
||||||
opcode == 0x1A || opcode == 0x1B || opcode == 0x1C || opcode == 0x1D) {
|
opcode == 0x66 || opcode == 0x67 || opcode == 0x68 || opcode == 0x69) {
|
||||||
// Branches and CALL take a two byte address, which only a label can supply.
|
// Branches and CALL take a two byte address, which only a label can supply.
|
||||||
if (nextType != LABEL) problem = "Branch without label.";
|
if (nextType != LABEL) problem = "Branch without label.";
|
||||||
} else if ((opcode & 0xF0) == 0xD0 || (opcode & 0xF0) == 0xE0) {
|
} else if ((opcode & 0xF0) == 0xD0 || (opcode & 0xF0) == 0xE0) {
|
||||||
// The instruction is either an input or output and must be followed by a value.
|
// The instruction is either an input or output and must be followed by a value.
|
||||||
if (nextType != VALUE) problem = "I/O without destination port.";
|
if (nextType != VALUE) problem = "I/O without destination port.";
|
||||||
} else if (opcode == 0x18) {
|
} else if (opcode == 0x72) {
|
||||||
// SWI names a vector, either by the name it was given in the Vector Segment or,
|
// SWI names a vector, either by the name it was given in the Vector Segment or,
|
||||||
// rarely, as a literal number. Without one it swallows whatever follows it and
|
// rarely, as a literal number. Without one it swallows whatever follows it and
|
||||||
// every address after that shifts.
|
// every address after that shifts.
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ typedef struct {
|
|||||||
int type;
|
int type;
|
||||||
} Label;
|
} Label;
|
||||||
|
|
||||||
|
// Every label and the address it was given, in address order, so that a tally of
|
||||||
|
// addresses can be turned back into a list of routine names.
|
||||||
|
void writeSymbolFile(const char *path);
|
||||||
|
|
||||||
// One line of the Vector Segment, once it has been worked out.
|
// One line of the Vector Segment, once it has been worked out.
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char* name; // What it was called, or NULL for a device, which is named by its port.
|
char* name; // What it was called, or NULL for a device, which is named by its port.
|
||||||
|
|||||||
+766
-52
@@ -51,10 +51,14 @@ static int writeBlock(FILE *image, uint16_t block, const uint8_t *from) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
uint8_t version;
|
||||||
uint16_t diskBlocks;
|
uint16_t diskBlocks;
|
||||||
uint16_t directoryStart;
|
uint16_t directoryStart;
|
||||||
uint16_t directoryBlocks;
|
uint16_t directoryBlocks;
|
||||||
uint16_t freeBlocks;
|
uint16_t freeBlocks;
|
||||||
|
uint16_t bootBlocks; // Per slot. Zero on a disk that cannot be booted.
|
||||||
|
uint8_t bootSlot; // Which of the two is live.
|
||||||
|
uint8_t bootState; // How the last start went. See sbfs.h.
|
||||||
} Superblock;
|
} Superblock;
|
||||||
|
|
||||||
// Reads block 0 and checks it really is one of ours. Without the magic a blank image and
|
// Reads block 0 and checks it really is one of ours. Without the magic a blank image and
|
||||||
@@ -68,15 +72,49 @@ static int readSuperblock(FILE *image, Superblock *super) {
|
|||||||
fprintf(stderr, "Error: That is not a SplitBit disk. Format it first.\n");
|
fprintf(stderr, "Error: That is not a SplitBit disk. Format it first.\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (block[SBFS_SUPER_VERSION] != SBFS_VERSION) {
|
// Either version is read. A version one disk has zeroes in the bytes version two
|
||||||
fprintf(stderr, "Error: That disk is version %u, and this understands version %u.\n",
|
// uses for a parent, and zero is the root, so every file on it reads as a file in the
|
||||||
block[SBFS_SUPER_VERSION], SBFS_VERSION);
|
// root - which is exactly where it is. Nothing is converted here.
|
||||||
|
if (block[SBFS_SUPER_VERSION] != SBFS_VERSION_FLAT
|
||||||
|
&& block[SBFS_SUPER_VERSION] != SBFS_VERSION_TREE) {
|
||||||
|
fprintf(stderr, "Error: That disk is version %u, and this understands %u and %u.\n",
|
||||||
|
block[SBFS_SUPER_VERSION], SBFS_VERSION_FLAT, SBFS_VERSION_TREE);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
// A directory big enough that its last entries cannot be named as a parent. See
|
||||||
|
// sbfs.h: those entries do not refuse what is put in them, they quietly put it in the
|
||||||
|
// root instead. Refused on the way in, so that nothing below ever has to wonder.
|
||||||
|
uint16_t directoryBlocks = readWord(block + SBFS_SUPER_DIRBLOCKS);
|
||||||
|
if (directoryBlocks > SBFS_MAX_DIRECTORY_BLOCKS) {
|
||||||
|
fprintf(stderr, "Error: That disk claims %u directory blocks, and %u is the most"
|
||||||
|
" that leaves every entry able to be named as a parent.\n",
|
||||||
|
directoryBlocks, SBFS_MAX_DIRECTORY_BLOCKS);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
super->version = block[SBFS_SUPER_VERSION];
|
||||||
super->diskBlocks = readWord(block + SBFS_SUPER_DISK);
|
super->diskBlocks = readWord(block + SBFS_SUPER_DISK);
|
||||||
super->directoryStart = readWord(block + SBFS_SUPER_DIRSTART);
|
super->directoryStart = readWord(block + SBFS_SUPER_DIRSTART);
|
||||||
super->directoryBlocks = readWord(block + SBFS_SUPER_DIRBLOCKS);
|
super->directoryBlocks = directoryBlocks;
|
||||||
super->freeBlocks = readWord(block + SBFS_SUPER_FREE);
|
super->freeBlocks = readWord(block + SBFS_SUPER_FREE);
|
||||||
|
super->bootBlocks = readWord(block + SBFS_SUPER_BOOTBLOCKS);
|
||||||
|
super->bootSlot = block[SBFS_SUPER_BOOTSLOT];
|
||||||
|
super->bootState = block[SBFS_SUPER_BOOTSTATE];
|
||||||
|
|
||||||
|
// The boot area and the directory's position describe the same fact from two sides,
|
||||||
|
// so they have to agree or one of them is wrong and there is no way to tell which.
|
||||||
|
uint32_t expected = SBFS_FIRST_BOOT_BLOCK
|
||||||
|
+ (uint32_t)super->bootBlocks * SBFS_BOOT_SLOTS;
|
||||||
|
if (super->directoryStart != expected) {
|
||||||
|
fprintf(stderr, "Error: That disk says %u blocks of boot area and puts its"
|
||||||
|
" directory at %u, which should then be %u.\n",
|
||||||
|
super->bootBlocks, super->directoryStart, expected);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (super->bootSlot >= SBFS_BOOT_SLOTS) {
|
||||||
|
fprintf(stderr, "Error: That disk names boot slot %u, and there are %u.\n",
|
||||||
|
super->bootSlot, SBFS_BOOT_SLOTS);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,11 +122,17 @@ static int writeSuperblock(FILE *image, const Superblock *super) {
|
|||||||
uint8_t block[SBFS_BLOCK_BYTES];
|
uint8_t block[SBFS_BLOCK_BYTES];
|
||||||
memset(block, 0, sizeof(block));
|
memset(block, 0, sizeof(block));
|
||||||
memcpy(block, SBFS_MAGIC, SBFS_MAGIC_BYTES);
|
memcpy(block, SBFS_MAGIC, SBFS_MAGIC_BYTES);
|
||||||
block[SBFS_SUPER_VERSION] = SBFS_VERSION;
|
// Written back at the version it was read at. Putting a file on a flat disk leaves it
|
||||||
|
// flat; only creating a directory on it makes the difference real, and only mkdir
|
||||||
|
// raises the number.
|
||||||
|
block[SBFS_SUPER_VERSION] = super->version;
|
||||||
writeWord(block + SBFS_SUPER_DISK, super->diskBlocks);
|
writeWord(block + SBFS_SUPER_DISK, super->diskBlocks);
|
||||||
writeWord(block + SBFS_SUPER_DIRSTART, super->directoryStart);
|
writeWord(block + SBFS_SUPER_DIRSTART, super->directoryStart);
|
||||||
writeWord(block + SBFS_SUPER_DIRBLOCKS, super->directoryBlocks);
|
writeWord(block + SBFS_SUPER_DIRBLOCKS, super->directoryBlocks);
|
||||||
writeWord(block + SBFS_SUPER_FREE, super->freeBlocks);
|
writeWord(block + SBFS_SUPER_FREE, super->freeBlocks);
|
||||||
|
writeWord(block + SBFS_SUPER_BOOTBLOCKS, super->bootBlocks);
|
||||||
|
block[SBFS_SUPER_BOOTSLOT] = super->bootSlot;
|
||||||
|
block[SBFS_SUPER_BOOTSTATE] = super->bootState;
|
||||||
return writeBlock(image, 0, block);
|
return writeBlock(image, 0, block);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +180,28 @@ static int entryInUse(const uint8_t *entry) {
|
|||||||
return (entry[SBFS_ENTRY_FLAGS] & SBFS_FLAG_IN_USE) != 0;
|
return (entry[SBFS_ENTRY_FLAGS] & SBFS_FLAG_IN_USE) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int entryIsDirectory(const uint8_t *entry) {
|
||||||
|
return (entry[SBFS_ENTRY_FLAGS] & SBFS_FLAG_DIRECTORY) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A save that was interrupted between writing its temporary and committing it. The blocks
|
||||||
|
// are genuinely spoken for - entryInUse says so, and the allocator must keep believing it
|
||||||
|
// - but nothing has claimed them under a name anybody asked for.
|
||||||
|
static int entryIsTemporary(const uint8_t *entry) {
|
||||||
|
return (entry[SBFS_ENTRY_FLAGS] & SBFS_FLAG_TEMPORARY) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The index of the entry this one lives in, or -1 for the root. Stored as index plus one
|
||||||
|
// so that a zeroed field - which is what every version one entry has - means the root.
|
||||||
|
static int entryParent(const uint8_t *entry) {
|
||||||
|
return SBFS_PARENT_INDEX(readWord(entry + SBFS_ENTRY_PARENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void entrySetParent(uint8_t *entry, int parent) {
|
||||||
|
writeWord(entry + SBFS_ENTRY_PARENT,
|
||||||
|
parent < 0 ? SBFS_PARENT_ROOT : SBFS_PARENT_OF(parent));
|
||||||
|
}
|
||||||
|
|
||||||
static uint32_t entrySize(const uint8_t *entry) {
|
static uint32_t entrySize(const uint8_t *entry) {
|
||||||
return (uint32_t)readWord(entry + SBFS_ENTRY_BLOCKS) * SBFS_BLOCK_BYTES
|
return (uint32_t)readWord(entry + SBFS_ENTRY_BLOCKS) * SBFS_BLOCK_BYTES
|
||||||
+ entry[SBFS_ENTRY_TAIL];
|
+ entry[SBFS_ENTRY_TAIL];
|
||||||
@@ -153,13 +219,58 @@ static void entryName(const uint8_t *entry, char *into) {
|
|||||||
into[SBFS_NAME_BYTES] = '\0';
|
into[SBFS_NAME_BYTES] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
static int findByName(const Directory *directory, const char *name) {
|
// ---- Paths ----
|
||||||
|
//
|
||||||
|
// A path is names separated by '/', and every path this tool is given is from the root:
|
||||||
|
// there is no working directory on the host, and one would have nowhere to live between
|
||||||
|
// two runs of a command line tool. The machine is the side that gets one of those.
|
||||||
|
//
|
||||||
|
// The root is not an entry. It is the absence of a parent, so -1 stands for it throughout
|
||||||
|
// and is a perfectly good answer rather than a failure - which is why every function here
|
||||||
|
// returns its outcome separately from the index it found.
|
||||||
|
|
||||||
|
// How long a path this tool will carry. Nothing in the format says: a path is not stored
|
||||||
|
// anywhere, it is only ever walked, and what is stored is one name and one parent.
|
||||||
|
#define SBFS_PATH_BYTES 512
|
||||||
|
|
||||||
|
// Copies the next name out of a path and returns where the path goes on, or NULL when
|
||||||
|
// there are no more. Empty pieces - a leading separator, a doubled one, a trailing one -
|
||||||
|
// are skipped rather than refused, so "/Apps/" and "Apps" walk the same way.
|
||||||
|
static const char *nextComponent(const char *path, char *into, int *tooLong) {
|
||||||
|
while (*path == SBFS_SEPARATOR) {
|
||||||
|
path++;
|
||||||
|
}
|
||||||
|
if (*path == '\0') {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
size_t n = 0;
|
||||||
|
while (*path != '\0' && *path != SBFS_SEPARATOR) {
|
||||||
|
if (n < SBFS_NAME_BYTES) {
|
||||||
|
into[n] = *path;
|
||||||
|
}
|
||||||
|
n++;
|
||||||
|
path++;
|
||||||
|
}
|
||||||
|
*tooLong = (n > SBFS_NAME_BYTES);
|
||||||
|
into[n > SBFS_NAME_BYTES ? SBFS_NAME_BYTES : n] = '\0';
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One named child of one directory. `parent` is an entry index, or -1 for the root.
|
||||||
|
//
|
||||||
|
// This is the whole of what a directory is. There is no list of children anywhere: being
|
||||||
|
// a child is a fact written in the child, so finding them means looking at all of them.
|
||||||
|
// That is the same walk the flat version did, with one more thing compared.
|
||||||
|
static int findIn(const Directory *directory, const char *name, int parent) {
|
||||||
char held[SBFS_NAME_BYTES + 1];
|
char held[SBFS_NAME_BYTES + 1];
|
||||||
for (int i = 0; i < directory->entries; i++) {
|
for (int i = 0; i < directory->entries; i++) {
|
||||||
const uint8_t *entry = entryAt(directory, i);
|
const uint8_t *entry = entryAt(directory, i);
|
||||||
if (!entryInUse(entry)) {
|
if (!entryInUse(entry)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (entryParent(entry) != parent) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
entryName(entry, held);
|
entryName(entry, held);
|
||||||
if (strcmp(held, name) == 0) {
|
if (strcmp(held, name) == 0) {
|
||||||
return i;
|
return i;
|
||||||
@@ -168,6 +279,102 @@ static int findByName(const Directory *directory, const char *name) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Walks a whole path. Zero if it got there, and then *index is the entry it names or -1
|
||||||
|
// for the root. Anything else leaves *why saying what stopped it.
|
||||||
|
static int resolve(const Directory *directory, const char *path, int *index,
|
||||||
|
const char **why) {
|
||||||
|
int at = -1;
|
||||||
|
char name[SBFS_NAME_BYTES + 1];
|
||||||
|
int tooLong = 0;
|
||||||
|
const char *rest = path;
|
||||||
|
while ((rest = nextComponent(rest, name, &tooLong)) != NULL) {
|
||||||
|
if (tooLong) {
|
||||||
|
*why = "has a name longer than 22 characters in it";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (strcmp(name, ".") == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "..") == 0) {
|
||||||
|
if (at >= 0) {
|
||||||
|
at = entryParent(entryAt(directory, at));
|
||||||
|
}
|
||||||
|
continue; // ".." from the root is the root.
|
||||||
|
}
|
||||||
|
// Only a directory can be walked through. The LAST thing on the path is not
|
||||||
|
// checked here, because only the caller knows whether it wanted a file or a
|
||||||
|
// directory, and it can say so far better than this can.
|
||||||
|
if (at >= 0 && !entryIsDirectory(entryAt(directory, at))) {
|
||||||
|
*why = "has something in it that is not a directory";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int found = findIn(directory, name, at);
|
||||||
|
if (found < 0) {
|
||||||
|
*why = "is not there";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
at = found;
|
||||||
|
}
|
||||||
|
*index = at;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walks all but the last name, so that a caller can make the last one. *parent comes back
|
||||||
|
// as the directory to make it in, and `leaf` as the name to make. `leaf` must have room
|
||||||
|
// for SBFS_NAME_BYTES + 1.
|
||||||
|
static int resolveParent(const Directory *directory, const char *path, char *leaf,
|
||||||
|
int *parent, const char **why) {
|
||||||
|
const char *cut = strrchr(path, SBFS_SEPARATOR);
|
||||||
|
const char *last = cut ? cut + 1 : path;
|
||||||
|
if (*last == '\0') {
|
||||||
|
*why = "ends in a separator, so it does not name anything";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (strlen(last) > SBFS_NAME_BYTES) {
|
||||||
|
*why = "ends in a name longer than 22 characters";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (strcmp(last, ".") == 0 || strcmp(last, "..") == 0) {
|
||||||
|
*why = "ends in a name that is already taken by the filesystem";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
strcpy(leaf, last);
|
||||||
|
if (cut == NULL) {
|
||||||
|
*parent = -1; // A bare name belongs in the root.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
char head[SBFS_PATH_BYTES];
|
||||||
|
size_t n = (size_t)(cut - path);
|
||||||
|
if (n >= sizeof(head)) {
|
||||||
|
*why = "is longer than this tool will carry";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
memcpy(head, path, n);
|
||||||
|
head[n] = '\0';
|
||||||
|
return resolve(directory, head, parent, why); // "" is the root, which is correct.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether something can be made inside *parent at all. The root always can.
|
||||||
|
static int parentIsUsable(const Directory *directory, int parent, const char *path) {
|
||||||
|
if (parent >= 0 && !entryIsDirectory(entryAt(directory, parent))) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" is inside something that is not a directory.\n", path);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether a directory has anything in it. Nothing points down, so this is the only way
|
||||||
|
// to know: something is in it if something says it is.
|
||||||
|
static int directoryHasChildren(const Directory *directory, int parent) {
|
||||||
|
for (int i = 0; i < directory->entries; i++) {
|
||||||
|
const uint8_t *entry = entryAt(directory, i);
|
||||||
|
if (entryInUse(entry) && entryParent(entry) == parent) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Where the first free run of the wanted length begins, or -1 if there is not one.
|
// Where the first free run of the wanted length begins, or -1 if there is not one.
|
||||||
//
|
//
|
||||||
// There is no allocation bitmap, and that is the design rather than an omission: with
|
// There is no allocation bitmap, and that is the design rather than an omission: with
|
||||||
@@ -224,10 +431,22 @@ static uint16_t countFree(const Directory *directory, const Superblock *super) {
|
|||||||
|
|
||||||
// ---- Commands ----
|
// ---- Commands ----
|
||||||
|
|
||||||
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks) {
|
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks,
|
||||||
if (blocks <= 1u + directoryBlocks) {
|
uint16_t bootBlocks) {
|
||||||
fprintf(stderr, "Error: A disk of %u blocks has no room for a superblock and a"
|
if (directoryBlocks > SBFS_MAX_DIRECTORY_BLOCKS) {
|
||||||
" directory of %u.\n", blocks, directoryBlocks);
|
fprintf(stderr, "Error: %u directory blocks is %u entries, and entry 65535 has no"
|
||||||
|
" parent number - adding one wraps to zero, which is the root."
|
||||||
|
" %u blocks is the most, giving %u entries.\n",
|
||||||
|
directoryBlocks, (unsigned)directoryBlocks * SBFS_ENTRIES_PER_BLOCK,
|
||||||
|
SBFS_MAX_DIRECTORY_BLOCKS, SBFS_MAX_ENTRIES);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
uint32_t overhead = 1u + (uint32_t)bootBlocks * SBFS_BOOT_SLOTS + directoryBlocks;
|
||||||
|
if (blocks <= overhead) {
|
||||||
|
fprintf(stderr, "Error: A disk of %u blocks has no room for a superblock, %u of"
|
||||||
|
" boot area and a directory of %u.\n",
|
||||||
|
blocks, (unsigned)((uint32_t)bootBlocks * SBFS_BOOT_SLOTS),
|
||||||
|
directoryBlocks);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
// Quietly, because a disk that is not there yet is the ordinary case for format and
|
// Quietly, because a disk that is not there yet is the ordinary case for format and
|
||||||
@@ -248,21 +467,116 @@ static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Superblock super;
|
Superblock super;
|
||||||
|
// A DISK IS AT THE LOWEST VERSION THAT DESCRIBES WHAT IS ON IT, and a fresh one has
|
||||||
|
// no directories on it, so it is flat and flat is version one. The number says what
|
||||||
|
// the disk contains, not which tool made it - which is what lets everything built
|
||||||
|
// here still be mounted by a reader that has never heard of a directory. mkdir is
|
||||||
|
// what raises it, because mkdir is what makes the difference true.
|
||||||
|
super.version = SBFS_VERSION_FLAT;
|
||||||
super.diskBlocks = blocks;
|
super.diskBlocks = blocks;
|
||||||
super.directoryStart = SBFS_FIRST_DIRECTORY_BLOCK;
|
// THE DIRECTORY MOVES UP BY THE BOOT AREA, and that is the whole mechanism. Both
|
||||||
|
// implementations already work out the first usable block as directoryStart plus
|
||||||
|
// directoryBlocks, so everything below the directory is reserved by arithmetic that
|
||||||
|
// was there before any of this, and no allocator changed.
|
||||||
|
super.directoryStart = (uint16_t)(SBFS_FIRST_BOOT_BLOCK
|
||||||
|
+ (uint32_t)bootBlocks * SBFS_BOOT_SLOTS);
|
||||||
super.directoryBlocks = directoryBlocks;
|
super.directoryBlocks = directoryBlocks;
|
||||||
super.freeBlocks = (uint16_t)(blocks - 1 - directoryBlocks);
|
super.freeBlocks = (uint16_t)(blocks - overhead);
|
||||||
|
super.bootBlocks = bootBlocks;
|
||||||
|
super.bootSlot = 0;
|
||||||
|
super.bootState = SBFS_BOOT_SETTLED;
|
||||||
if (writeSuperblock(image, &super)) {
|
if (writeSuperblock(image, &super)) {
|
||||||
fclose(image);
|
fclose(image);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
fclose(image);
|
fclose(image);
|
||||||
|
if (bootBlocks) {
|
||||||
|
printf("Formatted %s: %u blocks, two boot slots of %u, %u of directory, %u free.\n",
|
||||||
|
path, blocks, bootBlocks, directoryBlocks, super.freeBlocks);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
printf("Formatted %s: %u blocks, %u of directory, %u free.\n",
|
printf("Formatted %s: %u blocks, %u of directory, %u free.\n",
|
||||||
path, blocks, directoryBlocks, super.freeBlocks);
|
path, blocks, directoryBlocks, super.freeBlocks);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int commandList(const char *path) {
|
// The full path of an entry, built by walking up the parents and writing them out
|
||||||
|
// backwards. Nothing stores a path, so this is the only way to have one.
|
||||||
|
//
|
||||||
|
// The depth cap is what makes this safe on a disk whose parents form a loop: walking up
|
||||||
|
// would otherwise never reach the root. Running out of depth is reported as not fitting,
|
||||||
|
// which is what it is.
|
||||||
|
static int entryPath(const Directory *directory, int index, char *into, size_t room) {
|
||||||
|
int chain[64];
|
||||||
|
int depth = 0;
|
||||||
|
while (index >= 0 && depth < (int)(sizeof(chain) / sizeof(chain[0]))) {
|
||||||
|
chain[depth++] = index;
|
||||||
|
index = entryParent(entryAt(directory, index));
|
||||||
|
}
|
||||||
|
if (index >= 0) {
|
||||||
|
return -1; // Deeper than this will walk, or a loop.
|
||||||
|
}
|
||||||
|
size_t at = 0;
|
||||||
|
char name[SBFS_NAME_BYTES + 1];
|
||||||
|
for (int i = depth - 1; i >= 0; i--) {
|
||||||
|
entryName(entryAt(directory, chain[i]), name);
|
||||||
|
size_t n = strlen(name);
|
||||||
|
if (at + n + 2 > room) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
into[at++] = SBFS_SEPARATOR;
|
||||||
|
memcpy(into + at, name, n);
|
||||||
|
at += n;
|
||||||
|
}
|
||||||
|
into[at] = '\0';
|
||||||
|
return (int)at;
|
||||||
|
}
|
||||||
|
|
||||||
|
// What a listing added up to. Kept together because the two numbers are only meaningful
|
||||||
|
// beside each other: a disk runs out of entries, not of files.
|
||||||
|
typedef struct {
|
||||||
|
int files;
|
||||||
|
int directories;
|
||||||
|
int temporaries;
|
||||||
|
} Tally;
|
||||||
|
|
||||||
|
// Prints one directory and everything under it, depth first and in entry order, which is
|
||||||
|
// the order the machine will walk them in too.
|
||||||
|
//
|
||||||
|
// Recursing from the root can never loop, because it only ever descends into entries
|
||||||
|
// whose parent is where it already is. A cycle among entries is therefore not an infinite
|
||||||
|
// walk - it is a set of entries this never reaches, which is what the caller counts.
|
||||||
|
static void listTree(const Directory *directory, int parent, char *prefix, size_t at,
|
||||||
|
Tally *tally) {
|
||||||
|
char name[SBFS_NAME_BYTES + 1];
|
||||||
|
for (int i = 0; i < directory->entries; i++) {
|
||||||
|
const uint8_t *entry = entryAt(directory, i);
|
||||||
|
if (!entryInUse(entry) || entryParent(entry) != parent) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entryName(entry, name);
|
||||||
|
size_t n = strlen(name);
|
||||||
|
if (at + n + 2 >= SBFS_PATH_BYTES) {
|
||||||
|
continue; // Deeper than this tool will print.
|
||||||
|
}
|
||||||
|
prefix[at] = SBFS_SEPARATOR;
|
||||||
|
memcpy(prefix + at + 1, name, n + 1);
|
||||||
|
if (entryIsDirectory(entry)) {
|
||||||
|
printf("%-34s %8s %7s %7s\n", prefix, "dir", "-", "-");
|
||||||
|
tally->directories++;
|
||||||
|
listTree(directory, i, prefix, at + 1 + n, tally);
|
||||||
|
} else {
|
||||||
|
printf("%-34s %8u %7u %7u%s\n", prefix, entrySize(entry),
|
||||||
|
readWord(entry + SBFS_ENTRY_START), entryBlocksUsed(entry),
|
||||||
|
entryIsTemporary(entry) ? " unfinished" : "");
|
||||||
|
tally->files++;
|
||||||
|
tally->temporaries += entryIsTemporary(entry);
|
||||||
|
}
|
||||||
|
prefix[at] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int commandList(const char *path, const char *within) {
|
||||||
FILE *image = openImage(path, "rb");
|
FILE *image = openImage(path, "rb");
|
||||||
if (image == NULL) {
|
if (image == NULL) {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -273,34 +587,250 @@ static int commandList(const char *path) {
|
|||||||
fclose(image);
|
fclose(image);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
printf("%s: %u blocks, %u of directory, %u entries.\n",
|
printf("%s: %u blocks, %u of directory, %u entries, version %u.\n",
|
||||||
path, super.diskBlocks, super.directoryBlocks, directory.entries);
|
path, super.diskBlocks, super.directoryBlocks, directory.entries,
|
||||||
printf("%-22s %8s %7s %7s\n", "NAME", "BYTES", "START", "BLOCKS");
|
super.version);
|
||||||
char name[SBFS_NAME_BYTES + 1];
|
printf("%-34s %8s %7s %7s\n", "NAME", "BYTES", "START", "BLOCKS");
|
||||||
int shown = 0;
|
char prefix[SBFS_PATH_BYTES];
|
||||||
for (int i = 0; i < directory.entries; i++) {
|
Tally tally = { 0, 0, 0 };
|
||||||
const uint8_t *entry = entryAt(&directory, i);
|
int start = -1;
|
||||||
if (!entryInUse(entry)) {
|
if (within != NULL) {
|
||||||
continue;
|
const char *why = NULL;
|
||||||
|
if (resolve(&directory, within, &start, &why)) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" %s.\n", within, why);
|
||||||
|
free(directory.bytes);
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (start >= 0 && !entryIsDirectory(entryAt(&directory, start))) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" is not a directory.\n", within);
|
||||||
|
free(directory.bytes);
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
entryName(entry, name);
|
|
||||||
printf("%-22s %8u %7u %7u\n", name, entrySize(entry),
|
|
||||||
readWord(entry + SBFS_ENTRY_START), entryBlocksUsed(entry));
|
|
||||||
shown++;
|
|
||||||
}
|
}
|
||||||
|
// Listing one directory still prints whole paths, because a path that is only true
|
||||||
|
// relative to an argument the reader cannot see is worse than no path at all.
|
||||||
|
size_t from = 0;
|
||||||
|
prefix[0] = '\0';
|
||||||
|
if (start >= 0) {
|
||||||
|
int n = entryPath(&directory, start, prefix, sizeof(prefix));
|
||||||
|
if (n < 0) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" is nested too deep to print.\n", within);
|
||||||
|
free(directory.bytes);
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
from = (size_t)n;
|
||||||
|
}
|
||||||
|
listTree(&directory, start, prefix, from, &tally);
|
||||||
|
|
||||||
// The count in the superblock is a cache, so say what the directory actually adds up
|
// The count in the superblock is a cache, so say what the directory actually adds up
|
||||||
// to as well. If the two ever disagree, the directory is the one to believe.
|
// to as well. If the two ever disagree, the directory is the one to believe.
|
||||||
uint16_t counted = countFree(&directory, &super);
|
uint16_t counted = countFree(&directory, &super);
|
||||||
printf("%d file%s, %u blocks free", shown, shown == 1 ? "" : "s", counted);
|
printf("%d file%s", tally.files, tally.files == 1 ? "" : "s");
|
||||||
|
if (tally.directories > 0) {
|
||||||
|
printf(", %d director%s", tally.directories,
|
||||||
|
tally.directories == 1 ? "y" : "ies");
|
||||||
|
}
|
||||||
|
// Entries are the ceiling nobody notices until they hit it, and on a disk of small
|
||||||
|
// files they run out long before the blocks do. Saying both means never having to
|
||||||
|
// work out which one is about to bite.
|
||||||
|
printf(", %d of %d entries used, %u blocks free",
|
||||||
|
tally.files + tally.directories, directory.entries, counted);
|
||||||
if (counted != super.freeBlocks) {
|
if (counted != super.freeBlocks) {
|
||||||
printf(" (the superblock says %u, which is stale)", super.freeBlocks);
|
printf(" (the superblock says %u, which is stale)", super.freeBlocks);
|
||||||
}
|
}
|
||||||
printf(".\n");
|
printf(".\n");
|
||||||
|
|
||||||
|
// A save that stopped between deleting the old entry and naming the new one. The
|
||||||
|
// bytes are all there under the temporary's name and one rename brings them back,
|
||||||
|
// which is the whole of the recovery this format offers - so the thing that matters
|
||||||
|
// is that a listing says so rather than showing a file with an odd name.
|
||||||
|
if (tally.temporaries > 0) {
|
||||||
|
printf("%d unfinished write%s: the blocks are held and the data is there, under"
|
||||||
|
" that name. Rename it to keep it, delete it to let the blocks go.\n",
|
||||||
|
tally.temporaries, tally.temporaries == 1 ? "" : "s");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything in use should have been reached by walking down from the root. Anything
|
||||||
|
// that was not is pointing at a parent that is not there, or at itself, and that is
|
||||||
|
// worth saying out loud rather than quietly leaving off the listing.
|
||||||
|
if (within == NULL) {
|
||||||
|
int inUse = 0;
|
||||||
|
for (int i = 0; i < directory.entries; i++) {
|
||||||
|
if (entryInUse(entryAt(&directory, i))) {
|
||||||
|
inUse++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inUse != tally.files + tally.directories) {
|
||||||
|
printf("%d entr%s in use but not reachable from the root.\n",
|
||||||
|
inUse - (tally.files + tally.directories),
|
||||||
|
inUse - (tally.files + tally.directories) == 1 ? "y is" : "ies are");
|
||||||
|
}
|
||||||
|
}
|
||||||
free(directory.bytes);
|
free(directory.bytes);
|
||||||
fclose(image);
|
fclose(image);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Writing a boot slot ----
|
||||||
|
//
|
||||||
|
// Raw blocks, outside the filesystem, with no entry and no name. That is what makes this
|
||||||
|
// different from put: there is nothing to rename, so the safety comes from writing the
|
||||||
|
// slot that is NOT live and moving one byte afterwards.
|
||||||
|
//
|
||||||
|
// The one byte is moved by a separate command on purpose. Writing a slot and choosing it
|
||||||
|
// are different decisions - a slot can be written now and chosen after it has been looked
|
||||||
|
// at - and putting them in one command would make every write a commitment.
|
||||||
|
static int commandBoot(const char *path, const char *hostFile, long slot) {
|
||||||
|
FILE *image = openImage(path, "r+b");
|
||||||
|
if (image == NULL) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Superblock super;
|
||||||
|
if (readSuperblock(image, &super)) {
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (super.bootBlocks == 0) {
|
||||||
|
fprintf(stderr, "Error: That disk has no boot area. Format it with one.\n");
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (slot < 0 || slot >= SBFS_BOOT_SLOTS) {
|
||||||
|
fprintf(stderr, "Error: There are %d boot slots, numbered 0 and %d.\n",
|
||||||
|
SBFS_BOOT_SLOTS, SBFS_BOOT_SLOTS - 1);
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
FILE *source = fopen(hostFile, "rb");
|
||||||
|
if (source == NULL) {
|
||||||
|
fprintf(stderr, "Error: Couldn't open \"%s\".\n", hostFile);
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (fseek(source, 0, SEEK_END) != 0) {
|
||||||
|
fprintf(stderr, "Error: Couldn't measure \"%s\".\n", hostFile);
|
||||||
|
fclose(source); fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
long size = ftell(source);
|
||||||
|
rewind(source);
|
||||||
|
long blocks = (size + SBFS_BLOCK_BYTES - 1) / SBFS_BLOCK_BYTES;
|
||||||
|
if (blocks > super.bootBlocks) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" is %ld bytes, which is %ld blocks, and a slot"
|
||||||
|
" holds %u.\n", hostFile, size, blocks, super.bootBlocks);
|
||||||
|
fclose(source); fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE WHOLE SLOT IS WRITTEN, not just the part the file fills. A slot holding the tail
|
||||||
|
// of whatever was there before is a slot whose contents depend on its history, and the
|
||||||
|
// first stage reads all of it without knowing where the file stopped.
|
||||||
|
uint16_t first = (uint16_t)(SBFS_FIRST_BOOT_BLOCK
|
||||||
|
+ (uint32_t)slot * super.bootBlocks);
|
||||||
|
uint8_t block[SBFS_BLOCK_BYTES];
|
||||||
|
for (uint16_t i = 0; i < super.bootBlocks; i++) {
|
||||||
|
memset(block, 0, sizeof(block));
|
||||||
|
size_t got = fread(block, 1, SBFS_BLOCK_BYTES, source);
|
||||||
|
if (got == 0 && ferror(source)) {
|
||||||
|
fprintf(stderr, "Error: Couldn't read \"%s\".\n", hostFile);
|
||||||
|
fclose(source); fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (writeBlock(image, (uint16_t)(first + i), block)) {
|
||||||
|
fclose(source); fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(source);
|
||||||
|
fclose(image);
|
||||||
|
printf("Wrote %s into boot slot %ld: %ld bytes in %u blocks from block %u.%s\n",
|
||||||
|
hostFile, slot, size, super.bootBlocks, first,
|
||||||
|
slot == super.bootSlot ? "" : " It is not the live slot.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choosing which slot the machine starts from. One byte, written on its own, so that the
|
||||||
|
// change from one system to another is a single block write that either happened or did
|
||||||
|
// not.
|
||||||
|
static int commandBootSlot(const char *path, long slot) {
|
||||||
|
FILE *image = openImage(path, "r+b");
|
||||||
|
if (image == NULL) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Superblock super;
|
||||||
|
if (readSuperblock(image, &super)) {
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (super.bootBlocks == 0) {
|
||||||
|
fprintf(stderr, "Error: That disk has no boot area.\n");
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (slot < 0 || slot >= SBFS_BOOT_SLOTS) {
|
||||||
|
fprintf(stderr, "Error: There are %d boot slots, numbered 0 and %d.\n",
|
||||||
|
SBFS_BOOT_SLOTS, SBFS_BOOT_SLOTS - 1);
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
super.bootSlot = (uint8_t)slot;
|
||||||
|
if (writeSuperblock(image, &super)) {
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fclose(image);
|
||||||
|
printf("The machine now starts from boot slot %ld.\n", slot);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- How the last start went, from the host ----
|
||||||
|
//
|
||||||
|
// Shown with no argument and set with one. Setting it is how a disk that fell back is told
|
||||||
|
// to try again, which is a decision rather than a repair: the thing that did not start has
|
||||||
|
// to be fixed first, or the next start marks it and falls back once more.
|
||||||
|
static const char *bootStateName(uint8_t state) {
|
||||||
|
switch (state) {
|
||||||
|
case SBFS_BOOT_SETTLED: return "settled, so the next start will use the configuration";
|
||||||
|
case SBFS_BOOT_TRYING: return "trying, so the last start never arrived";
|
||||||
|
case SBFS_BOOT_FELLBACK: return "fell back, and will keep doing so until settled";
|
||||||
|
default: return "a number this does not recognise";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int commandBootState(const char *path, const char *setting) {
|
||||||
|
FILE *image = openImage(path, setting ? "r+b" : "rb");
|
||||||
|
if (image == NULL) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Superblock super;
|
||||||
|
if (readSuperblock(image, &super)) {
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (setting == NULL) {
|
||||||
|
printf("%u: %s\n", super.bootState, bootStateName(super.bootState));
|
||||||
|
fclose(image);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
long wanted = strtol(setting, NULL, 0);
|
||||||
|
if (wanted < 0 || wanted > SBFS_BOOT_FELLBACK) {
|
||||||
|
fprintf(stderr, "Error: The boot state is 0, 1 or 2.\n");
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
super.bootState = (uint8_t)wanted;
|
||||||
|
if (writeSuperblock(image, &super)) {
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fclose(image);
|
||||||
|
printf("%ld: %s\n", wanted, bootStateName((uint8_t)wanted));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static int commandPut(const char *path, const char *hostFile, const char *asName) {
|
static int commandPut(const char *path, const char *hostFile, const char *asName) {
|
||||||
FILE *source = fopen(hostFile, "rb");
|
FILE *source = fopen(hostFile, "rb");
|
||||||
if (source == NULL) {
|
if (source == NULL) {
|
||||||
@@ -316,14 +846,6 @@ static int commandPut(const char *path, const char *hostFile, const char *asName
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strlen(asName) > SBFS_NAME_BYTES) {
|
|
||||||
fprintf(stderr, "Error: \"%s\" is %zu characters, and a name may be %d.\n"
|
|
||||||
" Give a shorter one as the last argument.\n",
|
|
||||||
asName, strlen(asName), SBFS_NAME_BYTES);
|
|
||||||
fclose(source);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
FILE *image = openImage(path, "r+b");
|
FILE *image = openImage(path, "r+b");
|
||||||
if (image == NULL) {
|
if (image == NULL) {
|
||||||
fclose(source);
|
fclose(source);
|
||||||
@@ -336,8 +858,20 @@ static int commandPut(const char *path, const char *hostFile, const char *asName
|
|||||||
fclose(image);
|
fclose(image);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (findByName(&directory, asName) >= 0) {
|
// Where it goes and what it is called are one argument. A bare name still means the
|
||||||
fprintf(stderr, "Error: \"%s\" is already on the disk. Delete it first.\n", asName);
|
// root, so every command line that worked before this still works.
|
||||||
|
char leaf[SBFS_NAME_BYTES + 1];
|
||||||
|
int parent = -1;
|
||||||
|
const char *why = NULL;
|
||||||
|
if (resolveParent(&directory, asName, leaf, &parent, &why)) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" %s.\n", asName, why);
|
||||||
|
goto failed;
|
||||||
|
}
|
||||||
|
if (!parentIsUsable(&directory, parent, asName)) {
|
||||||
|
goto failed;
|
||||||
|
}
|
||||||
|
if (findIn(&directory, leaf, parent) >= 0) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" is already there. Delete it first.\n", asName);
|
||||||
goto failed;
|
goto failed;
|
||||||
}
|
}
|
||||||
int slot = -1;
|
int slot = -1;
|
||||||
@@ -380,7 +914,8 @@ static int commandPut(const char *path, const char *hostFile, const char *asName
|
|||||||
writeWord(entry + SBFS_ENTRY_START, (uint16_t)start);
|
writeWord(entry + SBFS_ENTRY_START, (uint16_t)start);
|
||||||
writeWord(entry + SBFS_ENTRY_BLOCKS, whole);
|
writeWord(entry + SBFS_ENTRY_BLOCKS, whole);
|
||||||
entry[SBFS_ENTRY_TAIL] = tail;
|
entry[SBFS_ENTRY_TAIL] = tail;
|
||||||
memcpy(entry + SBFS_ENTRY_NAME, asName, strlen(asName));
|
memcpy(entry + SBFS_ENTRY_NAME, leaf, strlen(leaf));
|
||||||
|
entrySetParent(entry, parent);
|
||||||
|
|
||||||
super.freeBlocks = countFree(&directory, &super);
|
super.freeBlocks = countFree(&directory, &super);
|
||||||
if (writeDirectory(image, &super, &directory) || writeSuperblock(image, &super)) {
|
if (writeDirectory(image, &super, &directory) || writeSuperblock(image, &super)) {
|
||||||
@@ -410,9 +945,16 @@ static int commandGet(const char *path, const char *name, const char *hostFile)
|
|||||||
fclose(image);
|
fclose(image);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
int slot = findByName(&directory, name);
|
int slot = -1;
|
||||||
if (slot < 0) {
|
const char *why = NULL;
|
||||||
fprintf(stderr, "Error: There is no \"%s\" on that disk.\n", name);
|
if (resolve(&directory, name, &slot, &why) || slot < 0) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" %s.\n", name, why ? why : "is the root, which is not a file");
|
||||||
|
free(directory.bytes);
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (entryIsDirectory(entryAt(&directory, slot))) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" is a directory. There is nothing to take off.\n", name);
|
||||||
free(directory.bytes);
|
free(directory.bytes);
|
||||||
fclose(image);
|
fclose(image);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -460,9 +1002,18 @@ static int commandDelete(const char *path, const char *name) {
|
|||||||
fclose(image);
|
fclose(image);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
int slot = findByName(&directory, name);
|
int slot = -1;
|
||||||
if (slot < 0) {
|
const char *why = NULL;
|
||||||
fprintf(stderr, "Error: There is no \"%s\" on that disk.\n", name);
|
if (resolve(&directory, name, &slot, &why) || slot < 0) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" %s.\n", name, why ? why : "is the root, which cannot be deleted");
|
||||||
|
free(directory.bytes);
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// delete is for files and rmdir is for directories, so that neither can be the one
|
||||||
|
// that took away more than was asked for.
|
||||||
|
if (entryIsDirectory(entryAt(&directory, slot))) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" is a directory. Use rmdir.\n", name);
|
||||||
free(directory.bytes);
|
free(directory.bytes);
|
||||||
fclose(image);
|
fclose(image);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -481,19 +1032,142 @@ static int commandDelete(const char *path, const char *name) {
|
|||||||
return failed;
|
return failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- mkdir ----
|
||||||
|
//
|
||||||
|
// A directory costs one entry and no blocks at all. Its start, blocks and tail stay zero,
|
||||||
|
// which is what keeps it out of the allocator's way: with files laid down contiguously an
|
||||||
|
// entry with no range cannot overlap anything.
|
||||||
|
//
|
||||||
|
// This is also the only thing that raises a disk from version one to version two, because
|
||||||
|
// it is the only thing that makes the difference between them real.
|
||||||
|
static int commandMakeDirectory(const char *path, const char *name) {
|
||||||
|
FILE *image = openImage(path, "r+b");
|
||||||
|
if (image == NULL) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Superblock super;
|
||||||
|
Directory directory;
|
||||||
|
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
char leaf[SBFS_NAME_BYTES + 1];
|
||||||
|
int parent = -1;
|
||||||
|
const char *why = NULL;
|
||||||
|
int failed = 1;
|
||||||
|
if (resolveParent(&directory, name, leaf, &parent, &why)) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" %s.\n", name, why);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
if (!parentIsUsable(&directory, parent, name)) {
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
if (findIn(&directory, leaf, parent) >= 0) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" is already there.\n", name);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
int slot = -1;
|
||||||
|
for (int i = 0; i < directory.entries && slot < 0; i++) {
|
||||||
|
if (!entryInUse(entryAt(&directory, i))) {
|
||||||
|
slot = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (slot < 0) {
|
||||||
|
fprintf(stderr, "Error: The directory is full: %d entries, all taken.\n",
|
||||||
|
directory.entries);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
uint8_t *entry = entryAt(&directory, slot);
|
||||||
|
memset(entry, 0, SBFS_ENTRY_BYTES);
|
||||||
|
entry[SBFS_ENTRY_FLAGS] = SBFS_FLAG_IN_USE | SBFS_FLAG_DIRECTORY;
|
||||||
|
memcpy(entry + SBFS_ENTRY_NAME, leaf, strlen(leaf));
|
||||||
|
entrySetParent(entry, parent);
|
||||||
|
|
||||||
|
int raised = (super.version < SBFS_VERSION_TREE);
|
||||||
|
super.version = SBFS_VERSION_TREE;
|
||||||
|
super.freeBlocks = countFree(&directory, &super);
|
||||||
|
if (writeDirectory(image, &super, &directory) || writeSuperblock(image, &super)) {
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
printf("Made \"%s\" as entry %d.\n", name, slot);
|
||||||
|
if (raised) {
|
||||||
|
printf("The disk is now version %d, because it has a directory on it.\n",
|
||||||
|
SBFS_VERSION_TREE);
|
||||||
|
}
|
||||||
|
failed = 0;
|
||||||
|
done:
|
||||||
|
free(directory.bytes);
|
||||||
|
fclose(image);
|
||||||
|
return failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- rmdir ----
|
||||||
|
//
|
||||||
|
// Refuses a directory with anything in it, and that refusal is not politeness. Parents
|
||||||
|
// are entry indices, and a freed index is handed out again to the next thing put on the
|
||||||
|
// disk - so the children of a deleted directory would reappear inside whatever took its
|
||||||
|
// place. Emptying it first is the only safe order, and making the caller do that is the
|
||||||
|
// smallest way to guarantee it.
|
||||||
|
static int commandRemoveDirectory(const char *path, const char *name) {
|
||||||
|
FILE *image = openImage(path, "r+b");
|
||||||
|
if (image == NULL) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Superblock super;
|
||||||
|
Directory directory;
|
||||||
|
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
|
||||||
|
fclose(image);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int slot = -1;
|
||||||
|
const char *why = NULL;
|
||||||
|
int failed = 1;
|
||||||
|
if (resolve(&directory, name, &slot, &why)) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" %s.\n", name, why);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
if (slot < 0) {
|
||||||
|
fprintf(stderr, "Error: The root is not something that can be removed.\n");
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
if (!entryIsDirectory(entryAt(&directory, slot))) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" is a file. Use delete.\n", name);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
if (directoryHasChildren(&directory, slot)) {
|
||||||
|
fprintf(stderr, "Error: \"%s\" still has things in it. Empty it first.\n", name);
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
memset(entryAt(&directory, slot), 0, SBFS_ENTRY_BYTES);
|
||||||
|
super.freeBlocks = countFree(&directory, &super);
|
||||||
|
if (writeDirectory(image, &super, &directory) || writeSuperblock(image, &super)) {
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
printf("Removed \"%s\".\n", name);
|
||||||
|
failed = 0;
|
||||||
|
done:
|
||||||
|
free(directory.bytes);
|
||||||
|
fclose(image);
|
||||||
|
return failed;
|
||||||
|
}
|
||||||
|
|
||||||
static void printUsage(const char *program) {
|
static void printUsage(const char *program) {
|
||||||
printf("Usage: %s <command> <image> [arguments]\n", program);
|
printf("Usage: %s <command> <image> [arguments]\n", program);
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf("Commands:\n");
|
printf("Commands:\n");
|
||||||
printf(" format <image> [blocks] [dirblocks] Lay down a fresh filesystem.\n");
|
printf(" format <image> [blocks] [dirblocks] Lay down a fresh filesystem.\n");
|
||||||
printf(" list <image> Show what is on the disk.\n");
|
printf(" list <image> [path] Show the disk, or one directory of it.\n");
|
||||||
printf(" put <image> <file> [name] Put a host file onto it.\n");
|
printf(" put <image> <file> [path] Put a host file onto it.\n");
|
||||||
printf(" get <image> <name> [file] Take one off it.\n");
|
printf(" get <image> <path> [file] Take one off it.\n");
|
||||||
printf(" delete <image> <name> Remove one.\n");
|
printf(" delete <image> <path> Remove a file.\n");
|
||||||
|
printf(" mkdir <image> <path> Make a directory.\n");
|
||||||
|
printf(" rmdir <image> <path> Remove an empty one.\n");
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf("Blocks are %d bytes. A name may be %d characters. Without one, put uses the\n",
|
printf("Blocks are %d bytes. A name may be %d characters, and a path is names with\n",
|
||||||
SBFS_BLOCK_BYTES, SBFS_NAME_BYTES);
|
SBFS_BLOCK_BYTES, SBFS_NAME_BYTES);
|
||||||
printf("file's own name, which is often too long, and it will say so.\n");
|
printf("'%c' between them, always from the root. Without a path, put uses the file's\n",
|
||||||
|
SBFS_SEPARATOR);
|
||||||
|
printf("own name, which is often too long, and it will say so.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// The part of a path after the last separator, so that put can default to a file's own
|
// The part of a path after the last separator, so that put can default to a file's own
|
||||||
@@ -518,15 +1192,55 @@ int main(int argc, char *argv[]) {
|
|||||||
if (strcmp(command, "format") == 0) {
|
if (strcmp(command, "format") == 0) {
|
||||||
long blocks = (argc > 3) ? strtol(argv[3], NULL, 0) : 512;
|
long blocks = (argc > 3) ? strtol(argv[3], NULL, 0) : 512;
|
||||||
long directoryBlocks = (argc > 4) ? strtol(argv[4], NULL, 0) : SBFS_DEFAULT_DIRECTORY_BLOCKS;
|
long directoryBlocks = (argc > 4) ? strtol(argv[4], NULL, 0) : SBFS_DEFAULT_DIRECTORY_BLOCKS;
|
||||||
|
// Blocks in EACH boot slot, and there are two of them. Left off, a disk gets no
|
||||||
|
// boot area at all, which is what every disk made before this had.
|
||||||
|
long bootBlocks = (argc > 5) ? strtol(argv[5], NULL, 0) : 0;
|
||||||
if (blocks < 2 || blocks > 0xFFFF || directoryBlocks < 1 || directoryBlocks > 0xFFFF) {
|
if (blocks < 2 || blocks > 0xFFFF || directoryBlocks < 1 || directoryBlocks > 0xFFFF) {
|
||||||
fprintf(stderr, "Error: A disk is between 2 and 65535 blocks, with at least"
|
fprintf(stderr, "Error: A disk is between 2 and 65535 blocks, with at least"
|
||||||
" one of directory.\n");
|
" one of directory.\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return commandFormat(path, (uint16_t)blocks, (uint16_t)directoryBlocks);
|
if (bootBlocks < 0 || bootBlocks * SBFS_BOOT_SLOTS > 0xFFFE) {
|
||||||
|
fprintf(stderr, "Error: A boot slot is between 0 and %d blocks, and there are"
|
||||||
|
" two of them.\n", 0xFFFE / SBFS_BOOT_SLOTS);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return commandFormat(path, (uint16_t)blocks, (uint16_t)directoryBlocks,
|
||||||
|
(uint16_t)bootBlocks);
|
||||||
}
|
}
|
||||||
if (strcmp(command, "list") == 0) {
|
if (strcmp(command, "list") == 0) {
|
||||||
return commandList(path);
|
return commandList(path, (argc > 3) ? argv[3] : NULL);
|
||||||
|
}
|
||||||
|
if (strcmp(command, "mkdir") == 0) {
|
||||||
|
if (argc < 4) {
|
||||||
|
fprintf(stderr, "Error: mkdir needs a directory to make.\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return commandMakeDirectory(path, argv[3]);
|
||||||
|
}
|
||||||
|
if (strcmp(command, "rmdir") == 0) {
|
||||||
|
if (argc < 4) {
|
||||||
|
fprintf(stderr, "Error: rmdir needs a directory to remove.\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return commandRemoveDirectory(path, argv[3]);
|
||||||
|
}
|
||||||
|
if (strcmp(command, "boot") == 0) {
|
||||||
|
if (argc < 4) {
|
||||||
|
fprintf(stderr, "Error: boot needs a file to write into a slot.\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return commandBoot(path, argv[3], (argc > 4) ? strtol(argv[4], NULL, 0) : 0);
|
||||||
|
}
|
||||||
|
if (strcmp(command, "bootstate") == 0) {
|
||||||
|
return commandBootState(path, (argc > 3) ? argv[3] : NULL);
|
||||||
|
}
|
||||||
|
if (strcmp(command, "bootslot") == 0) {
|
||||||
|
if (argc < 4) {
|
||||||
|
fprintf(stderr, "Error: bootslot needs the slot to start from.\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return commandBootSlot(path, strtol(argv[3], NULL, 0));
|
||||||
}
|
}
|
||||||
if (strcmp(command, "put") == 0) {
|
if (strcmp(command, "put") == 0) {
|
||||||
if (argc < 4) {
|
if (argc < 4) {
|
||||||
|
|||||||
+139
-6
@@ -1,5 +1,5 @@
|
|||||||
// sbfs.h
|
// sbfs.h
|
||||||
// The SplitBit Filesystem, version one.
|
// The SplitBit Filesystem, version two.
|
||||||
//
|
//
|
||||||
// This is the host side's copy of the format. The other implementation is SplitBit
|
// This is the host side's copy of the format. The other implementation is SplitBit
|
||||||
// assembly running on the machine itself, so nothing can be shared between them except
|
// assembly running on the machine itself, so nothing can be shared between them except
|
||||||
@@ -19,7 +19,26 @@
|
|||||||
|
|
||||||
#define SBFS_MAGIC "SBFS"
|
#define SBFS_MAGIC "SBFS"
|
||||||
#define SBFS_MAGIC_BYTES 4
|
#define SBFS_MAGIC_BYTES 4
|
||||||
#define SBFS_VERSION 1
|
|
||||||
|
// ---- Versions ----
|
||||||
|
//
|
||||||
|
// Version two adds directories, and adds them without moving a single byte that version
|
||||||
|
// one had defined: the parent lives in two of the four bytes each entry already reserved,
|
||||||
|
// and a directory is a flag bit in a byte that was using one of its eight.
|
||||||
|
//
|
||||||
|
// A VERSION ONE DISK IS ALREADY A VALID VERSION TWO DISK. The parent field is written as
|
||||||
|
// the entry's index PLUS ONE, so that the zero a version one disk has in those reserved
|
||||||
|
// bytes reads as "in the root" - which is exactly what every file on a flat disk is in.
|
||||||
|
// There is nothing to convert and no tool to convert it with.
|
||||||
|
//
|
||||||
|
// Compatibility therefore runs one way, which is the ordinary shape of it: this reads
|
||||||
|
// either version, and version one code reading a version two disk would list directories
|
||||||
|
// as strange empty files. A disk is written back at the version it was read at, and is
|
||||||
|
// only raised to two by the thing that makes the difference real - the first directory
|
||||||
|
// created on it.
|
||||||
|
#define SBFS_VERSION_FLAT 1
|
||||||
|
#define SBFS_VERSION_TREE 2
|
||||||
|
#define SBFS_VERSION SBFS_VERSION_TREE
|
||||||
|
|
||||||
#define SBFS_BLOCK_BYTES 256
|
#define SBFS_BLOCK_BYTES 256
|
||||||
|
|
||||||
@@ -32,13 +51,66 @@
|
|||||||
// 8 2 First directory block
|
// 8 2 First directory block
|
||||||
// 10 2 Blocks the directory occupies
|
// 10 2 Blocks the directory occupies
|
||||||
// 12 2 Free blocks, a cache rather than the authority
|
// 12 2 Free blocks, a cache rather than the authority
|
||||||
// 14 Reserved to the end of the block
|
// 14 2 Blocks in each boot slot, or zero for a disk that cannot be booted
|
||||||
|
// 16 1 Which boot slot is live, 0 or 1
|
||||||
|
// 17 1 How the last start went. See below.
|
||||||
|
// 18 Reserved to the end of the block
|
||||||
|
|
||||||
#define SBFS_SUPER_VERSION 4
|
#define SBFS_SUPER_VERSION 4
|
||||||
#define SBFS_SUPER_DISK 6
|
#define SBFS_SUPER_DISK 6
|
||||||
#define SBFS_SUPER_DIRSTART 8
|
#define SBFS_SUPER_DIRSTART 8
|
||||||
#define SBFS_SUPER_DIRBLOCKS 10
|
#define SBFS_SUPER_DIRBLOCKS 10
|
||||||
#define SBFS_SUPER_FREE 12
|
#define SBFS_SUPER_FREE 12
|
||||||
|
#define SBFS_SUPER_BOOTBLOCKS 14
|
||||||
|
#define SBFS_SUPER_BOOTSLOT 16
|
||||||
|
|
||||||
|
// ---- The boot area ----
|
||||||
|
//
|
||||||
|
// Blocks between the superblock and the directory, which the filesystem never allocates
|
||||||
|
// and never sees. Nothing had to be added to make room for them: both implementations
|
||||||
|
// work out the first usable data block as directoryStart + directoryBlocks, and
|
||||||
|
// directoryStart is a field rather than a constant, so moving the directory up reserves
|
||||||
|
// everything below it by arithmetic that was already there.
|
||||||
|
//
|
||||||
|
// A DISK MADE BEFORE THIS HAS ZERO HERE, which reads as "no boot area", which is true.
|
||||||
|
// The same shape as the version two parent field: the value an older disk already holds
|
||||||
|
// is the correct answer rather than something needing conversion.
|
||||||
|
//
|
||||||
|
// TWO SLOTS, ALWAYS, and the reason is that a boot slot is raw blocks. A file being
|
||||||
|
// rewritten is protected by writing a temporary and renaming it, and there is no name
|
||||||
|
// here to rename - so a machine interrupted while updating its only boot slot would not
|
||||||
|
// boot at all, which is the one failure on this disk with no way back. Writing the slot
|
||||||
|
// that is not live and then moving one byte turns that into a machine that boots the
|
||||||
|
// version it had before.
|
||||||
|
//
|
||||||
|
// block 0 the superblock
|
||||||
|
// 1 .. bootBlocks slot 0
|
||||||
|
// bootBlocks+1 .. 2*bootBlocks slot 1
|
||||||
|
// directoryStart .. the directory, and then files
|
||||||
|
// ---- How the last start went ----
|
||||||
|
//
|
||||||
|
// Written by the loader before it hands over and cleared by the system once it is running,
|
||||||
|
// so that a system which never gets that far leaves a mark saying so. THE MARK IS WHAT
|
||||||
|
// MAKES A NEW SYSTEM SAFE TO TRY: without it, pointing boot.cfg at something that crashes
|
||||||
|
// before the prompt is a machine that cannot be told anything ever again.
|
||||||
|
//
|
||||||
|
// What clears it is reaching the shell, and that is a deliberate choice of threshold. It
|
||||||
|
// does not mean the system works - a shell can be reached by something that is broken in
|
||||||
|
// every other way. It means A PERSON HAS CONTROL AGAIN, which is exactly what the fallback
|
||||||
|
// exists to restore and therefore exactly when it has done its job.
|
||||||
|
//
|
||||||
|
// 0 Settled. The last start finished. Start what the configuration says.
|
||||||
|
// 1 Trying. The loader handed over and nothing came back to say it arrived.
|
||||||
|
// 2 Fell back. A try failed and the fallback was used instead. Stays until somebody
|
||||||
|
// settles it, so that a system which crashes is not retried every other
|
||||||
|
// boot for ever.
|
||||||
|
#define SBFS_SUPER_BOOTSTATE 17
|
||||||
|
#define SBFS_BOOT_SETTLED 0
|
||||||
|
#define SBFS_BOOT_TRYING 1
|
||||||
|
#define SBFS_BOOT_FELLBACK 2
|
||||||
|
|
||||||
|
#define SBFS_BOOT_SLOTS 2
|
||||||
|
#define SBFS_FIRST_BOOT_BLOCK 1
|
||||||
|
|
||||||
// ---- Directory entries ----
|
// ---- Directory entries ----
|
||||||
//
|
//
|
||||||
@@ -47,10 +119,22 @@
|
|||||||
// 3 2 Whole blocks the file occupies
|
// 3 2 Whole blocks the file occupies
|
||||||
// 5 1 Bytes in the trailing part block, or zero if there is not one
|
// 5 1 Bytes in the trailing part block, or zero if there is not one
|
||||||
// 6 22 Name, padded with zeroes
|
// 6 22 Name, padded with zeroes
|
||||||
// 28 4 Reserved
|
// 28 2 Parent, as an entry index plus one. Zero is the root. (Version two.)
|
||||||
|
// 30 2 Reserved
|
||||||
//
|
//
|
||||||
// Thirty two divides two hundred and fifty six, so an entry never straddles a block and
|
// Thirty two divides two hundred and fifty six, so an entry never straddles a block and
|
||||||
// reading one never means handling a split.
|
// reading one never means handling a split. Version two did not change that, because it
|
||||||
|
// spent bytes that were already inside the entry.
|
||||||
|
//
|
||||||
|
// A DIRECTORY IS AN ENTRY WITH NO BLOCKS. Its start, blocks and tail are all zero, and it
|
||||||
|
// costs one entry and nothing else. That is what keeps the flat array of entries the
|
||||||
|
// whole allocation map: with files laid down contiguously, every block is inside some
|
||||||
|
// entry's range or it is not, and an entry with no range is in nobody's way.
|
||||||
|
//
|
||||||
|
// Because parents are entry indices, and because nothing ever compacts the directory,
|
||||||
|
// those indices are stable for as long as an entry is in use. DELETING A DIRECTORY THAT
|
||||||
|
// STILL HAS CHILDREN MUST BE REFUSED: the freed index would be handed to some unrelated
|
||||||
|
// file later, and the orphans would reappear inside it.
|
||||||
|
|
||||||
#define SBFS_ENTRY_BYTES 32
|
#define SBFS_ENTRY_BYTES 32
|
||||||
#define SBFS_ENTRIES_PER_BLOCK (SBFS_BLOCK_BYTES / SBFS_ENTRY_BYTES)
|
#define SBFS_ENTRIES_PER_BLOCK (SBFS_BLOCK_BYTES / SBFS_ENTRY_BYTES)
|
||||||
@@ -61,8 +145,57 @@
|
|||||||
#define SBFS_ENTRY_TAIL 5
|
#define SBFS_ENTRY_TAIL 5
|
||||||
#define SBFS_ENTRY_NAME 6
|
#define SBFS_ENTRY_NAME 6
|
||||||
#define SBFS_NAME_BYTES 22
|
#define SBFS_NAME_BYTES 22
|
||||||
|
#define SBFS_ENTRY_PARENT 28
|
||||||
|
|
||||||
#define SBFS_FLAG_IN_USE 0x01
|
#define SBFS_FLAG_IN_USE 0x01
|
||||||
|
#define SBFS_FLAG_DIRECTORY 0x02
|
||||||
|
|
||||||
|
// A FILE BEING WRITTEN, WHICH IS NOT YET A FILE. Saving something that already exists is
|
||||||
|
// done by writing a temporary, deleting the original and giving the temporary its name,
|
||||||
|
// so that nothing is lost if the writing fails. The temporary has to be an ordinary entry
|
||||||
|
// while that happens - it holds real blocks and needs a name - and the only thing that
|
||||||
|
// distinguishes it from a finished file is that nobody has committed it yet.
|
||||||
|
//
|
||||||
|
// That is not a property of its contents. The same bytes become the real file the moment
|
||||||
|
// the rename lands, so there is nothing to put inside it that would be true; it belongs
|
||||||
|
// in the entry, which is the thing the commit changes. It was formerly told apart by
|
||||||
|
// being called "sbfs.part" or "sbfs.out", and those are legal names a user may also
|
||||||
|
// choose, so cleaning up by name could delete somebody's file.
|
||||||
|
//
|
||||||
|
// Cleared as part of committing. An entry still carrying it is the wreckage of a write
|
||||||
|
// that stopped, and its blocks are spoken for until something clears it up.
|
||||||
|
#define SBFS_FLAG_TEMPORARY 0x04
|
||||||
|
|
||||||
|
// The root is not an entry. It is the absence of a parent, written as zero, which is why
|
||||||
|
// the field is an index plus one and why a freshly zeroed entry is already in the root.
|
||||||
|
#define SBFS_PARENT_ROOT 0
|
||||||
|
#define SBFS_PARENT_OF(index) ((uint16_t)((index) + 1))
|
||||||
|
#define SBFS_PARENT_INDEX(parent) ((int)(parent) - 1)
|
||||||
|
|
||||||
|
// ---- How big a directory may be ----
|
||||||
|
//
|
||||||
|
// The parent is an index plus one in sixteen bits, so index 65535 has no representation:
|
||||||
|
// adding one wraps to zero, and zero is the root. An entry that cannot be named as a
|
||||||
|
// parent is a directory that cannot hold anything, and it does not fail by refusing.
|
||||||
|
//
|
||||||
|
// WHAT IT DOES INSTEAD IS WORSE THAN FAILING. Creating something inside it writes a
|
||||||
|
// parent of zero, so the thing lands in the root while the tool reports the path it was
|
||||||
|
// asked for. Looking in that directory afterwards finds nothing, because the search is
|
||||||
|
// for a parent of 65536 and the entry says zero - so the same create succeeds again, and
|
||||||
|
// again, piling up entries of one name in the root. Duplicate names in one directory are
|
||||||
|
// the one thing rename refuses outright, on the grounds that a search answers with
|
||||||
|
// whichever it meets first and the rest can never be reached; this manufactured them.
|
||||||
|
//
|
||||||
|
// Eight entries to a block, and 65535 entries is the most that leaves every index one
|
||||||
|
// short of the wrap. 8191 blocks gives 65528 of them, which is the last whole block that
|
||||||
|
// fits. Checked when formatting and again when reading, because a disk claiming more may
|
||||||
|
// have been made by something that never checked at all.
|
||||||
|
#define SBFS_MAX_DIRECTORY_BLOCKS 8191
|
||||||
|
#define SBFS_MAX_ENTRIES (SBFS_MAX_DIRECTORY_BLOCKS * SBFS_ENTRIES_PER_BLOCK)
|
||||||
|
|
||||||
|
// Paths are separated by this, and a leading one means "from the root". A name may not
|
||||||
|
// contain it, which is what makes a path unambiguous without any quoting.
|
||||||
|
#define SBFS_SEPARATOR '/'
|
||||||
|
|
||||||
// The directory begins at block 1 and is this many blocks unless told otherwise, which
|
// The directory begins at block 1 and is this many blocks unless told otherwise, which
|
||||||
// is sixty four files. The superblock carries the real number, so this is only what a
|
// is sixty four files. The superblock carries the real number, so this is only what a
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "bootstrap.h"
|
#include "bootstrap.h"
|
||||||
#include "../Assembler/assembly.h" // For the boot image format, which both tools share.
|
#include "../Assembler/assembly.h" // For the boot image format, which both tools share.
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdint.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
// Reads a number of the given width, most significant byte first.
|
// Reads a number of the given width, most significant byte first.
|
||||||
@@ -144,18 +145,46 @@ static uint8_t readVectorSegment(FILE *file, uint8_t *Program) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Program Segment must come first, then the Data Segment. Short circuiting here means
|
||||||
|
// there is one exit, and so only one place that has to close whatever it was reading.
|
||||||
|
static uint8_t readImage(FILE *file, uint8_t *Program, uint8_t *Data) {
|
||||||
|
return readFileHeader(file)
|
||||||
|
|| readSegment(file, "PRG", Program)
|
||||||
|
|| readSegment(file, "DAT", Data)
|
||||||
|
|| readVectorSegment(file, Program);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Waking up in ROM ----
|
||||||
|
//
|
||||||
|
// The same reader as a named file, given the bytes instead of a path, because a ROM is a
|
||||||
|
// boot image and there is no reason for the machine to have two ways of understanding one.
|
||||||
|
//
|
||||||
|
// THIS IS SHADOWING, which is a real technique rather than a convenience: reset copies the
|
||||||
|
// ROM into Program Memory, including its boot vector, and the CPU then does exactly what it
|
||||||
|
// has always done - reads the boot vector and starts where it points. Nothing about the CPU
|
||||||
|
// changes to make a machine that starts itself.
|
||||||
|
//
|
||||||
|
// And because it is a copy rather than a mapping, the bytes are ordinary Program Memory
|
||||||
|
// once stage one has jumped away. The system may write over them; a reset puts them back.
|
||||||
|
uint8_t loadROM(const unsigned char *bytes, unsigned long length,
|
||||||
|
uint8_t *Program, uint8_t *Data) {
|
||||||
|
FILE *file = fmemopen((void *)(uintptr_t)bytes, (size_t)length, "rb");
|
||||||
|
if (file == NULL) {
|
||||||
|
fprintf(stderr, "Error: Couldn't open the boot ROM.\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
uint8_t failed = readImage(file, Program, Data);
|
||||||
|
fclose(file);
|
||||||
|
return failed;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) {
|
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) {
|
||||||
FILE *file = fopen(path, "rb");
|
FILE *file = fopen(path, "rb");
|
||||||
if (file == NULL) {
|
if (file == NULL) {
|
||||||
fprintf(stderr, "Error: Couldn't open file: %s\n", path);
|
fprintf(stderr, "Error: Couldn't open file: %s\n", path);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
// The Program Segment must come first, then the Data Segment. Short circuiting
|
uint8_t failed = readImage(file, Program, Data);
|
||||||
// here means there is one exit, and so only one place that has to close the file.
|
|
||||||
uint8_t failed = readFileHeader(file)
|
|
||||||
|| readSegment(file, "PRG", Program)
|
|
||||||
|| readSegment(file, "DAT", Data)
|
|
||||||
|| readVectorSegment(file, Program);
|
|
||||||
fclose(file);
|
fclose(file);
|
||||||
return failed;
|
return failed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,8 @@
|
|||||||
|
|
||||||
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data);
|
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data);
|
||||||
|
|
||||||
|
// The same, from bytes the emulator carries rather than a file it opens. See loadROM.
|
||||||
|
uint8_t loadROM(const unsigned char *bytes, unsigned long length,
|
||||||
|
uint8_t *Program, uint8_t *Data);
|
||||||
|
|
||||||
#endif // BOOTSTRAP_H
|
#endif // BOOTSTRAP_H
|
||||||
|
|||||||
@@ -152,6 +152,15 @@ static int rangeWritable(uint8_t bank, uint16_t address, uint32_t count) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What the moves below have cost since anybody last asked.
|
||||||
|
static unsigned long pendingCycles = 0;
|
||||||
|
|
||||||
|
unsigned long controllerTakeCycles(void) {
|
||||||
|
unsigned long taken = pendingCycles;
|
||||||
|
pendingCycles = 0;
|
||||||
|
return taken;
|
||||||
|
}
|
||||||
|
|
||||||
static void doBlit(void) {
|
static void doBlit(void) {
|
||||||
uint32_t count = transferLength();
|
uint32_t count = transferLength();
|
||||||
if (!rangeReadable(sourceBank, sourceAddress, count)) {
|
if (!rangeReadable(sourceBank, sourceAddress, count)) {
|
||||||
@@ -166,6 +175,9 @@ static void doBlit(void) {
|
|||||||
// designing against.
|
// designing against.
|
||||||
memmove(banks[destBank].memory + destAddress,
|
memmove(banks[destBank].memory + destAddress,
|
||||||
banks[sourceBank].memory + sourceAddress, count);
|
banks[sourceBank].memory + sourceAddress, count);
|
||||||
|
// A byte read and a byte written. Two banks are two memories and the pair overlaps;
|
||||||
|
// one bank is one memory and they do not. The odd cycle is the pipeline filling.
|
||||||
|
pendingCycles += (sourceBank == destBank) ? 2 * count + 1 : count + 1;
|
||||||
sourceAddress = (uint16_t)(sourceAddress + count);
|
sourceAddress = (uint16_t)(sourceAddress + count);
|
||||||
destAddress = (uint16_t)(destAddress + count);
|
destAddress = (uint16_t)(destAddress + count);
|
||||||
status = 0;
|
status = 0;
|
||||||
@@ -178,6 +190,7 @@ static void doFill(void) {
|
|||||||
}
|
}
|
||||||
// A fill has nowhere to read from, only a value, so SourceLow carries the byte and
|
// A fill has nowhere to read from, only a value, so SourceLow carries the byte and
|
||||||
// the rest of the source registers mean nothing here.
|
// the rest of the source registers mean nothing here.
|
||||||
|
pendingCycles += count + 1;
|
||||||
memset(banks[destBank].memory + destAddress, (int)(sourceAddress & 0xFF), count);
|
memset(banks[destBank].memory + destAddress, (int)(sourceAddress & 0xFF), count);
|
||||||
destAddress = (uint16_t)(destAddress + count);
|
destAddress = (uint16_t)(destAddress + count);
|
||||||
status = 0;
|
status = 0;
|
||||||
@@ -261,6 +274,7 @@ uint8_t controllerWrite(uint8_t value, uint8_t port) {
|
|||||||
// A byte into the destination, and the address steps on so that writing a
|
// A byte into the destination, and the address steps on so that writing a
|
||||||
// run of bytes is a loop over one instruction rather than four.
|
// run of bytes is a loop over one instruction rather than four.
|
||||||
if (canWrite(destBank, destAddress)) {
|
if (canWrite(destBank, destAddress)) {
|
||||||
|
pendingCycles++; // The byte itself, beyond reaching the port.
|
||||||
banks[destBank].memory[destAddress] = value;
|
banks[destBank].memory[destAddress] = value;
|
||||||
if (destBank == BANK_TABLE) {
|
if (destBank == BANK_TABLE) {
|
||||||
// Unreachable while the table is read only, and here so that it stays
|
// Unreachable while the table is read only, and here so that it stays
|
||||||
@@ -321,6 +335,7 @@ uint8_t controllerRead(uint8_t port) {
|
|||||||
if (!canRead(sourceBank, sourceAddress)) {
|
if (!canRead(sourceBank, sourceAddress)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
pendingCycles++; // As above, the other way round.
|
||||||
uint8_t value = banks[sourceBank].memory[sourceAddress];
|
uint8_t value = banks[sourceBank].memory[sourceAddress];
|
||||||
sourceAddress++;
|
sourceAddress++;
|
||||||
status = 0;
|
status = 0;
|
||||||
|
|||||||
@@ -85,6 +85,25 @@
|
|||||||
// Banks 0 to 2 belong to the machine rather than to any device.
|
// Banks 0 to 2 belong to the machine rather than to any device.
|
||||||
#define BANK_OWNER_MACHINE 0xFF
|
#define BANK_OWNER_MACHINE 0xFF
|
||||||
|
|
||||||
|
// ---- What the controller's work costs ----
|
||||||
|
//
|
||||||
|
// The controller moves memory, and memory takes time to move: a byte has to be read from
|
||||||
|
// somewhere and written somewhere else. A blit is not free just because the machine issues
|
||||||
|
// it with one instruction, and pretending otherwise made a quarter of a millisecond of
|
||||||
|
// work look like ten cycles.
|
||||||
|
//
|
||||||
|
// Banks are separate memories, which is what decides the rate. A move between two of them
|
||||||
|
// can overlap its read and its write - fetch the next byte while the last one is stored -
|
||||||
|
// so it settles at a byte a cycle. A move WITHIN one bank cannot, and costs two. A fill has
|
||||||
|
// nothing to read and costs one whatever the banks are.
|
||||||
|
//
|
||||||
|
// Returned and cleared, so the caller adds it to whatever it is charging for. The CPU picks
|
||||||
|
// it up after each port access, which makes the transfer a stall: the machine issues a blit
|
||||||
|
// and waits for it. Whether real hardware would let the two run at once is a live question -
|
||||||
|
// the memories are separate, so it plausibly could - and the answer wants measuring before
|
||||||
|
// it is designed.
|
||||||
|
unsigned long controllerTakeCycles(void);
|
||||||
|
|
||||||
void initializeController(uint8_t *programMemory, uint8_t *dataMemory);
|
void initializeController(uint8_t *programMemory, uint8_t *dataMemory);
|
||||||
|
|
||||||
uint8_t controllerWrite(uint8_t value, uint8_t port);
|
uint8_t controllerWrite(uint8_t value, uint8_t port);
|
||||||
|
|||||||
+277
-85
@@ -5,12 +5,52 @@
|
|||||||
|
|
||||||
#include "cpu.h"
|
#include "cpu.h"
|
||||||
#include "io.h"
|
#include "io.h"
|
||||||
|
#include "controller.h"
|
||||||
#include "../Assembler/assembly.h" // For the vector table layout, which both tools share.
|
#include "../Assembler/assembly.h" // For the vector table layout, which both tools share.
|
||||||
|
|
||||||
uint16_t shiftRegister;
|
uint16_t shiftRegister;
|
||||||
|
|
||||||
// Reads one entry out of a vector table. Most significant byte first, matching the
|
// Reads one entry out of a vector table. Most significant byte first, matching the
|
||||||
// branch instructions and both file formats.
|
// branch instructions and both file formats.
|
||||||
|
// ---- Every touch of memory, and what it costs ----
|
||||||
|
//
|
||||||
|
// One access, one cycle. These exist so that the cost is counted in the one place the
|
||||||
|
// access happens, rather than in a table of per instruction costs kept somewhere else -
|
||||||
|
// a table like that is a second copy of what the code does, and the two drift.
|
||||||
|
static inline uint8_t fetchProgram(CPURegisters *cpu, uint16_t at) {
|
||||||
|
cpu->busCycles++;
|
||||||
|
return cpu->Program[at];
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint8_t readData(CPURegisters *cpu, uint16_t at) {
|
||||||
|
cpu->busCycles++;
|
||||||
|
return cpu->Data[at];
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void writeData(CPURegisters *cpu, uint16_t at, uint8_t value) {
|
||||||
|
cpu->busCycles++;
|
||||||
|
cpu->Data[at] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A device port is reached over the same bus as memory, so it costs the same. That is a
|
||||||
|
// claim about the hardware rather than an observation - a machine could put devices
|
||||||
|
// somewhere faster or slower - and it is the simple thing until there is a reason to say
|
||||||
|
// otherwise.
|
||||||
|
static inline void portOut(CPURegisters *cpu, uint8_t value, uint8_t port) {
|
||||||
|
cpu->busCycles++;
|
||||||
|
OutputHandler(value, port);
|
||||||
|
// And whatever memory that made the controller move. The machine waits for it, which
|
||||||
|
// is the conservative reading: a blit stalls the program that asked for one.
|
||||||
|
cpu->busCycles += controllerTakeCycles();
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint8_t portIn(CPURegisters *cpu, uint8_t port) {
|
||||||
|
cpu->busCycles++;
|
||||||
|
uint8_t value = InputHandler(port);
|
||||||
|
cpu->busCycles += controllerTakeCycles();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
static uint16_t readVector(const uint8_t *programMemory, uint16_t base, uint8_t index) {
|
static uint16_t readVector(const uint8_t *programMemory, uint16_t base, uint8_t index) {
|
||||||
uint16_t address = base + (uint16_t)index * VECTOR_ENTRY_BYTES;
|
uint16_t address = base + (uint16_t)index * VECTOR_ENTRY_BYTES;
|
||||||
return ((uint16_t)programMemory[address] << 8) | (uint16_t)programMemory[address + 1];
|
return ((uint16_t)programMemory[address] << 8) | (uint16_t)programMemory[address + 1];
|
||||||
@@ -38,23 +78,23 @@ static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index, u
|
|||||||
}
|
}
|
||||||
// Order mirrors genericCall exactly: low byte then high byte, lowest numbered Data
|
// Order mirrors genericCall exactly: low byte then high byte, lowest numbered Data
|
||||||
// Pointer first, so that anything walking the Stack sees a familiar shape.
|
// Pointer first, so that anything walking the Stack sees a familiar shape.
|
||||||
cpu->Data[cpu->StackPointer] = resumeAddress & 0xFF;
|
writeData(cpu, cpu->StackPointer, resumeAddress & 0xFF);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
cpu->Data[cpu->StackPointer] = (resumeAddress >> 8) & 0xFF;
|
writeData(cpu, cpu->StackPointer, (resumeAddress >> 8) & 0xFF);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
for (int i = 0; i < DATA_POINTERS; i++) {
|
for (int i = 0; i < DATA_POINTERS; i++) {
|
||||||
cpu->Data[cpu->StackPointer] = cpu->DataPointer[i] & 0xFF;
|
writeData(cpu, cpu->StackPointer, cpu->DataPointer[i] & 0xFF);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
cpu->Data[cpu->StackPointer] = (cpu->DataPointer[i] >> 8) & 0xFF;
|
writeData(cpu, cpu->StackPointer, (cpu->DataPointer[i] >> 8) & 0xFF);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
}
|
}
|
||||||
cpu->Data[cpu->StackPointer] = cpu->B;
|
writeData(cpu, cpu->StackPointer, cpu->B);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
cpu->Data[cpu->StackPointer] = cpu->A;
|
writeData(cpu, cpu->StackPointer, cpu->A);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
cpu->Data[cpu->StackPointer] = cpu->Q;
|
writeData(cpu, cpu->StackPointer, cpu->Q);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
cpu->Data[cpu->StackPointer] = cpu->Status;
|
writeData(cpu, cpu->StackPointer, cpu->Status);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
// A handler runs with hardware interrupts held off unless it says otherwise, so an
|
// A handler runs with hardware interrupts held off unless it says otherwise, so an
|
||||||
// interrupt cannot arrive inside the handler for another one and grow the Stack
|
// interrupt cannot arrive inside the handler for another one and grow the Stack
|
||||||
@@ -103,6 +143,9 @@ void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemor
|
|||||||
cpu->StackPointer = 0xFFFF;
|
cpu->StackPointer = 0xFFFF;
|
||||||
cpu->Program = programMemory;
|
cpu->Program = programMemory;
|
||||||
cpu->Data = dataMemory;
|
cpu->Data = dataMemory;
|
||||||
|
cpu->busCycles = 0;
|
||||||
|
cpu->idleCycles = 0;
|
||||||
|
cpu->Waiting = 0;
|
||||||
cpu->Fault = FAULT_NONE;
|
cpu->Fault = FAULT_NONE;
|
||||||
cpu->FaultVector = 0;
|
cpu->FaultVector = 0;
|
||||||
}
|
}
|
||||||
@@ -112,33 +155,33 @@ void genericBranch(CPURegisters *cpu){
|
|||||||
// Byte order is imporant. Most Significant first, then Least Significant.
|
// Byte order is imporant. Most Significant first, then Least Significant.
|
||||||
cpu->ProgramCounter++; // Move to the next byte. (MSB)
|
cpu->ProgramCounter++; // Move to the next byte. (MSB)
|
||||||
uint16_t DestinationAddress;
|
uint16_t DestinationAddress;
|
||||||
DestinationAddress = (uint16_t)cpu->Program[cpu->ProgramCounter] << 8; // Cast the 8 bit value to a 16 bit value and shifts it up to the high byte.
|
DestinationAddress = (uint16_t)fetchProgram(cpu, cpu->ProgramCounter) << 8; // Cast the 8 bit value to a 16 bit value and shifts it up to the high byte.
|
||||||
cpu->ProgramCounter++; // Move to the next byte. (LSB)
|
cpu->ProgramCounter++; // Move to the next byte. (LSB)
|
||||||
DestinationAddress = DestinationAddress | (uint16_t)cpu->Program[cpu->ProgramCounter]; // Cast the 8 bit value to a 16 bit value and or it to add it to the desination.
|
DestinationAddress = DestinationAddress | (uint16_t)fetchProgram(cpu, cpu->ProgramCounter); // Cast the 8 bit value to a 16 bit value and or it to add it to the desination.
|
||||||
cpu->ProgramCounter = DestinationAddress-1;
|
cpu->ProgramCounter = DestinationAddress-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void genericCall(CPURegisters *cpu){
|
void genericCall(CPURegisters *cpu){
|
||||||
// Order, low byte, high byte
|
// Order, low byte, high byte
|
||||||
cpu->Data[cpu->StackPointer] = cpu->ProgramCounter & 0xFF;
|
writeData(cpu, cpu->StackPointer, cpu->ProgramCounter & 0xFF);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
cpu->Data[cpu->StackPointer] = (cpu->ProgramCounter >> 8) & 0xFF;
|
writeData(cpu, cpu->StackPointer, (cpu->ProgramCounter >> 8) & 0xFF);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
// Push the preserved Data Pointers to the Stack, lowest numbered first.
|
// Push the preserved Data Pointers to the Stack, lowest numbered first.
|
||||||
// Order within each one, low byte, high byte.
|
// Order within each one, low byte, high byte.
|
||||||
// The pointers above PRESERVED_DATA_POINTERS are deliberately left alone, so a
|
// The pointers above PRESERVED_DATA_POINTERS are deliberately left alone, so a
|
||||||
// subroutine can use one to hand an address back to whoever called it.
|
// subroutine can use one to hand an address back to whoever called it.
|
||||||
for (int i = 0; i < PRESERVED_DATA_POINTERS; i++) {
|
for (int i = 0; i < PRESERVED_DATA_POINTERS; i++) {
|
||||||
cpu->Data[cpu->StackPointer] = cpu->DataPointer[i] & 0xFF;
|
writeData(cpu, cpu->StackPointer, cpu->DataPointer[i] & 0xFF);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
cpu->Data[cpu->StackPointer] = (cpu->DataPointer[i] >> 8) & 0xFF;
|
writeData(cpu, cpu->StackPointer, (cpu->DataPointer[i] >> 8) & 0xFF);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
}
|
}
|
||||||
// Push B to the Stack.
|
// Push B to the Stack.
|
||||||
cpu->Data[cpu->StackPointer] = cpu->B;
|
writeData(cpu, cpu->StackPointer, cpu->B);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
// Push A to the Stack.
|
// Push A to the Stack.
|
||||||
cpu->Data[cpu->StackPointer] = cpu->A;
|
writeData(cpu, cpu->StackPointer, cpu->A);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
// Perform a Generic Branch to the Address.
|
// Perform a Generic Branch to the Address.
|
||||||
genericBranch(cpu);
|
genericBranch(cpu);
|
||||||
@@ -150,7 +193,7 @@ uint16_t *selectDataPointer(CPURegisters *cpu) {
|
|||||||
// rather than rejected, the way a narrow field in hardware would be. It is the
|
// rather than rejected, the way a narrow field in hardware would be. It is the
|
||||||
// assembler's job to refuse to emit one in the first place.
|
// assembler's job to refuse to emit one in the first place.
|
||||||
cpu->ProgramCounter++;
|
cpu->ProgramCounter++;
|
||||||
return &cpu->DataPointer[cpu->Program[cpu->ProgramCounter] & (DATA_POINTERS - 1)];
|
return &cpu->DataPointer[fetchProgram(cpu, cpu->ProgramCounter) & (DATA_POINTERS - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
||||||
@@ -160,7 +203,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
uint16_t result;
|
uint16_t result;
|
||||||
switch(Instruction) {
|
switch(Instruction) {
|
||||||
// 0x - Arithmetic and Logic Operations.
|
// 0x - Arithmetic and Logic Operations.
|
||||||
case 0x00:
|
case 0x10:
|
||||||
// ADD - A + B + Carry -> Q
|
// ADD - A + B + Carry -> Q
|
||||||
result = (uint16_t)cpu->A + (uint16_t)cpu->B + (cpu->Status & STATUS_CARRY);
|
result = (uint16_t)cpu->A + (uint16_t)cpu->B + (cpu->Status & STATUS_CARRY);
|
||||||
if (result > 255) {
|
if (result > 255) {
|
||||||
@@ -170,7 +213,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
}
|
}
|
||||||
cpu->Q = result & 0xFF;
|
cpu->Q = result & 0xFF;
|
||||||
break;
|
break;
|
||||||
case 0x01:
|
case 0x11:
|
||||||
// SUB - A - B - Carry -> Q
|
// SUB - A - B - Carry -> Q
|
||||||
result = (uint16_t)cpu->A - (uint16_t)cpu->B - (cpu->Status & STATUS_CARRY);
|
result = (uint16_t)cpu->A - (uint16_t)cpu->B - (cpu->Status & STATUS_CARRY);
|
||||||
if (result > 255) {
|
if (result > 255) {
|
||||||
@@ -180,34 +223,34 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
}
|
}
|
||||||
cpu->Q = result & 0xFF;
|
cpu->Q = result & 0xFF;
|
||||||
break;
|
break;
|
||||||
case 0x02:
|
case 0x12:
|
||||||
// AND - A and B -> Q
|
// AND - A and B -> Q
|
||||||
cpu->Q = cpu->A&cpu->B;
|
cpu->Q = cpu->A&cpu->B;
|
||||||
break;
|
break;
|
||||||
case 0x03:
|
case 0x13:
|
||||||
// OR - A or B -> Q
|
// OR - A or B -> Q
|
||||||
cpu->Q = cpu->A|cpu->B;
|
cpu->Q = cpu->A|cpu->B;
|
||||||
break;
|
break;
|
||||||
case 0x04:
|
case 0x14:
|
||||||
// XOR - A xor B -> Q
|
// XOR - A xor B -> Q
|
||||||
cpu->Q = cpu->A^cpu->B;
|
cpu->Q = cpu->A^cpu->B;
|
||||||
break;
|
break;
|
||||||
case 0x05:
|
case 0x15:
|
||||||
// NOTA - not A -> Q
|
// NOTA - not A -> Q
|
||||||
cpu->Q = ~cpu->A;
|
cpu->Q = ~cpu->A;
|
||||||
break;
|
break;
|
||||||
case 0x06:
|
case 0x16:
|
||||||
// NOTB - not B -> Q
|
// NOTB - not B -> Q
|
||||||
cpu->Q = ~cpu->B;
|
cpu->Q = ~cpu->B;
|
||||||
break;
|
break;
|
||||||
case 0x07:
|
case 0x17:
|
||||||
// SHL - Shift AB left.
|
// SHL - Shift AB left.
|
||||||
shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
|
shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
|
||||||
shiftRegister = (shiftRegister << 1) | (shiftRegister >> 15);
|
shiftRegister = (shiftRegister << 1) | (shiftRegister >> 15);
|
||||||
cpu->A = shiftRegister >> 8;
|
cpu->A = shiftRegister >> 8;
|
||||||
cpu->B = shiftRegister & 0xFF;
|
cpu->B = shiftRegister & 0xFF;
|
||||||
break;
|
break;
|
||||||
case 0x08:
|
case 0x18:
|
||||||
// SHR - Shift AB right.
|
// SHR - Shift AB right.
|
||||||
shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
|
shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
|
||||||
shiftRegister = (shiftRegister >> 1) | (shiftRegister << 15);
|
shiftRegister = (shiftRegister >> 1) | (shiftRegister << 15);
|
||||||
@@ -217,11 +260,11 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
//
|
//
|
||||||
// 1x - Branch Operations:
|
// 1x - Branch Operations:
|
||||||
//
|
//
|
||||||
case 0x10:
|
case 0x60:
|
||||||
// BRI - Branch Immediately
|
// BRI - Branch Immediately
|
||||||
genericBranch(cpu);
|
genericBranch(cpu);
|
||||||
break;
|
break;
|
||||||
case 0x11:
|
case 0x61:
|
||||||
// BRQ - Branch if Q = 0
|
// BRQ - Branch if Q = 0
|
||||||
if(cpu->Q == 0) {
|
if(cpu->Q == 0) {
|
||||||
genericBranch(cpu);
|
genericBranch(cpu);
|
||||||
@@ -230,7 +273,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
cpu->ProgramCounter+=2;
|
cpu->ProgramCounter+=2;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x12:
|
case 0x62:
|
||||||
// BRA - Branch if A = 0
|
// BRA - Branch if A = 0
|
||||||
if(cpu->A == 0) {
|
if(cpu->A == 0) {
|
||||||
genericBranch(cpu);
|
genericBranch(cpu);
|
||||||
@@ -239,7 +282,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
cpu->ProgramCounter+=2;
|
cpu->ProgramCounter+=2;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x13:
|
case 0x63:
|
||||||
// BRB - if B = 0
|
// BRB - if B = 0
|
||||||
if(cpu->B == 0) {
|
if(cpu->B == 0) {
|
||||||
|
|
||||||
@@ -248,7 +291,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
cpu->ProgramCounter+=2;
|
cpu->ProgramCounter+=2;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x14:
|
case 0x64:
|
||||||
// BRC - Do an immediate branch if the Carry Flag is set.
|
// BRC - Do an immediate branch if the Carry Flag is set.
|
||||||
if (cpu->Status & STATUS_CARRY) {
|
if (cpu->Status & STATUS_CARRY) {
|
||||||
genericBranch(cpu);
|
genericBranch(cpu);
|
||||||
@@ -256,7 +299,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
cpu->ProgramCounter+=2;
|
cpu->ProgramCounter+=2;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x15: {
|
case 0x65: {
|
||||||
// BRD - Branch to the address held in a Data Pointer.
|
// BRD - Branch to the address held in a Data Pointer.
|
||||||
// This is the only branch whose destination is not written into the
|
// This is the only branch whose destination is not written into the
|
||||||
// program, which is what makes a table of addresses something a program
|
// program, which is what makes a table of addresses something a program
|
||||||
@@ -266,11 +309,29 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
cpu->ProgramCounter = destination - 1;
|
cpu->ProgramCounter = destination - 1;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x17:
|
case 0x70:
|
||||||
|
// RCAL - Call, pushing nothing but the return address.
|
||||||
|
//
|
||||||
|
// The unsafe one, and it says so in its name. CALL puts A, B and the first
|
||||||
|
// three Data Pointers back the way it found them, which costs ten bytes of
|
||||||
|
// Stack and means a subroutine can only hand anything back through Q, DP3 or
|
||||||
|
// memory. RCAL costs two bytes and puts nothing back at all: everything the
|
||||||
|
// callee touches, the caller has lost.
|
||||||
|
//
|
||||||
|
// It must be returned from with RRET. The two frames are different sizes, so
|
||||||
|
// returning from one through the other walks the Stack to somewhere that was
|
||||||
|
// never a return address.
|
||||||
|
writeData(cpu, cpu->StackPointer, cpu->ProgramCounter & 0xFF);
|
||||||
|
cpu->StackPointer--;
|
||||||
|
writeData(cpu, cpu->StackPointer, (cpu->ProgramCounter >> 8) & 0xFF);
|
||||||
|
cpu->StackPointer--;
|
||||||
|
genericBranch(cpu);
|
||||||
|
break;
|
||||||
|
case 0x71:
|
||||||
// CALL - Push the Program Counter to the Stack, and perform an immediate branch.
|
// CALL - Push the Program Counter to the Stack, and perform an immediate branch.
|
||||||
genericCall(cpu);
|
genericCall(cpu);
|
||||||
break;
|
break;
|
||||||
case 0x1A:
|
case 0x66:
|
||||||
// BNQ - Branch if Q is not 0.
|
// BNQ - Branch if Q is not 0.
|
||||||
if(cpu->Q != 0) {
|
if(cpu->Q != 0) {
|
||||||
genericBranch(cpu);
|
genericBranch(cpu);
|
||||||
@@ -278,7 +339,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
cpu->ProgramCounter+=2;
|
cpu->ProgramCounter+=2;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x1B:
|
case 0x67:
|
||||||
// BNA - Branch if A is not 0.
|
// BNA - Branch if A is not 0.
|
||||||
if(cpu->A != 0) {
|
if(cpu->A != 0) {
|
||||||
genericBranch(cpu);
|
genericBranch(cpu);
|
||||||
@@ -286,7 +347,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
cpu->ProgramCounter+=2;
|
cpu->ProgramCounter+=2;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x1C:
|
case 0x68:
|
||||||
// BNB - Branch if B is not 0.
|
// BNB - Branch if B is not 0.
|
||||||
if(cpu->B != 0) {
|
if(cpu->B != 0) {
|
||||||
genericBranch(cpu);
|
genericBranch(cpu);
|
||||||
@@ -294,7 +355,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
cpu->ProgramCounter+=2;
|
cpu->ProgramCounter+=2;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x1D:
|
case 0x69:
|
||||||
// BNC - Branch if the Carry Flag is clear.
|
// BNC - Branch if the Carry Flag is clear.
|
||||||
if (!(cpu->Status & STATUS_CARRY)) {
|
if (!(cpu->Status & STATUS_CARRY)) {
|
||||||
genericBranch(cpu);
|
genericBranch(cpu);
|
||||||
@@ -302,13 +363,13 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
cpu->ProgramCounter+=2;
|
cpu->ProgramCounter+=2;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x18: {
|
case 0x72: {
|
||||||
// SWI - Software Interrupt. The byte after the opcode names the vector.
|
// SWI - Software Interrupt. The byte after the opcode names the vector.
|
||||||
// Never masked: this is an instruction the program deliberately ran, not
|
// Never masked: this is an instruction the program deliberately ran, not
|
||||||
// something a device asked for.
|
// something a device asked for.
|
||||||
uint16_t site = cpu->ProgramCounter;
|
uint16_t site = cpu->ProgramCounter;
|
||||||
cpu->ProgramCounter++;
|
cpu->ProgramCounter++;
|
||||||
uint8_t vector = cpu->Program[cpu->ProgramCounter];
|
uint8_t vector = fetchProgram(cpu, cpu->ProgramCounter);
|
||||||
// Execution resumes after the operand, which the Program Counter is sitting
|
// Execution resumes after the operand, which the Program Counter is sitting
|
||||||
// on, so the resume address is one further on than that.
|
// on, so the resume address is one further on than that.
|
||||||
if (enterInterrupt(cpu, SOFTWARE_VECTOR_BASE, vector, cpu->ProgramCounter + 1)) {
|
if (enterInterrupt(cpu, SOFTWARE_VECTOR_BASE, vector, cpu->ProgramCounter + 1)) {
|
||||||
@@ -317,55 +378,111 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
cpu->ProgramCounter = site - 1;
|
cpu->ProgramCounter = site - 1;
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
case 0x19: {
|
case 0x73: {
|
||||||
// RETI - Return from an interrupt. Pops the frame in the exact reverse of
|
// RETI - Return from an interrupt. Pops the frame in the exact reverse of
|
||||||
// the order enterInterrupt pushed it.
|
// the order enterInterrupt pushed it.
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
cpu->Status = cpu->Data[cpu->StackPointer];
|
cpu->Status = readData(cpu, cpu->StackPointer);
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
cpu->Q = cpu->Data[cpu->StackPointer];
|
cpu->Q = readData(cpu, cpu->StackPointer);
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
cpu->A = cpu->Data[cpu->StackPointer];
|
cpu->A = readData(cpu, cpu->StackPointer);
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
cpu->B = cpu->Data[cpu->StackPointer];
|
cpu->B = readData(cpu, cpu->StackPointer);
|
||||||
for (int i = DATA_POINTERS - 1; i >= 0; i--) {
|
for (int i = DATA_POINTERS - 1; i >= 0; i--) {
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
cpu->DataPointer[i] = (uint16_t)cpu->Data[cpu->StackPointer] << 8;
|
cpu->DataPointer[i] = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
cpu->DataPointer[i] |= (uint16_t)cpu->Data[cpu->StackPointer];
|
cpu->DataPointer[i] |= (uint16_t)readData(cpu, cpu->StackPointer);
|
||||||
}
|
}
|
||||||
uint16_t resumeAddress;
|
uint16_t resumeAddress;
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
resumeAddress = (uint16_t)cpu->Data[cpu->StackPointer] << 8;
|
resumeAddress = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
resumeAddress = resumeAddress | (uint16_t)cpu->Data[cpu->StackPointer];
|
resumeAddress = resumeAddress | (uint16_t)readData(cpu, cpu->StackPointer);
|
||||||
// The frame holds the address to carry on from. The Program Counter is
|
// The frame holds the address to carry on from. The Program Counter is
|
||||||
// stepped after every instruction, so land one short of it. RET does the
|
// stepped after every instruction, so land one short of it. RET does the
|
||||||
// same job with its +2, for the same reason.
|
// same job with its +2, for the same reason.
|
||||||
cpu->ProgramCounter = resumeAddress - 1;
|
cpu->ProgramCounter = resumeAddress - 1;
|
||||||
} break;
|
} break;
|
||||||
case 0x1F:
|
case 0x76: {
|
||||||
|
// SRET - Return from a handler that has an answer.
|
||||||
|
//
|
||||||
|
// THE SAME FRAME AS RETI, WITH THE CALL CONVENTION'S RULE APPLIED TO IT. CALL
|
||||||
|
// saves A, B and Data Pointers 0 to 2 and nothing else, which is exactly why
|
||||||
|
// Q and DP3 are how a subroutine hands something back. An interrupt saves all
|
||||||
|
// of it, so a handler with an answer had to reach into its own frame and
|
||||||
|
// un-save two fields by hand - thirty places in CosmOS did that, each of them
|
||||||
|
// knowing the frame's layout by an offset, and all thirty would have gone
|
||||||
|
// quietly wrong the day the frame gained a field.
|
||||||
|
//
|
||||||
|
// So: RETI restores everything and is how a hardware handler says it was never
|
||||||
|
// here. SRET restores what a RET restores and is how a service says it has
|
||||||
|
// replied. The saved Q and DP3 are stepped over and dropped.
|
||||||
|
cpu->StackPointer++;
|
||||||
|
uint8_t savedStatus = readData(cpu, cpu->StackPointer);
|
||||||
|
// The Interrupt Flag, and only that. Entering a handler clears it and the
|
||||||
|
// frame is what puts it back, so dropping the whole byte would leave a service
|
||||||
|
// silently turning interrupts off. Everything else in Status - the carry
|
||||||
|
// above all - is left as the handler leaves it, because that is what a RET
|
||||||
|
// does and the point of this instruction is that there is one rule.
|
||||||
|
cpu->Status = (uint8_t)((cpu->Status & ~STATUS_INTERRUPT)
|
||||||
|
| (savedStatus & STATUS_INTERRUPT));
|
||||||
|
cpu->StackPointer++; // The saved Q, dropped: the handler's answer stands.
|
||||||
|
cpu->StackPointer++;
|
||||||
|
cpu->A = readData(cpu, cpu->StackPointer);
|
||||||
|
cpu->StackPointer++;
|
||||||
|
cpu->B = readData(cpu, cpu->StackPointer);
|
||||||
|
for (int i = DATA_POINTERS - 1; i >= 0; i--) {
|
||||||
|
cpu->StackPointer++;
|
||||||
|
uint16_t high = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
|
||||||
|
cpu->StackPointer++;
|
||||||
|
uint16_t low = (uint16_t)readData(cpu, cpu->StackPointer);
|
||||||
|
// Data Pointer 3 is stepped over for the same reason as Q. The other three
|
||||||
|
// come back, exactly as a RET brings them back.
|
||||||
|
if (i != DATA_POINTERS - 1) {
|
||||||
|
cpu->DataPointer[i] = high | low;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint16_t resumeAddress;
|
||||||
|
cpu->StackPointer++;
|
||||||
|
resumeAddress = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
|
||||||
|
cpu->StackPointer++;
|
||||||
|
resumeAddress = resumeAddress | (uint16_t)readData(cpu, cpu->StackPointer);
|
||||||
|
cpu->ProgramCounter = resumeAddress - 1;
|
||||||
|
} break;
|
||||||
|
case 0x74:
|
||||||
|
// RRET - Return from an RCAL, taking back nothing but the return address.
|
||||||
|
cpu->StackPointer++;
|
||||||
|
cpu->ProgramCounter = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
|
||||||
|
cpu->StackPointer++;
|
||||||
|
cpu->ProgramCounter = cpu->ProgramCounter | (uint16_t)readData(cpu, cpu->StackPointer);
|
||||||
|
// Two on, to step over the address the RCAL branched through, exactly as RET
|
||||||
|
// does. Everything else RET restores, this deliberately does not.
|
||||||
|
cpu->ProgramCounter += 2;
|
||||||
|
break;
|
||||||
|
case 0x75:
|
||||||
// RET - Return from subroutine, restore the registers and set the Program Counter to the Return Address.
|
// RET - Return from subroutine, restore the registers and set the Program Counter to the Return Address.
|
||||||
// Pop A from the Stack.
|
// Pop A from the Stack.
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
cpu->A = cpu->Data[cpu->StackPointer];
|
cpu->A = readData(cpu, cpu->StackPointer);
|
||||||
// Pop B from the Stack.
|
// Pop B from the Stack.
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
cpu->B = cpu->Data[cpu->StackPointer];
|
cpu->B = readData(cpu, cpu->StackPointer);
|
||||||
// Pop the preserved Data Pointers from the Stack. This walks the pointers
|
// Pop the preserved Data Pointers from the Stack. This walks the pointers
|
||||||
// in the opposite order to genericCall, and takes the high byte before the
|
// in the opposite order to genericCall, and takes the high byte before the
|
||||||
// low byte, so that it exactly mirrors the way they were pushed.
|
// low byte, so that it exactly mirrors the way they were pushed.
|
||||||
for (int i = PRESERVED_DATA_POINTERS - 1; i >= 0; i--) {
|
for (int i = PRESERVED_DATA_POINTERS - 1; i >= 0; i--) {
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
cpu->DataPointer[i] = (uint16_t)cpu->Data[cpu->StackPointer] << 8;
|
cpu->DataPointer[i] = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
cpu->DataPointer[i] |= (uint16_t)cpu->Data[cpu->StackPointer];
|
cpu->DataPointer[i] |= (uint16_t)readData(cpu, cpu->StackPointer);
|
||||||
}
|
}
|
||||||
// Pop the Return Address from the Stack.
|
// Pop the Return Address from the Stack.
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
cpu->ProgramCounter = (uint16_t)cpu->Data[cpu->StackPointer] << 8;
|
cpu->ProgramCounter = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
cpu->ProgramCounter = cpu->ProgramCounter | (uint16_t)cpu->Data[cpu->StackPointer];
|
cpu->ProgramCounter = cpu->ProgramCounter | (uint16_t)readData(cpu, cpu->StackPointer);
|
||||||
// Add 2 to the Program Counter to skip over the address when it returns.
|
// Add 2 to the Program Counter to skip over the address when it returns.
|
||||||
cpu->ProgramCounter += 2;
|
cpu->ProgramCounter += 2;
|
||||||
|
|
||||||
@@ -424,12 +541,12 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
case 0x26:
|
case 0x26:
|
||||||
// INIA - Initialize A Immediately from Program Memory.
|
// INIA - Initialize A Immediately from Program Memory.
|
||||||
cpu->ProgramCounter++;
|
cpu->ProgramCounter++;
|
||||||
cpu->A = cpu->Program[cpu->ProgramCounter];
|
cpu->A = fetchProgram(cpu, cpu->ProgramCounter);
|
||||||
break;
|
break;
|
||||||
case 0x27:
|
case 0x27:
|
||||||
// INIB - Initialize A Immediately from Program Memory.
|
// INIB - Initialize A Immediately from Program Memory.
|
||||||
cpu->ProgramCounter++;
|
cpu->ProgramCounter++;
|
||||||
cpu->B = cpu->Program[cpu->ProgramCounter];
|
cpu->B = fetchProgram(cpu, cpu->ProgramCounter);
|
||||||
break;
|
break;
|
||||||
case 0x28:
|
case 0x28:
|
||||||
// CCF - Clear the Carry Flag.
|
// CCF - Clear the Carry Flag.
|
||||||
@@ -458,17 +575,17 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
//
|
//
|
||||||
case 0x30:
|
case 0x30:
|
||||||
// PSHQ - Push Q to the Stack.
|
// PSHQ - Push Q to the Stack.
|
||||||
cpu->Data[cpu->StackPointer] = cpu->Q;
|
writeData(cpu, cpu->StackPointer, cpu->Q);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
break;
|
break;
|
||||||
case 0x31:
|
case 0x31:
|
||||||
// PSHA - Push A to the Stack.
|
// PSHA - Push A to the Stack.
|
||||||
cpu->Data[cpu->StackPointer] = cpu->A;
|
writeData(cpu, cpu->StackPointer, cpu->A);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
break;
|
break;
|
||||||
case 0x32:
|
case 0x32:
|
||||||
// PSHB - Push B to the Stack.
|
// PSHB - Push B to the Stack.
|
||||||
cpu->Data[cpu->StackPointer] = cpu->B;
|
writeData(cpu, cpu->StackPointer, cpu->B);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
break;
|
break;
|
||||||
case 0x33: {
|
case 0x33: {
|
||||||
@@ -476,30 +593,30 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
// Order, high byte, low byte
|
// Order, high byte, low byte
|
||||||
// This ordering makes it easier to add offsets with register math.
|
// This ordering makes it easier to add offsets with register math.
|
||||||
uint16_t pushed = *selectDataPointer(cpu);
|
uint16_t pushed = *selectDataPointer(cpu);
|
||||||
cpu->Data[cpu->StackPointer] = (pushed >> 8) & 0xFF;
|
writeData(cpu, cpu->StackPointer, (pushed >> 8) & 0xFF);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
cpu->Data[cpu->StackPointer] = pushed & 0xFF;
|
writeData(cpu, cpu->StackPointer, pushed & 0xFF);
|
||||||
cpu->StackPointer--;
|
cpu->StackPointer--;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x34:
|
case 0x34:
|
||||||
// POPA - Pop A from the Stack.
|
// POPA - Pop A from the Stack.
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
cpu->A = cpu->Data[cpu->StackPointer];
|
cpu->A = readData(cpu, cpu->StackPointer);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 0x35:
|
case 0x35:
|
||||||
// POPB - Pop B from the Stack.
|
// POPB - Pop B from the Stack.
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
cpu->B = cpu->Data[cpu->StackPointer];
|
cpu->B = readData(cpu, cpu->StackPointer);
|
||||||
break;
|
break;
|
||||||
case 0x36: {
|
case 0x36: {
|
||||||
// POPD - Pop a Data Address from the Stack into the selected Data Pointer.
|
// POPD - Pop a Data Address from the Stack into the selected Data Pointer.
|
||||||
uint16_t *popped = selectDataPointer(cpu);
|
uint16_t *popped = selectDataPointer(cpu);
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
*popped = (uint16_t)cpu->Data[cpu->StackPointer];
|
*popped = (uint16_t)readData(cpu, cpu->StackPointer);
|
||||||
cpu->StackPointer++;
|
cpu->StackPointer++;
|
||||||
*popped |= (uint16_t)cpu->Data[cpu->StackPointer] << 8;
|
*popped |= (uint16_t)readData(cpu, cpu->StackPointer) << 8;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
//
|
//
|
||||||
@@ -515,32 +632,32 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
break;
|
break;
|
||||||
case 0x42:
|
case 0x42:
|
||||||
// LDA - Load A from Data.
|
// LDA - Load A from Data.
|
||||||
cpu->A = cpu->Data[*selectDataPointer(cpu)];
|
cpu->A = readData(cpu, *selectDataPointer(cpu));
|
||||||
break;
|
break;
|
||||||
case 0x43:
|
case 0x43:
|
||||||
// LDB - Load B from Data.
|
// LDB - Load B from Data.
|
||||||
cpu->B = cpu->Data[*selectDataPointer(cpu)];
|
cpu->B = readData(cpu, *selectDataPointer(cpu));
|
||||||
break;
|
break;
|
||||||
case 0x44:
|
case 0x44:
|
||||||
// STQ - Store Q into Data.
|
// STQ - Store Q into Data.
|
||||||
cpu->Data[*selectDataPointer(cpu)] = cpu->Q;
|
writeData(cpu, *selectDataPointer(cpu), cpu->Q);
|
||||||
break;
|
break;
|
||||||
case 0x45:
|
case 0x45:
|
||||||
// STA - Store A into Data.
|
// STA - Store A into Data.
|
||||||
cpu->Data[*selectDataPointer(cpu)] = cpu->A;
|
writeData(cpu, *selectDataPointer(cpu), cpu->A);
|
||||||
break;
|
break;
|
||||||
case 0x46:
|
case 0x46:
|
||||||
// STB - Store B into Data.
|
// STB - Store B into Data.
|
||||||
cpu->Data[*selectDataPointer(cpu)] = cpu->B;
|
writeData(cpu, *selectDataPointer(cpu), cpu->B);
|
||||||
break;
|
break;
|
||||||
case 0x47: {
|
case 0x47: {
|
||||||
// SETD - Set the selected Data Pointer.
|
// SETD - Set the selected Data Pointer.
|
||||||
uint16_t *destination = selectDataPointer(cpu);
|
uint16_t *destination = selectDataPointer(cpu);
|
||||||
cpu->ProgramCounter++;
|
cpu->ProgramCounter++;
|
||||||
uint16_t Address;
|
uint16_t Address;
|
||||||
Address = (uint16_t)cpu->Program[cpu->ProgramCounter] << 8; // Cast the 8 bits to a 16 bit value and shift them to the high byte.
|
Address = (uint16_t)fetchProgram(cpu, cpu->ProgramCounter) << 8; // Cast the 8 bits to a 16 bit value and shift them to the high byte.
|
||||||
cpu->ProgramCounter++;
|
cpu->ProgramCounter++;
|
||||||
Address |= (uint16_t)cpu->Program[cpu->ProgramCounter];
|
Address |= (uint16_t)fetchProgram(cpu, cpu->ProgramCounter);
|
||||||
*destination = Address;
|
*destination = Address;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -548,14 +665,44 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
// DPUP - Offset the selected Data Pointer up by the value of the next byte of Program Memory.
|
// DPUP - Offset the selected Data Pointer up by the value of the next byte of Program Memory.
|
||||||
uint16_t *target = selectDataPointer(cpu);
|
uint16_t *target = selectDataPointer(cpu);
|
||||||
cpu->ProgramCounter++;
|
cpu->ProgramCounter++;
|
||||||
*target += cpu->Program[cpu->ProgramCounter];
|
*target += fetchProgram(cpu, cpu->ProgramCounter);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x49: {
|
case 0x49: {
|
||||||
// DPDN - Offset the selected Data Pointer down by the value of the next byte of Program Memory.
|
// DPDN - Offset the selected Data Pointer down by the value of the next byte of Program Memory.
|
||||||
uint16_t *target = selectDataPointer(cpu);
|
uint16_t *target = selectDataPointer(cpu);
|
||||||
cpu->ProgramCounter++;
|
cpu->ProgramCounter++;
|
||||||
*target -= cpu->Program[cpu->ProgramCounter];
|
*target -= fetchProgram(cpu, cpu->ProgramCounter);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x4E: {
|
||||||
|
// DPUA - Offset the selected Data Pointer up by A.
|
||||||
|
//
|
||||||
|
// A rather than Q, because Q is what the ALU last worked out and would be
|
||||||
|
// gone by the time anything had been added to it. Working a step out and then
|
||||||
|
// moving a pointer by it took a store and a reload before this existed.
|
||||||
|
uint16_t *target = selectDataPointer(cpu);
|
||||||
|
*target += cpu->A;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x4F: {
|
||||||
|
// DPDA - Offset the selected Data Pointer down by A.
|
||||||
|
uint16_t *target = selectDataPointer(cpu);
|
||||||
|
*target -= cpu->A;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x50: {
|
||||||
|
// DPUW - Offset the selected Data Pointer up by A and B together, A being the
|
||||||
|
// most significant, which is how every sixteen bit value on this machine is
|
||||||
|
// carried between a pair of registers.
|
||||||
|
uint16_t *target = selectDataPointer(cpu);
|
||||||
|
*target += ((uint16_t)cpu->A << 8) | (uint16_t)cpu->B;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x51: {
|
||||||
|
// DPDW - Offset the selected Data Pointer down by A and B together.
|
||||||
|
uint16_t *target = selectDataPointer(cpu);
|
||||||
|
*target -= ((uint16_t)cpu->A << 8) | (uint16_t)cpu->B;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x4A: {
|
case 0x4A: {
|
||||||
@@ -566,8 +713,8 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
// follows the pointer in DP0 rather than tripping over itself.
|
// follows the pointer in DP0 rather than tripping over itself.
|
||||||
uint16_t *destination = selectDataPointer(cpu);
|
uint16_t *destination = selectDataPointer(cpu);
|
||||||
uint16_t source = *selectDataPointer(cpu);
|
uint16_t source = *selectDataPointer(cpu);
|
||||||
*destination = (uint16_t)cpu->Data[source] << 8;
|
*destination = (uint16_t)readData(cpu, source) << 8;
|
||||||
*destination |= (uint16_t)cpu->Data[(uint16_t)(source + 1)];
|
*destination |= (uint16_t)readData(cpu, (uint16_t)(source + 1));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x4B: {
|
case 0x4B: {
|
||||||
@@ -576,8 +723,8 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
// Data Memory when the pointer sits at the very top of it.
|
// Data Memory when the pointer sits at the very top of it.
|
||||||
uint16_t value = *selectDataPointer(cpu);
|
uint16_t value = *selectDataPointer(cpu);
|
||||||
uint16_t address = *selectDataPointer(cpu);
|
uint16_t address = *selectDataPointer(cpu);
|
||||||
cpu->Data[address] = (value >> 8) & 0xFF;
|
writeData(cpu, address, (value >> 8) & 0xFF);
|
||||||
cpu->Data[(uint16_t)(address + 1)] = value & 0xFF;
|
writeData(cpu, (uint16_t)(address + 1), value & 0xFF);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x4C: {
|
case 0x4C: {
|
||||||
@@ -616,7 +763,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
// OUTQ - Write the value of Q to an output port.
|
// OUTQ - Write the value of Q to an output port.
|
||||||
uint16_t site = cpu->ProgramCounter;
|
uint16_t site = cpu->ProgramCounter;
|
||||||
cpu->ProgramCounter++;
|
cpu->ProgramCounter++;
|
||||||
OutputHandler(cpu->Q, cpu->Program[cpu->ProgramCounter]);
|
portOut(cpu, cpu->Q, fetchProgram(cpu, cpu->ProgramCounter));
|
||||||
answerRefusal(cpu, site);
|
answerRefusal(cpu, site);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -624,7 +771,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
// OUTA - Write the value of A to an output port.
|
// OUTA - Write the value of A to an output port.
|
||||||
uint16_t site = cpu->ProgramCounter;
|
uint16_t site = cpu->ProgramCounter;
|
||||||
cpu->ProgramCounter++;
|
cpu->ProgramCounter++;
|
||||||
OutputHandler(cpu->A, cpu->Program[cpu->ProgramCounter]);
|
portOut(cpu, cpu->A, fetchProgram(cpu, cpu->ProgramCounter));
|
||||||
answerRefusal(cpu, site);
|
answerRefusal(cpu, site);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -632,7 +779,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
// OUTB - Write the value of B to an output port.
|
// OUTB - Write the value of B to an output port.
|
||||||
uint16_t site = cpu->ProgramCounter;
|
uint16_t site = cpu->ProgramCounter;
|
||||||
cpu->ProgramCounter++;
|
cpu->ProgramCounter++;
|
||||||
OutputHandler(cpu->B, cpu->Program[cpu->ProgramCounter]);
|
portOut(cpu, cpu->B, fetchProgram(cpu, cpu->ProgramCounter));
|
||||||
answerRefusal(cpu, site);
|
answerRefusal(cpu, site);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -643,7 +790,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
// INA - Read an Input to A.
|
// INA - Read an Input to A.
|
||||||
uint16_t site = cpu->ProgramCounter;
|
uint16_t site = cpu->ProgramCounter;
|
||||||
cpu->ProgramCounter++;
|
cpu->ProgramCounter++;
|
||||||
cpu->A = InputHandler(cpu->Program[cpu->ProgramCounter]);
|
cpu->A = portIn(cpu, fetchProgram(cpu, cpu->ProgramCounter));
|
||||||
answerRefusal(cpu, site);
|
answerRefusal(cpu, site);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -651,7 +798,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
// INB - Read an Input to B.
|
// INB - Read an Input to B.
|
||||||
uint16_t site = cpu->ProgramCounter;
|
uint16_t site = cpu->ProgramCounter;
|
||||||
cpu->ProgramCounter++;
|
cpu->ProgramCounter++;
|
||||||
cpu->B = InputHandler(cpu->Program[cpu->ProgramCounter]);
|
cpu->B = portIn(cpu, fetchProgram(cpu, cpu->ProgramCounter));
|
||||||
answerRefusal(cpu, site);
|
answerRefusal(cpu, site);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -661,6 +808,23 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
|||||||
case 0xF0:
|
case 0xF0:
|
||||||
// NOP - Do nothing.
|
// NOP - Do nothing.
|
||||||
break;
|
break;
|
||||||
|
case 0xFE:
|
||||||
|
// WAIT - Stop fetching until a device asks for attention.
|
||||||
|
//
|
||||||
|
// NOT A HALT. The machine is still clocked and devices still run; what stops
|
||||||
|
// is the CPU's use of the bus. HALT is how a program says it has finished and
|
||||||
|
// must stay that way, so this is a separate instruction rather than a gentler
|
||||||
|
// HALT - and its state is a field of its own rather than a Status bit, for
|
||||||
|
// the reason cpu.h gives.
|
||||||
|
//
|
||||||
|
// A LINE ALREADY UP MEANS THERE IS NOTHING TO WAIT FOR, and that is what
|
||||||
|
// makes the ordinary idiom race-free: a program tests its device, finds it
|
||||||
|
// busy, and waits. If the device finished in between, the line is standing
|
||||||
|
// and this does nothing at all rather than sleeping through the answer.
|
||||||
|
if (nextPendingInterrupt() < 0) {
|
||||||
|
cpu->Waiting = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 0xFF:
|
case 0xFF:
|
||||||
// HALT - Set the Halt Bit of the Status Register.
|
// HALT - Set the Halt Bit of the Status Register.
|
||||||
cpu->Status |= STATUS_HALT;
|
cpu->Status |= STATUS_HALT;
|
||||||
@@ -679,6 +843,34 @@ void stepCPU(CPURegisters *cpu) {
|
|||||||
// willing to be interrupted about it. Masking decides when a request is answered,
|
// willing to be interrupted about it. Masking decides when a request is answered,
|
||||||
// not whether the outside world is allowed to have happened.
|
// not whether the outside world is allowed to have happened.
|
||||||
serviceDevices();
|
serviceDevices();
|
||||||
|
// ---- Stopped in a WAIT ----
|
||||||
|
//
|
||||||
|
// Nothing is fetched and nothing is executed. A clock still passes, because a
|
||||||
|
// device that takes time has to be able to reach the end of it, and it is charged
|
||||||
|
// to idle rather than to the bus: the CPU is not using memory.
|
||||||
|
//
|
||||||
|
// A LINE OF ANY KIND ENDS THE WAIT, masked or not. Masking says who answers a
|
||||||
|
// request, not whether it happened - so a program can sleep on a device it has no
|
||||||
|
// handler for and simply read its status afterwards, which is the whole reason
|
||||||
|
// this is worth having and is what the filesystem does with it.
|
||||||
|
if (cpu->Waiting) {
|
||||||
|
if (nextPendingInterrupt() < 0) {
|
||||||
|
cpu->idleCycles++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cpu->Waiting = 0;
|
||||||
|
// Woken while masked, so nobody is going to answer this line and take it
|
||||||
|
// down. IT HAS TO BE TAKEN DOWN HERE. Left standing it would be found by the
|
||||||
|
// next WAIT, which would return at once, and by the one after that - the
|
||||||
|
// program would spin exactly as it did before while appearing to sleep.
|
||||||
|
//
|
||||||
|
// Unmasked, the dispatch below takes it down instead, and a line with no
|
||||||
|
// handler faults there the way it always has. Waiting changes what the CPU
|
||||||
|
// does between instructions; it does not change interrupt policy.
|
||||||
|
if (!(cpu->Status & STATUS_INTERRUPT)) {
|
||||||
|
clearInterrupt((uint8_t)nextPendingInterrupt());
|
||||||
|
}
|
||||||
|
}
|
||||||
// A device asking for attention is answered between instructions and never
|
// A device asking for attention is answered between instructions and never
|
||||||
// inside one, so the address that goes into the frame is always the start of an
|
// inside one, so the address that goes into the frame is always the start of an
|
||||||
// instruction and RETI always lands somewhere meaningful.
|
// instruction and RETI always lands somewhere meaningful.
|
||||||
@@ -704,7 +896,7 @@ void stepCPU(CPURegisters *cpu) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The CPU is not halted, so do a cycle.
|
// The CPU is not halted, so do a cycle.
|
||||||
if (executeOperation(cpu->Program[cpu->ProgramCounter], cpu)) {
|
if (executeOperation(fetchProgram(cpu, cpu->ProgramCounter), cpu)) {
|
||||||
// Nothing decodes that byte. Hand it to the fault vector, which gets the
|
// Nothing decodes that byte. Hand it to the fault vector, which gets the
|
||||||
// address of the offending byte itself rather than the one after it, so
|
// address of the offending byte itself rather than the one after it, so
|
||||||
// that a handler can read the byte that failed and say what it was.
|
// that a handler can read the byte that failed and say what it was.
|
||||||
|
|||||||
@@ -62,6 +62,32 @@ typedef struct {
|
|||||||
uint16_t StackPointer;
|
uint16_t StackPointer;
|
||||||
uint8_t *Program;
|
uint8_t *Program;
|
||||||
uint8_t *Data;
|
uint8_t *Data;
|
||||||
|
|
||||||
|
// ---- What the machine has cost so far ----
|
||||||
|
//
|
||||||
|
// ONE BUS ACCESS IS ONE CYCLE, and every access goes through it: fetching an opcode,
|
||||||
|
// fetching the bytes after it, reading or writing Data Memory, pushing or popping the
|
||||||
|
// Stack, and reaching a device port. Nothing is overlapped - no fetching the next
|
||||||
|
// instruction while this one finishes - because that is a thing hardware may or may
|
||||||
|
// not do and this is the model to design against before deciding.
|
||||||
|
//
|
||||||
|
// It replaces counting instructions. Counting instructions said an RSTA and a SETD
|
||||||
|
// cost the same, and that a CALL moving ten bytes of Stack cost what a branch costs,
|
||||||
|
// which is not true of any machine anybody could build.
|
||||||
|
unsigned long busCycles;
|
||||||
|
// ---- And what it has cost while doing nothing ----
|
||||||
|
//
|
||||||
|
// Clocks spent inside WAIT, where the CPU is stopped and the bus is idle. They are
|
||||||
|
// counted because time still has to pass - a device that takes a while has to be able
|
||||||
|
// to finish - and they are counted SEPARATELY because they are not the same thing as
|
||||||
|
// work. A machine waiting on a disk is not using memory, and charging it as though it
|
||||||
|
// were is exactly the sort of dishonest number the bus count was built to replace.
|
||||||
|
unsigned long idleCycles;
|
||||||
|
// Whether the CPU is stopped in a WAIT, which is not a Status bit and must not become
|
||||||
|
// one: the Status register rides into the interrupt frame and comes back out of it, so
|
||||||
|
// a machine interrupted while waiting would return from the handler still waiting, and
|
||||||
|
// wait again for the thing it had already been given.
|
||||||
|
uint8_t Waiting;
|
||||||
// Set alongside the Fault Flag, and read only by whatever reports the stop.
|
// Set alongside the Fault Flag, and read only by whatever reports the stop.
|
||||||
uint8_t Fault; // A FaultCause.
|
uint8_t Fault; // A FaultCause.
|
||||||
uint8_t FaultVector; // Which vector was empty, when Fault is FAULT_NO_HANDLER.
|
uint8_t FaultVector; // Which vector was empty, when Fault is FAULT_NO_HANDLER.
|
||||||
|
|||||||
+91
-10
@@ -5,6 +5,9 @@
|
|||||||
// Written by Anachronaut
|
// Written by Anachronaut
|
||||||
// 10/15/2024
|
// 10/15/2024
|
||||||
|
|
||||||
|
#include "rom.h"
|
||||||
|
#include "bootstrap.h"
|
||||||
|
#include "../Assembler/assembly.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -65,6 +68,22 @@ char *programFile = NULL;
|
|||||||
// Memory Banks:
|
// Memory Banks:
|
||||||
uint8_t Program[0x10000], Data[0x10000];
|
uint8_t Program[0x10000], Data[0x10000];
|
||||||
|
|
||||||
|
// How the run is reported. The idle half is mentioned only when there is one, so that
|
||||||
|
// every program written before WAIT existed prints exactly the line it always did.
|
||||||
|
//
|
||||||
|
// THE TWO ARE NOT THE SAME KIND OF TIME. A bus cycle is the machine using memory; an idle
|
||||||
|
// cycle is the machine stopped in a WAIT while a device catches up. Added together they
|
||||||
|
// are elapsed time, which is what a cycle limit measures; told apart they say whether a
|
||||||
|
// program was working or waiting.
|
||||||
|
static void reportCycles(const CPURegisters *cpu, unsigned long cycleCount) {
|
||||||
|
if (cpu->idleCycles > 0) {
|
||||||
|
printf("Execution halted after %lu cycles, %lu of them waiting.\n",
|
||||||
|
cycleCount, cpu->idleCycles);
|
||||||
|
} else {
|
||||||
|
printf("Execution halted after %lu cycles.\n", cycleCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int main (int argc, char *argv[]) {
|
int main (int argc, char *argv[]) {
|
||||||
EmulatorOptions options;
|
EmulatorOptions options;
|
||||||
uint8_t result = parseOptions(argc, argv, &options);
|
uint8_t result = parseOptions(argc, argv, &options);
|
||||||
@@ -78,17 +97,32 @@ int main (int argc, char *argv[]) {
|
|||||||
if (optind < argc) {
|
if (optind < argc) {
|
||||||
programFile = argv[optind];
|
programFile = argv[optind];
|
||||||
optind++;
|
optind++;
|
||||||
} else {
|
|
||||||
fprintf(stderr, "Error: No boot image specified.\n");
|
|
||||||
printHelp(argv[0]);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
if (optind < argc) {
|
if (optind < argc) {
|
||||||
fprintf(stderr, "Error: Unexpected argument: %s\n", argv[optind]);
|
fprintf(stderr, "Error: Unexpected argument: %s\n", argv[optind]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (loadFile(programFile, Program, Data)) {
|
// ---- Where the machine's first instruction comes from ----
|
||||||
fprintf(stderr, "Error: Couldn't read file: %s\n", programFile);
|
//
|
||||||
|
// Named an image, it is placed into memory and started - which is what a debugger
|
||||||
|
// does, and is how every test here runs. That path is not a shortcut to apologise
|
||||||
|
// for: placing memory from outside is a real thing real machines allow.
|
||||||
|
//
|
||||||
|
// Named none, the machine starts the way hardware would: the ROM is shadowed into
|
||||||
|
// Program Memory and it reads the disk for the rest. There has to be a disk for that
|
||||||
|
// to mean anything, and no image and no disk is a machine with nothing to run.
|
||||||
|
if (programFile == NULL && options.disk == NULL) {
|
||||||
|
fprintf(stderr, "Error: No boot image and no disk, so there is nothing to run.\n");
|
||||||
|
printHelp(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (programFile != NULL) {
|
||||||
|
if (loadFile(programFile, Program, Data)) {
|
||||||
|
fprintf(stderr, "Error: Couldn't read file: %s\n", programFile);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} else if (loadROM(bootROM, bootROMBytes, Program, Data)) {
|
||||||
|
fprintf(stderr, "Error: The boot ROM is not a boot image.\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (options.disk != NULL && attachDisk(options.disk, options.writeProtect)) {
|
if (options.disk != NULL && attachDisk(options.disk, options.writeProtect)) {
|
||||||
@@ -104,6 +138,7 @@ int main (int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CycleTimer timer;
|
CycleTimer timer;
|
||||||
|
setDiskLatency(options.diskCycles);
|
||||||
cycle_timer_init(&timer, CYCLE_RATE);
|
cycle_timer_init(&timer, CYCLE_RATE);
|
||||||
|
|
||||||
uint8_t limitReached = 0;
|
uint8_t limitReached = 0;
|
||||||
@@ -126,9 +161,55 @@ int main (int argc, char *argv[]) {
|
|||||||
} else {
|
} else {
|
||||||
cycles = cycle_timer_tick(&timer);
|
cycles = cycle_timer_tick(&timer);
|
||||||
}
|
}
|
||||||
for (int i = 0; i < cycles; i++) {
|
// ---- Spending a budget of cycles, not running a count of instructions ----
|
||||||
|
//
|
||||||
|
// An instruction costs what it touches, so a batch is finished when the cycles are
|
||||||
|
// gone rather than after so many steps. In debug mode the budget is one, and any
|
||||||
|
// instruction costs at least the fetch of its own opcode, so one step still runs.
|
||||||
|
for (long spent = 0; spent < cycles; ) {
|
||||||
|
// Both kinds of cycle, because both are time passing. A step that waits
|
||||||
|
// spends no bus at all, and a budget measured only in bus cycles would never
|
||||||
|
// be spent - the machine would sit inside one batch forever and the device it
|
||||||
|
// was waiting for would never be given a moment to finish.
|
||||||
|
unsigned long before = cpu.busCycles + cpu.idleCycles;
|
||||||
stepCPU(&cpu);
|
stepCPU(&cpu);
|
||||||
cycleCount++;
|
unsigned long took = (cpu.busCycles + cpu.idleCycles) - before;
|
||||||
|
spent += (long)took;
|
||||||
|
cycleCount += took;
|
||||||
|
// Time has passed, so anything waiting on it may be finished.
|
||||||
|
deviceTick(cycleCount);
|
||||||
|
|
||||||
|
// ---- Starting over ----
|
||||||
|
//
|
||||||
|
// Between instructions, which is the only place it can happen: a device cannot
|
||||||
|
// restart the machine from inside the instruction that asked for it.
|
||||||
|
//
|
||||||
|
// WHAT A RESET REPEATS IS HOW THIS MACHINE STARTED. Named an image, it is
|
||||||
|
// placed again; named none, the ROM is shadowed again and reads the disk for
|
||||||
|
// the rest. Anything else would mean a reset changed what the machine is,
|
||||||
|
// which is the one thing a reset must not do.
|
||||||
|
//
|
||||||
|
// The disk is not unplugged and its image keeps everything written to it. That
|
||||||
|
// is what warm means: the machine starts again, the world it starts into does
|
||||||
|
// not.
|
||||||
|
if (takeResetRequest()) {
|
||||||
|
// The vector table goes, and that is a deliberate departure from leaving
|
||||||
|
// memory alone. A vector points into whatever installed it, and after this
|
||||||
|
// that program is not running - so a handler left behind would aim an
|
||||||
|
// interrupt at an address belonging to something gone. It is the argument
|
||||||
|
// CosmOS already makes when it takes a program's vectors back at exit.
|
||||||
|
memset(Program + SOFTWARE_VECTOR_BASE, 0,
|
||||||
|
(size_t)(0x10000 - SOFTWARE_VECTOR_BASE));
|
||||||
|
uint8_t failed = (programFile != NULL)
|
||||||
|
? loadFile(programFile, Program, Data)
|
||||||
|
: loadROM(bootROM, bootROMBytes, Program, Data);
|
||||||
|
if (failed) {
|
||||||
|
fprintf(stderr, "Error: The machine could not be started again.\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
initializeCPU(&cpu, Program, Data);
|
||||||
|
break; // Out of this batch; the loop above carries on with a new CPU.
|
||||||
|
}
|
||||||
if (cpu.Status & STATUS_HALT) {
|
if (cpu.Status & STATUS_HALT) {
|
||||||
// We've halted.
|
// We've halted.
|
||||||
break;
|
break;
|
||||||
@@ -148,7 +229,7 @@ int main (int argc, char *argv[]) {
|
|||||||
printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount);
|
printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount);
|
||||||
} else if (cpu.Status & STATUS_FAULT) {
|
} else if (cpu.Status & STATUS_FAULT) {
|
||||||
// The Program Counter is still pointing at whatever the CPU could not get past.
|
// The Program Counter is still pointing at whatever the CPU could not get past.
|
||||||
printf("Execution halted after %lu cycles.\n", cycleCount);
|
reportCycles(&cpu, cycleCount);
|
||||||
if (cpu.Fault == FAULT_NO_HANDLER) {
|
if (cpu.Fault == FAULT_NO_HANDLER) {
|
||||||
fprintf(stderr, "Fault: Software vector %u, dispatched from Program Address 0x%04X, has no handler installed.\n",
|
fprintf(stderr, "Fault: Software vector %u, dispatched from Program Address 0x%04X, has no handler installed.\n",
|
||||||
cpu.FaultVector, cpu.ProgramCounter);
|
cpu.FaultVector, cpu.ProgramCounter);
|
||||||
@@ -164,7 +245,7 @@ int main (int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
printf("Execution halted after %lu cycles.\n", cycleCount);
|
reportCycles(&cpu, cycleCount);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-7
@@ -324,6 +324,15 @@ static uint8_t consoleStatus(void) {
|
|||||||
|
|
||||||
static uint8_t pendingInterrupts[INTERRUPT_LINE_BYTES];
|
static uint8_t pendingInterrupts[INTERRUPT_LINE_BYTES];
|
||||||
|
|
||||||
|
// Whether somebody has asked the machine to start over, and taking that request away.
|
||||||
|
static int resetWanted = 0;
|
||||||
|
|
||||||
|
int takeResetRequest(void) {
|
||||||
|
int wanted = resetWanted;
|
||||||
|
resetWanted = 0;
|
||||||
|
return wanted;
|
||||||
|
}
|
||||||
|
|
||||||
void raiseInterrupt(uint8_t port) {
|
void raiseInterrupt(uint8_t port) {
|
||||||
pendingInterrupts[port >> 3] |= (uint8_t)(1u << (port & 7));
|
pendingInterrupts[port >> 3] |= (uint8_t)(1u << (port & 7));
|
||||||
}
|
}
|
||||||
@@ -382,6 +391,22 @@ uint8_t refusingPort(void) {
|
|||||||
static FILE *diskImage = NULL;
|
static FILE *diskImage = NULL;
|
||||||
static uint32_t diskBlockCount = 0;
|
static uint32_t diskBlockCount = 0;
|
||||||
static uint8_t diskBuffer[DISK_BLOCK_BYTES];
|
static uint8_t diskBuffer[DISK_BLOCK_BYTES];
|
||||||
|
|
||||||
|
// ---- A disk that takes time ----
|
||||||
|
//
|
||||||
|
// The command is checked at once, because a refusal is not work: asking for a block that
|
||||||
|
// is not there, or writing to a protected disk, fails before any head moves. What takes
|
||||||
|
// time is the transfer, so that is remembered here and done when the machine has run far
|
||||||
|
// enough - and until then the buffer holds the block BEFORE this one, which is exactly
|
||||||
|
// what a program that ignores the busy bit deserves to read.
|
||||||
|
// The machine's clock as devices see it, which the emulator advances as the CPU spends
|
||||||
|
// cycles. A device says when it will be finished in these, and is believed.
|
||||||
|
static unsigned long deviceNow = 0;
|
||||||
|
static void diskTransfer(uint8_t command);
|
||||||
|
|
||||||
|
static unsigned long diskLatency = 0;
|
||||||
|
static unsigned long diskReadyAt = 0;
|
||||||
|
static uint8_t diskPending = 0;
|
||||||
static uint16_t diskBlock = 0;
|
static uint16_t diskBlock = 0;
|
||||||
static uint8_t diskStatus = 0;
|
static uint8_t diskStatus = 0;
|
||||||
static uint8_t diskProtected = 0;
|
static uint8_t diskProtected = 0;
|
||||||
@@ -458,23 +483,55 @@ static void diskCommand(uint8_t command) {
|
|||||||
raiseInterrupt(PORT_DISK);
|
raiseInterrupt(PORT_DISK);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
size_t moved = 0;
|
if (command != DISK_COMMAND_READ && command != DISK_COMMAND_WRITE) {
|
||||||
if (command == DISK_COMMAND_READ) {
|
|
||||||
moved = fread(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
|
|
||||||
} else if (command == DISK_COMMAND_WRITE) {
|
|
||||||
moved = fwrite(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
|
|
||||||
fflush(diskImage);
|
|
||||||
} else {
|
|
||||||
diskStatus |= DISK_STATUS_ERROR;
|
diskStatus |= DISK_STATUS_ERROR;
|
||||||
raiseInterrupt(PORT_DISK);
|
raiseInterrupt(PORT_DISK);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (diskLatency == 0) {
|
||||||
|
diskTransfer(command);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// It is going to take a while. Say so, and remember what to do when it is over.
|
||||||
|
diskStatus |= DISK_STATUS_BUSY;
|
||||||
|
diskPending = command;
|
||||||
|
diskReadyAt = deviceNow + diskLatency;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The transfer itself, whenever it happens to happen. The seek is done here rather than at
|
||||||
|
// the command, because nothing else may touch the image in between and doing it twice is
|
||||||
|
// the same answer.
|
||||||
|
static void diskTransfer(uint8_t command) {
|
||||||
|
size_t moved = 0;
|
||||||
|
long offset = (long)diskBlock * DISK_BLOCK_BYTES;
|
||||||
|
if (fseek(diskImage, offset, SEEK_SET) != 0) {
|
||||||
|
diskStatus |= DISK_STATUS_ERROR;
|
||||||
|
} else if (command == DISK_COMMAND_READ) {
|
||||||
|
moved = fread(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
|
||||||
|
} else {
|
||||||
|
moved = fwrite(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
|
||||||
|
fflush(diskImage);
|
||||||
|
}
|
||||||
if (moved != DISK_BLOCK_BYTES) {
|
if (moved != DISK_BLOCK_BYTES) {
|
||||||
diskStatus |= DISK_STATUS_ERROR;
|
diskStatus |= DISK_STATUS_ERROR;
|
||||||
}
|
}
|
||||||
|
diskStatus &= (uint8_t)~DISK_STATUS_BUSY;
|
||||||
raiseInterrupt(PORT_DISK);
|
raiseInterrupt(PORT_DISK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setDiskLatency(unsigned long cycles) {
|
||||||
|
diskLatency = cycles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void deviceTick(unsigned long now) {
|
||||||
|
deviceNow = now;
|
||||||
|
if (diskPending && now >= diskReadyAt) {
|
||||||
|
uint8_t command = diskPending;
|
||||||
|
diskPending = 0;
|
||||||
|
diskTransfer(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- A device that brings memory ----
|
// ---- A device that brings memory ----
|
||||||
//
|
//
|
||||||
// The simplest thing that owns a bank. Writing to its port fills its memory with the
|
// The simplest thing that owns a bank. Writing to its port fills its memory with the
|
||||||
@@ -522,6 +579,7 @@ static const DeviceRecord deviceTable[] = {
|
|||||||
{ PORT_CONSOLE, DEVICE_CONSOLE, 0 },
|
{ PORT_CONSOLE, DEVICE_CONSOLE, 0 },
|
||||||
{ PORT_TEST, DEVICE_TEST, 0 },
|
{ PORT_TEST, DEVICE_TEST, 0 },
|
||||||
{ PORT_REFUSE, DEVICE_REFUSE, 0 },
|
{ PORT_REFUSE, DEVICE_REFUSE, 0 },
|
||||||
|
{ PORT_MACHINE, DEVICE_MACHINE, 0 },
|
||||||
{ PORT_MEMORY, DEVICE_MEMORY, DEVICE_FLAG_HAS_MEMORY },
|
{ PORT_MEMORY, DEVICE_MEMORY, DEVICE_FLAG_HAS_MEMORY },
|
||||||
{ PORT_DISK, DEVICE_DISK, DEVICE_FLAG_HAS_MEMORY },
|
{ PORT_DISK, DEVICE_DISK, DEVICE_FLAG_HAS_MEMORY },
|
||||||
{ PORT_REGISTRY, DEVICE_REGISTRY, 0 },
|
{ PORT_REGISTRY, DEVICE_REGISTRY, 0 },
|
||||||
@@ -597,6 +655,15 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
|
|||||||
case DISK_BLOCK_HIGH: diskBlock = (uint16_t)(DataByte << 8) | (diskBlock & 0x00FF); break;
|
case DISK_BLOCK_HIGH: diskBlock = (uint16_t)(DataByte << 8) | (diskBlock & 0x00FF); break;
|
||||||
case DISK_BLOCK_LOW: diskBlock = (diskBlock & 0xFF00) | DataByte; break;
|
case DISK_BLOCK_LOW: diskBlock = (diskBlock & 0xFF00) | DataByte; break;
|
||||||
case DISK_COMMAND: diskCommand(DataByte); break;
|
case DISK_COMMAND: diskCommand(DataByte); break;
|
||||||
|
case PORT_MACHINE:
|
||||||
|
// Asked for here and acted on between instructions, because a device cannot
|
||||||
|
// restart the machine from inside the instruction that asked: the CPU is part
|
||||||
|
// way through a step and its state is not yet anything a reset could leave
|
||||||
|
// consistently behind.
|
||||||
|
if (DataByte == MACHINE_RESET) {
|
||||||
|
resetWanted = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case PORT_MEMORY:
|
case PORT_MEMORY:
|
||||||
// Fills the memory this device owns with the byte written. Nothing is
|
// Fills the memory this device owns with the byte written. Nothing is
|
||||||
// reachable from here: to get at it, register it as a bank and go through
|
// reachable from here: to get at it, register it as a bank and go through
|
||||||
|
|||||||
+46
-3
@@ -27,6 +27,21 @@
|
|||||||
#define PORT_REFUSE 0x11
|
#define PORT_REFUSE 0x11
|
||||||
#define PORT_MEMORY 0x12
|
#define PORT_MEMORY 0x12
|
||||||
|
|
||||||
|
// ---- Starting again ----
|
||||||
|
//
|
||||||
|
// Writing 1 here asks the machine to start over: whatever put the first instruction in
|
||||||
|
// memory does it again, and the CPU begins where the boot vector points.
|
||||||
|
//
|
||||||
|
// A PORT RATHER THAN A SERVICE, because a reset has to work when the system does not.
|
||||||
|
// Something that could only be asked for through SWI would be unavailable in exactly the
|
||||||
|
// case that most wants it, and a program that owns the whole machine has no system to ask.
|
||||||
|
//
|
||||||
|
// What it does NOT do is unplug anything. The disk stays attached and its image keeps
|
||||||
|
// whatever was written to it, which is what a warm restart means: the machine starts
|
||||||
|
// again, the world it starts into does not.
|
||||||
|
#define PORT_MACHINE 0x13
|
||||||
|
#define MACHINE_RESET 0x01
|
||||||
|
|
||||||
// The disk answers on a block of four ports and interrupts on the first of them. A device
|
// The disk answers on a block of four ports and interrupts on the first of them. A device
|
||||||
// that spans more than one port raises its line on its base, which is the rule the
|
// that spans more than one port raises its line on its base, which is the rule the
|
||||||
// machine has not needed until now: the controller spans sixteen and never interrupts.
|
// machine has not needed until now: the controller spans sixteen and never interrupts.
|
||||||
@@ -121,6 +136,10 @@ uint8_t consoleReadByte(void);
|
|||||||
#define DEVICE_REGISTRY 0x01
|
#define DEVICE_REGISTRY 0x01
|
||||||
#define DEVICE_CONSOLE 0x02
|
#define DEVICE_CONSOLE 0x02
|
||||||
#define DEVICE_CONTROLLER 0x03
|
#define DEVICE_CONTROLLER 0x03
|
||||||
|
// The machine itself, which is what a reset is asking. In the range kept for the machine
|
||||||
|
// rather than among the peripherals, because it is not one: it is not attached to
|
||||||
|
// anything and cannot be unplugged.
|
||||||
|
#define DEVICE_MACHINE 0x04
|
||||||
#define DEVICE_TEST 0x10
|
#define DEVICE_TEST 0x10
|
||||||
#define DEVICE_REFUSE 0x11
|
#define DEVICE_REFUSE 0x11
|
||||||
#define DEVICE_MEMORY 0x12
|
#define DEVICE_MEMORY 0x12
|
||||||
@@ -145,9 +164,12 @@ uint8_t consoleReadByte(void);
|
|||||||
#define DISK_COMMAND_READ 0x01
|
#define DISK_COMMAND_READ 0x01
|
||||||
#define DISK_COMMAND_WRITE 0x02
|
#define DISK_COMMAND_WRITE 0x02
|
||||||
|
|
||||||
// Set while an operation is still going. It always reads clear here, because the host
|
// Set while an operation is still going, and it now really is set: a disk given a latency
|
||||||
// finishes before the next instruction does, but a machine with a slower disk would set
|
// says busy, takes that many cycles, and finishes then. A program that does not wait gets
|
||||||
// it and a program that ignores it would break there. Honour it anyway.
|
// whatever was in the buffer before, which is what the hardware would give it.
|
||||||
|
//
|
||||||
|
// It reads clear the whole time when the latency is zero, which is the default and how
|
||||||
|
// every test here has always run.
|
||||||
#define DISK_STATUS_BUSY 0x01
|
#define DISK_STATUS_BUSY 0x01
|
||||||
// Set when the disk cannot be written at all. Unlike the two bits above it, this is not
|
// Set when the disk cannot be written at all. Unlike the two bits above it, this is not
|
||||||
// about the last operation: it is a standing property of the medium, readable before
|
// about the last operation: it is a standing property of the medium, readable before
|
||||||
@@ -160,6 +182,22 @@ uint8_t consoleReadByte(void);
|
|||||||
// so it says so rather than stopping the machine.
|
// so it says so rather than stopping the machine.
|
||||||
#define DISK_STATUS_ERROR 0x02
|
#define DISK_STATUS_ERROR 0x02
|
||||||
|
|
||||||
|
// ---- Devices that take time ----
|
||||||
|
//
|
||||||
|
// A real device does not finish inside the instruction that asked it to. It says it is
|
||||||
|
// busy, takes as long as it takes, and is done when the machine has run that far - so the
|
||||||
|
// emulator needs somewhere to notice that time has passed. That is this: called once per
|
||||||
|
// instruction with the machine's clock, it lets any device whose moment has come finish.
|
||||||
|
//
|
||||||
|
// It is written for the disk and is not about the disk. Anything that will take time - a
|
||||||
|
// display that refreshes, a port that waits on the host - wants exactly this shape.
|
||||||
|
void deviceTick(unsigned long now);
|
||||||
|
|
||||||
|
// How many cycles a block read or write takes. Zero means the answer is there before the
|
||||||
|
// next instruction is, which is what this machine has always done and what every recorded
|
||||||
|
// test assumes.
|
||||||
|
void setDiskLatency(unsigned long cycles);
|
||||||
|
|
||||||
// Attaches an image, making one if it is not there. A disk is read only if the host will
|
// Attaches an image, making one if it is not there. A disk is read only if the host will
|
||||||
// not let the file be written, or if writeProtect asks for it, which is the emulated
|
// not let the file be written, or if writeProtect asks for it, which is the emulated
|
||||||
// equivalent of the tab on the side of a floppy. Returns 1 if it could not attach.
|
// equivalent of the tab on the side of a floppy. Returns 1 if it could not attach.
|
||||||
@@ -194,6 +232,11 @@ uint8_t InputHandler(uint8_t Address);
|
|||||||
// must cost nothing when it does not.
|
// must cost nothing when it does not.
|
||||||
void serviceDevices(void);
|
void serviceDevices(void);
|
||||||
|
|
||||||
|
// Set when something has written MACHINE_RESET, and taken by the loop that acts on it. A
|
||||||
|
// request rather than an action, because a device cannot restart the machine from inside
|
||||||
|
// the instruction that asked - the CPU is mid-step and its state is not yet consistent.
|
||||||
|
int takeResetRequest(void);
|
||||||
|
|
||||||
void raiseInterrupt(uint8_t port);
|
void raiseInterrupt(uint8_t port);
|
||||||
|
|
||||||
void clearInterrupt(uint8_t port);
|
void clearInterrupt(uint8_t port);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// rom.h
|
||||||
|
// The bytes the machine wakes up in.
|
||||||
|
//
|
||||||
|
// Stage one, built from Programs/Boot/stage1.asm by the makefile rather than kept here as
|
||||||
|
// a copy, because a copy of a program stored beside the program is a copy that goes stale.
|
||||||
|
//
|
||||||
|
// It is an ordinary boot image, and that is the point: nothing about stage one changes
|
||||||
|
// when it moves from a file into a ROM except who puts it in memory.
|
||||||
|
//
|
||||||
|
// Written by Anachronaut
|
||||||
|
|
||||||
|
#ifndef ROM_H
|
||||||
|
#define ROM_H
|
||||||
|
|
||||||
|
extern const unsigned char bootROM[];
|
||||||
|
extern const unsigned long bootROMBytes;
|
||||||
|
|
||||||
|
#endif // ROM_H
|
||||||
@@ -11,13 +11,20 @@
|
|||||||
#include "../Assembler/assembly.h"
|
#include "../Assembler/assembly.h"
|
||||||
|
|
||||||
void printHelp(const char *programName) {
|
void printHelp(const char *programName) {
|
||||||
printf("Usage: %s [OPTIONS] <boot image>\n", programName);
|
printf("Usage: %s [OPTIONS] [boot image]\n", programName);
|
||||||
|
printf("\n");
|
||||||
|
printf("Named an image, it is placed into memory and started, which is what a\n");
|
||||||
|
printf("debugger does and how the test suite runs. Given only a disk, the machine\n");
|
||||||
|
printf("starts the way hardware would: the built in ROM is shadowed into Program\n");
|
||||||
|
printf("Memory, and it reads the disk for everything else.\n");
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf("Options:\n");
|
printf("Options:\n");
|
||||||
printf(" -d, --debug Enable debug mode.\n");
|
printf(" -d, --debug Enable debug mode.\n");
|
||||||
printf(" -c, --cycles N Stop after N cycles instead of running until the program halts.\n");
|
printf(" -c, --cycles N Stop after N cycles instead of running until the program halts.\n");
|
||||||
printf(" -f, --fast Run as fast as possible, ignoring the emulated cycle rate.\n");
|
printf(" -f, --fast Run as fast as possible, ignoring the emulated cycle rate.\n");
|
||||||
printf(" -D, --disk FILE Attach a disk image, making one if it is not there.\n");
|
printf(" -D, --disk FILE Attach a disk image, making one if it is not there.\n");
|
||||||
|
printf(" -L, --disk-cycles N How many cycles a block read or write takes. Zero, the\n");
|
||||||
|
printf(" default, finishes before the next instruction starts.\n");
|
||||||
printf(" -W, --write-protect Attach the disk read only. A disk the host will not let\n");
|
printf(" -W, --write-protect Attach the disk read only. A disk the host will not let\n");
|
||||||
printf(" you write is read only whether you ask for this or not.\n");
|
printf(" you write is read only whether you ask for this or not.\n");
|
||||||
printf(" -h, --help Display this help message.\n");
|
printf(" -h, --help Display this help message.\n");
|
||||||
@@ -30,6 +37,7 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
|||||||
{"fast", no_argument, 0, 'f'},
|
{"fast", no_argument, 0, 'f'},
|
||||||
{"disk", required_argument, 0, 'D'},
|
{"disk", required_argument, 0, 'D'},
|
||||||
{"write-protect", no_argument, 0, 'W'},
|
{"write-protect", no_argument, 0, 'W'},
|
||||||
|
{"disk-cycles", required_argument, 0, 'L'},
|
||||||
{"help", no_argument, 0, 'h'},
|
{"help", no_argument, 0, 'h'},
|
||||||
{0, 0, 0, 0 }
|
{0, 0, 0, 0 }
|
||||||
};
|
};
|
||||||
@@ -41,9 +49,10 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
|||||||
options->cycles = 0;
|
options->cycles = 0;
|
||||||
options->disk = NULL;
|
options->disk = NULL;
|
||||||
options->writeProtect = 0;
|
options->writeProtect = 0;
|
||||||
|
options->diskCycles = 0;
|
||||||
|
|
||||||
// Parse options
|
// Parse options
|
||||||
while ((opt = getopt_long(argc, argv, "dc:fhD:W", long_options, &option_index)) != -1) {
|
while ((opt = getopt_long(argc, argv, "dc:fhD:WL:", long_options, &option_index)) != -1) {
|
||||||
switch (opt) {
|
switch (opt) {
|
||||||
case 'd':
|
case 'd':
|
||||||
options->debug = 1;
|
options->debug = 1;
|
||||||
@@ -70,6 +79,9 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
|||||||
case 'W':
|
case 'W':
|
||||||
options->writeProtect = 1;
|
options->writeProtect = 1;
|
||||||
break;
|
break;
|
||||||
|
case 'L':
|
||||||
|
options->diskCycles = strtoul(optarg, NULL, 0);
|
||||||
|
break;
|
||||||
case 'h':
|
case 'h':
|
||||||
printHelp(argv[0]);
|
printHelp(argv[0]);
|
||||||
return OPTIONS_HELP;
|
return OPTIONS_HELP;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ typedef struct {
|
|||||||
uint8_t debug; // Step one instruction at a time, printing the registers.
|
uint8_t debug; // Step one instruction at a time, printing the registers.
|
||||||
uint8_t fast; // Ignore the cycle rate and run as fast as the host allows.
|
uint8_t fast; // Ignore the cycle rate and run as fast as the host allows.
|
||||||
unsigned long cycles; // Stop after this many cycles. Zero means run until the program halts.
|
unsigned long cycles; // Stop after this many cycles. Zero means run until the program halts.
|
||||||
|
unsigned long diskCycles; // How long a block move takes. Zero is instant, and the default.
|
||||||
const char *disk; // Disk image to attach, or NULL for a machine with no disk.
|
const char *disk; // Disk image to attach, or NULL for a machine with no disk.
|
||||||
uint8_t writeProtect; // Attach the disk read only, the way a tab on a floppy would.
|
uint8_t writeProtect; // Attach the disk read only, the way a tab on a floppy would.
|
||||||
} EmulatorOptions;
|
} EmulatorOptions;
|
||||||
|
|||||||
@@ -0,0 +1,859 @@
|
|||||||
|
// Linter.c
|
||||||
|
// Small, deliberately conservative source linter for SplitBit assembly.
|
||||||
|
//
|
||||||
|
// This first version works one source file at a time and looks only at instructions
|
||||||
|
// which are adjacent in that file. It is not an assembler and does not expand
|
||||||
|
// includes or follow control flow. That modest boundary makes every diagnostic easy
|
||||||
|
// to explain while the useful rules, and the interface they need, are still being
|
||||||
|
// discovered.
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "../Assembler/assembly.h"
|
||||||
|
#include "../Emulator/cpu.h"
|
||||||
|
|
||||||
|
#define LINE_CAPACITY 1024
|
||||||
|
#define TOKEN_CAPACITY 64
|
||||||
|
#define RULE_CAPACITY 32
|
||||||
|
|
||||||
|
// Every rule this knows, named once. The count in the clean run message comes from here,
|
||||||
|
// so a rule added without a name here is a rule the summary does not know about.
|
||||||
|
static const char *const ruleNames[] = {
|
||||||
|
"redundant-setd", "redundant-assignment", "pointer-offset", "redundant-ccf",
|
||||||
|
"known-branch", "zero-load", "dead-assignment", "q-through-stack", "self-push-pop",
|
||||||
|
"branch-to-next", "unreachable", "dead-suppression",
|
||||||
|
};
|
||||||
|
#define RULE_COUNT ((int)(sizeof(ruleNames) / sizeof(ruleNames[0])))
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char mnemonic[TOKEN_CAPACITY];
|
||||||
|
char spelling[TOKEN_CAPACITY];
|
||||||
|
char operand[TOKEN_CAPACITY];
|
||||||
|
long literal;
|
||||||
|
int hasLiteral;
|
||||||
|
int line;
|
||||||
|
} SourceInstruction;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
SOURCE_OTHER,
|
||||||
|
SOURCE_INSTRUCTION,
|
||||||
|
SOURCE_LABEL,
|
||||||
|
SOURCE_BOUNDARY
|
||||||
|
} SourceLine;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int aKnown;
|
||||||
|
int bKnown;
|
||||||
|
uint8_t a;
|
||||||
|
uint8_t b;
|
||||||
|
} KnownRegisters;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int known;
|
||||||
|
char base[TOKEN_CAPACITY];
|
||||||
|
uint16_t offset;
|
||||||
|
} KnownPointer;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CARRY_UNKNOWN = -1,
|
||||||
|
CARRY_CLEAR = 0,
|
||||||
|
CARRY_SET = 1
|
||||||
|
} KnownCarry;
|
||||||
|
|
||||||
|
static void usage(const char *name) {
|
||||||
|
printf("Usage: %s [options] <sourcefile> [sourcefile ...]\n", name);
|
||||||
|
printf("\n");
|
||||||
|
printf("Checks SplitBit assembly source for correct but needlessly long forms.\n");
|
||||||
|
printf("Warnings do not fail the command unless --fatal-warnings is given.\n");
|
||||||
|
printf("\n");
|
||||||
|
printf(" --fatal-warnings Exit non-zero if anything is reported.\n");
|
||||||
|
printf(" --machine One tab separated line per warning and nothing else:\n");
|
||||||
|
printf(" file, line, rule, message, help.\n");
|
||||||
|
printf("\n");
|
||||||
|
printf("A line whose comment says \"splitlint: <reason>\" is not reported on, and the\n");
|
||||||
|
printf("reason is required, so that a deliberate exception says what makes it one.\n");
|
||||||
|
printf("\"splitlint[rule]: <reason>\" silences one rule and leaves the line honest\n");
|
||||||
|
printf("about the others. A marker that silences nothing is itself reported.\n");
|
||||||
|
printf("\n");
|
||||||
|
printf("Rules:");
|
||||||
|
for (int i = 0; i < RULE_COUNT; i++) {
|
||||||
|
printf("%s %s", i && i % 4 == 0 ? "\n " : "", ruleNames[i]);
|
||||||
|
}
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void uppercase(char *text) {
|
||||||
|
for (; *text; text++) {
|
||||||
|
*text = (char)toupper((unsigned char)*text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A semicolon inside a string belongs to the string. Anything after one outside a
|
||||||
|
// string is a comment and must not accidentally look like an instruction.
|
||||||
|
static void removeComment(char *line) {
|
||||||
|
int inString = 0;
|
||||||
|
for (char *at = line; *at; at++) {
|
||||||
|
if (*at == '"') {
|
||||||
|
inString = !inString;
|
||||||
|
} else if (*at == ';' && !inString) {
|
||||||
|
*at = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int literalValue(const char *text, long *value) {
|
||||||
|
int base;
|
||||||
|
if (text[0] != '0' || (text[1] != 'x' && text[1] != 'X'
|
||||||
|
&& text[1] != 'd' && text[1] != 'D')) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
base = (text[1] == 'x' || text[1] == 'X') ? 16 : 10;
|
||||||
|
char *end;
|
||||||
|
long parsed = strtol(text + 2, &end, base);
|
||||||
|
if (end == text + 2 || *end != '\0' || parsed < 0 || parsed > 255) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
*value = parsed;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classifies enough of a physical source line for local analysis. Directives, strings,
|
||||||
|
// literal data, and unknown tokens are boundaries: until the linter shares the full
|
||||||
|
// assembler frontend, it makes no control-flow claim across something it did not parse.
|
||||||
|
static SourceLine parseInstruction(char *line, int lineNumber, SourceInstruction *result) {
|
||||||
|
removeComment(line);
|
||||||
|
char *token = strtok(line, " \t\r\n");
|
||||||
|
if (!token) {
|
||||||
|
return SOURCE_OTHER;
|
||||||
|
}
|
||||||
|
if (token[0] == '#' || token[0] == '"') {
|
||||||
|
return SOURCE_BOUNDARY;
|
||||||
|
}
|
||||||
|
size_t length = strlen(token);
|
||||||
|
if (length && token[length - 1] == ':') {
|
||||||
|
memset(result, 0, sizeof(*result));
|
||||||
|
if (length - 1 < sizeof(result->spelling)) {
|
||||||
|
memcpy(result->spelling, token, length - 1);
|
||||||
|
result->spelling[length - 1] = '\0';
|
||||||
|
}
|
||||||
|
result->line = lineNumber;
|
||||||
|
return SOURCE_LABEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
char mnemonic[TOKEN_CAPACITY];
|
||||||
|
if (length >= sizeof(mnemonic)) {
|
||||||
|
return SOURCE_BOUNDARY;
|
||||||
|
}
|
||||||
|
strcpy(mnemonic, token);
|
||||||
|
uppercase(mnemonic);
|
||||||
|
char *selector = strchr(mnemonic, '.');
|
||||||
|
if (selector) {
|
||||||
|
*selector = '\0';
|
||||||
|
}
|
||||||
|
if (getOpcode(mnemonic) == NOT_AN_OPCODE) {
|
||||||
|
return SOURCE_BOUNDARY;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(result, 0, sizeof(*result));
|
||||||
|
strcpy(result->mnemonic, mnemonic);
|
||||||
|
strncpy(result->spelling, token, sizeof(result->spelling) - 1);
|
||||||
|
result->line = lineNumber;
|
||||||
|
char *operand = strtok(NULL, " \t\r\n");
|
||||||
|
if (operand) {
|
||||||
|
strncpy(result->operand, operand, sizeof(result->operand) - 1);
|
||||||
|
result->hasLiteral = literalValue(operand, &result->literal);
|
||||||
|
}
|
||||||
|
return SOURCE_INSTRUCTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Saying that something is deliberate ----
|
||||||
|
//
|
||||||
|
// A line whose comment carries "splitlint: <reason>" is not reported on, and the reason is
|
||||||
|
// REQUIRED: a suppression with no explanation is a way to make a tool quiet rather than a
|
||||||
|
// way to say something. "splitlint[rule]: <reason>" silences one rule and leaves the line
|
||||||
|
// honest about every other, which matters once a line can trip more than one.
|
||||||
|
//
|
||||||
|
// Warnings are reported against a line that has already been read, sometimes an earlier
|
||||||
|
// one than the line in hand, so what is kept is the set of lines seen so far.
|
||||||
|
typedef struct {
|
||||||
|
int line;
|
||||||
|
char rule[RULE_CAPACITY]; // Empty for "every rule on this line".
|
||||||
|
int used;
|
||||||
|
} Suppression;
|
||||||
|
|
||||||
|
static Suppression *suppressions = NULL;
|
||||||
|
static int suppressedCount = 0;
|
||||||
|
static int suppressedRoom = 0;
|
||||||
|
static int suppressionsUsed = 0;
|
||||||
|
static int machineReadable = 0;
|
||||||
|
|
||||||
|
static void forgetSuppressions(void) {
|
||||||
|
free(suppressions);
|
||||||
|
suppressions = NULL;
|
||||||
|
suppressedCount = 0;
|
||||||
|
suppressedRoom = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int rememberSuppression(int line, const char *rule) {
|
||||||
|
if (suppressedCount == suppressedRoom) {
|
||||||
|
int room = suppressedRoom ? suppressedRoom * 2 : 16;
|
||||||
|
Suppression *grown = realloc(suppressions, (size_t)room * sizeof(Suppression));
|
||||||
|
if (!grown) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
suppressions = grown;
|
||||||
|
suppressedRoom = room;
|
||||||
|
}
|
||||||
|
suppressions[suppressedCount].line = line;
|
||||||
|
suppressions[suppressedCount].used = 0;
|
||||||
|
snprintf(suppressions[suppressedCount].rule, RULE_CAPACITY, "%s", rule ? rule : "");
|
||||||
|
suppressedCount++;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int isSuppressed(int line, const char *rule) {
|
||||||
|
for (int i = 0; i < suppressedCount; i++) {
|
||||||
|
if (suppressions[i].line != line) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (suppressions[i].rule[0] != '\0'
|
||||||
|
&& strcmp(suppressions[i].rule, rule) != 0) {
|
||||||
|
continue; // Names a different rule, so this one still applies.
|
||||||
|
}
|
||||||
|
suppressions[i].used = 1;
|
||||||
|
suppressionsUsed++;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A suppression that no longer suppresses anything is the thing the required reason was
|
||||||
|
// meant to prevent: an exception that outlived whatever made it necessary. Saying so is
|
||||||
|
// what keeps the count of them meaningful.
|
||||||
|
static int reportDeadSuppressions(const char *path) {
|
||||||
|
int dead = 0;
|
||||||
|
for (int i = 0; i < suppressedCount; i++) {
|
||||||
|
if (suppressions[i].used) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (machineReadable) {
|
||||||
|
printf("%s\t%d\t%s\t%s\t%s\n", path, suppressions[i].line,
|
||||||
|
"dead-suppression", "this line no longer has a warning to suppress",
|
||||||
|
"remove the splitlint comment");
|
||||||
|
} else {
|
||||||
|
printf("%s:%d: style: this line no longer has a warning to suppress"
|
||||||
|
" [dead-suppression]\n", path, suppressions[i].line);
|
||||||
|
printf(" help: remove the splitlint comment\n");
|
||||||
|
}
|
||||||
|
dead++;
|
||||||
|
}
|
||||||
|
return dead;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The reason has to be there and has to say something. A bare marker is refused rather
|
||||||
|
// than honoured, because a suppression nobody explained is the one that outlives whatever
|
||||||
|
// made it necessary.
|
||||||
|
//
|
||||||
|
// Returns 1 if the line is marked, 0 if it is not, and -1 if it is marked with nothing.
|
||||||
|
// A rule name in brackets, if there is one, is copied into `rule`.
|
||||||
|
static int suppressionOn(const char *rawLine, char *rule) {
|
||||||
|
rule[0] = '\0';
|
||||||
|
const char *at = strstr(rawLine, "splitlint");
|
||||||
|
if (!at) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
at += strlen("splitlint");
|
||||||
|
if (*at == '[') {
|
||||||
|
const char *close = strchr(at, ']');
|
||||||
|
if (!close || close == at + 1 || (size_t)(close - at - 1) >= RULE_CAPACITY) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(rule, at + 1, (size_t)(close - at - 1));
|
||||||
|
rule[close - at - 1] = '\0';
|
||||||
|
at = close + 1;
|
||||||
|
}
|
||||||
|
if (*at != ':') {
|
||||||
|
return 0; // "splitlint" inside ordinary prose is not a marker.
|
||||||
|
}
|
||||||
|
at++;
|
||||||
|
while (*at == ' ' || *at == '\t') {
|
||||||
|
at++;
|
||||||
|
}
|
||||||
|
return (*at == '\0' || *at == '\n' || *at == '\r') ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int warning(const char *path, const char *rule, int line, const char *message,
|
||||||
|
const char *help) {
|
||||||
|
if (isSuppressed(line, rule)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (machineReadable) {
|
||||||
|
// One line, tab separated, so nothing downstream has to read prose. This file's
|
||||||
|
// own diagnostics were parsed with regular expressions three times in a day
|
||||||
|
// before it had a shape anything could rely on.
|
||||||
|
printf("%s\t%d\t%s\t%s\t%s\n", path, line, rule, message, help);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// The rule goes in brackets at the end, the way a compiler names the flag that
|
||||||
|
// produced a warning, so that the sentence still reads as a sentence.
|
||||||
|
printf("%s:%d: style: %s [%s]\n", path, line, message, rule);
|
||||||
|
printf(" help: %s\n", help);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assignments whose only architectural effect is replacing one operand register. INA,
|
||||||
|
// INB and POP are intentionally absent: even when their result is overwritten, removing
|
||||||
|
// them would leave a byte unread from a device or an entry unconsumed from the stack.
|
||||||
|
static char assignedRegister(const SourceInstruction *instruction) {
|
||||||
|
static const char *aWriters[] = { "RSTA", "INIA", "MVQA", "LDA" };
|
||||||
|
static const char *bWriters[] = { "RSTB", "INIB", "MVQB", "LDB" };
|
||||||
|
for (size_t i = 0; i < sizeof(aWriters) / sizeof(aWriters[0]); i++) {
|
||||||
|
if (strcmp(instruction->mnemonic, aWriters[i]) == 0) {
|
||||||
|
return 'A';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < sizeof(bWriters) / sizeof(bWriters[0]); i++) {
|
||||||
|
if (strcmp(instruction->mnemonic, bWriters[i]) == 0) {
|
||||||
|
return 'B';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static int stopsFallthrough(const SourceInstruction *instruction) {
|
||||||
|
return strcmp(instruction->mnemonic, "BRI") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "BRD") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "RET") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "RRET") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "RETI") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "HALT") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int isDirectBranch(const SourceInstruction *instruction) {
|
||||||
|
static const char *branches[] = {
|
||||||
|
"BRI", "BRQ", "BRA", "BRB", "BRC", "BNQ", "BNA", "BNB", "BNC"
|
||||||
|
};
|
||||||
|
for (size_t i = 0; i < sizeof(branches) / sizeof(branches[0]); i++) {
|
||||||
|
if (strcmp(instruction->mnemonic, branches[i]) == 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void forgetRegisters(KnownRegisters *known) {
|
||||||
|
memset(known, 0, sizeof(*known));
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *selectorSuffix(const SourceInstruction *instruction) {
|
||||||
|
const char *suffix = strchr(instruction->spelling, '.');
|
||||||
|
return suffix ? suffix : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static int selectedPointer(const SourceInstruction *instruction) {
|
||||||
|
const char *selector = strchr(instruction->spelling, '.');
|
||||||
|
if (!selector) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
char *end;
|
||||||
|
long selected = strtol(selector + 1, &end, 10);
|
||||||
|
if (end == selector + 1 || selected < 0 || selected >= DATA_POINTERS) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (int)selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void forgetPointers(KnownPointer pointers[DATA_POINTERS]) {
|
||||||
|
memset(pointers, 0, sizeof(*pointers) * DATA_POINTERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int lintKnownPointers(const char *path, const SourceInstruction *instruction,
|
||||||
|
const KnownPointer pointers[DATA_POINTERS]) {
|
||||||
|
if (strcmp(instruction->mnemonic, "SETD") != 0 || !instruction->operand[0]) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int selected = selectedPointer(instruction);
|
||||||
|
const KnownPointer *pointer = &pointers[selected];
|
||||||
|
if (!pointer->known || pointer->offset != 0
|
||||||
|
|| strcmp(pointer->base, instruction->operand) != 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
char message[180];
|
||||||
|
snprintf(message, sizeof(message), "DP%d is already known to hold %s",
|
||||||
|
selected, instruction->operand);
|
||||||
|
return warning(path, "redundant-setd", instruction->line, message, "remove the redundant SETD");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void moveKnownPointer(KnownPointer *pointer, uint16_t amount, int downward) {
|
||||||
|
if (!pointer->known) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pointer->offset = downward ? (uint16_t)(pointer->offset - amount)
|
||||||
|
: (uint16_t)(pointer->offset + amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateKnownPointers(const SourceInstruction *instruction,
|
||||||
|
const KnownRegisters *registers,
|
||||||
|
KnownPointer pointers[DATA_POINTERS]) {
|
||||||
|
int selected = selectedPointer(instruction);
|
||||||
|
KnownPointer *pointer = &pointers[selected];
|
||||||
|
|
||||||
|
if (strcmp(instruction->mnemonic, "SETD") == 0) {
|
||||||
|
if (instruction->operand[0]) {
|
||||||
|
pointer->known = 1;
|
||||||
|
strncpy(pointer->base, instruction->operand, sizeof(pointer->base) - 1);
|
||||||
|
pointer->base[sizeof(pointer->base) - 1] = '\0';
|
||||||
|
pointer->offset = 0;
|
||||||
|
} else {
|
||||||
|
pointer->known = 0;
|
||||||
|
}
|
||||||
|
} else if (strcmp(instruction->mnemonic, "INCD") == 0) {
|
||||||
|
moveKnownPointer(pointer, 1, 0);
|
||||||
|
} else if (strcmp(instruction->mnemonic, "DECD") == 0) {
|
||||||
|
moveKnownPointer(pointer, 1, 1);
|
||||||
|
} else if (strcmp(instruction->mnemonic, "DPUP") == 0 && instruction->hasLiteral) {
|
||||||
|
moveKnownPointer(pointer, (uint16_t)instruction->literal, 0);
|
||||||
|
} else if (strcmp(instruction->mnemonic, "DPDN") == 0 && instruction->hasLiteral) {
|
||||||
|
moveKnownPointer(pointer, (uint16_t)instruction->literal, 1);
|
||||||
|
} else if (strcmp(instruction->mnemonic, "DPUA") == 0 && registers->aKnown) {
|
||||||
|
moveKnownPointer(pointer, registers->a, 0);
|
||||||
|
} else if (strcmp(instruction->mnemonic, "DPDA") == 0 && registers->aKnown) {
|
||||||
|
moveKnownPointer(pointer, registers->a, 1);
|
||||||
|
} else if (strcmp(instruction->mnemonic, "DPUW") == 0
|
||||||
|
&& registers->aKnown && registers->bKnown) {
|
||||||
|
moveKnownPointer(pointer, ((uint16_t)registers->a << 8) | registers->b, 0);
|
||||||
|
} else if (strcmp(instruction->mnemonic, "DPDW") == 0
|
||||||
|
&& registers->aKnown && registers->bKnown) {
|
||||||
|
moveKnownPointer(pointer, ((uint16_t)registers->a << 8) | registers->b, 1);
|
||||||
|
} else if (strcmp(instruction->mnemonic, "LDD") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "POPD") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "MVSD") == 0) {
|
||||||
|
pointer->known = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- A CALL ends every claim, and that is a deliberate loss of precision ----
|
||||||
|
//
|
||||||
|
// CALL really does save and restore A, B and Data Pointers 0 to 2, so a pointer set
|
||||||
|
// before one is genuinely still set after it, and this used to say so - forgetting
|
||||||
|
// only DP3, which CALL does not restore. It was right, and the advice it produced was
|
||||||
|
// not safe to take.
|
||||||
|
//
|
||||||
|
// 122 of the 178 redundant SETDs it found across the corpus were redundant ONLY
|
||||||
|
// because of that restore. Removing them is correct today and becomes a wrong-pointer
|
||||||
|
// bug the moment the callee is converted from CALL to RCAL - which is not a
|
||||||
|
// hypothetical, it is what RCAL was added to this machine for, and converting the hot
|
||||||
|
// helpers was measured at close to halving the assembler's memory traffic. Worse, the
|
||||||
|
// linter would go quiet rather than complain: it forgets everything across an RCAL, so
|
||||||
|
// it would simply stop reporting while the removals stayed removed.
|
||||||
|
//
|
||||||
|
// So a claim now ends at any call, the way a claim about carry already did. Fifty
|
||||||
|
// four recommendations that stay true are worth more than a hundred and seventy eight
|
||||||
|
// that are conditional on a change the project intends to make.
|
||||||
|
if (strcmp(instruction->mnemonic, "CALL") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "RCAL") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "SWI") == 0) {
|
||||||
|
forgetPointers(pointers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int lintKnownRegisters(const char *path, const SourceInstruction *instruction,
|
||||||
|
const SourceInstruction *previous,
|
||||||
|
const KnownRegisters *known) {
|
||||||
|
int warnings = 0;
|
||||||
|
char message[180];
|
||||||
|
char help[160];
|
||||||
|
char previousAssignment = assignedRegister(previous);
|
||||||
|
|
||||||
|
if (known->aKnown && previousAssignment != 'A'
|
||||||
|
&& ((strcmp(instruction->mnemonic, "RSTA") == 0 && known->a == 0)
|
||||||
|
|| (strcmp(instruction->mnemonic, "INIA") == 0
|
||||||
|
&& instruction->hasLiteral && known->a == instruction->literal))) {
|
||||||
|
snprintf(message, sizeof(message), "%s leaves A at its known value of %u",
|
||||||
|
instruction->spelling, known->a);
|
||||||
|
warnings += warning(path, "redundant-assignment", instruction->line, message, "remove the redundant assignment");
|
||||||
|
}
|
||||||
|
if (known->bKnown && previousAssignment != 'B'
|
||||||
|
&& ((strcmp(instruction->mnemonic, "RSTB") == 0 && known->b == 0)
|
||||||
|
|| (strcmp(instruction->mnemonic, "INIB") == 0
|
||||||
|
&& instruction->hasLiteral && known->b == instruction->literal))) {
|
||||||
|
snprintf(message, sizeof(message), "%s leaves B at its known value of %u",
|
||||||
|
instruction->spelling, known->b);
|
||||||
|
warnings += warning(path, "redundant-assignment", instruction->line, message, "remove the redundant assignment");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (known->aKnown && (strcmp(instruction->mnemonic, "DPUA") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "DPDA") == 0)) {
|
||||||
|
if (known->a == 0) {
|
||||||
|
warnings += warning(path, "pointer-offset", instruction->line, "pointer offset is known to be zero",
|
||||||
|
"remove the pointer instruction");
|
||||||
|
} else if (known->a == 1) {
|
||||||
|
snprintf(help, sizeof(help), "use %s%s",
|
||||||
|
strcmp(instruction->mnemonic, "DPUA") == 0 ? "INCD" : "DECD",
|
||||||
|
selectorSuffix(instruction));
|
||||||
|
warnings += warning(path, "pointer-offset", instruction->line, "pointer offset is known to be one", help);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (known->aKnown && known->bKnown
|
||||||
|
&& (strcmp(instruction->mnemonic, "DPUW") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "DPDW") == 0)) {
|
||||||
|
uint16_t word = ((uint16_t)known->a << 8) | known->b;
|
||||||
|
if (word == 0) {
|
||||||
|
warnings += warning(path, "pointer-offset", instruction->line, "word-sized pointer offset is known to be zero",
|
||||||
|
"remove the pointer instruction");
|
||||||
|
} else if (word == 1) {
|
||||||
|
snprintf(help, sizeof(help), "use %s%s",
|
||||||
|
strcmp(instruction->mnemonic, "DPUW") == 0 ? "INCD" : "DECD",
|
||||||
|
selectorSuffix(instruction));
|
||||||
|
warnings += warning(path, "pointer-offset", instruction->line, "word-sized pointer offset is known to be one", help);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateKnownRegisters(const SourceInstruction *instruction,
|
||||||
|
KnownRegisters *known) {
|
||||||
|
if (strcmp(instruction->mnemonic, "SHL") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "SHR") == 0) {
|
||||||
|
if (known->aKnown && known->bKnown) {
|
||||||
|
uint16_t word = ((uint16_t)known->a << 8) | known->b;
|
||||||
|
if (strcmp(instruction->mnemonic, "SHL") == 0) {
|
||||||
|
word = (uint16_t)((word << 1) | (word >> 15));
|
||||||
|
} else {
|
||||||
|
word = (uint16_t)((word >> 1) | (word << 15));
|
||||||
|
}
|
||||||
|
known->a = (uint8_t)(word >> 8);
|
||||||
|
known->b = (uint8_t)word;
|
||||||
|
} else {
|
||||||
|
known->aKnown = 0;
|
||||||
|
known->bKnown = 0;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(instruction->mnemonic, "RSTA") == 0) {
|
||||||
|
known->aKnown = 1;
|
||||||
|
known->a = 0;
|
||||||
|
} else if (strcmp(instruction->mnemonic, "INIA") == 0 && instruction->hasLiteral) {
|
||||||
|
known->aKnown = 1;
|
||||||
|
known->a = (uint8_t)instruction->literal;
|
||||||
|
} else if (strcmp(instruction->mnemonic, "INCA") == 0) {
|
||||||
|
if (known->aKnown) known->a++;
|
||||||
|
} else if (strcmp(instruction->mnemonic, "DECA") == 0) {
|
||||||
|
if (known->aKnown) known->a--;
|
||||||
|
} else if (strcmp(instruction->mnemonic, "MVQA") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "LDA") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "POPA") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "INA") == 0) {
|
||||||
|
known->aKnown = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(instruction->mnemonic, "RSTB") == 0) {
|
||||||
|
known->bKnown = 1;
|
||||||
|
known->b = 0;
|
||||||
|
} else if (strcmp(instruction->mnemonic, "INIB") == 0 && instruction->hasLiteral) {
|
||||||
|
known->bKnown = 1;
|
||||||
|
known->b = (uint8_t)instruction->literal;
|
||||||
|
} else if (strcmp(instruction->mnemonic, "INCB") == 0) {
|
||||||
|
if (known->bKnown) known->b++;
|
||||||
|
} else if (strcmp(instruction->mnemonic, "DECB") == 0) {
|
||||||
|
if (known->bKnown) known->b--;
|
||||||
|
} else if (strcmp(instruction->mnemonic, "MVQB") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "LDB") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "POPB") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "INB") == 0) {
|
||||||
|
known->bKnown = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A call ends what is known about A and B for the same reason it ends what is known
|
||||||
|
// about a pointer: CALL's convention saves them, so the knowledge is real, but it is
|
||||||
|
// knowledge about the CALLEE rather than about the code in hand - and it stops being
|
||||||
|
// true the day that callee is reached with RCAL instead. See updateKnownPointers.
|
||||||
|
if (strcmp(instruction->mnemonic, "CALL") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "RCAL") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "SWI") == 0) {
|
||||||
|
forgetRegisters(known);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int lintKnownCarry(const char *path, const SourceInstruction *instruction,
|
||||||
|
KnownCarry carry) {
|
||||||
|
if (carry == CARRY_UNKNOWN) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (strcmp(instruction->mnemonic, "CCF") == 0 && carry == CARRY_CLEAR) {
|
||||||
|
return warning(path, "redundant-ccf", instruction->line, "carry is already known to be clear",
|
||||||
|
"remove the redundant CCF");
|
||||||
|
}
|
||||||
|
if (strcmp(instruction->mnemonic, "BRC") == 0) {
|
||||||
|
if (carry == CARRY_SET) {
|
||||||
|
return warning(path, "known-branch", instruction->line, "BRC is always taken because carry is known set",
|
||||||
|
"use BRI");
|
||||||
|
}
|
||||||
|
return warning(path, "known-branch", instruction->line, "BRC is never taken because carry is known clear",
|
||||||
|
"remove the branch");
|
||||||
|
}
|
||||||
|
if (strcmp(instruction->mnemonic, "BNC") == 0) {
|
||||||
|
if (carry == CARRY_CLEAR) {
|
||||||
|
return warning(path, "known-branch", instruction->line, "BNC is always taken because carry is known clear",
|
||||||
|
"use BRI");
|
||||||
|
}
|
||||||
|
return warning(path, "known-branch", instruction->line, "BNC is never taken because carry is known set",
|
||||||
|
"remove the branch");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateKnownCarry(const SourceInstruction *instruction,
|
||||||
|
const KnownRegisters *registers,
|
||||||
|
KnownCarry *carry) {
|
||||||
|
if (strcmp(instruction->mnemonic, "CCF") == 0) {
|
||||||
|
*carry = CARRY_CLEAR;
|
||||||
|
} else if (strcmp(instruction->mnemonic, "INCA") == 0) {
|
||||||
|
*carry = registers->aKnown ? (registers->a == 0xFF ? CARRY_SET : CARRY_CLEAR)
|
||||||
|
: CARRY_UNKNOWN;
|
||||||
|
} else if (strcmp(instruction->mnemonic, "INCB") == 0) {
|
||||||
|
*carry = registers->bKnown ? (registers->b == 0xFF ? CARRY_SET : CARRY_CLEAR)
|
||||||
|
: CARRY_UNKNOWN;
|
||||||
|
} else if (strcmp(instruction->mnemonic, "DECA") == 0) {
|
||||||
|
*carry = registers->aKnown ? (registers->a == 0 ? CARRY_SET : CARRY_CLEAR)
|
||||||
|
: CARRY_UNKNOWN;
|
||||||
|
} else if (strcmp(instruction->mnemonic, "DECB") == 0) {
|
||||||
|
*carry = registers->bKnown ? (registers->b == 0 ? CARRY_SET : CARRY_CLEAR)
|
||||||
|
: CARRY_UNKNOWN;
|
||||||
|
} else if (strcmp(instruction->mnemonic, "ADD") == 0) {
|
||||||
|
if (registers->aKnown && registers->bKnown && *carry != CARRY_UNKNOWN) {
|
||||||
|
unsigned result = registers->a + registers->b + (unsigned)*carry;
|
||||||
|
*carry = result > 255 ? CARRY_SET : CARRY_CLEAR;
|
||||||
|
} else {
|
||||||
|
*carry = CARRY_UNKNOWN;
|
||||||
|
}
|
||||||
|
} else if (strcmp(instruction->mnemonic, "SUB") == 0) {
|
||||||
|
if (registers->aKnown && registers->bKnown && *carry != CARRY_UNKNOWN) {
|
||||||
|
int result = (int)registers->a - (int)registers->b - (int)*carry;
|
||||||
|
*carry = result < 0 ? CARRY_SET : CARRY_CLEAR;
|
||||||
|
} else {
|
||||||
|
*carry = CARRY_UNKNOWN;
|
||||||
|
}
|
||||||
|
} else if (strcmp(instruction->mnemonic, "CALL") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "RCAL") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "SWI") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "RET") == 0
|
||||||
|
|| strcmp(instruction->mnemonic, "RETI") == 0) {
|
||||||
|
*carry = CARRY_UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int lintInstruction(const char *path, const SourceInstruction *previous,
|
||||||
|
const SourceInstruction *current) {
|
||||||
|
int warnings = 0;
|
||||||
|
|
||||||
|
if (current->hasLiteral && current->literal == 0
|
||||||
|
&& strcmp(current->mnemonic, "INIA") == 0) {
|
||||||
|
warnings += warning(path, "zero-load", current->line, "loading zero into A takes two bytes", "use RSTA");
|
||||||
|
} else if (current->hasLiteral && current->literal == 0
|
||||||
|
&& strcmp(current->mnemonic, "INIB") == 0) {
|
||||||
|
warnings += warning(path, "zero-load", current->line, "loading zero into B takes two bytes", "use RSTB");
|
||||||
|
}
|
||||||
|
|
||||||
|
char previousAssignment = assignedRegister(previous);
|
||||||
|
if (previousAssignment && previousAssignment == assignedRegister(current)) {
|
||||||
|
char message[180];
|
||||||
|
snprintf(message, sizeof(message), "%s assigns %c, but %s replaces it immediately",
|
||||||
|
previous->spelling, previousAssignment, current->spelling);
|
||||||
|
warnings += warning(path, "dead-assignment", previous->line, message, "remove the first assignment");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Q already has direct moves to both operand registers. The older stack transfer
|
||||||
|
// idiom does the same job, but costs a write, a read, and an extra instruction. This
|
||||||
|
// is precisely the kind of sequence the linter exists to find in code written before
|
||||||
|
// the newer instruction was available.
|
||||||
|
if (strcmp(previous->mnemonic, "PSHQ") == 0
|
||||||
|
&& strcmp(current->mnemonic, "POPA") == 0) {
|
||||||
|
warnings += warning(path, "q-through-stack", previous->line, "moving Q to A through the stack takes two instructions",
|
||||||
|
"use MVQA");
|
||||||
|
} else if (strcmp(previous->mnemonic, "PSHQ") == 0
|
||||||
|
&& strcmp(current->mnemonic, "POPB") == 0) {
|
||||||
|
warnings += warning(path, "q-through-stack", previous->line, "moving Q to B through the stack takes two instructions",
|
||||||
|
"use MVQB");
|
||||||
|
} else if (strcmp(previous->mnemonic, "PSHA") == 0
|
||||||
|
&& strcmp(current->mnemonic, "POPA") == 0) {
|
||||||
|
warnings += warning(path, "self-push-pop", previous->line, "pushing A and immediately restoring it leaves A unchanged",
|
||||||
|
"remove both instructions if the stack write is not intentional");
|
||||||
|
} else if (strcmp(previous->mnemonic, "PSHB") == 0
|
||||||
|
&& strcmp(current->mnemonic, "POPB") == 0) {
|
||||||
|
warnings += warning(path, "self-push-pop", previous->line, "pushing B and immediately restoring it leaves B unchanged",
|
||||||
|
"remove both instructions if the stack write is not intentional");
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int lintFile(const char *path) {
|
||||||
|
FILE *file = fopen(path, "r");
|
||||||
|
if (!file) {
|
||||||
|
fprintf(stderr, "%s: error: could not open source file\n", path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char line[LINE_CAPACITY];
|
||||||
|
int lineNumber = 0;
|
||||||
|
int warnings = 0;
|
||||||
|
int havePrevious = 0;
|
||||||
|
int fallthroughStopped = 0;
|
||||||
|
int havePendingBranch = 0;
|
||||||
|
SourceInstruction pendingBranch;
|
||||||
|
KnownRegisters known = {0};
|
||||||
|
KnownPointer pointers[DATA_POINTERS] = {0};
|
||||||
|
KnownCarry carry = CARRY_UNKNOWN;
|
||||||
|
SourceInstruction previous;
|
||||||
|
forgetSuppressions();
|
||||||
|
while (fgets(line, sizeof(line), file)) {
|
||||||
|
lineNumber++;
|
||||||
|
// Refuse to silently analyze only the first part of an unusually long line.
|
||||||
|
if (!strchr(line, '\n') && !feof(file)) {
|
||||||
|
fprintf(stderr, "%s:%d: error: line is longer than %d characters\n",
|
||||||
|
path, lineNumber, LINE_CAPACITY - 2);
|
||||||
|
fclose(file);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// BEFORE THE COMMENT IS STRIPPED, because the marker lives in one. Recorded for
|
||||||
|
// the line rather than acted on here: a warning is reported against a line that
|
||||||
|
// has already been read, and not always the one in hand.
|
||||||
|
char suppressedRule[RULE_CAPACITY];
|
||||||
|
int marked = suppressionOn(line, suppressedRule);
|
||||||
|
if (marked < 0) {
|
||||||
|
fprintf(stderr, "%s:%d: error: splitlint: needs a reason after it\n",
|
||||||
|
path, lineNumber);
|
||||||
|
fclose(file);
|
||||||
|
forgetSuppressions();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (marked && !rememberSuppression(lineNumber, suppressedRule)) {
|
||||||
|
fprintf(stderr, "%s: error: out of memory recording a suppression\n", path);
|
||||||
|
fclose(file);
|
||||||
|
forgetSuppressions();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SourceInstruction current;
|
||||||
|
SourceLine sourceLine = parseInstruction(line, lineNumber, ¤t);
|
||||||
|
if (sourceLine != SOURCE_INSTRUCTION) {
|
||||||
|
havePrevious = 0;
|
||||||
|
if (sourceLine == SOURCE_LABEL) {
|
||||||
|
if (havePendingBranch && current.spelling[0]
|
||||||
|
&& strcmp(pendingBranch.operand, current.spelling) == 0) {
|
||||||
|
warnings += warning(path, "branch-to-next", pendingBranch.line,
|
||||||
|
"branch target is the next labeled address",
|
||||||
|
"remove the branch");
|
||||||
|
havePendingBranch = 0;
|
||||||
|
}
|
||||||
|
fallthroughStopped = 0;
|
||||||
|
forgetRegisters(&known);
|
||||||
|
forgetPointers(pointers);
|
||||||
|
carry = CARRY_UNKNOWN;
|
||||||
|
} else if (sourceLine == SOURCE_BOUNDARY) {
|
||||||
|
fallthroughStopped = 0;
|
||||||
|
havePendingBranch = 0;
|
||||||
|
forgetRegisters(&known);
|
||||||
|
forgetPointers(pointers);
|
||||||
|
carry = CARRY_UNKNOWN;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Reaching another instruction before the target label makes this something
|
||||||
|
// other than a branch to the next address.
|
||||||
|
havePendingBranch = 0;
|
||||||
|
if (fallthroughStopped) {
|
||||||
|
warnings += warning(path, "unreachable", current.line,
|
||||||
|
"no ordinary fallthrough reaches this instruction",
|
||||||
|
"remove it, or give an intentional indirect entry point a label");
|
||||||
|
}
|
||||||
|
SourceInstruction empty = {0};
|
||||||
|
warnings += lintKnownRegisters(path, ¤t,
|
||||||
|
havePrevious ? &previous : &empty, &known);
|
||||||
|
warnings += lintKnownPointers(path, ¤t, pointers);
|
||||||
|
warnings += lintKnownCarry(path, ¤t, carry);
|
||||||
|
warnings += lintInstruction(path, havePrevious ? &previous : &empty, ¤t);
|
||||||
|
if (stopsFallthrough(¤t)) {
|
||||||
|
fallthroughStopped = 1;
|
||||||
|
}
|
||||||
|
if (isDirectBranch(¤t) && current.operand[0]) {
|
||||||
|
pendingBranch = current;
|
||||||
|
havePendingBranch = 1;
|
||||||
|
}
|
||||||
|
updateKnownPointers(¤t, &known, pointers);
|
||||||
|
updateKnownCarry(¤t, &known, &carry);
|
||||||
|
updateKnownRegisters(¤t, &known);
|
||||||
|
previous = current;
|
||||||
|
havePrevious = 1;
|
||||||
|
}
|
||||||
|
if (ferror(file)) {
|
||||||
|
fprintf(stderr, "%s: error: could not read source file\n", path);
|
||||||
|
fclose(file);
|
||||||
|
forgetSuppressions();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
fclose(file);
|
||||||
|
warnings += reportDeadSuppressions(path);
|
||||||
|
forgetSuppressions();
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
int fatalWarnings = 0;
|
||||||
|
int firstFile = 1;
|
||||||
|
while (firstFile < argc && argv[firstFile][0] == '-' && argv[firstFile][1] == '-') {
|
||||||
|
if (strcmp(argv[firstFile], "--fatal-warnings") == 0) {
|
||||||
|
fatalWarnings = 1;
|
||||||
|
} else if (strcmp(argv[firstFile], "--machine") == 0) {
|
||||||
|
machineReadable = 1;
|
||||||
|
} else if (strcmp(argv[firstFile], "--help") == 0) {
|
||||||
|
usage(argv[0]);
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "%s: error: unknown option \"%s\"\n", argv[0], argv[firstFile]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
firstFile++;
|
||||||
|
}
|
||||||
|
if (argc <= firstFile || strcmp(argv[firstFile], "-h") == 0) {
|
||||||
|
usage(argv[0]);
|
||||||
|
return argc <= firstFile ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int warnings = 0;
|
||||||
|
int files = 0;
|
||||||
|
for (int i = firstFile; i < argc; i++) {
|
||||||
|
int found = lintFile(argv[i]);
|
||||||
|
if (found < 0) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
warnings += found;
|
||||||
|
files++;
|
||||||
|
}
|
||||||
|
// Machine readable output is warnings and nothing else, so that a reader can take
|
||||||
|
// every line the same way rather than having to know which ones are prose.
|
||||||
|
if (!machineReadable) {
|
||||||
|
if (warnings) {
|
||||||
|
printf("%d style warning%s found.\n", warnings, warnings == 1 ? "" : "s");
|
||||||
|
} else {
|
||||||
|
// SAYING SO IS THE POINT. A tool that exits silently has not told you it
|
||||||
|
// found nothing, it has told you nothing at all - and the two look identical
|
||||||
|
// from outside. This says what it looked at and what it looked for.
|
||||||
|
printf("No style warnings: %d file%s checked against %d rule%s.\n",
|
||||||
|
files, files == 1 ? "" : "s",
|
||||||
|
RULE_COUNT, RULE_COUNT == 1 ? "" : "s");
|
||||||
|
}
|
||||||
|
// Said out loud rather than left implicit. A suppression is a claim that something
|
||||||
|
// is deliberate, and a count of them is how anybody notices the claim has spread.
|
||||||
|
if (suppressionsUsed) {
|
||||||
|
printf("%d warning%s suppressed by splitlint comments.\n",
|
||||||
|
suppressionsUsed, suppressionsUsed == 1 ? "" : "s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fatalWarnings && warnings ? 1 : 0;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user