Quartz v5.25

Operation Piezoelectric Effects — Implementation Plan

Living plan document for the algebraic effects epic in Quartz.

Epic name: Operation Piezoelectric Effects (commit tag [piezo], casually “Piezo”). Named after the piezoelectric effect — the physical property of quartz crystals that converts mechanical stress into electric charge. Curie brothers, 1880.

Status

Design Philosophy: Invisible by Default, Rich When Wielded

Effects are pervasive internally — the language is effect-typed all the way down. But they are optional externally. The user engages at three levels, all first-class:

LevelWhat the user writesExample
InvisibleNo can annotations. No try. No with. Code reads as if effects don’t exist. Prelude-installed default handlers do the right thing.def main(); puts("hello"); end
ImplicitEffect rows inferred by the typechecker; visible in LSP hover, quartz doc, error messages. Code stays clean.def greet(name) = puts("hi, " + name) — inferred can StdIo, not annotated
ExplicitFull can rows, hand-crafted with handlers, try markers on propagation points, effect-polymorphic combinators, sandboxing.def sandbox_app() can FileIo = ... composed with with net_denied do ... end

The substrate is the same at all levels. The surface is progressive disclosure.

Non-negotiables

  • Trivial programs have zero effect ceremony.
  • can annotations are never required for correctness — inference fills them in. They document or constrain.
  • The rich explicit features (with catch, row polymorphism, try markers, effect-polymorphic map) are first-class, reachable from any scope — not gated by mode switches, pragmas, or “advanced mode.”
  • Writing invisibly never locks you out of wielding — you can drop into explicit at any expression boundary.
  • Effects are the substrate, not an “advanced feature layer.” Rules about visibility are about noise, not capability.

Philosophy alignment (cross-cutting)

This matches Quartz’s existing pattern:

  • Const by default, var to opt in
  • Infer types everywhere, annotate at boundaries
  • Types rich at compile time, erased at runtime
  • Implicit it in blocks, explicit parameter names when you want them
  • Open UFCS: three levels of dispatch (impl method → trait method → open UFCS)
  • Async colorless — no coloring tax
  • Trait auto-satisfaction (no impl Container for Queue needed when members already satisfy)
  • Drop/RAII automatic; custom Drop for cleanup

Effects take the next step: error handling, Io, state, diagnostics, allocator, async — all substrate. You wield them when you want to.

Filter for downstream decisions

When picking between options in the remaining design space, the tie-break is:

  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?

What’s committed (decisions made in planning so far)

Syntax

  • can keyword for effect annotations (not ! — Quartz already uses ! as Option unwrap)
  • Effect rows with comma-separated labels: can Io, Throws<E>
  • with ... do ... end for handler installation
  • effect ... end blocks for effect declaration
  • Inference by default; annotations at API boundaries only

Type system

  • Row polymorphism, Rémy-style unification
  • HM-extended inference including effect row variables
  • Effects are part of function types: (args) → ε return (Koka convention)
  • Every function has an effect row (empty ⇒ pure, equivalent to total)

Migration strategy

  • Stdlib-first pilot sequence: lograndom/clock → QSpec capture helpers
  • Result: kept as a type; the convention of returning Result for control-flow errors migrates to Throws. Result stays for error-as-data use cases.
  • opt! (Option unwrap): may become sugar for throw(None); decision deferred
  • Compiler dogfooding: Phase 5, not Phase 1. Prove machinery on stdlib first.

Soundness

  • Effect system is load-bearing, not decorative
  • No unchecked-exception escape hatches
  • Requires operational semantics + soundness proof (feeds the existing formal spec work)

Main and defaults (§8.5 of future EFFECTS.md)

  • Prelude installs default handlers for Throws, Panic, Log, FileIo, NetIo, ProcIo, StdIo, Clock, Random, Env (full v6.0 blessed Phase-1 set)
  • Trivial def main() programs have no effect ceremony
  • Custom handlers are strictly additive overrides

Default handler behaviors (committed 2026-04-18)

Panic effect — confirmed as a first-class effect, not a separate language builtin. Default handler prints a structured error message + stack trace to stderr, flushes all buffered output, and exits with code 101. Custom handlers unlock: structured JSON crash reports, debugger hooks (trigger int3 on panic), test-harness capture (QSpec installs a handler so a panicking test doesn’t kill the runner), cleanup-before-die (flush logs, close sockets, delete temp files), telemetry emission.

StdIo effect buffering — Option D (line-buffered for TTY, block-buffered for redirect/pipe, auto-flush on panic via the Panic default handler). Rationale:

  • TTY → line buffering is what users expect from a REPL/CLI; each line appears as written
  • Pipe/file → block buffering gives libc-level throughput when output is redirected
  • Auto-flush on panic is the killer missing guarantee in libc-style programs (last writes lost on crash); the Panic handler + StdIo handler composition provides it for free
  • Custom StdIo handlers enable: in-memory capture for tests, unbuffered mode for debugging, structured emit (JSON per write) for log aggregators

Both behaviors are example cases of “invisible default, rich when wielded” — the user who writes puts("hi") gets sensible behavior with zero config; the user who wants custom behavior installs a handler.

Error-handling surface cleanup (committed 2026-04-18)

Rather than stack Throws on top of the existing ! / $try / Option / Result / Panic surface, unify the whole thing into four clear tiers:

TierSemanticsCatchable?Typed?Form
Assertion”I proved this; crash if wrong”NoNox! (panics)
Control flow (effect)“Empty/Err is a legit outcome for my caller”Yescan Throws<E>try x, throw(e)
Error as data”I want the error value to store/pass/serialize”N/A (data)Return typeResult<T, E>
Panic”Unrecoverable (OOM, invariant)“NoNopanic(msg)

Per user-intent mapping (each intent gets exactly one canonical form):

IntentCanonical formNotes
Testx.some? / x.ok?Unchanged
Narrowx is Some(v)Unchanged
Destructurematch, if letUnchanged
Assertx! (panics, uncatchable)Unchanged
Propagatetry x (prefix keyword, optional per philosophy)Replaces $try(x) macro
Defaultx ?? default, x.unwrap_or(d)Both kept (ergonomic chaining)
Chain-througha?.b?.cUnchanged
Transformx.map(f), x.flat_map(f)Unchanged
Reify (effect→Result)reify { ... } blockNew
Convert (Result→effect)try resSame form as propagate

Key decisions:

  • try is optional sugar, not mandatory (per philosophy: invisible default, explicit when wielded). Implicit propagation still happens through effect-row inference; try is for when the user wants the propagation visible to a reader.
  • ! stays as assertion (panics, uncatchable, not effect-typed). Aligns with Rust .unwrap(). Three distinct intents keep three distinct forms (! / try / match).
  • $try(x) macro deleted in favor of try keyword.
  • Option propagation uses unified Throws with an Empty payload (not a separate Partial effect). Simpler substrate; catchable via standard with catch handlers.
  • throw is an expression (Never type), consistent with return and panic.
  • Error messages grade down by engagement level: invisible-level users see “uncaught throw, add can Throws<X> or install a handler”; explicit-level users see full row-diff vocabulary.

Compilation model

  • Committed (2026-04-18): evidence-passing (Koka, Leijen 2017 style)
  • Single-shot direct-call on the fast path; multi-shot exists as a slower escape hatch
  • Rejected alternatives:
    • Stack-copying / fibers (OCaml 5) — uniform but heavy per-op cost; per-target stack-switch assembly burden
    • Capability-passing (Effekt) — highest type-theory load; duplicates what evidence already gives us
  • Rationale: every effect on the Quartz roadmap (Throws, Io, Log, Clock, Random, State, Reader, Async) is single-shot. Ndet (multi-shot) is the only Phase 4 candidate and explicitly deferred indefinitely as nice-to-have. Evidence-passing’s single-shot fast path wins the 95% case at a fraction of the impl burden of fibers.

Open decisions (must close before Phase 1 begins)

  1. Compilation model — ✅ CLOSED 2026-04-18: evidence-passing. See Decision log.
  2. Scoped / duplicate labels — ✅ CLOSED 2026-04-18: allow duplicates with lexical shadowing (Model B, Koka-style). Inner handlers shadow outer; throw binds to innermost matching handler; re-throw propagates outward. Matches Koka, Effekt, OCaml 5, Eff. See Decision log.
  3. Initial effect set — ✅ CLOSED 2026-04-18: 14 blessed atomic effects + 1 row alias. Phase 1 pilot: Log first, Parse second. See Decision log for full set.
  4. opt! sugar — ✅ CLOSED 2026-04-18: ! stays as assertion / panic path, NOT effect-typed. See Error-handling surface cleanup below and Decision log.
  5. Effect row subtyping — ✅ CLOSED 2026-04-18 (see #8 below): flat row equality + tail polymorphism. Subtyping rejected.
  6. Error message quality bar — ✅ CLOSED 2026-04-18: six operational commitments + engagement-level detection heuristic. See Decision log and Phase 1 § “Error messages.”
  7. Io granularity — ✅ CLOSED 2026-04-18: hybrid. Atomic labels FileIo, NetIo, ProcIo, StdIo, Clock, Random; Io is a row alias for their union. Stdlib signatures default to atomic (information-dense); Io alias available as escape hatch. See Decision log.
  8. Effect row subtyping (Decision #5) — ✅ CLOSED 2026-04-18 as fallout of hybrid: flat row equality + tail polymorphism (Koka-style). Row subtyping rejected (HM inference interaction, error-message complexity, rejected by Koka for the same reasons). Hybrid model works cleanly with flat rows via tail-polymorphic row variables can Io | ε.

Phase 0 — Research completion

Goal: understand the design space deeply enough to make Phase 1 decisions irrevocable.

Entry criteria (met):

  • Notation primer complete
  • Abstract + §1 of Leijen 2014 read and unpacked

Deliverables:

  • Read Leijen 2014 §2–6 (rest of the paper) — deferred, largely subsumed by 2017 paper
  • ✅ Read Leijen 2017 “Type Directed Compilation of Row-Typed Algebraic Effects” (2026-04-18 — validates type system + scoped labels + inference rules)
  • ✅ Read Xie & Leijen 2020 “Effect Handlers in Haskell, Evidently” (2026-04-18 — validates evidence-passing compilation; surfaced three-kind operation taxonomy as critical design insight)
  • Skim Rémy row polymorphism (for unification details) — deferred, papers cover enough
  • Skim Effekt (Brachthäuser) for capability-passing contrast — deferred, capability-passing rejected
  • Skim OCaml 5 effects RFC for the alternative compilation model — deferred, fibers rejected
  • ✅ Populate docs/research/EFFECT_SYSTEMS_NOTES.md with per-paper notes (Leijen 2017 + Xie-Leijen 2020 entries)
  • ✅ Close each open decision above with a position in the decision log (10/10 decisions closed)
  • NEW: LLVM-specific compilation memo — docs/research/EFFECTS_LLVM_COMPILATION_MEMO.md

Exit criteria:

  • ✅ Compilation model chosen, with rationale (evidence-passing, Xie-Leijen 2020; rationale in both papers + LLVM memo)
  • ✅ Initial effect set curated (14 atomic effects + Io alias; Ndet + Yield deferred)
  • ✅ Syntax locked (can, effect...end, with...do...end, try, reify {}, throw, resume)
  • docs/EFFECTS.md stubbed with full phase structure

Phase 0 complete 2026-04-18. All exit criteria met. Phase 1 can begin.

Estimate: 3–5 quartz-days · 4–6 sessions

Phase 1 — Throws pilot (end-to-end)

Goal: working Throws effect, one stdlib module migrated, default main handler, real error messages. Proves the full machinery on the simplest useful effect.

Entry: Phase 0 exit met.

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

  • can keyword in function signatures
  • effect ... end declaration block
  • with ... do ... end handler block
  • Effect rows in generic parameter lists

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

  • Row representation: labels + optional tail variable
  • Row registry in TcRegistry
  • Row unification (Rémy-style, no duplicate labels yet)
  • Row substitution, occurs check, pretty-print
  • Effect row on function types (extend structured function type representation)
  • Call-site effect propagation: inherit callee’s row into caller’s
  • 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
  • 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

Stdlib

  • std/log module migrated (simplest effect) OR std/parse (clear Throws use case)
  • Default main-handler installation via prelude injection

Testing

  • QSpec specs for Throws: throw, catch, row in signatures, inference
  • Smoke programs (brainfuck.qz, style_demo.qz) continue to pass
  • Fixpoint (gen1 == gen2 byte-identical) holds through the transition

Error messages (quality bar — six commitments)

Every Phase 1 effect error message must:

  1. Point at the offending source location — file:line:col, rustc-style caret underlining, with the effect-producing expression clearly marked.
  2. Name the effect in plain English first, row-syntax second — “this can fail” before “adds Throws<ParseError> to the row.”
  3. Suggest a concrete fix — never just “error: row mismatch.” Always include either (a) the signature change to make, or (b) the handler to wrap with. Suggested code is compilable as shown.
  4. Grade by engagement level. See detection heuristic below.
  5. Never leak implementation vocabulary — no “unification failure,” no “ρ₁₂₇,” no “row variable tail,” no “substitution.” Use “row,” “handler,” “catches,” “throws,” “effect.”
  6. Runtime uncaught-throw reaches main formatted with a stack trace and a remediation hint — not a bare panic: ....

Engagement-level detection (committed 2026-04-18)

User is classified per-error-site by the typechecker using this heuristic (first match wins):

TriggerLevelMessage form
User code uses row variables (can X | ε), explicit handler composition, or effect-polymorphic combinators in the enclosing scopeExpertTerse row-diff. Zero teaching. No sidebars. expected row {FileIo, Throws<E>}, found {FileIo}.
Enclosing function has can ... annotation OR an enclosing with ... do ... end blockExplicitFull row-diff + concrete fix shown as code. Skip the conceptual teaching paragraph.
No can annotations anywhere in enclosing function, no with in scopeInvisible / learningPlain-English description first (“this can fail”), then the concrete fix, then why (one sentence on what an effect row is). Show both the signature-widening path AND the handler-wrapping path.

Filter principle (user quote, 2026-04-18): “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.”

Insulation means: the user who has never written can in their life never sees the words “row,” “label,” or “unification” in an error message. They see “this can fail — handle it here or declare it on your function signature.” Period.

Error-case coverage (Phase 1 must ship)

  • Uncaught throw — call-site view at Invisible level (full teaching) and Explicit level (row-diff).
  • Row mismatch at call site — show caller row, callee row, specific missing label(s), concrete fix.
  • Handler catches wrong label — point at both the handler declaration and the throw site, show combined-handler fix.
  • Inference ambiguity — when a function’s row can’t be determined, name the ambiguous binding in plain English, suggest an annotation.
  • Effect in pure-context — if a Fn(Int): Int (no row) binding receives a function that has effects, explain the row mismatch at binding type and at the lambda, not just at the call.

Phase 1 kill criterion

If any of these messages cannot be produced without a major type-system rewrite, Phase 1 is at risk. The messages ARE the product — not decoration.

Exit criteria:

  • All Phase 1 QSpec specs green
  • Fixpoint verified
  • Smoke programs pass
  • Error-message quality bar met (no row-unification jargon leaks)
  • Default main handler works end-to-end

Estimate: 5–7 quartz-days · 7–10 sessions

Phase 1 risks

  • Row unification edge cases harder than expected
  • Evidence-passing perf worse than direct-call baseline
  • Interaction with existing generic / HM inference surfaces latent bugs
  • Error messages require major type-system introspection to make teaching-grade

Phase 1 kill criteria

  • 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

Phase 2 — State and Reader

Goal: multi-effect composition works. Inference stress-tested with multiple labels per row.

Entry: Phase 1 exit met.

Deliverables:

  • State<S> effect: get / set operations
  • Reader<R> effect: ask / local operations
  • Rémy merging across multi-effect rows
  • Pilot: world-class CLI / config system — Ruby-Gemfile-quality ergonomics (explicitly NOT Python-style). Replaces ad-hoc argv parsing across examples and quake. Uses Reader<Config> for config, State<ArgState> or similar for parse state. Dogfoods the new effect machinery AND delivers immediate DX win for Quake tooling.
  • Handler composition: with state(0), reader(config) do ... end

Exit:

  • Multi-label rows unify cleanly
  • Inference produces predictable types (no surprising polymorphism collapse)
  • QSpec covers state + reader + composition
  • Pilot subsystem works

Estimate: 3–5 quartz-days

Phase 3 — Async-as-effect migration

Goal: migrate colorless async from its current special-case MIR lowering to the general effect machinery. Proves effects are Quartz’s unifying mechanism.

Entry: Phases 1–2 exit met.

Deliverables:

  • Async effect: await, suspend, spawn operations
  • Handler = scheduler
  • Migrate existing $poll state-machine lowering to effect-lowering
  • All existing async code continues to work (no user-visible regression)
  • Effect-polymorphic async combinators (go, await)

Exit:

  • All existing async QSpec passes unchanged
  • Special-case async MIR lowering deleted (Prime Directive 7, no compat shim)
  • Effect-polymorphic map / filter / etc. work with async functions
  • Scheduler intrinsics integrate cleanly with handler discharge

Estimate: 7–10 quartz-days · 1.5–2 calendar weeks. Highest risk. Load-bearing.

Phase 3 kill criteria

  • Migration requires > 2x the LOC of current async impl → redesign
  • Scheduler performance regresses > 10% → redesign
  • Effect-polymorphic call sites can’t monomorphize cleanly → reconsider compilation model

Phase 4 — Non-determinism and generators

Goal: multi-shot handlers. The hard case — handler resumes continuation more than once.

Entry: Phases 1–3 exit met.

Deliverables:

  • Ndet effect: choose operation
  • Generator syntax via Yield<T> effect
  • Continuation-capture machinery where evidence-passing isn’t enough
  • Backtracking handler example
  • Effect-polymorphic generators (the Q10 benchmark question comes home to roost)

Exit:

  • Multi-shot handlers work on non-trivial programs
  • Generators compose with other effects
  • Performance acceptable (continuation capture only when actually needed)

Estimate: 5–7 quartz-days

Phase 5 — Compiler dogfooding

Goal: the payoff. Rewrite compiler subsystems using effects, dramatically reducing plumbing.

Entry: Phases 1–4 exit met. Effect machinery stable and battle-tested.

Deliverables (rough order, each a sub-phase):

  1. Diagnostics as effect (biggest single win, ~15% LOC reduction in typecheck + codegen)
  2. Type context / scope state as State<TcCtx>
  3. LLVM IR emission as Emit effect
  4. Module resolution with Fs effect (testability win)
  5. Symbol / intern tables as effects

Exit:

  • Compiler LOC reduced measurably (target: 10–15%)
  • Compilation performance not regressed
  • Fixpoint holds through each migration
  • Error-reporting pipeline simpler / more readable

Estimate: 2–3 quartz-weeks · 20–30 sessions. Large, but each sub-migration is contained.

Cross-cutting: teaching + docs

Ships with Phase 1 (non-negotiable):

  • docs/EFFECTS.md tutorial chapter
  • 10–15 curated examples in examples/effects/
  • CLAUDE.md section on effect idioms (teaching material for future contributors, human or LLM)
  • Effect-system error-message style guide

Accumulates through Phases 2–4:

  • More examples per new effect
  • Pattern cookbook (common handler patterns, common anti-patterns)
  • Migration guide for existing code

Ships with Phase 5:

  • Retrospective on the dogfooding migration
  • Performance benchmarks, before/after
  • Compiler architecture doc updated

Project-level kill criteria

Conditions that trigger abandonment or major redesign of the effects direction:

  • Phase 3 async migration requires > 2x current async impl
  • Effect polymorphism inference fails without pervasive annotations
  • Evidence-passing > 10% slower than direct-call baseline
  • Linear/move-semantics interaction forces unacceptable handler restrictions
  • Error messages can’t be made teaching-grade in Phase 1

Estimation summary (quartz-time)

PhaseTraditionalQuartz-timeSessions
0 — Research1–2 weeks3–5 days4–6
1 — Throws pilot3–4 weeks5–7 days7–10
2 — State/Reader2 weeks3–5 days4–6
3 — Async migration4–6 weeks7–10 days10–14
4 — Multi-shot3 weeks5–7 days6–10
5 — Compiler dogfooding8–12 weeks2–3 weeks20–30
Total21–29 weeks5–8 weeks~50–75 sessions

Phases 1 and 3 are load-bearing. If both succeed, effects become Quartz’s organizing principle. If Phase 1 succeeds but Phase 3 doesn’t migrate cleanly, keep Phases 1–2 as a new feature and defer the unification story.

Decision log

Append-only. New entries at the bottom, never rewrite history.

  • 2026-04-17can chosen over ! (collision with Option unwrap). Strongman revisited after pushback; see earlier planning session.
  • 2026-04-17 — Effect inference is a Phase 1 hard requirement, not a later addition. Based on Koka’s empirical track record.
  • 2026-04-17 — Stdlib-first migration order committed. Compiler dogfooding explicitly deferred to Phase 5.
  • 2026-04-17 — Systems-language positioning: Quartz qualifies (LLVM IR, self-hosting, manual memory, cross-platform, no GC). Koka does not (C backend, Perceus RC). Phrase “first systems language with first-class algebraic effects” cleared for §1 of EFFECTS.md.
  • 2026-04-17 — §8.5 “Main and the Default Handler Stack” added as a distinctive Quartz design element. Prelude-installed default handlers mean trivial def main() has zero effect ceremony.
  • 2026-04-17 — Evidence-passing is the leading compilation model. Explicit commit deferred until Leijen 2017 is read.
  • 2026-04-18 — Compilation model committed: evidence-passing (Xie & Leijen 2020 “Effect Handlers in Haskell, Evidently” style — NOT Leijen 2017, which is type-directed selective CPS). Committed on pragmatic grounds: every effect on the Quartz roadmap is single-shot. Ndet was the sole multi-shot case; deferred indefinitely as nice-to-have. Stack-copying/fibers rejected (uniform but heavy per-op, per-target assembly burden); capability-passing rejected (highest type-theory load, duplicates evidence’s value proposition). Leijen 2017 read (2026-04-18, same session): validates type system, row polymorphism, scoped labels with duplicates (Model B), OPEN/CLOSE inference rules, and selective translation as the >80% cost reduction that makes the approach practical. But 2017 describes selective CPS, not evidence-passing — commitment validation continues in Xie-Leijen 2020 (next reading target). One design hole surfaced regardless of model: polymorphic-effect functions (map, filter, fold) need special compilation (worker-wrapper + runtime dispatch) — ~20% core-library code bloat per Koka’s experience.
  • 2026-04-18Io granularity committed: hybrid. Atomic labels FileIo, NetIo, ProcIo, StdIo, Clock, Random (exact set TBD with Decision #3). Io is a row alias for their union. Stdlib signatures default to atomic labels (information-dense for readers and for sandbox handlers); Io alias available as escape hatch for code that doesn’t care. Implies positioning commitment: sandbox-friendly runtime is on Quartz’s future roadmap (capability-style effect handlers become the enforcement mechanism — with net_denied do -> run_untrusted() end).
  • 2026-04-18 — Effect row subtyping rejected: flat row equality + tail-polymorphic row variables (Koka-style). Subtyping’s HM-inference interaction and error-message complexity are well-known pain points; Koka tried it and walked back. Hybrid Io model works cleanly with flat rows via can Io | ε tail polymorphism for call-site openness.
  • 2026-04-18Design Philosophy committed: “Invisible by Default, Rich When Wielded.” Three engagement levels (invisible / implicit / explicit), all first-class. Non-negotiable: trivial programs have zero effect ceremony; can never required for correctness; rich features reachable from any scope, not gated. Governs all downstream decisions.
  • 2026-04-18Panic default handler committed: print structured message + trace to stderr, flush buffered output, exit 101. Custom handlers unlock crash reports, debugger hooks, test-harness capture, cleanup-before-die. Panic remains uncatchable as an effect semantically (no catching in user with catch) but the default-handler pattern gives the rich-wielded escape hatch.
  • 2026-04-18StdIo buffering discipline committed: Option D (line-buffered for TTY, block-buffered for redirect/pipe, auto-flush on panic via composed Panic handler). Resolves the libc “pipe-to-grep shows nothing” surprise and the “last writes lost on crash” footgun in one pattern.
  • 2026-04-18Phase 2 pilot committed: world-class CLI / config system (Ruby-Gemfile-quality, not Python-ecosystem style). Replaces ad-hoc argv handling; dogfoods Reader<Config> + State<ArgState>. Quake tooling gets a DX win as a byproduct.
  • 2026-04-18Scoped labels committed: Model B — labels can appear multiple times in a row; handlers shadow lexically; throw binds to innermost matching handler; re-throw propagates to next outer. Matches Koka, Effekt, OCaml 5, Eff. Typechecker takes on label-scope discipline as the cost; expressiveness gain (nested catch, handler pipelining, re-throw) is worth it. Model A (flat, no duplicates) rejected: simpler typechecker but forces users to rename errors to layer catches, which is friction for no gain.
  • 2026-04-18Initial effect set committed (v6.0 blessed stdlib effects): 14 atomic effects + 1 row alias.
    • Phase 1 (Throws pilot infrastructure): Throws<E>, Panic, Log, FileIo, NetIo, ProcIo, StdIo, Clock, Random, Env
    • Phase 2: State<S>, Reader<R>, Alloc
    • Phase 3 (migration): Async
    • Row alias: Io = {FileIo, NetIo, ProcIo, StdIo, Clock, Random, Env}
    • Deferred: Ndet (multi-shot, indefinitely nice-to-have), Yield<T> (part of deferred Phase 4 — generators stay compile-time state-machine lowering until Phase 4 ever ships)
    • Explicitly subsumed / not blessed: Debug/Trace (subsumed under Log with severity levels), Exit (use Panic or Throws<ExitRequested>)
    • Env rationale: atomic effect (not subsumed under ProcIo). Security-audit use case — sandboxed runtimes and security software want to intercept every env var read/write. Default handler wraps getenv/setenv; custom handlers enable deny-all, log-all-reads, or deterministic fake-values-for-tests. Include in Io row alias for ergonomics; atomic form available when precise capability matters.
  • 2026-04-18Phase 1 pilot sequencing committed: std/log module migrated first (mechanical proof — simplest possible effect, no error type, single-line handler), then std/parse migrated (proves parameterized Throws<E> end-to-end). Debugging loop: Log breaking = infrastructure bug; Parse breaking after Log works = Throws-specific bug. Smaller test before bigger test.
  • 2026-04-18Error message quality bar committed: six operational commitments (source location + carets, plain-English first, concrete compilable fix, grade by engagement level, never leak implementation vocab, runtime uncaught-throw shows trace + hint) + engagement-level detection heuristic (Invisible / Explicit / Expert, first-match-wins on per-error-site basis). Governing principle (user quote): “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.” Operational: invisible-level users never see “row,” “label,” “unification,” “ρ,” “substitution” in any effect error message. Detection: expert = uses row variables or effect-polymorphic combinators; explicit = function has can annotation OR enclosing with; invisible = neither.
  • 2026-04-18Xie & Leijen 2020 read (“Effect Handlers in Haskell, Evidently”). Validates evidence-passing compilation commitment. Surfaced CRITICAL design insight: three-kind operation taxonomy. Every effect op is classified at declaration as value (constant resume), function (tail-resumptive, evaluates in-place, direct indirect call), or operation (general, captures continuation via multi-prompt control). In Quartz’s committed effect set, ONLY Throws.throw (aborts — free), Async.{await, spawn, suspend} (Phase 3), Panic.panic (aborts), and deferred Ndet.choose are operation-kind. All other 10+ ops are function-kind and hit the fast path. Benchmark evidence in paper §6: evidence-passing library (EV) is constant-time in deep effect stacks, ~1.5x faster than next alternative in realistic workloads, within 1% of pure on abort-like effects. Key implementation insight: operation-kind ops reuse Quartz’s existing $poll state-machine lowering — Phase 3 (async migration) is reframing, not rewriting. Full compilation strategy documented in EFFECTS_LLVM_COMPILATION_MEMO.md. Phase 0 exit criteria all met.
  • 2026-04-18Error-handling surface unified into four tiers (Assertion / Effect / Data / Panic) with exactly one canonical form per user intent. Resolves Option/Result/!/$try/Panic ambiguity. Sub-decisions:
    • ! stays as assertion (panics, uncatchable, not effect-typed). Closes Open Decision #4.
    • $try(x) macro deleted — replaced by try prefix keyword.
    • try is optional sugar, not mandatory (per Design Philosophy — invisible default, loud when wielded).
    • Option propagation uses unified Throws<Empty> (not a separate Partial effect). One substrate.
    • throw is an expression with Never type.
    • reify { ... } block added for effect→Result conversion.
    • ?? and .unwrap_or() both kept (ergonomic chaining, minor redundancy acceptable for “sexiest API” surface).
    • Error messages grade down by engagement level.

Next session when returning

  1. Confirm this plan’s shape or push back on phases / estimates
  2. Resume reading Leijen 2014 at §2 (motivating examples + concrete syntax)
  3. At end of §2, first explicit decision point: commit or defer the Phase 1 effect set