A program can ask where it came from

SWI osWhereAmI hands back the path the program was loaded from, on the
same terms as osArgument, and Libraries/path.asm joins a name to the
place another thing is in. Between them an application can find its own
assets: ask where you are, then pathBeside that and the file's name.

The answer FOLLOWS THE PROGRAM AND NOT THE PERSON, which is the whole
point and the reason the working directory could not serve. A program's
assets are relative to the program and its arguments are relative to
whoever ran it, and cwd can only be one of them - setting it to the
program's own would mean "Play mytune.tune", typed by somebody in their
own directory, looked in Play's.

It is made absolute before the program starts, because the path the
search settled on may be a bare name: a program found where somebody was
standing is named by the word that was typed, and a bare name means the
working directory - which a program is entitled to move out of. Worked
out once, at the start, since where a program came from is a fact about
its start and cannot change afterwards.

Joining is a LIBRARY and not a service. A service that opened a file
relative to the program would need a twin for every file operation there
is - read, save, info, block, start, write, done, delete, rename - while
one service handing back a path composes with all of them.

---- And the root's own path was "//" ----

Found by the first caller that asks. shellPath prepends a separator in
front of whatever string it is given, so being handed the separator
itself wrote two of them. Nothing saw it while the only caller was the
prompt, which asks where it is only when that is not the root. It is
handed an empty string now, and cosmosWhere runs a program from the root.

Where.sbx exists to be run rather than read, and is on the test disk
twice: at the root, where it is found by the bare word typed, and in
/Apps, where it is found by a path that already says where it is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-06 15:27:57 -04:00
co-authored by Claude Opus 5
parent d9ebc76cf5
commit 2defbb49e2
20 changed files with 583 additions and 23 deletions
+100
View File
@@ -0,0 +1,100 @@
; A program that says where it came from.
;
; The dullest thing on the disk, and that is the point of it: osWhereAmI and pathBeside are
; what an application with assets stands on, and both are much easier to check now, with
; nothing depending on them, than later inside a game that has gone quiet.
;
; Where says the path it was loaded from
; Where splash.tune says that, and the path of splash.tune beside it
;
; ---- The thing worth checking ----
;
; The answer follows the PROGRAM and not the person. Run this from the root and it says one
; thing; walk somewhere else and run the same copy and it says the same thing, because the
; system worked it out when the program started and a program's own place cannot move under
; it. A working directory is where the person is standing, and it is what the argument means:
; "Where notes.txt" asks about a notes.txt in the program's place, not in yours, which is
; exactly the distinction an application needs and the reason cwd could not serve both.
#Include services.asm
#Program
#Base 0x5000
start:
SETD.0 MyPath
INIB 0d192
SWI osWhereAmI
SETD.0 FromText
SWI osPrintString
SETD.0 MyPath
SWI osPrintString
CALL newLine
; With nothing after it, that is the whole of what it has to say.
SETD.0 Given
INIB 0d64
SWI osArgument
SETD.0 Given
LDA.0
BRA whereDone
SETD.0 MyPath
SETD.1 Given
SETD.2 Beside
INIB 0d192
CALL pathBeside
BNQ whereTooLong
SETD.0 BesideText
SWI osPrintString
SETD.0 Beside
SWI osPrintString
CALL newLine
whereDone:
RSTA
SWI osExit
whereTooLong:
; A path that did not fit is a wrong answer, and saying it would be worse than saying so.
SETD.0 TooLongText
SWI osPrintString
CALL newLine
INIA 0x01
SWI osExit
newLine:
SETD.0 NewLine
SWI osPrintString
RET
#Data
#Base 0x3000
FromText:
"loaded from "
BesideText:
"beside it: "
TooLongText:
"that path would not fit"
NewLine:
0x0A 0x00
MyPath:
#Reserve 0d192
Beside:
#Reserve 0d192
Given:
#Reserve 0d64
; ---- Included last, after both segments have been based ----
;
; A #Base says where a whole segment begins and so has to be the first thing in it, and a
; library brings code and data of its own. Put in front, its #Data would be the first thing
; in the Data Segment and this program's own #Base would be too late. This is where Lander
; keeps its seven, for the same reason.
#Include path.asm
+46 -1
View File
@@ -758,7 +758,7 @@ laid out in three directories:
| -- | -- |
| `/Apps` | What you run. The second place the shell looks for a word it does not recognise, so anything here starts by name from anywhere on the disk - programs, and scripts too. |
| `/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. |
| `/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. `path.asm` is the newest of them: joining a name to a place, which is arithmetic on two strings and so wants no help from the system. |
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
@@ -790,6 +790,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 <file>` 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. |
| 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. |
| 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. |
@@ -1346,6 +1347,7 @@ Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, w
| 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. |
| 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. |
| 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. |
@@ -1443,6 +1445,49 @@ Running off the end is how a reader finds out it has finished, so it gets an ans
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.
## Where A Program Came From:
`SWI osWhereAmI` hands back the path the program was loaded from, on the same terms as
`osArgument`: DP0 says where to put it and B how much room there is.
```text
/work> Where
loaded from /Apps/Where.sbx
```
**The answer follows the program and not the person.** It is worked out once, before the
program is given the machine, and a program's own place cannot move under it afterwards.
Standing in `/work` and running the copy in `/Apps` is told `/Apps`, which is the whole point.
**Made absolute, because the path the search settled on may be a bare name.** A program found
where somebody was standing is named by the word that was typed, and a bare name means the
working directory - which a program is entitled to move out of. So a program asked later
where it lives would be told "beside wherever you are now", which is the one answer that is
certainly wrong.
**Why this and not a service that opens a file beside the program.** That service would need a
twin for every file operation there is: read, save, info, block, start, write, done, delete,
rename. One service handing back a path composes with all of them, and joining a name to a
place becomes a library instead - `/Lib/path.asm`, whose `pathBeside` takes a path and a name
and answers with the name in the same place.
```
SETD.0 MyPath
SETD.1 Given
SETD.2 Beside
INIB 0d192
CALL pathBeside
```
**And why not simply put the program in its own directory.** Because a program's *assets* are
relative to the program and its *arguments* are relative to the person, and the working
directory can only be one of them. Setting it to the program's 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 things.
`Programs/CosmOS/Apps/Where.asm` is the whole of it in one program, and exists to be run
rather than read.
## The Filesystem Library:
The disk knows blocks and nothing else, so a filesystem is software. Programs/CosmOS/Source/sbfs.asm is one.
+123 -1
View File
@@ -4763,11 +4763,16 @@ shellPathStep:
shellPathDone:
; A machine at the root has written nothing at all, and the path to the root is the
; separator on its own.
;
; AN EMPTY STRING, because prepending puts a separator in FRONT of whatever it is given -
; so being handed the separator itself wrote two of them and the root came out as "//".
; Nothing saw it for as long as the only caller was the prompt, which asks where it is
; only when that is not the root; the first caller that always asks found it at once.
SETD.0 CwdAt
LDD.1.0
LDA.1
BNA shellPathReady
SETD.0 Separator
SETD.0 NoText
CALL shellPathPrepend
shellPathReady:
RET
@@ -5649,6 +5654,9 @@ runLoaded:
INIB 0d64
CALL copyText
; And where the program itself came from, so it can be asked later.
CALL runWhere
CALL installVectors
; The entry address is a number until BRD makes it a place. DP3 is the one to build it
@@ -5657,6 +5665,98 @@ runLoaded:
LDD.3.1
BRD.3
; ---- Where the program itself came from ----
;
; ProgramName holds the path the search settled on, and that path may be a bare name: a
; program found where somebody was standing is named by the word that was typed. A BARE NAME
; MEANS THE WORKING DIRECTORY, and a program is entitled to move out of it - so a program
; asked later where it lives would be told "beside wherever you are now", which is the one
; answer that is certainly wrong.
;
; So it is made absolute HERE, once. Where a program came from is a fact about its start and
; cannot change afterwards; where the person is can change on the program's own next line.
runWhere:
; A path that begins with a separator has already said where it is.
SETD.0 ProgramName
LDA.0
INIB 0x2F
CCF
SUB
BRQ runWhereAsFound
; Or a drive in front of it, which says so twice over. A digit and then a colon, both, the
; same test the path walker makes - a name may begin with a digit, and the colon is the
; whole of what tells the two apart.
LDA.0
INIB 0x30
CCF
SUB
BRC runWhereRelative ; Below a digit.
LDA.0
INIB 0x3A
CCF
SUB
BNC runWhereRelative ; Above one.
INCD.0
LDA.0
INIB 0x3A ; splitlint[redundant-assignment]: 0x3A above is one past '9' and here it is ':'
CCF
SUB
BRQ runWhereAsFound
runWhereRelative:
; Where the machine is, written out. shellPath builds it backwards from the end of its
; buffer and hands back a pointer into the middle, which is a string like any other.
CALL shellPath
SETD.0 CwdAt
LDD.0.0
SETD.1 RunPath
INIB 0d127
CALL copyText
; The end of it, and the last character on the way past. At the root there is no last
; character at all, which is an answer rather than a special case: nought is not a
; separator, so a separator gets written and the path begins with one.
SETD.0 RunPath
RSTA
SETD.1 RunPathLast
STA.1
runWhereEnd:
LDA.0
BRA runWhereJoin
SETD.1 RunPathLast
STA.1
INCD.0
BRI runWhereEnd
runWhereJoin:
SETD.1 RunPathLast
LDA.1
INIB 0x2F
CCF
SUB
BRQ runWhereName ; It ends in a separator already, so a second would be a lie.
INIA 0x2F
STA.0
INCD.0
runWhereName:
; And the program's own name under it. DP0 is where it goes, which is at most 127 bytes
; along a buffer of 192 - so the 64 a name can be always fits.
PSHD.0
POPD.1
SETD.0 ProgramName
INIB 0d64
CALL copyText
RET
runWhereAsFound:
SETD.0 ProgramName
SETD.1 RunPath
INIB 0d192
CALL copyText
RET
runNothing:
SETD.0 NothingLoaded
CALL printString
@@ -5836,6 +5936,16 @@ handleArgument:
CALL copyText
RETI
; The same bargain for the other thing a program is entitled to know about its own start:
; where it came from. Worked out in runWhere before the program was given the machine, so
; this only has to hand it over.
handleWhereAmI:
PSHD.0
POPD.1
SETD.0 RunPath
CALL copyText
RETI
; ---- The disk, on a program's behalf ----
;
; A loaded program that wanted a file used to include the whole filesystem, so it carried a
@@ -8110,6 +8220,17 @@ SearchDrive:
0x00
RunDrive:
0x00
; ---- Where the program came from, made absolute ----
;
; A hundred and ninety two, which is a working directory of up to 127, a separator, and a
; name of up to 64. None of those three can be made longer without the others being counted
; again.
RunPath:
#Reserve 0d192
RunPathLast:
0x00
ScreenSaved:
0x00
ScreenWasDrive:
@@ -8530,6 +8651,7 @@ CommandLine:
osBreak handleBreak
osLastStatus handleLastStatus
osTakeScreen handleTakeScreen
osWhereAmI handleWhereAmI
osBootState handleBootState
osBootSettle handleBootSettle
Device 0x20 diskDone
+23
View File
@@ -185,3 +185,26 @@
; NOT AUTOMATIC, and that is the point: dir and Say print and stop, and their output is the
; reason you ran them. Only a program that says it took the screen gets the screen put back.
osTakeScreen 0d36
; ---- Where the program itself came from ----
;
; DP0 says where to put it and B is how much room there is counting the zero, the same
; bargain osArgument offers. What comes back is the whole path the program was loaded from,
; made absolute: a program found where somebody was standing is named by the bare word that
; was typed, and a bare name means the working directory - which a program is entitled to
; move out of.
;
; ---- Why this and not "open a file beside me" ----
;
; A service that opened a file relative to the program would need a TWIN FOR EVERY FILE
; OPERATION there is: read, save, info, block, start, write, done, delete, rename. One
; service handing back a path composes with all of them, and joining a name to a place is
; then a library rather than a service - Libraries/path.asm, beside print.asm and math.asm.
;
; ---- And why not just move the program there ----
;
; Because a program's ASSETS are relative to the program and its ARGUMENTS are relative to
; the person, and the working directory can only be one of them. Setting it to the program's
; 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
+194
View File
@@ -0,0 +1,194 @@
; path.asm
; Putting a name together with the place another thing is in.
;
; DP0 = a path, DP1 = a name, DP2 = where the answer goes, B = how much room, counting
; the zero on the end
; CALL pathBeside Q is zero if the whole of it fitted
;
; Written by Anachronaut
;
; ---- What this is for ----
;
; A program asks the system where it came from - SWI osWhereAmI - and is handed the path it
; was loaded from. What it actually wants is the path of something NEXT TO that: its tune,
; its tiles, its saved state. This is that one step, and it is a library rather than a
; service because it is arithmetic on two strings and touches nothing the system owns.
;
; That split is the whole reason osWhereAmI hands back a path instead of opening files on a
; program's behalf. A service that opened a file relative to the program would need a twin
; for every file operation there is; a path composes with all of them, and the joining
; happens once, here, on a machine that has no string library to do it in each program.
;
; ---- Everything up to the last separator ----
;
; The place a thing is in is everything up to and INCLUDING the last separator in its path,
; so joining is a copy and not a search backwards from the end. A path with no separator in
; it names something in the working directory, and the answer is then the name on its own -
; which means exactly the same thing, in the same place, and needs no special case.
;
; /Packages/app.Lander/Lander + splash.tune = /Packages/app.Lander/splash.tune
; /Lander.sbx + splash.tune = /splash.tune
; Lander.sbx + splash.tune = splash.tune
;
; ---- The room is counted once, in one place ----
;
; Both halves go through pathPut, which counts the room down and always keeps a byte back
; for the zero. So an answer that did not fit is still a STRING, endable and printable, and
; a caller that ignores Q gets a short path rather than a walk off the end of its buffer.
; It gets a wrong answer, which is why Q exists - but not a broken machine.
;
; The answer is built through a stored pointer rather than DP2, because a CALL puts DP0 to
; DP2 back the way it found them and pathPut would otherwise write the same byte every time.
#Program
pathBeside:
SETD.3 PathPut
STD.2.3
SETD.3 PathRoom
STB.3
; ---- How much of the path is the place it is in ----
;
; Counted rather than pointed at, because what is wanted is a NUMBER OF BYTES TO COPY and
; a pointer would have to be turned into one. The count is set afresh at every separator,
; so what it holds at the end is the last one - which is the one that matters.
PSHD.0 ; The front of the path, to come back to.
RSTA
SETD.3 PathCut
STA.3
SETD.3 PathAt
STA.3
pathScan:
LDA.0
BRA pathScanned
INIB 0x2F
CCF
SUB
BNQ pathScanStep
; A separator, so everything up to and including it is the place.
SETD.3 PathAt
LDA.3
INCA
SETD.3 PathCut
STA.3
pathScanStep:
SETD.3 PathAt
LDA.3
INCA
STA.3
INCD.0
BRI pathScan
pathScanned:
POPD.0
SETD.3 PathCut
LDA.3
SETD.3 PathLeft
STA.3
pathPlace:
SETD.3 PathLeft
LDA.3
BRA pathName ; All of the place is written, or there was none of it.
DECA
STA.3
LDA.0
CALL pathPut
BNQ pathNoRoom
INCD.0
BRI pathPlace
pathName:
PSHD.1
POPD.0
pathNameChar:
LDA.0
BRA pathMade
CALL pathPut
BNQ pathNoRoom
INCD.0
BRI pathNameChar
pathMade:
CALL pathEnd
RSTA
RSTB
CCF
ADD ; Q is zero: the whole of it fitted.
RET
pathNoRoom:
CALL pathEnd
INIA 0x01
RSTB
CCF
ADD
RET
; The zero that makes it a string. There is always room for it: pathPut refuses the byte
; that would have taken the last one.
pathEnd:
SETD.3 PathPut
LDD.2.3
RSTA
STA.2
RET
; A holds a character. Puts it where the answer has got to and steps that on. Q is one if
; there is no room for it.
pathPut:
SETD.3 PathHold
STA.3 ; The character, across the pointer being fetched and put back.
SETD.3 PathRoom
LDB.3
DECB
BRB pathPutFull ; Only the zero's worth left, so this byte cannot be written.
STB.3
SETD.3 PathPut
LDD.2.3
SETD.3 PathHold
LDA.3
STA.2
INCD.2
SETD.3 PathPut
STD.2.3
RSTA
RSTB
CCF
ADD
RET
pathPutFull:
INIA 0x01
RSTB
CCF
ADD
RET
#Data
; Where the answer has got to, and how much room is left in it.
PathPut:
0x00 0x00
PathRoom:
0x00
; How much of the path is the place it is in, how far the scan has got, and how much of the
; place is still to be written.
PathCut:
0x00
PathAt:
0x00
PathLeft:
0x00
; One character, across the fetching and putting back of the pointer it is written through.
PathHold:
0x00