working on next-broken #228

Manually merged
mdoorn merged 5 commits from feat/228-working-on-next-broken into master 2026-09-14 08:30:00 +02:00
Owner

working on next-broken

Along the way:

  • console: the standard library gets one, in CkLang, over two floor leaves
  • fix_console.md: the surface defects the console exposed, counted
  • stdlib: a Mem tier, so a unit that wants a word at an offset stops spelling the machine
  • fix_console.md: item 1 landed with its number; item 2 is a fourth gap, not a surface fix

Touches 27 files: .claude/commands/next-broken.md, docs/13-floor.md, docs/examples/console/Main.ck, docs/examples/console/expect, docs/examples/console/project.ck, fix_console.md and 21 more.

working on next-broken Along the way: - console: the standard library gets one, in CkLang, over two floor leaves - fix_console.md: the surface defects the console exposed, counted - stdlib: a `Mem` tier, so a unit that wants a word at an offset stops spelling the machine - fix_console.md: item 1 landed with its number; item 2 is a fourth gap, not a surface fix Touches 27 files: `.claude/commands/next-broken.md`, `docs/13-floor.md`, `docs/examples/console/Main.ck`, `docs/examples/console/expect`, `docs/examples/console/project.ck`, `fix_console.md` and 21 more.
mdoorn approved these changes 2026-09-12 09:52:55 +02:00
mdoorn left a comment

Yes. The commit messages carried me through all 27 files, which is not always true.

Yes. The commit messages carried me through all 27 files, which is not always true.
@ -193,6 +214,7 @@ const CONDEMNED_ALLOC_SITES: &[(&str, usize)] = &[
("tests/corpus_test_run/bytes_roundtrip/Main.ck", 2),
("tests/corpus_test_run/clock_tick/Main.ck", 1),
("tests/corpus_test_run/freestanding_mem/Main.ck", 3),
("tests/corpus_test_run/console_io/Main.ck", 2),
Owner

I had to read Main twice. Nothing wrong with it, but one line of comment would save the next person the second read.

I had to read `Main` twice. Nothing wrong with it, but one line of comment would save the next person the second read.
@ -562,0 +579,4 @@
// not the leaf. Pinned by `a_floor_load_in_a_test_fn_body_does_not_lower`. It
// blocks nothing: the console reads its control block through
// `Stdio::last_len`, a cross-module call that was the better surface anyway.
const BROKEN_CELL_COUNT: usize = 44;
Owner

Where does the number beside BROKEN_CELL_COUNT come from? It reads as measured rather than chosen, and nothing on the line says which, at src/rust/crates/ck_driver/tests/capability.rs:582.

Where does the number beside `BROKEN_CELL_COUNT` come from? It reads as measured rather than chosen, and nothing on the line says which, at src/rust/crates/ck_driver/tests/capability.rs:582.
@ -0,0 +31,4 @@
fn out(s: String) -> Unit = write_str(1::i64, s)
// fd 2.
fn err(s: String) -> Unit = write_str(2::i64, s)
Owner

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

`err` 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

Measured. It is a named constant in src/rust/crates/ck_driver/tests/capability.rs now, with a line saying how, at src/rust/crates/ck_driver/tests/capability.rs:582.

Measured. It is a named constant in `src/rust/crates/ck_driver/tests/capability.rs` now, with a line saying how, at src/rust/crates/ck_driver/tests/capability.rs:582.
Author
Owner

Comment added on Main in src/rust/crates/ck_driver/tests/architecture/alloc.rs. It was obvious when I wrote it and it will not be in a month.

Comment added on `Main` in `src/rust/crates/ck_driver/tests/architecture/alloc.rs`. It was obvious when I wrote it and it will not be in a month.
Author
Owner

Split. The decision in err is a pure function now, with its own tests in tests/corpus/stdlib/Stdio.ck.

Split. The decision in `err` is a pure function now, with its own tests in `tests/corpus/stdlib/Stdio.ck`.
`Stdio` (fd 0/1/2, direct), `effect Console` + `StdioConsole` (the dispatched,
config-selectable face), six capability cells, a run-gated corpus app, and a
section in the book. No Rust, no new intrinsic, no floor change:
`write_bytes`/`read_bytes` have been in `Floor.ckh` since the IO widening.

WHY NOW. `tests/corpus/ml_tictactoe` had hand-written a console — a `say`, a
byte writer, and a buffered fd-0 reader with its own two-word control block —
which is the standard library admitting it had none. And nothing in this repo
RAN a program that read fd 0 at all: that app is size-pinned, never executed, so
the reader every interactive CkLang program needs was unproven at runtime.

THREE CONSTRAINTS WROTE THE API, and each is a gap showing through rather than
a preference (plan.md §4g carries the long form):

  * no `Alloc` — it is condemned and the self-hosted stdlib excises it, so the
    three units hold ZERO condemned sites and port unchanged. The price is
    visible: `line` is two writes because `s ++ "\n"` mmaps, and integers go
    through `int_into(scratch, n)` because `Prelude::show_i64` mmaps twice and
    releases one of them;
  * end of input is `-1` — `String` is structurally non-empty and `match` over
    a union does not lower, so it cannot be `""`, `Option`, or `Result`;
  * every `Console` operation returns `Unit` — binding an effect operation's
    result in a free `fn` leaves the frame slot unresolved and E0760 shuts the
    door, so `read_line` answers through `ctl` and `Stdio::last_len` reads it.

RATCHETS, all three moved deliberately:

  * `CELL_COUNT` 440 -> 446;
  * `BROKEN_CELL_COUNT` 43 -> 44 —
    `a_floor_load_in_a_test_fn_body_does_not_lower`, found building this.
    `Floor::load` written directly in a `test fn` body reaches codegen
    unresolved while the `Floor::store` beside it lowers and the same load
    through a cross-module `fn` lowers on both entries, so it is which bodies
    the test entry stamps, not the leaf. It blocked nothing;
  * `CONDEMNED_ALLOC_SITES_COUNT` 380 -> 394, seven rows, OWNER-BLESSED. The
    rule's own remedy is "an unavoidable site is an owner decision, not a table
    edit" — it was asked and granted. `read(2)` writes into caller memory and
    `Alloc::os_acquire` is CkLang's only way to obtain a writable `Region`;
    `Floor::mmap` reaches the same syscall past the counter and was NOT used.
    All 14 are in what exercises the console, none in the console.

MEASURED AND SAID OUT LOUD: the bundled test mock is declared, not dispatched
to. A `test fn` calling `Console::line` reaches `StdioConsole` and the bytes
land on fd 1; `Log::info` behaves identically. `Log.ckh`'s claim that the test
path resolves the mock automatically does not hold at that position, and
`Console.ckh` states what is true rather than repeating it.

Gates: 4 legs green, integration sweep 251 passed / 1 known-red (the standing
`lib.rs` R3), fmt and clippy clean.
Owner, 2026-08-18: "You should not have to use `Floor` that much. It means our
surface is not correct yet." Correct, and this file is the measurement plus the
three fixes it names.

`Stdio.ck` spells `Floor::` 43 times in code. SEVEN are the two syscalls the
unit exists to make; the other 36 are reading and writing a word at an offset —
five sixths of its floor traffic is not its subject. The callers, by contrast,
are already clean (zero `Floor::` in the corpus app, the docs example and all
six cells), and they got that way by exactly one move: naming the operation
(`Stdio::prepare`) instead of making the caller spell the offset. The three
items are that same test applied inward.

  1. `Alloc::read`/`write` are typed load/store, allocate nothing, and are
     imprisoned in a CONDEMNED unit — so a unit that must not depend on `Alloc`
     drops past it to the machine. Wants a `Mem` tier (recommended over
     `Bytes::get`/`set`, whose unit is not `Alloc`-free either). No compiler
     change; takes `Stdio.ck` from 43 `Floor::` to 7. It would also have routed
     around `a_floor_load_in_a_test_fn_body_does_not_lower` unnoticed, which is
     the sharper argument: a wrong surface walks its users into compiler bugs.
  2. `fd: i64` in six public signatures — `Stdio::newline(1::i64)` makes the
     caller name a kernel constant, and `7::i64` type-checks. Wants
     `newtype Stream` with `stdin`/`stdout`/`stderr`, the pattern `Floor.ckh`
     already uses for `Address`. Needs one probe first.
  3. No buffer type, so every caller hand-carves offsets out of one mmap and
     nothing checks they are disjoint. Not fixable here: blocked on const
     expressions in size positions, `12-range-view` (E0735, filed as "and the
     retirement of `Region`"), and the 2-word record ABI ceiling. The console's
     only correct output is the record that it is a consumer waiting on the slot
     model — when that lands, the 14 M1 sites retire together.

Deliberately NOT under `docs/`: that directory is gated prose whose every
behavioural sentence is reconciled on each gate run (CLAUDE.md §6). This is a
work list, and the long form of plan.md §4g.

Gates unchanged: 4 legs green.
`Stdio.ck` spelled `Floor::` **43 times**; **7** of those are the two syscalls
the unit exists to make. The other 36 — 18 `address_at`, 13 `store`, 5 `load` —
were 18 statements reading or writing one word at one offset, written at the
machine tier because the tier above did not have the operation. It does now:
`Stdio.ck` is at **7 `Floor::` occurrences**, all `write_bytes`/`read_bytes`.

WHAT MOVED, AND WHY IT COULD NOT STAY. `Alloc.ck:68-70` already held exactly
this — `address`/`read`/`write`, three one-line wrappers over
`Floor::address_at` + `load`/`store`. None of them allocates. None of them has
anything to do with allocation. They are load and store WITH A TYPE AND AN
OFFSET, which is the ordinary way any unit above the floor should touch memory.
They were imprisoned in a unit that is CONDEMNED (`Alloc.ckh` line 4, rulings
#49 / #52) and that the self-hosted stdlib excises altogether, so everything in
it dies with it — and a unit forbidden to depend on `Alloc` (`Stdio` is: a
console resting on it would not port) had no choice but to reach past the tier
that had the right operation, down to the machine.

That is a taxonomy error, not a preference. A unit named for allocation holding
an accessor decides what downstream units are allowed to depend on. `Mem` is the
tier that was missing: `Floor` is the machine, `Mem` is typed access to it,
`Alloc` is where memory comes FROM. It depends on `Floor` and nothing else, so
it holds zero condemned sites by construction and the M1 table is untouched.
The accessors leave the condemned unit BEFORE it dies rather than with it, which
is what makes `Alloc`'s eventual retirement smaller rather than larger.

`Alloc::address`/`read`/`write` stay, as one-line forwarders to `Mem`. Many
units and corpus apps call them by those names; this is a re-tiering, not a mass
rename, and deleting them is a separate decision that has not been taken.

THE SHARPER ARGUMENT, and it is already a filed bug.
`tests/capability/a_floor_load_in_a_test_fn_body_does_not_lower` exists because
`Floor::load` written directly in a `test fn` body reaches codegen unresolved,
while the same load through a cross-module `fn` lowers on both entries.
`Mem::read` IS that cross-module `fn`. The right surface would have routed
around that defect without anyone noticing it — a wrong surface does not merely
read badly, it walks its users into bugs the right one never reaches.

RATCHETS: none moved. `Mem` holds no condemned call site, so the M1 inventory
does not change; `CELL_COUNT` holds at 446.

MEASURED. `tests/frame_pin/ml_tictactoe.pin` gains two rows — `Mem::read<u8>`
and `Mem::write<u8>`, 49 B declared between them and **0 B emitted**: the
forwarders cost the machine nothing, which is the evidence that this was a
re-tiering and not a change. The docs example's `fn main` is still 72 B.
`docs/13-floor.md` goes 41 units -> 42 and gains the tier row.

Gates: 4 legs green (1354 unit, 107 corpus apps, 446 cells, 37 book
expectations), integration sweep 251 passed / 1 known-red (the standing
`lib.rs` R3) / 1 ignored, fmt and clippy clean.
ITEM 1 — LANDED (`acda5a97`), and the estimate held exactly. `Stdio.ck` went
**43 `Floor::` occurrences to 7**, the 7 being `5 write_bytes + 2 read_bytes` —
the two syscalls the unit exists to make. 18 statements rewritten, 36
occurrences gone, no ratchet moved, and the one pin that did move
(`ml_tictactoe.pin`, two new `Mem` rows) declares 49 B and emits 0 B, which is
what makes it a re-tiering rather than a change.

ITEM 2 — BLOCKED, and nothing of it landed. The item ordered a probe before the
surface was written on top of it, and the probe was right to be ordered: two of
its three halves work and the third refuses.

  * `newtype Stream = i64` in a unit — works.
  * `Stream(1::i64)` built inline and `.value` read in argument position at a
    `Floor::write_bytes(to.value, ..)` call — works, checks clean.
  * `const stdout: Stream = Stream(1::i64);` — refuses the moment a function
    body REFERENCES it. `ConstUnfoldable` at the reference, surfacing as E0760
    on the enclosing `fn`. Module-local and cross-module, argument position and
    `let` binding, all four the same.

It is neither a syntax problem nor a `const` problem, and three positive
controls say so: a scalar `const two: i64 = 1::i64 + 1::i64;` folds; the same
scalar const folds ACROSS modules; and `Stream(one)` — the newtype constructed
FROM a scalar const — folds. Declaring the unfoldable const and never
referencing it is clean too. Only the reference to a newtype-ASCRIBED const
refuses.

The seam is `ck_driver/src/compile/const_fold.rs` · `DeclaredShape`, and it is
by construction rather than by accident: the fold admits a CLOSED scalar frame
matched on the ascription's SPELLING (`declared_shape`), and its own doc says
"anything else keeps the const out of the table". `Stream` IS `i64` by the
newtype transparency law — the fold simply never asks. So the const never
reaches `ConstFacts` and `ck_lower/.../const_facts.rs:81` raises at the
reference.

WHAT WAS NOT DONE, deliberately. No workaround: the available dodge is spelling
the three constants as zero-argument `fn`s, which puts a call where the design
says a compile-time value goes — the same class of defect this file exists to
report. No capability cell: it would be `STATUS: BROKEN` and
`BROKEN_CELL_COUNT` is an owner call. No ratchet moved, in either direction.

The `Order of work` table now carries a state column and says what this costs
the report's own framing: item 2's "no compiler change expected" was written
without checking whether a newtype-ascribed const folds. Two of the three items
turning out to be language gaps rather than surface fixes is itself the finding.

Gates: 4 legs green (1354 unit, 107 corpus apps, 446 cells, 37 book
expectations), integration sweep 251 passed / 1 known-red (the standing
`lib.rs` R3) / 1 ignored, fmt and clippy clean.
mdoorn manually merged commit 2558ea1610 into master 2026-09-14 08:30:00 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 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!228
No description provided.