os/commands/ — P8X/OS commands written in C¶
Userland commands for P8X/OS, written in C and compiled with
p8cc to loadable /bin/*.bin programs. They run
in the transient program area ($5900) under run, reach OS/BIOS services
through the bios()/peek/poke/argstr() builtins and the OS syscall table
(see ../README.md), and read/write the standard streams via
getchar/putchar/puts — so the shell can redirect (</>) and pipe (|)
them like any program.
Running them¶
Once installed in /bin, a command runs by bare name — the shell's implicit
RUN searches path (default /bin) and appends .bin:
dir /bin cat README.TXT pwd
equivalently run /bin/dir.bin /bin, etc. Every command accepts -h to
print a one-line usage summary and exit.
Hand-assembled counterparts. Each of these commands also has a hand-written P8X assembler version in
../commands-asm/, verified byte-identical in behavior and ~2.3× smaller overall (up to 5.8×). Since 2026-08-20 the hand-asm builds ARE/bin— the default the shell's PATH finds — andrun.shinstalls these C builds to a parallel/binc, so you can still compare on-target:run /binc/grep.bin …vsgrep …. A command with no asm twin yet (cube) ships its C build in/bin.Sources on-card.
run.shalso ships the command sources under/src/commands/c(the command.cfiles — the sharedlib_*.chelpers are not duplicated here; they live only in/lib, where the nativecc's//#useopens them as/lib/lib_<name>.c) and/src/commands/asm(the hand-assembled command.asm+ the toolchain app sourcesp8xcc.asm/p8xasm.asm/p8xedit.asm; the shared includes are not here — like the C libs they live only in/lib, asglob/globx/regex/stdin/ distab/gfx.inc, where the on-targetasm's;#useopens them). So you can read — and, for C,cc /src/commands/c/pwd.c >pwd.asm— any command right on the machine. The deprecatedcpp/lex/cc1front end is not shipped (no binary, no man page, no source), matching its deprecation everywhere else.Rebuild on-card. Alongside the sources,
run.shlays down build-output dirs/src/commands/c/binand/src/commands/asm/bin, and a realMakefilein each source dir. The OSmakebuilt-in reads the CWDMakefile(target: deps+ TAB recipe), resolves prerequisites depth-first, and drivescc/asm:cd /src/commands/c && make pwdrebuilds one command,make allthe whole dir,make installpublishes to/bin,make cleanwipesbin/— always-rebuild (P8XFS has no mtimes yet). See Building on-target below.Drives. A second CF is mounted at
/d1in one unified namespace, so these commands are drive-unaware: an ordinary/d1/...path reaches drive 1 with no special syntax —cat /d1/NOTES,dir /d1/bin,dir /d1/*.C,grep x /d1/SRC/*.C, and cross-mountcp /d1/A /B(the read and write streams each carry their own drive viaROSDRV/WOSDRV). The drive selection lives in one place — theFRESOLVE/RV_STARTmount redirect — not in any command, so even the stdin-filter tools (grep,wc,head, …) get the mount for free without growing. See ../README.md "Two drives".Note — DIR, PWD, CAT, TREE, DEL and HELP are no longer shell built-ins (the minimal-kernel split): they were removed from the OS and run from
/binby bare name, sodir -R,pwd,cat file,tree,del name,helpall just work.deltombstones via theFDELETEBIOS call;helpis ~1.4 KB of reference text that no longer sits in the resident OS. The shell keeps only what can't be a/binprogram, in four groups: (1) shell state / the loader / the script engine —run/load,sh/make,cd/path,exit/mon,mount/umount,bootload; (2) deep-FS-internal ops with no syscall surface —rmdir/pack/fsck/format(moving them would add OS code, not save it); (3) memory tools that would overwrite themselves in the$5900TPA —save/dep; (4) filesystem BOOTSTRAP —mkdir(you need it to create/binon a freshly-formatted card, which has no/binto run a/binprogram from).dumpstays native for that same group-(3) reason — as a/binprogram it would load into the$5900TPA and overwrite the very memory it dumps. Consequence: a freshly-formatted card (no/bin) can'tdir/catuntil/binis repopulated (from the host, or a future master CF — backlog).
Commands¶
| Source | Usage | What it does |
|---|---|---|
dir.c |
dir [-R] [-S] [path\|glob] [-h] |
List a directory (the path, or the CWD if omitted). Each line is a right-justified byte size, two spaces, then the name; directories show a blank size and a trailing /. Entries are sorted within each directory (and per level under -R): by name (raw ASCII, so A-Z before a-z) by default, or by size largest-first with -S (ties by name; a dir has no size so counts as 0, sorting after files). -R recurses the whole subtree, indenting two spaces per level (the size column stays aligned). A last component with */? is a case-insensitive glob (via lib_glob): dir *.ASM, dir /bin/*.bin, dir -R *.C. Buffers a directory's entries (single global set reused per level) to sort, then streams them. |
pwd.c |
pwd [-h] |
Print the current working directory path. |
cat.c |
cat [file\|glob] [-h] |
Print a file, or copy stdin→stdout (the canonical filter) when given no file. So cat file, cat <file, and cat \| … all work. A last component with */? is a case-insensitive glob (via lib_globx): cat *.ASM concatenates every matching file, and cat *.ASM >ALL.TXT captures them — directory iteration now coexists with an open write stream (see FSDIRBUF below). Reading the console (e.g. cat >FILE), each key echoes and Ctrl-D ends the input. |
wc.c |
wc [file\|glob] [-h] |
Count lines, words, and bytes → L W B. A file, a glob (wc *.LOG = combined count over all matches), <file, or a pipe. Counts are 24-bit (files may be up to 16 MB), printed via a byte-wise divmod10. |
grep.c |
grep [-r] regex [file\|glob] [-h] |
Print lines matching a basic regex — . (any), */+/? (zero-or-more / one-or-more / zero-or-one), ^/$ (anchors); else literal. Reads the named file/glob (like cat) or stdin if none: grep "^al" foo.txt, … \| GREP "x.*y". -r recurses the CWD tree (depth-first, like dir -R/find) and searches file contents, printing each hit as path:line — grep -r "x.*y". Lines capped at 255 chars; -r is capped at 36 files. |
cp.c |
cp [-r] src dst [-h] |
Copy a file (read stream → write stream), or with -r a whole directory tree — recursively, and across the /d1 mount (cp -r /d1/SRC /SRC). A */? glob source copies every match into the destination directory (cp *.ASM /BAK). -r collects each level's entries before descending (the FNEXT cursor is global) and makes destination dirs via the SYS_MKDIR syscall. Supersedes the old IMPORT built-in. |
mv.c |
mv src dst [-h] |
Move/rename a file = copy + delete source (P8XFS has no rename primitive). A */? glob source moves every match into the destination directory (mv *.TMP TRASH). mv X X is refused. |
head.c |
head [-N] [file] [-h] |
First N lines (default 10) of a file or stdin. |
tail.c |
tail [-N] [file] [-h] |
Last N lines (default 10, max 40) of a file or stdin, via a ring buffer. |
more.c |
more [file] [-h] |
Page a file or stdin a screenful (23 lines) at a time: space=next page, Enter=one line, q=quit. Forward pager (not full less). |
sort.c |
sort [file] [-h] |
Sort lines ascending (file or stdin). In-memory: ≤128 lines of ≤79 chars. |
uniq.c |
uniq [file] [-h] |
Collapse adjacent duplicate lines (pair with sort). |
sed.c |
sed s/re/new/[g] [file] [-h] |
s/// substitution; the left side is a basic regex (. * + ? ^ $, via lib_regex — same matcher as grep), replacement is literal. First match or all with g; the whole matched span is replaced. * is non-greedy. |
awk.c |
awk [-F c] 'program' [file] |
Small awk: records split into fields on whitespace (or -F c); one rule [/regex/] { print items } — /regex/ (via lib_regex) or empty = every line, a bare pattern prints the line. print items: $0/$N/$NF/NF/NR/"str", comma = a space between. File arg or stdin (pipes). The program is one quoted arg (awk strips the quotes). Hand-asm twin ../commands-asm/awk.asm (verified identical output). No BEGIN/END/printf/expr yet. |
find.c |
find pattern [-h] |
Recursively print CWD paths whose name matches pattern: a case-insensitive glob (*/?, via lib_glob) if it contains * or ?, else a literal substring. So find *.C, find TEST?.ASM, and find BIN (substring) all work. |
diff.c |
diff f1 f2 [-h] |
Prefix/suffix-anchored line diff: < lines only in f1, > only in f2. ≤96 lines/file (≤79 chars). |
touch.c |
touch name [name...] [-h] |
Create each named file empty if missing; an existing file is left untouched (not truncated). No mtime yet (no RTC); no globbing (a pattern only ever matches existing files). |
del.c |
del name [name...] [-h] |
Remove file(s) — tombstone each entry via the FDELETE BIOS call (space reclaimed later by pack). CWD-relative or absolute; a missing name reports ?No such file and the rest still go. Files only (use rmdir). Moved out of the shell (was a built-in) so it sits with touch/cp/mv. |
help.c |
help |
Print the shell command reference — commands, redirection/pipe syntax, standard programs. Moved out of the shell (was a built-in): its ~1.4 KB of static text no longer occupies the resident OS. man name gives the full page on any one. |
tree.c |
tree [-h] |
Depth-first indented listing of the CWD tree (same recursion as dir -R). |
vi.c |
vi name [-h] |
Minimal modal VT100 screen editor. Reads keys raw (CONIN, no echo) and drives the cursor with ANSI escapes, so it needs a VT100-compatible terminal. h j k l move, i/a/A/o insert, x delete char, dd delete line, 0/$/G, u undo (single-level), /pat + n search (literal, forward, wraps), :w/:q/:wq/:q!. Selective redraw (one line per edit, full only on scroll) keeps it usable at serial baud. Flat 110×80 line buffer. Complements the line-oriented EDIT app. |
man.c |
man name [-h] |
Print the manual page for a command: streams /man/<name> to stdout (a cat with a fixed /man/ prefix, so it is CWD-independent). Works for both /bin commands and OS built-ins; an unknown name prints no manual entry for NAME. Pages are plain text authored in os/man/ and installed to /man by run.sh. |
dump.c |
dump addr [-h] |
Hex-dump 256 bytes from hex addr (16 rows of hex + ASCII); a console key pages, . exits. Memory-only (peek + CONIN). Formerly an OS built-in — moved out of the kernel (it needs no shell/FS state). |
dep.c |
dep addr b b ... [-h] |
Deposit hex byte values into memory starting at hex addr (poke); quiet on success. The counterpart to dump. Formerly an OS built-in. Note both live in the TPA at $5900, so don't dep over that region. |
cmp.c |
cmp file1 file2 [-h] |
Byte-for-byte file compare — silent if identical, else cmp: files differ: byte N, line M, or cmp: EOF on fileX when one is a prefix. Reads file1 into an 8 KB buffer, streams file2 (both via the single BIOS read stream, like diff). Hand-asm twin ../commands-asm/cmp.asm (identical output). The byte-level companion to diff. |
examine.c |
examine addr [-h] |
Interactive examine/modify from hex addr (peek/poke) — the shell counterpart of the monitor's E: shows aaaa: vv, then Enter advances, two hex digits write + advance, . quits. Console input via getchar (SYS_GETC). Runs in the $5900 TPA, so don't examine over that region. |
disasm.c |
disasm start end [-h] |
Disassemble the hex range [start,end) — one instruction per line (AAAA: bb bb.. MNEMONIC operand), unknown bytes print ???. Memory-only (peek). The opcode table lib_distab.c is generated from genucode.OPC by generators/gen_p8xdis.py (spliced via //#use distab), so it never drifts from the ISA. Runs in the $5900 TPA, so don't disassemble that range. |
cube.c |
cube [frames] |
Spinning perspective wireframe cube — the 3D demo and first client of lib_g3d (//#use gfx + //#use g3d). 64 frames (one full turn) by default. With the stage-8b geometry engine it uploads the static cube once and renders per frame with a matrix write (~3 ms of CPU, vsync-paced page flip, tear-free); cube N s forces the stage-7 software path (~72 ms/frame), which is also the automatic fallback without an engine. Rebuilt from constants each frame so it never accumulates rounding error. Needs the display (?No display without). C-only, and (like disasm) outside the p8cc.c self-host subset: its sine/edge tables are brace-initialized arrays. |
tri.c |
tri x0 y0 z0 x1 y1 z1 x2 y2 z2 [f] [k] [r g b] |
Draw one 3D triangle from the shell — the stage-9 g3tri primitive as a command: nine world coordinates, f fills, k keeps the screen so triangles stack into scenes, optional r g b colour (white default). House frame (window ±120, centred viewport, focal 256); engine when fitted, software otherwise, same pixels; POINT-friendly (no flip). C-only. |
paint.c |
paint |
Keyboard-driven vector paint: an on-screen palette (8 colours, line/box/circle/fill tools), a LINFUN-complement crosshair and rubber-band, AREABC drops whose boundary colour is probed by a PIXELR ray, and shape-at-a-time erase — the drawing is a display list that replays. Works with the mouse too, via lib_ptr.c — console keyboard + xterm SGR mouse events (press-drag-release draws; click the palette to select). The palette strip is protected by the card itself (WINDOW/VWPORT clamped to the canvas). man paint for the keys. C-only. |
desk.c |
desk |
The window system demo (lib_wm.c, //#use wm): overlapping draggable windows with close boxes, a menu bar with Mac-style press-drag-release pull-downs, per-window hardware clipping (a WINDOW/VWPORT pair per window — local coords, the card clips), painter's repaint, focus. The SHAPES window repaints from a card-resident command list (two wire bytes); TERM is a small terminal; FILES browses the disk (lib_dirent), opens .P8I pictures in a VIEW window (BLIT in window-local coords) and launches .BIN programs via SYS_EXEC — the app replaces desk in the TPA, System-1 style, and -d-aware programs (paint) chain back on quit. Events via lib_ptr.c. man desk. C-only. |
wdesk.c |
wdesk |
The desktop on the RESIDENT kernel — a thin launcher. The window manager lives in the OS (os/wmkernel_body.asm, folded into p8xos.asm; syscalls SYS_WKINIT..SYS_WKRAISE, $2027..$204B), so wdesk only records the SHAPES scene into a card list, SYS_WKOPENs its windows, points the launch key at paint -w (SYS_WKPATH) and hands over to SYS_WKRUN. wdesk draws its own menu bar (DESK L=PAINT C=CLOSE Q=QUIT) and drives the kernel one event at a time via SYS_WKEVENT: the kernel handles window mechanics, wdesk handles the menu keys. Its FILES window is a client-drawn directory browser (SYS_WKGET/SYS_WKTOP give the rect and focus; wdesk draws the listing into the body): n/p select, ENTER opens — navigate a dir or launch a .BIN (-w). Its TERM window is a client-drawn command line with scrollback: when focused it owns the keyboard (l/c/q type), ENTER runs the typed name (shell resolver → /bin, .bin; launched -w; ?EXEC on failure). Its VIEW window (opened on demand when a .p8i is opened from FILES) streams the picture in, one BLIT/row; a second .p8i re-fronts it via the new SYS_WKRAISE ($204B). Windows are addressed by title letter (S/T/F/V), not array index, because the kernel reorders records on focus (k_raise). L launches paint over wdesk; quitting paint (launched -w) re-execs wdesk -r, which redraws the same windows and the bar — the desktop survives the launch, because the records live in the OS and the picture on the card. The thing desk cannot do. man wdesk. C-only. |
rotate.c |
rotate [x y z] |
Rewrite the engine's rotation matrix (brads: 64 = 90°; yaw-pitch-roll about the world origin) and redraw the persisted scene — the engine's record list survives between commands, so tri-built scenes and cube's aftermath spin in place. No args = identity. Translation untouched; engine required. C-only. |
page.c |
page [sync\|flip] |
Rejoin the framebuffer pages (the recovery when a program died mid-flip and the screen ignores drawing) or flip them for manual double-buffering — the stage-8b page machinery's front door. Engine required. C-only. |
camera.c |
camera [ex ey ez ax ay az] |
Place the camera at the EYE point looking at the AIM point (the g3d look-at: basis normalized via the library's 32-bit integer sqrt) and redraw the persisted scene from there — walk around tri-built worlds. No args = home (origin, +z). Engine required. C-only. |
image.c |
image /path / image x y file / image read x0 y0 x1 y1 file |
VIEW mode (a bare absolute path, image /path) clears the screen, draws the picture full-screen and waits for a key — how the Finder opens a .P8I. Otherwise draw a P8I picture at (x,y), or GRAB a screen rectangle into a fresh P8I (replacing any existing file) — the two verbs are inverses, and a grab is immediately re-drawable here or by BASIC's IMAGE, or liftable to the host as a screenshot (p8xfs get + p8img.py). Grabs read what the panel shows (issues PGSYNC first when the geometry engine is fitted); corners self-sort; header errors say ?NOT P8I. Built on lib_gfx + lib_apath. Hand-asm twin ../commands-asm/image.asm, verified pixel- AND file-identical: 563 cycles/pixel (vs the inlined C's 4,435 — it out-runs even BASIC's 614), the mandrill in ~1.4 s; it is the /bin default (the C build lives at /binc/image.bin). |
finder.c |
finder [dir] |
Two-mode Finder desktop (P4): a full-screen file browser with a menu bar and a scrolling file list (dirs cyan, selection a yellow bar). Arrows/ENTER navigate a dir, launch a .BIN full-screen (SYS_EXEC), or open a .P8I in image's VIEW mode; a drops the Apps menu (paint/term/write/…) and f the File menu (rename / duplicate / move / new folder / delete -- each delegated to mv/cp/del/rmdir/mkdir through the launch-and-return chain, with a modal text box for the name); Backspace goes up, q quits to the shell. Claims the screen (GTSUSP) and sets its own GL ground state (WINDOW/VWPORT/PROJCT 0); decodes arrow ESC-sequences itself. Launches auto-return via a SYS_RUNSH script chain (no run-and-return syscall). man finder. C-only. |
term.c |
term |
Two-mode Term app (P5): an on-screen shell in the app frame. Enables the glass TTY (screen/GCONEN), runs one typed command with its output on the GL screen, and persists by re-launching itself (-c continue mode, since SYS_EXEC/SYS_RUNSH both replace the caller); exit/quit returns to the Finder. Launched from the Finder Apps menu (T). man term. C-only. |
write.c |
write [file] |
Two-mode Write app (P5): a full-screen text editor. A flat 2 KB buffer with a cursor, insert/backspace/newline, arrow movement (incl. up/down), ^O save and ^X/ESC back to the Finder; wraps at 78 columns. Launched from the Finder Apps menu (W). man write. C-only. |
kermit.c |
kermit send\|recv /path |
File transfer over the 2nd serial port (P5): moves a file across the second ACIA ($FF08/$FF09) in minimal Kermit-style packets (SEQ LEN data CHK, LEN=0 = EOF), the console keeping port 1. Run from the shell or the Term app; recv verifies each checksum. man kermit. C-only (asm twin backlogged). |
screen.c |
screen on\|off |
Glass-TTY switch (P2): turn the on-screen text console on or off — on mirrors CONOUT onto the GL display (clears + homes first), off goes back to serial only, no arg reports the state. On by default whenever a card is fitted — the monitor enables it at wake and puts its own banner on the LCD; off is the escape hatch when a program's graphics should stay undisturbed by the prompt. man screen. C-only (asm twin backlogged). |
Implementation notes¶
- dir.c —
argstr(), thebios()carry flag to end theFOPENDIR/FNEXTloop,SYS_OPENCWD($2012) to open the CWD with its full 16-bit LBA (so a CWD at LBA ≥ 256 lists correctly, not the truncatedSYS_CWDLBAlow byte), andFSDIRBUF($0145) to move iteration off the sharedSBUFso output can stream.-R: theFNEXTcursor is global BIOS state, so each level streams its entries while only recording child-directory LBAs (16-bit) into a small per-level array, then descends (poking the high byte intoLBA1/$7048beforeFOPENDIRAT) — bounded memory, no whole-tree buffer. - pwd.c —
SYS_GETCWD($2003): the CWD comes through the syscall ABI, not by peeking OS RAM. - wc.c / grep.c — stdin filters that compose with
</|. wc counts are 24-bit (byte-wise divmod10, like dir's size column). grep also takes an optional file argument (opened like cat — absolute path +FRESOLVE/FOPEN, read buffer at$FC00— else stdin) and matches a basic regex via the classic tiny matcher (matchhere/match): a single self-recursivematchhere(thec*case is an inline loop, not a separatematchstar) — deliberately no forward declaration / mutual recursion, since the nativep8cc.cbootstrap rejects a standalone prototype. See Shared code below.grep -radds a recursive content search: it can't grep files during the directory walk because theFNEXTcursor is global BIOS state (the same reasondir -R/findrecord-then- descend), so it runs in two phases — phase 1 walks the CWD tree depth-first (FSDIRBUF page$EA) collecting every file's absolute path intorfiles[](48 × 96), phase 2open_paths each and greps it, prefixing hits withpath:. The 48-file cap keeps grep's image low enough (ends ~$E400after the MOVW shrink) to leave ~11 levels ofcollect()recursion headroom under the$F800C-stack. - head.c / tail.c / more.c — file-or-stdin via the shared
nextc()/openarg()idiom (copied from cat/grep).headstops after N lines;tailkeeps the last N in a flat ring buffer (buf[slot*256+col], N≤40);morepages 23 lines then reads the continue key from the console (CONIN, BIOS $0100) — separate from the redirected stdin — so it pauses for bothmore fileandcmd | MORE. - cp.c / mv.c — copy SRC (read stream, buffer at
$FC00) to DST (write stream). The read and write streams use independent buffers, so the byte loop interleaves them; butFRESOLVE/FOPENand the write stream all transitSBUF, so DST is resolved beforeFWOPEN(which zeroesSBUFlast).mvthenFDELETEs the source;mv X Xis guarded. - cat.c — a filename argument is opened with
FRESOLVE($0133) +FOPEN/FGETB(read buffer$FC00). The BIOS resolves names from its own current directory (root for a fresh program), so cat builds an absolute path (CWD viaSYS_GETCWD, unless the arg is already absolute) —FRESOLVEalways starts at root, hence CWD-independent. With no argument it falls back to the stdin filter, so redirection and pipes are unchanged. A glob argument (*/?) is expanded bylib_globx'sglob_expandinto a path list, then each path is streamed in turn (cat *.ASM). The hard part is `cat *.ASMOUT
: a write stream is already open, and each file'sFRESOLVEwalks the directory throughSBUF— which is also the write stream's buffer, so the naïve version overwrites each file's already-buffered output with directory data (. BBB). Fix: cat pointsFSDIRBUF($0145) at page$FA, and **FSCAN now honors that page too** (not justFNEXT; default$71=SBUFkeeps every other caller byte-identical), so the per-file path walks read into$FA00and leave the write stream'sSBUFintact. This is the general fix that lets any glob-expanding command redirect to a file —cp/mv`'s resolve-DST-before-FWOPEN dance (above) only worked because they resolve a single target once.
Building on-target¶
You can rebuild any command on the P8X itself, from the sources shipped
under /src. Each source dir carries a real Makefile (target: deps + TAB
recipe, with all/clean/install targets); the make built-in reads the
CWD Makefile, resolves prerequisites depth-first (shared deps built once), and
runs the recipes through the toolchain (always rebuilds; no timestamps yet):
cd /src/commands/c
make pwd rebuild one command -> bin/pwd.bin (cc then asm)
make all rebuild every command -> bin/*.bin
make install publish this dir's bin/*.bin over /bin
make clean delete this dir's build outputs
Size caveat (stage 9): the graphics-library clients (
cube) are p8cc.py-built; compiling them with the on-targetcc(or the nativep8cc.c) now exceeds the TPA/64K — the native codegen runs ~2× the Python compiler's and the stage-9 triangle machinery tipped it over. Themake cuberecipe is therefore host-compiler territory until the codegen-shrink is mirrored (see BACKLOG).
The /src/commands/asm dir has the same targets (each recipe is a single asm
of the hand-asm twin), and /src/os-bios builds the monitor + OS. Recipes run
in the invoking CWD, so paths are dir-relative. A C command compiles then
assembles; a hand-asm command assembles directly (with ;#use includes pulled
from /lib):
cc pwd.c >T.ASM # cc writes T.ASM in the CWD (via the > redirect)
asm T.ASM bin/pwd.bin # asm reads that relative T.ASM, writes the binary
asm pwd.asm bin/pwd.bin # (asm dir: assemble the hand-asm twin directly)
So the loop is: edit a source under /src/commands/{c,asm} (with edit/vi),
cd into that dir, make <name>, and the fresh binary lands in bin/. To put
it on PATH, make install (or cp it to /bin).
Building (host)¶
Compile + assemble + install one (or let ../run.sh install all
three into /bin on a fresh disk):
python3 compiler/p8cc.py os/commands/dir.c -o dir.asm
python3 assembler/p8xasm.py dir.asm -o dir.bin --base 0x5900
python3 tools/p8xfs.py put disk.img dir.bin --name /bin/dir.bin --load 0x5900 --exec 0x5900
# on the P8X: DIR /bin (bare name via PATH) or RUN /bin/dir.bin /bin
Either compiler works: p8cc.py (the Python bootstrap) or the native
p8cc.c build (cc -O2 compiler/p8cc.c -o p8cc-host) — they emit behaviorally
equivalent P8X assembly.
Shared code (//#use + lib_*.c)¶
On-target too. The native
cc(apps/p8xcc.asm) now performs the same//#usesplicing on the P8X — its recursive preprocessor opens/lib/lib_NAME.c(thelib_*.csources, shipped to/libbyrun.sh). So the earliercpp | lex | cc1front end (which ran only the front half on-target) was retired (2026-07-14);cccompiles the whole thing on the machine.
There is no linker and no #include in p8cc, so reusable helpers are shared
by concatenation: a command opts in with a directive line
//#use stdin // splices in os/commands/lib_stdin.c, ahead of this source
and the build step (tools/clib.py) replaces that line
with the contents of os/commands/lib_<name>.c before p8cc runs. A source
with no //#use passes through unchanged, so the build can run clib.py over
every command uniformly. The helper text is spliced above the command, so
its functions are defined before any caller — keeping the combined source inside
the native p8cc.c subset (no forward declarations). Both compilers see the same
combined source: p8cc.py combined.c or p8cc_host < combined.c.
run.sh and the c_*_test.sh harness both run clib.py first. To share a new
helper, drop it in os/commands/lib_NAME.c and add //#use NAME to each
consumer.
Current libraries:
| Library | Provides | Used by |
|---|---|---|
lib_stdin.c |
path[80], fromfile, nextc() (next byte or 65535 at EOF), openarg(a) (open the optional file arg → 0 stdin / 1 opened / 2 not found). A */? arg is expanded (via lib_globx) and nextc() reads all matches as one concatenated stream, so every command below gets globs for free (grep x *.C, sort *.TXT, wc *.LOG). |
grep, head, tail, more, sort, uniq, sed, wc |
lib_apath.c |
abspath(out, a) — build an absolute path (CWD-prefixed when relative) into a caller buffer; returns chars consumed. Spliced with //#use apath. |
cp, mv, diff, touch |
lib_rdline.c |
readline(buf) — read one line via nextc() (CR dropped, LF-terminated); 1 = line, 0 = EOF. Spliced with //#use rdline; needs //#use stdin above it. |
uniq, sed |
lib_streq.c |
streq(p, q) — 1 if NUL-terminated strings are equal |
mv, uniq |
lib_glob.c |
gmatch(pat, name) — case-insensitive whole-string glob match (*, ?) |
dir, find, lib_globx |
lib_globx.c |
glob_expand(pat, out, maxn) — expand a glob into a list of matching file paths (pulls in lib_glob) |
cat, cp, mv, lib_stdin |
lib_regex.c |
match(re, t) / matchhere(re, t) — basic-regex matcher (. * + ? ^ $); matchhere sets rend to the match end. (Character classes [..] / escapes don't fit in grep's host build yet — see BACKLOG.) |
grep, sed |
lib_dirent.c |
de_read() snapshots the entry FNEXT just matched into de[18] ([17] = the 24-bit length's high byte); de_isfile()/de_isdir()/de_isdot()/de_len()/de_lba() query it, de_opendir(lba) descends — all via the SYS_DIRENTRY/SYS_OPENDIR syscalls, so commands never hardcode BIOS scratch addresses |
dir, find, grep, tree, lib_globx |
lib_gfx.c |
C veneer over the GL/PGC port ($FF50, via peek/poke) — the single graphics interface since the $FF20 device door closed. Keeps the historical SCREEN-SPACE API: gpresent (probe first — an absent card floats the bus), grgb/gcolor (RGB565 pen), gcls, gpixelw, gline, gbox(f), gcircle(f), gellipse(f), gpixelr (the GL PIXRD verb). Every call emits GL bytes with FIFO backpressure and maps y through the identity flip (271−y); ground state (identity window/viewport) self-establishes on first use. Asm twin: ../commands-asm/lib_gfx.inc (equates). |
cube, lib_g3d |
lib_g3d.c |
Wireframe 3D (STAGE7-DESIGN.md): retained edge pool (g3line, 512 edges), g3window/g3view (window-space clipping makes viewports true clip rectangles), g3persp, g3render (near clip → project → Cohen-Sutherland → viewport map → hardware LINE). Foundation: muldiv(a,b,c) — signed (a*b)/c through a 32-bit intermediate; it probes for the MDU (the stage-8a hardware muldiv at $FF30, bit-exact to the same contract) and routes through it when fitted, else a native-*// fast path when the product fits 16 bits, else the all-C 32-bit path. With the stage-8b geometry engine fitted, g3render auto-routes the whole pipeline into fabric (identical pixels), and the pro path — g3up/g3mat/g3flags/g3go/g3flip/g3sync — uploads a static model once and re-renders per frame with only a matrix write, page-flipped (man g3d on-target). Needs //#use gfx above it. |
cube |
lib_g3cam.c |
The look-at camera (stage 9d): g3cam(p) with p = eye x,y,z + aim x,y,z builds the normalized view basis (via i3sqrt, the library's 32-bit integer square root, and n3orm/c3ross) and writes the engine's matrix + translation. Split from lib_g3d so only camera users pay its ~6K — folded in, every g3d client blew past 64K. Needs //#use gfx + //#use g3d above it. |
camera |
lib_abi.c |
object-like #defines naming the BIOS jump table + OS syscalls (FOPEN, FGETB, FRESOLVE, SYS_GETCWD, SYS_MKDIR, …) and the shared read buffer (RDBUF = $FC00), so a command writes bios(FOPEN, RDBUF, 0) not bios(0x0124, 0xFC00, 0). #define is a compile-time substitution, so the code is byte-identical to the raw-hex form (no wrapper cost). Each address lives here alone; the asm twins use the parallel equates in ../commands-asm/lib_abi.inc. |
any command that calls the FS/console directly (cat, cp, dir, mv, vi, find, grep, tree, touch, pwd, diff, man, more, dump, lib_stdin, lib_apath, lib_dirent, lib_globx) |
When a helper depends on another (e.g. readline calls lib_stdin's nextc()),
list its //#use after the dependency's so clib.py splices them in the
right order (callee before caller).
Two rules for a helper meant to be lifted into a lib_*.c:
- keep it dependency-free (only the builtins / its own locals), and
- write it within the p8cc subset's intersection with the native
p8cc.c.
p8cc subset gotchas (learned the hard way building these — keep helpers and
commands inside these limits, especially for p8cc.c parity):
- No ++/-- — write i = i + 1.
- Watch for */ inside a block comment — e.g. writing a regex example like
s/a*/x/ in a /* ... */ comment ends the comment early (at the a*/) and
spills the rest as code. Reword (no literal */) or use // line comments.
- No break/continue (rejected by p8cc.py) — fold the exit into the loop
condition, or use a flag.
- No forward declarations / mutual recursion (p8cc.c drops the rest of the
file) — make functions self-recursive, define callees first.
- </> are UNSIGNED — never return a negative sentinel and test < 0
(it's always false); use a boolean comparator (e.g. lless() over masked
bytes) instead of a -1/0/1 lcmp().
- Avoid int arrays for indices — they misbehaved as a sort permutation
array; sort swaps the char slots in place instead. Prefer flat char
buffers indexed by slot*W+col.
- Don't pass array + expr as a pointer to a function (e.g.
puts(buf + i*64) printed the wrong slot) — index with buf[i*64+j] instead.
- Declarations go at the top of each function (and a lib_*.c's globals go
at its top, so they precede the command's own globals after splicing).
- TPA size limit (not a compiler bug): the shared file read buffer lives at
$FC00 (just under the stack), so a command's code+globals must stay below
$FC00 (~37.9 KB from the $5900 base). This bit sed/diff built with the
native p8cc.c, whose codegen is larger than p8cc.py's: with the old
$E000 buffer they overran it and read file data into their own code. Moving
the buffer to $FC00 fixed it — both build on both compilers now. (Was
long misfiled as a "p8cc.c file-arg miscompile".) diff is the largest at
~17.6 KB on p8cc.c, so keep an eye on headroom there. NB the gap widened when
p8cc.py gained a codegen-shrink (~17% tighter output) that has not been
mirrored to p8cc.c (a deferred item) — so p8cc.c-built commands are now
noticeably larger than the same source through p8cc.py.
Tests¶
These double as regression tests for the OS syscall, redirection, and pipe
machinery: emulator/test/c_dir_test.sh, c_dir_recursive_test.sh,
c_cat_test.sh, c_filters_test.sh (wc/grep), c_fileops_test.sh (cp/mv),
c_pager_test.sh (head/tail/more), c_stdin_test.sh, c_redirect_test.sh,
c_pipe_test.sh, and the implicit-RUN/PATH path in os_path_test.sh.
The core text/file utilities are all implemented (the table above). dir and
find match globs in place (via lib_glob); cat expands a glob into multiple
files (via lib_globx, e.g. cat *.ASM >OUT). Extending wildcards to the
remaining commands (wc/grep/sort/cp/del) is better done as a single
shell-level expansion pass (see the backlog) than per-command. Future ideas:
TR, wc -l-style flags, a real LESS (back-scroll).