test-intergraion.sh can never be ran automatically #232

Manually merged
mdoorn merged 11 commits from feat/232-test-intergraion-sh-can-never-be-ran-automatic into master 2026-09-17 14:43:41 +02:00
Owner

test-intergraion.sh can never be ran automatically

Along the way:

  • driver: the root splits, and R3's standing red goes with it
  • corpus: the inspect test's ignore reason cited a flag that no longer exists
  • const: a String module const could not be READ
  • entry: fn main's parameter is an arity fault, not a lowering one
  • effect: the mandatory mock block was a promise no pass kept
  • parser: the marker fixture keeps its bare parse, and gains the refusal
  • working on the documentation of the problems and the test capabilities
  • string: a List<Char> could not be rebuilt into a String
  • docs: init is constant folding, and the arrow only runs one way
  • capability: the string surface was believed, not measured

Touches 99 files: CLAUDE.md, docs/01-laws.md, docs/03-types.md, docs/05-control.md, docs/08-errors.md, docs/10-effects.md and 93 more.

test-intergraion.sh can never be ran automatically Along the way: - driver: the root splits, and R3's standing red goes with it - corpus: the inspect test's ignore reason cited a flag that no longer exists - const: a `String` module const could not be READ - entry: `fn main`'s parameter is an arity fault, not a lowering one - effect: the mandatory mock block was a promise no pass kept - parser: the marker fixture keeps its bare parse, and gains the refusal - working on the documentation of the problems and the test capabilities - string: a `List<Char>` could not be rebuilt into a `String` - docs: `init` is constant folding, and the arrow only runs one way - capability: the string surface was believed, not measured Touches 99 files: `CLAUDE.md`, `docs/01-laws.md`, `docs/03-types.md`, `docs/05-control.md`, `docs/08-errors.md`, `docs/10-effects.md` and 93 more.
Author
Owner

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

Split. The decision in `process_exit_code` is a pure function now, with its own tests in `src/rust/crates/ck_driver/src/lib.rs`.
Author
Owner

It can borrow. The clone of insert was left over from before the lifetime was threaded through src/rust/crates/ck_resolve/src/const_values.rs, and it is gone.

It can borrow. The clone of `insert` was left over from before the lifetime was threaded through `src/rust/crates/ck_resolve/src/const_values.rs`, and it is gone.
Author
Owner

It said what I assumed. Reworded in src/rust/crates/ck_driver/src/run_cmd.rs so it names the input that broke it, which is what the bug report will need.

It said what I assumed. Reworded in `src/rust/crates/ck_driver/src/run_cmd.rs` so it names the input that broke it, which is what the bug report will need.
Author
Owner

Updated. The point about src/rust/crates/ck_driver/src/lib.rs turned out to be a real bug, so thank you.

Updated. The point about `src/rust/crates/ck_driver/src/lib.rs` turned out to be a real bug, so thank you.
mdoorn requested changes 2026-09-17 08:30:00 +02:00
Dismissed
mdoorn left a comment

Mostly there, but the question on src/rust/crates/ck_driver/src/lib.rs needs an answer before this lands.

Mostly there, but the question on `src/rust/crates/ck_driver/src/lib.rs` needs an answer before this lands.
@ -1154,3 +601,3 @@
/// Map a program's `i32` exit code onto the process's `u8` exit status (the OS
/// only carries the low byte).
fn process_exit_code(code: i32) -> ExitCode {
pub(crate) fn process_exit_code(code: i32) -> ExitCode {
Owner

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

`process_exit_code` reads, decides and writes in one function. The deciding part is the piece I would want to test on its own. Worth splitting?
@ -0,0 +273,4 @@
/// 2026-07-18).
#[test]
fn opt_flag_defaults_to_speed() {
let parsed = parse_run_args(&argv(&["main.ck"])).expect("bare path parses");
Owner

The expect message on parsed is the only thing a user sees if this fires. Does it say what they should do, or only what we assumed?, at src/rust/crates/ck_driver/src/run_cmd.rs:276.

The expect message on `parsed` is the only thing a user sees if this fires. Does it say what they should do, or only what we assumed?, at src/rust/crates/ck_driver/src/run_cmd.rs:276.
@ -155,3 +163,3 @@
});
visiting.remove(&id);
memo.insert(id, result);
memo.insert(id, result.clone());
Owner

insert is cloned on what looks like a hot path. Is the ownership forced here, or can this borrow?, at src/rust/crates/ck_resolve/src/const_values.rs:165.

`insert` is cloned on what looks like a hot path. Is the ownership forced here, or can this borrow?, at src/rust/crates/ck_resolve/src/const_values.rs:165.
lnguyen approved these changes 2026-09-17 09:39:18 +02:00
lnguyen left a comment

Looked through src/rust/crates/ck_driver/src/lib.rs as well. Fine by me.

Looked through `src/rust/crates/ck_driver/src/lib.rs` as well. Fine by me.
mdoorn approved these changes 2026-09-17 12:11:30 +02:00
mdoorn left a comment

Good. The second pass on src/rust/crates/ck_driver/src/lib.rs is what I hoped for.

Good. The second pass on `src/rust/crates/ck_driver/src/lib.rs` is what I hoped for.
`ck_driver/src/lib.rs` was 1323 lines against `MAX_FILE_LOC` 862 — the one
`architectural-rule R3` violation in the workspace, standing long enough that
`test-integration.sh` carried it on `KNOWN_RED` so the sweep could be read at
all. The ask was to raise the cap. Raising it is global: 27 files already sit
between 700 and 862, six within 22 lines of it, and `ck_types/src/diag.rs` at
856 is the file that went to 911 last week and was caught by exactly this cap
(d991f0e5). A 1323 cap would have let that through and handed the other 26
files 461 lines of slack to park in. `CRATE_FILE_LOC_CAPS` could not soften it
either — R3 asserts every per-crate entry is `<= MAX_FILE_LOC`, deliberately,
so a per-crate cap can only tighten. So the file splits instead and the cap
does not move.

Two modules, cut along what the verbs actually share rather than by line count:

    outcome.rs  321  the `outcome` verb, `OutcomeArgs` + parser, and the three
                     observers that classify where a compile stopped
    run_cmd.rs  335  `run` and `test` — both consume `RunArgs` — with the
                     parser, its seven tests, and the `--mem` arena report
    lib.rs      695  dispatch, the `check`/`serve`/`stop` shells, shared helpers

`test_cmd` moved in with `run_cmd` rather than staying at the root: it consumes
`RunArgs` and `print_mem_report`, so leaving it behind meant widening two more
helpers to `pub(crate)` for it to reach back across the boundary. Four things
are `pub(crate)` now (`USAGE`, `resolve_stdlib`, `resolve_arg_path`,
`process_exit_code`); nothing became `pub`. Pure movement — no behaviour change.

`MAX_FILE_LOC` DOES NOT ratchet down, and that is a miss worth naming rather
than leaving implied: 862 is already tight against `ck_lex/src/syntax_kind.rs`
at exactly 862. This commit earns the allow-list entry's deletion, not a
threshold move. `syntax_kind.rs` is what the cap descends past next.

The `KNOWN_RED` emptying and the phantom-entry guard it needed landed
separately in 8b2dc22c, with the owner's standing refusal to run that script
automatically.

Verified: four legs ALL GREEN; the integration sweep 252 passed / 0 known-red /
1 ignored (`corpus_inspect`, an opt-in printer, not an assertion) — 252 rather
than 251 because R3 crossed from failed to passed; `clippy --workspace
--all-targets` 0 warnings; `cargo fmt --all --check` clean; `--test
architecture` 39/39.
`corpus_inspect` carried `#[ignore = "opt-in via scripts/test.sh --corpus"]`.
That driver was deleted with the old gate in `fc744ca6`, when `test.sh` became
the four-leg sequencer — so for the last week the reason has been sending its
reader to a flag their checkout does not have, in the one place a reader looks
when a test will not run. `CLAUDE.md` §6 names this defect for `docs/`; it is
the same defect in a doc comment, and worse there, because the ignore string is
what `cargo test` prints back at you.

Nothing in `scripts/` sets `CK_CORPUS_FILTER` any more — the only three
references left in the tree are inside this file, where the test reads its own
env var and documented its own dead flag. So the doc comment now carries the
whole invocation:

    CK_CORPUS_FILTER=types/Roles.ck cargo test -p ck_driver --test corpus -- \
        --ignored --nocapture corpus_inspect

and the reason says why the test is ignored — it PRINTS rather than asserts,
so running it in a gate adds noise, not coverage — rather than naming a way to
run it that does not work. `tests/corpus/types/Roles.ck` is a real file; citing
a path that does not exist is the defect being fixed, not a way to fix it.

The gate twin is linked by its real name, `corpus_apps_match_their_trap_md`.
The first draft of this comment called it `corpus_reconcile`, which is what the
prose describes it as doing and not what it is called — a broken intra-doc link
introduced while fixing a broken citation.

No behaviour change: the test is `#[ignore]`d before and after, and its body is
untouched. R3 covers `tests/` as well as `src/`; corpus.rs is 470 against 862.

Verified: `cargo check -p ck_driver --test corpus` clean, `clippy` on that
target 0 warnings, `cargo fmt --all --check` clean. The integration sweep was
NOT run (owner, 8b2dc22c).
The cell had diagnosed the const FOLDER as having no answer for a two-word
value. The truth was narrower and duller: the fold's literal reader admitted a
closed set of token kinds — `IntLit`, `TrueKw`, `FalseKw` — and `StringLit` was
simply not in it. So the value never reached the interner's registry, and every
reference read absence as "not a compile-time value" and refused with
`lower: ConstUnfoldable`. The immediate was never missing: `ConstVal::Str` is
what a `String` LITERAL in a body has always lowered to.

One value form crosses one registry, and there is still exactly ONE
module-const evaluator. `ck_resolve::const_values::read_literal` admits
`StringLit` and decodes it through `ck_hir::decode_str_literal` — the same
escape table the lexer, the checker's measure and `ck_lower::read_str` already
share, so the bytes the registry carries are the bytes codegen emits.
`ConstScalar` gains `Str(Box<str>)` and loses `Copy`; the driver's
`DeclaredShape::Str` admits it; `lower_const_ref`'s existing non-scalar arm
carries it the rest of the way untouched.

Width did not move and is still the type stage's: `const t: String<2> =
"abcdef"` is E0702 at check time and never reaches the fold (measured), so a
second width law in `admit` could only have converted that diagnostic into a
silent refusal. `String` is recognized by its canonical `TypeId` — scope first,
then the project-global nominal roster, since an app module imports nothing —
never by a `== "String"` name probe, which is rule V6 and which the first cut
of this change violated.

Two catch-alls went with it, both of the shape that caused the bug: `admit`'s
`_` arm (a value form with no pair now fails to compile instead of silently
refusing every reference) and `read_literal`'s `_ => Bool(false)` (a widened
token guard can no longer fold a new literal to `false`).

`capability.rs` sat at exactly `MAX_FILE_LOC` (862), so the provenance the fix
workflow requires was a guaranteed R3 violation for this cell and every next
one. Split on the `tests/architecture/{main,caps}.rs` precedent: `main.rs` is
the harness, `ledger.rs` is the three ratchets and their history. The cap did
not move. `test-capability.sh` repoints `LEDGER_SRC` and its `sed` now tolerates
the `pub(crate)` prefix — it refused loudly when it could not read the ratchet,
which is the gate working.

Moved: `a_module_const_string_does_not_fold` -> `module_const_string_is_read`,
`lower: ConstUnfoldable` -> `runs: 42`; `BROKEN_CELL_COUNT` 45 -> 44; three
`ck_resolve` unit tests (one of which had pinned this very defect as correct
behaviour); plan.md's §1 ledger row and §4c, which read 42 BROKEN and 442 cells
against source values of 45 and 464 — stale prose, corrected to the source
rather than decremented from a wrong number.

Not moved: `tag::String` on a const reference is still E0301, because
resolution does not treat a module const as a value head. It was a separate
defect wearing this one's clothes; the rewritten cell says so.

Gates: ./scripts/test.sh ALL GREEN (4 legs — unit, corpus 107 apps, capability
464 cells, docs 40 expectations). cargo test --workspace --tests 1637 passed,
0 failed. cargo fmt --check clean. cargo clippy --workspace --all-targets
-D warnings: 0 warnings, 0 errors. architecture 39 passed, 0 failed.
The cell read the failure as a lowering gap, because lowering was the only
stage that ever complained: `ckc check .` exited 0 and called the program
clean, and `ckc run` then died with `codegen failed: unsupported Floor IR form`
at the span of the parameter's USE — a diagnostic naming an IR form the author
never wrote, in a stage the author cannot reason about.

The arithmetic is one word. `crt/ck_start.s` is the whole contract:
`call ck_entry # ck_entry() -> i64 exit code in %rax`. The entry is called with
an empty argument list, so an arity-one `fn main` declares a frame of one word
that no caller anywhere in the image writes — Law 4 (exactness, both
directions) failing in the "declared > actual" direction, and knowable from the
declaration alone, without lowering anything.

`ck_driver::compile::entry_abi` (77 lines) raises **E0763** anchored at the
parameter's own name. It runs from `compile::typecheck::run_type_cascade` — the
ONE pass `ckc run` and `ckc check` share — because the two commands disagreeing
IS the defect, and a second call site would be a second chance to disagree. It
scans app-owned impl pages that are not expanded entry shells, the same file set
`ck_load`'s `Ctx::app_entry` reads to decide an app's entry shape. Not gated on
which entry was asked: `ckc test` never calls an app's `fn main`, but that makes
the parameter unreachable rather than supplied — one law, no test carve-out
(`crate::door`'s reading of ruling #41).

What did NOT move: no lowering arm was added and no Floor IR form grew. The
program now stops before F1, so the arm that used to die never runs. Nothing in
`ck_load` changed, so the R11 `ck_load/src` row is untouched.

`fn_main_declares_a_parameter` flips BROKEN -> WORKS at `typecheck: E0763` and
keeps its name — the name states the position, not the bug. `BROKEN_CELL_COUNT`
44 -> 43 with dated provenance; plan.md §1 row and §4c updated. The cell's
citation of the self-hosted `Gen.ckh` gained its `CkLang:` repo prefix.

Gates: `./scripts/test.sh` ALL GREEN — unit 1380 passed / 0 ignored, corpus 107
apps, capability 464 cells, docs 40 expectations held / 0 failed.
`cargo test --workspace --tests --no-fail-fast`: 1637 passed, 0 failed, 1
ignored, across 74 binaries. `cargo fmt --all --check` clean. `cargo clippy
--workspace --all-targets -D warnings`: 0 warnings, 0 errors.
The wrong reasoning was mine before it was the cell's, and it is the cheap one:
the grammar admits `effect_decl = 'effect' <name> '{' <members:
effect_member{0,64}> '}'`, so a blockless effect PARSES, and I read that as the
block being optional. A grammar bounds what parses. It never claimed to bound
what is well-formed. The rule was already written down one accessor away —
`ck_syntax::EffectDecl::test_block`: "every effect *must* declare one …
enforcing the rule is a resolver-stage concern, not the parser's." The cell was
pinning a promise the tree made and no pass kept.

The mechanism. `collect_project_effects` walks every `effect Name { … }` in the
build closure and already refuses one property of the declaration — E0501, the
name declared twice. Completeness is the same kind of fact: knowable from the
declaration alone, and it must hold before any call site resolves, for the same
supply-chain reason E0501 is front-loaded. So the walk reads
`decl.test_block()` and a `None` is **E0510**, anchored at the effect's own
name. The effect still REGISTERS when the block is missing: the declaration is
incomplete, not absent, and refusing to register it would bury one defect at
the declaration under an unknown-op cascade at every use.

The check is on PRESENCE, by ruling. A mock's operations mirror the effect's —
same names, same signatures, bodies free to be empty — but that shape is not
machine-checked, so an empty `test {}` satisfies E0510. A marker effect with no
operations owes the block too: one law, no carve-out for the degenerate arity,
and `EffectDecl::operations` already calls `effect Alloc<Ctx> { test { … } }`
the canonical marker shape.

What the stdlib cost, and what it taught. `Panic`, `Assert` and `Verdict` had
no mock and now carry one. `-> Nothing` accepts an empty body, so
`Panic::raise` — the operation that looked unmockable, since a mock returning
`Nothing` would have to diverge — was not. Its OVERLOAD is the real find: an
effect body is the one place CkLang allows an operation-name overload
(`collect_ops` says so and collects both), but a `test { }` block's members
share one flat scope, so the second `raise` is E0305. `Panic.ck` carries the
arity-2 mirror and says why, and the gap is pinned rather than papered over.

Ledgers. `BROKEN_CELL_COUNT` 43 -> 42 for the fix, then 42 -> 43 for the cell
that fix uncovered — `an_effect_mock_cannot_mirror_an_overload`, BROKEN, which
does not block E0510 and so stays a cell by the demand-driven rule.
`CELL_COUNT` 464 -> 465. Net: one closed, one opened, the queue the same
length, which is the honest reading. `plan.md` §1 and §4c follow.

What else moved. 31 mirrored blocks across the capability, resolve, types, lex
and parser fixtures and the ck_resolve unit fixtures; four `# trap`
coordinates re-anchored for the lines those blocks inserted; conformance
goldens re-blessed. The book stated the block was "optional"
(`docs/10-effects.md`) and that an effect "may" declare a mock
(`docs/12-testing.md`) — both false the day `EffectDecl::test_block` was
written, both corrected, and 10-effects now carries E0510 as a DELIVERED claim.
`EffectDecl::test_block`'s own doc no longer promises an enforcement that
exists.

Gates. `./scripts/test.sh` ALL GREEN — unit 1382 passed / 0 ignored, corpus 107
apps, capability 465 cells, docs 40 expectations held / 0 failed.
`cargo test --workspace --tests --no-fail-fast`: 1639 passed, 0 failed, 1
ignored. `cargo fmt --all -- --check`: 0. `cargo clippy --workspace
--all-targets -- -D warnings`: 0 warnings, 0 errors.
`tests/parser/effect/marker_with_generics` had been rewritten to
`effect Alloc<Ctx> { test {} }` so it would stay clean under E0510. That traded
away the thing the fixture existed for — the bare `effect Alloc<Ctx> {}` parse,
a marker effect with an empty body and a generic binder — to keep a diagnostics
section quiet. The owner's call is to keep the bare parse.

So the fixture states both halves instead. The source is `effect Alloc<Ctx> {}`
again, the CST golden pins the empty-bodied `EffectDecl` with its
`GenericParams`, and the diagnostics section carries the E0510 that shape now
earns, trapped as `E0510: 3, 8` so the untrapped ratchet does not absorb it (91
-> 87, back at the cap, unchanged). One page now says that the marker shape
parses clean and is refused after it parses — which is the split between what a
grammar bounds and what a resolver decides, written where both are visible.

Also corrects two claims that were false when they landed one commit ago. Both
the ledger paragraph and `plan.md` §4c said `tests/lex` and `tests/parser` kept
their blockless effects because "those stages do not run this pass". They do:
every `.cktest` runs the whole pipeline and reconciles diagnostics,
`# expected: cst` fixtures included, which is why those two trees needed blocks
at all. The corrected count is 31 mirrored blocks across the fixture trees — 12
capability, 5 resolve, 12 types, 1 lex, 1 parser — beside the 3 in the stdlib.

Gates. `./scripts/test.sh` ALL GREEN — unit 1382 passed / 0 ignored, corpus 107
apps, capability 465 cells, docs 40 expectations held / 0 failed.
`cargo test --workspace --tests --no-fail-fast`: 1639 passed, 0 failed, 1
ignored. `cargo fmt --all -- --check`: 0. `cargo clippy --workspace
--all-targets -- -D warnings`: 0 warnings, 0 errors.
`String::from_chars` was sketched in the docs and written in no `.ck` file, so
lexing a string in CkLang could not be expressed and `corpus_parked/route`
emitted E0301 four times. It was the ONE cell in the twelve class files on
record as blocking `ckc.ck`, which under CLAUDE.md §2 is the only fact that
authorized touching this repo at all.

The wrong reasoning was that constructing a `String` at runtime needed a
compiler change. The construction route already existed and already ran:
`impl Concat<String<sa>, String<sb>, String<sa + sb>>` (Prelude.ck) acquires
with `Alloc::os_acquire`, fills with `Bytes::copy`, and returns through
`Bytes::as_str`, which pairs a region pointer with a length into the two-word
`(position, len)` descriptor. `from_chars` is that route with a per-character
stride instead of a two-operand one. Zero lines of Rust changed.

The encode is UTF-8 and not ASCII, and it did not cost a `Prelude` edit.
`utf8_text` is module-private, but `impl Show<Char>` is its public route, so
`Show::show(cs[i])` IS the encoder and a codepoint above 127 arrives as the 2,
3 or 4 bytes it actually is. The alternative narrowing,
`Floor::ireduce::<i64, u8>(c::Int)`, is one instruction cheaper and silently
mangles every non-ASCII character. The cell discriminates the two: it builds
`hé€` from three `Char`s and compares 6 bytes against the literal, so the
truncating implementation prints 7 rather than 42. Measured by mutation, not
asserted.

The bound is written out (`List<Char, 8>`) because a size-generic parameter is
`lower: UnsupportedMatch`, the wall `impl_method_signature_size_param` pins.
Eight codepoints are at most 32 UTF-8 bytes, half of bare `String`'s 64-byte
bound, so no result can outrun the type that returns it; 16 would sit on the
ceiling and 17 would cross it silently. CkLang has no overloading, so a second
bound is a second function under a second name and buys no law. This unit ships
one. The loop is the register-law `let mut` cursor `Http.ck:52` already runs.

`mod String` sits beside `newtype String` on the resolver's record-head module
fallback, the same law that lets `mod Supervisor` coexist with
`record Supervisor`. This cell is the measurement that the fallback holds for a
NEWTYPE head and not only for the record heads it was written against.

TWO SILENT DEFECTS WERE WALKED INTO, and both are pinned rather than steered
around in silence. `a_union_return_from_a_bare_tail_carries_no_tag`: a `fn`
returning an `or`-union whose body ends in a bare expression never reaches
`lift_into_union` (`ck_lower::union_lower:64`, whose only caller is the
match-arm path at `match_lower.rs:399`), so word 0 carries no tag, every arm
compare misses, and control falls into the residual `ud2`. Exit 132 is
128 + SIGILL, confirmed by disassembly and by running the binary bare. Check is
clean and it links. `a_bare_capitalized_match_arm_is_a_catch_all_binder`: a
bare `EmptyString ->` arm binds a fresh local instead of testing a type
(`ck_resolve::bodies:601-603` binds every `Pat::Ident` with no case test;
`ck_lower::match_test:339` short-circuits on `PatternKind::Binder` before any
tag compare), so it swallows every scrutinee and returns 7 where the source
says 42. `ck_types::match_check:277-281` already computes `Cover::All` for that
arm and cannot report it, because `is_unreachable` tests a `Cover::Tags` arm
against `covered` alone and a type-assert arm is exactly `Cover::Tags`.

The second one costs today. The bare spelling is the HOUSE spelling:
`corpus_parked/route/Parser.ck` writes it four times (115, 181, 194, 262) under
a comment saying "the `EmptyString` arm is the only place an empty name can be
caught". It catches everything, so that parser's whole error path is
miscompiled the day the rest of it lowers. `tests/corpus_parked/README.md`
records it.

What did NOT move: no Rust pass changed, no diagnostic was added, no floor leaf
grew. The other half of the character surface, `p.chars()`, is untouched and
still E0728. `list_cons_match_arm` still stands behind `route`, so that app
moves one wall along and stays parked.

`string_char_surface` flips BROKEN -> WORKS at `runs: 42` and keeps its name;
the name states the position. `CELL_COUNT` 465 -> 467, `BROKEN_CELL_COUNT`
74 -> 75, both with dated provenance. The raise is one fix minus and two pins
plus: nothing regressed, both defects predate this commit, and what the ratchet
rule forbids is brokenness accumulating silently. plan.md §1 and §4c,
problems.md wave arithmetic, problem_composed_but_unimplemented.md and
corpus_parked/README.md all reconciled against the new state.

Gates: `./scripts/test.sh` ALL GREEN — unit 1382 passed / 0 ignored, corpus 107
apps, capability 467 cells, docs 40 expectations held / 0 failed.
`cargo fmt --all --check` clean. `scripts/test-integration.sh` NOT run at the
owner's instruction; clippy unrun for the same reason, and the only Rust
touched is ledger.rs comments plus two constants.
Chapter 16 described the two-phase split at length without ever naming the
operation it is. `init` is CONSTANT FOLDING, EXTENDED TO REACH EFFECTS. `2 + 3`
folds to `5` because the compiler can evaluate it before anything runs, and
*the widest file in this directory* folds to `111` for exactly the same reason
once the evaluator may call `getdents64`. Nondeterministic in, six numbers at
the fence, deterministic out. That framing is also why the split needs no new
law: a fold answers a constant, and a constant in a size position is Law 1 in
its ordinary form. It is the chapter's opening section now.

Three things the chapter was missing.

THE FOUR RULES OF A PREP PROGRAM lived only in the sibling's `Calculator.ckh`,
so the chapter had the mechanism and not the intent. They are here now, and
rule 4 is written as the trade it is. Prep is exempt from Laws 1 and 5 because
it is not the thing that flies, and it pays for that by never being allowed to
state a behavioral result, only a measurement rule 3 then re-checks. Both
halves are load-bearing and the chapter carried only the first.

"PREP FOLDS AN UNKNOWN WORLD INTO NUMBERS" IS FALSE, and the chapter leaned on
it. Only an unknown the build can see folds. A number measured on the corpus
you had and imposed on a world you never saw is a bet, not a fold. The honest
statement is that the project declares an admissible world and refuses
everything outside it, which is what `MAX_SOURCES` above the fence already is
and was never named as.

THE FOLD IS SOUND ONLY UNDER THREE CONDITIONS, two of which nothing checks. It
must be a function of the corpus, or the BUILD is nondeterministic and two
people compiling identical sources get two different memory contracts. And each
plex must fold under the right operator: `max` for a plex that resets between
sources, `+` for one that lives for the whole run. `Extent` is six `Int`s under
a single operator and cannot say which kind a field is, so the wrong operator
answers a well-typed number that is not a bound and no type catches it. This
tree escapes it only because `main` folds one source.

Also stated: space is deterministic here and time is not, because Law 5's STEP
counts still do not compute and `while<N>`'s per-loop bound is what blocks
them. Law 5 is a peer of Law 1, so "deterministic" does not get to ride on the
space half alone.

Open items five to six. The new one is `sources`: the build compares `init`'s
returned `Extent` with the fenced one by value, the corpus list is a `const`
beside it that no comparison covers, and it can therefore drift from the
directory prep walked. It was covered in v1, where prep emitted `.ck` text and
wrote the list itself, so the hole arrived with the change that made prep
unable to write at all. The reason given for keeping it out does not force it
out either: a field may not take its capacity from a SIBLING field, and
`List<String<64>, MAX_SOURCES>` takes its capacity from a project const.

`scripts/test-docs.sh`: 40 expectations held, 0 failed. `ckc check` on
`CkLang:tests/examples/calculator2` still reports the 87 the chapter prints.
Eight cells, and not one of them is a feature. `String::from_chars` landed
yesterday and the audit that followed asked a narrower question than "does it
work": which parts of the string and character surface does any gate in this
repo actually RUN. The answer was less than the cell count suggested. 82 cells
put `String`, `Char` or `EmptyString` in their code, and the cluster is deep
where a string is a two-word descriptor — bounds, coercion, concat arithmetic,
`Show` — and close to empty where it is text.

FIVE GREEN CELLS, which cost the BROKEN ratchet nothing and are the point of
the sweep. `char_ord_and_equals_operators`: four of the six glyphs (`!=`, `<=`,
`>`, `>=`) were run on a `Char` by nothing in the repo;
`tests/corpus_test_run/char_scalar/Main.ck:48-49` runs the other two on 'A' and
'B', both inside one byte. `char_int_round_trip`: `c::Int` is the spelling
`Prelude.ck:751` depends on and it was reached only from inside `Show<Char>`.
`char_show_is_utf8_at_every_width`: the 4-byte branch of `utf8_text` was run by
nothing — `string_char_surface` tops out at three. `string_from_chars_at_the_
declared_maximum`: `List<Char, 8>` with eight live elements and no spare slot,
every character four bytes, which is the `while<8>` bound taking its last
admitted turn. `string_equals_and_not_equals_over_computed_values`: every other
string equality in the repo has a literal on one side, and `!=` on a `String`
had no run at all.

THREE BROKEN CELLS, all three predating this commit.
`string_chars_view_has_no_method` — the READ half, `p.chars()`, E0728. It was a
sentence in four files (`string_char_surface`'s own carve-out,
`corpus_parked/README.md:120`, `problems.md`, `Parser.ckh:41`) and a number in
none. `string_index_read_does_not_lower` — `s[i]` on a `String` checks clean,
lowers, dies at `codegen: Unsupported`; `List.ck:338` writes that exact
spelling inside `impl Collection<String<n>, u8>`.
`collection_at_on_a_string_does_not_lower` — `lower: MissingVariantTag`, where
the same call on a `List` (`list_fn_at`) is `lower: PayloadLayout`. Two
receivers, one library function, two refusals: without both cells a
payload-layout fix would look like it cleared the string side too. Reading a
string now has three spellings on the board and two walls; what runs is
`Bytes::at(Bytes::of_str(s), i)`.

CONSOLIDATED, not multiplied. `Collection::is_empty` on a `String` gets no cell:
it reads the same field as `Collection::len` and stops at the same variant,
which `collection_len_on_a_string_does_not_lower` already holds.
`string_char_surface`'s "four things at once" is trimmed — its point 4 described
two defects that are now cells of their own, and describing a defect is not
pinning it.

THREE CLAIMS DID NOT SURVIVE MEASUREMENT, which is the part of this commit
worth reading. (1) The maximum cell first claimed it pinned the worst-case
region arithmetic. Rebuild the stdlib with `8::usize` at `String.ck:59` and it
still answers 42: `Bytes::as_str` pairs the pointer with the running offset,
nothing reads the region's declared size, and the acquisition rounds to a page.
An undersized result region is a silent overwrite no cell in this matrix would
notice, and the cell now records that instead of the opposite. (2) The `chars`
cell claimed the parked parser calls it four times. It calls it ONCE, at its
entry (`Parser.ck:37`); the four belong to `from_chars`, the direction that
landed. (3) `String.ckh` and `string_char_surface` called
`impl_method_signature_size_param` "the same wall" as a size-generic
`from_chars` parameter. Measured by rebuilding the unit with `List<Char, n>`:
the module fn is `lower: UnsupportedMatch`, that cell is `monomorphize:
NoInstantiation`. One family, two stages, and all three sites now say so.

THE BOOK, RECONCILED. `docs/03-types.md` gains the character surface, both
halves, DELIVERED and DESIGNED, with two fixtures — `docs/examples/
string_from_chars/` and `docs/examples/chars_has_no_method/`, the second failing
when the gap closes. `docs/13-floor.md`'s roster is 43 units with `String` in it,
its tier table gains a row, its "nothing acquires memory at run time" bullet now
names the value-returning exception it always had (`++` mmaps per call, and so
does `from_chars`), and `| units | 38 |` — a fossil from the book's first commit
that two later roster updates missed — is 43.

CHAPTER 5 WAS FALSE THE DAY IT WAS WRITTEN. It marked or-union match DESIGNED,
"does not lower (`UnsupportedMatch`, probe 07)".
`union_return_match_binds_a_record_member` landed 2026-08-17 at `runs: 42`, one
day before the book. Run the sibling's own probe 07 program here and it stops at
`codegen: Unsupported`, not `UnsupportedMatch`, and only because its `fn try`
has no body; give it one and it answers 42. The probe ledger runs against
`ckc-bootstrap-v2`, built from `b32e2ec2`, which does not contain the UN1 union
frame — `git merge-base --is-ancestor 737636e5 b32e2ec2` is false. The chapter
now states the delivered idiom and its two SILENT failure modes, both cells:
a bare capitalised arm is a catch-all binder, and a bare `= expr` tail returns
an untagged frame. `docs/15-gaps.md` carries the correction as its third note,
beside the two it already had for probes 03 and 12, and `docs/13-floor.md`'s
`-1` sentinel argument keeps its non-emptiness leg and loses the union one.

COUNTS SWEPT. `440`/`45` cells was stated in `docs/01-laws.md`,
`docs/12-testing.md` twice and `docs/15-gaps.md`; `437` in `common/mod.rs`;
`440` again in `outcome.rs`, `scripts/test.sh` and six comments in
`scripts/test-capability.sh`. All now 475/78 or re-expressed per-cell. Both
chapters claiming the BROKEN ratchet "never goes up" are corrected: the gate
asserts both directions and prints the instruction for each, which is what this
commit uses. `docs/README.md`'s sample output was 38 expectations and the real
number was 40 before this commit and is 42 after. `CLAUDE.md`'s "38-unit
superset" is 43, and its `BROKEN_CELL_COUNT` path lost the ledger split.

M1 WAS RED FOR A DAY AND IS NOW GREEN. `52b447de` added
`Alloc::os_acquire(32::usize)` at `String.ck:59` and never ran the architecture
target, so the frozen condemned-allocation inventory has been violated since.
The row is added deliberately, at the owner's instruction, with its
justification in the file: the region is not scratch, it is the RETURNED VALUE,
and `impl Concat<String<sa>, String<sb>, String<sa + sb>>` (`Prelude.ck:458`) is
the same shape already pinned. The two alternatives died by measurement, not
argument. Folding `++` is dead at two characters: `Show::show(a) ++
Show::show(b)` is `String<128>` against a declared `String<64>`, E0722, and
eight would be `String<512>`. Taking the region as a parameter does not remove
the acquire, it relocates it into four call sites, one of them the parked route
parser — the inventory grows by 4 to buy a claim of 0. Two false claims in that
file's own doc comment went with it: the orientation census summed to 380
against a table of 394, and "sorted by path" was false for six rows that drifted
when apps were renamed in place. `CONDEMNED_ALLOC_SITES_COUNT` 394 -> 395.

`CELL_COUNT` 467 -> 475 and `BROKEN_CELL_COUNT` 75 -> 78, both with dated
provenance. The raise is three bugs written down, not three bugs introduced;
what the ratchet rule forbids is brokenness accumulating silently.
`problems.md`'s wave ladder is recomputed and now agrees with its own summary
table, which it had not since 2026-08-19 — the table was updated and the
per-wave headers were not, leaving the ladder two off. Class membership is
argued rather than assumed: `string_index_read_does_not_lower` joins
composed-but-unimplemented beside the `len` cell it shares a wall with, and the
class rule is widened in the same edit to the one its members actually satisfy
(the library promised more than the pipeline delivers, by a `composes` clause in
five cases and an impl block in two). The other two are unclassed, because a
plain absence is not a broken promise and a cell that names the fixes which will
not clear it does not get filed under one of them.

Gates: `./scripts/test.sh` ALL GREEN — unit 1382 passed / 0 ignored, corpus 107
apps, capability 475 cells, docs 42 expectations held / 0 failed.
`cargo test -p ck_driver --test architecture` 39 passed / 0 failed, up from
38 / 1. `cargo fmt --all --check` clean. `scripts/test-integration.sh` and
clippy unrun, as before.

One thing this commit does not fix and should be someone's next decision:
`./scripts/test.sh` does not run the architecture target — its own footer lists
`architecture/` under "NOT COVERED BY THIS GATE YET". That is the mechanism by
which M1 stayed red for a day, and it will do it again.
mdoorn manually merged commit 1159a899c3 into master 2026-09-17 14:43:41 +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!232
No description provided.