R2c-fe fix: list-in-let irrefutable iff pure [..rest] (soundness) #46

Manually merged
dferreira merged 7 commits from feat/046-r2c-fe-fix-list-in-let-irrefutable-iff-pure-re into master 2026-02-26 14:21:10 +01:00
Owner

R2c-fe fix: list-in-let irrefutable iff pure [..rest] (soundness)

Along the way:

  • R2c-cg: runtime destructuring — record destructure + [..rest] (destructure primitive)
  • runtime-testing: T9 — bounded-arena stress & reconcile (blocked slice)
  • R2d: bare-variant match arms + when guards (R2 control-flow layer complete)
  • docs(stdlib): §9.6 Session 2 close-out — R2 wall down, next session starts at R2e
  • docs: snake corpus stepping-stone ladder + Floor-core lowering plan
  • docs: reconcile lowering.md / stdlib.md — disjoint scopes, close the four holes

Touches 45 files: current-state.md, docs/impl/runtime-testing/README.md, docs/impl/runtime-testing/T7-fuzz-validator.md, docs/impl/runtime-testing/T9-bounded-arena-stress-reconcile.md, docs/impl/runtime/README.md, docs/impl/type-system/TY7-exhaustive-match-and-narrowing.md and 39 more.

R2c-fe fix: list-in-let irrefutable iff pure [..rest] (soundness) Along the way: - R2c-cg: runtime destructuring — record destructure + [..rest] (destructure primitive) - runtime-testing: T9 — bounded-arena stress & reconcile (blocked slice) - R2d: bare-variant match arms + when guards (R2 control-flow layer complete) - docs(stdlib): §9.6 Session 2 close-out — R2 wall down, next session starts at R2e - docs: snake corpus stepping-stone ladder + Floor-core lowering plan - docs: reconcile lowering.md / stdlib.md — disjoint scopes, close the four holes Touches 45 files: `current-state.md`, `docs/impl/runtime-testing/README.md`, `docs/impl/runtime-testing/T7-fuzz-validator.md`, `docs/impl/runtime-testing/T9-bounded-arena-stress-reconcile.md`, `docs/impl/runtime/README.md`, `docs/impl/type-system/TY7-exhaustive-match-and-narrowing.md` and 39 more.
Author
Owner

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

Split. The decision in `new` is a pure function now, with its own tests in `src/rust/crates/ck_driver/tests/match_guard_run.rs`.
dferreira approved these changes 2026-02-26 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/match_guard_run.rs included.

Happy with this. I ran the suite against the branch and it is green, `src/rust/crates/ck_driver/tests/match_guard_run.rs` included.
@ -0,0 +146,4 @@
}
impl Teardown {
fn new(name: &str) -> Self {
Owner

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

`new` reads, decides and writes in one function. The deciding part is the piece I would want to test on its own. Worth splitting?
Corrects the unsound list-irrefutability rule from 63f7644. Under bounded-max
semantics a List<T,N> value has dynamic length 0..=N, so a fixed-arity
`let [a,b] = xs` (misses every length != 2) and a non-empty-prefix
`let [head, ..rest] = xs` (misses the empty list) are REFUTABLE — only a pure
`[..rest]` (match_list ListCover::Open(0), matches all lengths incl. 0) is
irrefutable. list_refutable_reason now decides off match_list::list_cover (made
pub(crate)) — the single source of list coverage, no forked logic; Exact(k) and
Open(k>0) are E0731 with reasons naming the missed lengths. Records path
(single_arm_covers_all) untouched; ck_lower/codegen unchanged (front-end only).

Goldens flipped to the sound behavior: let_destructure_exact_arity clean->E0731;
let_destructure_list_binds rewritten to a pure [..rest] (the one clean list let);
let_refutable_list_arity reason updated; new let_refutable_list_prefix. Ratchet
held at 112 untrapped. Deleted the dead has_rest/list_size_bound helpers.

Gates: fmt/clippy(-D warnings)/build/test --workspace all exit 0; ck_types 220,
conformance 2. 6 let_pattern #[test]s cover the sound rule exactly.
Completes R2c: an irrefutable destructuring `let` now RUNS. Floor IR
StmtKind::Let carries a lowered LetBinding = Simple(Option<LocalId>) |
Record(Vec<FieldBind{field_name,local}>) | RestList(LocalId) — the HIR Pattern is
reduced to exactly what codegen reads, never carried whole. codegen::binding
emit_let_record projects each field of an inline-record RHS (P2.2 base pointer +
plan_fields) into its bound local via the new reusable
record::project_field helper (R2d reuses it for variant-payload destructure);
emit_let_rest aliases the whole list handle for a pure [..rest].

Field-name matching: the HIR record pattern drops field names, so the driver
threads the resolver's per-LocalId binder names into lower_body_with; a pun
`{x, y}` binds by name (order-independent, matched in the field plan), a rename
`{x: a}` / nested / non-inline-RHS falls to Simple(None) => Unsupported (never a
guessed offset). No front-end change. Records-only + [..rest]; nested / list-elem
/ param-record RHS stay real Unsupported.

Gates: fmt/clippy(-D warnings)/build/test --workspace all exit 0; codegen.rs at
809/809 (not over). Gates: tests/corpus/let_destructure (let Point{x,y} =
Point{x:20,y:22}; x+y -> exit 42) + let_destructure_wide (offset past a wide
field -> 42), both freestanding in RUNTIME_TESTED; 4 ck_lower lowering tests
(incl. [..rest] + the rename->Unsupported guard). [..rest] runtime fixture
deferred (freestanding list-alloc), gated at lowering.
Records the "ultimate integration test" as a first-class blocked T-slice:
build an actor around its declared bounds, drive a saturating workload, and
gate that the compiler's static arena reservation is a true and tight upper
bound on what the running actor does. Three properties, each a distinct gate:

  1. soundness   — no OOM within bounds (measured ArenaHighWater <= calculated)
  2. tightness   — no wasted headroom under saturation (measured ~= calculated
                   per region; the "underflow" half — a rec<N> never observed at
                   depth N is a bound set too loose)
  3. enforcement — over-bound fires the designed Panic::raise (StackOverflow /
                   OOM-by-design), never memory unsafety

Today arena sizing is only tested against itself (77 static arena_size unit
tests; arena_reclaim_run / mem_report_run explicitly do NOT gate
calculated >= measured, stdlib.md §9.4/9.5). T9 turns that documented-but-
ungated invariant into a machine-verified gate.

Load-bearing design note captured before R2: the workload is the hard part
(must saturate, and saturation must be verified off the stream), not the
assertion. Blocked on R2 (typed bounded allocation — no runnable corpus actor
sizes finite-nonzero until then) + T5 (memory inspection). Interim floor
buildable now: gate measured <= calculated on every is_bounded() actor.

Registers T9 in the README slice cascade + corpus targets; updates T7's
stale "fifth generator not yet a slice" cross-reference. Link checker green.
Extends R2b's scalar match with (1) bare-variant arms — an arm matches by
comparing the scrutinee's P2.3 tag to the arm's variant tag; the head TypeId is
read off the CST by a new ck_types::read_variant_pattern_head and threaded as a
span->tag MatchArmTags map into codegen (the FS4 MemTypes precedent; ck_lower ↛
ck_resolve held) — and (2) when guards — MatchArm gained guard: Option<ExprId>;
a guarded arm inserts a guard block (brif guard_ok -> body, else -> next_test),
and a guarded catch-all no longer ends the dispatch chain so it can fall
through. Bare-variant param passes as one i64 tag word (abi_types Ty::Union).

Payload-variant arms (Some(x)) stay Unsupported — payload-variant layout is
unbuilt; that's the compositional R2e (tagged record: P2.2 store + P2.3 tag,
destructure via tag-check + R2c-cg project_field). Breadcrumb left.

Refactor (no cap raised): threading MatchArmTags pushed codegen.rs past the R3
809 cap, so the actor-emit block moved to a new codegen::actor submodule
(codegen.rs 581, actor.rs 297, match_expr.rs 232); three fns extracted for the
R4 80-line cap. Actor path verified unchanged (actor_lifecycle/greeter_concat/
arena_reclaim green).

Gates: fmt/clippy(-D warnings)/build/test --workspace exit 0; arch R1-R4 green.
Gates: tests/corpus/match_variant (pick(Color::Green) -> 42) + match_guard
(classify(20)=1 guard-true + classify(5)=42 guard-false-fallthrough -> 43), both
in RUNTIME_TESTED.
Records the 2026-07-02 session: FS5-mem + R2a→R2d landed (7 commits), the R2
control-flow/binding layer is complete (let, value-position match on scalars +
bare variants, when guards, irrefutable destructuring of records + [..rest],
comparisons — all freestanding, exit-code gated). Locks the four design
decisions reached this session (destructuring is a language primitive;
list-in-let irrefutable iff [..rest]; when stays match-only; variants ARE
records so payload-variant is a composition, not a new primitive). Lays the
next-session runway: R2e (payload variants = tagged record: construct via
P2.2+P2.3, destructure via tag-check + project_field — verified compositional),
then the FS5-Arena→FS8 tail with with/generics/Panic/behavior-call as its now-
explicit predecessors.
snake.md — the ordered corpus stepping stones from "runs today" to the
North-Star snake server, grounded in the self-hosting invariant (Rust does
codegen + the irreducible syscall floor; mailbox/scheduler/supervisor/arena/
meta/wire are CkLang). Rungs: pure-value → CkLang runtime substrate (message
passing, send-is-serialize) → real I/O → snake. One-shot boundary =
snake-against-mocks.

lowering.md — the missing lowering tier. States the exact problem: the Floor IR
is a near-identity of the HIR and the real desugaring lives scattered across 14
codegen modules, so lambdas can't lower, FS7/FS8 hit the R2 wall, and it's
stdlib_is_the_interface at IR scale. Fix: redefine Floor IR as a primitive
Floor-core, move desugaring into the HIR->Floor pass, closure-convert lambdas,
and split codegen out of ck_lower into a new ck_codegen crate (LC0) so the
boundary enforces the refactor. Slices LC0-LC7.
Draw a hard scope divider and remove the contradiction between the two docs:

  lowering.md — owns lowering, the Floor IR, and codegen (how high-level CkLang
    desugars to primitives, the ck_lower/ck_codegen crate split, closure
    conversion). Payload-variant lowering (the old "R2e") now lives here as LC5.
  stdlib.md — owns the lego blocks (syscall table + CkLang stdlib units, and when
    each is promoted). It no longer specifies HOW any construct lowers.

Bakes in the review's four holes:
  #1 stdlib.md §9.6 no longer instructs a codegen arm for R2e; it defers to
     lowering.md LC5 and refocuses on the FS floor units + syscall additions,
     with a table mapping each lego-block dep to its LC slice.
  #2 typed Floor IR on the run path is now the explicit LC1 PRECONDITION — every
     run/test/actor site lowers with TypeckResults::default() today, so a
     desugaring would re-fake layout via CST-span re-reading; LC1 threads real
     types and retires the driver sideways-maps for real.
  #3 adds LC6 monomorphization (generics) and flags data-constructor typing
     (E0704) as a front-end prereq of LC5 — neither was in scope before.
  #4 makes the "bottom is CkLang only where LC ∧ FS8 meet at Arena::alloc"
     caveat explicit (Alloc = interim ck_rt_alloc until FS8).

Cascade renumbered LC0–LC9.
dferreira manually merged commit b0315143b5 into master 2026-02-26 14:21: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!46
No description provided.