commands-asm — hand-coded assembler versions of the /bin commands¶
An experiment (branch commands-asm): rewrite the C /bin commands
(os/commands/*.c, compiled by p8cc) as hand-written P8X assembler, to
measure how much smaller carefully hand-coded asm is than the current p8cc
codegen. This is the concrete data behind the backlog's "ASM vs C commands"
question and the p8cc codegen-size concern (grep/sed/vi live at the 64 K TPA
ceiling because of code size).
Each NAME.asm here is a drop-in replacement for /bin/name.bin: same entry
($5900), same argument ABI (P2 = arg-tail pointer), same OS/BIOS calls, so it
must produce byte-identical behavior to the C version — verified in the
emulator, not just assumed.
The ABI a command relies on¶
| call | addr | in | out |
|---|---|---|---|
| entry | $5900 |
P2 = ptr to NUL-terminated arg tail |
RTS to OS |
| SYS_GETCWD | $2003 |
P1 = dest buf |
CWD path (incl. NUL) copied |
| SYS_PUTC | $2009 |
A = char |
— |
| SYS_PUTS | $200F |
P1 = string |
prints string, no newline |
| SYS_GETC | $200C |
— | A = char, or EOF |
| FRESOLVE | $0133 |
P1 = path |
sets DIRLBA/FNAME |
| FOPEN | $0124 |
P1 = 512-byte buf |
C=1 if not found |
| FGETB | $0127 |
— | A = byte, C=1 at EOF |
puts(s) in the C world = SYS_PUTS(s) then SYS_PUTC(10).
Measuring¶
sh os/commands-asm/compare.sh
builds every os/commands/NAME.c with p8cc and every os/commands-asm/NAME.asm
by hand — both through the same p8xasm.py --base 0x5900 — and prints a
size table with the ratio. Ported commands only in the TOTAL.
Scoreboard (fill-binary bytes)¶
Regenerated 2026-09-12, after the Tier A ISA work: both columns moved. The
compiled column shrank by more than half (frames on P3, word instructions,
CMPW conditions, relaxed branches), and the hand-asm column shrank ~4.5%
(tools/tierA_rewrite.py replaced its byte-wise word moves, pointer loads
and carry chains with single instructions).
| command | p8cc | hand-asm | ratio |
|---|---|---|---|
| touch | 877 | 431 | 2.0× |
| pwd | 282 | 164 | 1.7× |
| wc | 5715 | 3580 | 1.6× |
| more | 4760 | 3169 | 1.5× |
| mv | 5678 | 3786 | 1.5× |
| uniq | 5475 | 3746 | 1.5× |
| cat | 4498 | 3265 | 1.4× |
| head | 4711 | 3261 | 1.4× |
| sed | 7031 | 4953 | 1.4× |
| dir | 7339 | 6352 | 1.2× |
| vi | 15837 | 13358 | 1.2× |
| diff | 17516 | 16608 | 1.1× |
| sort | 15619 | 13961 | 1.1× |
| tail | 15450 | 13843 | 1.1× |
| grep | 10953 | 11932 | 0.9× |
| cp | 6556 | 8728 | 0.8× |
| tree | 1031 | 1318 | 0.8× |
| find | 3001 | 6172 | 0.5× |
| TOTAL | 132329 | 118627 | 1.1× |
(Regenerate with compare.sh; the C sizes include the //#use shared libs
spliced by clib.py, and each hand-asm binary that declares ;#use likewise
counts its include, so the comparison is apples-to-apples. The 2026-08 table
this replaces read 2.4× overall, 291,442 vs 122,770 bytes.)
Takeaways¶
All /bin twins are verified behaviourally identical to their C version by
verify.sh (diff of emulator transcripts), so the sizes compare equivalent
behavior. The 2026-08 finding -- hand asm 2.4× smaller, up to 5.8× on
code-dominated commands -- was a measurement of the OLD compiler: its software
C-stack, byte-by-byte word moves and helper calls for every operator. With the
Tier A ISA and the compiler emitting it, that overhead is gone and the gap
closed to 1.1× overall:
- Code-dominated commands (1.4–2.0×): hand asm still wins where a routine
is a tight byte loop the compiler cannot see through (
wc,more,uniq,sed), but the margin is now bytes, not multiples. - Data-dominated and recursive commands (0.5–1.1×): the compiler now
WINS.
findis half the size of its twin,cpandtree0.8×,grep0.9×: the twins pay for depth-indexed recursion arrays and manual 16-bit index math, while the compiled version keeps its locals in P3 frames and letsLDW (P3+d)do the indexing.diff/sort/tailare large buffers in both builds, so they sit at 1.1×.
So the reason this directory exists -- size -- has largely evaporated. What the twins still offer is speed in hand-tuned inner loops and a second, independently written implementation that the differential tests check the compiler against. Whether to keep porting new commands to asm is now a judgment per command, not a rule; see the BACKLOG discussion.
Shared hand-asm includes mirror the C //#use model (spliced by mkasm.sh):
lib_stdin.inc (open/read/glob engine), lib_glob.inc (gmatch + de[]),
lib_regex.inc (the recursive . * + ? ^ $ matcher for grep/sed),
lib_globx.inc (glob expansion for cp/mv wildcards, on top of lib_glob.inc),
lib_gfx.inc (equates for the GL/PGC port — GLDATA = $FF50, GLSTAT =
$FF51, … — the asm twin of the C //#use gfx library's address book),
and lib_abi.inc (equates naming the BIOS jump table + OS syscalls — FOPEN =
$0124, SYS_GETCWD = $2003, … — so a twin ;#use abi and does JSR FOPEN, the
asm counterpart of the C side's //#use abi / os/commands/lib_abi.c #defines).
Two structural techniques recur, forced by the ISA (P3 is the hardware stack
pointer, so only P1/P2 are general-purpose and there are no cheap software-stack
frames): recursion is done with depth-indexed arrays + a global w_depth
(tree, dir, find, cp, grep), and the genuinely recursive regex/glob matchers save
their pointer args on the hardware stack across non-tail calls. The CPU has
no divide, so decimal output uses a divmod10 subtraction routine.