Quartz v5.25

Handoff — Operation Piezoelectric Effects: Phase 0 Design Session (2026-04-18)

Epic: Operation Piezoelectric Effects (algebraic effects for Quartz) Commit tag: [piezo] (prefix in commit messages for all epic-related work) Casual name: “Piezo” Phase: 0 (design, complete) → 1 (implementation, ready to start)

The epic is named after the piezoelectric effect — the physical property by which quartz crystals convert mechanical stress into electric charge (and vice versa — the Curie brothers, 1880). It’s why quartz oscillators exist; why quartz clocks tick; why your motherboard has a heartbeat. Quartz converts effort into effect. Our programming language Quartz is about to do the same thing with algebraic effect handlers. The name writes itself.

Session purpose: close every open design decision for the Quartz algebraic effect system so Phase 1 coding can begin in a fresh context window without reconstructing intent.

Outcome: Phase 0 design complete. All 10 open decisions closed. Governing philosophy committed. Paper notes populated. docs/EFFECTS.md stubbed. 3 roadmap items filed. Both foundational papers (Leijen 2017, Xie-Leijen 2020) read. LLVM compilation strategy memo written.


What changed in this session

1. Pivoted out of the Joy-of-Quartz unikernel epic

The Joy-of-Quartz plan (docs/handoff/joy-of-quartz-unikernel-epic.md) is paused at clean phase boundary — all of KERN.4 + J.1–4 + DEF-A shipped, https://mattkelly.io/ served by the unikernel. Full four-phase plan (K/S/H/D) preserved verbatim; resumes at Phase K.1 (slab allocator) whenever we return to it. ROADMAP.md:370 updated with the pause + the effects pivot note.

2. Effects Phase 0 — all 10 open design decisions closed

#DecisionCommitted
1Compilation modelEvidence-passing (Xie & Leijen 2020 style — not Leijen 2017)
2Scoped labelsModel B — duplicates allowed, lexical shadowing
3Initial effect set14 atomic effects + 1 row alias (Io). Log→Parse pilot
4opt! sugar! stays as assertion; not effect-typed
5 / 8Row subtypingFlat row equality + tail polymorphism
6Error message bar6 commitments + engagement-level detection heuristic
7Io granularityHybrid — atomic labels + Io row alias
+Design philosophyInvisible by Default, Rich When Wielded
+Error-handling surfaceFour-tier unification (Assertion / Effect / Data / Panic)
+Panic-as-effectFirst-class with customizable default handler
+StdIo bufferingOption D (line-TTY, block-pipe, panic-flush)
+Phase 2 pilotWorld-class CLI / config (Ruby-Gemfile ergonomics, dogfoods Reader/State)

3. Roadmap items filed

  • Tier 2 #8aextend / impl unification. impl keyword deleted entirely. extend Type (inherent), extend Type with Trait (conformance), extend Type without Trait (negative impl). Swift-style. ~1 day.
  • Tier 2 #8b — Macro system audit. Quartz already has builtin + user-defined + derive macro infrastructure (~46KB macro_expand.qz). Audit: hygiene, AST pattern matching, error messages, docs. ~1 quartz-day. Output determines whether Tier 4 #25 “user-defined macros” is downgraded to polish or stays open.
  • Tier 3 #20a — Structured concurrency gap audit. Research-only; executed after effects Phase 3 (async-as-effect migration). Phase 3 may close the gap for free.

4. Documents created / edited

DocStatusPurpose
docs/ROADMAP.mdEdited (line 370 + Tier 2 + Tier 3)Pause Joy-epic, file 3 items, pivot note
docs/research/EFFECTS_IMPLEMENTATION_PLAN.mdHeavily editedDesign philosophy section added; 10 decisions closed; decision log brought current; error message quality bar + detection heuristic spec’d
docs/research/EFFECT_SYSTEMS_NOTES.mdCreatedPer-paper notes. Contains Leijen 2017 notes (first entry) + template for future entries
docs/EFFECTS.mdCreated (stub)Governing user-facing doc. Philosophy + syntax + blessed effect set + error tiers + default handler stack + compilation model reference. Phase 1 fills in §§ 2, 6, 7, 9, 11
docs/handoff/effects-phase-0-design-session.mdCreated (this doc)Session handoff

Governing philosophy (single quote to live by)

Invisible by default, rich when wielded.

Effects are pervasive internally — Quartz is effect-typed all the way down. But they are optional externally. Three engagement levels (invisible / implicit / explicit), all first-class.

Trivial programs have zero effect ceremony. can never required for correctness. The rich features are reachable from any scope, not gated by pragmas. Writing invisibly doesn’t lock you out of wielding.

Apply this filter to every Phase 1 design decision:

  1. Does this work with zero effect annotations in simple programs? (If no, redesign.)
  2. If the user wants to be explicit, is the rich form at hand?
  3. Are both paths first-class, or is one second-class?

User’s own framing on error messages (verbatim): “If we detect that they’re big enough to be on the ride, we can allow them in on some of the sausage making. Otherwise, we insulate.”


Phase 0 exit status (as of 2026-04-18)

Phase 0 exit criteria per the plan — all met:

  • ✅ Compilation model chosen, with rationale (evidence-passing, Xie-Leijen 2020; validated against the papers; LLVM-specific strategy in memo)
  • ✅ Initial effect set curated (14 atomic + Io alias)
  • ✅ Syntax locked (can, effect...end, with...do...end, try, reify {}, throw, resume)
  • ✅ Leijen 2017 read (type-system foundation — rows, scoped labels, OPEN/CLOSE, selective CPS)
  • ✅ Xie & Leijen 2020 read (“Effect Handlers in Haskell, Evidently” — evidence-passing compilation, three-kind operation taxonomy, perf benchmarks)
  • docs/research/EFFECT_SYSTEMS_NOTES.md populated (both paper entries + template)
  • docs/EFFECTS.md stubbed
  • docs/research/EFFECTS_LLVM_COMPILATION_MEMO.md written (concrete LLVM ABI + calling conventions + Phase 1 milestone breakdown)
  • ⬜ Leijen 2014 §2–6 — deferred (largely subsumed by 2017 paper)
  • ⬜ Rémy / Effekt / OCaml 5 skims — deferred (alternatives were explicitly rejected; skims unlikely to change picture)

Phase 0 is closed. Phase 1 can begin.

Critical design insight surfaced in the 2020 paper read

The three-kind operation taxonomy (value / function / operation) was NOT in our pre-paper plan. It’s now captured across all three docs. It’s the single biggest factor in whether effects make Quartz faster or slower. The short story: most of our ops are function-kind (tail-resumptive, direct indirect call, ~O(1) overhead). Only Throws, Async, Panic, and deferred Ndet hit the operation slow path — and the slow path reuses our existing $poll state-machine lowering, so Phase 3 (async migration) is reframing rather than rewriting.

One correction carried forward

I (Claude) mis-cited Leijen 2017 as the compilation-model paper through much of the design session. It’s actually the type-system paper (rows, scoped labels, OPEN/CLOSE rules, inference). Evidence-passing compilation lives in Xie & Leijen 2020. Both papers have now been read and incorporated. The plan doc’s decision log is correct.


The first substantive task in the next session

Start Phase 1 coding. Begin at Milestone A of the LLVM compilation memo:

  1. Add new lexer tokens: TOK_CAN, TOK_EFFECT, TOK_WITH, TOK_HANDLE, TOK_TRY, TOK_REIFY, TOK_THROW, TOK_RESUME in self-hosted/frontend/token_constants.qz and wire into the lexer.
  2. Parse effect Name ... end blocks in self-hosted/frontend/parser.qzps_parse_effect_decl.
  3. Extend ps_parse_function to handle trailing can Row in function signatures.
  4. Add QSpec for effect-declaration parsing + can-suffix parsing.
  5. quake guard — fixpoint should still pass since no type-system changes yet.

Estimate for Milestone A: ~1 quartz-day, 1-2 sessions. Finite, concrete, low-risk.

The full milestone breakdown (A through H, ~8 quartz-days total) is in docs/research/EFFECTS_LLVM_COMPILATION_MEMO.md § 7.


Phase 1 scope summary (when coding begins)

From EFFECTS_IMPLEMENTATION_PLAN.md Phase 1:

Parser (self-hosted/frontend/parser.qz)

  • can keyword in function signatures
  • effect ... end declaration block
  • with ... do ... end handler block
  • try prefix keyword
  • reify { ... } block
  • Delete $try macro from macro_expand.qz

Type system (self-hosted/middle/typecheck*.qz)

  • Row representation in TcRegistry (labels + optional tail variable)
  • Row unification (Rémy-style, duplicates allowed per Model B)
  • Row substitution, occurs check, pretty-print
  • Effect row on function types (extend structured fn type representation)
  • OPEN / CLOSE rules (load-bearing — if these don’t apply aggressively, every function prints with an effect row and the invisible level breaks)
  • Call-site effect propagation
  • Effect subtraction at handler boundaries
  • Effect-op call adds op’s label to caller’s row
  • Effect polymorphism in generalization / instantiation
  • Soundness story for let-polymorphism + effects (value restriction replacement)

MIR (self-hosted/backend/mir*.qz)

  • Effect-op primitive (evidence-indirect call)
  • Handler install / uninstall primitives
  • Effect row metadata on MIR function types

Codegen (self-hosted/backend/codegen*.qz)

  • Evidence parameter generation
  • Handler dispatch at effect-op sites
  • Stack-allocated handler structs
  • (Per the LLVM design memo above)

Stdlib pilots (order matters)

  • std/log — simplest effect, single-line handler, proves machinery
  • std/parse — parameterized Throws<ParseError>, proves Throws

Testing

  • QSpec specs for Throws + Log + handler install
  • Smoke programs (brainfuck.qz, style_demo.qz) continue to pass
  • Fixpoint (gen1 == gen2 byte-identical) holds through the transition

Error messages

  • All 6 commitments + engagement-level detection per the committed quality bar
  • Error messages ARE the product — not decoration. Kill criterion: if we can’t hit the bar without rewriting inference, Phase 1 is at risk.

Cross-cutting

  • docs/EFFECTS.md §§ 2, 6, 7, 9, 11 filled in
  • 10–15 curated examples in examples/effects/
  • Effect-system error-message style guide

Phase 1 estimate (plan doc): 5–7 quartz-days · 7–10 sessions. Kill criteria intact:

  • 2x estimated effort with no end in sight

  • Evidence-passing > 10% slower than direct-call baseline
  • Error messages can’t be made teaching-grade without major type-system rework

Key session dialog moments to preserve

These shaped the committed design and would be missed if the next session lost them:

  1. Multi-shot deferral reasoning (Q1): “We’re already going out on a limb supporting this new experimental research type effects programming. This is even feels even more far out, like this multi-shot version. Like, I think we can go without it and still wow people.” → Ndet pushed indefinitely; evidence-passing chosen.

  2. Sandbox-friendly positioning (Q2): User picked Hybrid Io (atomic labels + alias) over monolithic. This is a positioning commitment — sandbox-friendly runtime is on Quartz’s future roadmap. Effect system becomes the capability-based enforcement mechanism.

  3. Four-tier error surface discussion (Q3 → full design session): User insisted on unifying the whole Option/Result/!/$try/Panic surface before stacking Throws on top of it. Led to the intent-to-form mapping, try as optional keyword, reify block, $try macro deletion.

  4. Philosophy-as-governing-principle commitment: User’s phrasing drove the non-negotiables list. The philosophy filter now controls every remaining design decision.

  5. Extend/impl unification push (#8a): User rejected the weaker “split verbs” proposal and pushed to full Swift-style unification. Stronger philosophy, cleaner grammar, Rust-wart avoided.

  6. Env as its own effect: User flagged the security-audit use case. Env is atomic (not subsumed under ProcIo) because sandboxed runtimes and security software need to intercept every env var read/write.

  7. Error-message “sausage making” framing: User’s phrasing became the engagement-level detection heuristic. “If we detect that they’re big enough to be on the ride, we can allow them in on some of the sausage making. Otherwise, we insulate.”


Quick-start for the next session

  1. Read this doc (you are here).
  2. Scan docs/EFFECTS.md for the user-facing design.
  3. Scan docs/research/EFFECTS_IMPLEMENTATION_PLAN.md for the committed decisions + philosophy.
  4. Scan docs/research/EFFECTS_LLVM_COMPILATION_MEMO.md for the concrete implementation strategy + Milestone breakdown.
  5. Skim docs/research/EFFECT_SYSTEMS_NOTES.md for paper takeaways (two entries: Leijen 2017 + Xie-Leijen 2020).
  6. Start coding at Milestone A: lexer tokens + effect ... end parsing + can suffix in function signatures. self-hosted/frontend/token_constants.qz + self-hosted/frontend/lexer.qz + self-hosted/frontend/parser.qz.
  7. Remember quake guard before any commit that touches self-hosted/*.qz. Fixpoint at 2138 functions; protect it.

Roadmap breadcrumbs (where this work sits in Quartz’s broader tracks)

  • Effects is Track 1 of two parallel initiatives (the other is the Kernel Epic, paused). See ROADMAP.md § “Two major initiatives run in parallel.”
  • Effects unblocks: allocator-as-effect (Phase 2), async-as-effect migration (Phase 3), compiler dogfooding — diagnostics / scope / emit / Fs / interning as effects (Phase 5).
  • Phase 2 pilot (CLI/config): dog-foods Reader+State effects AND delivers a Quake tooling DX win as a byproduct. Side-effect positive.
  • Phase 5 dogfooding is where the real payoff lands: 10–15% LOC reduction in compiler typecheck + codegen via effect-ization of currently-threaded state.

Sanity check — things that are not blockers to report

(Use this to resist the urge to flag them as issues in the next session.)

  • The $try macro still exists in the codebase. Expected. It gets deleted in Phase 1 as part of the try keyword introduction. Directive 7 applies.
  • impl Type (inherent impl) still works in the parser. Expected. Gets deleted as part of Tier 2 #8a, which is scheduled independently of the effects work — can be done any time, low priority.
  • No can keyword in the lexer yet. Expected. Phase 1 starts there.
  • docs/EFFECTS.md has TBD in Phase 1 placeholders. Intentional. The stub captures what’s committed; Phase 1 fills in spec + examples + error reference.

Good hunting.