v0.30.0 Phase 0 Synchronous-subprocess Design Review
Result: passed (2026-09-03). Phase 0 defines the public and architectural boundary only. It changes no compiler or standard-library implementation and does not claim that Command is available in v0.29.0.
Reviewed Boundary
The review covers the future public declarations, exact-path behavior, argument ownership, inherited process state, synchronous lifecycle, launch-failure reporting, terminal-status model, allocation boundary, target feasibility, and the division between compiler context, raw target facts, and standard-library policy.
The accepted API contains only Command.new, Command.arg, Command.status, and the three allocation-free ExitStatus observations. It does not expose a child resource whose destruction would depend on undocumented caller behavior.
Findings Closed by the Design
Exec failure cannot use a reserved exit code
A child program may intentionally return every representable exit code. Inferring exec failure from 126, 127, or another convention would therefore repeat a decision without evidence. The accepted close-on-exec channel reports raw exec failure before the parent constructs the public result; successful exec closes the channel and leaves later exit status unambiguous.
Environment inheritance cannot use public UTF-8 queries
The current public environment functions validate UTF-8 and separate names from values. Rebuilding the child environment from them would reject or alter inherited bytes and would make subprocess correctness depend on a user-facing presentation API. The backend already stores the original environment-vector address in immutable process context. One private closed primitive may expose that existing fact to trusted standard source without giving the compiler knowledge of Command.
Child cleanup cannot depend on callers
An owning asynchronous child would require a defined wait, terminate, detach, and destruction policy. None is needed for the first practical use case. status consumes the command and owns both creation and reap, making an unreaped child a library defect rather than a permitted caller state.
Forked execution cannot re-enter ordinary Nocter runtime paths
Allocator state, destructors, and buffered user state are duplicated by fork but are not safe authorities for child-side failure handling. The accepted design prepares storage before creation and restricts the child branch to descriptor close, exec, raw errno write, and immediate raw exit. This invariant belongs in a target adapter contract and its tests, not in comments at each call site.
Feasibility Evidence
- the installed Darwin SDK defines direct syscall numbers for
fork,wait4,pipe,execve, andfcntl; - Apple XNU source confirms that
forkandpipeboth use two successful return words; - existing generic syscall roles cover zero through six arguments but their canonical
{ value, errno }result intentionally discards the second success register, so Phase 1 must add one narrow zero-argument pair-result role rather than pretending the current boundary is enough; - the ARM64 process-context layout already stores argument count, argument vector, environment vector, and environment count once at entry;
- existing raw process exit and descriptor read/write/close capabilities cover child failure and parent observation without a new public runtime;
- current Nocter structs, constructors, owning receivers,
String,Vec, optional values, and built-in errors can express the public API without new syntax or semantic operations.
Phase 1 must verify the pair-result materialization, exact syscall argument layouts, child marker, pipe descriptor order, and wait encoding in executable tests before standard policy consumes them. SDK constant presence and source inspection are feasibility evidence, not sufficient ABI qualification by themselves. The current Nocter runtime has no threads, dynamic runtime library, or registered at-fork handlers; adding any of those later would require revisiting the raw-fork boundary rather than silently assuming today's child-side rules still suffice.
Authority Review
The design has one owner for each decision:
- compiler process context owns the original environment-vector address;
- the internal OS boundary owns the one-word and pair-result syscall ABI facts;
- target-specific standard source owns process syscall selection and wait-layout facts;
- one launch-report transition owns exec success versus failure;
std/processowns validation, public errors, wait retry, and status semantics;- the consumed
Command.statusoperation owns complete child lifecycle.
No later layer reopens an earlier semantic product. No source projection participates. The compiler does not inspect standard source names beyond a closed primitive locator and cannot interpret Command storage. No accepted contract relies on a caller remembering to wait, close a hidden descriptor, preserve borrowed argument storage, or avoid a magic exit code.
Validation
Phase 0 documentation generation is deterministic, all documentation links resolve, Rust source formatting remains unchanged, repository whitespace checks pass, and the working tree began clean at the published v0.29.0 audit commit.
No unresolved correctness or responsibility-boundary finding remains in Phase 0 scope. Process context and raw target implementation remain explicitly pending Phase 1.