Programming Language

Nocter

A self-contained systems language built around simplicity, encapsulation, and foolproof design.

/development/reviews/v0.31.0-phase-0.md

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 dup2 and 230 for poll;
  • the SDK declares an eight-byte pollfd record and the POLLIN, POLLERR, POLLHUP, and POLLNVAL event bits;
  • existing standard source already exposes generic syscall2 and syscall3 roles 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, and String.from_utf8 already 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:

  • Output owns 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/process owns 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.