Portability

Lambda OS was written for a 64-bit x86 machine — page tables, multiple cores, ACPI, UEFI, ring 3. Every abstraction was allowed to assume that machine.

Then it went down: to riscv64, to aarch64, to a bare MMU-less RV32, and finally to an ARMv6-M Cortex-M0+ — a 32-bit part with no MMU, no FPU, no atomic instructions at all, and 264 KB of SRAM.

Landing on a part with no atomics breaks a kernel whose contracts were drawn in the wrong place: every assumption that leaked — a register layout, a lock, a page table, a compare-and-swap — becomes a rewrite at the bottom.

What is shared and what is swapped

The scheduler, the VM host and the network stack are the same source on all five targets.

Inside them are a handful of compile-time branches that size a table to the chip (task count, stack size, alignment — 264 KB is not 4 GiB). But no register layout, no lock and no page table crosses the line.

Arch-neutral code never imports an architecture's file directly. It always goes through the HAL selector.

The filesystem rides the block driver, so it is live on riscv64 and aarch64 only. The physical frame allocator is x86's.

The HAL contract

The selection happens at compile time. A target that has not implemented a module it needs fails at compile time, not at runtime.

Seven modules exist on all five architectures:

cpu · console · mmu · intc · trap · gdt · nic

Four more — smp, ioapic, pci, acpi — are x86-only, and the block driver blk exists on riscv64 and aarch64 only.

Each maps to that architecture's real hardware:

Concernx86_64riscv64aarch64RV32ARMv6-M
Interrupt controllerAPIC / I/O APICCLINTGICv2CLINT directNVIC
Memory protection4-level pagingSv39 pagingTTBR tablesPMP regionsMPU regions
Second-core startINIT-SIPI-SIPISBI HSM hart_startPSCI CPU_ONSIO FIFOSIO FIFO
TimebaseTSC (PIT-calibrated)rdtimeCNTVCT / CNTFRQCLINT mtimeµs TIMER
NICe1000virtio-netvirtio-netCYW43439CYW43439

How the neutral core stays arch-blind

Selection alone is not enough — the neutral code has to genuinely not depend on any arch detail. That comes from narrowing each boundary contract to a shape that carries no arch specifics.

The context switch is the clearest case. Register layout is exactly where architectures differ most, so the trap boundary is narrowed to one function that never exposes it:

onTrap(sp: usize) usize

The arch stub stacks every register on the interrupted stack, hands the scheduler only the stack pointer, and the scheduler saves it and returns the next task's pointer. The stub then pops and returns (iretq / sret / eret / an exception return).

The scheduler manipulates opaque stack pointers and never sees a register frame. What a frame looks like stays entirely inside the arch layer. On Cortex-M the hardware does half the job — exception entry already stacks eight registers — so the handler saves the other eight and the same contract holds.

That is why the same scheduler runs on all five targets.

ARMv6-M: an instruction set sharing no ancestry

It is ARMv6-M. It is not the aarch64 port — that is ARMv8-A: 64-bit, MMU, EL0/EL1, a GIC. ARMv6-M shares nothing with it but the vendor name: Thumb-1 only, no MMU, no FPU, and no atomic instructions at all.

Yet the same arch-neutral scheduler and the same Lambda C VM ran on it. The scheduler's only concession was its size table. Its logic went down as-is.

An instruction set that shares no ancestry with the other three still slotted under the same contracts — so a new target is a bounded port (write the arch layer), not a fork of the OS.

The MMU-less MCU profile

The same arch-neutral scheduler, VM and actor layers run on a bare RISC-V (RV32 / RV64) in M-mode with:

Three arch modules are swapped for the profile: the trap and the CPU become M-mode variants, and the MMU becomes a no-op stub. Everything above the HAL is unchanged.

The RV32 path further spans a soft-float, FPU-less real chip. There, the M-mode trap is FP-presence-adaptive at compile time: it saves the float registers only when the target has an FPU, so one trap source serves both a hardware-FP target and a chip with no float registers at all.

Bytecode is a separable artifact

The application bytecode is not welded to the kernel image. The default image is baked in, but bytecode can also be delivered at boot:

Where delivery exists, a distinct application runs in place of the baked-in one. The priority is network-fetch, then UART, then baked-in; a plain boot pays nothing and stays byte-identical.

This is what lets the operator's software ship as an artifact separate from the OS image — the same separation of intent (bytecode) from mechanism (kernel) that runs through the whole design.