Quartz Roadmap
Version: v5.28.0-alpha | Target: v6.0.0 (Launch) Last updated: April 18, 2026 (night — https://mattkelly.io/ now served by the Quartz unikernel, Caddy LE TLS termination, HTTP/2 + HTTP/3 advertised, 2077 connections served pre-handoff) Status: 2,026 functions (default features) | 523 spec files | 6,500+ tests | Fixpoint verified | 7.81 GB peak RSS on macOS (was 25.2 GB; -69%, wall 35.6s → 18.3s, -49%)
Language syntax frozen (Mar 19, 2026). Self-hosted compiler is sole compiler. All ship-blocking items resolved. Pattern exhaustiveness upgraded to world-class Maranget algorithm Apr 16 evening. Open compiler/parser holes are tracked below.
Priority Philosophy (read this before picking work)
The compiler comes first. Everything else is last-mile.
Quartz’s highest-weight priority is compiler completeness, unification, spec pass rate, robustness, and usability — making the language actually real, not approachable-but-broken. The anti-toy-language mindset: every feature documented must work; every spec must pass; every known hole must be filled. We do NOT put lipstick on a pig.
Order of operations:
- Close all open compiler/parser holes (Open compiler issues section + IMPL-TRAIT-RPITIT/TAIT/STRUCTURAL + cache-pattern miscompile)
- Make every spec pass (8 remaining failing specs — json SIGSEGV, separate_compilation, file_helpers, http2_frame, route_groups, semaphore, tls_async, actor Linux rebuild)
- Complete WASM backend (TGT.3 — 3 failing wasm specs close out multi-target story)
- Language feature gaps (Slice
, re-exports, user-defined macros, effect bounds) - Performance/infrastructure follow-ups (compiler memory, arena allocator, async lock hardening)
- Last mile ceremony (docs, package manager, demos, VS Code extension, blog post, community) — ONLY AFTER 1-5 are done
What this means: Tier 1 (Packaging & Launch) and the package-manager / doc-generator / demo-programs items in Tier 4 are DEFERRED until the language itself is complete. See the re-ranked view below.
Two major initiatives run in parallel with the above, starting now (Apr 17, 2026):
- Operation Piezoelectric Effects — algebraic effects for Quartz, Koka-style evidence-passing, 5 phases, ~5-8 quartz-weeks. Phase 3 migrates async onto effects. Unifying mechanism for throws / io / state / async / alloc. Named after the piezoelectric effect (quartz crystal → electric charge under stress, Curie 1880) — same metaphor: user code → observable effects. Phase 0 design COMPLETE 2026-04-18. Commit tag
[piezo]. - Kernel Epic — Bare-Metal & Unikernel — systems-language infrastructure buildout (SYS.1-5) + unikernel (KERN.1-4), ~6-10 quartz-weeks. Dogfoods Quartz as a legitimate kernel-capable systems language. Kernel scheduler =
Asynceffect handler; allocator =Alloceffect. Effects-independent SYS epics start immediately.
These two tracks are coupled: kernel scheduler waits for effects Phase 3, kernel allocator waits for effects Phase 2. Effects-independent parts of both can start today in parallel with compiler completeness work.
This is the single source of truth for Quartz roadmap work. Historical roadmaps and handoffs live in docs/archive/.
Where We Are
The language is feature-complete for launch. All ship-blocking compiler bugs are resolved. What remains is packaging, documentation, ecosystem, and a tail of compiler holes that the April 2026 dogfooding sprint surfaced.
Complete: Self-hosting compiler, H-M type inference, generics, bounded generics,
union/intersection/record types, pattern matching, borrow checker, move semantics,
Drop/RAII, generators, async/await, goroutines, channels, select, actors, race detector,
structured concurrency, HTTP/2 server (deployed on VPS), JSON/TOML/CSV parsers,
Unicode support, unified Map<K,V>, formatter, linter (24 rules), REPL, LSP, DWARF debugger,
WASM backend, playground, postfix ?, puts auto-coercion, string/map iteration,
negative indexing, char literals, destructuring, comprehensions, implicit it (documented surface; parser support is partial — see CMP-PARSER-IMPLICIT-IT below),
safe navigation, pipeline operator, triple-quoted strings, abstract trait methods,
trait auto-satisfaction (Swift/Go model), Container trait, Show/Eq/Serializable stdlib impls,
Open UFCS compiler migration, multi-line function signatures, trailing commas,
CLI style library (composable Style API, terminal capability detection, color degradation),
String ergonomics (String + Int auto-coerce, String * Int repetition, Bool.to_s() → "true"/"false"),
Map<Int,V> iteration (intmap_keys/values via occupancy bitmap, for-in typed loop vars),
Float hashability rejection (tc_struct_is_hashable walks fields), unqualified variant payload types,
Option ergonomics (is keyword, postfix ! force-unwrap, if let/elsif let, panicking m[key] subscript via map_fetch),
compiler smoke tests (guard:source clean-room build + brainfuck.qz + expr_eval.qz),
resolver full scope tracking (local vars/for/lambda/destructure shadow module names in UFCS),
@x sigil (implicit self in impl/extend — verified: nested blocks, lambdas, closures),
raw pthread_mtx intrinsics (internal lock/unlock without Mutex value slot).
Apr 15-16 sprint: pthread_mtx intrinsics (raw lock/unlock without value slot, 9 call sites swapped, imtx 64→72→64 bytes),
resolver full scope tracking (Tier 3 #11 — local vars/for-loops/lambda params shadow module names in UFCS),
@x sigil verified in all contexts (Tier 3 #16), async lock wait-list audit (15a: no leak, 2 hardening items filed),
concurrency stress sweep (15b: actor crash+restart+stop task loss, root-caused to stale pipe bytes + EV_ONESHOT + io_map race FIXED Apr 16 — actual root cause was cascade-stop race, not pipe bytes).
Apr 16 handoff session (11 commits, ~100+ tests unblocked):
SEND-RECV-SHADOW fixed (deleted dead extern "C" send/recv — unblocked 6+ concurrency specs),
async $poll extern collision fixed (cg_extern_var_index $-strip matched __Future_*$poll → libc poll()),
.size wart fixed at compiler level (typechecker auto-rewrites .size on Int-typed values to vec_size()),
impl Option/Result infinite recursion fixed (method bodies delegated to same-named free functions — inlined match logic),
collection stubs complete (reversed() + sorted() added to prelude — 21/21),
generic_ufcs_dispatch_spec fixed (test string concatenation bug, not compiler bug — 8/8 green),
move semantics holes verified already-enforced (s25_low 15/15 + s25_safety 13/13),
modules_spec fixed (stale expected error string — 35/35 with QUARTZ_FIXTURES).
Retest sweep: ~100 specs tested, ~90% pass rate. Stale roadmap entries closed en masse.
Apr 18 — Quartz unikernel goes public (7 commits, zero compiler source touched):
KERN.4 deploy + J.1 live telemetry + J.2 HTTP routing + J.3 Joy-demo page + multi-segment TCP + J.4 handoff + DEF-A 16-slot multi-connection TCP + HTTPS flip at root. https://mattkelly.io/ now served by a Quartz-authored unikernel through self-authored virtio-net + Ethernet + IPv4 + TCP + HTTP/1.1, with Caddy fronting Let’s Encrypt TLS. 58 KiB ELF, 2,077 connections served pre-handoff, PMM flat at 138 pages, 0 leak, 16 concurrent verified. See docs/handoff/kern4-to-joy-demo-handoff.md + Tier 6 below. Commits f287b68c…53e80796.
Apr 16 late evening — Maranget pattern exhaustiveness (3 commits, +35 functions):
Replaced the 290-line flat ad-hoc tc_check_exhaustiveness with the full Maranget (2007) pattern matrix algorithm — same as Rust’s rustc_pattern_analysis, OCaml’s match compiler, and GHC. Shipped in 3ddaa9fa (core algorithm, phases 0-6, 18 new tests), 13610c95 (bare TYPE_RESULT/TYPE_OPTION subjects — wildcard_pattern_spec 9/9), and 463fab4c (parser chain loop for triple-qualified patterns Mod::Enum::Variant(v) + deleted 303 lines of legacy checker). Witness generation shows missing patterns in errors (“missing pattern ‘West’”). QZ0108 unreachable arm warnings for closed types. Handles enum/Bool/Int/String/Option/Result/union/or-patterns/guards (conservative). Single canonical tc_check_exhaustiveness function, no legacy fallback.
Flagship demos: Self-hosted compiler (2,559 functions), Soul of Quartz
demo (50K tasks/sec + instant HTTP), Quartz unikernel at
https://mattkelly.io/ serving the Joy-of-Quartz
live-telemetry page over HTTPS (Apr 18, 2026 — self-authored virtio-net +
Ethernet + IPv4 + TCP + multi-segment + 16-slot multi-connection + HTTP/1.1
- dark-mode landing + JSON API polled from the browser, zero libc, zero Linux
in the request path, unikernel running as a QEMU
-M microvmguest under systemd, Caddy-terminated TLS via Let’s Encrypt).
Tier 1 — Packaging & Launch ⏸ DEFERRED — last mile
Do NOT start these until Tier 0 (compiler completeness), Tier 2 (cleanup), Tier 3 (language feature gaps), and WASM backend are complete. These are ceremony, not substance. Shipping these on a buggy compiler would be putting lipstick on a pig.
| # | Item | Effort | Notes |
|---|---|---|---|
| 1 | Publish VS Code extension | 3-4h | ⏸ Last mile. Defer until language fully crystallized and spec-complete. |
| 2 | Stdlib narrative guide | 2-3d | ⏸ Last mile. “How do I…” guide — writing docs for a compiler with open holes means rewriting them later. |
| 3 | Testing/FFI/Error/Debug guides | 3-4d | ⏸ Last mile. Same reasoning. Write docs ONCE, after the compiler is real. |
| 4 | Launch blog post | 1d | ⏸ Last mile. HN narrative — one-shot attention. Must have a working, bug-free language to point at. |
| 5 | Community infrastructure | 1d | ⏸ Last mile. Discord/Matrix, issue templates. Sets up for traffic we don’t want to invite yet. |
Tier 2 — Cleanup & Hardening
High-value polish. Do before or alongside launch.
| # | Item | Effort | Notes |
|---|---|---|---|
| 6 | Scheduler park/wake refactor | 2-3d | ✅ COMPLETE Apr 15, 2026 — all five follow-ups landed, zero open items. (1) __qz_sched_wake rewritten as parking_lot-style CAS retry loop (codegen_runtime.qz:2480-2530, commit ab9a7829). Pre-fix: 6 hangs + 2 crashes / 1500 runs (0.4% hang). Post-full-sprint: 0 hangs + 0 crashes / 8000+ runs. Test 4 of sched_park_spec.qz un-pended. (2) QZ0210 narrow rejection at MIR-lower time (commit 737b0a4e) — direct calls from main() rejected with actionable hint. (3) Dead spin-park codegen in cg_intrinsic_conc_sched.qz:2988-3086 replaced with call @abort() (~95 lines of CAS+spin IR deleted); wk_try_spin branch dropped from __qz_sched_wake since no code writes state 3 anymore (commit d1c58715). (4) QZ0210 widened from “main only” to “any non-async-target function” via new PASS 0.98 in mir_lower.qz that pre-scans all AST stores for NODE_ASYNC_CALL / NODE_GO targets, populating _async_func_names before PASS 1 (commit d1c58715). Regression-locked by 4 tests in qz0210_sched_park_compile_error_spec.qz. (5) SCHED-WAKE-SIGSEGV resolved — root-caused from the macOS crash report (~/Library/Logs/DiagnosticReports/spk*.ips) to a TOCTOU race in the multi_park_worker test helper (sched_park_spec.qz:57-85), not a compiler bug. The parker did atomic_add(g_frame_count, 1) BEFORE store(base, idx, frame), so the waker could see g_frame_count >= n and read a still-zero slot (malloc via alloc(4) returns uninitialized memory that happens to be zero-paged from fresh mmap). sched_wake(0) then crashed at __qz_sched_wake + 16 (the load atomic frame[5] — far=40 in the crash report). All 21 archived crash reports had the same signature (test_park_n_wake_all → __Future_multi_wake_worker$poll → __qz_sched_wake). Fix: waker waits per-slot until load(base, i) != 0 before calling sched_wake. park_state_spec.qz’s mw_park_task was already safe (stores before counting). Post-fix: 8000/8000 clean. The pre-fix Phase 1 “0.17% crash residue” I attributed to the fix itself was actually this test bug all along, unmasked by the hang fix running the waker faster. |
| 7 | ✅ ALL FIXED (Apr 2026). CMP-MAP-1..4: Cluster A (commit 02db857e). CMP-PARSER-IMPLICIT-IT-1/2 + FAIL-FAST: Cluster B (commit 0981c238). CMP-VARIANT-PATTERN: Cluster C-1 (commit f4ccbbfe). CMP-POSTFIX-TRY: Cluster C-2 via Path A (commit 5e2fd98d). CMP-MAP-FOLLOWUP: phases 1-3 (commits 3aa38ba0, b67c1914). Follow-up session (Apr 12): int-key iteration, for-in loop var typing, Float hashability, float-zext codegen, FFI dedup, Request retyping, unqualified variant payload resolution, dogfood 22 specs. | ||
| 8 | Fold in stale handoff/recovery state | — | ✅ DONE — docs/archive/ consolidated; all HANDOFF_*.md, OVERNIGHT_PLAN.md, VISION.md, prior docs/Roadmap/archive/ content moved to single location. |
| 8a | extend / impl unification — delete impl keyword | ~1 day | Philosophy-drift cleanup. self-hosted/frontend/parser.qz:6552-6553 confirms extend and impl (inherent) produce the same NODE_IMPL_BLOCK — two syntactic forms for the same intent. Unify under extend (Swift-style). New grammar: extend Type ... end (inherent), extend Type with Trait ... end (trait impl, replaces impl Trait for Type), extend Type without Trait ... end (negative impl, replaces impl !Trait for Type), extend<T> Vec<T> with Trait where ... end (generic trait impl). impl keyword deleted entirely — Directive 7, zero-user world, migrate all internal uses in the same commit that removes the keyword. Parser: ps_parse_impl deleted; ps_parse_extend extended with with/without clause and type-parameter/where-clause support. Lexer: retire TOK_IMPL. Typecheck + MIR: no change (same AST node). Migration scope: self-hosted/**/*.qz, std/**/*.qz, examples/**/*.qz, all QSpec files, docs/QUARTZ_REFERENCE.md. Rationale: one verb, one form, philosophy-aligned. Swift precedent (extension Type: Protocol { }) proves the pattern works. |
| 8b | Macro system audit — is it world-class? | ~1 quartz-day | Quartz has a fuller macro story than the Tier 4 #25 entry suggests: self-hosted/frontend/macro_expand.qz implements both builtin macros ($try, $unwrap, $assert, $debug) and user-defined macros (macro name(args) ... end with full registry, variadic params, quote/template variants), plus separate derive-macro machinery in self-hosted/frontend/derive.qz. The infrastructure is there; the audit question is whether it’s production-grade. Check: (1) hygiene — does expansion rewrite identifiers to prevent capture across macro call sites? (2) pattern matching on AST shapes — can a macro destructure its argument AST beyond positional params? (3) error quality — what does the user see when a macro is misused? (4) documentation — docs/MACROS.md doesn’t exist; zero user-facing docs. (5) test coverage — how much QSpec exercises macro edge cases? Output: a gap list + prioritized fixes, or a “it’s already world-class, just needs docs” closure. Report determines whether Tier 4 #25 (user-defined macros, Large) gets downgraded to “document + polish” or stays as an open feature sprint. |
| 8c | CG.1: defuse cg_extern_var_index extern-demangling landmine | ~1 quartz-day | ✅ FIXED Apr 19 2026. Root cause was not “resolver prefixes extern callees” but “the parser translates module::fn into the single identifier module$fn at parse time, which then arrives at codegen as the call’s func_name.” Rewrite at resolver/MIR lowering would have been correct but invasive. Actual fix: in cg_collect_extern_info, store the resolver entry’s entry.name (bare for bare registrations, prefixed for per-module re-registrations) as the call-site lookup key and keep the AST’s str1 (always the bare C symbol) as a parallel extern_var_c_names[i] for LLVM link-time emit. cg_extern_var_index is now a simple exact-match lookup — both the __ prefix guard and the cg_find_function user-function guard deleted. The tag=10 double-registration the resolver already does (bare + module$name) is what makes the table cover every call-site spelling without content-agnostic demangling. Verified: UFCS-EXTERN-SHADOW repro (FileHandle$close still resolves to itself, NOT libc close), __Future_*$poll original burn (12/12 async_spill_regression_spec), multi-module ffi_mod::abs (20/20 extern_def_spec), 15/15 ffi_spec, fixpoint (2142 functions) + smoke (style_demo/brainfuck/expr_eval). |
Tier 3 — Language Features
Post-launch or alongside. Ordered by design-readiness.
| # | Item | Effort | Notes |
|---|---|---|---|
| 9 | ✅ COMPLETE (Apr 12, 2026). Flow-sensitive narrowing at MIR level: MirContext tracks narrowed_var_names/narrowed_variant_names. if opt is Some narrows opt in then-block; if opt is None narrows to Some in else-block. mir_lower_match_expr detects narrowed subjects and emits payload extraction directly (no tag check). Works for Option (Some/None) and Result (Ok/Err). Also supports is-with-bindings (if opt is Some(v)). 7 tests in option_narrowing_spec.qz. | ||
| 10 | ✅ PARTIAL (Apr 12, 2026). Option::Some(v) and Option::None enum constructors now use MIR_ALLOC_STACK(2) instead of MIR_ALLOC. Escape analysis heap-promotes when returned/stored to heap. Fixed 2 escape analysis bugs: (1) multi-origin variable overwrite didn’t mark old origin escaped, (2) block ordering caused LOAD_VAR to miss variable origins (added second pass). Safe navigation ?. also uses alloc_stack. Codegen inline helpers (hashmap_get, vec_pop, etc.) still use malloc. | ||
| 11 | ✅ COMPLETE Apr 15, 2026. _ufcs_current_params now tracks all in-scope bindings: function params + local var/const bindings (NODE_LET, NODE_CONST_DECL, NODE_GLOBAL_VAR) + destructured bindings (tuple/struct/vec) + for-loop iter vars + lambda params. NODE_BLOCK walks children sequentially, pushing bindings after each statement; NODE_FOR/FOR_AWAIT push loop var before body; NODE_LAMBDA pushes params before body (was entirely missing). Verified discrimination: OLD binary returns wrong result on definitive collision test (calls module$method() with 0 args), NEW binary returns correct result via UFCS (calls module$method(value) with 1 arg). Remaining: match arm pattern captures + comprehension bindings not yet tracked (follow-up). 6 tests in resolver_local_shadow_spec.qz + fixture module scope_test_mod.qz. All 7 existing UFCS specs green. | ||
| 12 | ✅ COMPLETE Apr 16, 2026. Maranget (2007) pattern matrix algorithm — same as Rust/OCaml/GHC. tc_check_exhaustiveness in typecheck_match.qz: MxPatStore (flat parallel-vector storage), MxMatrix (row vectors), mx_is_useful (recursive usefulness), mx_find_witness (witness generation). Handles enum/Bool/Int/String/Option/Result/union types + or-patterns + guards (conservative). QZ0108 unreachable arm warnings for closed types. Witness patterns in error messages (“missing pattern ‘West’”). Parser fixed to handle triple-qualified patterns (Mod::Enum::Variant) via chaining loop in ps_parse_pattern. 303-line legacy checker deleted. 18 new tests + 13 existing + 9 wildcard tests green. 2026 functions, fixpoint verified. | ||
| 13 | Scheduler park/wake refactor | 2-3d | ✅ COMPLETE Apr 15, 2026 — zero open items (see Tier 2 #6 for the full writeup). Five pieces shipped: wake CAS retry loop, narrow QZ0210, dead spin-park codegen deletion, widened QZ0210 via PASS 0.98 async-target pre-scan, and SCHED-WAKE-SIGSEGV root-caused from crash report to a TOCTOU race in the multi_park_worker test helper (not a compiler bug). Failure rate 0.53%→0% across 8000 stress runs. Unblocks #15. |
| 14 | Re-exports (pub import) | Medium | Clean public API surfaces. import json gives you everything. |
RESOLVED (Apr 16, 2026). Root cause was NOT the pipe/io_map race hypothesized in the handoff — actor inboxes don’t use pipes (they use channel_park_recv + direct __qz_sched_wake). The actual bug was a cascade-stop race: when a.stop() stops Pinger, Pinger’s stop handler sends reply THEN sends LINK_STOPPED (tag -4) to Crasher. The reply unblocks the main thread, which immediately calls b.stop(). Meanwhile, the LINK_STOPPED races with b.stop()’s message. If LINK_STOPPED wins (~25%), Crasher cascade-stops and closes its inbox — b.stop()’s reply_ch never gets a response → main hangs forever. Fix (two parts): (1) Close inbox FIRST in both stop handler and cascade_stop, before notifications and drain. This makes the channel_closed check in the stop proxy see the actor as dead immediately. (2) Replace the blocking recv(reply_ch) in the stop proxy with a try_recv_or_closed poll loop that checks channel_closed(inbox) as a fallback — if the actor cascade-stopped before processing the stop message, the proxy detects the closed inbox and returns instead of blocking. Stress: 0/500 hangs (was 28/500). actor_link_spec 10/10 green. Fixpoint verified at 1989 functions. | |||
| 15a | Async lock hardening follow-ups (filed Apr 15) | S-M | Item (1) DONE Apr 19 2026 (commit TBD): async_mutex_free and async_rwlock_free now abort with the diagnostic async lock freed while wait list non-empty (UAF imminent) when slot-2 wait_head is non-zero. Helper qz_async_lock_drain_panic emitted by codegen_runtime.qz next to the other panic helpers; drain-check prologue inlined at both free intrinsic handlers in cg_intrinsic_conc_async.qz. New spec async_lock_drain_on_free_spec.qz (2 tests, both via assert_run_panics). Fixpoint 2144 functions. Item (2) STILL OPEN: async_rwlock_write_unlock only wakes ONE waiter. If a chain of readers is queued behind a writer, only the head reader wakes on write-unlock; the next reader waits for that reader’s unlock (state 1→0 triggers another wake). Readers serialize where they should parallelize — throughput hit under reader contention, not a liveness bug. Fix requires expanding wait-node layout from alloc(3) → alloc(4) to carry a reader/writer mode flag (the [398] layout comment already promises this slot — reserved but never wired), then in the wake path, if popped node mode==0 (reader), keep popping readers until mode==1 or list empty and wake them as a batch. Touches read-acquire + write-acquire + write-unlock in mir_lower_expr_handlers.qz + cg_intrinsic_conc_async.qz. Likely M, not S — budget a full session for wait-node layout change + batch-wake loop + reader-contention throughput test. |
| 15 | Async Mutex/RwLock | 2-3d | ✅ COMPLETE Apr 15, 2026. ASYNC-MUTEX-MISSED-WAKEUP root-caused to TWO independent bugs that interacted. Bug A (heap corruption): async_mutex_new and async_rwlock_new allocated 64 bytes for their internal imtx pthread_mutex_t, but mir_emit_async_mutex_lock/_rwlock_read/_rwlock_write call the mutex_lock(imtx) intrinsic which treats imtx as a Quartz Mutex struct and reads/writes byte 64 as a “protected value” slot. Every contended lock OOB-read 8 bytes past the end, and every unlock OOB-wrote 8 zero bytes past the end — corrupting adjacent heap allocations (task frames, wait nodes, other mutexes). The Quartz mutex_new intrinsic in cg_intrinsic_conc_sched.qz:11 allocates 72 bytes precisely because of this — it’s the canonical Mutex layout. Fix: grow both imtx allocations from 64 → 72 bytes to match the canonical layout. Bug B (missed wakeup): Classic parking_lot-style race in the contend paths — between the fast-path CAS failure and the internal-mutex acquire, the holder could complete its entire unlock path, observe an empty wait list, and return, leaving the acquirer appended and parked forever. Fix: after acquiring the internal wait-list mutex, re-run the fast-path CAS. If it succeeds (holder released in the window), release the internal mutex and jump to the acquired path. Shipped in mir_emit_async_mutex_lock, mir_emit_async_rwlock_read, and mir_emit_async_rwlock_write. Stress results: baseline 49 hangs + 9 crashes / 600 (9.7% fail) for async_mutex_spec; 1 hang + 1 other / 300 for async_rwlock_spec. Post-fix async_mutex: 0 / 2000 (sequential). Post-fix async_rwlock: 0 / 1500. Why my earlier re-check attempt failed: Bug A’s heap corruption was the dominant failure mode — fixing the protocol alone didn’t matter when memory was being trashed underneath. The 128-byte experiment isolated Bug A (crashes 9→0, hangs 49→29), and adding the re-check on top closed the remaining race. Neither fix alone is sufficient; both are load-bearing. |
| 16 | @x sigil (implicit self) | ✅ COMPLETE Apr 15, 2026. Verified working in all contexts: direct impl/extend methods, nested if/while/for blocks, lambdas/closures within methods (parser in_extend_method flag stays active through nested scopes). 13 tests in at_sigil_spec.qz (9 original + 4 new edge cases: nested-if, for-loop, lambda-passed, lambda-captured). No code changes needed — the implementation was already complete, just under-tested. Remaining design items (match-arm captures for @x, @self shorthand, directive namespace conflicts) are polish, not blockers. | |
| 17 | Effect bounds on function types | 3-5d | Fn(Int): Int @pure syntax. |
| 18 | usize type alias | Trivial | Semantic clarity. Low value since everything is i64. |
| 19 | Compiler memory optimization | 1-2w | ✅ PHASES 1+2+3a+3b.next+3c DONE — 25.2 GB → 7.81 GB peak RSS on macOS (-69%, wall 35.6s → 18.3s, -49%). Phase 1: eliminated builtin UFCS mangled names + tc_mangle cache. Phase 2: mimalloc + 33 mangle cache sites (mimalloc later disabled on macOS due to dyld TLS race; Linux still benefits). Phase 3a (889a758d): O(n³) parser blowup fixed via PascalCase gate on speculative generic-init walk in ps_parse_ident_expr (25.2→15.1 GB). Phase 3b (c4086eea): investigation only — discovered tc_free’s covered Vecs only hold ~1.4 MB; the actual 6.3 GB leak lives inside tc_function’s body walk. Phase 3c (c1eb4fd4): gated egraph and lint modules behind @cfg(feature: "egraph") / @cfg(feature: "lint") (15.1→12.47 GB; function count 2289→1985). Phase 3b.next (this session): bisected tc_stmt/tc_expr by node kind → NODE_CALL dominant → drilled into tc_expr_call → localized to arity-check suffix fallback. Root cause: 9 sites in typecheck_registry.qz used str_byte_slice(name, len-suf, len).eq(suffix) to check “ends with suffix” — allocating ~2100 substrings per fallback call. For module-internal calls (bare tc_parse_type inside typecheck.qz → suffix-matches typecheck$tc_parse_type), the fallback fires on ~10% of NODE_CALLs × 67k calls = 4.6 GB leaked. Fix: replace all 9 with zero-alloc name.ends_with(suffix) (runtime memcmp). Typecheck 5401→737 MB (-86%), peak 12.47→7.81 GB (-37%), wall 21.0→18.3s (-13%). Also shipped: tc.parse_type_cache field for future groundwork and a first-arg type cache in tc_expr_call to avoid redundant UFCS receiver re-walks (~90 MB). Further gains available from remaining allocations in tc_expr_call’s post-arg-walk section (borrow summary, lambda validation, trait bounds, container type propagation) if needed to push below 6 GB. |
| 20 | Arena-based compiler allocator | 2-3w | Long-term architecture: per-phase arenas using mmap-backed regions that drop via munmap at phase boundaries. Higher priority after Phase 3b investigation: macOS libc’s malloc never returns freed pages to OS (verified empirically — even madvise(MADV_FREE_REUSABLE) on malloc-allocated memory doesn’t drop RSS). The only reliable RSS drop on macOS is mmap → munmap. Evaluate after 3b.next ships; if peak still >8 GB the arena rewrite becomes the unblocker. |
| 20a | Structured concurrency ergonomics — gap audit | Research: ~0.5 day | Quartz has two well-defined concurrency endpoints: fire-and-forget (go do -> end, invisible) and full structured supervisor trees (rich-wielded). Audit question: is there a middle-ground user intent (“I want some structure — cancel children if parent exits, no zombies — but don’t want to write a full scope block”) that currently has no clean form? Research-only item. Execute AFTER effects Phase 3 (async-as-effect migration), because Phase 3 itself changes the concurrency ergonomics — go becomes an Async handler-op and with scope do -> ... end may become natural, potentially closing the gap for free. Deliverable: memo — “the gap exists, here’s what to build” or “gap is closed by Phase 3, no action.” |
Tier 4 — Ecosystem
Build the flywheel. Each item unlocks the next. Items 21-23 are last-mile (defer until compiler complete); items 24-25 are language features (work on alongside Tier 3).
| # | Item | Effort | Notes |
|---|---|---|---|
| 21 | Package manager | 2-3w | ⏸ Last mile. Quartz.toml, quake add, Git-based registry, lock file. Was previously called “THE post-launch priority” — reclassified as ceremony until language completeness is achieved. |
| 22 | Doc generator (static HTML) | 1w | ⏸ Last mile. quartz doc → searchable, cross-linked API docs from ## comments. Needs API to stop moving first. |
| 23 | Demo programs (15 designed) | Multi-sprint | ⏸ Last mile (mostly). See Demo Programs below. qz-http ✅ already shipped — further demos are marketing surface, build after compiler is bug-free. |
| 24 | Slice<T> type | Large | Language feature (keep in priority queue). {ptr, len} fat pointer + range indexing v[2..5]. Real systems language table stakes. |
| 25 | User-defined macros | Large | Language feature (keep in priority queue). Design phase first (Rust/Elixir/Zig study). |
Tier 5 — Moonshots
Uncommitted until design sessions happen. Full thinking is in docs/archive/VISION.md.
| # | Item | Notes |
|---|---|---|
| 26 | Refinement types | SMT solver (Z3). World-class or nothing. |
| 27 | GPU compute | @gpu → NVPTX backend. |
| 28 | LLM directives | @ai("prompt") annotations. |
| 29 | Stream abstraction | Channels + generators. |
| 30 | libLLVM integration | In-process backend: JIT/REPL, optimization control, faster compilation. |
Tier 6 — Kernel Epic (Bare-Metal & Unikernel)
Status: 🔥🔥🔥 The whole stack is LIVE on the public internet at
https://mattkelly.io/. (Apr 18, 2026.) KERN.1, KERN.3a–d, KERN.4, J.1, J.2, J.3, J.4, and DEF-A — all shipped in a single day. The root domain is now served by a 58 KiB x86_64 ELF written in Quartz, running as a QEMU microvm guest, speaking its own Ethernet/IPv4/TCP/HTTP with zero libc and zero Linux in the request path. Caddy fronts TLS via Let’s Encrypt; 16-slot per-connection state tables handle async concurrency; a browser-side 500 ms poll against/api/stats.jsondrives the live counter cards. 2,077 connections served pre-handoff, PMM flat at 138 pages — zero leak. This is the moment Quartz stops being “a language” and becomes “a language that can host itself all the way to your browser’s URL bar.” Next: deferred items (IRQ-driven RX, HTTP/2 port, M:N scheduler), most of which are multi-session epics. Full plan:docs/KERNEL_EPIC.md. Companion:docs/research/EFFECTS_IMPLEMENTATION_PLAN.md. Handoff for deploy session:docs/handoff/kern3-to-deploy-handoff.md.
🏁 Apr 18 2026 (night) — https://mattkelly.io/ served by the unikernel
The user visited https://mattkelly.io/ in a browser and saw the Quartz-authored dark-mode Joy-of-Quartz page render. The entire request path from TLS handshake termination onward ran code written in Quartz. User quote on the landing page:
“I just visited my own website. It is served by a completely custom stack on our own custom kernel, running our own custom web server, running our own custom web page in our own self-hosted language.”
What that actually means end-to-end:
browser
↓ TLS 1.3 (HTTP/2)
Caddy (:443 on Linux VPS, Let's Encrypt cert, auto-renewed)
↓ HTTP/1.1 cleartext on 127.0.0.1:8080
QEMU hostfwd (user-mode SLIRP, -M microvm)
↓ virtio-mmio v1 (legacy)
QUARTZ UNIKERNEL (58 KiB ELF, PVH boot, 64 MiB PMM, LAPIC scheduler)
↓ self-authored virtio-net driver (Quartz)
↓ self-authored Ethernet parse + IPv4 + TCP state machine (Quartz)
↓ 16-slot per-connection state table (Quartz)
↓ multi-segment TCP TX (Quartz)
↓ HTTP/1.1 request parser + router (Quartz)
↓ zero-alloc buf_write_* response builder with live kernel counters (Quartz)
→ response
No libc. No Linux kernel in the request path. No C anywhere in the network stack. The only non-Quartz bytes from the VPS-host-port-80 line onward are QEMU itself (bootloader handoff + virtio-mmio device emulation) and Caddy (TLS termination — a future KERN.5 stretch moves that inside the unikernel too).
Why this is unusual: self-hosted compilers are well-documented (rustc, Zig, Go, OCaml, …). Language-specific unikernels are documented (MirageOS in OCaml, HermitCore in Rust). What’s rare is the combination: the same language hosting the same compiler hosting the same runtime hosting the same TCP/IP stack hosting the same HTTP server hosting the same personal-site page at the same author’s root domain, with no third-party systems code on the critical path. Off the top: MirageOS gets close (OCaml-all-the-way), but MirageOS isn’t “the compiler team’s own compiler serves its own public website.” LISP-machine people in the 1980s had something philosophically similar at the OS level but not on the public internet. The set “self-hosted systems language + own kernel + own TCP/IP + own HTTP + your personal domain” is a very short list.
Pre-handoff scoreboard: 2,077 connections served, PMM flat at 138 pages, 0 leak, 16 concurrent verified.
🎯 Apr 18 2026 — The Unikernel HTTP Session
Holy shit moment. In one sitting, Quartz went from “we have a context-switching scheduler” to “our own self-hosted language is serving a full styled HTML page through its own self-authored network stack on its own unikernel.” No Linux kernel in the request path. No libc. Every byte from the NIC driver to the HTTP response body written in Quartz.
Pipeline of control, all Quartz:
virtio-mmio ISR → rx ring dequeue → Ethernet frame parse →
IPv4 header + checksum → TCP state machine (LISTEN → SYN_RCVD →
ESTABLISHED → CLOSE_WAIT → LAST_ACK) → HTTP/1.1 response builder →
tx ring enqueue → virtio-mmio kick → wire
End-to-end demo:
$ curl -v http://127.0.0.1:8094/
< HTTP/1.1 200 OK
< Server: Quartz-unikernel
{739 bytes of styled HTML}
This is the moment Quartz stops being “an educational language with cute demos” and becomes a language that can credibly front-line a real, on-the-wire, kernel-resident workload authored entirely in itself. Self-hosted compiler + self-authored TCP/IP + self-authored HTTP server = no excuses, no “but for the runtime.” The only non-Quartz bytes are the QEMU bootloader handoff. Everything else is ours.
Compiler bugs surfaced during the session: PSQ-9 (extern param named from OOMs typechecker) + PSQ-10 (and/or codegen mallocs per iteration — matters in tight kernel loops). Filed, not worked around in kernel source. Four bare-metal regression tests remain green (verify_hello_{aarch64,x86_64}, qemu_boot_x86_64, qemu_http). No compiler source touched; fixpoint at 2138 functions unchanged.
Write a unikernel — kernel + the Quartz web server linked as one bare-metal binary, booting on QEMU and eventually on the user’s VPS with no Linux underneath. The unikernel is the forcing function; the real deliverable is the systems-language infrastructure buildout (SYS epics) that makes Quartz a legitimate kernel-capable systems language. Once built, hypervisor / embedded / RTOS / driver work all become downstream.
Audit finding (Apr 17): Quartz is ~70% kernel-ready. Already present: atomics (all 5 orderings), volatile load/store, @[naked], @[repr(C)], @[packed], extern "C" definitions, @[section(...)] parsing, inline asm via @c("..."), aarch64-unknown-none + x86_64-unknown-none-elf freestanding targets, and a working bare-metal hello world at tools/baremetal/hello.qz that boots on QEMU aarch64-virt.
Committed design decisions:
- Allocator =
Alloceffect (Kokaalloc⟨h⟩model), not Zig-style explicit params. Handler swap = arena swap. - Interrupt calling convention = type-level (
extern "x86-interrupt" def ...), not attribute. Rust / Zig prior art. - Stdlib split = three-tier (
std/core/,std/alloc/,std/std/). Rust model; industry standard.
Systems-infrastructure epics (SYS — effects-independent, can start now)
| # | Item | Blocking | Estimate | Status |
|---|---|---|---|---|
| SYS.1 | Bare-metal completeness | — | ~1 week | ✅ DONE (2026-04-18). Atomic RMW, extern "x86-interrupt"/preserve_{all,most} cconvs, target knobs, @weak, FS/GS + TPIDR TLS, @panic_handler, field @align(N), wrmsr/rdmsr. Fifteen commits across two sessions. |
| SYS.2 | Stdlib three-tier scaffolding | — | ~2-3 days | Not started. Low priority — std/core/* + std/alloc/* modules can be reorganized any time; Vec/Map/String already work in freestanding so the hard problem (no-alloc vs. alloc boundary) is effectively solved via libc_stubs.c. |
| SYS.3 | Alloc effect proposal for effects Phase 0 | — | ~0.5 day | Not started. Design note. |
| SYS.4 | Alloc effect impl + stdlib API shape | Effects Phase 2 | ~3-5 days | Blocked on effects. |
| SYS.5 | x86_64-unknown-none parity with aarch64-virt | SYS.1 | ~2-3 days | ✅ DONE (2026-04-18). PVH ELF note + MB2 header + freestanding codegen complete. baremetal:verify_hello_x86_64 + baremetal:qemu_boot_x86_64 green. |
Kernel / unikernel epics (KERN)
Top-level goal: curl https://mattkelly.io/ served by a Quartz
unikernel, no Linux in the request path. Estimate: ~3-6 quartz-weeks
from today. TCP/IP stack is the long pole; everything else is bounded.
| # | Item | Blocking | Estimate | Status |
|---|---|---|---|---|
| KERN.1 | Unikernel synchronous parts | SYS.1, SYS.5 | ~1.5-2 weeks | ✅ DONE (2026-04-18). Context switch + APIC + RAM probe all landed. See detail below. |
| KERN.2 | Kernel scheduler as Async effect handler | KERN.1, Effects Phase 3 | ~1-2 weeks | Toy cooperative scheduler already landed. Real form waits on effects Phase 3 — but not on the critical path for KERN.3/KERN.4. |
| KERN.3 | Virtio-net + TCP/IP + HTTP server port | KERN.1 finish (not KERN.2) | ~2-4 weeks | ✅ 3a-3d DONE (Apr 18 2026). 3e/3f pending. See phased breakdown below. |
| KERN.4 | VPS deploy | KERN.3 | ~2-3 days | ✅ DONE (Apr 18 2026). Unikernel lives at http://195.35.36.247:8080/ on mattkelly.io VPS as a QEMU -M microvm guest under systemd. Caddy fronts TLS (auto-LE). Joy-demo dark landing page + /api/stats.json JS poll at 500 ms + /health. See detail below. |
| KERN.5 | TLS in unikernel | KERN.3 | ~2-3 weeks | Stretch. Port rustls-equivalent or hand-write. Cert provisioning via Let’s Encrypt. |
| KERN.6 | Static filesystem + content embedding | KERN.3 | ~3-5 days | Stretch. @[section(...)] + include_bytes!-equivalent for build-time content embed. Later: virtio-blk + minimal ext2 reader. |
| KERN.7 | Multiple concurrent connections via Async handler | KERN.2 + KERN.3 | ~1 week | Stretch. Drops out naturally once KERN.2 ships. |
| KERN.8 | SMP / multi-core | KERN.1 + APIC | ~2-4 weeks | Far stretch. Needs MP tables / ACPI, per-CPU state, cache coherency discipline. |
| KERN.9 | Virtio-blk + ext2 / simple FS | KERN.1 + SYS.4 | ~2-4 weeks | Far stretch. Alternative to KERN.6 if real persistence matters. |
KERN.1 — Unikernel Synchronous Parts ✅ DONE
All three pieces landed Apr 18 2026 in commits 6bcacfba, 721fc42c, 2af80240.
| Piece | Scope | Outcome |
|---|---|---|
| Context-switching scheduler | switch_to(from_slot, to_slot) asm helper + per-task 4 KiB stacks + seeded fake-suspended-frame via task_init_stack. Two tasks cooperatively yield. | ✅ QEMU prints A1 B1 A2 B2 A3 B3 A4 B4 A5 B5 — real per-task state survives context switches. |
| RAM probe + 1 GiB identity map | PMM pool bumped 1 MiB → 64 MiB in .bss (16384 pgs). 512-entry PD fill loop covers first 1 GiB with 2 MiB huge pages. 2 MiB/step write-pattern probe from 96 MiB up finds the RAM ceiling. | ✅ RAM: 128 MiB, PMM pool: 64 MiB (16384 pgs) on QEMU default. Multiboot2/PVH boot-info parsing queued as follow-up. |
| APIC + LAPIC timer | Dedicated pd_apic PD at PDPT[3] identity-maps the single 2 MiB page containing 0xFEE00000. apic_init enables globally via MSR 0x1B bit 11, configures Spurious Vector, LVT Timer (periodic, vector 0x20), Divide (/16), Initial Count (10M). pic_disable masks both 8259As. timer_isr EOIs via MMIO write. PIT + serial RX ISR deleted. | ✅ APIC enabled @ 0xFEE00000 + sched done (ticks=3) after a hlt-park loop proves the LAPIC timer actually fires. |
Compiler bug surfaced + filed: PSQ-9 — naming an extern parameter from OOM-loops the type checker (worked around with from_slot).
KERN.3 — Virtio-net + TCP/IP + HTTP (phased)
The long pole. Each phase has a clean standalone deliverable.
| Phase | Scope | Deliverable | Est |
|---|---|---|---|
| 3a: virtio-net driver ✅ | MMIO device discovery + feature negotiation + RX/TX queue setup + polling RX. Legacy virtio-mmio v1 (QEMU -M microvm offers no v2). Broadcast ARP request on TX elicits a reply from SLIRP which RX hex-dumps. | ✅ DONE Apr 18 2026 — full ARP roundtrip through Quartz-authored virtio-net. Commits 3a.1–3a.3 (f277509b, 0f236ea6, 2dec6567). | 3-5 days |
| 3b: ARP + IPv4 + ICMP ping ✅ | Ethernet / IPv4 / ICMP parsing + outbound checksum (RFC 1071) + generalized RX/TX (multi-frame). Host-side ping 10.0.2.15 still needs an IOAPIC / tap network (SLIRP is NAT-only). | ✅ DONE Apr 18 2026 — guest-initiated ping 10.0.2.2 roundtrip through SLIRP. Commit 9f31e987. | 2-4 days |
| 3c: TCP state machine ✅ | Single-connection RFC 793 subset — LISTEN → SYN_RCVD → ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED, pseudo-header checksum, proper FIN accounting. No retransmits / TIME_WAIT (echo server doesn’t need them). | ✅ DONE Apr 18 2026 — nc 127.0.0.1 8090 via SLIRP hostfwd round-trips cleanly. Commit 1a2e38d4. | 5-10 days |
| 3d: HTTP/1.1 server ✅ | Fixed-response server (doesn’t parse request path yet — any request gets the same landing page). Status line + headers + HTML body. FIN after response per Connection: close. | ✅ DONE Apr 18 2026 — curl http://127.0.0.1:8094/ returns a styled HTML5 page. Commit 944cc774. | 1-2 days |
| 3e: Embed “joy of Quartz” + marketing content | Static HTML + CSS + (optional) JS embedded via @[section(...)]. Build-time asset bundling. | Page looks good in a real browser. | 1-2 days |
| 3f: Port existing web server | If the hosted HTTP/2 server’s socket API abstracts over reasonable enough, swap libc syscalls for the kernel’s socket shim. Otherwise, treat 3d as the new server and upstream features from there. | Same code path in kernel as in userspace. | 1-5 days |
KERN.4 — VPS Deploy ✅ DONE (Apr 18 2026)
Shipped. Unikernel runs on mattkelly.io (195.35.36.247) as a
QEMU -M microvm guest under a systemd unit. Five commits across
the deploy session (f287b68c through 2d30eed4):
| Phase | Shipped | Notes |
|---|---|---|
| Deploy | baremetal:build_elf Quake task + scp pipeline + systemd unit + Caddy fronting | VPS was already running Caddy with reverse_proxy localhost:8080 in the Caddyfile — unikernel’s hostfwd=tcp::8080-:80 plugs straight in. Port 80/443 stay on Caddy (auto-LE cert on first hit). |
| J.1 — Live telemetry | Landing page embeds scheduler ticks + connections served + PMM pages used/total + virtio-net MAC via zero-alloc buf_write_* helpers. Caught + fixed two per-request PMM leaks (tcp_send + response buffer); serve loop converted from “bounded + exit on first conn” to perpetual. | PMM stays flat across sequential requests. |
| J.2 — Routing | GET / (landing), GET /api/stats.json (counters as JSON + CORS), GET /health (text “ok”), everything else → 404. http_request_matches is deliberately exact-match + space-terminated (distinguishes / from every path). | |
| J.3 — Joy-demo + multi-seg TCP | Dark-mode grid of 4 live counter cards, inline fetch() loop polling /api/stats.json every 500 ms. TCP ESTAB handler now splits responses > 1400 bytes into multiple segments (PSH on last); unblocked a 3391-byte styled page. | |
| DEF-A — Multi-connection TCP | 16-slot per-connection table in one PMM page (state / rcv_nxt / snd_nxt / snd_una / remote_port / peer_mac / peer_ip). tcp_handle_frame looks up slot by (peer_ip, src_port), dispatches on per-slot state. tcp_send takes slot as first param. tcp_active_conns exposed via /api/stats.json + landing page “Active” card. | 16 concurrent curls over public internet all returned 200, connections_active peaked at 7 simultaneous. |
| HTTPS flip | User pointed mattkelly.io root at the VPS. Caddy config: single block mattkelly.io { reverse_proxy localhost:8080 } + HSTS header. Let’s Encrypt cert landed in <10 s. 308 redirect from http:// to https://. HTTP/2 upstream-to-browser; HTTP/3 advertised via alt-svc. | https://mattkelly.io/ is live. 2,077 connections served pre-handoff with PMM flat at 138 pages — zero leak under real traffic. |
Strategy actually used (vs. original plan): Caddy replaced the
speculative nginx+certbot path — it was already on the VPS doing
auto-LE, so we rode it. No DNS changes needed (mattkelly.io root
still points to user’s personal fly.io site; unikernel reachable
by IP for now). Subdomain selection pending user decision.
Live demo (public internet):
$ curl http://195.35.36.247:8080/ # styled HTML, 3391 bytes
$ curl http://195.35.36.247:8080/api/stats.json
{"version":1,"ticks":1234,"connections_served":42,"pmm_pages_used":137,...}
$ curl http://195.35.36.247:8080/health # "ok"
Request path: public IP → port 8080 on Linux VPS → SLIRP hostfwd → QEMU microvm guest → Quartz-authored virtio-net driver → Ethernet parse → IPv4/TCP state machine → HTTP router → response builder → wire. Zero Linux bytes in the request path.
DEF-A shipped in same session (d8a84d83). 16-slot per-connection TCP table in one PMM page. tcp_handle_frame dispatches on per-slot state; tcp_send takes slot as first param. Active-count exposed via /api/stats.json + landing page. 16 concurrent curls on production VPS all returned 200 OK, connections_active peaked at 7 simultaneous.
Remaining menu (see handoff doc):
- HTTPS flip — pending user DNS decision. Add A record for a subdomain → VPS IP, append Caddyfile block,
systemctl reload caddy. Caddy grabs LE cert on first HTTPS hit. ~5-15 min ops. - DEF-B — IOAPIC + IRQ-driven RX (replaces
rx_waitpolling print-hack). ~5-8 quartz-hours. HIGH brick risk — remote recovery requires redeploy. - DEF-C — Fix PSQ-10 (
and/ormalloc per eval) in compiler. Requiresquake guard+ fix-specific golden. ~3-5 quartz-hours. - DEF-D — HTTP/2 (h2c) in unikernel — multi-session epic. KERN.3f.
- DEF-E — M:N scheduler in unikernel — multi-session epic. KERN.2 + KERN.7. Blocked on effects Phase 3.
KERN.5 — TLS in unikernel (stretch)
Options (ranked by practicality):
- Hand-write minimal TLS 1.3 in Quartz. ~3000 LoC. Proves systems-language chops.
- Port rustls to Quartz. Translate the state machines; reuse the same cryptographic primitives.
- Link a C TLS lib (BearSSL, mbedTLS) via FFI. Fastest; dodges the “all Quartz” claim.
Needs: Let’s Encrypt ACME client (~500 LoC), cert storage (tie into KERN.6), SNI routing.
KERN.6 — Static FS + content embedding (stretch / optional)
Two paths:
- Cheap:
@[section(".content")]attribute + build-time asset injection. Embedded content is just bytes in the ELF. ~50 LoC kernel, ~100 LoC Quake task. - Real: virtio-blk driver + minimal ext2 / FAT32 reader. ~2 weeks. Only worth it if we need runtime mutability (user uploads, logs persisted across reboots).
For the “joy of Quartz” site, cheap is almost certainly right.
KERN.7+ — SMP / effects-backed concurrency / etc. (far stretch)
All well-scoped but not on the MVP path. Sequence after KERN.4 lands.
Joy-of-Quartz Unikernel Epic — ⏸ PAUSED at clean phase boundary (Apr 18, 2026). All of KERN.4 + J.1–4 + DEF-A shipped; https://mattkelly.io/ served by the unikernel end-to-end. Four-phase plan (K / S / H / D, ~6–10 quartz-weeks across 10–15 sessions) preserved verbatim in docs/handoff/joy-of-quartz-unikernel-epic.md; resumes at Phase K.1 (slab allocator on top of the bump PMM). Endgame unchanged: live htop-style dashboard + “spawn 10,000 tasks” button on mattkelly.io demonstrating Elixir-level concurrency on a ~60 KB ELF.
Next step (compiler track): Operation Piezoelectric Effects — Phase 1 Milestone A IN PROGRESS (Apr 19, 2026) (commit tag [piezo]). Phase 0 design closed Apr 18; lexer + parser surface syntax landed in Milestone A. What shipped: TOK_CAN/TOK_EFFECT/TOK_WITH/TOK_REIFY wired as keywords; TOK_HANDLE/TOK_THROW/TOK_RESUME reserved as token constants only; ps_parse_effect_decl accepts effect Name<T> ... end (discards to top-level no-op); ps_parse_can_row accepts can Row function-sig suffix (parsed + discarded); ps_parse_handler_block accepts with H do -> body end (handler discarded, body evaluated eagerly); ps_parse_reify accepts reify { expr }; ps_parse_try_catch extended to emit ast_try_expr when no catch follows (new propagation-prefix form); $try macro + QZ0509 deleted; 11 std/iter.qz + 4 spec + 1 example $try(...) call sites migrated to try .... 5 new QSpec parse specs filed.
Deferred inside Milestone A (not blockers; file follow-up milestones):
- Wire
handle/throw/resumeas hard keywords. Blocks on:handlehas ~45 identifier-collision sites across self-hosted + std + specs (struct fields inArena,Task<T>; locals inpipe_opencallers; fn params inast_await,_wasm_find_block_idx).throw/resumeneed to survive as method names insideeffectbodies — requires either contextual-keyword handling inps_parse_functionor a contextless-name relaxation in effect-op declarations. Pick when Milestone C/D wires operation-kind bodies. can Rowcurrently parsed + discarded. Milestone B wires it intoTcRegistry.rowsfor effect-row inference.effect Name<T> ... endcurrently emitsast_import("", "")at top level and discards all method signatures. Milestone B introducesNODE_EFFECT_DECL+ a parallel side-table onTcRegistryfor effect ops.with H do -> body endevaluates body as a plain block; handler is parsed-and-discarded. Milestone D introducesNODE_WITHwith evidence-chain linkage.reify { expr }returns the inner expression directly. Milestone D wraps with a prelude handler that converts throws toErr(e)and returns toOk(v).
Phase 1 remaining (per docs/research/EFFECTS_LLVM_COMPILATION_MEMO.md § 7): Milestone B (type-system rows + inference, ~2 quartz-days), Milestone C (MIR effect primitives + kind classification, ~1 quartz-day), Milestone D (codegen evidence threading + handler structs, ~1.5 quartz-days), Milestones E/F/G/H (stdlib pilots, error messages, fixpoint, docs). Phase 1 total estimate: 5-7 quartz-days · 7-10 sessions.
Demo Programs
Purpose: 15 impressive programs that prove Quartz is a world-class systems language. Not toy examples — real tools that only a systems language could build well.
What we’re proving: Speed. Portability. Syntactic sweetness. Practical power.
The compiler itself is Demo #0: 20K+ lines, self-hosted, ring buffers, arena allocators, e-graph optimization, 3,239 tests, fixpoint-verified. Everything below builds on that foundation.
Planning Process
Every demo program follows this process before a single line of code is written.
- Research — Study what the community and world loves best. Find out what the big players are already doing. Identify the most beloved, simple, unified APIs. Document findings in a research brief.
- Design Interview — Iterative planning session — interview until consensus. Present API options with trade-offs. Compare against best-in-class implementations. Synthesize the best of all approaches into our design.
- Approval — Write implementation plan with full API surface. Get explicit user approval before proceeding. No implementation begins without a signed-off plan.
- Implementation — Build to the plan, following prime directives. Any gap discovered during implementation → fill it or backlog it. Fixpoint verification after stdlib changes.
- Showcase — Write a showcase section for the README. Benchmark against comparable implementations in other languages. Document what the demo proves about Quartz.
Dependency Graph
graph TD
HTTP["1. qz-http<br/>HTTP Server"] --> JSON["2. qz-json<br/>JSON Parser"]
HTTP --> ARG["3. qz-arg<br/>CLI Args"]
JSON --> DB["11. qz-db<br/>Key-Value Store"]
ARG --> GREP["9. qz-grep<br/>File Search"]
ARG --> FMT["8. qz-fmt<br/>Code Formatter"]
ARG --> TEST["10. qz-test<br/>Test Runner"]
ARG --> COMPRESS["7. qz-compress<br/>Compressor"]
HTTP --> WASM["14. qz-wasm<br/>WASM Backend"]
WASM --> RENDER["12. qz-render<br/>3D Renderer"]
RENDER --> CANVAS["★ Canvas Site<br/>WASM + 3D"]
HASH["5. qz-hash<br/>SHA-256"] -.-> COMPRESS
ALLOC["4. qz-alloc<br/>Allocator Bench"]
REGEX["6. qz-regex<br/>Regex Engine"] -.-> GREP
LISP["13. qz-lisp<br/>Lisp Interpreter"]
SHELL["15. qz-shell<br/>Interactive Shell"]
style HTTP fill:#ff6b6b,color:#fff
style JSON fill:#ff6b6b,color:#fff
style ARG fill:#ff6b6b,color:#fff
style CANVAS fill:#ffd93d,color:#000
Compiler Prerequisites for Demo Programs
These compiler gaps were discovered during qz-http implementation. Most are now resolved.
A. extern "C" def Type Checker Support ✅ RESOLVED
extern "C" def Type Checker Supportextern "C" def declarations parse correctly and codegen emits proper LLVM declare statements, but the type checker doesn’t register them as callable functions.
✅ Already fully implemented. typecheck_walk.qz:169 registers NODE_EXTERN_FN in Phase 2 via tc_register_extern_fn_signature() (typecheck.qz:3584-3624). Full pipeline working: parser → AST → resolver (tag=10, dedup) → type checker → MIR (skip) → codegen. 25+ tests across extern_def_spec.qz, ffi_spec.qz, cimport_spec.qz. Real usage in std/ffi/tls.qz (50+ OpenSSL bindings), self-hosted/shared/repl.qz. This roadmap entry was a fossil from an earlier codebase state — the fix was part of the April 2026 source fossil resolution.
B. Socket Intrinsics (Nice-to-Have)
Add POSIX socket functions (socket, bind, listen, accept, tcp_recv, tcp_send, close_fd, htons, setsockopt) as compiler intrinsics, following the qz_malloc/qz_free pattern in codegen.qz.
Note: With A resolved, this is optional — extern "C" def declarations work and can be used instead. Intrinsics provide better DX (no boilerplate declarations) but are not a blocker.
C. Hex Literal Parser Support 🟡 DISCOVERED
The self-hosted parser does not support hex literals (0xFFFF, 0x0004), producing Parse error: Expected ')'. Decimal workarounds exist but hex is important for bit manipulation constants.
Tier 1: Foundation
1. qz-http — HTTP/1.1 + HTTP/2 Server ✅ SHIPPED — deployed on mattkelly.io
What it proves: Quartz can build real networked services from raw TCP.
Implementation:
std/net/http_server.qz(3821 LOC, HTTP/1.1 router + middleware),std/net/http2.qz(765 LOC, HPACK + Huffman). Demo atexamples/qz-http/main.qzexercises the full router/middleware/route_param API. HTTP/2 variant deployed on mattkelly.io VPS (2 vCPU / 8 GB / x86_64 linux-gnu). Seedocs/PLATFORMS.mdfor cross-compilation notes.
| Aspect | Detail |
|---|---|
| Scope | Minimal, zero-dependency HTTP/1.1 server with routing, middleware, static file serving |
| API Style | Go-style single handler: Fn(Request): Response |
| Middleware | Rack-style composition via pipeline: handler | with_logging | with_cors |
| Concurrency | Thread-per-connection (v1), event-loop upgrade path (v2) |
| Benchmark | Hello-world req/s vs Go net/http, Node.js http, Python http.server |
| Depends on | Existing networking stack (std/net/tcp.qz), string handling |
| Enables | Demo platform for all other projects, WASM canvas site, dogfooding vision |
Core API:
# Minimal server — trailing block syntax
http_serve(8080) do req: Request ->
Response { status: 200, body: "Hello, Quartz!" }
end
# With router
var r = router_new()
r.get("/") do req -> text_response("Hello!") end
r.get("/users/:id") do req -> json_response(get_user(req.param("id"))) end
# Middleware composition via pipeline
http_serve(8080, r.handler() | with_logging | with_cors | with_auth)
Why this is a flex: Pipeline middleware is better than Go (no pipelines), better than Rack (no class-per-middleware), and better than Express (explicit app.use() ordering). One function type, composed left-to-right.
2. qz-json — JSON Parser/Serializer
What it proves: Quartz’s enums, pattern matching, and recursive data structures are production-ready.
| Aspect | Detail |
|---|---|
| Scope | Full JSON spec (RFC 8259) — parse, serialize, pretty-print, path queries |
| Research | Rust serde_json, Go encoding/json, simdjson (for perf ideas), jq (for query API) |
| API targets | json_parse(str) → Result<JsonValue, JsonError>, json_stringify(val), val["key"] access |
| Benchmark | Parse speed vs Go encoding/json, Python json on a 1MB file |
| Depends on | String handling, Result<T, E>, enums with payloads |
| Enables | Config files for other demos, HTTP request/response bodies, qz-db storage format |
NOTE: We already have
std/json.qzandstd/json/parser.qz— this builds on that foundation, hardening it to full spec compliance and benchmarkable performance.
3. qz-arg — CLI Argument Parser
What it proves: Quartz’s struct ergonomics and builder patterns make declarative APIs elegant.
| Aspect | Detail |
|---|---|
| Scope | Declarative CLI parsing with subcommands, flags, help generation, error messages |
| Research | Rust clap, Go cobra/flag, Python argparse/click, Zig std.process |
| API targets | Builder pattern, auto-generated --help, type-safe flag values |
| Depends on | String handling, Vec<String>, struct builders |
| Enables | Every subsequent demo uses this for CLI interface |
NOTE: We already have
std/argparse.qz(TS.10) — this either hardens it or replaces it with a world-class implementation modeled after the best in class.
Tier 2: Systems Flexes
4. qz-alloc — Custom Allocator Benchmark Suite
What it proves: Quartz has language-level memory control that garbage-collected languages can’t touch.
| Aspect | Detail |
|---|---|
| Scope | Arena vs pool vs slab vs malloc benchmarks with visualization |
| Research | jemalloc, mimalloc, Zig allocator interface, Rust GlobalAlloc |
| Benchmark | Allocation throughput, fragmentation, cache behavior across patterns |
| Depends on | Existing arena/pool/slab allocators, timing intrinsics |
Why this is a flex: We already HAVE arenas, pools, and slab allocators implemented in Quartz. Nobody else has this at the language level in a 47-day-old language.
5. qz-hash — SHA-256 / BLAKE3
What it proves: Quartz’s bitwise ops and tight loops compete with C.
| Aspect | Detail |
|---|---|
| Scope | Pure Quartz SHA-256 and BLAKE3. Hash files from CLI, streaming API |
| Research | Reference C implementations, Rust sha2/blake3 crates |
| API targets | sha256(data) → String, sha256_file(path) → String, streaming Hasher struct |
| Benchmark | MB/s vs C openssl, Rust sha2. Within 2x of C = headline material |
| Depends on | Fixed-width integers (U32, U64), bitwise ops, byte-level buffer access |
6. qz-regex — Regex Engine
What it proves: Quartz can build complex state machines with clean, readable code.
| Aspect | Detail |
|---|---|
| Scope | NFA-based regex matcher via Thompson construction. Character classes, quantifiers, groups |
| Research | Russ Cox’s RE2 articles, Thompson NFA, Pike VM, Rust regex crate architecture |
| API targets | regex_compile(pattern) → Regex, regex.match(str) → Option<Match> |
| Benchmark | Pathological cases (where backtracking engines explode), basic throughput |
| Depends on | Enums, dynamic arrays, state machine patterns |
7. qz-compress — LZ77/Deflate Compressor
What it proves: Byte-level manipulation, ring buffers, and performance-critical code.
| Aspect | Detail |
|---|---|
| Scope | LZ77 sliding window compression + Huffman coding. Compress/decompress files |
| Research | RFC 1951 (DEFLATE), zlib, LZ4, zstd (for API ideas) |
| API targets | compress(data) → Vec<U8>, decompress(data) → Vec<U8>, file CLI |
| Benchmark | Compression ratio and speed vs gzip, LZ4 |
| Depends on | Ring buffers (already have), bit-level I/O, fixed-width integers |
Why this is a flex: We already have ring buffers in the compiler. Now we use them for what they were designed for.
Tier 3: Developer Tools
8. qz-fmt — Code Formatter
What it proves: Quartz is mature enough to have its own tooling ecosystem.
| Aspect | Detail |
|---|---|
| Scope | Auto-format Quartz source code. Parse → AST → pretty-print |
| Research | gofmt, rustfmt, Prettier, Black (Python). Philosophy: one true style, no config |
| API targets | quartz fmt file.qz, quartz fmt --check, stdin/stdout mode |
| Dogfood | Format the compiler source itself. Diff should be zero after initial formatting |
| Depends on | Parser (reuse compiler’s parser), AST traversal |
9. qz-grep — Fast File Search
What it proves: Quartz can build the kind of tool ripgrep is — fast, practical, real.
| Aspect | Detail |
|---|---|
| Scope | Recursive grep with regex, colored output, .gitignore awareness, file type filters |
| Research | ripgrep (architecture blog posts), grep, ag (The Silver Searcher) |
| API targets | qz-grep PATTERN [PATH], --ignore, --type, --count, colored output |
| Benchmark | Search speed vs grep on a large codebase |
| Depends on | qz-regex (or built-in regex), filesystem traversal, qz-arg |
10. qz-test — Test Runner Framework
What it proves: Quartz’s macro system and reflection capabilities enable meta-programming.
| Aspect | Detail |
|---|---|
| Scope | Discover test functions, run them, report pass/fail with colors, assertion helpers |
| Research | Go testing, Rust #[test], Jest, pytest, Zig test blocks |
| API targets | @test attribute on functions, assert_eq(a, b), assert(cond), CLI runner |
| Dogfood | Run on the compiler’s own test utilities |
| Depends on | Macro system (TS.20-21), qz-arg, colored terminal output |
Tier 4: Jaw-Droppers
11. qz-db — Embedded Key-Value Store
What it proves: Quartz can build infrastructure — not just tools that use infrastructure.
| Aspect | Detail |
|---|---|
| Scope | B-tree or LSM-tree backed KV store. put, get, delete, scan. File-backed with WAL |
| Research | LevelDB, RocksDB (LSM), SQLite (B-tree), BoltDB (Go), sled (Rust) |
| API targets | db_open(path) → DB, db.put(key, value), db.get(key) → Option<String> |
| Benchmark | Ops/sec vs SQLite, BoltDB on sequential and random workloads |
| Depends on | File I/O, qz-json (for metadata), binary serialization |
12. qz-render — Software Rasterizer / Ray Tracer
What it proves: Quartz handles compute-heavy math, SIMD, and produces visual output.
| Aspect | Detail |
|---|---|
| Scope | Ray tracer outputting PPM/BMP images. Spheres, planes, reflections, shadows |
| Research | ”Ray Tracing in One Weekend” (Peter Shirley), PBRT, smallpt |
| API targets | Scene description → rendered image. CLI: qz-render scene.json -o output.ppm |
| Benchmark | Render time vs C ray tracer. SIMD acceleration via F32x4 |
| Extends to | WASM + Canvas: Compile to WASM, render directly to HTML5 Canvas in browser. Real-time 3D in the browser, rendered by Quartz — not the DOM, not WebGL shaders, but our own rasterizer spitting pixels into a canvas. Like a game engine. |
| Depends on | F32/F64 math, SIMD (F32x4), qz-json (scene files) |
THIS IS THE CENTERPIECE OF THE WASM VISION. The Quartz marketing site could ultimately be rendered by this engine running in the browser via WebAssembly — no HTML, no CSS, no DOM. Just Quartz code drawing pixels to a canvas. The most unhinged flex possible.
13. qz-lisp — Lisp Interpreter
What it proves: Meta-circular flex — a language written in a language that wrote itself.
| Aspect | Detail |
|---|---|
| Scope | S-expression parser → tree-walking interpreter. Closures, tail-call optimization, GC |
| Research | SICP, MAL (Make-A-Lisp), Peter Norvig’s lis.py |
| API targets | REPL mode + file execution. (define fib (lambda (n) ...)) |
| Depends on | String parsing, recursive data structures, enums |
14. qz-wasm — WebAssembly Compiler Backend
What it proves: Quartz can target the browser — compile code → WASM → run in browser.
| Aspect | Detail |
|---|---|
| Scope | Compile a simple expression language (or subset of Quartz) → .wasm binary |
| Research | WASM spec, wat2wasm, Binaryen, AssemblyScript compiler |
| API targets | qz-wasm compile input.qz -o output.wasm |
| Extends to | Combined with qz-http, serve a web page that runs Quartz-compiled WASM |
| Depends on | Binary serialization, WASM binary format knowledge |
15. qz-shell — Interactive Shell / REPL
What it proves: Quartz is a real systems language — you can implement an OS-level tool.
| Aspect | Detail |
|---|---|
| Scope | Line editing, history, pipes, job control. Parse commands, fork/exec, wait |
| Research | bash source, fish shell, Oil shell, Rust nushell |
| API targets | Interactive REPL with prompt, history (up/down), tab completion |
| Depends on | FFI (fork, exec, waitpid, pipe, dup2), terminal I/O, qz-arg |
Implementation Order
| Phase | Projects | Rationale |
|---|---|---|
| Sprint 1 | qz-http, qz-json | Foundation — demo platform + data format. Enables everything |
| Sprint 2 | qz-arg, qz-hash | CLI infrastructure + first systems flex with benchmarks |
| Sprint 3 | qz-alloc, qz-fmt | Memory flex + first dev tool (dogfood on compiler) |
| Sprint 4 | qz-regex, qz-grep | State machines + practical tool with benchmarks |
| Sprint 5 | qz-test, qz-compress | Meta-programming + byte-level manipulation |
| Sprint 6 | qz-render, qz-lisp | Jaw-dropper demos — visual output + meta-circular |
| Sprint 7 | qz-db, qz-wasm, qz-shell | Infrastructure + browser target + OS-level tool |
| Sprint 8 | Canvas Site | WASM renderer in browser — the ultimate demo |
Demo Success Metrics
| Metric | Target |
|---|---|
| All 15 demos compile and run | ✅ |
| Each demo has benchmarks vs comparable impl | Numbers published |
qz-hash within 2x of C | Headline benchmark |
qz-http handles 10K+ req/s | Competitive with Go |
qz-render produces 1080p image | Visual proof |
| WASM canvas renders in browser | The ultimate flex |
| README showcases all demos | First impression |
| Style guide reflects real idioms | Authentic, not prescriptive |
Known Bugs
| Bug | Impact | Status |
|---|---|---|
value.to_s() breaks if module named value | ✅ FIXED (Apr 7) — Resolver checks param names before module rewrite. 3 tests. Follow-up: full scope tracking (Tier 3). | |
✅ FIXED (Apr 7) — New sb_append_byte intrinsic + 6 lexer paths fixed. 68→0 corrupted strings. | ||
✅ FIXED (Apr 7) — ps_skip_newlines() after ), :. 6 tests. | ||
| ✅ FIXED — QZ0550, resolver cycle detection. | ||
✅ FIXED — Explicit init_intrinsic_registry() before pipeline (commit 1d23b1dd). | ||
"prefix " + "#{await h}" → LLVM rejects IR with “Instruction does not dominate all uses” | ✅ FIXED (Apr 11) — 3440903f binary-op handler spills LHS to generator frame slot when RHS contains await or suspending intrinsic (recv, sched_sleep, etc). | |
while count < await h segfaults on second iteration: pthread_join + free + re-read = UAF | ✅ FIXED (Apr 11) — e2f829fd writes state=-1 after join, drops free; subsequent awaits dispatch through cached-future path. | |
x -> x + await h emitted load ptr %h against undefined alloca | ✅ FIXED (Apr 11) — 73a14d56 NODE_AWAIT / NODE_GO / NODE_ASYNC_CALL cases walk left child so free vars are captured. | |
@__qz_sched[12] (iomap base) is zero when the scheduler never initialized, but codegen indexed it unconditionally | ✅ FIXED (Apr 11) — Added null guard on .iomap.i in both try_send and channel_close codegen. Unlocked concurrency_spec (57 tests) and the select-send smoke path. | |
sched_shutdown freed the iomap but left @__qz_sched[12] pointing at freed memory; channel_close afterwards chased the dangling pointer | ✅ FIXED (Apr 11) — sched_shutdown now stores 0 to slot 12 after freeing. Unlocked channel_result_spec (6 tests). | |
spawn + await hung forever: @__qz_completion_map[3] (the mutex proxy for “scheduler active”) was freed but not nulled, so the await walked into the dangling completion-pipe path on a dead scheduler | ✅ FIXED (Apr 11) — 73dd0479 zeros slots 0–3 of both @__qz_completion_map and @__qz_task_locals after their respective frees in __qz_sched_shutdown. Regression test rs_spawn_after_shutdown in async_spill_regression_spec. | |
channel_new(10) actually held 16 items; channel_pressure(ch) returned 31 (5/16) instead of 50 (5/10); channel_remaining reported 11 instead of 5 | ✅ FIXED (Apr 11) — 020e8194 removes the bit-smearing round-up in channel_new and switches ring-buffer indexing from and count, (cap-1) to urem count, cap at all 8 send/recv/try_send/try_recv sites. Unlocked backpressure_spec (7/7 passing, was 1/7). |
| Int-key map iteration (3 chained bugs) | Silent wrong results for | ✅ FIXED (Apr 12) — for k in map over Map<Int,V>524abbfc: (1) TC ptype→base normalization in NODE_FOR.extra, (2) MIR map_key_types tracking threads elem_type to codegen, (3) intmap_keys/values rewritten to use occupancy bitmap instead of delegating to hashmap_keys which treated key values 0/1 as sentinels. |
| For-in loop variable typing | Loop var always typed as Int, even for Map<String,V> keys | ✅ FIXED (Apr 12) — 0b4e6fc5: TC binds loop var with container’s key/element type derived from ptype. |
| Float map keys accepted by tc_type_is_hashable | F64/F32 keys allowed despite NaN!=NaN and -0.0==+0.0 breaking hash invariants | ✅ FIXED (Apr 12) — 85152035: removed Float types from hashable list; tc_struct_is_hashable now walks field types. |
| MIR_INDEX float zext codegen | | ✅ FIXED (Apr 12) — v[i] on Veczext float to i648435d663: 4 MIR_INDEX* handlers now branch on is_float (fpext+bitcast for read, bitcast+fptrunc for write). |
| FFI declaration dedup missed define | Functions both defined and declared as extern emitted duplicate symbols, rejected by LLVM | ✅ FIXED (Apr 12) — e8efa41a: dedup scans for both declare and define in context window. |
| Request.params/query/context typed as Int | UFCS | ✅ FIXED (Apr 12) — form.get() dispatched to wrong function, SIGSEGVba4e4bd2: retyped to Map<String, String>. |
| Unqualified variant payload types wrong | | ✅ FIXED (Apr 12) — Ok(data) in match resolved against first-registered enum (builtin Result<T,E>) instead of the subject’s actual type1c68b67c: match handler derives enum name from subject expression’s function return annotation. |
| Linter SIGSEGV on all lint invocations | | ✅ FIXED (Apr 12) — quartz lint crashed on every file — 6 spec files, 58 tests blocked885f80c9: Three .size field accesses on Int-typed Vec handles read capacity instead of size (offset 0 vs 8). Fixed to vec_size(). Also fixed parser consuming significant newline after ) which silently disabled T2 lint rules. |
| Extern declaration dedup false positive | | ✅ FIXED (Apr 12) — extern "C" def abs(...) missing from IR when test string constants contained declare i64 @abs(i64)646c4c07: Dedup searched 80 bytes before @name( for “declare”/“define”, false-positived on string constants. Fix: find line start, check line begins with declare/define. |
| MIR guard node check off-by-one | | ✅ FIXED (Apr 12) — arm_guard_node > 0 would skip guard at AST handle 04c1ed05d: Changed to >= 0. |
Pre-existing spec failures still open (updated Apr 16 evening):
| Spec | Mode | Impact | Investigation |
|---|---|---|---|
json_spec.qz | ✅ FIXED (Apr 16) — Sprint 1: Object(fields: Int) payload erased the Map<String, JsonValue> type at destructure. keys[i] from map_keys(Int-typed receiver) typed as Int → map_get(fields, key) dispatched to intmap_get on a hashmap → SIGSEGV. Stdlib workaround: helper _json_key_to_string casts the i64 slot back to String. The “right” fix (typed enum payloads Vec<JsonValue>/Map<String, JsonValue>) is blocked on OPEN-UFCS-HASH-SENSITIVITY (see new entry below). 18/18 json_spec passes. | ||
separate_compilation_spec.qz | 5/6 FAIL:link | Separate compilation linker gaps — missing symbols | Infrastructure gap, not a quick fix. |
file_helpers_spec.qz | ✅ FIXED Apr 19, 2026 (UFCS-EXTERN-SHADOW). Not a runtime bug and not a typecheck bug — the typechecker correctly rewrote fh.close() → FileHandle$close(fh), and MIR preserved it. The collision was in codegen: cg_extern_var_index (codegen_util.qz:424-443) has a $-strip fallback for cross-module extern calls (ffi_mod$abs → abs) that was matching FileHandle$close → close → the POSIX extern "C" def close(fd: CInt) from ffi/socket.qz. Prior fix (5abfe9b5) added a __-prefix guard for compiler-generated names; this extends that with a cg_find_function(state, name) != 0 guard so any fully-prefixed user function (including all impl methods) shadows the fallback. Three-line fix; the whole class of impl-method-shadowed-by-libc-extern bugs dies with it. 8/8 file_helpers_spec green, fixpoint verified at 2142 functions. | ||
http2_frame_spec.qz | ✅ FIXED Apr 19, 2026 (d2680e3b). Not a missing library — the QSpec runner’s OpenSSL auto-detection only matched three hardcoded symbol names (SSL_CTX_new, SSL_connect, OPENSSL_init_ssl) and missed ERR_clear_error / SSL_read / etc. that HTTP/2 uses. Broadened to prefix checks (@SSL_, @ERR_, @OPENSSL_) across Quakefile qspec/qspec_file runners and std/qspec/subprocess.qz. 11/11 green. | ||
route_groups_spec.qz | ✅ FIXED Apr 19, 2026 (d2680e3b). Same root cause as http2_frame_spec. 6/6 green. | ||
semaphore_spec.qz | Timeout in PTY | Scheduler-based test hangs in Claude Code PTY context | Works in native terminal (needs verification). |
tls_async_spec.qz | 0/6 passing | TLS networking integration (depends on OpenSSL + real sockets); not a compiler bug | Pre-existing — reproduces on pre-fix golden |
actor_spec.qz | Linux rebuild only | __Future_<pointer>$name non-determinism truncates IR when compiler is rebuilt on Linux | Happens only when rebuilding the compiler on Linux; the 38c88277-era golden is deterministic and passes 21/21. See docs/archive/HANDOFF_SESSION_4.md §“What I learned” point 4. |
%push in socket$pipe_create) | ~~Warm-cache direct `quartz | llcproduces invalid IR with undefined%push` local~~ | FIXED Apr 19 2026. Three-part fix in self-hosted/quartz.qz + self-hosted/shared/dep_graph.qz: (1) replace “any-fragment-exists” coarse gate with per-module fragment check — unchanged modules whose short-name .ll fragment is missing are added to changed_modules before depgraph_invalidate_with_cutoff; (2) normalize recompile_set to short names after invalidation so downstream consumers line up with function-mangling / fragment-filename convention; (3) TC-skip gate now resolves the owning module via depgraph_name_for_ast_store(entry.ast_store) rather than parsing the function-name prefix, correctly handling impl methods whose prefix is a struct name (Progress$spinner_only) rather than a module name. 4-compile repro reports 0 bad %push loads across 6 consecutive rounds; cached vs --no-cache compiled binaries produce identical test output; fixpoint verified at 2142 functions. See docs/handoff/pipe-create-push-cache-bug.md for the diagnosis writeup. Follow-up: audit the 10+ --no-cache sites in Quakefile.qz now that the cache path is sound. |
Apr 13 PM: triage tail audit — most entries below were stale.
The “Apr 12 triage tail” section that follows lists 6 single-test failures and 4 systemic issues. An audit on Apr 13 (commit forthcoming) found that 9 of the 10 entries were already fixed by intervening compiler work, and one was actually a typo in the spec source. Specifically:
Result$unwrapextracts wrong field (line 475): ✅ FIXED —r.unwrap()onResult::Ok(55)correctly returns 55. Verified directly.option_narrowing_spec7/7.- Parser
defer count += 1(line 476): ✅ FIXED —cross_defer_safety_spec8/8 with QUARTZ_COMPILER set. - Parser
extern "C" defwith body (line 477): ✅ FIXED —ffi_spec15/15,extern_def_spec20/20. never_type_spec“never type in let binding” (line 483): ✅ FIXED — 11/11.stress_type_system_spec“enum variant with named payload” (line 484): ✅ FIXED — 43/43.error_codes_spec“QZ1215 triple-quoted indentation” (line 485): ✅ FIXED — 35/35.arenas_advanced_spec“Arena$drop QZ0551” (line 486): ✅ FIXED — 28/28.stress_pattern_matching_spec“generic unwrap function” (line 488): ✅ FIXED.stress_pattern_matching_spec“match on Option” (line 487): ✅ FIXED via test typo correction — the test source had MyOption<T>with variantsSome/Nonebut the match arms referencedMySome/MyNone. The compiler emitted bad IR (which llc rejected) instead of producing a typecheck error. The typo is now consistent on both sides; the underlying compiler bug (unknown enum variants in match should be QZ02xx, not bad IR) is real but separate and not yet filed.
The .size field access on Int-typed Vec handles audit (line 474) is the only systemic issue I did NOT verify in this audit. If new sites have crept in since Apr 12, the bug is still present. Worth a fresh grep before claiming closed.
Remaining spec failures after Apr 12 triage sprint
After the Apr 12 triage that fixed ~165 tests across 17 spec files, the following are known-failing and deliberately left open. Each has a root cause identified, so none are mysteries — just honest work that needs a real session.
Systemic issues discovered, not yet fixed (world-class fix required):
| Issue | Impact | Root cause | Fix shape |
|---|---|---|---|
.size field access on Int-typed Vec handles reads capacity, not size | .size on TYPE_INT values with no struct name. | RESOLVED (Apr 16, 2026). When .size falls through to the “no struct name” path and object_type is TYPE_INT, the typechecker now rewrites to vec_size() call. Verified: Int-typed Vec handle returns correct size (3, not capacity). Fixpoint verified. | |
Result$unwrap extracts wrong field for Result::Ok payloads | r.unwrap() on Result::Ok(55) returns 0 instead of 55. | option_narrowing_spec (2/7 tests fail). | RESOLVED (Apr 16, 2026 retest). option_narrowing_spec 7/7 green. |
defer only accepts expressions, not statements | cross_defer_safety_spec. | ps_parse_defer calls ps_parse_expr. | RESOLVED (Apr 16, 2026 retest). cross_defer_safety_spec 8/8 green. |
extern "C" def with body at top level | ffi_spec and extern_def_spec each lose 1 test. | ps_parse_extern_fn stops at ). | RESOLVED (Apr 16, 2026 retest). ffi_spec 15/15, extern_def_spec 20/20 green. |
Single-test failures (deep codegen bugs):
| Spec | Test | Symptom | Category |
|---|---|---|---|
never_type_spec | RESOLVED (Apr 16, 2026 retest). 11/11 green. | ||
stress_type_system_spec | RESOLVED (Apr 16, 2026 retest). 43/43 green. | ||
error_codes_spec | RESOLVED (Apr 16, 2026 retest). 35/35 green. | ||
arenas_advanced_spec | RESOLVED (Apr 16, 2026 retest). 28/28 green. | ||
stress_pattern_matching_spec | llc failed | RESOLVED (Apr 16, 2026 retest). 28/28 green with QUARTZ_COMPILER set. | |
stress_pattern_matching_spec | RESOLVED (Apr 16, 2026 retest). Was stale roadmap data. |
.size wart — FIXED at compiler level (Apr 16, 2026):
Not yet audited. RESOLVED: Typechecker now auto-rewrites .size on Int-typed values (no struct name) to vec_size(). Previously-broken sites (lint.qz, codegen.qz, toml_spec, unicode specs, extern_def_spec) were manually fixed; the compiler-level fix prevents future occurrences.
Missing stdlib functions (feature gap):
RESOLVED (Apr 16, 2026). collection_stubs_spec tests reversed, sorted, unique, flatten, enumerate, zip, partition, group_by. None exist in the stdlib.reversed and sorted added to prelude (using vec_clone + vec_reverse/vec_sort builtins). The other 6 functions already existed. 21/21 green.
Move semantics enforcement (S2.5 holes, 3 specs):
RESOLVED (Apr 16, 2026). All 6 holes (#4 while-loop, #5 list-comp, #8 return-position, #10 match-subject, #11 NLL-interaction, #12 transitive-borrow) are wired up and enforced. s25_low_holes_spec (10/13 failing), s25_safety_holes_spec (10/13 failing).s25_low_holes_spec 15/15, s25_safety_holes_spec 13/13 — all green. The handoff’s claim “infrastructure exists, checks missing” was outdated; the wiring was already in place.
WASM backend gaps (3 specs):
wasm_core_spec: exit codes, variables, functions, control flow, recursion — core backend missingwasm_data_spec: closures, bitwise ops, string ops (str_concat, to_str, str_eq, str_starts_with, str_from_char, str_is_empty), vec opswasm_extended_spec: char predicates (is_alnum, is_whitespace, is_upper/lower), str_trim, str_downcase/upcase, str_repeat, str_cmp
This is TGT.3 Direct WASM Backend — 10 phases, not yet started per user memory.
Generic Iterator bounded dispatch (generic_ufcs_dispatch_spec, 8/8 failing 8/8 green):
RESOLVED (Apr 16, 2026). The underlying compiler feature works correctly — sum, count, break, continue, multiple Iterator types, empty iterator, and adapter chains all pass. The <T: Iterator> bounded generic functions with for-in body emit invalid code.enddef parse error was a test infrastructure bug: Quartz triple-quoted strings strip trailing newlines, so gud_counter_src() + """...""" concatenated end directly with def on the same line. Fix: append "\n" to helper functions. 8/8 tests green.
Module path resolution (2 specs):
modules_spec:needsRESOLVED (Apr 16, 2026). The spec requiresspec/qspec/fixtureson include path.-I spec/qspec/fixturesfor direct compilation andQUARTZ_FIXTURESenv var for subprocess tests. With those set, 35/35 pass (one test had a stale expected error string: “Undefined function” → “has no function”). Not a compiler bug.NOT A BUG (Apr 16, 2026). Hierarchicalaccept_worker_spec: multi-level pathstd/net/http_serverdoesn’t resolve.import std/net/http_serverresolves correctly viaresolver.qzstep 1 (base_path + module_name + ".qz"). Compiles and links fine.
Scheduler/async (pre-existing, roadmap items):
UNBLOCKED (Apr 16, 2026 — SEND-RECV-SHADOW fix)backpressure_spec: BLOCKED — send/recv shadow bugUNBLOCKED (Apr 16, 2026)semaphore_spec: BLOCKED — sameUNBLOCKED (Apr 16, 2026)graceful_shutdown_spec: BLOCKED — sameFIXED (Apr 16 retest) — 9/9 green, no SIGSEGVcompiler_bugs_p30_spec: SIGSEGV in subprocessmap_index_specinternals — FIXED Apr 12
Retested Apr 16 after #13 park/wake fix + 15b cascade-stop fix. compiler_bugs_p30_spec now passes. The other three can’t compile because All three unblocked (Apr 16 — SEND-RECV-SHADOW fix deleted dead externs). All 6 specs now compile.send/recv channel builtins are shadowed.
Other known-failing but deliberate:
RESOLVED (Apr 13, commitufcs_complete_spec—m.delete()causes runtime SIGSEGV06b936d2). The spec was usingm.del(1)(a phantom verb), notm.delete(). Fixed in place; 21/21 tests green including thedeletes via UFCScase. The underlyingmap_deleteintrinsic was always sound — the spec just had a typo from an earlier iteration.
Open compiler / bootstrap issues
These are gaps discovered during the April 2026 recovery and dogfooding sprint. Each one has a clear reproducer and a sketched fix.
Batch A + Batch B sprint summary (Apr 14, 2026)
Nine items from the April 13 overnight-sprint handoff landed across two sessions. Fixpoint 2281 functions byte-identical throughout. Every commit passed quake guard + quake smoke + per-item regression sweep.
| # | Commit | Item | Outcome |
|---|---|---|---|
| A1 | a16b8c19 | macOS mimalloc gate | Fixed (one-line link-flag change; diagnosis corrected from IOAccelerator to dyld TLS-slot race) |
| A2 | 25032709 | OPTION-CTOR-IN-IMPL-BODY | Already fixed on trunk — locked in with 5 regression tests, root cause was stale .quartz-cache |
| A3 | 94f80b64 | Stale typecheck_registry.qz.bak | Deleted |
| A4 | daaefd88 | quake launcher stderr surfacing | Fixed (Ninja-style sh_buffered helper; llc not found false positive eliminated) |
| A5 | 25df7bf8 | Lambda capture walker intrinsic-name shadow | Fixed (1-line check reorder in mir_collect_captures_walk; first/sum/map-named locals now capture correctly) |
| B1 | 6e46491e | IMPL-TRAIT-SEND-AUTO coinductive solver | Fixed (~30 lines plumbing existing structural walker into tc_check_trait_bound) |
| B2 | b61da0f4 | Scheduler state machine + free audit | Fixed (atomic @__qz_sched_state word, 7 zero-after-frees, 5 missing frees → ~4.5 MB/cycle leak closed, 7-test lifecycle spec) |
| B3 | dbe2bab7 | PSQ-4 Vec element type loss | Already fixed on trunk — locked in with regression spec; adjacent B3-DIRECT-INDEX-FIELD bug filed |
| B4 | 258468f5 | step! in while loop miscompile | 73185be8) |
Follow-ups filed during the sprint: QUAKE-STDERR-AUDIT, POST-RESOLVE-IDENT-ASSERTION, IMPL-NOT-SEND, B3-DIRECT-INDEX-FIELD, B4-UNWRAP-IN-LOOP.
New regression spec files: enum_ctor_in_impl_spec.qz (5), closure_intrinsic_shadow_spec.qz (5), send_auto_trait_spec.qz (5), vec_element_type_spec.qz (4+1 pending), unwrap_in_loop_spec.qz (3+1 pending), sched_lifecycle_spec.qz (7) = 29 new tests + 2 pending markers.
Bootstrap & infrastructure
| Issue | Impact | Investigation |
|---|---|---|
✅ ALL RESOLVED (Apr 12, 2026). All 11 fossils now have real definitions in source: ast_func_is_cconv_c (stub at ast.qz:1489), TYPE_MAP (type_constants.qz:90), NODE_IS_CHECK (node_constants.qz:257), tc_type_is_hashable/tc_struct_is_hashable/tc_hashable_rejection_reason/tc_struct_hashable_rejection (full impls in typecheck_helpers.qz), mir_register_poll_callee/mir_func_set_cconv_c/mir_func_is_cconv_c (mir.qz), mir_block_set_switch/mir_block_get_switch_* (mir.qz), cg_emit_struct_hash_eq_functions/cg_emit_custom_map_functions (codegen_runtime.qz). Source-only build verified: gen1 (1,679,869 lines) == gen2 byte-identical. Standing patch set in .quartz-guard/ retired. | ||
send/recv channel builtins shadowed by POSIX socket externs | extern "C" def send/recv in std/ffi/socket.qz shadows 2-arg channel builtins. | RESOLVED (Apr 16, 2026). Deleted the dead extern "C" def send/recv declarations entirely — zero callers, socket_send/socket_recv wrappers already use sendto/recvfrom. Unblocked 6+ specs: concurrency_spec, channel_handoff_spec, unbounded_channel_spec, backpressure_spec, semaphore_spec, graceful_shutdown_spec. Fixpoint verified (1989 functions). |
| Cache-pattern miscompile in 54eb4965 onward | The mangle/suffix caching pattern (as_int(string) → intmap → as_string(int) round-trip used in resolver.qz and typecheck_registry.qz) miscompiles when the cache grows large (~9.7 GB into a self-compile): cross-module struct type lookups silently fail. Recovery workaround was to revert the cache calls to raw "#{prefix}$#{suffix}" interpolation. | PARTIAL (Sprint 1, Apr 16): root cause confirmed and first sub-bug fixed (253b6edb) — __map_get_raw was missing from the str2-propagation guard in mir_lower_expr_handlers.qz:2261, so Int-keyed __map_get_raw(intmap_handle, int_key) silently routed to __hashmap_get_sentinel codegen, calling qz_str_hash on the Int key. Two more sub-bugs block re-enabling the dedup cache: (a) __intmap_get_raw codegen missing — cg_intrinsic_map.qz:116 maps __map_get_raw → intmap_get for Int-keyed maps, but intmap_get returns Option<V>, not a raw sentinel. The hashmap path has __hashmap_get_sentinel (raw value or 0); the intmap path needs the same. (b) compiler hangs on match map_get(...)/Some/None do...end pattern in mangle — switching to the Option-wrapped path triggered an infinite loop in gen1 when self-compiling. Likely a typechecker recursion on Optionstring_intern.qz mangle/mangle_int using match map_get(...); gen1 hangs at 100% CPU with no output. Next steps: (1) implement __intmap_get_raw codegen (~150-line variant of intmap_get returning raw value or 0). (2) After (a), the cache can use __map_get_raw directly without the Option/match wrap that triggered the hang. The cache value would be intern_id (small Int, dense, stable — no UAF possible). See string_intern.qz:255-301 for the exact form to restore. ~2 GB peak RSS recovery expected. |
__Future_<pointer>$<name> non-determinism | ✅ VERIFIED FIXED (Apr 12, 2026). All Future symbols now use stable string identifiers (__Future_Counter$__poll$new, __Future_Accumulator$__poll$new, etc.). Two back-to-back compilations of actor_spec.qz produce byte-identical IR. The old pointer-based naming was replaced with function-name-based naming in mir_lower_gen.qz and mir_lower.qz. | |
__qz_sched_shutdown | free()’d without being zeroed, leaving dangling pointers that codegen treats as “scheduler still alive” via ptr != 0 proxy checks. | RESOLVED (Apr 14, 2026 Batch A+B sprint B2). Introduced @__qz_sched_state atomic state word (UNINIT=0, RUNNING=1, STOPPING=2, STOPPED=3) as the authoritative scheduler-lifecycle signal. sched_init CAS-transitions UNINIT→RUNNING, sched_shutdown CAS-transitions RUNNING→STOPPING→UNINIT, and the sched_is_active() intrinsic was rewritten to load atomic seq_cst the state word and compare with RUNNING — replacing the completion_map[3] != 0 pointer-proxy check that broke under free-without-zero. Also closed the remaining unzeroed slots (0 worker_array, 1 global_queue, 6 mutex, 7 condvar, 17 worker_data, @__qz_timer_peers, @__qz_pidfd_set) and — more importantly — added the missing free() calls for priority queue slots 20/24/28 (3×524 KB), priority table slot 32 (524 KB), and task_registry slot 0/3 (2 MB+64 B). Previously every shutdown leaked ~4.5 MB of priority + registry memory. Design follows Go runtime’s scheduler state machine, Tokio’s worker State atomic, and libuv’s loop_alive predicate — all converged on “single atomic state variable, explicit transitions, zero on free” as the pattern to replace pointer-proxy checks. Locked in by spec/qspec/sched_lifecycle_spec.qz (7 tests: init→active→shutdown→inactive, double-init, double-shutdown, shutdown-without-init, re-init cycle). Scheduler regression sweep verified: sched_lifecycle_spec 7/7, sched_idle_hook_spec 5/5, scheduler_spec 4/4, spawn_await_spec 3/3, proc_suspend_spec 3/3, process_async_spec 5/5, colorblind_async_spec 13/13, await_nonasync_spec 6/6, async_channel_spec 6/6. Fixpoint 2281 functions, byte-identical. |
quake guard:source clean-room verification | ✅ quake guard:source implemented (Apr 12). Full clean-room pipeline: gen0→gen1→gen2 fixpoint + 3 smoke tests (brainfuck, expr_eval, style_demo). Not yet wired into CI. | Phase 2 (guard:forward per-PR) and Phase 3 (guard:history weekly sweep) remain. See docs/QUARTZ_GUARD.md. |
self-hosted/bin/quartz segfaults during self-compile on macOS because mimalloc on Apple Silicon crashes during dyld init. | RESOLVED (Apr 14, 2026). Fixed in the Batch A+B sprint by dropping -lmimalloc from the macOS link path in Quakefile.qz:1327 (_mimalloc_flags() returns "" on macOS). Revised root cause per microsoft/mimalloc#343: the crash is a dyld TLS-slot bootstrap race — ImageLoader calls malloc before tpidr_el0 points at a valid TCB, so mi_tls_slot() null-derefs. The earlier “IOAccelerator region walk” theory was wrong; the fix is unchanged. Linux builds still link mimalloc and keep the 49% RSS win. Performance loss on macOS self-compile is acceptable. See docs/PLATFORMS.md allocator note and docs/BOOTSTRAP_RECOVERY.md §“The macOS mimalloc/IOAccelerator collision”. | |
Match arm type mismatch errors with misleading file attribution. | RESOLVED (Apr 13, 2026). The prior session’s investigation went down the wrong path — this was never about multi-clause desugaring. It was two entangled bugs, both of which also affected non-multi-clause code: (1) Scope-shadowing bug in tc_expr_call: a user-level function named f was shadowing parameters named f (e.g. prelude’s result_and_then(f: Fn(T): Result<U,E>) saw the user’s def f(n: Int): Int in the registry instead of the parameter). Fix: short-circuit to indirect call when a NODE_IDENT callee resolves to a scope-bound Fn-typed variable, mirroring the existing FIELD_ACCESS indirect path (typecheck_expr_handlers.qz). A plain def f(n: Int): Int = n * 2 in the user file (no multi-clause at all) triggered the same error. (2) Guard sentinel bug in MIR match lowering (mir_lower_expr_handlers.qz:3196): if arm_guard_node >= 0 used >= 0 instead of > 0. Since node 0 is the AST null sentinel, every guardless enum/value-pattern arm got a phantom guard that lowered to 0 (false), making the arm unreachable. That’s why fib(0) and fib(1) recursed into the default case and blew the stack. Both bugs landed pre-existing the file-attribution regression. With both fixed, examples/expr_eval.qz runs 22/22 end-to-end; smoke baseline regenerated. Separately, the file-attribution bug (the user-file-with-prelude-line-numbers thing) was also fixed via depgraph_path_for_ast_store lookup. | |
map_new() inside a stdlib function emits a call without an @hashmap_new/@intmap_new declaration, llc fails | use of undefined value '@hashmap_new' | RESOLVED (Apr 13, 2026). Misdiagnosed in the handoff — the bug was NOT in codegen.qz FFI emission. The call i64 @hashmap_new() came from a separate broken group_by builtin intrinsic in cg_intrinsic_collection.qz, not from the user’s map_new(). That intrinsic was internally inconsistent: it called @hashmap_new() to create the map but then used @intmap_has / @intmap_set / @intmap_get to operate on it — and it always used hashmap regardless of key type. Because it was registered as a TC builtin, it shadowed any user-space def group_by in prelude. Fix: deleted the intrinsic entirely (237-line handler block in cg_intrinsic_collection.qz + entries in intrinsic_registry.qz + typecheck_builtins.qz). The user-space def group_by in std/prelude.qz now handles it correctly via map_new() (which dispatches to @intmap_new for Int keys). collection_stubs_spec now runs 21/21 green including the previously it_pending group_by test. |
Some(v) / None inside trait impl method body emits %Some undef / %None undef IR | return None or return Some(v) inside a trait impl method silently compiled through the typechecker and produced LLVM IR with %None undef / %Some undef references. llc rejected the IR with “use of undefined value”, surfacing as a confusing build error. The workaround was to use qualified forms Option::None / Option::Some(v) or to import traits first. | RESOLVED / NOT REPRODUCIBLE (Apr 14, 2026). Regression hunt on current trunk: the bug does not reproduce after a clean .quartz-cache rebuild. All four originally-broken forms (return None, return Some(v), same with @field shorthand, same with import traits) now behave correctly — bare forms are typecheck errors (QZ0403/QZ0401), qualified forms compile and run. The earlier “silent undef” behavior observed during the impl Trait sprint was caused by stale .quartz-cache state from a previous session, not a current compiler defect. Most likely fixed indirectly by commit 39a984ca (UNKNOWN-VARIANT-MATCH variant validation) and/or commit 1c68b67c (unqualified variant payload type fix), both landed April 13. Locked in via regression spec spec/qspec/enum_ctor_in_impl_spec.qz with 5 tests. A follow-up guardrail — post-resolve NODE_IDENT assertion promoting silent-undef to a hard internal-compiler-error — is filed as POST-RESOLVE-IDENT-ASSERTION below for future consideration but deliberately not implemented now (no failing test to justify speculative infrastructure per Directive 5). |
NODE_IDENT that reaches MIR lowering without a resolved binding to a hard internal-compiler-error. | RESOLVED (Apr 14, 2026 Batch C sprint C4). Implementation narrowed from the original plan: the guardrail fires only for PascalCase identifiers, not all unresolved NODE_IDENTs. Rationale: during development the unconditional check false-positived 61 times in a single self-compile on lowercase names like v, e, idx — match pattern bindings and closure captures that have legitimate late-bound resolution paths in codegen (not MIR lowering). Quartz convention reserves PascalCase for types, enums, and variant constructors; any bare PascalCase ident that fails local-var / function / arity-mangled / global lookups at MIR entry is almost certainly an unresolved enum variant leaking past a resolver scope-push omission (the OPTION-CTOR-IN-IMPL-BODY class). Lowercase names are deliberately excluded from the check. Implementation: new helper mir_ident_has_global_resolution in mir_lower.qz checks mir_ctx_is_global + mir_find_global_name fallback; new helper mir_ident_is_pascal_case gates the check by first-character case; the NODE_IDENT branch in mir_lower.qz:1279 fires QZ9501 and exit(1) on match with a filename/line/col + remediation hint. 5 canary tests in spec/qspec/post_resolve_assertion_spec.qz — all common PascalCase patterns (qualified/unqualified variants, bare Some/None in impl bodies, struct constructors, user enum variants) compile cleanly. The assertion is a prophylactic guardrail: it has no direct trigger on current trunk because all known resolver holes have been patched, but any FUTURE resolver regression that leaks a PascalCase ident will fail loudly at MIR entry instead of producing %<name> undef IR that llc rejects with cryptic “use of undefined value” errors. Fixpoint verified at 2285 functions; B-sprint regression sweep (16 specs) + post_resolve_assertion_spec all green. Design reference: rustc’s Res::Err invariant — MIR lowering refuses to lower unresolved names. | |
2>/dev/null sites in Quakefile.qz beyond the A4-fixed build/link chain in tools/quake.qz. | RESOLVED (Apr 14, 2026 Batch C sprint C1). Migrated 13 build/link sites in Quakefile.qz from sh("... 2>/dev/null") / sh("... 2>>tmp/build.err") / system("... 2>/dev/null") to sh_buffered(cmd, step_name) with explicit fail() wrappers. Sites covered: Gen1 build (llc-gen1 / link-gen1), guard task (llc-guard-gen1 / link-guard-gen1), guard:source task (llc-source-gen1 / link-source-gen1), build_compiler in-live branch (opt-build / llc-build-opt / llc-build / link-build-release / link-build-debug), and _compile_qz_tool helper (compile-tool / llc-tool / link-tool). Each site now surfaces real tool stderr on failure via the sh_buffered red-header + captured-stderr dump — success remains silent, so Live UI cursor tracking is unaffected. Fixpoint verified at 2281 functions; smoke 4/4 + 22/22 green. Remaining sites (qspec runner, fuzz task, smoke task test-runs) intentionally untouched — those are non-compile-critical paths where the current sh_maybe/system forms are appropriate. | |
vec[i].field access (no intermediate var binding) produces QZ0301: Unknown struct: Struct, Int when the Vec element is a user struct. | RESOLVED (Apr 14, 2026 Batch C sprint C2). Root cause: tc_type_name_full in typecheck.qz:2310 was rendering Vec ptypes as Vec<Struct, Int> by treating ptype_arg2 as a second type id. But for TYPE_VEC, arg2 holds the struct_idx metadata (used to deduplicate Vec<Point> vs Vec<Triple> in the ptype table — see typecheck.qz:1144, 1172), not a type id. Rendering Vec<VA> with struct_idx=1 therefore produced "Vec<Struct, Int>" — bare "Struct" from tc_type_name(TYPE_STRUCT) plus "Int" from coercing struct_idx=1 into tc_type_name(TYPE_INT). That corrupted annotation then propagated into var va: Vec<VA> = vec_new() bindings via the init_type_kind >= PTYPE_BASE reconstruction branch at typecheck_walk.qz:1707, and the NODE_INDEX type-annotation walker at typecheck_generics.qz:278-290 stripped the outer Vec<> to produce "Struct, Int" as the inferred element type — which then hit tc_check_field_access and failed with QZ0301: Unknown struct: Struct, Int. Why the workaround (var a = va[0]; a.y) worked: the intermediate binding’s struct_name was set via a different code path (NODE_INDEX-aware str2 annotation at typecheck_walk.qz:1655-1659) that reads ast_get_str2 rather than reconstructing from the ptype. Fix: special-case TYPE_VEC in tc_type_name_full — for Vec ptypes, ignore arg2, prefer the cached ptype_name slot (set by tc_ptype_set_name for struct elements), and fall back to tc_type_name_full(arg1) for primitive elements. Locked in by 3 new tests in spec/qspec/vec_element_type_spec.qz: direct va[0].y access, nested va[i].inner.field chain, and arithmetic combination va[0].y + va[1].y. Commit <next>. Fixpoint verified at 2281 functions; B-sprint regression sweep (15 specs) all green. | |
step! (macro $unwrap) inside a while loop body produces a silent miscompile when step is reassigned each iteration. | RESOLVED (Apr 14, 2026 Batch C sprint C5). Root cause was NOT in the match lowering path (where both Session 2 fix attempts looked) — it was in expand_node at self-hosted/frontend/macro_expand.qz:219-233. The NODE_ASSIGN / NODE_INDEX_ASSIGN / NODE_FIELD_ASSIGN branch unconditionally read ast_get_right(h) for the “value” slot, but the actual slot layouts diverge: NODE_ASSIGN stores value in left, NODE_INDEX_ASSIGN in extra, NODE_FIELD_ASSIGN in extra. The wrong-slot read meant the macro expander silently SKIPPED the RHS of every compound assignment — so sum += step! (which parser desugars to sum = sum + step!) carried a raw NODE_MACRO_CALL("unwrap") straight through into typecheck and MIR lowering. Downstream phases interpret unresolved macro calls as zero-valued, producing the observed %v11 = add i64 %v10, %v0 where %v0 = add i64 0, 0. The two Session 2 fix attempts (payloads sentinel, subject hoist) didn’t help because the macro was never running in the first place — they were patching code that wasn’t executing. Why step! outside the loop worked: var a = step! is NODE_LET, not NODE_ASSIGN, and the NODE_LET branch of expand_node reads left (the correct slot). Why the explicit match in loop worked: no macro call to expand — the parser already produced a NODE_MATCH, which expand_node walks through correctly. Fix: split the collapsed if kind == 26 or 27 or 28 branch into three explicit per-kind branches reading the correct slots. Also updated the $unwrap macro (expand_builtin_unwrap) to desugar to an if x is Some(payload) block using battle-tested narrowing instead of a 3-arm match — defense-in-depth so any future hole in match-in-loop-with-reassignment gets caught by the narrowing path. Locked in by 3 new tests in spec/qspec/unwrap_in_loop_spec.qz: while-with-reassignment (the original repro), for-with-reassignment, and nested-while. Fixpoint verified at 2285 functions; B+C regression sweep (17 specs) all green. Lessons learned: when two fix attempts both no-op, suspect the fix site isn’t reached at all. A targeted eputs in the macro body would have surfaced this in 5 minutes. | |
match x; NotAReal(v) => ...; AlsoFake => ... end with x: MyEnum silently typechecks, then llc rejects with use of undefined value '%AlsoFake'. Users got confusing downstream failures instead of a clear “unknown variant” error. | RESOLVED (Apr 13, 2026). Two holes, one in each pattern shape: (1) tc_bind_pattern_variables at typecheck_expr_handlers.qz silently fell through when a NODE_ENUM_ACCESS pattern referenced a variant that didn’t exist in the resolved enum — no error, no binding, arm body typechecked against a bogus type. (2) NODE_IDENT patterns were always bound as capture variables regardless of name shape; a typo’d variant like AlsoFake then got bound as a scope variable named AlsoFake, while MIR lowering separately tried to treat it as an enum variant tag comparison (mir_lower_expr_handlers.qz:3103-3123), emitting load i64, ptr %AlsoFake from an alloca that was never declared. Fix: validate NODE_ENUM_ACCESS variants against tc_enum_has_variant; classify NODE_IDENT patterns by first-character case — PascalCase goes through the variant-lookup path (error if not in any enum, error if in a different enum than the subject), lowercase binds as a capture. Regression spec at spec/qspec/match_unknown_variant_spec.qz covers unqualified variants, variants with payloads, and fully qualified patterns. Self-compile handles the compiler’s own Found(v) => ... NotFound => ... patterns via the module-prefix suffix-match fallback. |
Map type checker holes (dogfooding sprint)
| Issue | Impact | Investigation |
|---|---|---|
m = {"hello" => 42}; m.get("hello") fails. | FIXED (Apr 2026, Cluster A): NODE_MAP_LIT now returns TYPE_MAP (not TYPE_INT); NODE_MAP_COMP typechecks key/value exprs and returns Map<K,V>; new tc_try_resolve_map_key_value_types infers K/V from the first constraining map_set/map_insert call (mirrors Vec inference). | |
.some?/.none? predicates fail on let-bound Map.get() result | result = m.get(k); assert(result.some?) fails to typecheck. | FIXED (Apr 2026, Cluster A): map_get result is now wrapped in Option<V> at the result-type assignment site, so the let binding records Option<V> and predicate narrowing finds the proper Option. |
Map.del() short alias missing | m.del(k) is undefined; only m.delete(k) works. | WONTFIX (per API_GUIDELINES.md §“Vocabulary Rules”). Test removed from spec/qspec/map_ufcs_spec.qz. |
is Some / is None on let-bound Map.get() result causes runaway memory in type inference | FIXED (Apr 2026, Cluster A): root cause was the same as CMP-MAP-2 — let binding recorded Int instead of Option<V>, so is Some narrowing recursed through unbound type vars. Fixing the Option<V> wrap at the source eliminates the runaway. Regression test: spec/qspec/map_int_spec.qz line 40-46. | |
map_* intrinsics across LLVM and WASM backends, delete hashmap_*/intmap_* user-facing names | hashmap_*/intmap_* intrinsic names are still registered as user-callable builtins. | FIXED (Apr 2026, two commits): (1) WASM backend gained a translate-at-entry layer in wasm_runtime.qz (_wasm_resolve_map_name mirroring cg_intrinsic_map._map_to_hashmap_name/_map_to_intmap_name) so codegen_wasm.qz reads slot2 and resolves map_* before downstream processing — verified with quartz check --feature wasm since the WASM target is @cfg(feature: "wasm")-gated. (2) TC dispatch rewrite at typecheck_expr_handlers.qz lines 1487-1571 replaced with a 6-line annotation pass that sets slot2 = "Int" for int-keyed maps, the struct name for struct-keyed maps, or empty (string-default) — runs in both the direct-call site and the UFCS rewrite block. (3) Inliner fix in egraph_opt.qz eg_inline_call: cloned MIR_CALL/MIR_INTRINSIC instructions now copy slot2 so inlined map_* calls keep their elem_type annotation (found via global_vars_spec where gv_init_imap() was inlined into qz_main and lost its int-key annotation). (4) Deleted hashmap_* and intmap_* builtin registrations from typecheck_builtins.qz, deleted __hashmap_get_raw and __hashmap_get_sentinel definitions from str_compat.qz, migrated all backend callers (egraph.qz, domtree.qz, mir_lower_async_registry.qz) to __map_get_raw / map_*. (5) benchmarks/hash_map_bench.qz migrated to unified map_*. After this commit, user code calling hashmap_new() or intmap_new() produces Undefined function errors. The internal codegen still uses hashmap_*/intmap_* as runtime helper symbol names — these are not user-visible. |
Parser holes (dogfooding sprint)
| Issue | Impact | Investigation |
|---|---|---|
.method() { it ... } syntax not supported | items.map { it * 2 } and items.map() { it * 2 } errored. | FIXED (Apr 2026, Cluster B). Parser changes in parser.qz: (1) ps_parse_do_end_block_if_present now treats every {...} in trailing-block position as a lambda body — if no explicit -> header is present, an it parameter is synthesized; (2) the dot-method branch in ps_parse_postfix recognizes paren-free .method { ... } form via the new helper ps_paren_free_block_is_implicit_it which disambiguates from struct/map literals (which require IDENT : or IDENT => as the first body token). Both .map { it * 2 } and .map() { it * 2 } now work, including multi-statement bodies like .filter { var t = 12; it > t }. |
{ it ... } block-as-lambda not supported | var f = { it * 2 } errored. | FIXED (Apr 2026, Cluster B). Primary expression { dispatcher in ps_parse_postfix’s ps_parse_primary recognizes { it ... (where it is followed by a non-header token like an operator) as a single-arg implicit-it lambda and elaborates to it -> body. Set literals are still recognized when the first body token isn’t it. |
FIXED (Apr 2026, Cluster B). Added call_rewrite_depth: Int field to TypecheckState (initialized to 0) and a depth guard in tc_expr_call’s UFCS rewrite path (typecheck_expr_handlers.qz line 2143 area). Cap = 32, chosen as 8x the legitimate maximum (1-3 levels) and half of Rust’s general-purpose recursion_limit (128). Any future method-resolution loop now fast-fails with QZ0205: method resolution exceeded recursion depth 32 instead of consuming multi-GB of RAM. Note: the RUNAWAY itself is also fixed by resolving the underlying parser issue (CMP-PARSER-IMPLICIT-IT-1) — but the depth guard remains as a defense in depth. | ||
? on let-binding RHS not parsed | var val = opt? errors with “Undefined variable: opt?”. | RESOLVED via Path A (Apr 2026, Cluster C-2): postfix ? was deliberately removed in v5.12.27 in favor of the $try() macro because the lexer rule that glues trailing ? into identifiers (which makes .some?/.none?/.empty? predicate methods work) is incompatible with a standalone postfix operator. The bug was documentation drift, not a missing feature. Fix: CLAUDE.md anti-hallucination table updated to point at $try(expr); docs/QUARTZ_REFERENCE.md “Postfix ?” section retitled to $try() macro with the rationale; spec/qspec/postfix_try_spec.qz rewritten to use $try(opt) (9/9 tests passing). The alternative Path B.3 (rename predicate methods to free ? for postfix-try) was offered to the user as a follow-up but Path A was chosen for this session. |
Err(e) => ... errors with “Expected expression / Expected pattern”. | FIXED (Apr 2026, Cluster C-1): the bug was actually in match-arm body parsing, not pattern parsing. After parsing the body of arm N (e.g. Ok(v) => v), the parser scanned for “next arm” tokens but missed the IDENT ( form — Err(e) was being parsed as a function-call continuation of the previous body. Fix: in ps_parse_match_arm’s extra-statement loop, treat a PascalCase identifier followed by ( as an arm boundary (Quartz convention reserves PascalCase for types/variants). Also generalized tc_bind_pattern_variables to scan all registered enums when the unqualified variant has no enum_name, so payload types resolve correctly for user enums (mirrors the existing built-in Option/Result handling). | |
is keyword not recognized by parser | x is Some fails with “Expected newline” | FIXED (Apr 12, 2026). Root cause: is had no token constant, no lexer keyword mapping, no parser rule. All downstream (typecheck, MIR, codegen) already worked. Fix: added TOK_IS = 126 to token_constants.qz, "is" => TOK_IS to lexer.qz, ast_is_check() to ast.qz, TOK_IS branch in ps_parse_equality() in parser.qz (equality precedence, with optional binding form). 21/21 tests passing (is_keyword_spec 12/12, is_binding_spec 9/9). |
impl Trait args without an explicit impl Send for X | tc_validate_trait_bounds → tc_check_trait_bound only consults tc_lookup_impl. | RESOLVED (Apr 14, 2026 Batch A+B sprint B1, commit 6e46491e). tc_check_trait_bound now falls through to the structural field-recursive tc_type_is_send / tc_type_is_sync helpers when the bound is Send/Sync and no explicit impl exists. The existing helpers already had coinductive cycle detection via g_send_checking / g_sync_checking visited stacks, so recursive types like struct Node { next: Option<Box<Node>> } correctly prove Send via structural induction. Follows RFC 0458 (“structural, not nominal”) and rustc’s auto-trait solver design. 5 regression tests in spec/qspec/send_auto_trait_spec.qz. A follow-up IMPL-NOT-SEND (explicit impl !Send for X opt-out) is filed below for a future sprint — this commit handles the positive auto-derivation direction only. |
impl !Send for X / impl !Sync for X opt-out syntax | RESOLVED (Apr 14, 2026 Batch C sprint C3). Parser accepts impl !TraitName for TypeName end (optionally with leading impl<T> type params). Implementation: parser consumes an optional TOK_BANG after type params in ps_parse_impl, records the flag in NODE_IMPL_BLOCK.int_val, and errors if the negative impl lacks a for clause (there is no such thing as a negative inherent impl). tc_register_impl_block short-circuits on the negative flag — writes the (trait, for_type) pair into new parallel registry vecs neg_impl_trait_names / neg_impl_for_types and returns without registering any method bodies. Three consultation sites: tc_type_is_send, tc_type_is_sync, and the Send/Sync branch of tc_check_trait_bound all consult tc_lookup_neg_impl FIRST, returning 0 on match — before the positive tc_lookup_impl path and before the structural walker. tc_lookup_neg_impl strips <...> on both the query type and each registered type so impl !Send for X rejects both X and X<Int>. Locked in by spec/qspec/impl_not_send_spec.qz (5 tests: basic reject, positive regression, Sync analogue, all-primitive-fields still rejected, unrelated positive impl does not mask). Fixpoint verified at 2283 functions (+2 from 2281: new tc_register_neg_impl and tc_lookup_neg_impl helpers). B-sprint regression sweep (16 specs incl. send_auto_trait) all green. Design reference: Rust RFC 0019 (ImplPolarity::Negative). | |
struct W<C: Counter> { inner: C } hangs the parser | ps_parse_optional_type_params on the : — ps_expect(TOK_COMMA) recorded an error without advancing the cursor, so the outer while ps_check(TOK_GT) == 0 kept seeing the same token. | FIXED (Apr 13 2026, impl-Trait sprint Phase 5). ps_parse_optional_type_params now consumes :Trait and +Trait bounds and preserves them inline in the returned bracketed source form (e.g. "<C: Counter>"). Downstream: tc_strip_struct_tp_bound and tc_extract_struct_tp_bound helpers split the bound from the name at the registry boundary; tc_infer_struct_type_args and tc_substitute_field_type strip the bound before name-matching against field annotations; new tc_check_struct_type_param_bounds validates each concrete type arg at struct-init time via the existing tc_check_trait_bound helper. Positive/negative tests in spec/qspec/impl_trait_spec.qz. |
var w = Wrapper { inner: Cell { ... } } (no explicit Wrapper<Cell>) causes tc_infer_struct_type_args → tc_type_name(TYPE_STRUCT) to return the generic string "Struct" instead of the actual struct name. Downstream field access through w.inner.val then fails with QZ0301: Unknown struct: Struct. | FIXED (Apr 13 2026, impl-Trait sprint follow-up). Root cause confirmed as hypothesized: tc_find_field_type_for_param returned only the integer type ID, which the caller then ran through tc_type_name, collapsing every struct field to the generic kind name “Struct”. Fix: tc_infer_struct_type_args now takes the field initializer AST nodes, and the new helper tc_find_field_type_name_for_param calls the existing tc_infer_expr_type_annotation walker on each initializer to recover the source-level struct name (handles NODE_STRUCT_INIT, NODE_IDENT, NODE_CALL, NODE_FIELD_ACCESS, etc.). Falls back to tc_type_name on the runtime type ID when the AST walker can’t infer an annotation. Two files touched (typecheck_generics.qz, typecheck_expr_handlers.qz); regression test in spec/qspec/impl_trait_spec.qz — ITLift { source: ITBase { val: 41 } } now binds with type ITLift<ITBase> and l.source.val resolves to 41. Unblocks Phase 6 (std/iter.qz) writing adapter constructors like MapIter { source: src, f: f } without explicit type args. | |
def sink<I: Iter>(src: I): Int the call it.i_next() should re-dispatch through the caller’s concrete type for I at each monomorphization. Currently the dispatch either (a) reuses the outer caller’s concrete type directly (collapsing adapter wrappers), or (b) stamps the wrong impl into the call site. End-to-end prototype sink(map_doubler(range)) returns garbage even though each isolated layer compiles. | FIXED (Apr 13, 2026, impl-Trait sprint Phase 6 unblock). Root cause was two-fold. (1) When the MIR lowered var d = f(...) for a bounded generic f returning Foo<T>, it recorded the raw declared return Foo<T> as d’s tracked struct type. That leaked the type parameter into downstream monomorphization: calling sink(d) from main produced the spec name sink$1$MapDoubler$I (not $RangeSrc) and a degenerate spec_param_types mapping of I → MapDoubler<I>. (2) The A8 trait-rewriting block in mir_lower_expr_handlers.qz only consulted spec_param_types for the UFCS receiver — it never looked at the variable’s tracked struct type, and it concatenated spec_current_type literally without stripping the <...> generic suffix, so bodies like var it = src; it.method() failed to build a valid Type$method symbol and fell through to whatever method the earlier fallback dispatch happened to pick. Fix (three edits across two files): added mir_subst_type_with_map and mir_subst_return_type_for_call helpers in self-hosted/backend/mir.qz that substitute type parameters in the declared return type using the caller’s inferred argument types; self-hosted/backend/mir_lower_stmt_handlers.qz now uses the substituted return type when marking the call-result variable’s struct type; self-hosted/backend/mir_lower_expr_handlers.qz A8 block now falls back to mir_ctx_get_var_type for local-variable receivers and strips the generic suffix off both the resolved type and spec_current_type before building the mangled method name. End-to-end sink(map_doubler(range)) now returns 12 as expected. 44/44 in spec/qspec/impl_trait_spec.qz (was 42/42, added two regression tests — nmutp_direct_struct_init and nmutp_wrapped_generic_call), fixpoint verified at 2278 functions, smoke + regression sweep clean (iterable_trait, abstract_trait, traits, hybrid_ufcs_traits, collection_stubs, arity_overload, match_unknown_variant). Unblocks Phase 6 (std/iter.qz rewrite) and the bounded-generic RPITIT case below. | |
impl Trait function | var x: Hello = make_greeter() compiles even though make_greeter(): impl Greeter is supposed to hide the concrete type. Phase 7 of the impl-Trait sprint was scoped for opacity enforcement but the scope binding currently unifies struct_name (for dispatch) and type_ann (for source-level matching) into a single slot, so splitting them is bigger than the 2-4h budget estimated in the sprint handoff. | FIXED (Apr 13, 2026, impl-Trait sprint Phase 7). Added a parallel binding_display_types: Vec<String> slot to TcScope (typecheck_util.qz) wired through a new tc_scope_bind_full_with_display and a matching tc_scope_lookup_display_type reader. tc_lookup_impl_return_opaque and tc_is_opaque_marker were added to typecheck_registry.qz; tc_infer_expr_display_annotation was added to typecheck_generics.qz to return the opaque marker "impl <Trait>@<func_name>" for impl Trait-returning calls (NODE_BLOCK unwrap to last expression handles if c then make() ... end). The NODE_LET handler in typecheck_walk.qz stamps the opaque marker into the binding’s display slot when the call target returns impl Trait, propagates it through var b = a IDENT bindings, and emits QZ0167 when the user provides an explicit annotation that names the concrete type behind the opaque return. The expression-context NODE_IF branch unifier emits QZ0168 when both branches return values whose display annotations are different opaque markers. The dispatch_type slot (existing binding_type_annotations) keeps holding the resolved concrete struct name, so UFCS method lookup and bounded-generic bound resolution still work — verified by 4 positive opacity tests including a refactor-safety case (changing ITHello → ITWorld in a helper does not break callers). 39/39 in spec/qspec/impl_trait_spec.qz (was 33/33), fixpoint verified at 2276 functions, smoke + regression sweep clean. |
| OPEN-UFCS-HASH-SENSITIVITY: edits to one stdlib file silently miscompile method dispatch in another | sb.append("m") in std/style.qz:_style_render lowers to %v112 = load i64, ptr %append, align 8 (closure-call path) referencing an undefined %append SSA value. Triggers when ANY top-level def is added to std/json.qz (or its body changes meaningfully) — purely line-count / hash-bucket sensitive. The open-UFCS rewrite of sb.append(...) falls through to the closure-call path instead of finding sb_append as a free function. Reproducer: add a single def _foo() = 0 to the end of std/json.qz, then compile spec/qspec/json_pretty_spec.qz. Affects 4 specs in baseline (json_pretty_spec, json_float_spec, json_edge_cases_spec, json_ufcs_spec) — these were ALL pre-existing failures, not caused by the json fix. Discovered Apr 16 during Sprint 1. Investigation: walk the open-UFCS rewrite path in typecheck_expr_handlers.qz — find why method-name lookup for append against StringBuilder fails non-deterministically. Likely a stale resolution cache or an iteration-order-dependent dispatch. | Sprint 1 follow-up. Blocks the typed-payload fix for JsonValue::Object/Array (the right answer for the json crash). |
JSON-OBJECT-PAYLOAD-INT: enum payloads typed as Int lose Map<K,V> / Vec<T> info on destructure | enum JsonValue { ... Object(fields: Int) ... } — when matched, fields has type Int. map_keys(fields) → Vec<Int> (default fallback). keys[i] types as Int. map_get(fields, key) then dispatches to intmap_get even though fields was created via map_new() (string-keyed hashmap) → SIGSEGV at runtime. The right fix is Object(fields: Map<String, JsonValue>) (typed payload), but that triggers OPEN-UFCS-HASH-SENSITIVITY above and breaks style.qz. Current workaround: helper _json_key_to_string(k: Int): String = as_string(k) reinterprets the Int back to its String pointer (no-op at runtime, hashmap key slots ARE string pointers stored as i64). | Sprint 1 (Apr 16) — fix landed. Underlying compiler bug (Int-typed receiver dispatching map_get to intmap based on key arg type when receiver IS a hashmap) remains. The deeper fix requires either typed-payload migration (blocked) or smarter dispatch fallback (when key type is Int but receiver was created via map_new() with no annotation, prefer hashmap path). |
IMPL-TRAIT-RPITIT: impl Trait in trait method return types | PARTIALLY LANDED (Apr 13, 2026, impl-Trait sprint Phases 11 + 4). Phase 11 added per-impl concrete-return tracking for trait methods declared as def m(self): impl OtherTrait, with direct-receiver dispatch routing through the impl-specific concrete via the impl-return registry. Phase 4 wired trait Iterable<T> { def iter(self): impl Iterator<T> } into std/traits.qz and added impl Iterable<Int> for X end (auto-satisfaction) for all six stdlib collections (Stack, Queue, Deque, LinkedList, PriorityQueue, SortedMap). 6/6 direct-receiver tests in spec/qspec/iterable_trait_spec.qz, 42/42 in spec/qspec/impl_trait_spec.qz (incl. 3 RPITIT cases), fixpoint verified at 2276 functions. Still missing: bounded-generic dispatch — def sum_all<C: Iterable<Int>>(c: C): Int = ... does NOT yet route c.iter() through the per-monomorphization concrete; that path is blocked on NESTED-MONO-UFCS-TYPE-PARAM above. | |
IMPL-TRAIT-TAIT: type alias impl Trait | type Foo = impl Iterator<Int> — lets you name an opaque type once and reuse it in struct fields, static items, and recursive contexts. Follow-up for a future sprint after the core impl Trait + opacity + RPITIT work lands. | Filed per impl-Trait sprint handoff. See Rust RFC 2515 for the design reference. |
| IMPL-TRAIT-STRUCTURAL: structural opaque return types | def pair(): (impl Iter, impl Iter) — tuples/arrays of opaque types. Follow-up for a future sprint. | Filed per impl-Trait sprint handoff. See Swift SE-0328 for the design reference. |
Completed (Recent)
| Item | Commit | Notes |
|---|---|---|
| Fix impl Option/Result infinite recursion | b984ab71 | impl Option and impl Result method bodies delegated to same-named free functions (def is_err() = is_err(self)), causing infinite recursion. Method name shadows the free function inside impl scope. Fix: inline match logic directly. Unblocked result_helpers_spec (16/16), csv_spec (9/9), option_result_spec (17/17), lookup_result_dogfood_spec (8/8), and any code using .is_some()/.is_none()/.is_ok()/.is_err()/.unwrap()/.unwrap_or() method syntax. |
| Fix .size wart | b6432169 | Typechecker auto-rewrites .size on Int-typed values (no struct name) to vec_size() call. Previously read capacity (offset 0) instead of size for untyped Vec handles. |
| Fix async $poll extern name collision | 5abfe9b5 | cg_extern_var_index fallback stripped __Future_*$poll via last-$ lookup → matched libc poll(). Fix: skip $ fallback for __-prefixed compiler-generated names. async_spill_regression_spec 12/12. |
Add reversed() and sorted() to prelude | 65ab0ed9 | Two missing eager collection operations via vec_clone + vec_reverse/vec_sort. collection_stubs_spec 21/21. |
| Fix SEND-RECV-SHADOW | 965bfbaa | Deleted dead extern "C" def send/recv from std/ffi/socket.qz. Zero callers — wrappers use sendto/recvfrom. Unblocked 6+ concurrency specs. |
proc_suspend(pid) intrinsic — pidfd / EVFILT_PROC child exit | ab443188 | Two new scheduler intrinsics. proc_suspend(pid) user-facing, MIR-lowered via new @__qz_io_pending_is_proc TLS flag that worker task_io_real reads to dispatch to register_proc instead of register_io. sched_register_proc(pid, task) raw form: Linux pidfd_open + epoll + @__qz_pidfd_set cleanup; macOS kevent EVFILT_PROC + NOTE_EXIT with task in udata, atomic register+check via paired changelist/eventlist call. I/O poller extended for both platforms (macOS filter dispatch, Linux pidfd close-after-fire). 3-test QSpec using extern fork/usleep/_exit/waitpid. Replaces read==0 EOF heuristic for child exit detection — fixes daemonizing-child and grandchild-piped failure modes. Stdlib integration (rewrite std/process.qz on fork+exec) is the P1 follow-up. |
| 7 P0 progress sprint quirks filed as compiler bugs | 766296c9 | docs/bugs/PROGRESS_SPRINT_QUIRKS.md catalogues PSQ-1..PSQ-7 with status, root-cause hypothesis, fix direction, and existing workaround. Top priority: PSQ-4 (Vec<T> element type loss → silent wrong-struct field reads — hit twice in 1a60633c). Closely related: PSQ-6 (Vec.size reads 0 from poller-thread context). |
std/process.qz async-aware subprocess + Live render mutex | 1a60633c | New std/process.qz with sh_run_async/sh_capture_async/sh_stream_lines_async using io_suspend(fd) on EAGAIN. Quakefile sh_with_progress rewritten to use go + await, drives quake build’s 8-phase compile bar through cooperative async. Live _render_mtx field guards _render_force against poller/main thread interleaving. 5-test process_async_spec. |
| Scheduler idle hook intrinsic + Live integration | 945cc265 | sched_idle_hook_set/clear intrinsic — fires registered Fn(): Void once per I/O poll iteration in __qz_sched_io_poller. Slot 35 of @__qz_sched, tagged-pointer dispatch (closure/plain). Replaces the polling go-task ticker that the previous commit shipped. End-to-end verification: quake build TTY mode renders 441 frames over 17s compile (was 2 frames). 5-test sched_idle_hook_spec. |
std/progress background go-ticker for Live in TTY mode | d5ddaa36 | First animation fix: a polling go-task that calls _render_force on a fixed cadence. Superseded by 945cc265’s idle hook (kept for history). |
std/progress: composable spinners, bars, multi-progress Live UI | 05581f77..be285aed | New stdlib library: 36 vendored cli-spinners styles, parameterized Col grammar ({spinner}, {bar}, {pos}, {msg}, {eta}, custom columns), Progress + Live structs. Live multi-progress surface with global bar + top-N active rows + above-region log. JSON mode via QZ_PROGRESS=json. 56-test progress_spec. |
Quake tasks migrated to std/progress Live UI | e5cd2224, 2d944089, 6ecab203, 53a17978 | qspec runner (Live UI), quake build (consume --progress events from compiler subprocess via sh_with_progress), quake guard (multi-phase Live with per-phase log lines), quake fmt/lint (progress bars over file walks). |
| QSpec triage sprint (Apr 12) | 885f80c9..8e70f0fa | Fixed ~165 tests across 17 spec files in one session. Linter SIGSEGV (3 .size bugs + parser newline consumption), extern decl dedup false positive from string constants, match guard spec variant names, stale channel capacity expectations, panicking m[key] spec rewrite for map_index_spec and nested_generics, unicode .size() fixes, F32 coercion in packed_arrays, type_inference auto-coerce update, new option_narrowing_spec (5/7), committed pre-existing Option narrowing source that was in binary but not git. |
Option ergonomics: is, !, if let, map_fetch | 5fe28075 | is keyword (lexer/parser/AST), postfix ! force-unwrap (parser desugars to $unwrap), if let/elsif let (desugars to if Some(v) = expr), panicking m[key] via map_fetch intrinsic (hashmap + intmap + unified map). 2,242 functions. |
| Compiler smoke tests | a4277b67 | Enhanced brainfuck.qz (117→386 lines, 35+ features documented), new expr_eval.qz (330 lines, 22 tests, Tier 3 features). Run after quake guard to catch semantic regressions beyond fixpoint. |
guard:source clean-room build task | 5fe28075 | Quakefile task: gen0→gen1→gen2 fixpoint + 3 smoke tests (brainfuck, expr_eval, style_demo). Verifies the source tree is buildable from scratch with no baked-in binary state. |
| Bool.to_s() → “true”/“false” + auto-coerce in String+Bool | 6a3d09a9 | New intrinsic chain: Bool$to_s, bool_to_str, qz_bool_to_str runtime. 2,242 functions. |
| String ergonomics: String+non-String auto-coerce, String*Int repetition | 3eea9906 | TC AST rewrite wraps non-String in to_str/f64_to_str; * routes to existing str_repeat. 11 new tests. |
| Fix unqualified variant payload types + dogfood 22 spec files | 1c68b67c | Match subject’s enum name derived from function return annotation. Unblocked 4 net/tls specs. |
| Retype Request.params/query/context as Map<String,String> | ba4e4bd2 | Fixes SIGSEGV from UFCS dispatch on Int-typed map handles. |
| Dedup FFI declarations against both define and declare | e8efa41a | Fixed text-based dedup that missed return types between declare/define and @name. |
| Fix float→i64 widen in MIR_INDEX/INDEX_STORE codegen | 8435d663 | 4 handlers (INDEX, INDEX_RAW, INDEX_STORE, INDEX_STORE_RAW) lacked is_float branch. |
| Reject Float map keys; walk struct fields for hashability | 85152035 | F64/F32/CFloat/CDouble removed from hashable. tc_struct_is_hashable walks fields. |
| Bind for-in loop variable with container’s element type | 0b4e6fc5 | TC normalizes ptype to base for NODE_FOR.extra; loop var gets key/elem type. |
| Fix int-key map iteration (3 chained bugs) | 524abbfc | TC ptype normalization + MIR map_key_types tracking + intmap_keys/values occupancy bitmap codegen. |
| Bootstrap recovery: mangle UAF + fossils + Map migration + Quake Guard | a23f71ed | The Apr 2026 recovery commit |
| Park HM type inference v2 modules | fd4810b8 | Moved to experiments/parked/hm-inference/ |
| Complete first phase of HashMap → Map spec migration | be13bf84 | 4 specs migrated, 6 holes filed |
| Dogfood char literals + UFCS in examples/benchmarks/projects/site | f47aded4 | 5 files |
| Update HashMap → Map terminology in documentation | 53e8cb01 | 7 docs |
| Dogfood char literals + UFCS in tools | 40515677 | 4 files (excl lint) |
| Dogfood UFCS + char literals + variants in stdlib | 3084eec6 | 24 files |
| Add 5 new spec files for v5.26+ language features | 1cf399e3 | + 4 new compiler holes filed |
| Dogfood UFCS + char literals + variants in 92 spec files | da786e89 | + CMP-VARIANT-PATTERN filed |
| HPACK: bounds-check guards + 7 new Huffman decode tests | 99750257 | std/net/hpack.qz + spec |
| WASM backend test expansion | 685a6f23 | 4 wasm spec files |
| http_server + http2: cross-platform TLS + frame helpers | bdc050e6 | 2 networking files, 11 new defs |
| lint: extend QZ7104 + add QZ7206 bare-go rule | 36f376c5 | tools/lint.qz |
Abstract trait methods (bodyless def in trait blocks) | 6362a926 | 8 tests |
| Stdlib trait impls (Show, Eq, Serializable) | 5f6e7f53 | 9 tests |
| Trait auto-satisfaction + Container trait + HashMap→Map | 8d773bb0 | 8 tests |
| Multi-line function signatures | 78123538 | 6 tests |
| Trailing commas in parameter lists | 777757ee | 6 tests |
| Modernize 5 examples + activate group_by test | 576b13dc | — |
| Fix UFCS module name collision (param precedence) | eb75db87 | 3 tests |
| Exhaustiveness for Bool and Result<T,E> | 655f405c | 4 tests |
| Fix UTF-8 double-encoding (sb_append_byte + 6 lexer paths) | 5f9448b1 | 68→0 corrupted strings |
| CLI style library: terminal detection + composable Style API | — | 35 tests |
| Fix UFCS impl method priority for call-expression receivers | — | fixpoint |
| MirContext UFCS migration | ab2130b9 | 5 MIR lowering files |
| MirFunc/Block/Instr UFCS | 844b8876 | 3 codegen files |
Dogfooding Backlog
| Item | Status | Notes |
|---|---|---|
| DG.2 Triple-quoted strings | ✅ | ~93 spec files, ~132 commits |
| DG.3 Unified intrinsic registry | ✅ | Single Map, 696 names |
| DG.4 String match + length dispatch | ✅ | Length→hash→str_eq for >=5 arms |
| GAP-1 Abstract trait methods | ✅ | Bodyless def, body=-1 sentinel |
| DG.5a-b UFCS compiler migration | ✅ | 8 backend files migrated |
| DG.6a-d Stdlib trait impls + Container | ✅ | Auto-satisfaction, 4 collections |
| DG.8b-c Examples modernization | ✅ | traits.qz, fibonacci.qz, string_processing.qz |
| Trait auto-satisfaction | ✅ | Swift/Go model, resolver + typechecker |
| DF.10 OPQ Goal B | Pending | Provenance preservation via MIR_TYPE_PTR |
| DF.11 Memory optimization | Pending | vec_shrink_to_fit, arena mmap |
DF.12 std/ptr.qz module | Pending | Typed pointer ops for FFI |
| DF.13 Compiler decomposition | Partial | Phase 0-1 done, Phases 2-3 pending |
Reference
docs/API_GUIDELINES.md— Canonical API design policiesdocs/QUARTZ_REFERENCE.md— Complete language syntax referencedocs/ARCHITECTURE.md— Compiler pipeline explanationdocs/INTRINSICS.md— All built-in intrinsicsdocs/QUAKE.md— Quake build systemdocs/STYLE.md— Code style guidedocs/QUARTZ_GUARD.md— Source-only build invariantdocs/BOOTSTRAP_RECOVERY.md— Recovery proceduredocs/archive/— Historical roadmaps, vision, handoffs, audits