bootstrap: the compiler becomes a pinned tool — src/cklang stops needing src/rust #214

Manually merged
hkowalski merged 7 commits from feat/214-bootstrap-the-compiler-becomes-a-pinned-tool-s into master 2026-09-01 16:19:27 +02:00
Owner

bootstrap: the compiler becomes a pinned tool — src/cklang stops needing src/rust

Along the way:

  • split: this repo becomes the bootstrap — the language moves out
  • split: the two harnesses that reached across the seam are settled, not stranded
  • rescue: the pre-split branches and the loose editor script land where they belong
  • lists: a record element's read is its address — indexing a list of records lowers
  • consts: a module const may spell a size — and the gate that should have caught its debt starts running again
  • corpus: ml_othello is deleted, and every gate that leaned on it says so out loud

Touches 55 files: .gitignore, CLAUDE.md, LICENSE, README.md, scripts/cklang_ai.sh, scripts/editor.sh and 49 more.

bootstrap: the compiler becomes a pinned tool — src/cklang stops needing src/rust Along the way: - split: this repo becomes the bootstrap — the language moves out - split: the two harnesses that reached across the seam are settled, not stranded - rescue: the pre-split branches and the loose editor script land where they belong - lists: a record element's read is its address — indexing a list of records lowers - consts: a module const may spell a size — and the gate that should have caught its debt starts running again - corpus: ml_othello is deleted, and every gate that leaned on it says so out loud Touches 55 files: `.gitignore`, `CLAUDE.md`, `LICENSE`, `README.md`, `scripts/cklang_ai.sh`, `scripts/editor.sh` and 49 more.
hkowalski approved these changes 2026-09-01 09:56:18 +02:00
hkowalski left a comment

Reads well. One question on src/rust/crates/ck_resolve/src/const_values.rs, nothing blocking.

Reads well. One question on `src/rust/crates/ck_resolve/src/const_values.rs`, nothing blocking.
@ -0,0 +155,4 @@
// Reason: the fold threads its collected initializers, the module table, the
// interner, and the memo + cycle sets through one recursive walk; bundling them
// would only move the unpacking inward.
fn eval_expr(
Owner

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

`eval_expr` 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 eval_expr is a pure function now, with its own tests in src/rust/crates/ck_resolve/src/const_values.rs.

Split. The decision in `eval_expr` is a pure function now, with its own tests in `src/rust/crates/ck_resolve/src/const_values.rs`.
dferreira approved these changes 2026-09-01 14:21:33 +02:00
dferreira left a comment

Second read on src/rust/crates/ck_resolve/src/const_values.rs. Nothing to add.

Second read on `src/rust/crates/ck_resolve/src/const_values.rs`. Nothing to add.
The bootstrap is not a codebase we live in; it is a tool `ckc.ck` is compiled
BY, worked on only when `ckc.ck` needs it. This makes that structural. Both
src/cklang gates now resolve their compiler from a declared pin instead of a
build tree, and both run GREEN with `src/rust` renamed off the disk and the
compiler cache cleared — the split is now a file move, not a leap.

The pin is a DIRECTORY, not a binary: `ckc` resolves the freestanding crt
beside its own executable (ck_driver/src/link.rs:41) and refuses to link
without it, so pinning the compiler alone would pin half a tool and leave the
other half floating in a build tree. `bin/ckc-bootstrap-v<N>/{ckc,ck_start.o}`,
hashed whole by the same construction corpus-selfhost.sh uses over src/cklang.

`[profile.bootstrap]` — optimized, but `debug-assertions` and `overflow-checks`
ON. Plain `--release` is 10M and silently drops both; the bootstrap leans on 13
`debug_assert` sites (one documented at ck_parser/src/operator_table.rs:174 as
the detector for a bundled-Operators.ck bug), and unchecked overflow in a
compiler whose whole job is Law 1 arithmetic turns a loud refusal into a silent
miscompile of the program that replaces it. 129M debug -> 9.5M, guards intact.

The `src/rust/target/debug/ckc` default is DELETED, not redirected. A build
tree is not a pin: it changes underfoot, it is unversioned, and its presence
made every gate result quietly a function of whatever `cargo build` last left
there. Absent pin, missing binary, sha mismatch -> refusal naming the reason.
`--ckc` survives only to validate a CANDIDATE bootstrap, and downgrades the
verdict to "CANDIDATE OK — UNPINNED, NOT a gate result" so an override run can
never be quoted as a gate pass.

The binaries are gitignored: ~9.5M a version, kept by git forever, from a
source tree that is not public. So the pin DECLARES an identity and verifies
it rather than distributing it. A fresh clone therefore refuses until a
bootstrap is built at the commit scripts/.env names — the correct failure.

Also, each one a bill this work surfaced rather than a digression:

- src/cklang/bin -> src/cklang/.cache. Root bin/ is tracked pinned INPUT; that
  directory is ignored generated OUTPUT. Two dirs named `bin` with opposite
  meanings is the carve-out Law 2 forbids.
- time-compile.sh joins the pin. It compiles against src/cklang/stdlib and
  refuses a dirty src/cklang — a src/cklang tool that was resolving its
  compiler from a build tree, so its reports were keyed by rev but built by
  whatever happened to be on disk. The report now records the bootstrap
  version: two bootstraps emit two images from one source.
- timing reports become `<stamp>-<rev>.md`, stamp first, so `ls timing/` reads
  chronologically and re-measuring one commit records a second observation
  instead of erasing the first. Existing four renamed by git-add time.
- tictactoe/ttt_train_report/cklang_ai deliberately do NOT take the pin, and
  now say so: they drive tests/corpus/, the bootstrap's own oracle, and want
  the freshest build precisely so a stale binary cannot lag the source they
  exist to exercise. They travel with the bootstrap.
- cklang_ai.sh had `ws` as a local array in one fn and a local string in
  another. No runtime bug, but a reader trap and a linter error; the string is
  now `swh`, which is what it holds.

Licence: MIT OR Apache-2.0 -> source-available, all rights reserved (read it,
do not use it). `license-file` cannot be used — all 20 member crates inherit
`license.workspace`, so it is an SPDX `LicenseRef-`, which claims nothing.

selfhost-verify: 287 passed, 0 failed. corpus-selfhost: 66 rows, 0 fails.
Both with src/rust absent. shellcheck -x: 0 across all seven scripts.
The bootstrap is a TOOL that `ckc.ck` is compiled BY, worked on only when
`ckc.ck` needs it (owner, 2026-08-17). Keeping it in the same tree as the
language made that impossible to see: agents landed in src/rust by default,
and two stdlib roots had to be kept apart by a hand-spelled rule whose
violation caused the Bounds.ck incident.

So the tree splits. This side keeps what implements the v0 compiler and what
proves it; ~/Projects/CkLang keeps the language. History is preserved whole —
this repo is a clone of the pre-split tree with the other side removed, so
`git log --follow` still reaches every commit that shaped these files.

Kept here: src/rust (20 crates), crt/ck_start.s, tests/{capability, corpus,
corpus_parked, corpus_test_run, frame_pin, lex, parser, resolve, sizes_pin,
types}, and the scripts that drive them — test.sh/test-rotate.sh (the cargo
gate), deploy.sh, run.sh, clean.sh, ckeditor-integration.sh, and the three
ML/Othello stones (cklang_ai.sh, tictactoe.sh, ttt_train_report.sh) which
drive tests/corpus and deliberately want the freshest build, not a pin.

Removed here: src/cklang, tests/corpus_pure, docs, editors, reports, timing,
the plan files, and the three src/cklang scripts with their pin library.

docs/ is deliberately NOT duplicated. Two copies of a design doc is two
designs; the CkLang repo is the single source of truth and CLAUDE.md §6 says
so. What this repo gains instead is its own CLAUDE.md: the demand-driven
maintenance rule, the BROKEN-cell queue discipline, and the pin-cutting
protocol — including why a pin is built with [profile.bootstrap] and never
--release, and why the pin is `ckc` PLUS `ck_start.o`.

The machine is still one machine: rotate-exec.sh and cpu_broken.md are
present on both sides because both repos can build at once.
Pruning the language side left two ck_driver test harnesses pointing at
fixtures that now live in the other repo. Neither is deleted for convenience;
each is resolved on what it actually asserts.

`corpus_pure.rs` is DELETED. Its subject is `tests/corpus_pure/` compiled by
`src/cklang/compiler` — both now in the CkLang repo, where
`scripts/corpus-selfhost.sh` runs the identical oracle (same pin forms, same
one-image law, same two-compiles-one-image assertion) without cargo. Its own
header already said the bootstrap "is NOT a party to this oracle": the pin is
the truth and the self-host is the thing on trial. Nothing here was measuring
this compiler, so nothing here is lost.

`stdlib_composes_silence.rs` STAYS, repointed from `src/cklang/stdlib` to
`tests/corpus/stdlib`. Three of its four assertions are claims about THIS
compiler's checker — that an unknown composed trait is silent in a dependency
page, that the silence is an attribution boundary rather than an absent check,
and that the same bytes fire E0753 when checked as the app. They used the
CkLang stdlib only as a realistic fixture. The bootstrap's own stdlib root
carries a byte-identical `String` declaration, so `STRING_DECL` matches
unchanged and the doctoring still bites.

Its fourth assertion, `the_live_stdlib_checks_clean_as_its_own_root`, now
guards THIS repo's stdlib. The same gate for `src/cklang/stdlib` does not
vanish with it: it is re-homed in the CkLang repo as a leg of
selfhost-verify.sh, because the composes clauses it vouches for are exactly
the claims that got deleted for an afternoon on 2026-08-15 when no gate read
them.

cargo metadata: ok. No path in this repo now reaches across the seam except
prose deliberately citing the other side.
The clone that made this repo copied every object but kept only `wip_v2` as a
ref — `git remote remove origin` dropped the rest. So 80 branches, carrying 32
commits that never reached wip_v2, existed here as unreachable objects: present
in the database, invisible to every command, and one `git gc` from collection.

They are fetched now under their original names. Their content is Rust, the
capability matrix, and the old corpus — this repo's subject matter, unmergeable
into the CkLang tree by construction because the paths they patch left it. The
notable ones: slice-stack-3 (6 commits, the frame-trail work), slice-t0-
soundness (3, name-resolved type positions), surface-if-finish (4, block-param
words and five capability cells). Verified reachable, not merely present:
`git branch --contains 5dc7edd3` now answers slice-stack-3.

`scripts/editor.sh` was untracked in the error-tooling worktree — 103 lines
opening a corpus app in CkEditor, defaulting to tests/corpus/calc. Those apps
are here, so the script is too, and it stops being one `rm -rf` from gone.

This lands BEFORE the CkLang side drops its worktrees and branches, so that
removal costs nothing.
The pass could already WRITE a record element into a list buffer and had no way
to read one back, because an index-read could only ever produce a Load of a
scalar and a record is not a scalar. What closes it is not a new node: a record
value in the Floor IR IS a frame base pointer, and the element's frame is
already laid out in place inside the buffer, so the strided address xs[i]
computes IS the value. The read is the address, and every record position the
pass already had takes it from there.

  - lower_element_read: a scalar element Loads its repr; a record element
    returns the Offset itself, typed with the read's checked type so it
    classifies back as ValueRepr::Record.
  - index_receiver: ONE classifier for what counts as a list receiver, a
    literal or a local, so xs[i] and xs[i].f cannot disagree about what a list
    is. record_lower's dedicated xs[i].f arm is DELETED — with the index-read
    able to produce a record value, a second path to the same bytes is a second
    layout waiting to disagree.
  - element_frame: a record element's fields are written BY NAME into the
    element type's declared ABI row, not in the order the literal spells them.
    That row is the layout the projection, a fn boundary, and a seeded list
    parameter all already assume; the write now agrees with its readers instead
    of with the source's spelling.
  - seed_lists: a record-element list PARAMETER seeds from the same row, so
    caller and callee agree because they read one table.

Codegen gains one arm: emit_record_value accepts an Offset, which is already an
address — the same no-copy sub-address the Load arm yields for a nested field.

R3: list_lower.rs hit 569 against the 540 cap, so the READ moved out rather than
the cap moving up. list_store.rs became list_slot.rs and took the read in beside
the write, which is where it belonged — they already shared element_address, and
the module header now states that as its reason for existing. lower.rs sits at
exactly 540, which is why the read joined an existing module instead of opening a
new one; the next ck_lower module has to split lower.rs first.

Ratchets, all in this commit:
  - record_as_list_element: lower: ListLayout -> runs: 41. CELL_COUNT 416 -> 424
    for seven further cells — the element bound to a local, crossing a fn
    boundary, through a list parameter, out-of-order element fields, a nested
    record two deep, the store door still refusing, and one bug below.
  - CARRIER_COUNT 12 -> 11: `lists` retired, its gap was exactly this one. It
    left tests/sizes_pin/unbounded_inventory.txt entirely, 12 apps -> 11.
  - UNMARKED_CELL_COUNT 149 -> 148.
  - BROKEN_CELL_COUNT 48 -> 50, for two bugs this UNCOVERED rather than caused —
    the ListLayout refusal was the lid on both, and both reproduce at the parent
    commit:

    record_literal_field_order_is_lost_crossing_a_fn — a plain record literal is
    laid out in the order its fields are WRITTEN, not DECLARED, so
    P { b: 41, a: 1 } handed to fn pick(p: P) -> Int { p.b } returns 1. Self
    consistent inside the caller, wrong at the boundary, and no refusal fires.
    The fix is the one applied to list elements here, one tier over:
    lower_record_ctor should take its layout from record_layout_of_ty and store
    by name, keeping plan_lowered_fields as the row-less fallback. Not done here
    because it is the record tier's door and would move cells this slice does
    not own.

    a_list_literal_in_a_test_fn_body_fails_the_suite — any arena-allocating
    construct in a test fn body fails the test, with no read of it and a
    trivially-true predicate, while the identical literal on the run entry is
    correct. It had never executed: corpus_test_run/lists stopped at ListLayout
    in a sibling body, so its two Int assertions had never once run. corpus_run
    does not catch it either — lists/run.md declares exit: 0 and the driver
    returns 0 even when the tests it ran failed, so that app has been green on a
    gate that never looked at the tally.

The element STORE (xs[i] with mut <record>) still refuses, honestly and on
purpose: a record element write is the per-field write construction already
does, aimed at a computed address, and that is D1's wide-element half.
record_list_element_store_does_not_lower is the pair that pins the asymmetry.

Gates: fmt and clippy --all-targets -D warnings clean, zero warnings; capability
matrix green at 424 cells; door and unbounded-inventory gates green. Two gates
are RED and were RED at the parent commit, both split leftovers pointing at paths
that moved to the CkLang repo: ck_docs_linkchecker wants docs/, and
architecture::floor::f2 wants src/cklang/stdlib/Floor.ck. They are not this
change's and are left for a decision about whether they belong here at all.
Point 1 of the Main3.ck header: a size is a number the compiler computes from
source (Law 1), so it must be spellable by the `const` that derives it rather
than by a literal repeated beside a comment. `ConstScalar` + the interner's
registry hold the settled values; `ck_resolve::const_values` is the ONE
evaluator, hung inside `link_project` because linking is the pass every harness
funnels through and a fold hanging off any single entry point would be a fact
one harness had and another did not. `value_slot_const` reads the registry in
the lift, `check_value_slot_node` re-runs the same predicate so check and lift
cannot disagree, and `fold_closure_consts` stops evaluating anything — it now
only admits a registered value against its declared ascription. No operator
glyph is matched in Rust; `+` reaches its method name through
`standard_operator_table`, exactly as lowering desugars it.

That work was not gate-green, and the gate could not say so.

`scripts/test.sh` ran `cargo test --workspace` with no `--no-fail-fast`, so
cargo stopped at the first failing test binary and every target after it
silently never ran. `ck_docs_linkchecker` failed first — it asserts a `docs/`
tree that `07a49fef` deleted from this repo — and behind that one stale red sat
three more the gate never reached:

  - R3: ck_types/src/lift.rs at 914 lines against the 862 cap;
  - R4: ck_resolve/src/link.rs `link_project` at 110 against the 100 cap;
  - floor::f2, reading src/cklang/stdlib/Floor.ck, which the split moved out.

A gate whose output depends on which red comes first is not a gate. It reported
one failure when the truth was four.

  - ck_docs_linkchecker is DELETED (owner call). `docs/` is in neither repo
    now, so the check could never go green as written, and a gate that cannot
    pass teaches readers to skip the suite. Its row leaves the R1 crate graph
    with it.
  - floor::f2 keeps its teeth on what this repo OWNS: tests/corpus/**/Floor.ck,
    65 intrinsic sites. The sibling's stdlib floor answers to the sibling's
    gate (CLAUDE.md §6) — a path this repo cannot see is not a rule it can
    enforce. A non-vacuity assert now fails loudly if that corpus file ever
    disappears, because a silently-empty half A is the one way this rule rots.
  - R3 is resolved by remedy (a), splitting, never by raising the cap:
    lift/value_slot.rs takes the generic-argument slotting unit it already was
    — lift_generic_args, the two value_slot_* recognisers, and the value-param
    binding they share. lift.rs 914 → 651.
  - R4 likewise: bind_scalar_aliases_to_fixpoint lifts the alias-transparency
    second pass out whole. link_project 110 → 85.
  - test.sh runs --no-fail-fast, so one stale red can never again hide an
    unbounded number of live ones.

Gate: GREEN, 0 failures, 0 warnings, full sweep.
The owner removed the app (11 files: the program, its `tests/sizes_pin` and
`tests/frame_pin` entries). It was the corpus's largest source of noise — 240
of the 262 rows in the unbounded inventory were its unlowerable bodies — and
the request was zero errors, not a quieter report.

What makes this more than a `git rm` is that eight other places named it, and
each had to be settled honestly rather than edited to compile:

  - `door::CARRIERS` loses its `ml_othello` row, but the comment left behind
    records that this is NOT a retired gap. `MixedArmValues` and
    `MutLocalShape` are still open; they simply have no corpus witness now.
    The carrier ledger is evidence of what the compiler cannot do, and
    deleting the demonstration does not close the gap. `CARRIER_COUNT` 11 → 10
    with the same distinction spelled out.

  - `a_listed_carrier_is_downgraded_not_refused` hardcoded the app and died
    with it. It now walks the whole registry — a test pinning a law over a
    registry should not have a favourite row. Broadening it immediately found
    that the law has TWO halves at different scopes: every carrier is
    DOWNGRADED (never E0760), but only `tests/corpus` is positive-only, and
    `mock_dispatch` legitimately reports E0503 standalone because supplying
    that provider is the substrate's job. Conflating them made the broadened
    test fail on a correct app.

    That split exposes the real cost of this deletion: ml_othello was the ONLY
    carrier under `tests/corpus`, so the positive-only half is VACUOUS today.
    The assertion is kept live and the vacuity is stated in the test, not
    hidden — it re-arms by itself the moment a corpus carrier appears.

  - M20 fell 64 → 14 (ml_othello's 8x8 board literal was the corpus worst case
    for items in one list literal; it is now algorithms_BFS_trapped). The
    `grammar_structure` reconciliation caught this on its own and printed the
    corrected row, which is the gate working. The DECLARED bound stays at 128:
    it is a language limit written into `Grammar.ckx`, not a lint threshold,
    and lowering it would reject a 30-item literal that is legal today. The
    seeding note now says that instead of citing a program nobody can read.

  - The unbounded inventory is re-blessed: 11 apps / 262 components → 10 / 22.
    Pure shrink, the only direction that harness permits.

  - Four historical citations (two doc comments, three capability provenance
    notes) keep their claims — the history is true — and gain the deletion
    date so no reader hunts for a missing path. `tests/corpus/README.md` is
    the exception: its "without the floor" bullet was a CLAIM, that the
    integer-native capabilities do not depend on the carved-region substrate,
    and ml_othello was its only demonstration. Law 3 says a behavioural claim
    exists as a runnable cell or it does not exist, so the bullet is rewritten
    as an open gap rather than quietly dropped from the list.
hkowalski manually merged commit e5824fe5af into master 2026-09-01 16:19:27 +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!214
No description provided.