Programming Language

Nocter

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

/development/milestones/v0.29.0.md

v0.29.0: Standard Input and Run Invocation

Status: complete, published, and externally audited (2026-09-03). v0.29.0 closes the remaining invocation boundary for ordinary command-line applications: nocter run forwards exact child arguments after one explicit separator, and std/io exposes inherited standard input as a non-owning byte reader. It reuses the existing process-entry, file-descriptor, Reader, and BufReader authorities without adding language syntax, semantic roles, MIR operations, ABI lanes, or target syscalls.

Completion Boundary

  • nocter run INPUT -- ARG... parses compiler input only before -- and forwards every later OS argument unchanged to the launched executable;
  • forwarded values begin at std/process.arg(1), while argument zero remains the launcher's generated-executable spelling;
  • package and single-file working-directory rules, inherited standard streams, launch failures, child exit status, and temporary-executable cleanup remain unchanged;
  • io.stdin() returns a noalloc, borrowed File wrapper around standard-input descriptor zero;
  • closing or dropping one standard-stream wrapper affects only that wrapper and never closes the process-global descriptor;
  • byte and line input use the existing Reader and BufReader contracts, with no hidden global buffer or stateless line reader;
  • CLI help, diagnostics, native execution, formatter, hover, completion, navigation, public examples, complete workspace gates, and installed-home qualification cover the new surface.

Responsibility Boundaries

The command argument parser owns exactly one run-invocation partition. It produces compiler arguments and opaque child arguments once; generic option parsing receives only the compiler side and cannot reinterpret the child side. The closed run plan carries the child vector beside its existing input selection, executable selector, and working directory. Only the launch adapter opens that vector and passes it to the operating system. Discovery, checking, native compilation, and package resolution never inspect child arguments.

std/io owns public standard-stream wrappers and its focused input/output implementations select the process descriptors. The target OS module continues to own raw syscall facts, while the existing File read authority owns interruption retry, initialized-byte bounds, and stable error mapping. stdin performs no read and introduces no second read algorithm. Existing borrowed File state already owns the rule that local close does not close a process-global descriptor.

BufReader remains the explicit owner of unread bytes and line assembly. A free io.read_line would either discard read-ahead bytes or hide mutable process-global buffering, so it is outside the accepted design. Applications that need lines construct BufReader.new(io.stdin()) and make the state visible in their own ownership graph.

The run argument channel is not a subprocess API. A later subprocess design may reuse its opaque OS-argument transport and standard-stream ownership rules, but this milestone does not speculate about command builders, environment mutation, pipes, waiting, or signals.

Phase 0: Contract and Feasibility

Status: complete. The command-line specification fixes the run separator, exact forwarding, source names beginning with -, argument-zero rule, working directory, inherited streams, launch failure, and child-status behavior. The practical-standard-library specification fixes borrowed stdin ownership, noalloc construction, read behavior, explicit buffering, and the absence of a hidden line-input singleton.

Feasibility inspection found one existing parser mismatch that Phase 1 must replace rather than extend: the shared option loop currently treats -- only as an end-of-options marker and then continues assigning positional source input. Run parsing must partition at the first standalone separator before invoking that shared loop. Other commands retain their current interpretation; no second option grammar or fallback parse is permitted.

ParsedRunCommand and RunCommandPlan already form the typed path from source invocation to the launch adapter. Adding one opaque Vec<OsString> field to that path is sufficient. The compiler's analysis and native-image requests remain argument-free because child values are runtime launch inputs, not program semantics. Rust's process launcher already inherits standard streams and accepts native OS strings, so no lossy conversion is required.

The standard library already has FileState.borrowed_open, shared descriptor read policy, and BufReader construction from File. Phase 2 needs one standard-input descriptor fact and one ordinary borrowed File constructor in a focused input source file. It needs no primitive, target syscall, compiler role, or new allocation contract.

Phase 1: Exact Run Argument Channel

Status: complete. run_invocation now owns the only separator partition and produces a typed RunProgramArguments value. The generic option parser receives only its compiler side. The opaque native-string value then moves through ParsedRunCommand and RunCommandPlan without entering resolution, discovery, checking, or native compilation, and the launch adapter consumes it when it configures the child process.

The command schema, rather than a help-renderer command special case, declares the trailing [-- <ARG>...] channel. Parser tests fix the changed run -- --script.nct meaning, the explicit --file ./--script.nct spelling, and unchanged end-of-options behavior for other commands. Native execution tests prove exact option-like and space-containing values at std/process.arg(1..) and prove that a non-Unicode OS argument crosses parsing and launch without compiler decoding.

Qualification passes all 55 nocter-command library tests, the complete workspace test suite, and warning-denying workspace Clippy for all targets. No compatibility fallback, second parse, string conversion, semantic-product field, or new compiler-stage dependency was introduced. The opaque value has no production read accessor or value-revealing debug representation; only the launcher can consume its stored arguments.

Phase 2: Borrowed Standard Input

Status: complete. std/io/input.nct now contains the sole standard-input constructor. It creates the existing borrowed File state and performs no read, allocation, or buffering. The Darwin target module owns all three standard-descriptor values, replacing the former output-local constants with one target-fact authority.

File reads still cross one syscall loop. That loop delegates every syscall result to a pure classification step whose retry, completion, count validation, and stable-error outcomes are covered by deterministic standard-library tests. A complete native session additionally pipes an exact byte sequence containing invalid UTF-8, reads it to EOF through the public Reader contract, and proves that explicit close and destruction make only their local wrappers terminal. Subsequent wrappers retain access to the inherited descriptor. No input primitive, parallel read algorithm, hidden buffer, semantic role, MIR operation, or ABI lane was introduced.

Phase 3: Buffered Input Integration

Status: complete. The new stdin-prefix public package combines one exact process argument, BufReader.new(io.stdin()), reusable line storage, and ordinary Writer output. Its shared execution contract supplies exact stdin bytes beside arguments, status, stdout, and stderr, so later native, CLI, installed-home, and editor qualification can consume one scenario authority. The successful scenario covers empty lines, LF, CRLF, retained lone CR, a multi-byte scalar, and an unterminated final line. A separate scenario proves stable invalid-UTF-8 failure after an already emitted valid line.

A complete native session also uses a three-byte BufReader capacity over a real pipe. It forces multiple descriptor reads and a UTF-8 scalar across buffer boundaries, then proves clean EOF, destination reuse, retained capacity, and repeated terminal queries. Existing file-backed tests continue to own the common BufReader failure-state details; stdin integration adds no second line decoder, buffering state machine, hidden global state, or input-specific compiler path.

Phase 4: Tooling and Distribution Integration

Status: complete. The recursive public-example formatter gate consumes both authored stdin-prefix sources without a formatter-specific fixture. The language-server reference suite opens the same implementation source and verifies io.stdin hover, navigation to its public contract, buffered-reader method presentation, and standard-stream completion through one normal semantic generation.

The public-example execution model now treats stdin bytes as part of each exact process contract. The ordinary sample input and expected output are repository-owned files shared by native command tests, user documentation, and installed qualification. qualify-local-release.sh runs the extracted compiler's nocter run with the sample piped to stdin and the prefix passed after --, then compares both output streams exactly. A fresh temporary archive and extracted home passed that path without NOCTER_HOME; the temporary image was removed and no repository distribution artifact was changed. No editor-only source copy, formatter fixture, second run parser, or installed-input implementation was introduced.

Phase 5: Qualification and Review

Status: complete. The Phase 5 review found one run separator authority, opaque native child-argument transport, one descriptor-read policy, explicit per-reader buffering, and closed borrowed-descriptor ownership. No child input enters semantic products, no lossy conversion or compatibility path exists, and no target fact reaches the public API.

Complete locked workspace tests, warnings-denied Clippy, no-default-features checking, native, editor, formatter, documentation, and repository gates pass. Phase 4 separately qualified a fresh temporary archive and extracted home against the same exact process contract. There is no open finding. Phase 5 itself did not assign release identity or produce repository distribution artifacts.

Release identity, reproducible archive generation, extracted-home qualification, publication, and public re-download evidence are recorded by the release-preparation and immutable release records; they are not claimed by the implementation review.

Non-goals

  • a stateless io.read_line or hidden global input buffer;
  • terminal detection, raw mode, styling, or interactive prompts;
  • child environment or working-directory overrides on nocter run;
  • subprocess creation, pipes, waiting, signals, or process groups;
  • asynchronous or nonblocking I/O;
  • another target or target-independent descriptor ABI;
  • new language syntax, effects, semantic roles, MIR operations, or native calling conventions.