diff --git a/bend2/comp.ts b/bend2/comp.ts index ff0ca30fb..f5feda076 100644 --- a/bend2/comp.ts +++ b/bend2/comp.ts @@ -1304,9 +1304,13 @@ export function io_type(book: Bend.Book): HTerm | null { return xs?.length === 1 ? xs[0] : null; } +function io_status(): boolean { + return ty_adt(io_type(FL.book))?.k === "U32"; +} + export function io_run(book: Bend.Book, args: string[]): number { const src = `${js_lib(book)}\n${RUNTIME_MAIN}\ncli_args = ${ - JSON.stringify(args)};\nreturn io_run(${js_sat("main")});`; + JSON.stringify(args)};\nreturn io_run(${js_sat("main")}, ${io_status()});`; return new Function("require", src)(import.meta.require); } @@ -2883,6 +2887,7 @@ export function compile_book(book: Bend.Book): string { `#define WL_TABLE ${entries.map((s) => `WL_X(${s.fid})`).join(" ") } WL_X(FID_EXIT)`, `#define MAIN_FID ${seg_fid("main")}`, `#define MAIN_PURE ${Number(show !== null)}`, + `#define MAIN_STATUS ${Number(io_status())}`, `#define BLK_SHR ${Number(FL.hot.has("t:Array"))}`); const tabs = [defs.join("\n"), ...[...FL.tabs].map(([r, i]) => `CONSTV u64 TAB_${i}[] = { ${r} };`)].join("\n\n"); @@ -3247,7 +3252,7 @@ export function js_book(book: Bend.Book): string { const show = show_main(); return `${lib}\n${RUNTIME_MAIN}\ncli(process.argv.slice(1));\nio_exit(${ js_sat("main")}, ${JSON.stringify(show && show.map((c) => - typeof c === "string" ? Bend.name_key(c) : c))});`; + typeof c === "string" ? Bend.name_key(c) : c))}, ${io_status()});`; } // RuntimeC @@ -5372,6 +5377,13 @@ typedef Term (*Effect)(Env e, Term* f, IoWork* w); Effect io_eff_rows[sizeof CID_T / sizeof *CID_T]; static u32 io_live; +static int io_code(u32 code) { + if (code > 255) { + err_fail("exit status must be between 0 and 255"); + } + return code; +} + static u64 io_tick(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); @@ -5418,6 +5430,9 @@ static IoWork* io_runs; static IoWork* io_park; static IoWork* io_jobs; +static IoWork* io_root; +static u32 io_result; + static void io_push(IoWork** q, IoWork* a) { IoWork* l = *q != NULL ? *q : a; a->next = l->next; @@ -5440,9 +5455,7 @@ static void io_spawn(Term m) { io_live += 1; } -// io_park stays in deadline order (time 0, none, sorts last; ties keep -// their park order), so io_wait wakes due timers in the order they expire. -// It links both ways, so io_park_cut drops a waiter in O(1). +// Deadline order: time 0 last, stable ties. Doubly linked for O(1) removal. static void io_park_add(IoWork* w) { IoWork* p = io_park; if (p == NULL || p->time - 1 <= w->time - 1) { @@ -5882,6 +5895,10 @@ static void io_step(Env e, IoWork* a) { Term req = corpus_eval(e.mem, term_tsk(FID(Clo~apply), ap)); u32 c = (u32)term_aux(req); if (c == CID(Emit)) { + if (MAIN_STATUS && a == io_root) { + io_result = e.mem[term_peek(e.mem, req)]; + io_root = NULL; + } term_drop(e, req); free(a); io_live -= 1; @@ -5891,8 +5908,9 @@ static void io_step(Env e, IoWork* a) { u32 n = cid_arity(c); spare_free(e, cls_fit(n), ctr_take(e, req, n, fs)); if (c == CID(Halt)) { + int code = io_code(fs[0]); io_errs(e, fs[1]); - exit((int)(u32)fs[0]); + exit(code); } if (io_eff_rows[c] == NULL) { err_fail("an alien request"); @@ -5921,6 +5939,9 @@ OUTLINE void io_loop(u64* H) { return; #endif io_spawn(m); + if (MAIN_STATUS) { + io_root = io_runs; + } u64 look = 0; for (u32 n = 0;; n += 1) { if (io_runs == NULL) { @@ -6012,7 +6033,7 @@ int main(int argc, char** argv) { } io_loop(corpus_setup(why == NULL, thr > 0 ? thr : cpu_count(), mem)); io_sync(); - return 0; + return MAIN_STATUS ? io_code(io_result) : 0; } #endif @@ -6154,21 +6175,23 @@ function show_val(D, d, v, chain) { // Io // == -// Apple arm64 passes variadic fcntl flags on the stack, so io_sys -// binds fcntl there with the flags as the ninth fixed argument. A -// parked effect waits for fd (a write when out) or until at -// (performance.now()), either one undefined when unused; once due, io_wait -// calls more at once, as C calls pack, and resumes k with its value, while -// undefined parks it again. The waits stay in deadline order, as io_park -// does in C. +// Apple arm64 fcntl flags are stack argument 9. Deadline-ordered waits accept +// fd/out or at (performance.now()); more (C: pack) resumes k, undefined re-parks. + +function io_code(code) { + if (code < 0 || code > 255) { + throw "bend: exit status must be between 0 and 255"; + } + return code; +} -function io_exit(main, show) { +function io_exit(main, show, status) { try { if (show !== null) { io_out(1, io_bytes(show_val(show, 0, run_loop(main()), 0) + "\n")); process.exit(0); } - process.exit(io_run(main)); + process.exit(io_run(main, status)); } catch (e) { io_errs(String(e)); process.exit(1); @@ -6351,16 +6374,20 @@ function io_park_on(fd, out, k, more, at) { ws.splice(i + 1, 0, { fd, out, k, more, at }); } -function io_run(m) { +function io_run(m, status) { const io = { runs: [], live: 0, waits: [] }; globalThis.BEND_IO = io; + let code = 0; try { - io_push(run_loop(m()), (x) => ({ $: "Emit", value: x }), true); + io_push(run_loop(m()), (x) => { + if (status) code = x; + return { $: "Emit", value: x }; + }, true); let look = 0; for (let n = 0;; n += 1) { if (io.runs.length === 0) { if (io.live === 0) { - return 0; + return status ? io_code(code) : 0; } if (io.waits.length === 0) { io_errs("bend: deadlock: every computation waits on a channel"); @@ -6384,8 +6411,9 @@ function io_run(m) { break; } if (op.$ === "Halt") { + const halt = io_code(op.code); io_errs(op.message); - return op.code; + return halt; } const run = $0eff[op.$]; if (run === undefined) { diff --git a/guide/GUIDE.md b/guide/GUIDE.md index 87d0162f9..60fc535dd 100644 --- a/guide/GUIDE.md +++ b/guide/GUIDE.md @@ -368,13 +368,13 @@ def main() -> IO(Unit): IO.print(text) ``` -Every bind is annotated, and `x : T = v` binds a pure value in the middle of a -block. A fallible effect answers `Result<&1, &1, U32 & String, A>`: `IO.try` -unwraps it or exits with the error, and `IO.die` exits with your own. `IO.args` -answers the command line, less the runtime's own options (a `--` ends them): -its head is the program as invoked, like C's `argv[0]`. A handle (`File`, -`Socket`, `Window`) is an affine, opaque value, so every effect on one hands it -back beside its result, and no program can forge or reuse one. +Binds need types; `x : T = v` binds a pure value. Fallible effects answer +`Result<&1, &1, U32 & String, A>`; `IO.try` unwraps or exits with the error. +`IO.die` exits with your message. An `IO(U32)` main exits silently with its +result; `IO(Unit)` exits 0. Main/die codes outside 0..255 fail with a diagnostic +and exit 1. `IO.args` includes the program name and omits runtime options before +`--`. Handles are affine and opaque: effects return them beside results, +preventing forging or reuse. `TCP.listen(host, port)` and `UDP.bind(host, port)` bind the IPv4 literal `host`: `"127.0.0.1"` serves this machine only, `"0.0.0.0"` every interface. diff --git a/tests/io/channel_fork_join.bend b/tests/io/channel_fork_join.bend index 69fdf48c5..13e6d7fba 100644 --- a/tests/io/channel_fork_join.bend +++ b/tests/io/channel_fork_join.bend @@ -1,9 +1,7 @@ # io (io_channel_fork_join): bend3's channel round trip on bend's # surface. A spawned producer sends 11 and 22 into a channel with # room for four and closes it; main receives both in order (Some) and -# prints 1122, then forks a pure value and joins it. main's value is -# not the exit status: the process exits 0 once every computation -# has answered. +# prints 1122, then forks and joins a pure value, returning success. import Base law producer: @@ -40,7 +38,7 @@ def main.go(c): z : Unit <- IO.print(U32.show((got(x) * 100 + got(y) : U32))) t : Chan(U32) <- IO.fork(U32, IO.pure(U32, 5)) w : U32 <- IO.join(U32, t) - return w + return 0 law main: IO(U32) diff --git a/tests/io/die_status_invalid.bend b/tests/io/die_status_invalid.bend new file mode 100644 index 000000000..9a673101c --- /dev/null +++ b/tests/io/die_status_invalid.bend @@ -0,0 +1,7 @@ +import Base + +def main() -> IO(Unit): + IO.die(Unit, 256, "x") + +#|bend: exit status must be between 0 and 255 +#|exit 1 diff --git a/tests/io/exit_status.bend b/tests/io/exit_status.bend new file mode 100644 index 000000000..21b710473 --- /dev/null +++ b/tests/io/exit_status.bend @@ -0,0 +1,9 @@ +import Base + +def main() -> IO(U32): + do IO: + IO.print_err("usage error") + return 2 + +#|usage error +#|exit 2 diff --git a/tests/io/exit_status_children.bend b/tests/io/exit_status_children.bend new file mode 100644 index 000000000..db9e4613b --- /dev/null +++ b/tests/io/exit_status_children.bend @@ -0,0 +1,22 @@ +import Base + +def grandchild() -> IO(U32): + do IO: + IO.print("grandchild finished") + return 9 + +def child() -> IO(U32): + do IO: + IO.sleep(1) + IO.spawn(U32, grandchild()) + IO.print("child finished") + return 7 + +def main() -> IO(U32): + do IO: + IO.spawn(U32, child()) + return 2 + +#|child finished +#|grandchild finished +#|exit 2 diff --git a/tests/io/exit_status_invalid.bend b/tests/io/exit_status_invalid.bend new file mode 100644 index 000000000..2b8880caa --- /dev/null +++ b/tests/io/exit_status_invalid.bend @@ -0,0 +1,8 @@ +import Base + +def main() -> IO(U32): + do IO: + return 256 + +#|bend: exit status must be between 0 and 255 +#|exit 1 diff --git a/tests/io/exit_status_limit.bend b/tests/io/exit_status_limit.bend new file mode 100644 index 000000000..d6d84a29b --- /dev/null +++ b/tests/io/exit_status_limit.bend @@ -0,0 +1,7 @@ +import Base + +def main() -> IO(U32): + do IO: + return 255 + +#|exit 255 diff --git a/tests/io/fiber_fork_join.bend b/tests/io/fiber_fork_join.bend index 8b90b547e..0295a076e 100644 --- a/tests/io/fiber_fork_join.bend +++ b/tests/io/fiber_fork_join.bend @@ -12,6 +12,6 @@ def main(): a : Unit <- IO.join(Unit, t) u : Unit <- IO.spawn(Unit, IO.pure(Unit, Unit{})) b : Unit <- IO.print("joined") - return 9 + return 0 #|joined diff --git a/tests/io/print_write.bend b/tests/io/print_write.bend index 5a7c64b8e..79b7beb94 100644 --- a/tests/io/print_write.bend +++ b/tests/io/print_write.bend @@ -1,8 +1,6 @@ # io (io_print_write): print (newline) and write (raw) sequence in # order through the event loop, and U32.show feeds a computed string -# to an effect. Ported from bend3; bend3's result-word exit code was -# bend3 CLI behavior -- bend's loop discards the Emit word, and the -# interp lane pins no exit line. +# to an effect. import Base law main: @@ -13,7 +11,7 @@ def main(): a : Unit <- IO.print("hello") b : Unit <- IO.write("num=") c : Unit <- IO.print(U32.show((40 + 2 : U32))) - return 7 + return 0 #|hello #|num=42