ww: rename toolchain to w-prefix + hare-style build/run/test driver

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.
This commit is contained in:
2026-05-11 13:49:27 +09:00
parent e217cd32d1
commit 2c33228b7e
96 changed files with 2129 additions and 973 deletions

View File

@@ -18,19 +18,21 @@ export fn assert(cond: bool, msg: str) void = {
if (!cond) { abort(msg); };
};
def SYS_READ: i64 = 0;
def SYS_WRITE: i64 = 1;
def SYS_OPEN: i64 = 2;
def SYS_CLOSE: i64 = 3;
def SYS_LSEEK: i64 = 8;
def SYS_ACCESS: i64 = 21;
def SYS_DUP2: i64 = 33;
def SYS_GETPID: i64 = 39;
def SYS_FORK: i64 = 57;
def SYS_EXECVE: i64 = 59;
def SYS_EXIT: i64 = 60;
def SYS_WAIT4: i64 = 61;
def SYS_UNLINK: i64 = 87;
def SYS_READ: i64 = 0;
def SYS_WRITE: i64 = 1;
def SYS_OPEN: i64 = 2;
def SYS_CLOSE: i64 = 3;
def SYS_LSEEK: i64 = 8;
def SYS_ACCESS: i64 = 21;
def SYS_DUP2: i64 = 33;
def SYS_GETPID: i64 = 39;
def SYS_FORK: i64 = 57;
def SYS_EXECVE: i64 = 59;
def SYS_EXIT: i64 = 60;
def SYS_WAIT4: i64 = 61;
def SYS_UNLINK: i64 = 87;
def SYS_GETCWD: i64 = 79;
def SYS_GETDENTS64: i64 = 217;
// open(2) flags. Linux values, matching <fcntl.h>.
def O_RDONLY: i32 = 0;
@@ -65,7 +67,7 @@ export fn close(fd: i32) i32 = {
// dup2(2): make `newfd` refer to the same description as `oldfd`,
// closing `newfd` first if open. Returns `newfd` on success or a
// negative errno. Used by 6c_ww to redirect stdout into an output
// negative errno. Used by w6c_ww to redirect stdout into an output
// file without changing the cgen emit path.
export fn dup2(oldfd: i32, newfd: i32) i32 = {
return syscall2(SYS_DUP2, oldfd: i64, newfd: i64): i32;
@@ -179,7 +181,33 @@ export fn wait4(pid: i32, status_out: *i32, options: i32, rusage: *void) i32 = {
options: i64, rusage: i64): i32;
};
// selfhost/cmd/wwc/mem.ww — port of cmd/wwc/mem.c.
// getcwd(2) — Linux flavour. Writes the NUL-terminated cwd into `buf`
// and returns the number of bytes written (including the NUL), or a
// negative errno. The driver uses it to expand `.` to the cwd's
// basename for `ww build` / `ww test`.
export fn getcwd(buf: *u8, n: u64) i64 = {
return syscall2(SYS_GETCWD, buf: i64, n: i64);
};
// getdents64(2) — Linux directory enumeration. The fd must be opened
// with O_RDONLY on a directory. `buf` receives a packed sequence of
// linux_dirent64 records:
//
// struct linux_dirent64 {
// u64 d_ino; // 0..7
// i64 d_off; // 8..15
// u16 d_reclen; // 16..17 — total bytes for this record
// u8 d_type; // 18 — DT_REG/DT_DIR/...
// u8 d_name[]; // 19.. — NUL-terminated name + padding
// };
//
// Returns bytes written into `buf` (advance by d_reclen to walk),
// 0 at end-of-directory, or a negative errno.
export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = {
return syscall3(SYS_GETDENTS64, fd: i64, buf: i64, n: i64);
};
// selfhost/cmd/wcc/mem.ww — port of cmd/wcc/mem.c.
//
// Bump arena allocator. Backed by the runtime page allocator
// (rt_alloc / rt_free), no libc. Each chunk is mmap'd; when the
@@ -187,7 +215,7 @@ export fn wait4(pid: i32, status_out: *i32, options: i32, rusage: *void) i32 = {
// unmaps the chain.
//
// Memory handed out is 16-byte aligned. The C version under
// cmd/wwc/ is retained until the three-stage bootstrap diffs clean.
// cmd/wcc/ is retained until the three-stage bootstrap diffs clean.
use os;
@@ -406,8 +434,8 @@ export fn parseu64(s: str) (u64 | str) = {
return v;
};
// selfhost/cmd/wwc/tok.ww — port of cmd/wwc/tok.c plus the Tkind /
// Tok / Pos shapes from cmd/wwc/ww.h.
// selfhost/cmd/wcc/tok.ww — port of cmd/wcc/tok.c plus the Tkind /
// Tok / Pos shapes from cmd/wcc/ww.h.
//
// Token kind values must stay numerically equal to the C side: the
// 990_selfhost test diffs ww-side wwdump output against C-side
@@ -415,13 +443,13 @@ export fn parseu64(s: str) (u64 | str) = {
// integers and breaks the diff.
//
// Bottom of file: tokprint, which emits one token per line in a
// format identical to cmd/wwc/tok.c:tokprint().
// format identical to cmd/wcc/tok.c:tokprint().
use os;
use strconv;
// ---- Tkind ------------------------------------------------------------
// Mirror of the C enum in cmd/wwc/ww.h. Don't reorder.
// Mirror of the C enum in cmd/wcc/ww.h. Don't reorder.
def TK_NONE: i32 = 0;
def TK_EOF: i32 = 1;
@@ -516,7 +544,7 @@ def TK_LAST: i32 = 80;
// ---- Pos / Tok --------------------------------------------------------
//
// `pos` is used at error-reporting boundaries; we always pass it via
// *pos so the value never gets struct-copied (6c can't yet copy a
// *pos so the value never gets struct-copied (w6c can't yet copy a
// 24-byte struct).
//
// `tok` is flat — file/line/col live directly on the token rather than
@@ -554,7 +582,7 @@ fn streq_n(a: *u8, b: str, n: i32) bool = {
// kwlookup — returns the matching TK_* keyword kind for a byte run,
// or TK_NONE if it's an ordinary identifier. Linear search over a
// small alphabetised list, matching cmd/wwc/tok.c.
// small alphabetised list, matching cmd/wcc/tok.c.
export fn kwlookup(p: *u8, n: i32) i32 = {
if (streq_n(p, "as", n)) { return TK_AS; };
if (streq_n(p, "break", n)) { return TK_BREAK; };
@@ -683,7 +711,7 @@ export fn tokname(k: i32) str = {
// ---- writer for tokprint ----------------------------------------------
//
// fputq mirrors cmd/wwc/tok.c:fputq — quote the string with C-style
// fputq mirrors cmd/wcc/tok.c:fputq — quote the string with C-style
// escapes for \, ", \n, \t, \r and \xNN for other non-printables.
fn fputc_byte(fd: i32, b: u8) void = {
@@ -750,14 +778,14 @@ fn fputq(fd: i32, p: *u8, n: i32) void = {
};
// tokprint — write one token line to fd. Format must match
// cmd/wwc/tok.c:tokprint() byte-for-byte: that's the diff anchor.
// cmd/wcc/tok.c:tokprint() byte-for-byte: that's the diff anchor.
// "<file>:<line>:<col> <kindname>[ <value>]\n"
//
// Takes `t` by pointer because 6c can't yet pass a >16-byte struct
// Takes `t` by pointer because w6c can't yet pass a >16-byte struct
// by value; the C version takes Tok by value.
export fn tokprint(fd: i32, t: *tok) void = {
// Chained-dot field reads (`t.x.y`) on str sub-fields aren't yet
// reduced by 6c — `t.x.y` returns the whole str. Lift the str
// reduced by w6c — `t.x.y` returns the whole str. Lift the str
// fields into locals so we can use the str pseudo-field path.
let tfile: str = t.file;
let ttext: str = t.text;
@@ -894,14 +922,14 @@ export fn toupper(c: u8) u8 = {
return c;
};
// selfhost/cmd/wwc/lex.ww — port of cmd/wwc/lex.c.
// selfhost/cmd/wcc/lex.ww — port of cmd/wcc/lex.c.
//
// The DFA, the helpers, and the order of decisions all mirror the C
// version exactly. The 990_selfhost test diffs the resulting token
// stream against the C-side wwdump byte-for-byte; any divergence is
// a port bug.
//
// Calling-convention note: 6c can't yet pass or return structs >16
// Calling-convention note: w6c can't yet pass or return structs >16
// bytes by value, so `tok` and `pos` are passed by pointer (out
// params). The C version passes `Tok` by value; we differ here only
// in shape, not in observable behaviour. Token kind values stay
@@ -1551,7 +1579,7 @@ export fn lexnext(l: *lex, out: *tok) void = {
out.text = astrndup(l.a, one.ptr, 1u64);
};
// selfhost/cmd/wwc/ast.ww — port of cmd/wwc/ast.c (Node defs + printer).
// selfhost/cmd/wcc/ast.ww — port of cmd/wcc/ast.c (Node defs + printer).
//
// Status: AST printer is fully ported. Constructor `newnode` is here.
// The parser (parse.ww) is currently minimal — see its file header.
@@ -1567,7 +1595,7 @@ use tok;
// ---- Nkind ------------------------------------------------------------
//
// Mirror of cmd/wwc/ww.h Nkind. Values must stay numerically equal so
// Mirror of cmd/wcc/ww.h Nkind. Values must stay numerically equal so
// the AST diff probe in 990_selfhost works.
def N_NONE: i32 = 0;
@@ -1887,7 +1915,7 @@ export fn astprint(fd: i32, n: *node) void = {
pr(fd, n, 0);
};
// selfhost/cmd/wwc/parse.ww — port of cmd/wwc/parse.c.
// selfhost/cmd/wcc/parse.ww — port of cmd/wcc/parse.c.
//
// Status: GROWING stub. Currently handles top-level `use IDENT;`,
// `def NAME: TYPE = LIT;`, `type NAME = TYPE;`, and `fn NAME(params)
@@ -1900,7 +1928,7 @@ export fn astprint(fd: i32, n: *node) void = {
// surface form lands here gradually so the AST diff in 990_selfhost
// grows toward whole-language coverage one increment at a time.
//
// Calling-convention shim: 6c can't yet pass a sub-struct field
// Calling-convention shim: w6c can't yet pass a sub-struct field
// (e.g. p.cur.line where p.cur is a `tok` of size 76). The parser
// stores the current token as flat primitive fields rather than a
// nested `tok` struct; `refill` copies a freshly lexed token in.
@@ -2635,7 +2663,7 @@ fn parsestmt(p: *parser) *node = {
};
// expression statement, or tuple-destructure multi-assign:
// a, b = expr;
// Mirrors cmd/wwc/parse.c:1015-1031. We parse the first lvalue
// Mirrors cmd/wcc/parse.c:1015-1031. We parse the first lvalue
// with parseexpr (matches the C side); subsequent lvalues go
// through parsebin(parseunary, 1) so the `=` stays for us to
// consume — parseexpr would absorb it.
@@ -2870,7 +2898,7 @@ export fn parsefile(p: *parser) *node = {
return f;
};
// selfhost/cmd/wwc/type.ww — port of cmd/wwc/type.c.
// selfhost/cmd/wcc/type.ww — port of cmd/wcc/type.c.
//
// Status: full structural port. The C version uses module-globals for
// the primitive types (ty_void, ty_i32, …); ww doesn't have writable
@@ -2882,7 +2910,7 @@ use os;
use mem;
// ---- TypeKind ---------------------------------------------------------
// Numeric values must stay aligned with cmd/wwc/ww.h TypeKind so the
// Numeric values must stay aligned with cmd/wcc/ww.h TypeKind so the
// next diff signal (typed-AST printer / cgen) can compare across the
// two implementations.
@@ -3200,7 +3228,7 @@ export fn type_eq(a: *tinfo, b: *tinfo) bool = {
return true; // primitives match by kind alone
};
// selfhost/cmd/wwc/sym.ww — port of cmd/wwc/sym.c.
// selfhost/cmd/wcc/sym.ww — port of cmd/wcc/sym.c.
//
// Per-scope hashtable, chained to the parent. Lookup walks up.
// Plan 9 / Hare flavoured. Duplicate definitions in the same scope
@@ -3210,7 +3238,7 @@ use mem;
use typ;
use ast;
// Symbol kinds — must stay numerically aligned with cmd/wwc/ww.h Skind.
// Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind.
def SK_NONE: i32 = 0;
def SK_VAR: i32 = 1;
def SK_PARAM: i32 = 2;
@@ -3314,11 +3342,11 @@ export fn scope_define(s: *scope, name: str, k: i32, t: *tinfo, decl: *node) *sy
return sy;
};
// selfhost/cmd/wwc/check.ww — minimal port of cmd/wwc/check.c.
// selfhost/cmd/wcc/check.ww — minimal port of cmd/wcc/check.c.
//
// Status: name-resolution + primitive-type seeding only. Full type
// inference, conversion rules, tagged-union dispatch typing, return-
// type checking, etc. all live in cmd/wwc/check.c (937 lines) and
// type checking, etc. all live in cmd/wcc/check.c (937 lines) and
// will land here in subsequent commits.
//
// What this version does:
@@ -3562,10 +3590,10 @@ export fn check_file(c: *checker, file: *node) void = {
};
};
// selfhost/cmd/wwc/cgen.ww — port of cmd/6c/cgen.c.
// selfhost/cmd/wcc/cgen.ww — port of cmd/w6c/cgen.c.
//
// Status: GROWING. Each subsystem we add is verified by `wwdump_ww -c`
// producing byte-identical output to C-side `6c` for the same source,
// producing byte-identical output to C-side `w6c` for the same source,
// then by assembling + linking + running the result.
//
// Current coverage:
@@ -3760,7 +3788,7 @@ fn local_alloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
fn local_add(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
// Name-based slot reuse for N_LETs and params: if `name` is
// already declared in this function, return its existing
// offset. Mirrors C cgen (cmd/6c/cgen.c:localoff). Two
// offset. Mirrors C cgen (cmd/w6c/cgen.c:localoff). Two
// disjoint scopes that declare the same name share one slot —
// so `escape` in wwdump (three `let cp: pos;` across separate
// branches) reserves one slot, not three. scan_locals does
@@ -4097,7 +4125,7 @@ fn fnret_lookup(c: *cgen, name: str) *node = {
// ---- def-constant registry ------------------------------------------
//
// `def NAME: T = LIT;` becomes a DATA symbol the C-side 6c emits; an
// `def NAME: T = LIT;` becomes a DATA symbol the C-side w6c emits; an
// ident reference loads it via `MOVQ NAME(SB), AX`. We collect them at
// file load and consult on N_IDENT lookup.
@@ -4725,7 +4753,7 @@ fn node_isunsigned(c: *cgen, n: *node) bool = {
// Walks the base local's declared type and pulls the element
// out — *u8 → u8, [N]u32 → u32, []u64 → u64. Without this the
// compare-codegen for `p[i] >= 48u8` falls back to signed JGE
// instead of JAE, diverging from C 6c on byte indexing.
// instead of JAE, diverging from C w6c on byte indexing.
if (k == N_INDEX) {
let base: *node = n.lhs;
if (base != nil) {
@@ -5198,7 +5226,7 @@ fn cgexpr(c: *cgen, n: *node) void = {
// path the cgen falls through and AX retains whatever the
// inner expression left there — typically the *struct pointer
// itself, so reads silently get the pointer value instead of
// the field. (Showed up porting 6l/pass.ww.)
// the field. (Showed up porting w6l/pass.ww.)
if (lhs != nil) {
if (lhs.kind == N_DOT) {
let inner_t: *node = dot_inner_struct_ptr(c, lhs);
@@ -5487,7 +5515,7 @@ fn cgexpr(c: *cgen, n: *node) void = {
};
cgexpr(c, n.rhs);
// Push order matches C cgen
// (cmd/6c/cgen.c:1033-1041): PUSHQ AX
// (cmd/w6c/cgen.c:1033-1041): PUSHQ AX
// (ptr) first, then PUSHQ BX (len) if
// str, so the pop sequence is POP CX
// (len) → POP AX (ptr) → MOVQ AX,
@@ -6259,7 +6287,7 @@ fn cgstmt(c: *cgen, n: *node) void = {
};
} else {
// Bare `let x: T;` with no initializer. C cgen
// (cmd/6c/cgen.c:2181-2183) zero-inits only when
// (cmd/w6c/cgen.c:2181-2183) zero-inits only when
// the underlying type's natural size is 8 — pointers,
// i64/u64, function pointers, ints. Structs/arrays/
// slices/strings/tagged/tuples are left for per-field
@@ -6330,7 +6358,7 @@ fn cgstmt(c: *cgen, n: *node) void = {
// Tuple-destructure assign: `a, b = call();`. The call's tuple
// return lands in (AX, DX); push DX to free it, store AX into
// the first lvalue, then pop DX into the second. Mirrors
// cmd/6c/cgen.c:2424-2440. Lvalues beyond two are dropped (same
// cmd/w6c/cgen.c:2424-2440. Lvalues beyond two are dropped (same
// as C — no fixture uses >2 today).
if (k == N_MASSIGN) {
if (n.rhs != nil) { cgexpr(c, n.rhs); };