Sizes: a bound written where it is used is a number — and the argument that named it stops being dropped #215
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/215-sizes-a-bound-written-where-it-is-used-is-a-nu"
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?
Sizes: a bound written where it is used is a number — and the argument that named it stops being dropped
Along the way:
fnfinally gets to say it binds oneTouches 31 files:
src/rust/crates/ck_driver/src/compile/sizes/decls.rs,src/rust/crates/ck_driver/tests/capability.rs,src/rust/crates/ck_driver/tests/fixtures/sizes_value_expr/Main.ck,src/rust/crates/ck_driver/tests/fixtures/sizes_value_expr/project.ck,src/rust/crates/ck_driver/tests/sizes_query.rs,src/rust/crates/ck_resolve/src/const_values.rsand 25 more.Reads well. One question on
src/rust/crates/ck_types/src/lift/binders.rs, nothing blocking.@ -317,0 +387,4 @@}(Some(inner), None, None) => fold_value_expr(inner, src, scope, interner, depth + 1),(None, None, None) => fold_value_atom(node, src, scope, interner),_ => None,The catch-all arm means a variant added later compiles and lands here without a word. Can the match on
_name every variant so the compiler tells us?, at src/rust/crates/ck_resolve/src/const_values.rs:390.@ -95,0 +91,4 @@/// [`NameToken`]s and threw it away, which is how a `fn`'s value binder came/// to be silently allocated a type variable.#[must_use]pub fn generic_params(&self) -> Vec<crate::TypeParam<'a>> {generic_paramsis public. Does anything outside the crate call it, or is it pub because that was easier? Every public item is a promise.@ -0,0 +286,4 @@}/// The source text a token span covers.fn tok_text(span: Span, src: &str) -> &str {tok_textreads, 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
tok_textis a pure function now, with its own tests insrc/rust/crates/ck_types/src/lift/binders.rs.Named them all. A new variant is a compile error in
src/rust/crates/ck_resolve/src/const_values.rsnow, which is where we want to find out.Nothing outside calls
generic_params. It is pub(crate) insrc/rust/crates/ck_syntax/src/bodies.rsnow.`Pair2<1 + 1, Int>` is correct CkLang, and the compiler answered it with a diagnostic about the wrong argument: E0742 on the `Int`, blaming it for the value parameter `size` that the `1 + 1` beside it had already filled. Nothing about that is a missing feature. The type-level value expression is a `GenericValueExpr` node, `TypeRef::from_node` admits it nowhere, so both the lift and the E0742 walk silently dropped the argument AND left the slot cursor where it was — every later argument then read against the previous parameter. Correct code, wrong span, wrong parameter, and no way for the author to act on it. The quieter half is worse, because it is a Law 1 breach with no diagnostic at all: `record B { xs: List<byte, 64 + 0> }` sized as a field with no bound, and `Slot<PAGE + 1>` checked clean while the compiler held no number for the slot. A size the compiler cannot compute must be a refusal; a size it CAN compute must be the number. Both were being absorbed instead. WHERE THE EVALUATOR LIVES, AND WHY. In `ck_resolve::const_values` — the module `const` fold — as a second ENTRY POINT, not a second evaluator. The two candidates were that fold (CST-shaped, holds the registry) and `ck_types::const_eval` (ConstExpr-shaped). The second was checked and rejected: reaching it means `ConstEvalBodies` + the coherence table + prepared bodies, none of which exist at lift time, so `lift::impl_head::build_value_expr` + a leaf resolver would have meant writing a fresh ConstExpr walker — a THIRD evaluator wearing a reused constructor. The fold, by contrast, already owns every piece the type slot needs: `arith_method` (which reads the operator's method name off `ck_parser::standard_operator_table` through the shared `lower::op_token` — no glyph is matched in Rust), `fold_arith`'s checked integer set, and now one `read_int_text`. `fold_generic_value_expr` adds a walk over the parser's other CST shape and nothing else; the leaves differ only in that a type slot reads the settled `Interner::const_int` registry the same fold filled during linking, because by then linking has run. One frame, one arithmetic, two shapes — so `(PAGE - 1) + LEXEME` cannot mean one number in a `const` initializer and another in a size slot. The cursor now advances once per ARGUMENT NODE in both `lift_generic_args` and `param_args::check_type_expr`, unconditionally, foldable or not. That is the half that fixes the misdiagnosis, and it is the half that has to hold whatever new node kinds the parser grows. (`WildcardType` was checked for the identical hole and does not have it: `parse_generic_args` takes `_` as a bare token before `parse_type_expr` could wrap it, so no `WildcardType` node ever reaches the slot walk. The new arm would absorb it correctly if that ever changed.) E0742 now names the third legal spelling and blames the expression at its own span. Still one fact: this slot needs a number the compiler has, and this argument is not one. DEFECT 2 — THE STDLIB-HEAD BLIND SPOT — IS DIAGNOSED, NOT CLOSED, AND THAT IS DELIBERATE. `param_args` resolves its head with a per-module `scope.lookup` while the lift resolves the same head through the project-global nominal roster, so a stdlib head always misses in an app module and the whole slot check is skipped: `record Holder { bytes: List<byte, WOBBLY> }` checks clean and the field absorbs to unbounded. Same class as `list_literal_nominal`'s lost size determinism (rule V6b). The fallback was written and verified — it closes that case exactly. It does not land, because it also turns 46 stdlib signatures into E0742 (18 sites in `tests/corpus/stdlib/List.ck`) and reds `stdlib_type_clean`. Those spell a FREE size binder `n`; List.ck's header calls that the idiom, E0742's rule calls it a refusal, and the capability cell `impl_method_signature_size_param` declares that `fn count(c: List<Int, n>)` reaches CODEGEN — which is only observable if it is not a type error first. Three owner-authored artifacts, two answers. The Laws do not decide between them, so it is one question for the owner and not a thing to settle from a slice. The blind spot is recorded where the next reader will hit it: in `check_type_expr` at the lookup, and in the cell that already existed to name it. The multi-segment `Mod::SIZE` half is untouched, as instructed. - `ck_resolve::const_values`: `fold_generic_value_expr` + `fold_value_expr` + `fold_value_atom`, over the existing `arith_method` / `fold_arith`; the literal reader is factored to `read_int_text` so `1_000` reads the same in both entry points. - `ck_types::lift::value_slot`: `value_slot_expr`, and the unconditional cursor advance with an `Unknown` fill for a node the lift has no reading for. - `ck_types::param_args`: the same cursor rule, `value_slot_expr` in the legal set, and the recorded blind spot. - E0742's message and doc; three `tests/types` goldens re-blessed. - Cells: `a_size_slot_whose_arithmetic_does_not_fold_refuses` (new, `CELL_COUNT` 428 -> 429); `a_size_slot_whose_const_does_not_fold_refuses`'s position note now carries the measured reason it is still app-owned. - Tests: three `param_args` unit tests (the `Pair2<1 + 1, Int>` misdiagnosis pinned exactly), `tests/types/param_value_expr_arg`, and `sizes_query::a_size_slot_spelled_as_arithmetic_sizes_to_the_folded_number` over the new `sizes_value_expr` fixture — 208 B of record that used to be three unbounded fields. Gate: NOT run. The owner suspended `./scripts/test-rotate.sh` mid-slice (package at 87 C, one core pinned by four agents' single-threaded test phases) and will run it serialized. Verified narrowly instead, all green: `cargo fmt --check`; `cargo clippy --workspace --all-targets -D warnings` (0/0); `ck_types` 261, `ck_resolve` 113, `ck_parser`/`ck_syntax`/`ck_lex`/`ck_hir` unit tests; `ck_driver` conformance, sizes_query, architecture (R3/R4 and V6b included), stdlib_type_clean, stdlib_composes_silence, grammar_stage1 — the last three being exactly the targets the withheld fallback had turned red. `--test capability` is unverified: it executes CkLang in a container, which the owner forbade for the rest of this slice.fnfinally gets to say it binds one dcad2bebe1`fn f<n: Size>(xs: List<Int, n>)` has parsed since the binder list existed. `parse_generic_params` is ONE production used at all nine declaration sites, and it has always taken an optional `: TypeExpr` after each binder — `<size: Size>` and `<T>` come out of the same loop with no arm for either. Nothing was missing from the grammar. What was missing was a consumer: `FnDecl::generic_params()` yielded bare name tokens and threw the ascription away, and the signature pass then allocated a `TypeVarId` per binder UNCONDITIONALLY. A `fn`'s size binder became a type variable. A type declaration never had that bug — it read `ck_hir::ParamKind` off its own list — and that asymmetry, not the `fn` case, is what this fixes. The visible cost was a diagnostic that named the wrong kind of thing. `const_generic_fn_call` pinned **E0706**, "the generic type parameter `n` was not pinned by a turbofish", about a parameter that is a NUMBER and is waiting for a literal, not a turbofish. One name, one meaning: a value binder must not wear a type variable, so `GenericParam` now carries a kind mirroring `ParamKind` — `Type { var }` or `Value { ty }` — and `lookup_generic_param` returns the binder rather than a bare `TypeVarId`, so a caller cannot fail to see which it got. THE RULE, AND WHY IT HAS NO ARMS. A name in a value slot is legal **iff it is bound**: by an enclosing generic scope (a type declaration's `<…>`, an `impl` head, or a `fn`'s binder list), by a module `const` the fold settled, or as a concrete tag. Otherwise E0742. That sentence replaces a POSITION rule: the E0742 walk used to skip an `impl` head's argument list wholesale, because a lowercase binder there is legal — which made legality a fact about where a name was written, and left the very binder the head introduced illegal in the method signatures the head exists to serve. The walk now carries a binder scope, extends it with whatever each node binds, and asks the one question at every slot. An impl head is a binding site the way a `<…>` list is; that is a fact about BINDING, not a licence about position, and it is the only reason the head's own `String<sa + sb>` still passes. Binder scopes NEST, which is the half that had no representation at all: `ModuleFn` now carries its enclosing `impl`, and a method's lift map is the head's binders followed by its own. THE BLIND SPOT CLOSES IN THE SAME COMMIT, BECAUSE IT COULD NOT CLOSE ALONE. E0742's head lookup was a per-module `scope.lookup` while the lift resolved the same head through the project-global nominal roster, so a STDLIB head missed in every app module and the whole slot check was skipped — `List<byte, WOBBLY>` checked clean and the field absorbed to unbounded, a Law 1 breach with no diagnostic. The previous slice measured the fallback, verified it, and withheld it: closing it turned 46 stdlib signatures into E0742, every `impl Collection<List<Int, n>, Int>` method among them, and contradicted `impl_method_signature_size_param`, which declares that `fn count(c: List<Int, n>)` reaches CODEGEN and is therefore not a type error first. That looked like two answers from three owner-authored artifacts. It was one rule missing. With binders tracked, the 36 bound sites in `List.ck` pass and only the free ones refuse — measured, exactly 13 diagnostics over 5 module-level signatures. Those five were not an idiom. `fn rec<64> reverse_onto(xs: List<Int, n>, …)` sits under no impl head; the `<64>` is the recursion bound, a different slot. They spell a size nothing binds, and the only reason nobody noticed is that the head lookup missed. They now declare what they always meant — `reverse_onto<n: Size>` — which the grammar accepted unchanged, because it always would have. A value binder still lifts to `Ty::Unknown` at a type position, which is what a free `n` lifted to before. That is not a fail-soft to be embarrassed about: the bootstrap does not monomorphise, so there is no number yet, and `Unknown` is this compiler's one sentinel for "no reading here". Law 1 survives because monomorphisation substitutes a literal at each concrete instantiation; the cell that pins its absence is `const_generic_fn_call`, now honestly stopped by E0760. - `ck_syntax`: `FnDecl::generic_params()` returns `Vec<TypeParam>` through the shared `generic_params` projection — the same one `trait` / `newtype` / `record` / `data` read; `ImplDecl::node()`, so the head is reachable as a binding site. - `ck_types::lift::binders` (new): `decl_binders` (a `<…>` list, plus an impl head's by-appearance value binders), `binder_list` (the ONE reading of a binder list, now also `ck_driver`'s `sizes` fold), `value_slot_names` and `value_slot_names_are_bound` — the shared predicate the check runs, so what binds and what is accepted read the same spelling. - `GenericParam` + `GenericParamKind`; every construction site updated (`signature`, `predicate`, `fields`, `payload`, `trait_methods`, `ck_driver::compile::sizes::decls`), and every `.var` reader routed through `GenericParam::var() -> Option<TypeVarId>`. - `ck_resolve::ModuleFn::enclosing_impl`. - E0742 names the fourth legal spelling; three `tests/types` goldens re-blessed. - Cells: `a_free_size_name_in_a_value_slot_refuses` (new — the case that used to be SILENT), `CELL_COUNT` 429 -> 430. `const_generic_fn_call` rewritten (E0706 -> E0760) and MARKED, so `BROKEN_CELL_COUNT` 50 -> 51 and `UNMARKED_CELL_COUNT` 148 -> 147 — a marking of a pre-existing gap, not a regression, and the convention's own migration. The stale "held until the owner settles it" notes on the two refusal cells are replaced by what was measured. - Five `param_args` unit tests: a declared binder is spellable, a TYPE binder in a value slot still refuses, a binder does not escape its declaration, an impl head binds what its methods spell and no further, and the head lookup no longer lets an argument with no number reach the absorption. Gate: NOT run, and this is not a claim of green. Verified narrowly instead, all passing: `cargo fmt --check`; `clippy --workspace --all-targets -D warnings` (0/0); every workspace `--lib` suite (16 targets, `ck_types` 266, `ck_parser` 530, `ck_resolve` 113); `ck_driver` architecture (38, R3/R4 included), conformance (2), sizes_query (2), stdlib_type_clean (1), stdlib_composes_silence (1), grammar_stage1 (2), corpus (1 + 1 ignored — the strict-exhaustive `trap.md` reconciliation, which is what proves no corpus app gained or lost a diagnostic), check, check_path_resolution, grammar_structure, literal_claims_*, meta, recursion_check, stdlib_faults, stdlib_memo, typed_literal_lex, raise_cell_size, timer_table_size, store_load_width, footprint_door, trail_depth. `--test capability` is unverified by instruction (it executes CkLang in a container); each touched cell's outcome was measured by hand with `ckc check` / `ckc disassemble`, including that `impl_method_signature_size_param` still checks clean and still stops at `codegen: Unsupported`. `sizes_pin` / `frame_pin` / `unbounded_inventory` were not run; no alias target in the stdlib spells a value binder, so the `sizes` binder-kind change is expected to move nothing.