diff --git a/Programs/CosmOS/Apps/Walk.asm b/Programs/CosmOS/Apps/Walk.asm new file mode 100644 index 0000000..bda2d4b --- /dev/null +++ b/Programs/CosmOS/Apps/Walk.asm @@ -0,0 +1,137 @@ +; A program that says what is on the disk. +; +; Which no program could do until now. Everything the system offered took a name a program +; already knew - read it, save it, rename it, delete it, ask how big it is - and there was no +; way to ask what names there are. dir could list only because it lives in the shell and calls +; the filesystem directly. So a file manager, a backup, and the package manager still to come +; were all unwritable for want of two services. +; +; This is those two, used as simply as they can be: walk, say what each thing is, and ask +; about it. +; +; > Walk +; f Say.sbx 1 +; d Apps 0 +; u halfsaved.txt 3 +; +; ---- And it asks about each one AS IT GOES, which is the point ---- +; +; Where a walk has got to is the system's, and so is where the last file it was asked about +; lives - the same eight bytes of filesystem state. A program that walks a directory and asks +; osFileInfo about each entry is using both at once, which is the obvious thing to write and +; the thing that would quietly go wrong if the two shared a position. +; +; Calling osFileInfo between two steps of the walk is therefore not decoration here. It is the +; check, and a walk that came back wrong afterwards would show up as a short listing or a +; repeated name rather than as an error. + +#Include services.asm + +#Program + + #Base 0x5000 + +start: + SETD.0 Name + INIB 0d24 + SWI osDirFirst + BRI walkCheck + +walkStep: + SETD.0 Name + INIB 0d24 + SWI osDirNext + +walkCheck: + ; The kind is written down before anything else wants the registers: the comparison below + ; is an ALU operation and Q is where its answer goes. + MVQA + SETD.0 Kind + STA.0 + INIB 0xFF + CCF + SUB + BRQ walkDone + + ; f, d or u. A chain rather than a table, because three is not enough to index - and A is + ; still the kind, since SUB writes Q and leaves it alone. + BRA walkFile + INIB 0d1 + CCF + SUB + BRQ walkDirectory + SETD.0 Unfinished + BRI walkSay +walkDirectory: + SETD.0 Directory + BRI walkSay +walkFile: + SETD.0 File + +walkSay: + SWI osPrintString + SETD.0 Name + SWI osPrintString + SETD.0 Space + SWI osPrintString + + ; ---- And how big it is, asked BETWEEN two steps of the walk ---- + ; + ; The name is the only thing the walk hands over, which is what keeps it from being a record + ; both sides have to agree the shape of. A program that wants more asks about the name, the + ; same way anything else does. + ; + ; A directory is not asked about. osFileInfo answers for one perfectly well and says nought + ; blocks, which is true and reads as a size - and nought is a size a file can genuinely + ; have, so the two would be indistinguishable in the listing. + SETD.0 Kind + LDA.0 + INIB 0d1 + CCF + SUB + BRQ walkNoSize + + SETD.0 Name + SWI osFileInfo + BNQ walkNoSize + + PSHD.3 + POPB + POPA + SWI osPrintNumber + BRI walkEnded + +walkNoSize: + SETD.0 NoSize + SWI osPrintString + +walkEnded: + SETD.0 NewLine + SWI osPrintString + BRI walkStep + +walkDone: + RSTA + SWI osExit + +#Data + + #Base 0x3000 + +File: +"f " +Directory: +"d " +Unfinished: +"u " +Space: +" " +NoSize: +"-" +NewLine: + 0x0A 0x00 + +Kind: + 0x00 +Name: + #Reserve 0d24 diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index 275be4d..b947ba9 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -791,6 +791,7 @@ from every assembly file in it. Several are old programs written for the bare ma | 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. | | Play | Four voices on one clock, which is what music is and one channel cannot be. The timer keeps a tick and every voice keeps its own place in its own track and its own count of how much longer the note it is holding lasts, so the parts move at four different rates and share nothing but the beat. A track is pairs of bytes, what to play and how many ticks it lasts: 1 to 127 is a MIDI note, zero is a rest, and 255 ends it - MIDI stops at 127, so neither of those had to be invented. A note's duration is its whole life and the gate goes down when the count runs out, which means a gap between two notes is written as a rest rather than invented by the player out of some fraction it decided on. Each voice loads an instrument of its own before a note is played - an oboe for the melody, strings under it, a square wave for the bass and a kalimba for the arpeggio - out of the format SoundPatch writes, which the player reads as a count and that many parameter and value pairs and understands nothing else about. Four patches can be up at once because a patch belongs to its channel; Kalimba has an LFO switched off and the other three have one on, and under a device where the LFOs belonged to the whole machine the last patch loaded would have imposed its setting on every part. A voice does not play one long track: it walks an ORDER LIST of its own, a table of sequence addresses, and takes the next one when a sequence runs out. That is where repetition comes from and it costs no notation - the bass plays the same sequence in the first bar and the last, written once. The four columns are how it reads and how a tracker would show it; per voice is how it is stored, because a voice's order cursor is then a pointer it advances by itself. Sequence and patch names are INDICES through two tables, which are the only places an address lives - so a tune read from a file will need its base added to two arrays and nothing else, rather than a loader that walks every sequence looking for addresses to correct. A sequence can also carry commands, which take no time at all: 0x80 plays the rest of that voice on a different patch, which is how the melody's last bar becomes a swell rather than a reed. Nothing keeps the voices together except that their sequences add up to the same length, which is the first thing a compiler should check. The patch each voice starts on is declared rather than assumed, because a voice given no instrument would play on whatever the device woke up with. `Play ` reads a tune and plays that; `Play` on its own plays the one built into it. A tune file is "SBTU", a version, the tick in cycles, and offsets to a patch table, a sequence table and four order lists - everything in it an OFFSET from wherever it was put, so loading one is adding the base to two tables and pointing four voices at their order lists. No sequence is walked and nothing inside one is an address, which is what makes a malformed tune something that plays wrongly rather than something that takes the loader with it; the magic is checked first, because the loader follows what the offsets name. The patch each voice starts on is in the header, because a starting instrument is state. The engine that plays it is Libraries/player.asm rather than this program. It spends over ninety nine per cent of its time asleep, because a beat is something to be woken by rather than counted up to. | | Say | Prints whatever it was told, which is the shortest thing that shows osArgument working. | +| Walk | Says what is in the working directory, one line each: what kind of thing it is, its name, and how many blocks. The shortest thing that shows osDirFirst and osDirNext working, and the first program that could see a directory at all. | | Where | Says the path it was loaded from, and with a name after it, the path of that name beside it. The shortest thing that shows osWhereAmI and path.asm working, and the answer follows the program rather than whoever ran 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. | @@ -1350,6 +1351,8 @@ Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, w | 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. | | osTakeScreen | Says this program is about to use the whole screen and would like what is on it put back when it exits. Q is zero if that was arranged; anything else means it was not, which is the ordinary answer on a machine with no volatile drive. See Giving The Screen Back. | | osWhereAmI | DP0 names somewhere to put the path the program was loaded from, B says how much room there is. See Where A Program Came From. | +| osDirFirst | DP0 names somewhere to put an entry's name, B says how much room there is. Q says what it is: 0 a file, 1 a directory, 2 an unfinished save, 0xFF nothing more. Starts a walk of the working directory. See Seeing What Is On The Disk. | +| osDirNext | The same, for the entry after it. | | 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. | @@ -1490,6 +1493,41 @@ directory stays the person's; this is how a program finds its own things. `Programs/CosmOS/Apps/Where.asm` is the whole of it in one program, and exists to be run rather than read. +## Seeing What Is On The Disk: + +Every other file service takes a name a program already knows - read it, save it, rename it, +delete it, ask how big it is. **None of them could find out what names there are.** `dir` +could list only because it lives in the shell and calls the filesystem directly, so nothing +loaded could list anything at all: a file manager, a backup and a package manager were each +unwritable for want of two services. + +``` + SETD.0 Name + INIB 0d24 + SWI osDirFirst + ... + SWI osDirNext +``` + +**Q answers the kind rather than a yes or no**, so one value says both whether there is an +entry and what it is: 0 a file, 1 a directory, 2 a save that stopped before it committed, +0xFF nothing more. A caller that only wants names tests for 0xFF and ignores the rest. + +**The size is not in it.** A walk hands back a name, and a program that wants the size asks +`osFileInfo` about that name. The alternative is a record in memory whose shape both sides +have to agree on, which is exactly what these services went out of their way to avoid for +file sizes - and most callers want names and nothing else. + +**One walk at a time, and the system holds where it has got to**, the same bargain +`osFileStart` makes for writing. A program that starts a second walk before finishing the +first gets the second; there is one position, not a handle per caller. The state is small, so +a program that stops in the middle leaves nothing behind to clean up. + +`Programs/CosmOS/Apps/Walk.asm` is the whole of it in one program. It asks `osFileInfo` about +each entry **between two steps of the walk**, which is not decoration: where a walk has got to +and where the last file asked about lives are both the system's, and two things sharing one +position would show as a listing that stopped early or said a name twice. + ## An Application With Things Of Its Own: A simple program is one file and lives in `/Apps`. One with **assets** gets a directory of diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index 0c29f7a..e509ffd 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -6184,6 +6184,82 @@ fileForget: ; Blocks, not bytes, and that is forced rather than chosen: a file on a sixteen megabyte ; disk is up to twenty four bits long, which does not fit in a pointer. Blocks do, and the ; bytes in the last one come back from osFileBlock when the reader gets there. +; ---- Walking a directory on a program's behalf ---- +; +; The one thing the filesystem could do that no program could ask for. dir has always been +; able to list because it lives in here; a loaded program had no way to find out what names +; exist at all, which left a file manager, a backup, and the package manager still to come +; all unable to be written. +; +; Two entry points into one routine, because the difference between them is a single call. +; +; Q ANSWERS THE KIND, so one value says both whether there is an entry and what it is: 0 a +; file, 1 a directory, 2 a save that stopped before it committed, 0xFF nothing more. A +; caller that only wants names tests for 0xFF and ignores the rest. +handleDirFirst: + SETD.2 DiskReady + LDA.2 + BRA dirWalkNoMore + CALL sbfsFirst + BRI dirWalkGot + +handleDirNext: + SETD.2 DiskReady + LDA.2 + BRA dirWalkNoMore + CALL sbfsNext + +dirWalkGot: + BNQ dirWalkNoMore + + ; The name first, because working out the kind wants the registers. B is still the room + ; the caller asked for: a CALL puts it back, and nothing above has taken it. + PSHD.0 + POPD.1 + SETD.0 SbfsName + CALL copyText + + ; ---- What kind of thing it is ---- + ; + ; Asked in the same order dir asks it. An unfinished save is looked at FIRST because it is + ; the one thing here that is not really a file yet, and it is SHOWN rather than hidden: + ; the bytes are all there under that name, so a program that can see it is a program that + ; can rename it back. + SETD.2 SbfsFoundFlags + LDA.2 + INIB 0x04 + AND + BNQ dirWalkUnfinished + + SETD.2 SbfsFoundFlags + LDA.2 + INIB 0x02 + AND + BNQ dirWalkDirectory + + RSTA + BRI dirWalkAnswer + +dirWalkDirectory: + INIA 0d1 + BRI dirWalkAnswer + +dirWalkUnfinished: + INIA 0d2 + BRI dirWalkAnswer + +dirWalkNoMore: + ; A machine with no disk has nothing to walk, and says the same thing as a walk that has + ; run out. There is no third answer worth telling apart: a caller that cannot list is a + ; caller that lists nothing either way. + INIA 0xFF + +dirWalkAnswer: + RSTB + CCF + ADD ; A is the answer, so Q becomes it. + SRET + handleFileInfo: SETD.2 DiskReady LDA.2 @@ -8652,6 +8728,8 @@ CommandLine: osLastStatus handleLastStatus osTakeScreen handleTakeScreen osWhereAmI handleWhereAmI + osDirFirst handleDirFirst + osDirNext handleDirNext osBootState handleBootState osBootSettle handleBootSettle Device 0x20 diskDone diff --git a/Programs/CosmOS/Source/services.asm b/Programs/CosmOS/Source/services.asm index b4e7a8f..c4cdacd 100644 --- a/Programs/CosmOS/Source/services.asm +++ b/Programs/CosmOS/Source/services.asm @@ -208,3 +208,37 @@ ; own would mean "Play mytune.tune", typed by somebody in their own directory, looked in ; Play's. The working directory stays the person's; this is how a program finds its own. osWhereAmI 0d37 + +; ---- Seeing what is on the disk ---- +; +; Everything above takes a name a program already knows. NOTHING HERE COULD FIND OUT WHAT +; NAMES THERE ARE - a machine whose programs can read, write, rename and delete files and +; cannot ask what files exist. dir could only list because it lives in the shell and calls +; the filesystem directly; no loaded program could list anything at all. +; +; DP0 says where to put the name and B how much room there is, counting the zero, the same +; bargain osArgument and osWhereAmI offer. +; +; Q ANSWERS THE KIND RATHER THAN A YES OR NO, which is one value carrying both "is there +; one" and "what is it": +; +; 0 a file +; 1 a directory +; 2 a save that stopped before it committed +; 0xFF there are no more +; +; ---- And the size is not in it ---- +; +; A walk hands back a NAME, and a program that wants the size of what it found asks +; osFileInfo about that name. The alternative is a record in memory whose shape both sides +; have to agree on, which is exactly what this file went out of its way to avoid for file +; sizes - and most callers of this want names and nothing else. +; +; ---- One walk at a time, and the system holds it ---- +; +; Where a walk has got to is the system's, the way an open write is. A program that starts a +; second walk before finishing the first gets the second; there is one position, not a handle +; per caller. That is the same bargain osFileStart makes and for the same reason: the state +; is small, and a program that stops in the middle leaves nothing behind to clean up. + osDirFirst 0d38 + osDirNext 0d39 diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index da0a850..f7a97da 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -125,7 +125,7 @@ from `make`, not from here. ### 1. Recorded output `Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares -everything it printed against a file in `Tests/expected`. 216 tests, of which 154 run, 35 +everything it printed against a file in `Tests/expected`. 217 tests, of which 155 run, 35 only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image given at all. diff --git a/Tests/expected/cosmosWalk.out b/Tests/expected/cosmosWalk.out new file mode 100644 index 0000000..ac95446 --- /dev/null +++ b/Tests/expected/cosmosWalk.out @@ -0,0 +1,54 @@ +CosmOS +> Walk +f greet.sbx 1 +f hello.sbx 1 +f Life.sbx 6 +f Snake.sbx 9 +f Keys.sbx 3 +f Say.sbx 1 +f Where.sbx 4 +f Break.sbx 1 +f Grid.sbx 3 +f Press.sbx 4 +f Mode.sbx 1 +f Flip.sbx 1 +f Sprite.sbx 2 +f Depth.sbx 3 +d Packages - +f Pad.sbx 2 +f Crash.sbx 3 +f vars.script 1 +f blocks.script 2 +f loops.script 2 +f tune.sbx 2 +f Play.sbx 10 +f notes.txt 1 +d Apps - +f hi.script 1 +f bad.script 1 +f plain.script 1 +f cross.script 2 +f nonl.script 1 +f outer.script 2 +f inner.script 1 +f loop.script 1 +f hush.script 1 +f aloud.script 1 +f args.script 1 +f pass.script 1 +f holds.script 1 +finished +> cd /Apps +/Apps> Walk +f Copy.sbx 7 +f Say.sbx 1 +f Where.sbx 4 +f Walk.sbx 1 +f Lander 4 +f where.sh 3 +finished +/Apps> cd / +> exit +halted +Execution halted. +[exit 0] diff --git a/Tests/input/cosmosWalk.in b/Tests/input/cosmosWalk.in new file mode 100644 index 0000000..aa18e47 --- /dev/null +++ b/Tests/input/cosmosWalk.in @@ -0,0 +1,5 @@ +Walk +cd /Apps +Walk +cd / +exit diff --git a/Tests/lint-baseline.txt b/Tests/lint-baseline.txt index 4f5aed5..02559e3 100644 --- a/Tests/lint-baseline.txt +++ b/Tests/lint-baseline.txt @@ -15,7 +15,7 @@ Programs/CosmOS/Assembler/token.asm redundant-assignment 4 Programs/CosmOS/Assembler/token.asm redundant-setd 2 Programs/CosmOS/Assembler/tokenTest.asm redundant-setd 1 Programs/CosmOS/Source/config.asm redundant-assignment 1 -Programs/CosmOS/Source/cosmos.asm redundant-setd 10 +Programs/CosmOS/Source/cosmos.asm redundant-setd 11 Programs/CosmOS/Source/sbfs.asm branch-to-next 1 Programs/CosmOS/Source/sbfs.asm redundant-assignment 4 Programs/CosmOS/Source/sbfs.asm redundant-setd 1 diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index 7967f83..bcb7b85 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -105,6 +105,14 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/ "$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ "$ROOT/Programs/CosmOS/Apps/Say.asm" -o "$WORK/Say.sbx" >/dev/null "$TOOL" put "$DISKS/cosmos.img" "$WORK/Say.sbx" >/dev/null +# Walk.sbx is the first program that can see a directory at all. Every service before it took +# a name a program already knew, so nothing loaded could find out what names there are - dir +# could list only because it lives in the shell. It asks osFileInfo about each entry BETWEEN +# two steps of the walk, which is the hazard: where a walk has got to and where the last file +# asked about lives are both the system's, and a listing that came back short or repeated +# itself would be the two of them sharing a position. +"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ + "$ROOT/Programs/CosmOS/Apps/Walk.asm" -o "$WORK/Walk.sbx" >/dev/null # Where.sbx says where it was loaded from, and where a name would be beside it. TWO COPIES # on purpose, which is the whole test: the one at the root is found where you are standing # and is named by the bare word that was typed, so the system has to work out what that word @@ -252,6 +260,9 @@ python3 -c "open('twoblocks.txt','w').write('the second disk, at length. ' * 20) "$TOOL" put "$DISKS/cosmos.img" "$WORK/Copy.sbx" /Apps/Copy.sbx >/dev/null "$TOOL" put "$DISKS/cosmos.img" "$WORK/Say.sbx" /Apps/Say.sbx >/dev/null "$TOOL" put "$DISKS/cosmos.img" "$WORK/Where.sbx" /Apps/Where.sbx >/dev/null +# Walk goes in /Apps and nowhere else, so that it is reachable from any directory - which is +# what lets the same program list the root and then list /Apps. +"$TOOL" put "$DISKS/cosmos.img" "$WORK/Walk.sbx" /Apps/Walk.sbx >/dev/null # And the launcher for Lunar Porter, under the bare name somebody types. Put here rather than # beside the game above, because /Apps is not made until now. "$TOOL" put "$DISKS/cosmos.img" "$ROOT/Programs/CosmOS/Launchers/Lander" /Apps/Lander >/dev/null diff --git a/Tests/manifest b/Tests/manifest index cc3d41d..5788354 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -1017,6 +1017,22 @@ cosmosScriptArgs | CosmOS/Source/cosmos.asm | run | cosmosScr # # And load then run, because that is the other way into the same place. cosmosWhere | CosmOS/Source/cosmos.asm | run | cosmosWhere.in | 200000000 | disks/cosmos.img +# ---- A program that can see a directory ---- +# +# osDirFirst and osDirNext, which are the first way anything loaded could find out what is on +# the disk. Every service before them took a name a program already knew; dir could list only +# because it lives in the shell and calls the filesystem directly. +# +# Q ANSWERS THE KIND rather than a yes or no - file, directory, unfinished save, or nothing +# more - so one value says both whether there is an entry and what it is. +# +# THE HAZARD IS THE INTERLEAVING, and it is why this program asks osFileInfo about each entry +# between two steps of the walk rather than walking cleanly and asking afterwards. Where a +# walk has got to and where the last file asked about lives are both held by the system, and +# two things sharing one position would show up here as a listing that stopped early or said +# a name twice. Then the same walk in /Apps, because a walk that only ever ran at the root +# would not have proved it walks where you are. +cosmosWalk | CosmOS/Source/cosmos.asm | run | cosmosWalk.in | 200000000 | disks/cosmos.img # ---- Starting itself ---- # # /System/Boot/startup.sh runs before anybody can type. This one is also the check on #quiet