Handoff — SYS.5 infrastructure + freestanding link partial unblock
Head: b86991a3 on trunk. Fixpoint stable at 2138 functions.
Session picked up from the SYS.1-complete handoff. Three threads shipped:
- Parser hang fixes — both hang forms from the SYS.1 handoff’s Discoveries #3 and #4 are gone.
- SYS.5 infrastructure twin — x86_64 Multiboot2 linker-script + boot stub + Quake verification task land alongside the aarch64-virt infra that was already in the tree.
- Freestanding link partial unblock — the first two batches of
LLC-unresolved-symbol errors from compiling
tools/baremetal/hello.qzfreestanding are closed. Full freestanding ELF linking is NOT yet achieved; this commit is on the path, not at the destination.
Scorecard
| SHA | Scope |
|---|---|
503813ba | Parser defensive-advance on two error paths. Unknown-attribute @bogus and multi-line extern "C"\ndef foo no longer hang the parser. 5-test parser_hang_recovery_spec.qz wraps each case in a shell timeout 5 so a regression surfaces as exit 124/137, never a hanging test run. |
13f2d372 | SYS.5 infrastructure: tools/baremetal/x86_64-multiboot.ld + tools/baremetal/smoke_x86.s (Multiboot2 header, 32-bit entry, cli/hlt/jmp spin) + tools/baremetal/hello_x86.qz (skeleton) + quake baremetal:verify_x86_64 task. Verifies ELF is X86-64, entry at 0x100020, .multiboot carries magic d6 50 52 e8. No QEMU boot — that’s KERN.1. |
33361b36 | MIR: skip fuel_check instrumentation for freestanding targets. Module-level _mir_is_freestanding flag in mir_lower.qz; set from quartz.qz::do_build based on target string. Third skip case in mir_emit_fuel_check (joins existing skips for async-$poll and @no_preempt). Closed 13 fuel_refill refs from a representative freestanding compile. 3 new assertions in freestanding_spec.qz. |
b86991a3 | Codegen: freestanding panic stubs (qz_overflow_panic / qz_bounds_panic / qz_map_key_panic / qz_print_backtrace get unreachable bodies; @abort gets a define with the same shape) + link-time extern declarations (malloc/free/memcpy/memset/qsort) following the Rust no_std + alloc pattern. 3 new freestanding_spec assertions. |
What’s unblocked now
A representative freestanding compile (hello.qz with @panic_handler)
closed these LLC-unresolved-symbol errors:
@__qz_fuel_refill,@__qz_sched_fuel(via fuel_check skip)@backtrace,@backtrace_symbols_fd(via backtrace stub)@abort(via freestanding@abortdefine)@longjmp,@write,@sprintf,@__qz_panic_jmpbuf_get— gone from the qz_bounds/overflow/map_key panic helper bodies.
Five more symbols get declare statements for link-time resolution
(kernel project supplies at link time):
@malloc,@free,@memcpy,@memset,@qsort
What still blocks (the remaining path)
tools/baremetal/hello.qz and tools/baremetal/hello_x86.qz still do
not reach an ELF. LLC errors remaining, in rough order of how deep the
fix sits:
A. cg_emit_runtime_helpers_2 emits libc-free helpers that are still gated on freestanding
Symptom: llc: use of undefined value '@qz_sort_cmp_asc'.
Location: cg_emit_runtime_helpers_2 in codegen_runtime.qz:4572,
called from cg_emit_runtime_decls which early-returns for freestanding.
The helpers inside cg_emit_runtime_helpers_2 are a mix — some are pure
LLVM IR with no libc refs (@qz_sort_cmp_asc, @qz_vec_sort which only
references @qsort which is now declared), others do call libc. The fix:
audit the function, hoist the libc-free ones into a shared “always-emit”
section, leave the libc-dependent ones in the hosted-only branch. This
is a ~1 hr audit followed by a small refactor.
Next session should start here. Verify: after the hoist, compile
tools/baremetal/hello.qz and check LLC accepts the IR OR advances to
a different undefined symbol.
B. Prelude functions unconditionally emit libc panic paths
Symptom: llc: use of undefined value '@.newline' (on even the simplest
def main(): Int = 42).
The auto-emitted prelude functions (@unwrap, @unwrap_ok, @unwrap_or,
@assert, @panic, etc.) emit bodies that call @write / @longjmp /
reference @.newline / @.panic.prefix / @__qz_panic_jmpbuf_get.
For freestanding, these globals aren’t emitted (per cg_emit_runtime_decls
early-return) and the longjmp machinery doesn’t apply.
Two design options:
-
Route prelude panic paths through
@panic_handlerwhen one is registered, falling back tounreachablewhen not. Requires threading@panic_handler-awareness into the intrinsic handlers for unwrap / assert / panic. Most-correct solution. -
Mark prelude functions as
internallinkage so LLVM’sglobaldcepass can remove them if unused. Requires inserting anopt -passes=globaldcestep in the compile pipeline between Quartz → IR emission and LLC. This is exactly the SYS.1 handoff’s noted “prelude internal-linkage + dead-code-elimination” work, and is the cleaner architectural fix.
Either option is multi-session. Option 2 also unblocks other ergonomic wins (smaller freestanding binaries, tree-shaking).
C. hello_x86.qz-specific: port-I/O intrinsics missing
Legacy COM1 UART at x86 I/O port 0x3F8 needs outb/inb intrinsics.
hello_x86.qz currently uses a placeholder MMIO address, consistent with
hello.qz’s PL011 MMIO — but for actual QEMU x86_64 boot we’d need
either port I/O or PCIe-configured MMIO serial.
Not a blocker for SYS.5 infrastructure (already shipped via asm smoke),
but a prerequisite for getting hello_x86.qz to actually print to serial
under QEMU. Scope: ~1 day to add the intrinsics + spec.
Recommended next thread
Start with (A) — hoist libc-free helpers out of the freestanding gate
in cg_emit_runtime_helpers_2. This is the smallest remaining step
with the highest chance of materially advancing freestanding-link
progress in one session. If it unblocks hello.qz end-to-end (which it
might, depending on what else hello.qz pulls in from prelude), we’ve
closed the SYS.5 epic. If it doesn’t, we’ve scoped (B) precisely —
either via option 1 (panic_handler routing) or option 2 (globaldce pass).
After the hoist, the concrete win condition to aim for:
./self-hosted/bin/quartz --target aarch64-unknown-none tools/baremetal/hello.qz > hello.ll
llc -march=aarch64 -filetype=obj hello.ll -o hello.o
ld.lld -T tools/baremetal/aarch64-virt.ld -o hello.elf hello.o
# hello.elf exists, passes llvm-readelf structural checks
Ship this as quake baremetal:verify_hello_aarch64 (exercises the FULL
freestanding Quartz → ELF pipeline). Same for x86_64 after.
State of the tree
- Branch:
trunk - HEAD:
b86991a3— codegen: freestanding panic stubs + link-time libc extern declarations - Fixpoint: 2138 functions, gen1 == gen2 byte-identical
- Smokes: brainfuck + style_demo both PASS
- QSpec coverage:
freestanding_spec.qznow 14/14 (was 8/8); newparser_hang_recovery_spec.qz5/5 - Backup binaries saved this session:
backups/quartz-pre-parser-hang-fix-goldenbackups/quartz-pre-fuel-freestanding-goldenbackups/quartz-pre-freestanding-stubs-golden
Workflow rules that continued to hold
The five rules from the SYS.1 handoff (write-spec-before-rebuild,
avoid-timeout-on-hang-prone-input, one-build-one-guard-per-slice,
pgrep-before-heavy-steps, fix-specific-backups) all held. No session-
stability incidents. Four quake guard cycles completed cleanly,
fixpoint held on each.
Prime-directive scorecard for the session
- PD1 (highest impact): Picked SYS.5 (THE unblocker for KERN.1) over the easier escape-analyzer investigation. Even the parser hang fixes were framed as load-bearing for session stability, not busywork.
- PD2 (design before build): Researched Rust
no_std/ Zigfreestanding/ Blog OS for the freestanding-link path. Adopted theextern "C"link-time declaration pattern (Rust no_std model) rather than stubbing allocator functions. - PD3 (pragmatism vs. shortcut): SYS.5 deliberately scoped to infrastructure twin (like aarch64’s existing smoke) while naming the full-source-boot path as KERN.1 work. No shortcuts taken.
- PD5 (report reality): This handoff section “What still blocks” is the main evidence. Freestanding is NOT fully unblocked; the remaining blockers are named precisely with file locations and fix sketches.
- PD6 (holes get filled or filed): Every observed gap is either
fixed in-commit or recorded in
docs/KERNEL_EPIC.mdDiscoveries (2026-04-18 entries) + this handoff.