Splitting arch_hw.c

Status: two cuts done. 9088 → 8359 lines. Last review: 2026-09-02

Why

kernel/arch/x86_64/arch_hw.c reached 9088 lines. Worse than the number is how it got there: during a session spent explicitly on "reducing the monolith" it grew by 633 lines, and nobody noticed until the person paying for the work asked why a file being reduced had gone from seven thousand to nine.

The growth was not careless. Every addition was a diagnostic or a fix that earned its place — hw_ctx_check, hw_cache_audit, hw_exec_refuse, the panic that parks every core. The problem is that the plans which were supposed to shrink the file only ever added portable code beside it. The task state machine became portable; g_tasks[], hw_task_alloc and hw_pick_next stayed exactly where they were.

So this plan has one criterion the others lacked: the line count of arch_hw.c is a completion condition, not a side effect.

Why it was hard to start

Everything in that file is static over shared file-scope globals. Nothing can move until somebody decides what the rest of the file is allowed to see. That decision is now kernel/arch/x86_64/arch_hw_internal.h, and it is deliberately not a public API: nothing outside kernel/arch/x86_64/ includes it, and a declaration arriving there should be read as a cost — a thing two files must now agree about — rather than as progress.

Cut 1 — the Linux socket syscalls (done, 2026-09-02)

kernel/arch/x86_64/linux_socket.c. None of what it does is architecture: reading a sockaddr out of user memory, allocating a descriptor, blocking until a connection arrives is Linux ABI translation over the portable TCP/IP stack in kernel/net/inet.c. It sat beside the GDT because that is where the file started.

The numbers, including the part that is not flattering:

lines
arch_hw.c before9088
arch_hw.c after8544 (−544)
linux_socket.c416
arch_hw_internal.h258
the three together9218 (+130)

The monolith lost 544 lines and the tree gained 130. That 130 is the seam, and it is the right thing to watch: a cut that costs more header than it removes source is a rename.

The seam is ten functions and six globals wide — and it is wider than the socket code alone needed, which is worth saying out loud. The socket file wants hw_task_t; a task holds a saved register frame, an address space and a descriptor table, so moving the task moved vibeos_x86_64_isr_frame_t, hw_proc_t, vibeos_hw_aspace_t, the Linux errno values and five constants with it. That is the honest cost of the first cut, and most of it is paid once: the next files to leave will find those types already on the far side.

One deliberate narrowing: g_current_task is a macro over per-CPU state. Exporting it would have put hw_cpu_t on the header for the sake of one integer, so the seam has hw_current_task() instead. A file that has been lifted out has no business knowing how this kernel finds the current core.

Cut 2 — signal delivery (done, 2026-09-02)

kernel/arch/x86_64/linux_signal.c. Building a frame on a user stack, calling a handler on it, and taking the frame back when the handler returns is Linux ABI, not x86. What is architectural about it - the selectors, the trap frame layout - is named in the seam header and nothing else in the file needs to know.

The two halves were four hundred lines apart, with the crash dumper and half the syscall table between them. That is the more useful thing this cut fixed: hw_signal_deliver and hw_sys_rt_sigreturn are one subject and now sit together.

lines
arch_hw.c after cut 18544
arch_hw.c after cut 28359 (−185)
linux_signal.c154
seam header258 → 330 (+72)

A correction to this plan's own estimate. The table below called this section 915 lines. It is not: the banner "delivering a signal" covers signals, the memory-diagnostic walk, the panic summary, the crash dumper and the whole Linux syscall table. The signal code proper is 185 lines. Section banners in that file describe where somebody stopped writing, not what follows.

The diagnostics group was measured and deliberately left. hw_walk_step, hw_frame_still_mapped, hw_panic_cpu_summary, hw_dump_vanished and vibeos_x86_64_crash_dump are ~427 cohesive lines and would make a good file - but they touch eleven globals including g_cpus, which would put hw_cpu_t on the seam. That is exactly the widening avoided in cut 1 with hw_current_task(). It needs an accessor pass first, so it waits rather than being taken at the wrong price.

Cut 3 — the storage and I/O bring-ups (done, 2026-09-07)

11166 -> 9995 lines. And the first number is the point of this section.

Between cut 2 and cut 3 the file went from 8359 to 11166 lines. Not because anybody decided to grow it: the I5 and I5b work - filesystem images, the log sink, the scratch device, the volume scan, the mount report - was written straight into it, one bring-up at a time, over three days in which this document sat here saying the length of the file is the completion criterion.

That is the same failure this plan already records once ("a session dedicated to reducing the monolith grew it by 633 lines"), at four times the size, and by the person who had read the note. Writing it down again rather than quietly fixing the number, because the lesson is evidently not learned by being written once: new code goes to its own file on the day it is written, not after.

What moved: hw_swap_bringup, hw_write_proof, the log sink bring-up and its console tail, hw_fsimages_bringup and the filesystem-image table, hw_scratch_bringup, hw_volumes_bringup, hw_mount_report. All of it runs once from the boot path and none of it is reachable from a syscall, which is why it separates cleanly.

The seam measured eight names in and seven out - narrow for a monolith, and narrow because the code was new. It had not yet grown the incidental couplings that make the older sections hard to separate. That is an argument for cutting recent additions first: the seam is cheapest on the day the code lands and gets more expensive every week it sits there.

Six statics had to stop being static (g_rootfs, g_swap_bitmap, hw_alloc_page, hw_free_page_why, hw_frame_identity_map, and the swap-slot constant moved to the header beside the array it sizes). They are grouped under one banner in arch_hw_internal.h so the next cut can see what this one cost: every line there is encapsulation given up to buy separation.

Verified by the boot, not by the compiler: all six mounts, all four filesystem images and the log sink read back exactly as before.

Next cuts, in the order they look worth doing

Measured, not estimated from section banners - which is what cut 2 taught:

sectionlinesgoes toblocked on
crash and wedge diagnostics~427arch_diag.cneeds an accessor for g_cpus
the Linux syscall table~693linux_syscall.c-
what a C runtime asks for~408with the syscall table-
the kernel log~434kernel/core/ — genuinely portable-

And one that is not a move at all: X-P2 steps 3 and 4. Demand paging removes the eager read, and with it g_exec_elf, g_interp_elf, the staging cache and the copy loop around them - several hundred lines that leave without needing a seam, because they stop existing.

The log is the only one that can become genuinely portable rather than merely a separate file in the same layer. The others stay architecture-adjacent, and that is fine: a 700-line file that does one thing is not the problem this plan exists to solve.