Documentation channel: current development main at compiler checkpoint 1d7e15e. The latest tagged release is v0.0.3; pages identify APIs that are not yet released.

Processes

Release status: exec, exec_shell, the two-argument process, and an earlier pipeline form are available in v0.0.3. The options/token process overloads, current four-argument pipe_exec, P6 lifecycle hardening, and cancellable pipes described here are Unreleased at compiler checkpoint 1d7e15e.

Buffered execution

exec(string program, string[] args) -> exec_result : ExecError
exec(string program, string[] args, json options) -> exec_result : ExecError

exec passes an argv array directly, waits for completion, drains standard output and standard error concurrently, and returns exit_code, stdout, and stderr. It never interprets the program or arguments as shell text, including empty arguments and arguments containing spaces, quotes, or trailing backslashes.

Options are a JSON object:

result := exec("git", ["status", "--short"], {
    "cwd": "/srv/project",
    "env": {"LANG": "C.UTF-8"}
});

On Windows, program, argv, environment, and cwd are converted from UTF-8 to UTF-16 and launched with CreateProcessW. Microsoft argv quoting preserves empty and Unicode arguments. Captured Windows output retains the v0.0.3 compatibility behavior of normalizing CRLF to LF. POSIX uses parent-built argv and environment with posix_spawnp; cwd is atomic on macOS and glibc 2.29 or newer, while unsupported POSIX libcs reject non-empty cwd.

Streaming processes

process(string program, string[] args) -> process : ExecError
process(string program, string[] args, json options) -> process : ExecError
process(string program, string[] args, cancellation_token token) -> process : ExecError
process(string program, string[] args, json options, cancellation_token token) -> process : ExecError

The options and token overloads are Unreleased. Streaming options support only cwd and string-valued env; capture and inherit_stdio are rejected because process always creates the three pipe endpoints in, out, and err.

function run_child(cancellation_token token) -> int : ExecError {
    child := process("worker", ["--stream"], {
        "cwd": ".",
        "env": {"MODE": "batch"}
    }, token);
    child.in.write_line("first job");
    child.close_input();
    child.terminate();
    return child.wait();
}
function main() -> int {
    return 0;
}

Pipe methods

child.in.write(string)          // ExecError
child.in.write_line(string)     // ExecError
child.in.write_bytes(bytes)     // ExecError
child.in.flush()
child.in.close()

child.out.read(int)             // string : ExecError
child.out.read_line()           // string : ExecError
child.out.read_all()            // string : ExecError
child.out.read_bytes(int_64)    // bytes : ExecError
child.out.read_all_bytes()      // bytes : ExecError
child.out.read_all_bytes(limit) // bytes : ExecError
child.out.eof()
child.out.close()

err has the same methods as out. Byte reads return at most the requested count; an empty result denotes EOF only when eof() is true. Byte writes complete fully or raise an error. Pipes are blocking and apply operating-system backpressure: no unbounded user-space queue is inserted. Drain stdout and stderr concurrently when a child may fill both pipes; waiting or draining one stream first can deadlock. One operation may be active on an endpoint at a time, while stdin, stdout, and stderr may progress concurrently.

Cancelling the token bound at spawn wakes blocked reads and writes with ExecError("process I/O cancelled", 125). It does not terminate or wait for the process. Pre-cancellation fails before I/O; observed native completion or EOF wins in the same wake cycle. If cancellation and close or another I/O failure are both pending at interruption, cancellation wins. Closing an endpoint alone wakes its active operation with a distinct closed-pipe error.

Process lifecycle

child.close_input();
child.terminate();  // ExecError
int status := child.wait(); // ExecError
bool live := child.running();
int code := child.exit_code(); // -1 while running

On POSIX, each process owns a new process group. On Windows it owns a kill-on-close Job Object. terminate() requests termination of that group or Job. wait() waits for the direct child, cleans up members still in the owned group or Job, and returns the direct child's exit status; concurrent status checks refresh direct-child state without blocking.

Destruction closes stdin, requests graceful termination, performs bounded graceful and force-wait intervals, then transfers a still-live child to a detached reaper or handle closer. If that final native thread cannot be created, cleanup reaps synchronously rather than leaking a zombie, so only that resource-exhaustion fallback is not latency-bounded.

Containment has platform limits. A POSIX descendant can escape with setsid() or setpgid(). A Windows Job is stronger but can be unavailable under external Job policy. Children must not be reaped independently through native SIGCHLD handling because Strut owns direct-child identity and reaping.

Two-stage pipelines

pipe_exec(string first, string[] first_args,
          string second, string[] second_args) -> exec_result : ExecError

This four-argument form is current-main and differs from the earlier release signature. pipe_exec connects the first stdout to the second stdin without a shell. It concurrently pumps that connection and drains both stderr streams plus the second stdout, so outputs larger than pipe capacity do not deadlock. The result uses the second process exit code and stdout; stderr is the first stderr followed by the second stderr. It constructs exactly two stages and has no cwd, environment, or cancellation options.

Shell opt-in

result := exec_shell("printf '%s\\n' shell-is-explicit");

exec_shell(command) is the explicit shell-evaluation API. Prefer argv-based exec, process, or pipe_exec whenever values are data rather than trusted shell syntax.