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.
This commit is contained in:
20
PLAN.md
20
PLAN.md
@@ -293,16 +293,26 @@ Exit criteria:
|
||||
|
||||
### Status (2026-05)
|
||||
|
||||
Steps 1–6 done. The four ww-side tools live in `selfhost/cmd/`:
|
||||
Steps 1–6 done. The five ww-side tools live in `selfhost/cmd/`:
|
||||
|
||||
- `wwc/` — ww-native lex/parse/check/cgen
|
||||
- `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
|
||||
- `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 990–993 confirm each
|
||||
selfhost tool is byte-identical to its Cstage counterpart.
|
||||
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) 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/`,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@symbol("rt_syscall") fn syscall1(num: i64, a: i64) i64;
|
||||
@symbol("rt_syscall") fn syscall2(num: i64, a: i64, b: i64) i64;
|
||||
@symbol("rt_syscall") fn syscall3(num: i64, a: i64, b: i64, c: i64) i64;
|
||||
@symbol("rt_syscall") fn syscall4(num: i64, a: i64, b: i64, c: i64, d: i64) i64;
|
||||
|
||||
@symbol("rt_alloc") fn alloc(n: u64) *void;
|
||||
@symbol("rt_free") fn free(p: *void, n: u64) void;
|
||||
@@ -17,12 +18,18 @@ 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_EXIT: i64 = 60;
|
||||
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_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;
|
||||
|
||||
// open(2) flags. Linux values, matching <fcntl.h>.
|
||||
def O_RDONLY: i32 = 0;
|
||||
@@ -127,6 +134,42 @@ export fn writefull(fd: i32, buf: *u8, n: u64) i64 = {
|
||||
return sent: i64;
|
||||
};
|
||||
|
||||
// ---- process and filesystem helpers used by the `ww` driver ----------
|
||||
|
||||
// access(2): returns 0 if the file is reachable, negative errno
|
||||
// otherwise. mode is the bitset described in <unistd.h> (F_OK=0).
|
||||
export fn access(path: *u8, mode: i32) i32 = {
|
||||
return syscall2(SYS_ACCESS, path: i64, mode: i64): i32;
|
||||
};
|
||||
|
||||
// unlink(2).
|
||||
export fn unlink(path: *u8) i32 = {
|
||||
return syscall1(SYS_UNLINK, path: i64): i32;
|
||||
};
|
||||
|
||||
// getpid(2). Used by the driver to mint unique scratch paths.
|
||||
export fn getpid() i32 = {
|
||||
return syscall0(SYS_GETPID): i32;
|
||||
};
|
||||
|
||||
// fork(2): 0 in the child, child pid in the parent, negative errno
|
||||
// on failure.
|
||||
export fn fork() i32 = {
|
||||
return syscall0(SYS_FORK): i32;
|
||||
};
|
||||
|
||||
// execve(2): on success, does not return.
|
||||
export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = {
|
||||
return syscall3(SYS_EXECVE, path: i64, argv: i64, envp: i64): i32;
|
||||
};
|
||||
|
||||
// wait4(2): wait for `pid` (or any child if -1), store status in
|
||||
// `*status_out`, return the pid that ended (or negative errno).
|
||||
export fn wait4(pid: i32, status_out: *i32, options: i32, rusage: *void) i32 = {
|
||||
return syscall4(SYS_WAIT4, pid: i64, status_out: i64,
|
||||
options: i64, rusage: i64): i32;
|
||||
};
|
||||
|
||||
// selfhost/cmd/wwc/mem.ww — port of cmd/wwc/mem.c.
|
||||
//
|
||||
// Bump arena allocator. Backed by the runtime page allocator
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@symbol("rt_syscall") fn syscall1(num: i64, a: i64) i64;
|
||||
@symbol("rt_syscall") fn syscall2(num: i64, a: i64, b: i64) i64;
|
||||
@symbol("rt_syscall") fn syscall3(num: i64, a: i64, b: i64, c: i64) i64;
|
||||
@symbol("rt_syscall") fn syscall4(num: i64, a: i64, b: i64, c: i64, d: i64) i64;
|
||||
|
||||
@symbol("rt_alloc") fn alloc(n: u64) *void;
|
||||
@symbol("rt_free") fn free(p: *void, n: u64) void;
|
||||
@@ -17,12 +18,18 @@ 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_EXIT: i64 = 60;
|
||||
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_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;
|
||||
|
||||
// open(2) flags. Linux values, matching <fcntl.h>.
|
||||
def O_RDONLY: i32 = 0;
|
||||
@@ -127,6 +134,42 @@ export fn writefull(fd: i32, buf: *u8, n: u64) i64 = {
|
||||
return sent: i64;
|
||||
};
|
||||
|
||||
// ---- process and filesystem helpers used by the `ww` driver ----------
|
||||
|
||||
// access(2): returns 0 if the file is reachable, negative errno
|
||||
// otherwise. mode is the bitset described in <unistd.h> (F_OK=0).
|
||||
export fn access(path: *u8, mode: i32) i32 = {
|
||||
return syscall2(SYS_ACCESS, path: i64, mode: i64): i32;
|
||||
};
|
||||
|
||||
// unlink(2).
|
||||
export fn unlink(path: *u8) i32 = {
|
||||
return syscall1(SYS_UNLINK, path: i64): i32;
|
||||
};
|
||||
|
||||
// getpid(2). Used by the driver to mint unique scratch paths.
|
||||
export fn getpid() i32 = {
|
||||
return syscall0(SYS_GETPID): i32;
|
||||
};
|
||||
|
||||
// fork(2): 0 in the child, child pid in the parent, negative errno
|
||||
// on failure.
|
||||
export fn fork() i32 = {
|
||||
return syscall0(SYS_FORK): i32;
|
||||
};
|
||||
|
||||
// execve(2): on success, does not return.
|
||||
export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = {
|
||||
return syscall3(SYS_EXECVE, path: i64, argv: i64, envp: i64): i32;
|
||||
};
|
||||
|
||||
// wait4(2): wait for `pid` (or any child if -1), store status in
|
||||
// `*status_out`, return the pid that ended (or negative errno).
|
||||
export fn wait4(pid: i32, status_out: *i32, options: i32, rusage: *void) i32 = {
|
||||
return syscall4(SYS_WAIT4, pid: i64, status_out: i64,
|
||||
options: i64, rusage: i64): i32;
|
||||
};
|
||||
|
||||
// selfhost/cmd/wwc/mem.ww — port of cmd/wwc/mem.c.
|
||||
//
|
||||
// Bump arena allocator. Backed by the runtime page allocator
|
||||
|
||||
@@ -24,6 +24,7 @@ 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;
|
||||
@@ -62,6 +63,14 @@ export fn close(fd: i32) i32 = {
|
||||
return syscall1(SYS_CLOSE, fd: i64): 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
|
||||
// file without changing the cgen emit path.
|
||||
export fn dup2(oldfd: i32, newfd: i32) i32 = {
|
||||
return syscall2(SYS_DUP2, oldfd: i64, newfd: i64): i32;
|
||||
};
|
||||
|
||||
// Fallible wrappers. The error variant is a plain str (Plan 9 errstr
|
||||
// model, see lib/errors); the sum type makes success/failure explicit
|
||||
// without overloading length-zero.
|
||||
@@ -687,16 +696,22 @@ fn append_lit(stem: *u8, suffix: str) *u8 = {
|
||||
};
|
||||
|
||||
// build_one — compile `src` into the executable named `out`.
|
||||
// self_dir: NUL-terminated dir containing this driver (and 6c/6a/6l)
|
||||
// self_dir: NUL-terminated dir containing this driver and the
|
||||
// wwstage tools (6c_ww/6a_ww/6l_ww)
|
||||
// src: NUL-terminated path to the .ww file
|
||||
// out: NUL-terminated desired output path
|
||||
// incs: NUL-terminated colon-list of -I dirs (may be empty)
|
||||
//
|
||||
// The ww-side driver shells to the ww-side tools so a `ww_ww build`
|
||||
// touches no C-built code at runtime. The C `ww` driver in cmd/ww/
|
||||
// still drives the C-built 6c/6a/6l. Test 993 pins the two
|
||||
// pipelines to byte-identical output on a corpus.
|
||||
fn build_one(self_dir: *u8, src: *u8, out: *u8, incs: *u8) i32 = {
|
||||
let a: *arena = newarena();
|
||||
|
||||
let c6: *u8 = join_path_lit(self_dir, "6c");
|
||||
let a6: *u8 = join_path_lit(self_dir, "6a");
|
||||
let l6: *u8 = join_path_lit(self_dir, "6l");
|
||||
let c6: *u8 = join_path_lit(self_dir, "6c_ww");
|
||||
let a6: *u8 = join_path_lit(self_dir, "6a_ww");
|
||||
let l6: *u8 = join_path_lit(self_dir, "6l_ww");
|
||||
|
||||
// Default lib search path: <self_dir>/../../lib
|
||||
let dotdot_lib: *u8 = os.alloc(PATH_MAX): *u8;
|
||||
|
||||
@@ -408,16 +408,22 @@ fn append_lit(stem: *u8, suffix: str) *u8 = {
|
||||
};
|
||||
|
||||
// build_one — compile `src` into the executable named `out`.
|
||||
// self_dir: NUL-terminated dir containing this driver (and 6c/6a/6l)
|
||||
// self_dir: NUL-terminated dir containing this driver and the
|
||||
// wwstage tools (6c_ww/6a_ww/6l_ww)
|
||||
// src: NUL-terminated path to the .ww file
|
||||
// out: NUL-terminated desired output path
|
||||
// incs: NUL-terminated colon-list of -I dirs (may be empty)
|
||||
//
|
||||
// The ww-side driver shells to the ww-side tools so a `ww_ww build`
|
||||
// touches no C-built code at runtime. The C `ww` driver in cmd/ww/
|
||||
// still drives the C-built 6c/6a/6l. Test 993 pins the two
|
||||
// pipelines to byte-identical output on a corpus.
|
||||
fn build_one(self_dir: *u8, src: *u8, out: *u8, incs: *u8) i32 = {
|
||||
let a: *arena = newarena();
|
||||
|
||||
let c6: *u8 = join_path_lit(self_dir, "6c");
|
||||
let a6: *u8 = join_path_lit(self_dir, "6a");
|
||||
let l6: *u8 = join_path_lit(self_dir, "6l");
|
||||
let c6: *u8 = join_path_lit(self_dir, "6c_ww");
|
||||
let a6: *u8 = join_path_lit(self_dir, "6a_ww");
|
||||
let l6: *u8 = join_path_lit(self_dir, "6l_ww");
|
||||
|
||||
// Default lib search path: <self_dir>/../../lib
|
||||
let dotdot_lib: *u8 = os.alloc(PATH_MAX): *u8;
|
||||
|
||||
@@ -24,6 +24,7 @@ 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;
|
||||
@@ -62,6 +63,14 @@ export fn close(fd: i32) i32 = {
|
||||
return syscall1(SYS_CLOSE, fd: i64): 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
|
||||
// file without changing the cgen emit path.
|
||||
export fn dup2(oldfd: i32, newfd: i32) i32 = {
|
||||
return syscall2(SYS_DUP2, oldfd: i64, newfd: i64): i32;
|
||||
};
|
||||
|
||||
// Fallible wrappers. The error variant is a plain str (Plan 9 errstr
|
||||
// model, see lib/errors); the sum type makes success/failure explicit
|
||||
// without overloading length-zero.
|
||||
@@ -3706,10 +3715,12 @@ fn cgen_init(c: *cgen, a: *arena) void = {
|
||||
c.loop_cont_buf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
|
||||
};
|
||||
|
||||
fn local_add(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
||||
// Round slot size up to 8-byte multiple so all locals stay
|
||||
// 8-aligned (C cgen does the same — buf[4]u8 still occupies an
|
||||
// 8-byte slot).
|
||||
// local_alloc — append a slot for `name` without dedup. Used for
|
||||
// match-arm bindings, which C cgen allocates via cgexpr's by-value
|
||||
// `locals` list — so two separate matches each get fresh slots even
|
||||
// when their bind names collide. scan_locals follows the same rule
|
||||
// for N_MCASE.
|
||||
fn local_alloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
||||
let asz: i32 = sz;
|
||||
if (asz < 8) { asz = 8; };
|
||||
if ((asz & 7) != 0) { asz = (asz + 7) & ~7; };
|
||||
@@ -3724,6 +3735,50 @@ fn local_add(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
||||
return off;
|
||||
};
|
||||
|
||||
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
|
||||
// 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
|
||||
// the matching dedup at prologue time so the SUBQ stays in
|
||||
// sync.
|
||||
//
|
||||
// On a dedup hit we also overwrite the stored tnode to match
|
||||
// the new declaration's type. C reads `n->lhs->type` (filled
|
||||
// by the checker) at every N_DOT/N_CAST site; we read
|
||||
// `lc.tnode`, so it must follow source order. Without this,
|
||||
// a later `let m: *node` inside a branch keeps an earlier
|
||||
// `let m: i32`'s tnode and `m.next` falls into the SB fallback.
|
||||
let cur: *local = c.locals;
|
||||
for (cur != nil) {
|
||||
let cn: str = cur.name;
|
||||
if (streq(cn, name)) {
|
||||
cur.tnode = tnode;
|
||||
return cur.off;
|
||||
};
|
||||
cur = cur.lnext;
|
||||
};
|
||||
return local_alloc(c, name, sz, tnode);
|
||||
};
|
||||
|
||||
// scan_seen_mark — called by scan_locals on every let / match-bind
|
||||
// site. Returns true if `name` is already tracked in c.locals (so
|
||||
// the slot will be shared at emission time — no new frame bump).
|
||||
// Otherwise appends a name-only stub and returns false. Stubs are
|
||||
// thrown away when cgfn resets c.locals before emission.
|
||||
fn scan_seen_mark(c: *cgen, name: str) bool = {
|
||||
if (local_find_node(c, name) != nil) { return true; };
|
||||
let l: *local = amalloc(c.a, 48u64): *local;
|
||||
l.name = name;
|
||||
l.off = 0;
|
||||
l.tnode = nil;
|
||||
l.lnext = c.locals;
|
||||
c.locals = l;
|
||||
return false;
|
||||
};
|
||||
|
||||
fn local_find_node(c: *cgen, name: str) *local = {
|
||||
let l: *local = c.locals;
|
||||
for (l != nil) {
|
||||
@@ -4608,14 +4663,18 @@ fn cgexpr(c: *cgen, n: *node) void = {
|
||||
let k: i32 = n.kind;
|
||||
|
||||
if (k == N_INTLIT) {
|
||||
// Print signed (i64), not unsigned (u64). C cgen uses
|
||||
// `$%lld` so 64-bit constants with bit 63 set show up as
|
||||
// negative — e.g. FNV-1a's offset basis prints as
|
||||
// $-3750763034362895579, not $14695981039346656037.
|
||||
emit_line("\tMOVQ\t$");
|
||||
emit_uint(n.uval);
|
||||
emit_int(n.uval: i64);
|
||||
emit_line(", AX\n");
|
||||
return;
|
||||
};
|
||||
if (k == N_RUNELIT) {
|
||||
emit_line("\tMOVQ\t$");
|
||||
emit_uint(n.uval);
|
||||
emit_int(n.uval: i64);
|
||||
emit_line(", AX\n");
|
||||
return;
|
||||
};
|
||||
@@ -4678,11 +4737,15 @@ fn cgexpr(c: *cgen, n: *node) void = {
|
||||
return;
|
||||
};
|
||||
// Fn-name used as a value (e.g. `let f = some_fn;` or
|
||||
// `... = some_fn;`). LEAQ the symbol address into AX.
|
||||
// `... = some_fn;`). LEAQ the symbol address into AX —
|
||||
// resolved through ffi_resolve so a body-less FFI binding
|
||||
// emits the C symbol it was declared with via @symbol(),
|
||||
// not the ww-side ident.
|
||||
let rt: *node = fnret_lookup(c, nm);
|
||||
if (rt != nil) {
|
||||
let resolved: str = ffi_resolve(c, nm);
|
||||
emit_line("\tLEAQ\t");
|
||||
os.write(1, nm.ptr, nm.len: u64);
|
||||
os.write(1, resolved.ptr, resolved.len: u64);
|
||||
emit_line("(SB), AX\n");
|
||||
return;
|
||||
};
|
||||
@@ -4822,7 +4885,11 @@ fn cgexpr(c: *cgen, n: *node) void = {
|
||||
if (pat != nil) {
|
||||
let bsz: i32 = 8;
|
||||
if (is_str_type(c, pat)) { bsz = 16; };
|
||||
let voff: i32 = local_add(c, bn, bsz, pat);
|
||||
// local_alloc (not local_add): match-arm
|
||||
// binds don't dedup with same-named binds
|
||||
// in *other* matches, since C's cgexpr
|
||||
// allocates a fresh slot per match expr.
|
||||
let voff: i32 = local_alloc(c, bn, bsz, pat);
|
||||
emit_line("\tMOVQ\t");
|
||||
emit_off((scrut_off + 8): i64);
|
||||
emit_line("(BP), AX\n");
|
||||
@@ -5197,9 +5264,19 @@ fn cgexpr(c: *cgen, n: *node) void = {
|
||||
callee_name.ptr = nil; callee_name.len = 0;
|
||||
// Detect fn-pointer field call: `w.emit(args)` where `w` is
|
||||
// a struct local and `emit` is an N_TFN field. Load the
|
||||
// field value into AX and CALL through it.
|
||||
// field value into AX and CALL through it. Also detect a
|
||||
// bare `fp(args)` where `fp` is a local holding a function
|
||||
// pointer — mirror C cgen's localfind dispatch (commit
|
||||
// 635818e). Without this the call emits `CALL fp(SB)` and
|
||||
// the linker rightly fails.
|
||||
let is_fnptr_call: bool = false;
|
||||
if (callee != nil) {
|
||||
if (callee.kind == N_IDENT) {
|
||||
let cn: str = callee.str;
|
||||
if (local_find_node(c, cn) != nil) {
|
||||
is_fnptr_call = true;
|
||||
};
|
||||
};
|
||||
if (callee.kind == N_DOT) {
|
||||
let base: *node = callee.lhs;
|
||||
let fld: str = callee.str;
|
||||
@@ -5317,17 +5394,24 @@ fn cgexpr(c: *cgen, n: *node) void = {
|
||||
};
|
||||
};
|
||||
cgexpr(c, n.rhs);
|
||||
if (elem_str) { emit_line("\tPUSHQ\tBX\n"); };
|
||||
// Push order matches C cgen
|
||||
// (cmd/6c/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,
|
||||
// (BX) → MOVQ CX, 8(BX).
|
||||
emit_line("\tPUSHQ\tAX\n");
|
||||
if (elem_str) { emit_line("\tPUSHQ\tBX\n"); };
|
||||
cgexpr(c, inner);
|
||||
emit_line("\tMOVQ\tAX, BX\n");
|
||||
emit_line("\tPOPQ\tAX\n");
|
||||
if (elem_str) {
|
||||
emit_line("\tMOVQ\tAX, (BX)\n");
|
||||
emit_line("\tPOPQ\tCX\n");
|
||||
emit_line("\tPOPQ\tAX\n");
|
||||
emit_line("\tMOVQ\tAX, (BX)\n");
|
||||
emit_line("\tMOVQ\tCX, 8(BX)\n");
|
||||
return;
|
||||
};
|
||||
emit_line("\tPOPQ\tAX\n");
|
||||
emit_line("\t");
|
||||
emit_line(store_op);
|
||||
emit_line("\tAX, (BX)\n");
|
||||
@@ -5830,13 +5914,21 @@ fn scan_locals(c: *cgen, n: *node) i32 = {
|
||||
// scan_locals must agree with local_add or the prologue
|
||||
// SUBQ undersizes the frame and lets overflow into the
|
||||
// caller's stack — corrupting whatever's at -frameSize..-1
|
||||
// of the caller.
|
||||
let sz: i32 = slot_size(c, n.lhs);
|
||||
if (sz < 8) { sz = 8; };
|
||||
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
|
||||
total += sz;
|
||||
// of the caller. Same-name re-declarations share the first
|
||||
// slot (see scan_seen_mark / local_add).
|
||||
if (!scan_seen_mark(c, n.str)) {
|
||||
let sz: i32 = slot_size(c, n.lhs);
|
||||
if (sz < 8) { sz = 8; };
|
||||
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
|
||||
total += sz;
|
||||
};
|
||||
};
|
||||
// Match-arm binding (`case let v: T => ...`) gets a slot too.
|
||||
// Crucially we do NOT dedup these against c.locals: C cgen
|
||||
// handles a match as an expression with a by-value locals copy,
|
||||
// so two separate matches in the same function each allocate
|
||||
// their `v`/`e` slots fresh. Treating these as deduped would
|
||||
// shrink the frame below what local_add then bumps it to.
|
||||
if (n.kind == N_MCASE) {
|
||||
let bn: str = n.str;
|
||||
if (bn.len > 0) {
|
||||
@@ -5926,6 +6018,11 @@ fn cgstmt(c: *cgen, n: *node) void = {
|
||||
return;
|
||||
};
|
||||
cgexpr(c, rhs);
|
||||
} else {
|
||||
// Bare `return;` in a void fn — zero AX so the caller
|
||||
// sees a deterministic value (matches C cgen, which
|
||||
// always falls through to `cgexpr_int(c, 0)`).
|
||||
emit_line("\tMOVQ\t$0, AX\n");
|
||||
};
|
||||
// SysV: 16-byte aggregates (str, 2-tuple) return in (AX, DX).
|
||||
// cgexpr leaves str in (AX, BX); shuffle BX→DX.
|
||||
@@ -6337,6 +6434,9 @@ fn cgfn(c: *cgen, fn_: *node) void = {
|
||||
|
||||
// Pre-scan total frame: 24 bytes per slice param, 16 per str
|
||||
// param, 8 per other param, plus per-let from scan_locals.
|
||||
// Seed c.locals with param-name stubs so scan_locals dedups a
|
||||
// re-declared `let <name>` in the body against the param's
|
||||
// slot (matches C cgen). Stubs get cleared before emission.
|
||||
let scan_p: *node = fn_.list;
|
||||
let frame: i32 = 0;
|
||||
for (scan_p != nil) {
|
||||
@@ -6345,10 +6445,13 @@ fn cgfn(c: *cgen, fn_: *node) void = {
|
||||
else { if (is_slice_type(c, scan_p.lhs)) { frame += 24; }
|
||||
else { if (is_str_type(c, scan_p.lhs)) { frame += 16; }
|
||||
else { frame += 8; }; }; };
|
||||
scan_seen_mark(c, scan_p.str);
|
||||
};
|
||||
scan_p = scan_p.next;
|
||||
};
|
||||
if (fn_.body != nil) { frame += scan_locals(c, fn_.body); };
|
||||
// Drop the stubs so emission rebuilds c.locals with real offsets.
|
||||
c.locals = nil;
|
||||
if ((frame & 15) != 0) {
|
||||
frame = (frame + 15) & ~15;
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ 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;
|
||||
@@ -62,6 +63,14 @@ export fn close(fd: i32) i32 = {
|
||||
return syscall1(SYS_CLOSE, fd: i64): 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
|
||||
// file without changing the cgen emit path.
|
||||
export fn dup2(oldfd: i32, newfd: i32) i32 = {
|
||||
return syscall2(SYS_DUP2, oldfd: i64, newfd: i64): i32;
|
||||
};
|
||||
|
||||
// Fallible wrappers. The error variant is a plain str (Plan 9 errstr
|
||||
// model, see lib/errors); the sum type makes success/failure explicit
|
||||
// without overloading length-zero.
|
||||
|
||||
Reference in New Issue
Block a user