ADR 0028 — M15 S7: the host services a compiler needs
Inherited. ADR 0028 was accepted before CeeSafe's fork (Decision 0006), when the language had its earlier name. In this record, "the reference" is that language and project and "the reference compiler" its compiler: read them as CeeSafe and
ceesafec. Links to documents CeeSafe did not keep are given as text.
- 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
ceesafec buildand its package commands use, which The reference's hosted library (type-system §26) lacks: a compiler written in the reference (OD-33) cannot do without them.
1. Context
The services the reference compiler itself uses (an audit of src/, 2026-10-30):
| Service | Where the reference compiler uses it | The reference 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, CEESAFE_HOME, CEESAFE_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. The reference 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 (ceesafec mcp, ceesafe_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/ceesafe_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 the reference (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. The reference compiler'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.ceehas one.Environment,Directory,PathKind,ProcessOutputandHostServicesmatched nothing in the repository, CeeSafe or the site's guide. - Where it lives:
lib/std/host.cee, with its unsafe code behind documented contracts;- ten
ceesafe_host_*functions inruntime/hosted/ceesafe_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.cee, run in every C configuration and in the interpreter;check-fail/host-services.cee;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.ceefor 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.