Programming Language

Nocter

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

/development/milestones/v0.30.0.md

v0.30.0: Synchronous Subprocess Execution

Status: complete, published, and externally audited (2026-09-03). v0.30.0 targets the smallest subprocess boundary that is useful to ordinary command-line applications and has a closed ownership model: an owning Command launches one exact executable path with owned arguments, inherits process state, waits, and returns a typed terminal status. It does not introduce a shell, PATH search, output capture, or an independently discardable child handle.

Completion Boundary

  • Command.new validates and owns an exact executable-path spelling;
  • Command.arg validates and owns arguments in insertion order without partially applying an invalid input;
  • Command.status creates exactly one child, passes exact argument bytes, inherits environment, working directory, and standard streams, and reaps that child;
  • ExitStatus distinguishes ordinary exit from signal termination and defines success as exit code zero;
  • exec failure is reported to the parent independently of any child exit status;
  • inherited environment bytes cross process creation without public UTF-8 decoding or reconstruction;
  • no child-side path returns to Nocter allocation, destruction, or user code after fork;
  • native, editor, formatter, authored-standard, installed-home, and complete-workspace gates cover the same public source contracts;
  • final review finds no duplicate launch authority, target-layout leakage, unreaped-child path, caller-discipline protocol, or obsolete compatibility surface.

Accepted Design

The public contract is owned normatively by spec/30-subprocesses.md. It remained explicitly marked as future behavior until implementation and qualification closed in Phase 5.

The first boundary is synchronous by design. Publishing Child before pipes, nonblocking wait, and destruction behavior are specified would create a resource whose safe default disposal is unclear: blocking drop is surprising, non-reaping drop leaks a zombie, and implicit termination is destructive. Consuming Command into status makes successful child creation and eventual reap one operation.

The executable parameter is an exact path. Silent PATH lookup would add environment parsing, empty-component behavior, executable probing, and error precedence to this milestone. Shell execution would additionally introduce quoting and injection semantics. Both remain separate future APIs.

Responsibility Boundaries

std/process owns source-visible command state, validation, owned C-compatible launch values, public failure selection, wait retry, and ExitStatus. It must prepare every allocation and every pointer vector before creating a child.

Target-specific standard source owns syscall numbers, wait-status bits, descriptor flags, and the raw launch-report protocol. Generic syscall primitives continue to expose { value, errno } facts; they do not gain process policy. The close-on-exec report channel is the sole authority for distinguishing an exec failure from a successfully executed program that returns a conventional failure code.

The compiler already retains the original environment-vector address in its immutable process context. Phase 1 adds one closed primitive role that reads this existing context word for trusted standard-library use. Darwin fork and pipe each return two successful words, while the existing ordinary syscall boundary intentionally preserves only one. A separate zero-argument pair-result syscall role will retain that target ABI fact without expanding every ordinary SyscallResult or creating a subprocess-specific compiler operation. The compiler does not decode environment entries, build command state, select public errors, or interpret wait bits.

Phase 0: Contract and Feasibility

Status: complete. The public future-direction chapter fixes exact-path execution, owned arguments, inherited state, synchronous waiting, exit/signal representation, stable failures, close-on-exec reporting, allocation scope, and non-goals.

Feasibility inspection confirms that the Darwin ARM64 SDK exposes direct fork, wait4, pipe, execve, and fcntl syscall numbers. Apple XNU source additionally confirms that both fork and pipe publish two successful return words. The existing generic syscall roles can carry wait4, execve, and fcntl, but cannot carry fork or pipe because their canonical result deliberately discards the second success register. Phase 1 therefore adds one zero-argument pair-result syscall role and one read of the environment-vector word already stored in process context. Subprocess types and policy remain ordinary standard source.

The child-side rule is stricter than ordinary source execution. All owned NUL-terminated strings, the null-terminated argument-pointer vector, descriptor channel, and close-on-exec flag are prepared before fork. The child may then close one channel end, call execve, write the raw errno on failure, and terminate through the existing raw exit boundary. It cannot allocate, drop command storage, format an error, or unwind through ordinary Nocter control flow.

The parent closes its child-only channel end, reads the launch result, retries interrupted wait, and always reaps the created pid before returning. The report channel closes automatically on a successful exec, so exit code 127 or any other program result remains an ordinary ExitStatus.

The Phase 0 review found no language-syntax, semantic-identity, MIR, or native-image-format change required for the public types. Phase 1 does require two closed primitive additions and ARM64 selection/materialization for their exact results. The review also rejects four shortcuts: discarding Darwin's second success register, reconstructing the environment through UTF-8 views, treating a magic exit code as exec failure, and exposing Child without a closed disposal contract.

Phase 1: Process Context and Raw Target Boundary

Status: complete. The closed primitive registry now contains one source-private inherited- environment-vector role and one package-visible zero-argument pair-result syscall role. Runtime contracts, the bundled standard profile, target validation, Machine process-context propagation, ARM64 selection, and ARM64 materialization carry those exact facts without adding a public API or changing ordinary syscall results.

The pair-result materializer preserves Darwin success registers x0 and x1, writes zero errno, and on carry-set failure clears both result words while moving the kernel error to the third word. A native pipe qualification was authored to send one byte through the returned write descriptor and receive it from the returned read descriptor. Phase 2's first reachable public command build showed that Phase 1 had not actually selected that test into an executable and had modeled its three-word result with the wrong direct ABI. Phase 2 closes that qualification gap with the ordinary indirect aggregate ABI and executes both the pair test and the private nonzero environment-vector test through complete native sessions.

Target-gated std/process source is now the sole owner of fork parent/child classification, close-on-exec launch-channel setup, exact four-byte exec-report reads including interruption and partial-read handling, and terminal wait-status decoding. Deterministic transition tests cover parent, child, fork failure, ordinary exit, signal termination, interrupted wait, wait failure, invalid wait status, empty success reports, and exact errno reports. Operations that would be unused until owning Command exists were deliberately not added; Phase 2 introduces fork, exec, child reporting, and wait calls together with their only consumer.

The Phase 1 review finds no open finding or duplicate ABI authority. Command and ExitStatus remain unpublished future contracts.

Phase 2: Owning Command and Status

Status: complete. std/process/index.nct now exposes bodyless Command and ExitStatus contracts. Private command source owns validated NUL-terminated path and argument buffers, builds the complete null-terminated pointer vector before fork, and retains both buffers and vector in one prepared owner until the parent has observed and reaped the child. Argument validation completes before mutation, so a rejected argument cannot partially change command state.

The Darwin orchestration consumes the Phase 1 typed transitions. The child branch receives only stable addresses and descriptors, closes its parent-only descriptor, calls execve, reports an exact errno payload on rejection, and terminates through raw exit. The parent closes its child-only descriptor, reads the close-on-exec report, retries interrupted waits behind a result type that can no longer expose interruption, reaps before selecting any public result, and maps only completed wait outcomes into ExitStatus or stable errors.

First complete execution exposed that the three-word SyscallPairResult follows the ordinary ARM64 aggregate ABI and is returned indirectly, while the Phase 1 selector incorrectly required a three-register direct result. Selection now validates the canonical 24-byte indirect layout and materializes both successful Darwin result words plus errno into caller-owned result storage. Native standard tests execute this path, so the earlier unexecuted assumption is no longer part of the design.

The Phase 2 review records the ownership, lifecycle, ABI, and qualification audit with no open finding.

Phase 3: Native Failure and Lifecycle Qualification

Status: complete. One public-contract executable now crosses complete native sessions for successful exit, nonzero exits including ordinary code 127, signal termination, missing and denied paths, an invalid executable image, relative-path resolution, exact empty and whitespace-bearing arguments, embedded-NUL rejection, and Darwin's actual oversized-argument rejection. Ninety-six consecutive exec failures followed by a successful launch exercise descriptor cleanup and child reaping through the real public operation.

Native standard tests drive the close-on-exec channel through clean EOF, exact errno, partial payload EOF, and read failure. Interrupted wait remains deterministically qualified at the raw transition boundary: EINTR can only produce the retry-only WaitAttempt.interrupted variant, while the narrower result returned to policy cannot represent interruption. No signal-handler or thread API was added merely to manufacture that operating-system event.

The Phase 3 review traces every result and resource path and finds no second launch authority, leaked descriptor, magic exit-code interpretation, unreaped normal or reported-failure child, target policy in the compiler, or caller-discipline requirement.

Phase 4: Tooling and Practical Integration

Status: complete. The public subprocess-status package invokes its repository-owned executable helper through the exact relative path ./helper.sh, preserves a whitespace-bearing argument, and reports the helper's typed nonzero exit status. The helper is an execution precondition rather than a second subprocess implementation: the example itself uses only the published Command and ExitStatus contracts.

The shared public-example catalog now represents executable fixtures explicitly and remains the single execution contract used by native compilation, command execution, and installed-home observation. Formatter qualification discovers the same authored .nct files from examples/, while the language-server test opens status.nct itself and verifies hover, completion, declaration navigation, and implementation navigation. No editor-only or formatter-only source copy was introduced.

The Phase 4 review finds no duplicate example behavior, packaged- home shortcut, or tooling-specific semantic surface. Phase 5 is the final repository-wide qualification and complete subprocess review.

Phase 5: Qualification and Review

Status: complete. Complete locked workspace tests, warnings-denied Clippy, no-default-features checking, standard formatting, deterministic documentation generation, native, installation, packaging-syntax, and repository gates pass. The normative subprocess chapter and release index now describe the published v0.30.0 artifact.

The Phase 5 review traces the complete subprocess path and finds no duplicate launch or ABI authority, child-side return to ordinary Nocter execution, descriptor or child leak on a representable path, wait race, source-visible compiler policy, obsolete compatibility surface, or caller-discipline requirement. Release identity, reproducible archive generation, extracted-home qualification, publication, and public re-download evidence are owned by the release-preparation and immutable release records.

Non-goals

  • shell parsing or execution;
  • PATH search;
  • environment mutation or removal;
  • child working-directory overrides;
  • standard-stream redirection, pipes, or output capture;
  • an asynchronous or pollable Child handle;
  • parent-sent signals, process groups, terminal ownership, or job control;
  • threads, async tasks, or another target.