cc¶
native C compiler (runs on the P8X)
NAME
cc - native C compiler (runs on the P8X)
SYNOPSIS
cc src.c >out.asm
asm out.asm PROG.BIN (then: run PROG.BIN)
DESCRIPTION
A small C compiler written directly in assembly, compact enough to run
ON the machine. It reads a C source (streamed through the BIOS read
stream) and writes P8X assembly to stdout, which the native assembler
(asm) turns into a RUNnable binary. So a C program can be compiled,
assembled, and run entirely on the P8X:
cc hello.c >hello.asm
asm hello.asm HELLO.BIN
run HELLO.BIN
cc does the whole job on-target -- preprocess (splicing /lib/lib_NAME.c for
each //#use), parse, and code-generate -- so nothing off the machine is
needed. Variable storage is 16-bit-addressed (a program may use thousands of
slots), so cc compiles the large shared-code commands -- dir, grep, sed, vi
and the like, each pulling in several //#use libraries -- entirely on the
machine.
Supported C subset (`int` is 16-bit; `char` is a 1-byte element type — see
below.)
program : (func | global | struct)*
struct: struct TAG { (type [*]member ;)* } ; member access: v.m , p->m
global: type [*] NAME [ [ N ] ] ; (file-scope variable, zero-init)
func : type NAME ( [type [*]P (, type [*]P)*] ) { <stmt>* }
stmt : type NAME [ = <expr> ] ; (declare a variable)
| type * NAME ; (pointer variable)
| type NAME [ N ] ; (array of N ints)
| NAME = <expr> ; (assign)
| NAME++ ; | NAME-- ; | NAME += e ; | NAME -= e ; (scalar)
| NAME [ <expr> ] = <expr> ; (array/pointer element store)
| * <expr> = <expr> ; (store through a pointer)
| <expr> ; (expression stmt, e.g. a call)
| if ( <expr> ) <stmt> [ else <stmt> ]
| while ( <expr> ) <stmt>
| for ( [asg] ; [<expr>] ; [asg] ) <stmt>
| break ; | continue ; (innermost loop)
| { <stmt>* } (block)
| putchar( <expr> ) ;
| return [<expr>] ;
expr : cond ? expr : expr (ternary) | land ('||' land)*
land : bor ('&&' bor)* (short-circuit, yields 0/1)
bor : bxor ('|' bxor)* (bitwise or)
bxor : band ('^' band)* (bitwise xor)
band : rel ('&' rel)* (bitwise and)
rel : sh [ relop sh ] relop: < <= > >= == !=
sh : add (('<<' | '>>') add)* (shifts, variable count)
add : term (('+' | '-') term)*
term : unary (('*' | '/' | '%') unary)*
unary : ('-'|'!'|'*'|'&'|'++'|'--') unary | factor | NAME++ | NAME--
factor: NUMBER | 0xHEX | 'c' | "string" | NAME | NAME[ <expr> ]
| bios( ADDR, p1, a ) | NAME( args ) | '(' <expr> ')'
Multiple functions with parameters; execution starts at main(). Functions
return a value via `return`. Parameters and locals use STATIC per-function
storage, but a function saves its own live slots around a direct recursive
call, so (direct) RECURSION works — fact(), fib(), etc. POINTERS are 16-bit:
`&x` takes a variable's address, `*p` dereferences (read), `*p = e` stores;
this gives pass-by-reference — set(&x), swap(&a,&b). ARRAYS: `int a[N]`
reserves N ints; `a[i]` reads/writes an element; an array name doesn't decay
on its own, but `&a[0]` gives a pointer you can index (`p[i]`) and pass to a
function. A bare array name now DECAYS to &a[0] (so `puts(buf)` and
passing an array work). BUILTINS (like p8cc): putchar(c), puts(s) [+newline],
getchar() [->char or 0xFFFF at EOF], peek(addr), poke(addr,val), argstr()
[->the program's argument string], and bios(ADDR,p1,a) [a raw BIOS/OS syscall,
ADDR constant, -> A|carry<<8]. STRINGS: a char literal 'A' is a number; a string literal "abc"
is emitted inline (jumped over) as NUL-terminated data padded with an extra
zero, and evaluates to its address. So a classic
char *s; s = "hi"; while (*s) { putchar(*s); s = s + 1; }
loop and helpers like puts()/strlen() work. CHAR is a real 1-byte element:
`char a[N]` packs N bytes (2 per word slot), `char *p`/`char a[]` deref and
index (`*p`, `p[i]`, `a[i]`) read/write ONE byte and pointer arithmetic on
them steps by 1 — so a char buffer strcpy(&d[0], s) works. (A scalar `char c`
is still stored in a word; the byte width matters for arrays and pointers.)
Escape sequences \n \t \r \0 \\ \' \" work in char and string literals, and
// line and /* block */ comments are skipped; a `//#define NAME value`
directive defines an object-like macro (value = a decimal or 0x-hex number)
that is substituted wherever NAME appears -- e.g. `//#use abi` brings in the
BIOS/OS address names so a program writes bios(FOPEN, RDBUF, 0). And a `//#use NAME`
directive splices in /lib/lib_NAME.c on-target (recursive, deduped -- the
native counterpart of the host clib.py). Arguments pass on a runtime arg
stack (the callee pops them into its own slots), so FORWARD CALLS and MUTUAL
RECURSION work when the callee has a forward prototype `int f(int);` before
its use. NOTE: within one call's argument list a later
argument must not read a parameter an earlier one overwrote; and taking the
address of a local across a DIRECT recursive call is undefined (that local is
saved/restored). Comparisons yield 0/1. 16-bit
`int` — arithmetic runs through a memory accumulator (__ax) and runtime
helpers (__add/__sub/__mul/__cmp/__div); integer division, and '/' or '%' by
0 gives 0. putchar takes the low byte.
It grows one tested feature at a time (functions, char/pointers, ...) toward
the p8cc subset. Redirect the output to a RELATIVE filename
(cc src.c >out.asm) — an absolute redirect target is not yet supported by
the shell.
EXAMPLES
cc t.c >t.asm compile; then asm t.asm T.BIN ; run T.BIN
SEE ALSO
asm, edit, run
The text of man cc on P8X (os/man/cc in the repository). All commands