SageOS is a modern, capability-based operating system built from the ground up using SageLang and targeting the SageVM (SRVM) execution environment.
To create a high-performance, secure, and portable OS where the majority of the system logic is expressed in Pure Sage, minimizing the reliance on C for everything except the most fundamental hardware shims.
- RV64 (Primary focus)
- x86_64
- ARM64
src/arch/<arch>/: Architecture-specific boot code, linker script and metal shim. This is what the build uses — see "Two copies of every architecture" below, because it is not thearch/you might expect.src/kernel/: Pure Sage kernel implementation.src/Makefile: the build. There is no top-levelMakefile, so build commands run fromsrc/.arch/<arch>/: git submodules (SageOS_arm64 / _rv64 / _x64). Unused mirrors — see below.SageBoot/,SageLang/,SageVM/: submodules, pinned. SageLang's pin is kept current so ESP32 work is visible from here.docs/: System specifications and architectural design.tests/: Comprehensive test suite for all subsystems.check_arch_drift.py: reports divergence between the two copies (below).
Recorded before any of the reconciliation work, because it blocks verifying it. Two independent problems, one fixed and one open.
end was used as a block terminator after every if, while and proc. That
is not SageLang -- blocks are delimited by indentation -- and the compiler
rejects it:
error: unknown name 'end' in compiled code
--> kernel/main.sage:33:5
so make ARCH=rv64 could not generate the kernel bytecode at all
("Failed to generate SVM from kernel/main.sage"). Every architecture was
affected and it looked like a build-system problem rather than a source one.
The terminators are removed. This is the same class of bug as the pass that
had been sitting in the ESP32 boot.sage.
With main.sage fixed, the next step fails:
VM compile error: Statement requires AST fallback and cannot be emitted as a
compiled VM artifact yet.
DEBUG: Unsupported stmt type 17 requires AST fallback
Two distinct bugs are involved.
1. sagevm does not validate the compiler it finds. (Fixed.)
sagevm resolves the Sage compiler by taking the first sage on PATH, and
never checks that it works. A stale or broken sage earlier on PATH therefore
produces the "AST fallback" message above, which reads like a fault in the Sage
source and is not one. This was found by planting a failing sage on PATH,
which reproduced the exact failure from both make and a plain shell while the
real compiler succeeded from both. In the wild it was a sage left in
~/.opencode/bin (an agent tooling directory that is on PATH), shadowing
/usr/local/bin/sage.
src/Makefile now pins the compiler -- preferring the one built from the
pinned SageLang submodule -- puts its directory first on PATH for both the
sagevm and sageboot steps, and fails with an explicit message if the chosen
compiler is not executable or does not run. The build now prints which compiler
it is using instead of silently using whatever it finds.
2. sage --emit-vm rejected aliased imports. (Fixed, in SageLang.)
Under strace the failing invocation was just the compiler exiting 1:
execve("/usr/local/bin/sage", ["sage","--emit-vm","kernel/main.sage",
"-o","kernel/main.sage.svm"]) = 0
+++ exited with 1 +++
main.sage opens with
import drivers.memory.pmm as pmm_mgr
import drivers.memory.vmm as vmm_mgrso the cause is the alias form. It reproduced deterministically, with a four-case matrix:
| source | result |
|---|---|
| no import | compiles |
import os |
compiles |
import os as o |
fails |
from os import path as p |
fails |
The error named a type number, which sent the investigation after a missing enum case:
VM compile error: Statement requires AST fallback and cannot be emitted as a
compiled VM artifact yet.
DEBUG: Unsupported stmt type 17 requires AST fallback
17 is STMT_IMPORT, and compile_stmt does have a case STMT_IMPORT for it.
The emitter was never the problem: it already writes BC_OP_IMPORT followed by
BC_OP_DEFINE_GLOBAL naming the alias, or the last dotted segment when there is
no alias. What rejected the statement was stmt_requires_ast_fallback, which
vetoed any import with an alias, on the belief that aliased imports "need the
richer binding logic in interpreter.c". So the emitter and the gate disagreed
about which forms were compilable, and the gate won.
Fixed in SageLang 4128a9a3, by dropping the alias from the veto. From-imports
still need the AST walker and still fail, but now by name:
from-imports ('from module import name') are not supported by the bytecode VM
yet; use 'import module' or 'import module as name'
The artifact path stays BYTECODE_COMPILE_STRICT on purpose. The walker is
reached through chunk->ast_stmts[], which holds pointers into the live AST
and is never serialized, so a fallback opcode in a file would load and then do
nothing. Constructs that need the walker have to compile natively instead.
make ARCH=rv64 is 6/6 and links a 32 KB ELF. The checked-in
kernel/main.sage.svm grows 436 → 1537 bytes, which is the import opcode plus
the module bindings that were previously missing.
One loose end: sage --emit-vm was also observed succeeding 40 runs out of 40
on this same input, which the fix above does not account for. Nothing in the
build depends on that variance now, so it is noted rather than chased.
3. BC_OP_IMPORT has no execution case. (Open, in SageLang.)
With the kernel compiling, sagevm run reaches
SageOS Booting...
Initializing PMM...
PMM Init OK.
only because src/Makefile resolves the driver modules at compile time. Import
a module the build cannot resolve and the opcode binds an empty module, because
core/src/vm/vm.c never executes BC_OP_IMPORT — it appears only in the
validator's opcode tables. The kernel cannot call into the drivers it imports
until that opcode actually loads a module.
Every architecture exists twice, and this is deliberate-but-undocumented, so it has been allowed to rot:
| path | kind | used by the build | state |
|---|---|---|---|
src/arch/<arch>/ |
vendored plain files | yes | ahead |
arch/<arch>/ |
git submodule | no | behind |
src/Makefile writes -T arch/$(ARCH)/linker.ld and lists
arch/$(ARCH)/boot.S, which resolve relative to src/ — so the build reads the
vendored copy. 1533 of this repo's 1587 tracked files live under src/arch/.
The vendored copies are ahead, not behind. src/arch/rv64/boot.S carries the
trap vector, S-mode delegation and the handoff-structure argument that the
submodule pin lacks, and the metal_shim.c files differ by 44/104/43 lines,
with the vendored side carrying the real UART output and the MetalRV64VM
instantiation. Last vendored change 2026-06-19; submodule pins are 2026-06-16.
Decision: src/arch/ is authoritative. The submodules are unused, so
"just build from the submodules" is not currently true, and deleting either side
before reconciling would lose work.
The reconciliation is deliberately not done yet. The intended end state is:
push the real arch content up to the SageOS_* repositories, re-pin the
submodules, point src/Makefile at ../arch/$(ARCH)/, and delete src/arch/
-- which would also delete a stale 1464-file nested copy of SageLang 0.7.0 that
nothing references (the build takes SageLang from the SageLang submodule, which
is at 4.1.16). That is 1533 files removed and a genuine de-duplication.
It is blocked because SageOS cannot currently be built, so a refactor of that
size could not be verified. Deleting 1533 files -- including the only working
copy of the newer boot.S and metal_shim.c for each architecture -- on the
strength of a build that does not run would be reckless. The src/arch/ tree
stays until the build is green.
One change must not be taken blindly when it does happen: the vendored
rv64/linker.ld moves the load address to 0x80800000, which nothing else in
the tree references. 0x80200000 is used by SageBoot's KERNEL_LOAD_ADDR,
src/kernel/main.sage and both VMM/PMM tests, while make run loads the ELF at
0x81000000 -- three different values already. The vendored linker script is
otherwise byte-identical to the submodule's, so its only change is that address,
and it should not be adopted without settling the inconsistency first.
To see the current state:
python3 check_arch_drift.py # human-readable report
python3 check_arch_drift.py --strict # exit 1 on any drift, for CIThe remaining cleanup is to push src/arch/<arch> up to the corresponding
SageOS_* repository and re-pin the submodule, after which arch/ can be dropped
entirely. That is left undone deliberately: removing gitlinks to published
repositories is a structural change with consequences beyond this checkout, and
it should not happen as a side effect of a sync commit.
Also note the build instructions below say make run, which only works from
src/.
SageOS integrates SageBoot as a submodule bootloader. When running, SageBoot is dynamically compiled for the target architecture (ARCH) and loaded as the primary -kernel payload in QEMU, while the SageOS kernel ELF is loaded into memory using QEMU's -device loader:
There is no top-level Makefile; these run from src/.
cd src
make clean && make run ARCH=rv64
make clean && make run ARCH=arm64