Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bend2/base.bend
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ law Chan:
type IO.OP<-R: Type> is Type:
Emit{value: R}
Halt{code: U32, message: String}
Exit{code: U32}

type App<-S: Type> is Type:
App{view: S -> Pair(S, Image), tick: List<Event> -> S -> IO(Maybe<S>)}
Expand Down Expand Up @@ -195,6 +196,9 @@ def Process.run(program: String, args: List<String>, input: String,
def IO.die(-A: Type, code: U32, msg: String) -> IO(A):
R => k => Halt{code, msg}

def IO.exit(code: U32) -> IO(Unit):
R => k => Exit{code}

def IO.pass(-A: Type, r: Result<&1, &1, U32 & String, A>) -> IO(A):
match r:
case Done{value}:
Expand Down
21 changes: 13 additions & 8 deletions bend2/comp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1162,8 +1162,7 @@ function lits_cond(w: string, j: number, n: number): string {
return j >= 32 ? `${w} == ${n}` : `(${w} & ${2 ** j - 1}) == ${n}`;
}

// An IO.OP match keeps its default arm: a request is none of its arms, so
// it fail-stops, and a user's default arm ("_") takes only Emit and Halt.
// An IO.OP default accepts its constructors, never a foreign request.
function mat_ctrs(x: HTerm, adt: Of<"ADT">): [Name, HTerm][] {
const { arms, end } = mat_arms(x);
const io = adt.k === "IO.OP";
Expand All @@ -1174,7 +1173,7 @@ function mat_ctrs(x: HTerm, adt: Of<"ADT">): [Name, HTerm][] {
}

function mat_ops(eq: (k: Name) => string): string {
return ["Emit", "Halt"].map(eq).join(" || ");
return ["Emit", "Halt", "Exit"].map(eq).join(" || ");
}

// Fun
Expand Down Expand Up @@ -5889,9 +5888,11 @@ static void io_step(Env e, IoWork* a) {
}
Term fs[256];
u32 n = cid_arity(c);
spare_free(e, cls_fit(n), ctr_take(e, req, n, fs));
if (c == CID(Halt)) {
io_errs(e, fs[1]);
if (term_tag(req) == TAG_PAK) fs[0] = term_loc(req);
else spare_free(e, cls_fit(n), ctr_take(e, req, n, fs));
if (c == CID(Halt) || c == CID(Exit)) {
if (c == CID(Halt)) io_errs(e, fs[1]);
if ((u32)fs[0] > 255) err_fail("exit code must be in 0..255");
exit((int)(u32)fs[0]);
}
if (io_eff_rows[c] == NULL) {
Expand Down Expand Up @@ -6383,8 +6384,12 @@ function io_run(m) {
io.live -= 1;
break;
}
if (op.$ === "Halt") {
io_errs(op.message);
if (op.$ === "Halt" || op.$ === "Exit") {
if (op.$ === "Halt") io_errs(op.message);
if (op.code > 255) {
io_errs("bend: exit code must be in 0..255");
return 1;
}
return op.code;
}
const run = $0eff[op.$];
Expand Down
10 changes: 6 additions & 4 deletions bend2/docs/BendRT/main.typ
Original file line number Diff line number Diff line change
Expand Up @@ -505,14 +505,16 @@ device), and the harness checks that all three print the same bytes.
= Effects <sec:io>

`IO(A)` is a definition of the base library, not a primitive: a
continuation over one datatype `IO.OP` with two constructors, `Emit`
and `Halt`. A _foreign fill_, a def whose body is `import` lines naming
continuation over one datatype `IO.OP` with three constructors, `Emit`,
`Halt` and `Exit`. A _foreign fill_, a def whose body is `import` lines naming
a `.c` and a `.js` file, compiles to one segment that packs its
arguments and its continuation into a request. The event loop runs on
one thread: it evaluates `main` to a request through the pure machine,
runs the effect's C handler, applies the continuation to the answer,
and evaluates again; `Emit` ends the program and `Halt` exits with its
code. Effects never run inside an evaluator, and the machine performs
and evaluates again; `Emit` ends the program, `Halt` prints its message
and exits with its code, and `Exit` exits with its code silently. A code
outside 0..255 exits with 1 and an error. Effects never run inside an
evaluator, and the machine performs
no IO. A fallible operation answers a `Result` carrying errno, and a
handle threads back outside the `Result`, so even a failure cannot
lose it.
Expand Down
11 changes: 11 additions & 0 deletions tests/io/exit_status.bend
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Base

def Host.exit_status() -> IO(U32):
import "./exit_status.js"

def main() -> IO(Unit):
do IO<Unit>:
status : U32 <- Host.exit_status()
IO.print(U32.show(status))

#|0
49 changes: 49 additions & 0 deletions tests/io/exit_status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
function exit_status() {
const fs = require("node:fs");
const path = require("node:path");
const child = require("node:child_process");
const dir = fs.mkdtempSync(path.join(process.cwd(), ".bend-exit-status-"));
try {
const bun = typeof Bun !== "undefined";
const command = bun ? process.execPath : "bun";
const prefix = bun && Bun.main.startsWith("/$bunfs/")
? [] : [path.resolve("bend2/main.ts")];
const cases = [
["IO.exit(0)", 0, ""],
["IO.exit(255)", 255, ""],
["IO.exit(256)", 1, null],
["IO.exit(4294967295)", 1, null],
['IO.die(Unit, 7, "")', 7, "\n"],
['IO.die(Unit, 256, "reported")', 1, null, "reported\n"],
];
for (let i = 0; i < cases.length; i += 1) {
const [expression, status, stderr, start] = cases[i];
const file = path.join(dir, "main.bend");
fs.writeFileSync(file, `import Base
def main() -> IO(Unit):
do IO<Unit>:
IO.print("before")
${expression}
IO.print("after")
`);
const ran = child.spawnSync(command, [...prefix, file], {
encoding: "utf8",
env: { ...process.env, HOME: dir, BEND_ORIGIN: "http://127.0.0.1:9" },
});
if (ran.error) {
throw ran.error;
}
if (ran.status !== status || ran.stdout !== "before\n"
|| (stderr !== null && ran.stderr !== stderr)
|| (start !== undefined && !ran.stderr.startsWith(start))) {
return i + 1;
}
}
return 0;
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}

io_eff(CID(Host.exit_status), exit_status);
15 changes: 15 additions & 0 deletions tests/io/quiet_exit.bend
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Base

def classify(op: IO.OP<U32>) -> U32:
match op:
case Emit{v}: v
case _: 91

def main() -> IO(Unit):
do IO<Unit>:
IO.print(U32.show(classify(Exit{7})))
IO.exit(23)
IO.print("unreachable")

#|91
#|exit 23
3 changes: 3 additions & 0 deletions tests/io/request_out_of_band.bend
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ def intercept.at(op):
do IO<U32>:
u : Unit <- IO.print(m)
return c
case Exit{c}:
IO.pure(U32, c)

law intercept:
IO(U32)
Expand All @@ -49,6 +51,7 @@ def decode(op):
match op:
case Emit{v}: U32.show(v)
case Halt{c, m}: m
case Exit{c}: U32.show(c)

law main:
IO(U32)
Expand Down
2 changes: 1 addition & 1 deletion tests/io/request_wildcard.bend
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# io (io_request_wildcard): an IO.OP match's own default arm takes only
# what an IO.OP is (Emit, Halt), never a request. decode's case _ takes a
# what an IO.OP is (Emit, Halt, Exit), never a request. decode's case _ takes a
# purely built Halt and prints "other"; intercept's case _ meets the
# request IO.print("SECRET") applies to its continuation, and every lane
# refuses it instead of running the user's arm on it (which printed
Expand Down