Phase 10 step 8 (delete the C trees) is deferred to v1.0 — until the compiler stops churning we keep Cstage as the fresh-checkout entry point. Split the Makefile so the two stages are named, and add BOOTSTRAP.md describing the cstage → ww1 → ww2 → ww3 fixed-point flow. PLAN.md gets a status note pointing at it.
14 KiB
PLAN
A phased plan for ww. Each phase ends with a green make test and
a tagged checkpoint. Don't start phase N+1 until phase N is green.
CLAUDE.md is the rulebook. This file is the schedule.
Phase 0 — Skeleton (C bootstrap scaffold)
Goal: a repo that builds, has a Makefile, and has a ww driver
that prints its version. No compilation yet.
Deliverables:
Makefile(POSIX) withall,test,clean,installtargets.cmd/wwc/C library skeleton (buildslibwwc.a):ww.h— central typedefs (Node,Sym,Type,Lex)mem.c— bump arena allocator (nomallocin hot paths)err.c—errorf,fatal,warn- (lex/parse/check stubs added in later phases)
cmd/ww/driver skeleton: argv parsing, version print.test/run— shell test harness.test/wwc/000_smoke.c— smoke test that assertsww -Vprints the version.
Per-target binaries (6c, 6a, 6l) are NOT created here. They
ship in their own phases (4, 5, 6).
Exit criteria:
makeproducesout/bin/wwandout/lib/libwwc.a.make testruns the smoke test and passes.
Phase 1 — Lexer (in libwwc.a)
Goal: tokenize ww source into a stream of Tok structs.
Deliverables:
cmd/wwc/lex.c— hand-rolled DFA. Handles:- identifiers, keywords (
fn,let,def,if,else,for,switch,case,return,use,type,struct,defer,break,continue,export,proc,chan,nil,true,false) - integer literals (dec, hex, oct, bin) with suffixes (
u8,i32...) - float literals
- rune literals
'x'and string literals"..."(UTF-8) - operators and punctuation, including
@for attributes (@symbol("name"), etc.) - explicit
;terminators — no automatic insertion (Hare rule) //and/* */comments
- identifiers, keywords (
cmd/wwc/tok.c— token names, debug printer.test/wwc/100_lex.c— table-driven: input → token sequence.test/lang/lex/*.ww— small files exercised via a tiny test harness that links againstlibwwc.a.
Exit criteria:
- A
lextooltest binary dumps a representative sample's token stream deterministically. - All lex tests green.
Phase 2 — Parser & AST (in libwwc.a)
Goal: parse the full grammar into an AST. No types yet.
Deliverables:
cmd/wwc/parse.c— recursive descent. Pratt expression parser for precedence. No yacc.cmd/wwc/ast.c—Nodeconstructor helpers; printer.- Grammar coverage:
useimports (paths use.not::)typedeclarations (struct, alias, fn type) with trailing=defconstantsletdeclarations (top-level and local)fndefinitions with= { ... };body. No methods; receivers are just first arguments.exportvisibility marker- all expressions, statements, control flow
defer- body-less
fndeclarations and@symbol("name")attribute for FFI imports
test/wwc/200_parse.c— table-driven AST shape tests.test/lang/parse/*.ww— programs that should parse but not yet typecheck; harness only checks parser exits 0.
Exit criteria:
- 50+ parse tests green, drawing shapes from
ref/hare/cmd/hare/main.ha. - AST printer output is deterministic.
Phase 3 — Type checker (in libwwc.a)
Goal: resolve names, infer/check types, produce a typed AST.
Deliverables:
cmd/wwc/sym.c— symbol table. Lexical scopes. Plan 9 style hash.cmd/wwc/type.c— type representation, equality, conversion rules.cmd/wwc/check.c— type checking pass over the AST. Errors carry source locations.- Built-in types: all integer widths,
bool,f32,f64,rune,str,void, pointers, slices[]T, fixed arrays[N]T, function types. - Slice semantics:
{ ptr, len, cap }, indexing bounds-checked at runtime (debug builds; release may elide via flag). test/wwc/300_check.c— table-driven: src → expected error or OK.test/lang/check/*.ww— both passing and failing cases.
Exit criteria:
- All built-in types correctly checked.
- Negative tests produce stable, useful diagnostics.
Phase 4 — 6c amd64 compiler
Goal: turn typed AST into Plan 9-style amd64 assembly text. The
binary 6c is a Plan 9 cc analogue for amd64. There is no separate
SSA IR file; the only on-disk intermediate is .s.
Deliverables:
cmd/6c/(links againstlibwwc.a):6.out.h— amd64 opcode enum, register names, addressing modes. Mirrorref/plan9front/sys/src/cmd/6c/shape.gc.h—Prog,Adr, scratch regs, stack layout.cgen.c— typed AST →Proglist (walking, not SSA).txt.c—Proglist → textual Plan 9 amd64 asm.swt.c,peep.c,reg.c— switch lowering, peephole pass, naive register allocation (linear scan or spill-everywhere first; tighten later).mkfile— Plan 9 mkfile next to the Makefile entries.
- Output suffix
.s. Plan 9 amd64 asm syntax (not GAS). test/lang/6c/*.ww— golden tests: src → expected.stext.
Exit criteria:
6c hello.wwproduceshello.s.- 20+ programs round-trip identically across runs.
- The output is consumable by phase 5's
6a.
Phase 5 — 6a amd64 assembler
Goal: assemble Plan 9 amd64 asm into ELF object files. ELF (not Plan 9 a.out) so we can interop with C archives in phase 8.
Deliverables:
cmd/6a/:lex.c— tokenize Plan 9 asm.parse.c— hand-rolled grammar (Plan 9 uses yacc; we go hand-rolled to keep rule #6 honest).asm.c— encode amd64 instructions (REX, ModR/M, SIB).obj.c— emit ELF64 relocatable objects.mkfile
- Pseudoregisters supported:
SP,FP,SB(static base) per Plan 9 convention, on top ofAX,BX, ...,R8-R15. test/wwc/500_asm.c— round-trip known asm to known bytes.
Exit criteria:
6a hello.s -o hello.oproduces a valid ELF amd64 object.objdump -d hello.omatches expectations across a 20-program corpus.
Phase 6 — 6l amd64 linker
Goal: link ELF objects into a static ELF executable.
Deliverables:
cmd/6l/:obj.c— load ELF.ofiles and.aarchives.sym.c— global symbol table; resolution.pass.c— section layout, relocation application.out.c— emit static ELF executable.mkfile
- Static linking only. No dynamic loader. No PT_INTERP segment.
lib/libwwrt.a(built later in phase 7) is auto-included.
Exit criteria:
6l -o hello hello.o libwwrt.aproduces a static ELF binary.ldd helloreports "not a dynamic executable".- The binary exits 0 with the program's intended semantics.
Phase 7 — Runtime + driver wiring
Goal: a tiny static runtime, plus the ww driver wired to call
6c → 6a → 6l end to end.
Deliverables:
rt/start.s— entry, sets up argc/argv, callsmain.rt/syscall.s— Linux syscall trampoline.rt/panic.c—panic,abort, stack unwind fordefer.rt/mem.c— small page allocator built onmmap. Users get raw pages; higher-level allocators ship inlib/.rt/slice.c— slice helpers used by codegen (bounds_check,slice_growfor explicit user calls).- Build artifact:
out/lib/libwwrt.a. cmd/ww/driver: readsww build foo.ww, runs6cthen6athen6l, linkslibwwrt.ainto the output.
Exit criteria:
- A ww program with no libc dependency runs.
- Adding
use ospulls in syscall wrappers; still static.
Phase 8 — C FFI + static linking C libs
Goal: bind to C libraries; produce static binaries containing them.
Deliverables:
- Body-less
fndeclarations +@symbol("name")attribute parser and checker rules already in phases 2/3 — finalize codegen. - amd64 SysV ABI: formalize struct-by-value and varargs in
6c. 6llearns to slurp static archives from a search path.lib/c/libc/— minimal libc bindings (malloc,free,printf,read,write,open,close).lib/c/tls/— bindings to libtls or BearSSL (decide; BearSSL is more aligned with our static-linking story).lib/c/crypto/— bindings to libcrypto subset (sha256, aes, hmac).lib/c/curses/— ncurses bindings (initscr, getch, mvprintw, ...).wwdriver:ww build -lcrypto -ltls -lncursesresolves these via apkg-config-style probe, hands.apaths to6l.
Exit criteria:
examples/tlsclient.wwconnects to https://example.org and prints the response. Statically linked. No.sodeps inldd.examples/menu.wwruns an ncurses TUI.
Phase 9 — Standard library (Hare-shaped)
Goal: a usable stdlib. Each module ships with tests.
Order of build (each is its own milestone):
typeslimits, int helpersbytesslice ops on[]u8stringsops onstriostreamstruct (function-pointer vtable, no interface);eofsentinel valuebufiobuffered reader/writer (Plan 9bioanalogue)fmtprintf-family writing throughio.streamosargv, env, stdin/out/err, file opserrorserror = strand sentinel constantsstrconvitoa, atoi, parse floatsortsort.slice, binary searchpathfilepath opsencoding/utf8,encoding/hex,encoding/base64hash/crc32,hash/fnv,hash/sha256(pure ww)timemonotonic, wall clock, sleepnetdial/listen TCP, UDP
Each module follows the Hare layout: one directory, multiple files,
+linux.ww, +test.ww overlays where useful.
Exit criteria per module:
- Module compiles standalone with
ww build ./lib/<mod>. - Module tests pass:
ww test ./lib/<mod>. - Public API documented in
READMEinside the module dir.
Phase 10 — Self-host
Goal: rewrite libwwc, 6c, 6a, 6l, and ww in ww. Drop the
C bootstrap.
Steps:
- Translate
ww.handNode/Sym/Type/Prog/Adrto ww structs. - Port
lex.c→lex.ww. Diff token streams against C output. - Port
parse.c→parse.ww. Diff ASTs. - Port
check.c. Diff typed ASTs. - Port
6c(cgen, txt, peep, reg, swt). Diff.soutput byte for byte. - Port
6aand6l. Diff resulting.oand final binaries. - Three-stage bootstrap:
Cstage (gcc-built tools) → ww1 → ww2 → ww3, withcmp ww2 ww3OK for each tool.
Exit criteria:
make bootstrapruns the three-stage build green.- The C
cmd/wwc/,cmd/6c/,cmd/6a/,cmd/6l/C trees are deleted in this phase's final commit.
Status (2026-05)
Steps 1–6 done. The four ww-side tools live in selfhost/cmd/:
wwc/— ww-native lex/parse/check/cgen6a/— ww-native amd64 assembler6l/— ww-native amd64 linkerww/— ww-native driver
Step 7 reaches its fixed point: make bootstrap produces ww1 → ww2 →
ww3 with cmp ww2 ww3 byte-identical. Tests 990–993 confirm each
selfhost tool is byte-identical to its Cstage counterpart.
Exit criterion 1 (bootstrap green) is met. Exit criterion 2 (delete
the C trees) is deferred to v1.0: removing cmd/wwc/, cmd/6c/,
cmd/6a/, cmd/6l/, cmd/ww/ is a one-way door. Until the compiler
stops churning we keep Cstage as the canonical fresh-checkout entry
point and treat selfhost/cmd/* as the path forward. At v1.0, the
plan is to ship a checked-in stage-0 binary archive
(bootstrap/<arch>/{ww,6c,6a,6l}), delete the C trees, and promote
selfhost/cmd/* to cmd/*.
See BOOTSTRAP.md for the build flow and make cstage/make wwstage
targets.
Phase 11 — CSP
Goal: channels and lightweight processes, without GC.
Deliverables:
- Runtime scheduler: cooperative, M:N, with stack-per-proc (start with fixed-size stacks; segmented stacks are a research item).
proc f(args)statement: spawns a process. Fire-and-forget by default. To keep a handle, use the stdlib spawner (sched.spawn(f, args)returns aprocval). No GC means proc lifetimes must be explicit.chan Ttype.c <- vsend;let v = <-creceive;select { ... }.- Channel ownership: closing a channel is a single-writer responsibility (Go rule). Buffered and unbuffered both supported.
syncpackage:mutex,waitgroup,once.
Concurrency-safety constraints (because no GC):
- Sending a pointer over a channel transfers ownership unless the
type is explicitly marked
shared. The checker enforces this. - No data races by construction in the safe subset;
unsafepkg exists for power users.
Exit criteria:
- Classic CSP examples (ping-pong, prime sieve, fan-in/fan-out) compile and run.
- Race detector under
-race(later sub-phase) catches a planted race in tests.
Test cadence
Every phase has its own test directory (test/wwc/<phase>_*.c,
test/lang/<phase-area>/*.ww). make test runs them all in order
of phase. CI is just make test in a clean tree.
Versioning checkpoints
Tag the tree at each phase boundary:
v0.0end of phase 0v0.1end of phase 1- ...
v0.10end of phase 10 (self-hosted)v1.0end of phase 11 (CSP merged, ABI declared stable enough to start caring)
Risks and mitigations
Prog/Adrdesign churn. Mitigate by reading Plan 9 cc and per-target compilers before we write our own. Don't invent until you've copied.- Writing a linker is hard. ELF is documented, static linking
is the small subset. Read
ref/plan9front/sys/src/cmd/8l/and the ELF spec before writing6l. Start with hello-world and grow. - Static linking against C libs. OpenSSL is a pain to build static; BearSSL or LibreTLS are more pleasant. Pick early.
- Self-host bootstrap divergence. Diff outputs at every stage,
not just the final binary. Token streams, ASTs, and
.stext must match. - CSP without GC. Channel-of-pointer ownership rules are the
hard part. Prototype the checker with
unsafe-allowed first; tighten later.
Non-goals (restated)
- No package manager.
- No generics, no interfaces, no tagged unions, no closures, no lambdas.
- No async/await.
- No
map. - No GC.
- No LLVM, no QBE, no external compiler infrastructure. The
toolchain is
cc/8c/8a/8l-shaped (Plan 9), per target.