ww: import toolchain — C bootstrap + ww-side self-host (phases 0-10)

C bootstrap (phases 0-9):
  cmd/wwc, cmd/6c, cmd/6a, cmd/6l, cmd/ww, rt, lib/*.

ww-side self-host (phase 10):
  selfhost/cmd/wwc — ww-cgen frontend; bootstrap fixed point.
  selfhost/cmd/6a  — assembler; byte-identical to C 6a (test 991).
  selfhost/cmd/6l  — linker w/ archive (.a) support; byte-identical
                     to C 6l (test 992).
  selfhost/cmd/ww  — driver (build/run/version); byte-identical to
                     C ww (test 993).

make test: 15/15. make bootstrap: ww2.s == ww3.s, ww2.o == ww3.o,
ww2 == ww3 byte-identical, with the full ww-tooled chain.
This commit is contained in:
2026-05-11 02:17:47 +09:00
parent 4c8fc59ca1
commit 1657bdeda3
106 changed files with 35654 additions and 15 deletions

349
cmd/ww/main.c Normal file
View File

@@ -0,0 +1,349 @@
/*
* ww — the user-facing driver. Plan 9 cc(1) / Hare hare(1) analogue.
*
* Pipeline:
* ww build foo.ww → 6c foo.ww > foo.s ; 6a foo.s > foo.o ;
* 6l -o foo foo.o <runtime.o>
* ww run foo.ww → build then exec ./foo
*
* Tool paths default to siblings of $0 (so a fresh build runs out of
* out/bin/), and can be overridden with WW_6C / WW_6A / WW_6L.
*/
#include "ww.h"
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <libgen.h>
static const char *usage =
"usage: ww [-V] <subcommand> [args...]\n"
" -V print version and exit\n"
" build <path> compile module to a static binary\n"
" run <path> build then exec\n"
" test <path> build and run module tests\n"
" fmt <path> reformat ww source\n"
" version print version and exit\n";
static char *self_dir; /* directory containing this binary */
static const char *
toolpath(const char *envvar, const char *name)
{
const char *p = getenv(envvar);
if (p && p[0]) return p;
static char buf[1024];
snprintf(buf, sizeof buf, "%s/%s", self_dir, name);
return strdup(buf);
}
static int
run(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return 1;
}
/* Set of imported module paths, kept on the heap. Used to break
* cycles in `use` resolution. Linear because typical imports are
* a handful per build. */
struct ImportSet {
char **paths;
int n, cap;
};
static int
import_seen(struct ImportSet *s, const char *path)
{
for (int i = 0; i < s->n; i++)
if (strcmp(s->paths[i], path) == 0) return 1;
return 0;
}
static void
import_add(struct ImportSet *s, const char *path)
{
if (s->n + 1 > s->cap) {
s->cap = s->cap ? s->cap * 2 : 8;
s->paths = realloc(s->paths, s->cap * sizeof *s->paths);
}
s->paths[s->n++] = strdup(path);
}
/* try <dir>/X.ww then <dir>/X/X.ww; return resolved path in `out` or 0. */
static int
locate_import_in(const char *dir, const char *name, char *out, size_t outsz)
{
snprintf(out, outsz, "%s/%s.ww", dir, name);
if (access(out, 0) == 0) return 1;
snprintf(out, outsz, "%s/%s/%s.ww", dir, name, name);
if (access(out, 0) == 0) return 1;
return 0;
}
/* Walk a colon-separated dirlist trying to resolve `name`. Returns 1
* on the first hit. */
static int
locate_import(const char *dirs, const char *name, char *out, size_t outsz)
{
const char *p = dirs;
while (*p) {
const char *e = strchr(p, ':');
size_t n = e ? (size_t)(e - p) : strlen(p);
if (n > 0 && n < outsz) {
char dir[1024];
if (n >= sizeof dir) n = sizeof dir - 1;
memcpy(dir, p, n);
dir[n] = '\0';
if (locate_import_in(dir, name, out, outsz)) return 1;
}
if (!e) break;
p = e + 1;
}
return 0;
}
/* Recursively expand `path`: for each top-level `use IDENT;` we find,
* resolve the import and expand it first, then append our own bytes.
* Already-visited paths are skipped. */
static void
expand(FILE *out, const char *path, struct ImportSet *visited,
const char *libdir)
{
/* Use the path as-is for cycle detection. Different syntactic
* paths to the same file would re-import, which is harmless given
* our flat-scope concatenation (duplicate decls would fail at
* check time, surfacing the issue). */
if (import_seen(visited, path)) return;
import_add(visited, path);
FILE *in = fopen(path, "rb");
if (in == NULL) {
fprintf(stderr, "ww: cannot read %s\n", path);
return;
}
/* Scan once for `use X;` clauses, expand each. We keep the line
* format simple — leading whitespace + "use" + IDENT + optional
* dotted suffix + ";". Inside-comment occurrences would slip
* through, but ww source rarely puts that pattern in a comment. */
char line[2048];
while (fgets(line, sizeof line, in)) {
const char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (strncmp(p, "use ", 4) != 0 && strncmp(p, "use\t", 4) != 0)
continue;
p += 4;
while (*p == ' ' || *p == '\t') p++;
char name[256] = {0};
int j = 0;
while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')
|| *p == '_' || *p == '.' || (*p >= '0' && *p <= '9'))
if (j + 1 < (int)sizeof name) name[j++] = *p++;
if (j == 0) continue;
char ipath[1024];
if (!locate_import(libdir, name, ipath, sizeof ipath))
continue; /* silently skip if not found */
expand(out, ipath, visited, libdir);
}
rewind(in);
int ch;
while ((ch = fgetc(in)) != EOF) fputc(ch, out);
fputc('\n', out);
fclose(in);
}
static int
build_one(const char *src, const char *out, const char *extra_includes)
{
const char *c6 = toolpath("WW_6C", "6c");
const char *a6 = toolpath("WW_6A", "6a");
const char *l6 = toolpath("WW_6L", "6l");
const char *libdir = getenv("WW_LIB");
if (libdir == NULL || libdir[0] == 0) {
static char libbuf[1024];
snprintf(libbuf, sizeof libbuf, "%s/../lib", self_dir);
libdir = libbuf;
}
const char *srcdir = getenv("WW_SRCLIB");
static char srcbuf[1024];
if (srcdir == NULL || srcdir[0] == 0) {
/* in-tree default: ../../lib relative to bin/ */
snprintf(srcbuf, sizeof srcbuf, "%s/../../lib", self_dir);
if (access(srcbuf, 0) == 0) srcdir = srcbuf;
else if (access("lib", 0) == 0) srcdir = "lib";
else srcdir = libdir;
}
/* Compose the search path: any -I dirs first, then srcdir.
* locate_import walks them left-to-right. */
static char searchpath[4096];
if (extra_includes && extra_includes[0])
snprintf(searchpath, sizeof searchpath, "%s:%s", extra_includes, srcdir);
else
snprintf(searchpath, sizeof searchpath, "%s", srcdir);
srcdir = searchpath;
/* Strip extension to derive a stem; e.g. /tmp/foo.ww → /tmp/foo */
char stem[1024];
snprintf(stem, sizeof stem, "%s", src);
char *dot = strrchr(stem, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
char asmf[1024], obj[1024], combined[1024];
snprintf(asmf, sizeof asmf, "%s.s", stem);
snprintf(obj, sizeof obj, "%s.o", stem);
snprintf(combined, sizeof combined, "%s.combined.ww", stem);
/* Resolve `use X;` imports by concatenating sources into a temp
* file. The compiler then sees one flat source. */
{
FILE *cf = fopen(combined, "wb");
if (cf == NULL) {
fprintf(stderr, "ww: cannot open %s\n", combined);
return 1;
}
struct ImportSet visited = {0};
expand(cf, src, &visited, srcdir);
fclose(cf);
for (int i = 0; i < visited.n; i++) free(visited.paths[i]);
free(visited.paths);
}
char cmd[4096];
snprintf(cmd, sizeof cmd, "%s -o %s %s", c6, asmf, combined);
if (run(cmd) != 0) {
fprintf(stderr, "ww: 6c failed\n");
return 1;
}
snprintf(cmd, sizeof cmd, "%s -o %s %s", a6, obj, asmf);
if (run(cmd) != 0) {
fprintf(stderr, "ww: 6a failed\n");
return 1;
}
/* Link runtime: prefer libwwrt.a (selective archive pull) but
* fall back to start.o + syscall.o in the in-tree obj/ dir if
* we're running uninstalled. */
char rtargs[2048] = {0};
char path[1024];
snprintf(path, sizeof path, "%s/libwwrt.a", libdir);
if (access(path, 0) == 0) {
snprintf(rtargs, sizeof rtargs, "%s", path);
} else {
char a1[1024], a2[1024];
snprintf(a1, sizeof a1, "%s/../obj/rt/start.o", self_dir);
snprintf(a2, sizeof a2, "%s/../obj/rt/syscall.o", self_dir);
snprintf(rtargs, sizeof rtargs, "%s %s", a1, a2);
}
snprintf(cmd, sizeof cmd, "%s -o %s %s %s", l6, out, obj, rtargs);
if (run(cmd) != 0) {
fprintf(stderr, "ww: 6l failed\n");
return 1;
}
return 0;
}
static int
do_version(void)
{
printf("ww %s\n", WW_VERSION);
return 0;
}
static int
do_build(int argc, char **argv)
{
const char *src = NULL;
char libs[2048] = {0};
char incs[2048] = {0};
for (int i = 0; i < argc; i++) {
if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) {
char libpath[512];
const char *libdir = getenv("WW_LIB");
if (libdir == NULL) {
static char def[1024];
snprintf(def, sizeof def, "%s/../lib", self_dir);
libdir = def;
}
snprintf(libpath, sizeof libpath, "%s/lib%s.a",
libdir, argv[i] + 2);
size_t n = strlen(libs);
snprintf(libs + n, sizeof libs - n, " %s", libpath);
} else if (strcmp(argv[i], "-I") == 0 && i + 1 < argc) {
size_t n = strlen(incs);
snprintf(incs + n, sizeof incs - n,
"%s%s", n ? ":" : "", argv[++i]);
} else if (strncmp(argv[i], "-I", 2) == 0 && argv[i][2]) {
size_t n = strlen(incs);
snprintf(incs + n, sizeof incs - n,
"%s%s", n ? ":" : "", argv[i] + 2);
} else if (src == NULL) {
src = argv[i];
}
}
if (src == NULL) { fputs("ww build: missing source\n", stderr); return 2; }
char out[1024];
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
snprintf(out, sizeof out, "%s", base);
char *dot = strrchr(out, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
(void)libs; /* libs string is gathered; build_one currently
* always links libwwrt.a; -l support pending more
* glue between driver and 6l invocation. */
return build_one(src, out, incs);
}
static int
do_run(int argc, char **argv)
{
if (argc < 1) { fputs("ww run: missing source\n", stderr); return 2; }
char tmp[1024];
snprintf(tmp, sizeof tmp, "/tmp/ww_run_%d", getpid());
if (build_one(argv[0], tmp, "") != 0) return 1;
int rc = run(tmp);
unlink(tmp);
return rc;
}
static int
do_test(int argc, char **argv)
{
(void)argc; (void)argv;
fputs("ww: test: not implemented in this phase\n", stderr);
return 1;
}
static int
do_fmt(int argc, char **argv)
{
(void)argc; (void)argv;
fputs("ww: fmt: not implemented in this phase\n", stderr);
return 1;
}
int
main(int argc, char **argv)
{
if (argc >= 1) {
char buf[1024];
snprintf(buf, sizeof buf, "%s", argv[0]);
self_dir = strdup(dirname(buf));
}
if (argc < 2) { fputs(usage, stderr); return 2; }
const char *cmd = argv[1];
if (strcmp(cmd, "-V") == 0 || strcmp(cmd, "version") == 0)
return do_version();
if (strcmp(cmd, "-h") == 0 || strcmp(cmd, "--help") == 0) {
fputs(usage, stdout); return 0;
}
if (strcmp(cmd, "build") == 0) return do_build(argc - 2, argv + 2);
if (strcmp(cmd, "run") == 0) return do_run(argc - 2, argv + 2);
if (strcmp(cmd, "test") == 0) return do_test(argc - 2, argv + 2);
if (strcmp(cmd, "fmt") == 0) return do_fmt(argc - 2, argv + 2);
fprintf(stderr, "ww: unknown subcommand: %s\n", cmd);
fputs(usage, stderr);
return 2;
}