diff --git a/PLAN.md b/PLAN.md index 3f961a8a..eac9303a 100644 --- a/PLAN.md +++ b/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/`, diff --git a/selfhost/cmd/6a/main.combined.ww b/selfhost/cmd/6a/main.combined.ww index 9955400c..c0e78b4d 100644 --- a/selfhost/cmd/6a/main.combined.ww +++ b/selfhost/cmd/6a/main.combined.ww @@ -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 . 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 (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 diff --git a/selfhost/cmd/6l/main.combined.ww b/selfhost/cmd/6l/main.combined.ww index fbc77b4f..49100009 100644 --- a/selfhost/cmd/6l/main.combined.ww +++ b/selfhost/cmd/6l/main.combined.ww @@ -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 . 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 (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 diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww index 53679a3c..54af9b2a 100644 --- a/selfhost/cmd/ww/main.combined.ww +++ b/selfhost/cmd/ww/main.combined.ww @@ -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: /../../lib let dotdot_lib: *u8 = os.alloc(PATH_MAX): *u8; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 16de55f9..2c6fb8b3 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -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: /../../lib let dotdot_lib: *u8 = os.alloc(PATH_MAX): *u8; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index c13fdfe6..86775984 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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 ` 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; }; diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index fe2a82a6..52ff859f 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -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.