PXX portable userland (mini OS-personality) — one shell, any kernel
- Type: feature — Track E (Examples/apps; file-ownership Track B —
examples/**/ app dir /lib/**, plus the NilPy-frontend deps below which are Track A). Umbrella / demo arc. - Status: backlog — filed 2026-07-10.
- Opened: 2026-07-10 (design-thesis session — the PAL seam paying off).
- Owner: —
The idea (one line)
PXX supplies the userland the kernel deliberately leaves out. The "kernel" below is just an ABI — Linux (syscalls) or FreeRTOS (tasks/IPC). The PAL ([[feature-platform-abstraction-layer]], landed) is the seam that swaps them, so one shell/app source runs as Linux PID 1, on ESP32/FreeRTOS, or as a desktop process — swap the PAL backend, not the app. This is the ir-as-substrate thesis made visible: thin frontend + shared IR + one platform seam ⇒ the app is kernel-agnostic by construction, not by luck.
Backends (the same userland, three kernels)
- Linux
/init— [[feature-kernel-matrix-bootroom]] (rainy-day) already has this: static x86-64 ELF, raw syscalls only,init=/init, PID-1-safe, no libc/coreutils/systemd. "Throw away the distro userland." This umbrella adopts bootroom as the Linux backend (leave it rainy-day; link it here so it stops reading as an orphan stunt). - ESP32 / FreeRTOS — the memory-constrained variant. FreeRTOS is a preemptive
RTOS kernel (scheduler + IPC), no processes/MMU/FS — exactly the userland gap
the shell fills. Applets = FreeRTOS tasks (
xTaskCreate); pipes = stream buffers (blocking read ⇒ natural backpressure + concurrency, no hand-coded yields). IDF path already exists ([[project_esp_idf_isr_2026_06_21]], ESP threading). - Desktop process — normal x86-64/aarch64 binary for fast edit-run dev; pipes map to real threads/pipes. Same source, same shape.
Flagship app — the NilPy shell (busybox/applet model)
A Unix-ish shell written in NilPy (Nil-Python): line editor → parse →
applet dispatch → a | b | c. No fork/exec (no MMU on classic ESP32): one
binary, commands are built-in applet functions dispatched by name
(echo cat ls grep wc head tail ps), optionally run as tasks. A VFS gives the
filesystem illusion (borrow from the sqlite file-VFS + PAL groundwork).
Why NilPy: it doubles as a frontend forcing function — the shell drags NilPy from "proven toy" (classes, control flow, auto-typing, C-import binding all work) to a real language, one concrete feature at a time. And it demos the cross-target thesis at the concurrency level (preemptive tasks on ESP, threads on desktop).
Why not busybox (decided — stop re-litigating)
busybox recurs in brainstorms as "the shell." It's welded to the Linux kernel
model: fork/exec (multi-call binary re-invokes itself per applet), termios,
signals, /proc, the full syscall + VFS surface. Porting it to ESP32/FreeRTOS
(no processes, no MMU, no fork/exec, one address space) = reimplementing half a
kernel — significant hacks, wrong effort. So busybox is NOT the ESP path;
the hand-written NilPy applet shell is. Two legitimate busybox uses stay open,
both Linux-only and separate from this demo's portable goal:
- busybox-ash via cfront as a monster Track C corpus target
([[idea-c-realworld-test-targets]]) — and, if it compiles, a real
/bin/shfor the Linux/initbackend only. We borrow busybox's internal applet-dispatch pattern (a name→function table in one binary), not its process model.
Real dependencies (NilPy frontend gaps the shell forces — Track A)
- [[feature-nilpy-collections-and-string-methods]] —
list/dict(argv, env, job table) + string methods (split/join/stripfor parsing). The hard blocker; file/advance this first. - exceptions + a file-I/O API surface (redirects) — smaller, follow-on.
Phasing
- Desktop-first, NilPy: shell loop + parse + applet dispatch +
a|bvia buffers, on x86-64. Surfaces the collection/string gaps → drive them. - Applet set:
echo cat ls grep wc head tail psover a VFS abstraction. - Cross to ESP: desktop PAL → IDF PAL; applets as FreeRTOS tasks; pipes as stream buffers.
- Polish: history, redirects,
&background = spare task.
Gate (file-ownership Track B for the app; Track A for the NilPy deps)
App/demo builds with $(PXX_STABLE) (Track B rule — never rebuild the compiler);
make lib-test / demos green; desktop shell runs; ESP variant boots under the
QEMU/IDF harness. NilPy-frontend deps carry Track A's gate (self-host
byte-identical) since they touch shared frontend/RTL.
Links
[[feature-platform-abstraction-layer]] (the seam) · [[feature-kernel-matrix-bootroom]]
(Linux backend) · [[feature-nilpy-collections-and-string-methods]] (blocker) ·
[[project_nil_python_arc]] · thesis devdocs/dev/ir-as-substrate.md.
Log
-
2026-07-10 — phase-1 scouting (Track E):
examples/shell/shell0.npyskeleton written (tokenize → dispatch → applets echo/wc/upper/rev, canned session) and committed BLOCKED — it surfaced [[bug-nilpy-str-param-length-index]] (str params unusable in functions: wrong Length, index segfault, codegen ICE; the hard blocker ahead of even the collections gap), plus: no stdin/readline surface,import sysutilstrips "array of const requires builtinheap", and 1-based string indexing (dialect decision needed). PAL imports from NilPy work (import platform→ PalRead callable). See examples/shell/README.md. -
2026-07-20 — Track E re-checked phase 1.
bug-nilpy-str-param-length-index(the hard blocker) is RESOLVED, andshell0.npynow COMPILES under$(PXX_STABLE). It does not yet RUN correctly, and the reason is not a new bug: the pinned stable binary is v222 (2026-07-17) but the 0-based-subscript fix ([[bug-nilpy-str-index-off-by-one]]) landed 2026-07-19, and shell0.npy was ported to the NEW 0-based convention as part of that fix. So the demo is correct for HEAD and wrong for the pin — it printsunknown applet: hel(every token shifted one character) because the pin still indexes 1-based.Phase 1 is therefore gated on a Track A re-pin, not on any Track E work. Track E cannot rebuild the compiler, so there is nothing to do here until
make pinmoves past c6505149. After the re-pin, re-run$(PXX_STABLE) examples/shell/shell0.npy— it should print the canned session unchanged — and only then continue phase 1 (stdin loop, more applets, pipes).The other listed gaps are unchanged and still real: [[feature-nilpy-collections-and-string-methods]] (list/dict/split — still the blocker for argv, the job table and
a | b), no stdin/readline surface, andimport sysutilstripping "array of const requires builtinheap".