ReviewOS

home-lang/home-os

A6.1: aarch64 lowering for the kernel codegen path — a proof-of-life ladder mirroring Phase 0.5, ratcheted per Appendix-A file

#51
Open chris opened this 8 days ago ·
8 days ago

Epic: #113 · Milestone: MC1 — Toolchain: aarch64

Why

home build <file> --kernel lowers through packages/codegen/src/home_kernel_codegen.zig (4,204 lines) which emits x86-64 assembly only (351 register references, zero arm64). aarch64_native_codegen.zig (1,461 lines) is a userspace path that supports ≤8 i64 params, structs, let/if/while, literals, direct calls, fixed i64 arrays, match on literals — and returns error.NotImplemented for pointers, methods, struct args/returns, slices. Nothing in HomeOS compiles for ARM today. This is the whole critical path of the media center; per #44 we extend the direct emitter.

The ladder (each rung is a kernel that boots in qemu-system-aarch64 -M virt and prints on the PL011)

Order matters; each rung depends on the previous, exactly like Phase 0.5:

  1. putc via inline str to the UART — integer literals, local calls, simple-form inline asm (AAPCS64 prologue/epilogue, x29/x30, 16-byte stack alignment)
  2. string literals in .rodata + adrp/add addressing
  3. arrays and indexing (ldr/str with scaled offsets)
  4. structs, field access, struct-by-pointer args
  5. while/for over data; switch
  6. function pointers, indirect calls (blr)
  7. imports across files; global var in .bss/.data
  8. const/comptime evaluation parity with x86 path
  9. volatile MMIO loads/stores + barriers (#53)
  10. 64-bit shifts/bit ops, @truncate/@bitCast, packed structs
  11. exceptions: vector-table alignment (.align 11), eret, msr/mrs inline asm forms used by arch/arm64/exceptions.home
  12. NEON intrinsics subset for blits/decoders (ld1/st1, add/sub/mul on .16b/.8h/.4s) — needed by MC4/MC6, may land last

Tasks

  • In home-lang/home: packages/codegen/src/home_kernel_codegen_aarch64.zig (or an arch-abstracted lowering inside the existing file) + tests under packages/codegen/tests/; reuse arm64.zig as the assembler.
  • Register allocation and calling convention per AAPCS64; caller-saved x0–x17, callee-saved x19–x28; x18 reserved.
  • Each rung = a file kernel/src/arm64_poc/step-NN.home + expected serial output, run by #55.
  • Extend scripts/mvk-compiles.sh with --arch=aarch64 reporting per-file failures naming the unlowered construct (diagnostics are a deliverable).

Acceptance

mvk-compiles-aarch64 ratchet reaches 70/70 Appendix-A files (same set as x86) and the rung-12 kernel boots in QEMU virt. Revisit trigger from the ADR: < 35/70 after the planned effort → reconsider LLVM.

Size: XL (the largest single item in the plan) · Depends on: #44 · Blocks: #57, #60, everything in MC2+

8 days ago

Compiler-side work mirrored to home-lang/home#582.

8 days ago

Landed: the architecture seam and the first aarch64 kernel assembly

home-lang/home@29b9eb740 on branch feat/aarch64-kernel-codegen.

What was actually wrong. packages/codegen/src/home_kernel_codegen.zig had 352 emission sites naming x86 registers and mnemonics directly. The lowering was already architecture-neutral — a stack machine over an accumulator, temporaries on the machine stack, locals at fixed frame offsets — so only the instruction text was x86. That is now the whole content of the new packages/codegen/src/kernel_target.zig (1,801 lines incl. tests), which exposes ~50 operations named for meaning rather than for one architecture's spelling.

Evidence the refactor is behaviour-preserving: the x86-64 assembly for kernel/src/mvk_poc.home is byte-identical to the pre-refactor compiler.

Evidence the aarch64 path is real: home build kernel/src/mvk_poc.home --kernel --target=aarch64-freestanding produces assembly containing only aarch64 mnemonics (add adrp and b bl cbz cmp cset ldp ldr ldur mov movz ret stp str stur sub wfi, zero x86 tokens), and zig cc -c -x assembler -target aarch64-freestanding accepts it, producing a 3,600-byte object file.

aarch64 decisions worth reviewing

  • Frame record is the x29/x30 pair — a branch-and-link does not save the return address, so a frame omitting it cannot survive a nested call.
  • A push moves sp by 16, not 8, because sp must stay 16-byte aligned wherever an exception can be taken. Eight bytes per push are wasted; packing two values per slot would make the stack machine's depth accounting non-local, which is not worth it.
  • 64-bit constants are built halfword-at-a-time with movz/movk. This one matters for the Pi 5: every peripheral base is above 2^32 and would silently truncate under a 32-bit form.
  • Frame offsets past ldur's signed 9-bit reach compute their address rather than wrapping into a neighbouring local.
  • sdiv + msub for remainder; cset/csel for comparisons and conditional moves; adrp + :lo12: for symbol addresses.

Ladder status against the 12 rungs in this issue

RungState
1 putc / literals / local calls✅
2 string literals in .rodata✅ (adrp/add addressing)
3 arrays and indexing✅
4 structs, field access, struct-by-pointer✅
5 while/for, switch✅
6 function pointers, indirect calls✅ (blr)
7 imports across files, globals in .bss/.data✅
8 const/comptime parity✅ (shared front end)
9 volatile MMIO + barriers✅ see #584
10 64-bit shifts/bit ops, @truncate/@bitCast✅
11 exception vectors, eret, msr/mrs⬜ needs inline-asm work
12 NEON intrinsics⬜ not started

Rungs 1–10 fell together rather than one at a time, because the seam made them one change. 11 and 12 are genuinely separate and stay open.

Not yet done: the per-file ratchet over the Appendix A set (mvk-compiles --arch=aarch64) and a QEMU virt boot. Those are home-lang/home-os#55 and are next.

Sign in to comment on this issue.