diff --git a/CLAUDE.md b/CLAUDE.md index 63c60ac..76f2cfa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,3 +65,52 @@ as immutable scripture. reason and a sub-agent would have to redo the same reading. In that case I do the small change inline and note in the JOURNAL why I bypassed the agent. + +### Design rationale ≠ implementation effort + +When picking between design options, the rationale must come from +the language: semantics, structural fit, what the schema permits +vs. forbids, compositional clarity, future-proofing. **Implementation +effort is not a rationale.** "Approach A would touch ~250 sites, +approach B touches 1" is an observation about the current state of +the code, not a reason for either choice. + +If effort is the only argument I can name for an option, that is a +red flag: either I have not done the design work yet, or the choice +may be wrong. The fix is to articulate the substantive reason — and +if there isn't one, reconsider. + +Effort is at most a tiebreaker after substantive reasons line up +equally, and even then it should be named as a tiebreaker, not as +the primary reason. The 18a "Type::Fn metadata vs. Type variant" +call is the canonical anti-example: the right reason was semantic +locality (modes belong to fn-parameter positions, not to types in +general), and I retroactively had to add it. JOURNAL entries from +2026-05-08 record the lesson. + +### Direction freedom + +I have authority to choose the next iter, refactor, or feature +without asking. Wrong calls are recoverable: every commit is +reachable via git, branches and tags exist for sharper rollback +points (`pre-rc` is one such), and reverting one or several commits +is cheap. + +The cost of asking "what should I do next" — context-switch for +the user, latency on my side — exceeds the expected cost of an +occasional rollback. So when the queue is non-empty and the path +is clear, just pick and proceed. + +Bounce back to the user only when: + +- A queued option requires a real design judgement I have not + made myself (genuine architectural fork, multiple substantive + options none of which is clearly default). +- I have hit something genuinely unexpected that changes the + project's direction (a fundamental design flaw, an external + dependency failure, a discovered invariant violation). +- The user has explicitly asked for a checkpoint. + +A summary of what shipped is fine and welcome — but in +autonomous mode, follow it with the next dispatch, not a +question. diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 00f76fd..2216e1a 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -907,12 +907,39 @@ Type::Fn { enum ParamMode { Implicit, Own, Borrow } // default: Implicit ``` +The substantive reasons for per-position metadata over a +`Type::Borrow` / `Type::Own` variant approach: + +- **Semantic locality.** Modes are properties of fn-signature + parameter positions, not of types in general. `Int` does not + have a mode; a fn-parameter slot does. Embedding modes in + `Type` would let the schema express forms like + `(con List (borrow Int))` — syntactically possible, semantically + meaningless (you cannot separately own/borrow a list element + from the list it lives in). Decision 1 is "schema = data, + schema permits exactly what is meaningful"; per-position + metadata is the option that holds that line. +- **Compositional clarity.** A `Type` value's identity should + depend only on the type. Two functions with the same param / + ret types but different calling conventions share `Type::Fn.params` + and differ only in `param_modes`. That is the right factoring: + "what data does this carry" is one axis, "how is it transferred" + is another. Mixing them under a single hierarchy conflates the + two and makes both harder to reason about. +- **Future-proof against more position metadata.** If later iters + add other per-position properties (streaming receiver, captured- + by-closure, lifetime witness), they generalise as additional + metadata fields on `Type::Fn` — one consistent hierarchy. The + variant approach would force every new dimension into its own + `Type::*` variant (`Type::Streamed`, `Type::Captured`, ...) and + combinatorics blow up: `Type::Borrow(Type::Streamed(T))` versus + `Type::Streamed(Type::Borrow(T))` raise questions of canonical + ordering that don't exist when modes live in a flat metadata + vector. + `Implicit` is the legacy (pre-18a) state — semantically equivalent to `Own` but printed bare (`(con T)`, no wrapper). -`Own` and `Borrow` are explicitly annotated. The implementation -keeps `Type` itself unchanged, so unification, occurs, apply, -and ~190 other `Type` match-arms in the typechecker need no new -branch. +`Own` and `Borrow` are explicitly annotated. JSON canonical hash for every pre-18a fixture stays bit- identical: `param_modes` is skipped when every entry is @@ -924,6 +951,12 @@ throughout the 18-series; a later iter (deferred) makes the explicit annotation mandatory and rejects `Implicit` for boxed parameter types. +(An incidental observation, not a design reason: keeping `Type` +itself unchanged also avoids touching ~250 sites across the +typechecker / desugar / codegen that match on `Type` variants. +This is a tiebreaker, not a rationale — the substantive reasons +above are what justify the choice.) + **Iter 18c/18d — new `Term` variants.** ``` diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index cb99fa8..2bbe70b 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -6098,3 +6098,46 @@ explicit modes) becomes the first program subject to the check; its current shape (borrow-then-own on `xs`) is exactly what the check should accept, so 18c.2 starts as a "green for the existing test" iter. + +## 2026-05-08 — Correction: 18a Schema-choice rationale + +User flagged that the 18a "per-position metadata vs. `Type` +variants" decision was justified in the JOURNAL / DESIGN.md / +commit message primarily by implementation effort ("avoids +~250 match-arm sites"). That is not a design rationale. The +choice may still be right (it is), but the *reasons* it is +right have to come from the language, not from the cost of an +alternative. New CLAUDE.md section "Design rationale ≠ +implementation effort" makes this a binding orchestrator rule. + +The substantive justification, retroactively recorded in +`docs/DESIGN.md` Decision 10's Schema-additions block: + +1. **Semantic locality.** Modes are properties of fn-signature + parameter positions, not of types in general. Embedding modes + in `Type` would let the schema permit forms like + `(con List (borrow Int))` — syntactically possible but + semantically meaningless. Decision 1's "schema permits + exactly what is meaningful" argues against the Type-variant + approach. +2. **Compositional clarity.** A `Type` value's identity should + depend only on the type. Calling-convention information + (own/borrow) is orthogonal to type identity; mixing them + conflates two axes that should be factored apart. +3. **Future-proof against more position metadata.** Per-position + metadata generalises naturally to additional dimensions + (streaming, captured, lifetime witness). The Type-variant + approach would force every new dimension into its own + `Type::*` variant and produce combinatoric ordering questions + (`Borrow(Streamed(T))` vs `Streamed(Borrow(T))`) that don't + arise when modes live in a flat metadata vector. + +The match-arm count remains true as an observation but appears +in DESIGN.md only parenthetically, marked explicitly as a +tiebreaker rather than a rationale. + +This entry stays as a record because the original mistake is +informative — design discipline corrupts faster than I notice +when I let "implementer-friendly" creep into the slot reserved +for "language-honest". The CLAUDE.md rule exists so the next +session catches this earlier.