Programming Language

Nocter

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

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

v0.31.0 Phase 1 Raw Descriptor and Readiness Review

Result: passed (2026-09-03). Phase 1 implements and natively qualifies the private Darwin descriptor boundary. Output and Command.output remain future contracts and are not exposed by the standard library.

Reviewed Boundary

The review covers close-on-exec pipe creation, descriptor normalization and destruction, descriptor installation, staged launch reports, fork and wait separation, two-record poll layout, readiness decoding, bounded reads, interruption representation, source responsibility placement, and regression of the published Command.status path.

Implemented Structure

Each physical source now owns one replaceable responsibility:

  • pipe_darwin.nct owns OwnedPipe, normalized close-on-exec creation, idempotent endpoint close, destruction, and exact descriptor installation;
  • launch_report_darwin.nct owns the fixed stage-plus-errno payload, close-on-exec success, child exec transition, report writes, and complete report reads;
  • capture_darwin.nct owns the Darwin pollfd representation, two-stream readiness, supported event vocabulary, inactive descriptor representation, and one bounded read;
  • subprocess_darwin.nct owns only fork classification, interrupted-wait retry, and terminal-status decoding;
  • command_darwin.nct consumes those typed facts and no longer receives launch-pipe descriptors as unrelated integers.

The compiler remains unchanged. The internal Darwin module adds only ordinary source constants for dup2, poll, F_GETFD, and F_DUPFD_CLOEXEC; existing generic syscall2 and syscall3 roles carry the operations through the already-qualified native pipeline.

Findings Closed

A pipe was not previously an owning value

The v0.30.0 launch path passed read and write descriptors independently across the policy boundary. Every branch happened to close them, but the representation could not enforce cleanup after a later early return. OwnedPipe now retains both endpoint states and its drop body closes only open ends. Explicit close is idempotent and changes the owner's state before another operation can return. A fork-failure branch drops the owner rather than reproducing two raw close calls.

Descriptor normalization must precede fork

Every raw pipe endpoint is duplicated with F_DUPFD_CLOEXEC and a lower bound of descriptor 3, then the original pair is closed. This single transition handles both collision avoidance and the close-on-exec flag. If either duplication fails, every already-created descriptor is closed before the typed failure is returned. Native tests query both flags and verify that explicit close plus owner destruction leaves no live descriptor.

Launch failure had only one implicit stage

The former four-byte exec errno cannot represent a failure while installing captured standard descriptors. The report authority now owns an eight-byte { stage, errno } record. Only setup and exec stage values with nonzero errno are accepted. Empty EOF means executed; partial payload, unknown stage, zero errno, oversized read, and channel failure remain distinct typed outcomes. Command.status maps the expanded private facts to its unchanged v0.30.0 errors.

Readiness needed a closed semantic result

No future buffer owner reads raw pollfd memory or syscall counts. capture_darwin.nct verifies the two-record result count, rejects events outside POLLIN | POLLERR | POLLHUP | POLLNVAL, and returns two DescriptorEvents values. Inactive streams are represented to Darwin by descriptor -1 and must return no events. The pointer-sized descriptor is range-checked before its low 32-bit target representation is projected; this representation operation remains target-local and cannot leak into process policy.

Read attempts similarly become exactly bytes, EOF, interruption, target failure, or malformed result. A zero-capacity buffer is rejected rather than being confused with EOF. Phase 2 can therefore implement fairness and buffer ownership without knowing errno or raw count validity.

Native Evidence

The process test target executes nine authored cases. New cases prove:

  • both endpoints are at least descriptor 3, distinct, and close-on-exec;
  • endpoint close is idempotent and owner drop closes the remaining endpoint;
  • dup2 makes the exact requested descriptor share the source pipe and reports a closed source as failure;
  • the authored PollDescriptor layout is accepted by Darwin's real poll syscall;
  • one readable stream does not fabricate readiness for the other;
  • a closed writer reports hangup, and the subsequent read is the authority for EOF;
  • an inactive first record is ignored while the second remains serviceable;
  • reading a closed descriptor retains its nonzero target errno;
  • synthetic syscall facts deterministically cover ready, interrupted, failed, malformed, byte, and EOF classification without requiring a signal race.

The existing complete native Command.status success, nonzero exit, signal, exec-rejection, argument, descriptor-stress, and exact-child lifecycle tests also pass after migration to OwnedPipe and the staged report.

Authority Review

No raw pipe descriptor now crosses into public policy as an independent resource. Pipe lifetime, launch classification, capture readiness, and terminal observation each have one source owner. None reopens parser, checker, semantic, MIR, Machine, or ABI data. The compiler has no subprocess-specific primitive and cannot interpret a poll event or launch stage.

Historical v0.30.0 reviews retain the type names that described that frozen release; active source contains no LaunchPipeAttempt, ExecReportAttempt, create_launch_pipe, read_exec_report, or F_SETFD compatibility path. No current contract requires an upper layer to remember two close calls, validate a raw syscall count, infer a launch stage, or know a target record offset.

Remaining Work

Phase 2 must add the public owning result and compose three pipes into one parent lifecycle. It must close capture writers in the parent, close capture readers in the child, install both child descriptors, drain bounded chunks from every ready stream, and close both parent readers before exact-child observation on capture failure. Those are policy and ownership tasks; no missing raw ABI operation remains.

No unresolved correctness or responsibility-boundary finding remains in Phase 1 scope.