Phase 3: boot Loon as a unikernel under QEMU - #90
Merged
Conversation
A RISC-V machine whose kernel is a Loon program. No userspace, no syscall
boundary, no OS underneath: init performs effects and the outermost handler
is a UART driver instead of a call into Linux.
make -C crates/loon-kernel run # boot it
make -C crates/loon-kernel check # boot it, diff against `loon run`
The frontend stays hosted. `loon image` serializes a lowered EIR module
(eir/image.rs); the kernel's build.rs invokes it, embeds the result and
interprets it, so no parser, checker or lowering enters the bare-metal build
graph. The kernel is its own workspace — it only builds for
riscv64gc-unknown-none-elf and must not be swept into --workspace.
The kernel interpreter mirrors the host VM's structure rather than
reimplementing it freely: same frame stack, same handler stack keyed by
prompt depth, same continuation capture on `perform`. Deep-handler semantics
are load-bearing, so boot/init.oo exercises handler forwarding, abort
(a clause that never resumes) and non-tail resume on hardware. `make check`
diffs the machine against the host byte for byte, and a skip-if-unavailable
test (tests/unikernel_boot.rs) keeps that honest in CI. A language whose
semantics depend on where it runs is not what we are building.
The image carries a builtin name table so intrinsics dispatch on names, not
on enum discriminants; tests/boot_image.rs pins the operator tags that are
still numeric, since nothing in the type system connects the two crates.
Not yet: preemption (cooperative only — a pure loop owns the machine), SMP,
static handler resolution, most of the builtin set (missing ones raise a
loud error naming the builtin, never a silent unit). Untuned: ~0.9 us per
interpreted op, since a call allocates a register file and each op allocates
an operand vector over a first-fit allocator at ~0.5 us per allocation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
CI's `cargo fmt --check` rejected three of the new files; formatting only, no behaviour change. Also clean the kernel's own clippy run (it sits outside the workspace, so CI never reaches it): drop a stray blank line after an attribute, and pass the higher-order intrinsics' element through `slice::from_ref` instead of cloning it into a one-element array — one less clone per element in map/filter/each. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A RISC-V machine whose kernel is a Loon program. No userspace, no syscall
boundary, no OS underneath:
boot/init.ooperforms effects, and the outermosthandler is a UART driver instead of a call into Linux.
This is the phase-3 exit criterion from
docs/plans/2026-07-01-loon-os.md,and the thing that makes the syscalls-as-effects thesis concrete rather than a
design doc.
Shape
The frontend stays hosted. A new
loon imagesubcommand serializes a loweredEIR
Module(crates/loon-lang/src/eir/image.rs); the kernel'sbuild.rsinvokes it, embeds the bytes, and interprets them. No parser, checker or
lowering enters the bare-metal build graph.
crates/loon-kernel/src/main.rs.bssclear, stack, theHostimpl that is the machinecrates/loon-kernel/src/eir/crates/loon-kernel/src/heap.rscrates/loon-kernel/src/uart.rscrates/loon-kernel/src/mmio.rscrates/loon-kernel/src/sbi.rscrates/loon-kernel/boot/init.ooThe invariant that matters
make checkdiffs the machine againstloon runon the same source, and theoutput is byte-identical. A language whose semantics depend on where it runs is
not what we are building, so the kernel interpreter mirrors the host VM's
structure rather than reimplementing it freely: same frame stack, same handler
stack keyed by prompt depth, same continuation capture on
perform, samedepth-pruned ephemeral handlers.
Deep-handler semantics are load-bearing — a clause that re-performs its own
effect must forward outward — so
boot/init.oodeliberately exerciseshandler forwarding, abort (a clause that never resumes), and non-tail resume
on hardware. All three match the host.
Reviewer notes
crates/loon-kernelis its own workspace ([workspace]in itsCargo.toml). It only builds for
riscv64gc-unknown-none-elfand must not beswept into
cargo build --workspace. Host-side cargo commands run from thatdirectory must
cd ../..first or they inherit the bare-metal target fromits
.cargo/config.toml— this bit bothbuild.rsand the Makefile.name table so intrinsics dispatch on names rather than enum discriminants.
BinOp/UnOpare still numeric tags, pinned bycrates/loon-lang/tests/boot_image.rs— reordering either enum withoutupdating
decode.rswould otherwise silently remap an operator.0..npositionally, not to the entry block's params. Getting this wrongfails far from its cause ("cannot call a unit in tail position").
crates/loon-lang/tests/unikernel_boot.rsrunsmake checkand skips (doesnot fail) when qemu or the riscv target is absent, so contributors without
the bare-metal toolchain are unaffected.
cargo test --workspaceis green.What this does not deliver
is currently the expensive thing. Measured rather than assumed: ~0.9 µs per
interpreted op, ~0.5 µs per allocation, with the UART accounting for only
~0.4 ms of the ~30 ms demo run. A call allocates a register file and every op
allocates an operand vector.
the machine.
it yet), and no static handler resolution.
the builtin; they never silently return
().Next, in order: a slab allocator plus operand-vector reuse to make the perf
claim real, then the timer interrupt for preemption.
🤖 Generated with Claude Code