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
| # | Decision | Committed |
|---|---|---|
| 1 | Compilation model | Evidence-passing (Xie & Leijen 2020 style — not Leijen 2017) |
| 2 | Scoped labels | Model B — duplicates allowed, lexical shadowing |
| 3 | Initial effect set | 14 atomic effects + 1 row alias (Io). Log→Parse pilot |
| 4 | opt! sugar | ! stays as assertion; not effect-typed |
| 5 / 8 | Row subtyping | Flat row equality + tail polymorphism |
| 6 | Error message bar | 6 commitments + engagement-level detection heuristic |
| 7 | Io granularity | Hybrid — atomic labels + Io row alias |
| + | Design philosophy | Invisible by Default, Rich When Wielded |
| + | Error-handling surface | Four-tier unification (Assertion / Effect / Data / Panic) |
| + | Panic-as-effect | First-class with customizable default handler |
| + | StdIo buffering | Option D (line-TTY, block-pipe, panic-flush) |
| + | Phase 2 pilot | World-class CLI / config (Ruby-Gemfile ergonomics, dogfoods Reader/State) |
3. Roadmap items filed
- Tier 2 #8a —
extend/implunification.implkeyword 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
| Doc | Status | Purpose |
|---|---|---|
docs/ROADMAP.md | Edited (line 370 + Tier 2 + Tier 3) | Pause Joy-epic, file 3 items, pivot note |
docs/research/EFFECTS_IMPLEMENTATION_PLAN.md | Heavily edited | Design philosophy section added; 10 decisions closed; decision log brought current; error message quality bar + detection heuristic spec’d |
docs/research/EFFECT_SYSTEMS_NOTES.md | Created | Per-paper notes. Contains Leijen 2017 notes (first entry) + template for future entries |
docs/EFFECTS.md | Created (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.md | Created (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.
cannever 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:
- Does this work with zero effect annotations in simple programs? (If no, redesign.)
- If the user wants to be explicit, is the rich form at hand?
- 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 +
Ioalias) - ✅ 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.mdpopulated (both paper entries + template) - ✅
docs/EFFECTS.mdstubbed - ✅
docs/research/EFFECTS_LLVM_COMPILATION_MEMO.mdwritten (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:
- Add new lexer tokens:
TOK_CAN,TOK_EFFECT,TOK_WITH,TOK_HANDLE,TOK_TRY,TOK_REIFY,TOK_THROW,TOK_RESUMEinself-hosted/frontend/token_constants.qzand wire into the lexer. - Parse
effect Name ... endblocks inself-hosted/frontend/parser.qz—ps_parse_effect_decl. - Extend
ps_parse_functionto handle trailingcan Rowin function signatures. - Add QSpec for effect-declaration parsing +
can-suffix parsing. 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)
cankeyword in function signatureseffect ... enddeclaration blockwith ... do ... endhandler blocktryprefix keywordreify { ... }block- Delete
$trymacro frommacro_expand.qz
Type system (self-hosted/middle/typecheck*.qz)
Rowrepresentation inTcRegistry(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 machinerystd/parse— parameterizedThrows<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:
-
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.
-
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. -
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,tryas optional keyword,reifyblock,$trymacro deletion. -
Philosophy-as-governing-principle commitment: User’s phrasing drove the non-negotiables list. The philosophy filter now controls every remaining design decision.
-
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.
-
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.
-
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
- Read this doc (you are here).
- Scan
docs/EFFECTS.mdfor the user-facing design. - Scan
docs/research/EFFECTS_IMPLEMENTATION_PLAN.mdfor the committed decisions + philosophy. - Scan
docs/research/EFFECTS_LLVM_COMPILATION_MEMO.mdfor the concrete implementation strategy + Milestone breakdown. - Skim
docs/research/EFFECT_SYSTEMS_NOTES.mdfor paper takeaways (two entries: Leijen 2017 + Xie-Leijen 2020). - Start coding at Milestone A: lexer tokens +
effect ... endparsing +cansuffix in function signatures.self-hosted/frontend/token_constants.qz+self-hosted/frontend/lexer.qz+self-hosted/frontend/parser.qz. - Remember
quake guardbefore any commit that touchesself-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
$trymacro still exists in the codebase. Expected. It gets deleted in Phase 1 as part of thetrykeyword 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
cankeyword in the lexer yet. Expected. Phase 1 starts there. docs/EFFECTS.mdhasTBD in Phase 1placeholders. Intentional. The stub captures what’s committed; Phase 1 fills in spec + examples + error reference.
Good hunting.