Sizes: the project's tables learn what its types weigh, before anything asks #222

Manually merged
tvidal merged 3 commits from feat/222-sizes-the-project-s-tables-learn-what-its-type into master 2026-09-07 14:37:43 +02:00
Owner

Sizes: the project's tables learn what its types weigh, before anything asks

Along the way:

  • sizes: the project's tables learn what its types weigh, before anything asks
  • frames: the layout is laid out once and then read, never folded twice
  • traits: the impl instantiation a call already computed is written down

Touches 22 files: src/rust/crates/ck_driver/src/compile/cross_module.rs, src/rust/crates/ck_driver/src/compile/frames.rs, src/rust/crates/ck_driver/src/compile/mod.rs, src/rust/crates/ck_driver/src/compile/recursion.rs, src/rust/crates/ck_driver/src/compile/sizes/fns.rs, src/rust/crates/ck_driver/src/compile/sizing.rs and 16 more.

Sizes: the project's tables learn what its types weigh, before anything asks Along the way: - sizes: the project's tables learn what its types weigh, before anything asks - frames: the layout is laid out once and then read, never folded twice - traits: the impl instantiation a call already computed is written down Touches 22 files: `src/rust/crates/ck_driver/src/compile/cross_module.rs`, `src/rust/crates/ck_driver/src/compile/frames.rs`, `src/rust/crates/ck_driver/src/compile/mod.rs`, `src/rust/crates/ck_driver/src/compile/recursion.rs`, `src/rust/crates/ck_driver/src/compile/sizes/fns.rs`, `src/rust/crates/ck_driver/src/compile/sizing.rs` and 16 more.
tvidal approved these changes 2026-09-07 10:19:12 +02:00
tvidal left a comment

Happy with this. I ran the suite against the branch and it is green, src/rust/crates/ck_driver/tests/architecture/values.rs included.

Happy with this. I ran the suite against the branch and it is green, `src/rust/crates/ck_driver/tests/architecture/values.rs` included.
@ -42,0 +41,4 @@
///
/// `pub(super)` so the S1 size-construction rule (`mod sizes`) scans the same
/// file set rather than growing a third copy of this walk.
pub(super) fn all_production_sources() -> Vec<(PathBuf, String)> {
Owner

all_production_sources reads, decides and writes in one function. The deciding part is the piece I would want to test on its own. Worth splitting?

`all_production_sources` reads, decides and writes in one function. The deciding part is the piece I would want to test on its own. Worth splitting?
Author
Owner

Split. The decision in all_production_sources is a pure function now, with its own tests in src/rust/crates/ck_driver/tests/architecture/values.rs.

Split. The decision in `all_production_sources` is a pure function now, with its own tests in `src/rust/crates/ck_driver/tests/architecture/values.rs`.
dferreira approved these changes 2026-09-07 14:00:00 +02:00
dferreira left a comment

Second read on src/rust/crates/ck_driver/tests/architecture/values.rs. Nothing to add.

Second read on `src/rust/crates/ck_driver/tests/architecture/values.rs`. Nothing to add.
The owner's ruling: the coherence table carries the sizes of the types
themselves, and every binder carries its size and a reference to it. This
is the first third of that — the row store, minted, filled, and read by
nobody.

Three moves, all inert by construction:

1. `build_field_types` / `build_payload_types` run FIRST in both cascade
   sites. The one real dependency edge in that cascade is
   `build_coherence_table <- build_trait_methods`; the shape tables sat
   after coherence for no reason, and the size pricing reads exactly them.
   Verified inert, not assumed: no cascade builder writes interner state a
   later one reads — each takes `&mut Interner` only to `intern`
   identifier text, and the cascade's one allocation (`alloc_impl`, inside
   `build_coherence_table`) mints an id no shape table looks at. Measured:
   `ckc sizes` and `ckc check` over all 146 apps of BOTH corpus roots,
   stdout, stderr and exit code, byte-identical before and after (noise
   floor established first: two baseline sweeps, zero difference).

2. `ProjectSizeTable` (`ck_types::arena_size::table`) — `BTreeMap<Ty,
   TypeSize>`, a pure memo over `type_size` and nothing else. `Ty` and
   never `TypeId`, because `String<64>` is not `String<128>` and both
   share one id; a `TypeId`-keyed size table is ruling 1's bug reborn as a
   cache. `BTreeMap` and never `HashMap`, because `traits_for` already
   measured what a `RandomState` key sweep costs: one unchanged directory,
   two different binaries.

3. `Tables.sizes`, filled by `sizing::price_declared_types` from the two
   shape tables' own rows — not a second CST walk, so it cannot drift from
   the walk that found those positions. Nothing reads it. `#[expect]` and
   not `#[allow]`, so the suppression itself fails the build the moment
   the first reader lands.

And the guard that should have existed before the table did: architectural
rule S1 — a `TypeSize` may only be CONSTRUCTED inside
`ck_types/src/arena_size/`. This is the `LIST_HANDLE_BYTES` lesson made
machine-readable: that constant outlived its repr, under-counted every
`List` by a word everywhere it was sized, and its doc comment cited an
enum variant no crate ever had. Prose was the half that failed. Two
exemptions, each justified, both directions checked; red-proofed on both
detection arms.

`Tables` moves to its own file: adding the size row pushed `compile/mod.rs`
over the R3 cap, and splitting is that rule's sanctioned answer — the file
that drives the compile and the record of what the type stage settled are
two different things.

Ratchets: none moved, none may — this slice changes no number.
Gates: cargo fmt clean; cargo clippy --workspace --all-targets -D warnings,
0 warnings; architecture 39/39; ck_types::arena_size::table 3/3.
`frame_layout` computed a `FrameLayout { slots, size }` and two of its three
callers threw the slots away through a `frame_size` wrapper; the third
re-folded the raw slots at join time, a whole compile after they were known.
Both halves of that are the same defect: one law, more than one place it
runs.

`frame_size` is deleted. It was `frame_layout(…).size` and nothing else — a
second name for one answer (Law 2), and this module's own header is a
record of what a second answer to a size question costs. Its three callers
(`sizes::fns`, `recursion`, `arena_size`) read the total off the layout they
lay out. The unit test that asserted the two agreed is replaced by the
invariant that made them agree: the plex total IS the Σ of its slots' sizes.

`LoweredUserFn` carries `declared_frame: FrameLayout` in place of
`frame_slots: Vec<Ty>`. The layout is laid out by `frames::declared_frame`
where the slots exist — inside the lowering walk, beside
`TypeckResults::frame_slots` — and `FrameReport::join` READS it.
`join` no longer takes a `TypeLayout` at all, which is the point: there is
no longer a place where a declared frame could be computed differently from
the one the fn carries.

Not done, and why: `Tables.frames: BTreeMap<FnId, FrameLayout>` as designed
is not implementable without a behaviour change. The set the reconciliation
joins over is the LOWERED set — stdlib bodies, provider and arena fns, and
LC6 mono stamps whose `FnId`s are minted DURING lowering (`mono::alloc_fn`)
— while the type cascade walks only app-owned, surface-filtered fns. A
table filled in the cascade would be missing rows for exactly those, and a
lookup-with-fallback would be two paths that must agree, which is the shape
plan.md §6 records a live regression taking. The layout is stored on the fn
that owns it instead, which is the same "stop discarding it" with no set to
get wrong.

Verified: `tests/frame_pin/*` byte-identical for all 36 corpus apps
(`--test frame_reconcile`, no containers); `ckc sizes` + `ckc check` over
all 146 apps of both corpus roots byte-identical, stdout/stderr/exit;
all 36 `tests/sizes_pin/*` sizes sections byte-identical.

Ratchets: none moved — no number changed. Gates: fmt clean; clippy
--workspace --all-targets -D warnings, 0; architecture 39/39;
ck_types arena_size 133/133.
`capture_and_subst` binds a generic impl head against the call's concrete
operands — `sa = 4` from `String<4>` against `impl Concat<String<sa>,
String<sb>, String<sa + sb>>` — and const-evaluates the output head to the
exact `String<6>`. It then used that substitution once, to type the call,
and dropped it. Every monomorphisation of every generic impl in the program
was computed and thrown away.

`TypeckResults::impl_insts: BTreeSet<ImplInst>` is where it lands: the impl
twin of `generic_insts`, `(trait, concrete head args in declaration order)`,
recorded at the same statement that records `resolved_method`. A SET, not a
per-call map — two calls at the same widths are one instantiation, and what
the size table will want is the instantiations a program contains. `BTree`,
so the sweep order is the same in every process.

The head args are read out of `capture_and_subst`'s own substitution map,
before it is consumed as a rewrite, so the instantiation recorded and the
types the call is given cannot be two different bindings. Threaded through
`substituted()` exactly as `generic_insts` is, and re-collected rather than
mapped in place: substitution can make two rows equal, and a set holding
both would claim two instantiations where the program has one.

NOTHING reads it. Dispatch is unchanged, the coherence key is still
wildcarded, and the compiler accepts and rejects exactly what it did.

The open design question — is the OUTPUT substitution available in a form a
row can store? — is answered YES, measured on the real compiler. Probing
`ml_tictactoe` recorded 43 instantiations, six of them Concat:

    Concat[String<64>,  String<64>,  String<128>]
    Concat[String<128>, String<64>,  String<192>]
    Concat[String<192>, String<64>,  String<256>]
    Concat[String<256>, String<64>,  String<320>]

The computed widths are there, exact. The design's doubt came from
`unify_value_slots` capturing only the `Param` shape, but that is the
CAPTURE direction (head → env); the output is computed by the separate
`subst_and_eval_ty` branch, which folds `sa + sb` through the
coherence-resolved `Add` impl. Slice 4 is implementable as designed.

Cell: `method_call_tests` pins the plumbing claim on the non-size-generic
form (`impl Convert<Celsius, Fahrenheit>` ⇒ the output-only `U` is recorded)
— the size-generic branch is the same code with `subst_and_eval_ty` folding
arithmetic whose values `tests/sizes_pin` already gates end to end. The
suite is a `#[path]` sibling (the `coherence` / `coherence_tests` pattern)
to hold `method_call.rs` under its R3 cap.

Verified inert: `ckc sizes` + `ckc check` over all 146 apps of both corpus
roots byte-identical; all 36 sizes-pin sizes sections byte-identical;
`tests/frame_pin/*` byte-identical.

Ratchets: none moved. Gates: fmt clean; clippy --workspace --all-targets
-D warnings, 0; architecture 39/39; ck_types lib 270/270.
tvidal manually merged commit 666e508fa2 into master 2026-09-07 14:37:43 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
northgale/cklang-bootstrap!222
No description provided.