ADR 0028 — M15 S7: the host services a compiler needs
Inherited from Nova. This is Nova's ADR 0028, which CeeSafe inherited at the fork (Nova
v0.6.0; CeeSafe's Decision 0006). Its text is Nova's record and is not changed: read Nova andnovacas the language and compiler CeeSafe forked from,.novaas.cee, and Nova's C names as CeeSafe's (spec/c-backend.md). Links to documents CeeSafe did not fork go to Nova's repository.
- Status: Accepted (project owner, 2026-10-30), every question as recommended (§5). The wording is the agent's. Implemented the same day (§6).
- Scope: M15 S7 (OD-35). These are the operating-system services
novac buildand its package commands use, which Nova's hosted library (type-system §26) lacks: a compiler written in Nova (OD-33) cannot do without them.
1. Context
The services novac itself uses (an audit of src/, 2026-10-30):
| Service | Where novac uses it | Nova today (§26) |
|---|---|---|
| Run a program and capture its stdout and stderr, optionally with stdin | the C compiler and linker (Backend.C/CBackend.cs:252-290); tool discovery (CCompilers.cs:165) | Process.run: inherited stdio, exit status only |
| Read an environment variable | CC, PATH, NOVA_HOME, NOVA_INDEX, INCLUDE | none |
| Create and list directories | -o DIR for --emit c (Program.cs:586, :603, CBackend.cs:42); src/ of a package (Manifest.cs:271) | none |
| Whether a path exists, and what it is | manifests, the cache, fmt on a directory | none (File.open and look at the error) |
| A fresh temporary directory, removed afterwards | every build (Program.cs:614) | none |
| The working directory | package lookup (Manifest.FindDirectory), source paths (SourceText.SourcePath) | none |
The CeeSafe port (P1, host-process probe) confirmed the rest works: writing a C file, running cc with an argument vector, its exit status, and reading the object back.
2. Options
A. Built-in library API in lib/std and §26 (proposed) | B. C adapters in each program | C. A generic extern "C" POSIX binding | |
|---|---|---|---|
| Safety | safe API; the runtime owns the unsafe parts | every program writes unsafe FFI | unsafe everywhere, and POSIX only |
| Portability | the runtime selects the platform (macOS, Linux, Windows, CosmoOS), as Process.run does (§26.6) | per program | none on Windows or CosmoOS |
| Interpreter | modelled, and refused in the sandbox | runs no C (§28.6) | runs no C |
3. Proposed decision: A
Hosted-only functions (N0293 in freestanding code), blocking, with errors as IoError:
Process.capture(program, arguments, input) -> Result<ProcessOutput, IoError>.program: StringViewis found onPATH;arguments: borrow Vector<StringView>;input: Slice<u8>?is written to the child's stdin, which is then closed (null: an empty stdin).ProcessOutputis a built-in struct withstatus: i32, asProcess.runreports it, plusstdout: Vector<u8>andstderr: Vector<u8>.- The runtime reads both pipes and writes stdin concurrently (
pollon POSIX, overlapped pipes on Windows), so a child that fills one pipe cannot deadlock it. - Failures: allocation is
OutOfMemory; the program not found isNotFound; arguments as forProcess.run.
Environment.variable(name: StringView) -> StringView?: the value as static text, likeProcess.argument(§26.1). It isnullif absent or not valid UTF-8. Nova has no way to set a variable, so the text never changes under the program.Directory.create(path) -> Result<void, IoError>(one level;AlreadyExistsif it exists) andDirectory.remove(path)(an empty directory).Directory.list(path) -> Result<Vector<String>, IoError>: the entries' names, without.and.., sorted by byte value, so programs stay deterministic (c-backend §1's rule applied to I/O).Path.kind(path) -> Result<PathKind?, IoError>:File,DirectoryorOther, ornullif nothing is there. It follows symbolic links.Directory.temporary() -> Result<String, IoError>: a new, empty, private directory, created atomically (mkdtemp) under the platform's temporary directory. The caller removes it.Directory.current() -> Result<String, IoError>.
Paths are StringViews, under the same rules as File.open: zero bytes and paths over 4,095 bytes are InvalidInput. The interpreter models these with .NET's APIs on macOS and Linux, and the sandbox (novac mcp, nova_run) refuses them all, as it refuses processes today.
4. Consequences
- Spec: type-system §26.7–§26.10, diagnostics (none new expected), and the runtime in
runtime/hosted/nova_rt_io.c(POSIX and Windows) with CosmoOS's variants selected at compile time. - Tests:
- run-pass and run-fail programs for each function (a child writing more than a pipe's buffer to both streams; absent variables; sorted listing; temporary-directory cleanup);
InterpreterTestsandMcpTestsfor the sandbox;- the MSVC configuration for Windows;
c3.pyin a CosmoOS guest for the spawn path.
- A compiler written in Nova (the CeeSafe port) can then implement
buildand package mode without C adapters.
5. Questions for the owner
- Option A, with this API? (Recommended.)
Environment.variableas aStringView?of static text, rather than a copiedString? (Recommended: it matchesProcess.argument, and nothing can change the environment.)Directory.list: should a name that is not valid UTF-8 make the call fail withInvalidInput, or be skipped? (Recommended: fail. Skipping could make a compiler silently miss a source file.)- CosmoOS: implement
Process.capturethere in 0.4.0 (its spawn API needs pipe redirection; to be audited in CosmoOS's code), or reportOtheruntil a later release? (Recommended: audit first, and decide with the evidence.) - Windows: full parity in the same release? (Recommended: yes. novac's own Windows builds depend on these services.)
6. Implementation (2026-10-30)
- Q4 (CosmoOS), from the audit. CosmoOS's libc (
libc/include/spawn.h,unistd.h,dirent.h,sys/stat.h,stdlib.h) hasspawnvpwithspawn_handlemappings, pluspipe,getenv,getcwd,opendir/readdir,stat,mkdirandrmdir, but nopolland nomkdtemp. SoProcess.captureis implemented there in this release. On every platform it routes the child's streams through files in a private temporary directory, not pipes, so nothing needspollor threads to avoid a deadlock. The temporary directory is made by trying names untilmkdirsucceeds, which is atomic on every platform. - One name changed.
Path.kindisFile.kind. A lib/stdPathwould break programs that declare their own: lib/std names cannot be redeclared in a hosted package (§38.5), and the corpus'srun-pass/struct-nested.novahas one.Environment,Directory,PathKind,ProcessOutputandHostServicesmatched nothing in the repository, CeeSafe or the site's guide. - Where it lives:
lib/std/host.nova, with its unsafe code behind documented contracts;- ten
nova_host_*functions inruntime/hosted/nova_rt_io.c, for POSIX, CosmoOS and Windows, declared inrt.h; - two checker hooks, so
Process.captureandFile.kindcall lib/std'sHostServices; - the interpreter's models in
Interpreter/HostServices.cs.
- Tests:
run-pass/host-services.nova, run in every C configuration and in the interpreter;check-fail/host-services.nova;HostServicesTests, where capture runs natively and in the interpreter with a child writing 300,000 bytes to each stream, and the sandbox refuses.
- CosmoOS, in a guest.
c3.py --sshbuildsintegrations/cosmoos/c3/c3-host.novafor CosmoOS and runs it. The first run found that CosmoOS'sS_ISDIR/S_ISREGtest the stat record'sst_type(aDT_*value), notst_mode(itssys/stat.h). The runtime selectsst_typethere. - Not yet run: the Windows
CreateProcesspath. The CI's Windows job builds and runs the run corpus with MSVC, which covers everything but capture;HostServicesTestscovers capture, but not on Windows.