Bear in mind that a small kernel would be handy for fitting into L1 caches and GPU units.
As far as using OO Forth, you can also just use specific words that offer a safe interface over more dangerous raw memory words. Old Forths probably worked directly with the filesystem more because of code size limitations than any real abstraction penalty.
I guess that this hypothetical stc Forth would have to have abstracted I/O and registers- any spare ones outside the stack management could be used for data passing, maybe as the top of the data stack.
Of note, this cache friendliness would dictate the kernel wordset, so choosing it would be dependent on your cpu targets.
Really the OO is in support of vocabulary modules and data spaces. Freeforth has a cross compiler environment to MSP430 cpus - as a module this would make hygienically writing to new targets easy.
> which is an STC Forth that assigns registers to the top two stack elements to avoid SWAP instructions. I take your point about caches, but surely a Forth like this that optimises the words during compile (inlining etc) would help?
Certainly. The way Freeforth seems to be doing things is by storing the swapped state and by testing against that to make a branch. I'm curious how this works with branch predictor penalties, but if swaps are done in loops then it might actually work out very favorably. You need to be careful that your code doesn't use too much ROT or DUP but as long as it's mostly SWAP oriented it should be okay.
> I guess that this hypothetical stc Forth would have to have abstracted I/O and registers- any spare ones outside the stack management could be used for data passing, maybe as the top of the data stack.
Forth implementations are notoriously architecture specific for exactly this reason and the differences in registers and stack semantics between architectures. A lot of recreational Forths target abstract VMs exactly to be able to target a single architecture and not care much about performance, but if you want to adapt Freeforth to different architectures you may need to make deep changes. I'm not sure how FASM works but maybe FASM decreases some of the pain.
before that, though, he was using ram as his 'block file' (i think having reduced from 1024-byte blocks to 256-byte blocks) and only using the disk to make changes persistent. also he still recompiles programs from source every time he switches context from one program to another; the program he's switching from gets discarded, and the one he's switching to gets recompiled
i remember having to defragment my floppy under the p-system in order to make a big enough piece of free space for the file i wanted to copy onto it. i have the vague memory that cp/m supported non-contiguous files; hdos certainly did
while i don't intend to question your assertion that forth users at the time maintained their tables of contents blocks manually, it seems like you could implement a fat-like directory-maintenance system in less than one screen of code. like cp/m, you could maintain file sizes in blocks rather than in bytes (but 1024-byte blocks, of course); like some dec tape formats (?), you could use four bytes for the filename with a weird encoding (forth's native base 36 instead of dec radix-50). suppose you have a double-sided disk with 40 tracks of 9 512-byte sectors per side; that's 720 in total, or 360 forth blocks. you can't fit a block number into one byte, so the simplest thing is to use two. so if you want to make fat-style linked lists, you need 720 bytes of block numbers, for the following block number. that leaves you 304 bytes for filenames and starting block numbers; if filenames are 4 bytes and starting block numbers are 2 bytes you have room for 50 files, which is probably okay
so i feel like maintaining a table of contents automatically would have cost about 1% of the disk space (1 block for the code and 1 block for the directory and allocation table), so it would have been a good idea
the interface might look something like
open ( d u -- flag ) create file named d, size u blocks, returning 0 on success
extent ( d -- u ) return the number of blocks in file d, or 0 if nonexistent
delete ( d -- ) delete file named d if it exists
filename ( c-addr u -- d ) convert ascii filename to a double-precision number
.filename ( d -- ) print the filename d
file ( u -- d ) return the name of the uth file in the directory or 0 if none
maxfiles ( -- 50 ) return the maximum possible file index to pass to file
index ( d u1 -- u2 ) return the block number for block u of file d
and from there you can use the usual forth block words to read or modify the contents of the files, such as block, update, load, and save-buffers. things like refill and thru, which presuppose semantics for the sequencing of block numbers, might benefit from file-aware versions. for example, you might supplement thru with a word old which loads each block of a file, whatever sequence they're in on disk: 2variable oldfile
: old ( d -- ) 2dup oldfile 2! extent 0 ?do oldfile 2@ i index load loop ;
or i guess that, if you can trust the code being loaded to not leave crap on the stack between blocks, it might be justifiable to just write : old ( d -- ) 2dup extent 0 ?do 2dup i index load loop 2drop ;
you might also want to list the directory of files: : stat ( d -- ) 2dup or if 2dup .filename ." " extent . cr else 2drop then ;
: ls-l ( -- ) maxfiles 0 do i file stat loop ; open ( d -- flag ) create file d, size 0 blocks, returning 0 if ok
extent ( d -- u ) return the number of blocks in file d, or -1 if none
delete ( d -- ) delete file named d if it exists
extend ( d -- flag ) append a block to file d, returning 0 if ok
filename ( c-addr u -- d ) convert ASCII filename to a double number
.filename ( d -- ) print the filename d
file ( u -- d ) return name of the uth file in the directory or 0
maxfiles ( -- 50 ) return the maximum valid file index
index ( d u1 -- u2 ) return the block number for block u1 of file dSo maybe make this filesystem work on triad units: an index fits in a byte, more space for longer filenames. Not sure what to do with the extra block in the filesystem triad.
i think that, if you want to put the metadata into multiple blocks, you need to have some control over the order of buffer saving. one way to do this would be to have two copies of the metadata that you alternate between, which would be desirable anyway in case of power loss during metadata writes. maybe once every 30 seconds, say. after your last modification to replica tweedledee and before your first modification to replica tweedledum, you would call save-buffers, ensuring that briefly tweedledee is also consistent on disk (tweedledum already having been consistent), even if buffer evictions later put tweedledee into a torn state
then the question is how, upon startup, to know whether to use tweedledee or tweedledum? maybe an update serial number at the beginning and end of the block that has to match?
using a cluster size larger than the forth block size seems like it might complicate the calling interface somewhat
glad to hear from you!
I wasn't proposing keeping file metadata in multiple blocks -- just, might as well consider if any functionality might be worth adding in the remaining block of the filesystem triad, if it's gonna be a triad (source code, metadata, empty). (This makes the larger minimum size of these triads a bit of an attractive nuisance from a "Forth Zen" pov...)
At a fundamental level, you had a kernel of some kind, and free space within which programs could be loaded and executed. With a Forth, you could get that with about 6K of total overhead.
One of the big contrasts is that with Forth, at the time, programs were loaded from source code, whereas everyone else used binary code. But another interesting aspect was that in Forth, when you were done with a program, you had to free up the space, if necessary, to run another program. In something like CP/M, when a program was exited, the OS reclaimed the space, and, to run it again, the program would have to be reloaded from storage.
The next big difference was the file system (or lack thereof). Forth does not have a file system, it simply organizes storage as a sequence of 1K blocks. It was common to have "table of contents" screens to track where programs were stored on disk, these all had to be manually maintained.
CP/M had a much better file system, but its interesting to contrast it with the UCSD P-System, which had a very crude file system. While it indeed had named files, it suffered from requiring them to be contiguous, and really only allowing one file to be written at a time (not exactly true, but only one file could grow freely at a time -- the file "at the end"). However, reorganizing, and compacting free space was a routine task under the P-System.
The Forth system being rock simple was quite easy to make a mistake and lose data, for example if you were relocating source code to make new space, you could easily overwrite something accidentally, since you were responsible for the actual disk locations of the operation.
The final nice feature of the Forth system, speaking of disk blocks, was the crude virtual memory system. Here, disk blocks were arbitrarily mapped to memory buffers, with a simple LRU system, and dirty buffers automatically being flushed back to disk. This made persistent memory mechanics quite easy to use.
Of course, for most micro users this was not as hazardous, as most of the work was done at the "floppy" level in contrast to working on a hard disk with 5000 screens. But that was the reality of native Forth systems of the day, and folks just worked with it, and leveraged it to get work done.
The idea of those basic ideas being scaled up to a modern machine with GB of RAM and enormous hard disks is an interesting curiosity. It certainly gives you a lot of room to be sloppy. Such a system wouldn't happen, the frugal nature of those early Forth systems was no longer required, so adding complexity and space to make the userland more friendly is worth the investment.