splitting functions out of main to check if this still works #52

Manually merged
dferreira merged 7 commits from feat/052-splitting-functions-out-of-main-to-check-if-th into master 2026-03-05 16:10:10 +01:00
Owner

splitting functions out of main to check if this still works

Along the way:

  • MA1.2a: while lowers — back-edged CFG, ENFORCED bound, arena ×N sizing
  • MA1.2b: for lowers — literal ranges + list literals; row 14a item (1) COMPLETE
  • plan §4: record module-local trait Error duplication (Arena.ck + Bounds.ck); restore the clobbered honest-error bullet head
  • MA1.3: recursion EXECUTION gated — fib_runs exits 55 freestanding
  • MA1.4: Memory::load/store:: — the third machine-op width, byte-proven
  • MA1.5a: Bytes.ck — pure-CkLang byte copy + the String ABI-word leaves

Touches 61 files: current-state.md, docs/impl/memory-model/arenas.md, docs/syntax.md, lowering.md, plan.md, src/rust/crates/ck_codegen/src/call.rs and 55 more.

splitting functions out of main to check if this still works Along the way: - MA1.2a: while<N> lowers — back-edged CFG, ENFORCED bound, arena ×N sizing - MA1.2b: for lowers — literal ranges + list literals; row 14a item (1) COMPLETE - plan §4: record module-local trait Error duplication (Arena.ck + Bounds.ck); restore the clobbered honest-error bullet head - MA1.3: recursion EXECUTION gated — fib_runs exits 55 freestanding - MA1.4: Memory::load/store::<U8> — the third machine-op width, byte-proven - MA1.5a: Bytes.ck — pure-CkLang byte copy + the String ABI-word leaves Touches 61 files: `current-state.md`, `docs/impl/memory-model/arenas.md`, `docs/syntax.md`, `lowering.md`, `plan.md`, `src/rust/crates/ck_codegen/src/call.rs` and 55 more.
dferreira approved these changes 2026-03-05 13:00:00 +01:00
dferreira left a comment

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

Happy with this. I ran the suite against the branch and it is green, `src/rust/crates/ck_driver/tests/fib_runs_run.rs` included.
@ -0,0 +90,4 @@
/// `ckc --container <shared> --work <name> run <app> --stdlib <repo>/tests/corpus/stdlib`,
/// returning the process exit code.
fn run_ck(name: &str, app: &Path) -> i32 {
Owner

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

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

Split. The decision in `run_ck` is a pure function now, with its own tests in `src/rust/crates/ck_driver/tests/fib_runs_run.rs`.
The loop-lowering half of plan.md row 14a item (1), first checkpoint:
`while<N>` desugars in the pass (ck_lower::lower::loop_lower) to LC2
Floor-core blocks with a back-edge — the byte-copy-loop (LC4) shape
generalized: entry `Jump head(0)` materializing the counter param, the
condition re-evaluated per iteration in the head (its own lowering may
split blocks), the guard `i < N`, the body in its own LC10 scope (defers
run per iteration) ending in the back-edge `Jump head(i+1)`. ZERO new
codegen — the mechanical CFG walker was untouched.

THE BOUND IS ENFORCED AT RUNTIME (the bounded-everything family's uniform
rule: rec<N> → StackOverflow, arena → OutOfMemory::Exhausted, while<N> →
IterationBound::Exceeded): beginning iteration N+1 while the condition
holds takes the guard's exceeded path — an injected ordinary Call on the
NEW live-stdlib unit Bounds.ck (`data IterationBound composes Error {
Exceeded }` + `fn exceeded()` = a REAL Panic::raise, the Arena.ck OOM
shape; nothing invented in Rust) followed by the sanctioned post-call
raise check (the arena_alloc_local precedent) and a Trap-terminated dead
continuation. Driver: bounds_wiring locates Bounds::exceeded by name (the
arena_wiring shape), LoopFacts (the 12th positional fact table) arms it on
all entry paths; discovery rides the existing cross-module fixpoint (the
injected Ref is an ordinary callee). UNARMED (no live Bounds.ck), a loop
errors honestly — an unenforced bound would let `while<8> true {}` spin,
the exact CPU burn decisions-scheduler-model.md bans.

Arena sizing, same slice (plan §4's rule, owner 2026-07-03): the pass
records the product of the enclosing loops' declared bounds on every
arena-allocation node (floor_ir::Body's new alloc-factor side-table;
recorded at the ONE chokepoint arena_alloc_local), and
frame_alloc_footprint prices the site × the factor — body sites ×N,
CONDITION sites ×(N+1) (it runs once more than the body), nested loops
multiply, FrameAlloc stays ×1 by construction, an unbounded component
stays honestly unbounded (×N of unbounded is unbounded). Exact-byte unit
tests in arena/tests.rs (the FS5.1 style: List<Int,4> in while<8> = 40×8 =
320 B; nested ×12; factor-1 unrecorded = byte-identical pre-MA1.2 walk).

Corpus stones + gates: tests/corpus/while_bound — memory-held state (no
`let mut`; Memory::store/load over an os_acquire'd word) steps 0 → 42 in
six iterations inside the bound of 8, exit 42, freestanding (9 448 B
observed, empty NEEDED, nm -u empty, no libc/ck_rt) — gated by
while_bound_run.rs; tests/corpus/while_bound_exceeded — the arena_oom
twin: while<4> never falsifies → exactly 4 iterations → the raise →
stderr names IterationBound::Exceeded + the hint, exit 101, no stdout, NO
SPIN — gated by while_bound_exceeded_run.rs. Both in RUNTIME_TESTED.
Pass-level goldens: lowering_loops.rs (armed CFG shape: back-edge to an
earlier block, the i<3 guard, the injected call, the Trap, the tail 42;
unarmed = honest LowerError::LoopUnsupported).

Docs, same commit: lowering.md LC9 row annotated (loop half pulled
forward, while landed MA1.2a); plan.md row 14a + the §4 loop-sizing
bullet (multiplier LANDED; slot-reuse escape analysis stays deferred
post-MA4); current-state.md §3 (while<N> RUNS); docs/syntax.md §Loops
(the runtime enforcement sentence); arenas.md (the landed ×N pricing with
exact numbers). LowerError::LoopUnsupported narrowed to `for`/range +
unarmed-while; R3/R4 held by splitting (guard/back-edge helpers,
alloc_factor accessor beside its builder, doc trims — floor_ir.rs 550/550
exactly at cap).

Gates (from src/rust): cargo fmt --check clean; cargo clippy --all-targets
-- -D warnings 0 warnings; cargo test --workspace exit 0 (architecture
R1–R4, conformance, all container e2e incl. the two new gates); cargo
build --workspace clean. 0 warnings, 0 errors. helloWorld unchanged.
The second loop-lowering checkpoint: `for pattern in iterable { body }`
desugars in the pass (loop_lower) to the same LC2 back-edged CFG as
while<N>, minus the runtime guard — a `for` iterable is statically bounded
BY CONSTRUCTION, so no exceeded path and no Trap block exist (pinned by
golden). Two iterable shapes lower:

- a LITERAL-ENDPOINT integer range `a..b` / `a..=b` — the endpoints fold
  at compile time (read_int on the manifest literals), giving the static
  trip count max(0, b−a(+1)) that is also the body's arena-sizing factor;
  the head compares Lt (exclusive) or Le (inclusive); the binder binds the
  counter each iteration.
- a LIST LITERAL of one-word scalar elements — the buffer builds ONCE
  before the loop (its allocation prices ×1), then each iteration loads
  the element at buffer + i × stride into the binder; the literal's length
  is the static count and sizing factor.

THE HONEST SCOPE (the list-repr finding, plan §4's new residue bullet): a
list VALUE is one buffer-pointer word with NO runtime length (LC4's repr;
the pass keeps no per-local list table), so `for` over a list-typed
local/param CANNOT lower without guessing a length — the new real spanned
LowerError::ForIterable names it and its future slice (the list-length
materialization slice). Same error covers runtime-endpoint ranges (no
static trip count for bounded-everything to price), wide (String/record)
elements, and non-binder/wildcard patterns. Never a guessed length.

Corpus stone + gate: tests/corpus/for_loop — `for i in 0..5` sums 10
(exclusive end), `for x in [10, 11, 11]` adds 32 through strided loads;
memory-held accumulation (no `let mut`), exit 42, freestanding — gated by
for_loop_run.rs, registered in RUNTIME_TESTED. Pass goldens
(lowering_loops.rs, now 4): the for-range CFG shape (back-edge, `i < 5`
head, the binder Let each iteration, NO guard/Trap — and it lowers
UNARMED, needing no Bounds fn) + the dynamic-iterable honest error.
Structural R4 splits: the shared emit_loop_head (`for`'s two shapes) +
emit_loop_back_edge (shared with while).

Docs, same commit: plan.md row 14a item (1) marked COMPLETE + the new §4
dynamic-length-list residue bullet (named future slices); lowering.md LC9
row annotated (for landed; the higher-order list-ops half remains LC9's,
behind LC8); current-state.md §3 (`for` RUNS, the honest iterable scope).

Gates (from src/rust): cargo fmt --check clean; cargo clippy --all-targets
-- -D warnings 0 warnings; cargo test --workspace exit 0 (architecture
R1-R4 incl. the corpus-coverage check, conformance, all container e2e
incl. for_loop_run); cargo build --workspace clean. 0 warnings, 0 errors.
Live: for_loop exit 42; helloWorld unchanged.
The 'Anything a slice cannot lower…' bullet lost its first line in one of
today's concurrent §4 edits — restored. New residue: each raising stdlib
unit currently carries its own trait Error copy until cross-module trait
references lower; counted so it can't hide.
Plan row 14a item (2): "plausibly a gate, not a feature" — and so it was.
FS5.2 built rec<N>'s parse / cycle-check (E0901/E0902) / stack-frame
sizing; this checkpoint proves a recursive fn EXECUTES: the S1 user-fn
discovery is cycle-safe on the self-call (a recursive callee is discovered
once, never looping the fixpoint), and the compiled double self-call is an
ordinary Cranelift call to the fn's own symbol. NOTHING in the compiler
changed — the checkpoint is the stone + gate + doc reconciliation.

New corpus stone tests/corpus/fib_runs (the lean freestanding `-> Int`
shape, NOT the Log-effect fibonacci app): `fn rec<10> fib(n: Int) -> Int`
— the classic two-branch match + double self-call — with main exiting
fib(10) = 55. Observed: exit 55, 5 000-byte freestanding ELF, empty
NEEDED, nm -u empty, no libc/ck_rt symbols. Gate fib_runs_run.rs;
registered in RUNTIME_TESTED.

Honest scope (declared, not overclaimed — the stone's comments say so):
the rec<10> depth stays COMPILE-TIME machinery only (sizing + the E0901
family); runtime depth-overflow enforcement is FS9's deferred
StackOverflow raise — the while<N>-enforces-vs-rec<N>-doesn't asymmetry
declared in the FS5.2 row / arenas.md.

Docs, same commit: plan.md row 14a item (2) ✅ LANDED; stdlib.md FS5.2 row
annotated (execution now gated; the "still unbuilt" note retired);
current-state.md §3 (recursion EXECUTES).

Gates (from src/rust): cargo fmt --check clean; cargo clippy --all-targets
-- -D warnings 0 warnings; cargo test --workspace exit 0 (architecture,
conformance, container e2e incl. fib_runs_run); cargo build clean.
0 warnings, 0 errors.
Plan row 14a item (3), the FS4 pattern: the turbofish→width map learns the
byte. Spelled U8 (the U16 precedent — capitalized primitives are the
language's spelling; the design docs' lowercase `u8` was shorthand):
Primitive::U8 joins the enum's documented-provisional fixed-width set +
"U8" in primitive_from_name + scalar_width 1 + render — NO new inference,
no widening, plan §5 held (the closed-set extension U16 already modeled).

The seam end to end: driver scalar_kind_of maps Ty U8 → the new
ScalarKind::Byte (→ ValueRepr::Byte, the LC4 repr); codegen's machine-op
arms grow the width-aware pair — load::<U8> emits `uload8` (reads 8 bits,
ZERO-EXTENDS into the i64 word, so the byte composes with scalar
arithmetic — never a smeared 8-byte read), store::<U8> emits `istore8`
(truncates the word to its low byte, writes exactly one byte, neighbors
untouched); size_of::<U8> = 1 and handle_at::<U8>'s raw byte-offset add
fall out of the existing repr law.

Two honest findings, both resolved within the locks:
- a bare Int literal in U8 position is E0702 (no numeric widening —
  manifest typing working as designed); the universal pin is the spelling:
  `42::<U8>` (synthesis-only, the turbofish IS the pin).
- the pass's literal reader (read_int) choked on a pinned literal — the
  HIR Lit span covers `42::<U8>` — fixed: the VALUE is everything before
  the `::` (the pin was consumed at check time).

Corpus stone tests/corpus/u8_roundtrip — each width PROVEN, not assumed
(exit codes truncate to 8 bits, so exit arithmetic alone cannot see a
word-smear; every check is a full-width `==` in a match): pre-fill a word
511 (FF 01 …), istore8 42 over byte 0 — `word == 298` proves the ONE-BYTE
store (a smeared store would read 42); `load::<U8>(0) == 42` proves the
byte load (a smeared load would read 298); the neighbor byte survives
(`== 1`); 40 + size_of::<U8>() twice = exit 42 freestanding. Gate
u8_roundtrip_run.rs; registered in RUNTIME_TESTED.

Docs, same commit: stdlib.md §2 machine-instruction-leaf bullet grew the
width tier (Int/I64 word; U8 byte with the uload8/istore8 semantics);
plan.md row 14a item (3) ✅ LANDED; current-state.md §3; Memory.ckh's
prose spelling fixed `::<u8>` → `::<U8>`. R3 held by moving
primitive_name into check_util beside the other render helpers.

Gates (from src/rust): cargo fmt --check clean; cargo clippy --all-targets
-- -D warnings 0 warnings; cargo test --workspace exit 0 (architecture,
conformance, container e2e incl. u8_roundtrip_run); cargo build clean.
0 warnings, 0 errors. helloWorld unchanged.
The first half of plan row 14a item (4): the byte-operations unit the row's
primitives were built FOR, promoted into the live stdlib.

Bytes::copy(src, dst, at, len) is 100% CkLang over the row's own work — a
while<65536> (MA1.2's ENFORCED bound; the ceiling is one region's capacity,
the bounded-everything choice — a longer copy RAISES
IterationBound::Exceeded, never spins; documented in .ck and .ckh) over a
memory-held cursor (no `let mut` exists; the counter lives in a scratch
region acquired/released per call — the honest two-syscall bootstrap cost,
also documented), one Memory::{load,store}::<U8> per pass (MA1.4's width;
runtime-endpoint ranges don't lower — the MA1.2b scope — so while<N> is
the loop).

Plus the TWO String ABI-word selection leaves the Meta.ck rewrite needs —
`of_str(s) -> Region` (word 0, the content base) and `str_len(s) -> Int`
(word 1) — registered `= intrinsic` in the closed floor registry (the
`size_of` tier: pure word selection, NO instruction at all — codegen's new
str_abi_word arm just returns one of the String argument's two ABI words).
Declared explicitly as floor-ISA tier members, never a hidden pass form.
No speculative API: copy + the two leaves are exactly what MA1.5b consumes.

Corpus stone tests/corpus/bytes_roundtrip → exit 42 freestanding: region-
to-region copy with placement AND the stop-at-len proof (the third source
byte must NOT arrive), then a runtime-length String-to-region copy through
of_str/str_len ('c'=99/'k'=107 verified by full-width ==) — exactly the
append_str rewrite shape. Gate bytes_roundtrip_run.rs; RUNTIME_TESTED.

Housekeeping (the flagged R3 chronic): corpus.rs properly SPLIT — the
RUNTIME_TESTED registry + its runtime_coverage_is_exhaustive gate moved to
the new tests/runtime_registry.rs (534 + 324 lines; the registry grows with
every stone and is a separate responsibility from fixture reconciliation);
no more comment-whittling. R4 held by extracting codegen's str_abi_word.

Docs: stdlib.md §8 gained the MA1.5 Bytes row. (The floor-ISA pinned table,
the Meta.ck rewrite, and the row-14a completion land in MA1.5b.)

Gates (from src/rust): cargo fmt --check clean; cargo clippy --all-targets
-- -D warnings 0 warnings; cargo test --workspace exit 0 (architecture,
conformance, container e2e incl. bytes_roundtrip_run + the relocated
coverage gate); cargo build clean. 0 warnings, 0 errors.
dferreira manually merged commit 57ab936545 into master 2026-03-05 16:10:10 +01: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!52
No description provided.