v0.31.0 Phase 0 Captured-output Design Review
Result: passed (2026-09-03). Phase 0 fixes the public and architectural contract only. It does not make Command.output available in published v0.30.0 artifacts and changes no compiler or standard-library implementation.
Reviewed Boundary
The review covers the owning public result, arbitrary-byte representation, two-stream progress, EOF semantics, descriptor normalization, child setup reporting, failure precedence, allocation, blocking, exact-child observation, target feasibility, and ownership division.
The accepted addition is only Output { status, stdout, stderr } and the consuming Command.output(). It does not expose a live child or pipe whose cleanup depends on callers.
Findings Closed by the Design
Sequential reads can deadlock
Calling read_to_end for stdout and then stderr is incorrect even though each helper is correct in isolation. A child can fill stderr while the parent blocks waiting for stdout EOF, leaving both processes unable to progress. The accepted transition observes both descriptors with poll, reads only a bounded chunk from each ready stream, and repeats until both reads report EOF.
Hangup is not equivalent to drained EOF
POLLHUP can be returned together with readable buffered bytes. Closing immediately on hangup would lose output. Readiness owns only permission to attempt a read; the read result is the sole authority for stream EOF.
Standard descriptors cannot be assumed open
If descriptor 1 or 2 is closed before pipe creation, a new pipe may reuse that number. A naive sequence of dup2 calls can then overwrite a still-needed endpoint. Every internal pipe descriptor is normalized to 3 or greater before fork, and the pipe owner closes replaced originals. Child redirection can consequently use a fixed order without hidden host preconditions.
Setup failure needs a distinct launch fact
The v0.30.0 report contains only exec errno. Captured launch can fail earlier while installing stdout or stderr. Guessing the stage from errno or child exit status would repeat a decision without evidence. The report becomes one fixed stage-plus-errno payload; clean close-on-exec EOF remains the sole successful-exec fact.
Capture failure must unblock and observe the child
Returning immediately from a poll, read, or buffer-transition error could leave the child blocked on a full pipe and later unreaped. The accepted parent transition closes both captured read ends first, then enters the same exact-child terminal observation used by normal completion. No representable capture or report failure bypasses that transition. A permanent kernel wait failure remains explicitly reportable rather than being disguised as successful cleanup.
Captured output is not text
Decoding during capture would reject valid process output and couple process lifecycle to string policy. Vec<u8> is the sole captured representation. Existing explicit String.from_utf8 owns the optional text interpretation.
Feasibility Evidence
- the installed Darwin SDK declares direct syscall numbers 90 for
dup2and 230 forpoll; - the SDK declares an eight-byte
pollfdrecord and thePOLLIN,POLLERR,POLLHUP, andPOLLNVALevent bits; - existing standard source already exposes generic
syscall2andsyscall3roles through the package-private target boundary; - existing ARM64 lowering and materialization qualify those generic syscall roles, so capture adds target constants and standard-source transitions rather than a subprocess-specific compiler primitive;
Vec<u8>, owned aggregates, consuming methods, built-in errors, andString.from_utf8already express the public ownership and explicit-text contracts;- the v0.30.0 launch report, exact-child wait, environment inheritance, and child raw-exit boundary remain the authorities to extend rather than duplicate.
The SDK proves target layout availability, not implementation correctness. Phase 1 must validate record offsets, signed timeout representation, result and errno interpretation, interruption, descriptor normalization, staged report reads, and close transitions in executable tests.
Authority Review
The design leaves one owner for every decision:
Outputowns completed public data and no live process resource;- one private pipe abstraction owns close-on-exec creation, descriptor normalization, and endpoint close transitions;
- target-specific standard source owns
dup2,pollfd, event bits, and syscall selection; - one launch report owns setup-versus-exec failure classification;
- one parent lifecycle transition owns simultaneous draining and exact-child observation;
std/processowns public failure precedence and buffer construction;- the compiler owns only already-existing generic syscall lowering.
No compiler layer interprets process output or knows Output. No public source must choose a drain order, close a descriptor, wait for a child, decode errno, or avoid a descriptor-number collision. No target-specific record crosses into the target-independent command representation.
Validation
Phase 0 documentation generation is deterministic, public links resolve, source formatting remains unchanged, and repository whitespace checks pass. No unresolved correctness or responsibility- boundary finding remains in Phase 0 scope. Raw descriptor and readiness implementation remain explicitly pending Phase 1.