Plan 9-style w-prefix on the per-arch tools, disambiguating from the
real Plan 9 6c/6a/6l in ref/plan9front/:
cmd/wwc/ → cmd/wcc/ libwwc.a → libwcc.a
cmd/6{c,a,l} → cmd/w6{c,a,l} binary names too
test/wwc/ → test/wcc/ 6 test files w/ w6 prefix
selfhost/cmd mirror in lockstep
bootstrap/amd64/{w6c,w6a,w6l} snapshot binaries (gitignored)
WW_6{C,A,L} → WW_W6{C,A,L} env-var overrides
Plan 9 source-tree refs ("Plan 9 6c shape", ref/plan9front/, etc.)
preserved. Hare-style driver, both C and ww sides:
ww test [path] discover *_test.ww in a directory module, run
each; single-file mode for `ww test foo.ww`
Module-by-name `ww build foo` resolves to foo.ww or foo/foo.ww
via search path (cwd : -I dirs : $WW_LIB)
Default-to-cwd `ww build` / `ww test` build the cwd module
Run pass-through `ww run path arg1 arg2` reaches the program
lib/os: getcwd (79) and getdents64 (217) syscalls power `.` resolution
and directory enumeration on the ww side.
Makefile: wwstage tool deps now include lib/os/os.ww (+ lib/strconv
for wwdump_ww) so lib/* edits force their rebuild instead of leaving
stale binaries — surfaced when test 995 first failed against a stale
w6c_ww built before the lib/os additions.
Test 993 byte-identical parity gate (C-side ww vs ww-side ww_ww on a
build corpus) stays green; all 19 tests pass.
15 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/wcc/C library skeleton (buildslibwcc.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/wcc/000_smoke.c— smoke test that assertsww -Vprints the version.
Per-target binaries (w6c, w6a, w6l) are NOT created here. They
ship in their own phases (4, 5, 6).
Exit criteria:
makeproducesout/bin/wwandout/lib/libwcc.a.make testruns the smoke test and passes.
Phase 1 — Lexer (in libwcc.a)
Goal: tokenize ww source into a stream of Tok structs.
Deliverables:
cmd/wcc/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/wcc/tok.c— token names, debug printer.test/wcc/100_lex.c— table-driven: input → token sequence.test/lang/lex/*.ww— small files exercised via a tiny test harness that links againstlibwcc.a.
Exit criteria:
- A
lextooltest binary dumps a representative sample's token stream deterministically. - All lex tests green.
Phase 2 — Parser & AST (in libwcc.a)
Goal: parse the full grammar into an AST. No types yet.
Deliverables:
cmd/wcc/parse.c— recursive descent. Pratt expression parser for precedence. No yacc.cmd/wcc/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/wcc/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 libwcc.a)
Goal: resolve names, infer/check types, produce a typed AST.
Deliverables:
cmd/wcc/sym.c— symbol table. Lexical scopes. Plan 9 style hash.cmd/wcc/type.c— type representation, equality, conversion rules.cmd/wcc/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/wcc/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 — w6c amd64 compiler
Goal: turn typed AST into Plan 9-style amd64 assembly text. The
binary w6c is a Plan 9 cc analogue for amd64. There is no separate
SSA IR file; the only on-disk intermediate is .s.
Deliverables:
cmd/w6c/(links againstlibwcc.a):6.out.h— amd64 opcode enum, register names, addressing modes. Mirrorref/plan9front/sys/src/cmd/w6c/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/w6c/*.ww— golden tests: src → expected.stext.
Exit criteria:
w6c hello.wwproduceshello.s.- 20+ programs round-trip identically across runs.
- The output is consumable by phase 5's
w6a.
Phase 5 — w6a 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/w6a/: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/wcc/500_asm.c— round-trip known asm to known bytes.
Exit criteria:
w6a hello.s -o hello.oproduces a valid ELF amd64 object.objdump -d hello.omatches expectations across a 20-program corpus.
Phase 6 — w6l amd64 linker
Goal: link ELF objects into a static ELF executable.
Deliverables:
cmd/w6l/: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:
w6l -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
w6c → w6a → w6l 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, runsw6cthenw6athenw6l, 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
w6c. w6llearns 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 tow6l.
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 libwcc, w6c, w6a, w6l, 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
w6c(cgen, txt, peep, reg, swt). Diff.soutput byte for byte. - Port
w6aandw6l. 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/wcc/,cmd/w6c/,cmd/w6a/,cmd/w6l/C trees are deleted in this phase's final commit.
Status (2026-05)
Steps 1–6 done. The five ww-side tools live in selfhost/cmd/:
wwc/— ww-native lex/parse/check/cgen (the frontend library)w6c/— ww-native compiler binary; thin driver over wwc's cgenw6a/— ww-native amd64 assemblerw6l/— ww-native amd64 linkerww/— ww-native driver, shells tow6c_ww/w6a_ww/w6l_ww
Step 7 reaches its fixed point: make bootstrap produces ww1 → ww2 →
ww3 with cmp ww2 ww3 byte-identical. Tests 990–994 confirm each
selfhost tool is byte-identical to its Cstage counterpart on the
wwdump-corpus (and ww_ww build is byte-identical to ww build on
test 993's hello + wwdump corpus, despite using a different
toolchain underneath).
ww_ww build foo.ww is now C-free at runtime: the driver invokes
the wwstage tools end-to-end. Cstage is still required at first-
checkout time (no checked-in stage-0 binary yet). ET_DYN dynamic
linking is also fully ported to the ww side (test 996 confirms
w6l_ww -L ... -l ... is byte-identical to w6l on snake); the one
remaining gap is that ww_ww build's argv parser does not yet
forward -L/-l to the linker — w6l_ww accepts them when invoked
directly, just not through the ww-side driver. Snake's Makefile
still drives the C ww for that reason; switching it to w6l_ww +
explicit args works today.
Exit criterion 1 (bootstrap green) is met. Exit criterion 2 (delete
the C trees) is deferred to v1.0: removing cmd/wcc/, cmd/w6c/,
cmd/w6a/, cmd/w6l/, 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,w6c,w6a,w6l}), 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/wcc/<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 writingw6l. 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.