Files
ww/PLAN.md
Hojun-Cho 502b304841 ww: driver shells to wwstage tools
build_one now invokes 6c_ww / 6a_ww / 6l_ww from $self_dir, not
the C-built binaries that share the directory. After this change
`ww_ww build foo.ww` touches no cstage code at runtime — the
fresh-checkout cstage is still needed to bring the wwstage into
existence, but day-to-day work runs on the ww toolchain end to
end. The C `ww` driver in cmd/ww/ still drives the C 6c/6a/6l.

Test 993 (which used to be trivial — both drivers invoked the
same C tools) now meaningfully compares the cstage pipeline
against the wwstage pipeline on hello + wwdump and confirms
byte-identical exes.

The .combined.ww files for 6a/6l/ww/wwdump and smoke are
regenerated by the ww driver's `expand()` step; their diff is
the lib/os dup2 wrapper and the cgen.ww port from the prior two
commits, propagating into the bootstrap inputs.
2026-05-11 11:20:23 +09:00

14 KiB
Raw Blame History

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) with all, test, clean, install targets.
  • cmd/wwc/ C library skeleton (builds libwwc.a):
    • ww.h — central typedefs (Node, Sym, Type, Lex)
    • mem.c — bump arena allocator (no malloc in hot paths)
    • err.cerrorf, 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 asserts ww -V prints the version.

Per-target binaries (6c, 6a, 6l) are NOT created here. They ship in their own phases (4, 5, 6).

Exit criteria:

  • make produces out/bin/ww and out/lib/libwwc.a.
  • make test runs 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
  • 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 against libwwc.a.

Exit criteria:

  • A lextool test 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.cNode constructor helpers; printer.
  • Grammar coverage:
    • use imports (paths use . not ::)
    • type declarations (struct, alias, fn type) with trailing =
    • def constants
    • let declarations (top-level and local)
    • fn definitions with = { ... }; body. No methods; receivers are just first arguments.
    • export visibility marker
    • all expressions, statements, control flow
    • defer
    • body-less fn declarations 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 against libwwc.a):
    • 6.out.h — amd64 opcode enum, register names, addressing modes. Mirror ref/plan9front/sys/src/cmd/6c/ shape.
    • gc.hProg, Adr, scratch regs, stack layout.
    • cgen.c — typed AST → Prog list (walking, not SSA).
    • txt.cProg list → 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 .s text.

Exit criteria:

  • 6c hello.ww produces hello.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 of AX, BX, ..., R8-R15.
  • test/wwc/500_asm.c — round-trip known asm to known bytes.

Exit criteria:

  • 6a hello.s -o hello.o produces a valid ELF amd64 object.
  • objdump -d hello.o matches 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 .o files and .a archives.
    • 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.a produces a static ELF binary.
  • ldd hello reports "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, calls main.
  • rt/syscall.s — Linux syscall trampoline.
  • rt/panic.cpanic, abort, stack unwind for defer.
  • rt/mem.c — small page allocator built on mmap. Users get raw pages; higher-level allocators ship in lib/.
  • rt/slice.c — slice helpers used by codegen (bounds_check, slice_grow for explicit user calls).
  • Build artifact: out/lib/libwwrt.a.
  • cmd/ww/ driver: reads ww build foo.ww, runs 6c then 6a then 6l, links libwwrt.a into the output.

Exit criteria:

  • A ww program with no libc dependency runs.
  • Adding use os pulls 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 fn declarations + @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.
  • 6l learns 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, ...).
  • ww driver: ww build -lcrypto -ltls -lncurses resolves these via a pkg-config-style probe, hands .a paths to 6l.

Exit criteria:

  • examples/tlsclient.ww connects to https://example.org and prints the response. Statically linked. No .so deps in ldd.
  • examples/menu.ww runs 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):

  1. types limits, int helpers
  2. bytes slice ops on []u8
  3. strings ops on str
  4. io stream struct (function-pointer vtable, no interface); eof sentinel value
  5. bufio buffered reader/writer (Plan 9 bio analogue)
  6. fmt printf-family writing through io.stream
  7. os argv, env, stdin/out/err, file ops
  8. errors error = str and sentinel constants
  9. strconv itoa, atoi, parse float
  10. sort sort.slice, binary search
  11. path filepath ops
  12. encoding/utf8, encoding/hex, encoding/base64
  13. hash/crc32, hash/fnv, hash/sha256 (pure ww)
  14. time monotonic, wall clock, sleep
  15. net dial/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 README inside the module dir.

Phase 10 — Self-host

Goal: rewrite libwwc, 6c, 6a, 6l, and ww in ww. Drop the C bootstrap.

Steps:

  1. Translate ww.h and Node/Sym/Type/Prog/Adr to ww structs.
  2. Port lex.clex.ww. Diff token streams against C output.
  3. Port parse.cparse.ww. Diff ASTs.
  4. Port check.c. Diff typed ASTs.
  5. Port 6c (cgen, txt, peep, reg, swt). Diff .s output byte for byte.
  6. Port 6a and 6l. Diff resulting .o and final binaries.
  7. Three-stage bootstrap: Cstage (gcc-built tools) → ww1 → ww2 → ww3, with cmp ww2 ww3 OK for each tool.

Exit criteria:

  • make bootstrap runs 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 16 done. The five ww-side tools live in selfhost/cmd/:

  • wwc/ — ww-native lex/parse/check/cgen (the frontend library)
  • 6c/ — ww-native compiler binary; thin driver over wwc's cgen
  • 6a/ — ww-native amd64 assembler
  • 6l/ — ww-native amd64 linker
  • ww/ — ww-native driver, shells to 6c_ww/6a_ww/6l_ww

Step 7 reaches its fixed point: make bootstrap produces ww1 → ww2 → ww3 with cmp ww2 ww3 byte-identical. Tests 990994 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) and for examples that exercise the C-only features still missing from the ww side: ET_DYN dynamic linking in 6l (commit ecf0a84, used by snake).

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 a procval). No GC means proc lifetimes must be explicit.
  • chan T type. c <- v send; let v = <-c receive; select { ... }.
  • Channel ownership: closing a channel is a single-writer responsibility (Go rule). Buffered and unbuffered both supported.
  • sync package: 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; unsafe pkg 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.0 end of phase 0
  • v0.1 end of phase 1
  • ...
  • v0.10 end of phase 10 (self-hosted)
  • v1.0 end of phase 11 (CSP merged, ABI declared stable enough to start caring)

Risks and mitigations

  • Prog/Adr design 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 writing 6l. 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 .s text 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.