# Python on danos: the CPython milestone

A design note (not built yet) on bringing **CPython** to danos, compiled with the Zig
toolchain (`zig cc`). Like [zig-self-hosting.md](zig-self-hosting.md), it is
forward-looking: it sets a direction and the decisions that follow from it.

## Why Python, and why now

The Zig self-hosting road is gated on a compiler fork and a long std-library seam.
Python is the **stop-gap that removes the wait**: a working CPython gives danos a way
to write programs — services, tools, application prototypes — *without* the Zig
compiler being self-hosted, and it brings the pure-Python package ecosystem along as
a bonus. The intended division of labour:

- **Zig** — the kernel, drivers, and anything on a data plane (interrupt paths,
  DMA rings, block I/O). Unchanged.
- **Python** — the control plane and the prototyping surface: services that are
  event loops over IPC, policy logic that changes often, application experiments,
  and eventually the shell.

Python is also the scripting language for the terminal-and-shell arc: the first
real danos application is planned as a terminal, a terminal wants a shell, a shell
wants a scripting language — and [xonsh](https://xon.sh) (a shell written in
Python) marks where that road can end.

### Non-goals

- **No drivers in Python.** Interrupt handling, ring management, and DMA stay in
  Zig. Python may *supervise and configure* drivers; it does not sit in their hot
  paths (interpreter overhead and garbage-collection pauses in an interrupt path
  are disqualifying).
- **No dynamic loading during bring-up, no `pip`.** The whole arc here ships
  statically linked. Dynamic libraries are a real *later* milestone
  ([dynamic-libraries.md](dynamic-libraries.md)) — an application-layer
  facility that unlocks `ctypes` and loadable extension modules; the operating
  system itself stays static and lean regardless (the size doctrine below).
  `pip` stays out either way until a networking track exists.
- **No fork.** `os.fork` will not exist. This costs almost nothing (see "The
  spawn model fits").

## The realization that shapes everything: the compiler is not the obstacle

`zig cc` is a full Clang-based C cross-compiler, and CPython is portable C with
official precedent for stranger targets than danos — the WASI port is upstream
tier-2, and it runs **without fork, without dynamic loading, and without working
threads**. Every "CPython can't possibly run there" objection has already been
answered upstream by a target *more* constrained than danos.

What CPython actually needs is a **C environment**: headers and a `libc.a`. danos
has neither — and that is the whole project. In the language of the Zig roadmap's
three doors, this is the **door-2-shaped work** (the deferred "musl door"), not the
`std.os.danos` seam: CPython never touches Zig's std.

### The same surface, a third time

The Zig roadmap observed that door 1 (`std.os.danos`) and door 2 (a libc) implement
the *same* ~30 danos-facing operations at different layers. CPython consumes that
identical surface through C spellings. So nothing here is throwaway: the
danos-native operations backing `runtime.os` are the same ones the libc bottoms out
in, and the gaps this track must close (stdio byte streams, cwd, environment,
entropy) are **exactly the Phase-1 gaps the Zig roadmap already lists**. The two
tracks share a road until Python forks off at "build the libc."

## Where danos stands: coverage vs. the gaps

Judged against the minimal CPython configuration (static, WASI-like):

| CPython need | danos today | Gap |
|--------------|-------------|-----|
| open/read/write/close/lseek, readdir | VFS + FAT via `runtime.fs` | none — wrap in C |
| mkdir / unlink / rename / truncate | done (self-hosting Phase 2) | none |
| stat with mtime | done (`wall_clock` + FAT mtime) | none |
| mmap/munmap (object allocator) | native syscalls | none |
| monotonic + wall clock | `clock` + `wall_clock` syscalls | none |
| a place for `Lib/` | FAT boot image | none — better than WASI has it |
| fork / exec | not needed (subprocess disabled at first) | — |
| dynamic loading | not needed (static extension modules) | — |
| getcwd / chdir | — | **missing** (shared with Zig Phase 1) |
| environment variables | `Init` has no env | **missing** (can start empty) |
| entropy | — | **missing** (hash seed; `PYTHONHASHSEED` pins it meanwhile) |
| byte-stream stdin/stdout (fd 0/1/2) | `debug_write` out; structured `InputEvent` in | **missing** (shared with Zig Phase 1; the REPL needs it) |
| signals | — | stubs suffice (WASI precedent); M17 signals-over-IPC maps on later |
| threads | native `thread_spawn`/futex | build threadless first; a pthread subset later (xonsh needs it) |

The clustering repeats the Zig roadmap's: **files, memory, and time are done; the
work is the C packaging plus the small seam pieces** (tty bytes, cwd, env, entropy).

## The libc decision: hand-rolled in Zig, computation lifted from musl

Two viable shapes were considered:

| Option | What it is | Verdict |
|--------|-----------|---------|
| **Mini-libc in Zig** | C-ABI-exporting Zig library over `runtime.os`/`runtime.fs`, shipped as headers + `libc.a`. | **Take this.** Reuses the danos-native surface directly; no Linux assumptions to fight. |
| **Port musl** | Full musl with a danos syscall backend. | Defer, again. musl assumes Linux syscall semantics in places; heavier than the need. |

The trick that makes the mini-libc tractable: musl's `string/`, `math/` (libm —
CPython needs essentially all of it), and number-conversion layers are **pure
computation with no syscalls**. Lift those wholesale (MIT-licensed, designed to
compile standalone) and hand-write only:

- the OS-facing bottom: fds, `mmap`, clocks, `exit`, `getcwd` — thin C-ABI wrappers
  over `runtime.os`;
- a `FILE*` stdio layer (buffered, over the fd layer);
- `malloc` over danos `mmap` (a simple allocator is fine; CPython does its own
  small-object arena management above it);
- the headers (`stdio.h`, `stdlib.h`, `string.h`, `math.h`, `errno.h`, …).

Estimate: **100–150 functions**, of which the hard 40% (libm, string, printf/strtod
cores) are lifted, not written. Correctness hot spots are `strtod`/`dtoa` (Python's
float repr round-trips through them) — another reason to lift musl's, not improvise.

## C interop: static extension modules, not ctypes

"Python can interface with C libraries" is true on danos with one important
correction: **`ctypes` does not work at first** — it is built on `dlopen` + libffi,
both of which arrive only with the [dynamic-libraries](dynamic-libraries.md)
milestone (D2). Until then the interop story is the other, older one:

- **Extension modules statically linked into the interpreter** via CPython's
  `Modules/Setup` mechanism (the standard route for embedded/static builds).
- **Zig speaks C ABI natively**, so danos extension modules are written in Zig
  against `Python.h` — no C required. Two modules are planned from the start:
  - **`danos`** — the system module: endpoints, send/receive, capability passing,
    spawn, exit notification. This is what makes a Python *service* possible: an
    event loop over IPC, speaking the same wire protocols as Zig services.
  - **UI toolkit bindings** — the in-progress danos UI toolkit exposed to Python,
    so application prototypes drive real windows.

The package story follows: **pure-Python packages work** (unpack into
`Lib/site-packages` on the FAT image); packages with C extensions must be
cross-compiled and baked into the interpreter — a curated set chosen per image,
not `pip install`. That is the honest shape of the stop-gap.

## The roadmap

### Phase 0 — Toolchain + libc bring-up

`zig cc -target x86_64-freestanding-none` plus `-isystem` the danos headers and the
mini-libc archive. No compiler fork required — this track deliberately avoids the
Zig roadmap's Phase-0 gate (if the fork lands first, the triple becomes a clean
`x86_64-danos`; nothing else changes). Exit criterion: a **hello-world C program**
compiles on the host and runs on danos, printing via the libc's `write`.

### Phase 1 — The shared seam pieces

The same list as Zig self-hosting Phase 1, closed once for both tracks:

- fd 0/1/2 as console **byte** streams (output exists as `debug_write`; input is a
  new small thing — cooked line input first, raw mode when the REPL wants editing);
- `getcwd`/`chdir`;
- environment variables (an empty block is a valid start);
- an entropy syscall or service (until then, builds pin `PYTHONHASHSEED`).

### Phase 2 — Cross-compile CPython, minimal configuration

Pin one CPython release (3.13 — strongest WASI-era cross-compile support). The
mechanics are well-trodden upstream since 3.11:

- a same-version **build-Python on the host** (`--with-build-python`);
- a `config.site` cache answering what configure cannot probe cross
  (`ac_cv_file__dev_ptmx=no` and friends);
- a `config.sub` patch so `x86_64-unknown-danos` parses;
- `--disable-shared`, static `Modules/Setup` with a minimal module set
  (`posix`, `errno`, `_io`, `_codecs`, `time`, `math`, …);
- `Lib/` shipped on the FAT image; `PYTHONHOME` pointed at it.

Exit criterion: `python -c 'print(2**100)'` runs on danos under QEMU.

### Phase 3 — Terminal + REPL: the first real application

Depends on the display track's font rendering (already its stated next step) and
Phase 1's tty. A terminal emulator drawing a `python` REPL is the milestone demo:
interactive, self-evidently real, and it needs **zero** process-control machinery.

### Phase 4 — The `danos` module and Python services

Write the `danos` extension module and the UI-toolkit bindings; prototype one real
service in Python (a policy-shaped one — e.g. hot-plug policy or a settings
service) speaking the existing IPC protocols. This is the payoff phase for
"prototyping a service or application."

### Phase 5 — Process control, then the shell

The shell — any shell, in any language — forces the surface danos has deferred so
far: **exec-of-path, argv/envp passing, numeric exit status (`WEXITSTATUS`, not the
categorical `ExitReason`), fd inheritance, and pipes.** That is a kernel/VFS
milestone cluster of its own. Then, in order:

1. `subprocess` enabled in CPython (maps onto danos spawn — see below);
2. a **small Python shell** (a few hundred lines over `subprocess` + line input, no
   job control) — the forcing function that reveals which process-control pieces
   actually matter;
3. **explicitly deferred:** a pthread subset over `thread_spawn`/futex
   (create/join/mutex/condition/thread-locals), signals via M17 signals-over-IPC,
   termios job control — and then **xonsh**, which wants all three.

### The spawn model fits

One genuinely good alignment: **CPython does not need fork.** `subprocess` maps
cleanly onto a posix_spawn-style model — exactly what danos has — and the existing
exit-notification-via-endpoint is a *better* fit for `Popen.wait` than Unix's
`wait` semantics. `os.fork` simply won't exist, as on Windows, and almost nothing
in practice cares.

## Risks and gotchas

- **Binary size — and the size doctrine that makes it acceptable.** danos's
  leanness mandate applies to the **operating system**: the kernel and the system
  services stay small (the kernel is measured in kilobytes, not megabytes), and
  nothing in this track changes that — Python never enters the OS layer. An
  **application** budget is different: a statically-linked CPython with its
  module set will be tens of megabytes in ReleaseSafe (the measured ~2×
  safety-check factor compounds it), and that is *allowed* — applications live
  on the FAT image, not in the kernel's world. It still shapes the image, and it
  means every Python service shares one interpreter binary + per-service
  scripts, so the spawn model needs **argv** before "run this .py" works at all.
- **FAT mtime granularity is 2 seconds.** CPython's `.pyc` cache validation is
  mtime-based by default; a rapid edit-run cycle can see stale bytecode. Use
  hash-based `.pyc` invalidation (PEP 552, `--invalidation-mode checked-hash` at
  freeze time) or accept the quirk during bring-up.
- **FAT name lookups are case-insensitive.** Long file names preserve case but
  match insensitively — the same world Python inhabits on Windows/macOS, so
  importlib copes, but two modules differing only by case cannot coexist on the
  image.
- **`strtod`/float repr correctness.** Python's float round-tripping is exacting;
  lift musl's conversions rather than writing them, and run CPython's float tests
  early.
- **Threadless build is load-bearing, initially.** Like WASI, the first builds have
  no working `threading`. The escape hatch is real (danos has native threads and
  futexes; a pthread subset is Phase-5 work) but keep the configuration honestly
  single-threaded until then.
- **The test suite is the porting harness.** CPython ships its own conformance
  suite; getting `test_builtin`, `test_int`, `test_float`, `test_io` running on
  danos early converts "it seems to work" into a checklist. Budget image space for
  the test `Lib/` tree during bring-up.
- **Entropy before exposure.** `PYTHONHASHSEED=0` is fine for bring-up and wrong
  forever; hash randomization exists because attacker-controlled dict keys are a
  denial-of-service vector. Land the entropy source before any Python service
  parses external input.

## Related

- [python-on-danos-milestones.md](python-on-danos-milestones.md) — the execution
  plan (P0–P5) for this note.
- [c-library-compatibility.md](c-library-compatibility.md) — the mini-libc
  (libdanos-c) design behind Phase 0.
- [character-devices-and-tty.md](character-devices-and-tty.md) — the stream-node /
  console / no-pty design behind Phase 1.
- [zig-self-hosting.md](zig-self-hosting.md) — the sibling track; shares Phase 1,
  diverges at the libc.
- [os-development/syscall.md](os-development/syscall.md) — the kernel ABI the
  mini-libc bottoms out in.
- [os-development/vdso.md](os-development/vdso.md) — the public ABI boundary the
  `danos` extension module wraps.
- [os-development/sysv.md](os-development/sysv.md) — the entry stack (argv/envp)
  the spawn-argv work extends.
- [device-driver-development/ipc.md](device-driver-development/ipc.md) — the IPC
  surface Python services speak.
- [file-system-development/file-system-hierarchy.md](file-system-development/file-system-hierarchy.md)
  — where `Lib/` and `site-packages` land on the image.
