Quartz v5.25

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:

  1. Parser hang fixes — both hang forms from the SYS.1 handoff’s Discoveries #3 and #4 are gone.
  2. 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.
  3. Freestanding link partial unblock — the first two batches of LLC-unresolved-symbol errors from compiling tools/baremetal/hello.qz freestanding are closed. Full freestanding ELF linking is NOT yet achieved; this commit is on the path, not at the destination.

Scorecard

SHAScope
503813baParser 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.
13f2d372SYS.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.
33361b36MIR: 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.
b86991a3Codegen: 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 @abort define)
  • @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:

  1. Route prelude panic paths through @panic_handler when one is registered, falling back to unreachable when not. Requires threading @panic_handler-awareness into the intrinsic handlers for unwrap / assert / panic. Most-correct solution.

  2. Mark prelude functions as internal linkage so LLVM’s globaldce pass can remove them if unused. Requires inserting an opt -passes=globaldce step 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.

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.qz now 14/14 (was 8/8); new parser_hang_recovery_spec.qz 5/5
  • Backup binaries saved this session:
    • backups/quartz-pre-parser-hang-fix-golden
    • backups/quartz-pre-fuel-freestanding-golden
    • backups/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 / Zig freestanding / Blog OS for the freestanding-link path. Adopted the extern "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.md Discoveries (2026-04-18 entries) + this handoff.