Derive: composes Equals had a coherence row and no body #229
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/229-derive-composes-equals-had-a-coherence-row-and"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Derive: composes Equals had a coherence row and no body
Along the way:
x::Stringdid nothing, and said nothing about itTouches 40 files:
docs/03-types.md,docs/examples/bounded_coercion_is_written/Main.ck,docs/examples/bounded_coercion_is_written/expect,docs/examples/bounded_coercion_is_written/project.ck,docs/examples/bounded_narrowing_refuses/Main.ck,docs/examples/bounded_narrowing_refuses/expectand 34 more.Approving. I would have split
src/rust/crates/ck_load/src/derive/tests.rsdifferently, but not enough to hold this up.@ -0,0 +119,4 @@let mut this = Self {aliases: HashMap::new(),explicit: HashSet::new(),candidates: Vec::new(),candidatesallocates a fresh buffer per call. If this runs per frame or per token, can the buffer be reused?@ -0,0 +66,4 @@}#[test]fn a_field_with_no_equals_is_left_alone() {a_field_with_no_equals_is_left_alonereads, decides and writes in one function. The deciding part is the piece I would want to test on its own. Worth splitting?Split. The decision in
a_field_with_no_equals_is_left_aloneis a pure function now, with its own tests insrc/rust/crates/ck_load/src/derive/tests.rs.It runs per token. The buffer behind
candidateslives on the struct insrc/rust/crates/ck_load/src/derive.rsnow and is cleared rather than reallocated.The wrong reasoning is one sentence, and it was written down: coherence phase 2 said the derived body was "deferred with the instantiation set (no code-gen consumer yet); the *row* is what TY4c needs." A row with no body is not a deferral, it is a promise nothing keeps. The mechanism. `register_derives` allocated an `ImplId` carrying a span and zero methods, then inserted it at `(Equals, [Pt])`. That is enough for `T is Equals` to answer Yes in one hash probe, so `p == q` resolved through TY5, typed `Bool`, and `ckc check` reported the program CLEAN; monomorphization then found no `FnId` behind that `ImplId`, and `emit_call_value` fell past user-fn, machine-op, syscall and the tick grain to its terminal `Err(CodegenError::Unsupported(callee_span))` — `unsupported Floor IR form`, at the surface an author reaches last. `plan_traits.md` §3 forbids the cheap repair ("there is no compiler-level `==`"), so the derive owes a real hand-writable impl. `ck_load::derive` writes it source-to-source on the §2a actor desugar's precedent, in `finish` where the whole closure is loaded, and CONSUMES the sugar: the `Equals` token is cut out of the `composes` clause by byte range (every other trait keeps its text and its generic arguments) and the page gains impl Equals<Pt> { fn equals(a: Pt, b: Pt) -> Bool = (a.x == b.x && a.y == b.y) fn not_equals(a: Pt, b: Pt) -> Bool = !((a.x == b.x && a.y == b.y)) } The conjunction folds LEFT with explicit parentheses because CkLang has no universal associativity — `a && b && c` is E0146. Two fields hide that; `todo_effect`'s three-field `Task` and `Dijkstra`'s `Arc` found it. What moved: `ck_load` only. `ck_types` was NOT touched — with the clause consumed, phase 2 never sees a derived record, and the generated impl registers through phase 1 like any hand-written one. Its comment is rewritten to say why its own reasoning was wrong. What did NOT move: the derive fires only where the equality it writes genuinely resolves — a field type reaching a real `impl Equals<…>`, or another derived record, over a project-global fixpoint with `type` aliases followed. That is stricter than E0711 on purpose: a `data … composes Equals` holds a coherence row no `==` can reach (its values type as the VARIANTS — the still-OPEN `data_sum_…` cell), so E0711 passes the stdlib's `record LogLine { level: LogLevel, … }`, which is in every app's closure, and a generated body over it would not resolve. Records out of reach — `snake`'s `Player { body: List<Pos, 256>, … }` — keep their clause, their bodyless row and today's behaviour byte for byte. Generic records are refused: one coherence row cannot serve every instantiation. `data` is untouched, and stays OPEN. Ledgers. `BROKEN_CELL_COUNT` 43 -> 42; the cell is renamed `auto_derived_equals_on_a_record` and declares `runs: 42`. `CELL_COUNT` 440 -> 442 for the two cells that make it discriminate: `auto_derived_equals_discriminates_on_a_field` (fields differ -> 7, so a body answering `true` blindly cannot pass) and `auto_derived_equals_not_equals_is_derived_too` (the `!=` glyph, so the derive owes both of the trait's methods). R11 `ck_load/src` 9243 -> 9863, a RAISE carried with a written justification in its doc log — owner-visible, veto = revert; `ck_load/src/desugar` does not move. Gates. `./scripts/test.sh` ALL GREEN — 4 legs: unit 1364, corpus 106 apps, capability 442 cells, docs 35 expectations, 0 failures. Workspace `cargo test --workspace --tests`: 1621 passed, 1 failed. `cargo fmt --all -- --check` 0 diffs; `cargo clippy --workspace --all-targets -D warnings` 0 errors 0 warnings. The one red is `r3_no_file_exceeds_loc_cap` — `ck_driver/src/lib.rs: 1323 lines (cap 862)`. STANDING, not caused here: the file is byte-identical to HEAD and untouched by this commit, and plan.md §1 already tracks it as the one open item ("splitting the driver root — a slice, not a cleanup").x::Stringdid nothing, and said nothing about it 90e15b3920`plan_values_02.md` §1a makes the `fn` boundary strict in both directions and names exactly one way across it: "an argument's type must already equal the parameter's type … or the programmer coerces it explicitly with a turbofish." It then says the resulting call-site verbosity is "accepted, not a defect to fix … load-bearing". That only holds while the verbose spelling works. It did not. `synth_local_ref` and `synth_coercion` both opened with a scalar test and returned the receiver's type unchanged for anything else: if !from_float && !from_intlike { // A non-scalar local carries no width conversion; keep its type. return local_ty; } So `widen(a::String)` over a `String<2>` local produced `String<2>`, the E0702 fired on the value the author had just coerced, and nothing anywhere pointed at the coercion. The strict boundary had no escape at all: the only route across was to launder the value through an annotated `let`, which is a different surface with a different law. That is the whole of "String is the worst surface" — not a missing subsumption rule, a missing escape hatch. The comment was half right. A `String` has no WIDTH to convert, so the scalar clause (`ireduce`/`sextend`) genuinely does not govern it. §1.4 has a second clause that does — container into same-constructor, same-element-type container, bound differing only, fit-gated one-way — and §5 prices it: an identity retype — a coercion between two types whose lowered word layout is identical (e.g. the `String<6> -> String<64>` widening: the same `(position, len)` pair, the bound is type-level only) — is ZERO instructions and lowers as a no-op. Both clauses were already written. Only the container one was unreachable from a non-literal receiver, so it fired for `"hi"::String<4>` and never for `a::String`. `coerce.rs` gains that clause's non-literal half and stays its one home: `rebound_target_bound` (extracted from `over_measured_bound`, so "does this clause govern?" and "does the value fit?" cannot drift apart), `declared_measure` (the measure the compiler HAS for a value it cannot read — the value's own bound, conservative in the only safe direction), and `coerce_container_into`. Keyed on SHAPE — a `Ty::Nominal` pair differing only in its `Ty::Value` argument — so `List` rides it for the same reason `String` does and no arm names a type. Lowering needed nothing: `Lowering::coerce` already folds a non-scalar pair to identity, which is §5's zero instructions. widen(a::String) String<2> -> String<64> E0702 -> runs 42 (a ++ b)::String String<128> -> String<64> E0722 -> E0715 widen((a)::List<Int,8>) List<Int,2> -> List<Int,8> E0702 -> runs 42 The middle row is the same clause refusing: the compiler's measure for a `++` result is the bound it computed, 128, and 128 into 64 has no total operation. Narrowing is still written by re-presenting the descriptor through the newtype constructor — the round-trip `Types.ck` already uses — which is the author asserting a length, not the compiler proving one. CELL_COUNT 442 -> 447, BROKEN_CELL_COUNT 42 -> 43. Three cells land with the clause; `list_concat_bound_does_not_narrow_through_the_turbofish` comes OFF the broken list (it pinned "the pin is simply not consulted" — it is consulted now, and answers E0715); and two cells pin what the same probe found and this commit deliberately does NOT fix: · `coercion_turbofish_with_type_arguments_needs_parentheses` — `x::String<128>` dies in the PARSER at E0146, because after an identifier `::Name` is read as a path segment and a path segment cannot carry generic arguments. Only the argument-free `x::String` and the parenthesised `(x)::String<128>` reach the checker. A parser change, not this one. · `a_let_slot_narrows_a_computed_bound_unchecked` — `coerce_into_slot` bails when the initializer has no LITERAL measure, so `let s: String = a ++ a` takes bound 64 over a `String<128>` with no gate and no diagnostic. Passing the same measure the turbofish now uses is one line, and it is not made: `List` has no `set`, so `let joined: List<Int,4> = take ++ [v] ++ drop` is the only spelling of a bounded-list write there is. Which surface closes is an owner call about the language. Gates: 4 legs green; test-integration 250 passed, 1 ignored, and the standing `r3_no_file_exceeds_loc_cap` red (ck_driver/src/lib.rs, 1323 vs cap 862) unchanged. fmt clean, clippy clean.A systematic sweep of `String` across every position the type appears in — `let` / param / return / record field / record literal, `==` and `!=`, `++`, substring and slice, `size_of` / `len`, trait impl heads and bodies, generic `String<n>` impls, actor `receive<M>` payloads and `send`, the `Show` return path, cross-module and cross-`.ckh` uses, module `const`s. No compiler change here; this is the record. Seven cells, and the split matters more than the count. THE POSITIVE, and it was an open question rather than an assumption. `a_string_payload_crosses_the_actor_boundary` — a `String` field in a message record survives `send` with its length intact. §4 of `plan_values_02.md` states the one-copy rule ("a `String`'s content bytes travel inline in the buffer … every position rebases for free"), and §5 listed actor-boundary crossing as deferred with no instrument behind it. It lands. The cell earns that claim with a control: the receiver routes the length through a `Panic::raise` guard, and the same fixture with the guard set to 4 instead of 5 was measured at `runs: 101`, so `runs: 0` is only reachable when `str_len` really answers 5 on the far side. FOUR GAPS, none of them regressions, none fixed here. · `a_module_const_string_does_not_fold` — `const tag: String = "ab"` is accepted and cannot be READ: every reference is `lower: ConstUnfoldable`. Measured in all four corners (alias / turbofish / exact width / unread), so it is neither the alias nor the coercion — it is the const folder, which has no answer for a two-word value. `project.ck`'s `const name: String` lowers today only because nothing reads it. · `string_size_of_is_composed_but_never_implemented` — the String parity of the existing List cell. `Sized` reaches `String` DIRECTLY and `List` only through `Collection composes Sized`, so a fix that took one route would leave the other red and look complete from either side. · `string_ord_is_composed_but_never_implemented` — `Types.ck:61` composes `Ord`; `Prelude.ck` writes fifteen scalar `impl Ord` blocks and no string one. Worth reading beside `string_plus_string_is_unresolved`: both are E0728, one is the law (`Add` is deliberately absent from the clause) and one is a hole (`Ord` is in it), and only the declaration line tells them apart. · `collection_len_on_a_string_does_not_lower` — type-checks clean, dies at `codegen: Unsupported`, and resolves against an `impl Collection<String<n>, u8>` in `List.ck` for a trait `Types.ck` deliberately does NOT compose on `String`. So a `composes` clause is read back in neither direction: a clause with no impl is accepted, an impl with no clause is dispatched. That is one defect wearing three faces across these last three cells. TWO BY DESIGN, recorded because lawful is not the same as discoverable. `a_show_impl_returning_a_bare_string_literal_refuses` is §1a's strict-out boundary meeting the most-implemented signature in the language: `fn show(x: P) -> String = "p"` is E0722 naming `String<2>` and `String<64>`, two types the author never wrote, with no remedy in the text. Three lawful spellings run and the cell lists all three. `an_actor_state_slot_does_not_coerce_a_string` is the `let mut` in a state block that reads like §1.3's coercing slot and is a record FIELD — the diagnostic points at desugared source that exists in no file. Neither may be "fixed" by widening the boundary; that is the pin channel §1a deletes, and `Money<USD>` vs `Money<EUR>` rides the same strictness. Also strengthens `coercion_turbofish_with_type_arguments_needs_parentheses` with the measurement that makes it a grammar fork rather than a one-line patch: relaxing `parse_bare_angle_generic_args` to admit a terminal bare-angle list would swallow `two(a < b, c > d)` — a running program on this compiler, `runs: 42` — as a generic-argument list on `a`. The narrower follow-token rule that would work is spelled out; ratifying it belongs in `Grammar.ckx` first. CELL_COUNT 447 -> 454, BROKEN_CELL_COUNT 43 -> 47. Gates: 4 legs green (1369 unit, 106 corpus apps, 454 cells, 35 book expectations). fmt clean, clippy clean.Strict-in/strict-out (`plan_values_02.md` §1a) is settled and the refusals are correct. The messages were the problem: argument has type `String<2>`, parameter declared with type `String<64>` Both types are ones the author never wrote. They wrote `"hi"` and `String`, and the alias's bound is invisible at that spelling — `type String = String<64>` is in `Types.ck`, not in their file. The sentence offers nothing to do, and the two conclusions it does support are both false: that the same-name alias is broken, or that a `String` literal cannot cross a boundary. §1a is explicit that the call-site verbosity is "accepted, not a defect to fix … load-bearing"; a diagnostic that never names the verbose spelling is most of what makes it feel like a defect instead. So both sites now append the remedy when the two types differ ONLY in their size bound: argument has type `String<2>`, parameter declared with type `String<64>` — the two differ only in their size bound, and a boundary coerces nothing in either direction; coerce at the site with `(value)::String<64>` E0749 already set this precedent for the homogeneous-trait tie ("Coerce one operand to the other's size with the letter form"); this is the same advice at the two boundary sites. Three things keep it honest. It asks `crate::coerce` the SAME two questions the coercion surfaces ask — does §1.4's container clause govern this pair, and does the source fit — so it is one reader, not a second opinion that can drift. A narrowing therefore gets no advice at all: `returning String<128> … surface String<64>` stays bare, because `(value)::String<64>` there is E0715 and advice that trades one error for another is worse than none. It is keyed on SHAPE, which the blessed goldens show directly: `Name<5>` -> `Name<8>`, `Car<4>` -> `Car<5>` and `Str<8>` -> `Str<64>` are user-declared newtypes and records in the conformance fixtures, and they get the clause for the same reason `String` and `List` do. `param_mixed_list` is the discriminator in one file — its `List<i64,4>` -> `List<i64,8>` row gains the remedy and its `List<i64,4>` -> `List<Bool,4>` row does not, because a different element type is not a re-bound. It prints a spelling that PARSES. `x::String<64>` does not — after an identifier `::Name` is read as a path segment, which cannot carry generic arguments (`coercion_turbofish_with_type_arguments_needs_parentheses`) — so the clause suggests the parenthesised form, which routes through the general postfix surface and works in every position. A message must not print something the compiler would then reject. It says "a boundary" rather than "the `fn` boundary" because E0702 also serves RECORD CONSTRUCTION, strict for the same §1a reason, which is how an actor's state block reaches it (`an_actor_state_slot_does_not_coerce_a_string`). No cell moves: the capability matrix matches on the error VARIANT and never on a rendered message, which is the discipline that makes rewording safe. Seven conformance goldens under `tests/types/` carry rendered text and are blessed. Gates: 4 legs green (1369 unit, 106 corpus apps, 454 cells, 35 book expectations); test-integration ALL GREEN — 251 passed, 1 ignored, and the allow-listed `r3_no_file_exceeds_loc_cap` standing red (ck_driver/src/lib.rs, 1323 vs cap 862) unchanged throughout. fmt clean, clippy clean.