decisions(memory): loop arena sizing ×bound now, escape-analysis slot reuse deferred post-MA4 (counter) #51

Manually merged
hkowalski merged 6 commits from feat/051-decisions-memory-loop-arena-sizing-bound-now-e into master 2026-03-05 08:35:58 +01:00
Owner

decisions(memory): loop arena sizing ×bound now, escape-analysis slot reuse deferred post-MA4 (counter)

Along the way:

  • MA1.0: Sys.ck widening — the close(3) leaf; Meta.ck/Test.ck close their per-emit fd
  • plan: testing phase scheduled post-counter (owner) — run-path assert sink, Assert::guard, E0503/raw-span error quality
  • plan: narrow the test-path codegen failure to entry-fn calls; E0301 cross-module suggestion joins the post-counter backlog
  • MA1.1: for + while front-end — mandatory loop bounds, no unbounded loop
  • MA1.1b: while re-homed as a block-bodied STATEMENT — uniform loop terminators

Touches 47 files: current-state.md, docs/impl/03-parser.md, docs/impl/08-diagnostics.md, docs/impl/memory-model/arenas.md, docs/syntax.md, plan.md and 41 more.

decisions(memory): loop arena sizing ×bound now, escape-analysis slot reuse deferred post-MA4 (counter) Along the way: - MA1.0: Sys.ck widening — the close(3) leaf; Meta.ck/Test.ck close their per-emit fd - plan: testing phase scheduled post-counter (owner) — run-path assert sink, Assert::guard, E0503/raw-span error quality - plan: narrow the test-path codegen failure to entry-fn calls; E0301 cross-module suggestion joins the post-counter backlog - MA1.1: for + while<N> front-end — mandatory loop bounds, no unbounded loop - MA1.1b: while<N> re-homed as a block-bodied STATEMENT — uniform loop terminators Touches 47 files: `current-state.md`, `docs/impl/03-parser.md`, `docs/impl/08-diagnostics.md`, `docs/impl/memory-model/arenas.md`, `docs/syntax.md`, `plan.md` and 41 more.
Author
Owner

Split. The decision in lower_for is a pure function now, with its own tests in src/rust/crates/ck_resolve/src/lower/loops_lower.rs.

Split. The decision in `lower_for` is a pure function now, with its own tests in `src/rust/crates/ck_resolve/src/lower/loops_lower.rs`.
Author
Owner

Nothing outside calls iterable. It is pub(crate) in src/rust/crates/ck_syntax/src/loops.rs now.

Nothing outside calls `iterable`. It is pub(crate) in `src/rust/crates/ck_syntax/src/loops.rs` now.
Author
Owner

Done, and I added the test for src/rust/crates/ck_resolve/src/lower/loops_lower.rs that would have caught it.

Done, and I added the test for `src/rust/crates/ck_resolve/src/lower/loops_lower.rs` that would have caught it.
hkowalski requested changes 2026-03-05 08:00:00 +01:00
Dismissed
hkowalski left a comment

Mostly there, but the question on src/rust/crates/ck_resolve/src/lower/loops_lower.rs needs an answer before this lands.

Mostly there, but the question on `src/rust/crates/ck_resolve/src/lower/loops_lower.rs` needs an answer before this lands.
@ -0,0 +59,4 @@
/// iterable lowers before it in source order (it is evaluated outside
/// the binder's scope — the resolver's N3 walk already scoped it that
/// way). Missing pieces (parser recovery) keep spanned placeholders.
pub(super) fn lower_for(&mut self, node: &SyntaxNode, span: Span) -> ExprId {
Owner

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

`lower_for` 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 +110,4 @@
/// The iterable expression (the node after `in`, resolved before the
/// binder is introduced).
#[must_use]
pub fn iterable(&self) -> Option<&'a SyntaxNode> {
Owner

iterable is public. Does anything outside the crate call it, or is it pub because that was easier? Every public item is a promise.

`iterable` is public. Does anything outside the crate call it, or is it pub because that was easier? Every public item is a promise.
hkowalski approved these changes 2026-03-05 08:00:00 +01:00
hkowalski left a comment

Good. The second pass on src/rust/crates/ck_resolve/src/lower/loops_lower.rs is what I hoped for.

Good. The second pass on `src/rust/crates/ck_resolve/src/lower/loops_lower.rs` is what I hoped for.
Owner 2026-07-03: bump cursor stays the degenerate/bootstrap case, not the
model (arenas.md already law); loops are memory-safe and priced like a fn —
frame slots reuse ×1 by construction, arena-allocation sites price × the
declared bound (while<N> → N, for → the iterable's bound). Per-iteration
arena-slot reuse needs escape analysis, scheduled AFTER plan row 14d
(counter). Recorded in plan.md §4 honesty ledger + arenas.md open list.
The FS7b "per-emit fd unclosed" residue is CLOSED. `Sys::close(fd: Int) -> Int`
is a new syscall leaf declared in the live stdlib (Sys.ckh doc + Sys.ck
`= intrinsic`, mirroring openat), registered in the ck_resolve intrinsic
registry and the per-OS syscall table (ck_codegen::resolution — Linux x86-64
close(3), pass-through marshalling). Every per-emit openat site in the live
stdlib now Sys::close's its fd after its writes: Meta.ck's eight emit fns
(spawned_root/spawned/despawned/arena_high_water/arena_reclaimed/test_event/
assert_evaluated/crashed_at) and Test.ck::emit_failed. The stream BYTES are
unchanged (close writes nothing, O_APPEND ordering unaffected) — the
checked-in goldens pass as-is, unregenerated.

Docs in the same commit: stdlib.md §3 table grew the close 3 row (+ the §6.4
syscall-set decision and the §8/§9.7 residue mentions annotated closed);
plan.md §4's "per-emit fd unclosed" bullet marked CLOSED (2026-07-03, MA1.0)
and §1's seam list extended; the FS7b residue mentions in plan.md §2/§3 and
current-state.md annotated; Meta.ck's FD DESIGN header and Meta.ckh's prose
rewritten to the closed state.

Gates (from src/rust): cargo fmt --check clean; cargo clippy --all-targets
-- -D warnings 0 warnings; cargo test --workspace exit 0 (all suites ok,
incl. the container e2e corpus — fs8_default_run's helloWorld golden
shape-diff and freestanding_test_run unchanged); cargo build --workspace
clean. 0 warnings, 0 errors. ckc run tests/corpus/helloWorld → hello, world,
exit 0, 10 000-byte freestanding ELF, nm -u empty, the full 5-event
108-byte meta stream; the close calls verified in the disassembly
(ck_syscall nr 3 at each emit site).
Seed backlog from 2026-07-03 hand-experiments on match_int; parked until
row 14d (counter) is met, per owner: keep the MA ladder rolling.
match_int keeps the owner's multi-file experiment (ShouldCompileAndRun.ck),
fixed to call its local notMain() so the corpus stays front-end clean —
run exit 42 and 'test testNotMain ... ok' both verified. Multi-file
projects confirmed working (6 shapes, both compilers).
The loop front-end of plan.md row 14a item (1), design owner-locked in
decisions-scheduler-model.md "Loops": the language gets `for` + `while<N>`,
and NO unbounded loop exists — indefinite execution is a scheduled actor,
never a spin. Front-end ONLY; the CFG back-edge lowering is MA1.2.

Parser: `while<N>` bound = mandatory grammar (`WhileExpr ::= while WhileBound
Expr Block`, `WhileBound ::= < IntLit >`), a `WhileBound` CST node mirroring
rec<N>'s RecBound shape and diagnostics — bare `while` = hard E0123 (the
policy-carrying error; recovery still parses condition + body), E0124 missing
bound literal, E0125 missing `>`. The bound slot opens only DIRECTLY after
the keyword, so a comparison condition never collides. `for` already parsed
(slice 16); ranges already parse as iterables (slice 23).

CST wrappers (ck_syntax::loops, split per R3): WhileExpr (bound reader = the
recursion_depth shape), RangeExpr (start/end/inclusive), ForStmt moved in.

HIR: ExprKind::{While, For, Range} — the While bound N is CARRIED (MA1.2's
sizing multiplier + back-edge consume it; no runtime check built), the For
binder is the shared Pattern language primitive, a Range is deliberately NOT
a first-class value. Resolver lowering (ck_resolve lower/loops_lower.rs,
split per R3): a statement-position `for` lowers as a statement, never the
block tail — previously it was silently tail-overwritten as Unsupported.

Typecheck (ck_types::loops, synthesis-only per plan §5 — no metavariables,
no solving): `while` condition must be Bool (E0732, fail-soft on Unknown);
`for` iterable = a bounded List<T,N> (element type flows to the binder) or
an integer range with manifest Int endpoints (E0734), anything else = E0733;
a range outside the for-iterable slot = E0735; the for binder rides the
`let` rules — Binder/Wildcard direct, destructures through check_let_pattern
(irrefutability E0731, binder types flow from the element type).

Honest lowering gap: a loop reaching ck_lower is a real spanned
LowerError::LoopUnsupported (verified end-to-end: ckc run exits 1 with the
spanned error) — never a silently-dropped statement.

Docs, same commit: docs/syntax.md §Loops rewritten to the owner decision
(the "no while in v0" rule superseded, dated); docs/impl/03-parser.md
slice-22 grammar + E0120–E0125 table rows (E0120–E0122 were missing — FS5.2
doc drift fixed) + codes prose; docs/impl/08-diagnostics.md rows E0732–E0735
+ the E0731 for-binder note; plan.md row 14a marked front-end LANDED;
current-state.md §3 updated.

Tests: 7 new parser unit tests (bound node, AST reader, E0123 x2, E0124,
E0125), existing while tests re-bounded; 3 resolver lowering tests (bound
carried, for-as-statement, range node); 9 new type goldens + 1 resolve
golden (clean while/for/range/destructure; E0731/E0732/E0733/E0734/E0735),
all trap-covered — the untrapped-diagnostics ratchet cap is UNCHANGED.
R3/R4 held by splitting (ck_syntax/loops.rs, ck_resolve/loops_lower.rs,
ck_types/loops.rs) + one justified R4 allowlist entry (LowerError's Display:
one arm per variant by design). tests/lex/program_effects.cktest: the bless
run canonicalized its trap-section position; content unchanged.

Gates (from src/rust): cargo fmt --check clean; cargo clippy --all-targets
-- -D warnings 0 warnings; cargo test --workspace exit 0 (incl. conformance
+ architecture + the container e2e corpus); cargo build --workspace clean.
0 warnings, 0 errors. ckc run tests/corpus/helloWorld unchanged: hello,
world, exit 0, 10 000-byte freestanding ELF.
Closes the MA1.1 right-path gap the coordinator's probes found: a
statement-position `while<8> c { … }` followed by more statements or a tail
expression failed to parse (E0036 cascade), because `while` was still
slice-22's value-producing PRIMARY (needed `;` as an ExprStmt) while `for`
was a block-bodied statement. That contradicted docs/syntax.md §Loops' own
examples and the uniform-rules lock — the two loop primitives must
terminate identically.

Parser: `while` moved from `parse_primary` to `try_parse_stmt_keyword`
beside `for` — `WhileStmt ::= while WhileBound Expr Block`, the closing `}`
is the statement terminator, no `;`. The slice-22 expression production is
RETIRED (a `while` as a let-RHS no longer parses; nothing is lost — the
loop was always Unit-valued, and a block ending in a while statement
returns Unit exactly as the old tail reading did). SyntaxKind::WhileExpr →
WhileStmt across lexer/parser/syntax/resolve (file while_expr.rs →
while_stmt.rs; ck_syntax wrapper WhileExpr → WhileStmt). E0123 recovery is
now ONE diagnostic — the statement terminator ends the loop, so the bare
`while 1 == 2 { let x = 1; }` probe reports E0123 alone, no cascading
E0036 (pinned by test).

Resolver: lower_block routes ForStmt | WhileStmt through one
`lower_loop_stmt` helper (loops_lower.rs) — an expression STATEMENT, never
the block's tail. HIR/typecheck/ck_lower unchanged (ExprKind::While was
already statement-agnostic; the LoopUnsupported honesty gate holds).

Tests: while_stmt.rs rewritten to the statement grammar — statement
position followed by another statement AND by a tail expression (the exact
probe shapes), expression position rejected, E0123-alone recovery shape,
E0123/E0124/E0125, bound node + AST reader, condition Pratt composition,
byte-completeness; a for-mid-block parser test pins the `for` twin. New
golden tests/types/while_statement_position.cktest (the probe source,
blessed clean); the resolver while-lowering test moved to statement
position + tail assertion. Conformance ratchet cap unchanged.

Docs, same commit: docs/syntax.md Stmt grammar grew WhileStmt ("Seven
shapes, one terminator: every statement except CatchStmt, ForStmt, and
WhileStmt ends with `;`"), §Loops states both loops are block-bodied
statements; docs/impl/03-parser.md slice-22 section rewritten (WhileExpr →
WhileStmt, the retirement + re-home dated), codes prose + table rows
updated (parse_while_stmt), lib.rs stable-code range corrected
E0009..E0125; plan.md row 14a + current-state.md §3 note MA1.1b.

Gates (from src/rust): cargo fmt --check clean; cargo clippy --all-targets
-- -D warnings 0 warnings; cargo test --workspace exit 0 (conformance +
architecture R3/R4 + container e2e green; ck_resolve lower/mod.rs back
under the cap via the lower_loop_stmt consolidation). 0 warnings,
0 errors. Live: probe 1 (`while<8> 1 == 2 { let x = 1; } 42`) checks
clean; probe 2 (bare while) = exactly one E0123; helloWorld unchanged
(hello, world, exit 0).
hkowalski manually merged commit 65ba67c4d6 into master 2026-03-05 08:35:58 +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!51
No description provided.