feat(parser): the §1b letter-form coercion attaches to any postfix expression — (e)::T lands, debts #56/#59 die #75

Manually merged
tvidal merged 6 commits from feat/075-feat-parser-the-1b-letter-form-coercion-attach into master 2026-04-07 11:02:34 +02:00
Owner

feat(parser): the §1b letter-form coercion attaches to any postfix expression — (e)::T lands, debts #56/#59 die

Along the way:

  • fix(codegen): per-stamp mem widths + Convert emission — generic load/store:: and ireduce/sextend/uextend lower
  • feat(abi): return-area convention for >2-word record returns — the word-list ABI loses its ceiling; Arena API live
  • kill(probes): the four surviving name probes die — String/EmptyString lift fallbacks, the kernel/state Region text-matches
  • fix(coherence): candidate selection keys the full operand tuple — the silent u32+i32 hole closes (debt #62)
  • fix(test-path): test fns stamp generic calls like any fn — the divergent test path narrows

Touches 60 files: handoff.md, src/rust/crates/ck_codegen/src/actor.rs, src/rust/crates/ck_codegen/src/call.rs, src/rust/crates/ck_codegen/src/format.rs, src/rust/crates/ck_codegen/src/freestanding.rs, src/rust/crates/ck_codegen/src/lib.rs and 54 more.

feat(parser): the §1b letter-form coercion attaches to any postfix expression — (e)::T lands, debts #56/#59 die Along the way: - fix(codegen): per-stamp mem widths + Convert emission — generic load/store::<T> and ireduce/sextend/uextend lower - feat(abi): return-area convention for >2-word record returns — the word-list ABI loses its ceiling; Arena API live - kill(probes): the four surviving name probes die — String/EmptyString lift fallbacks, the kernel/state Region text-matches - fix(coherence): candidate selection keys the full operand tuple — the silent u32+i32 hole closes (debt #62) - fix(test-path): test fns stamp generic calls like any fn — the divergent test path narrows Touches 60 files: `handoff.md`, `src/rust/crates/ck_codegen/src/actor.rs`, `src/rust/crates/ck_codegen/src/call.rs`, `src/rust/crates/ck_codegen/src/format.rs`, `src/rust/crates/ck_codegen/src/freestanding.rs`, `src/rust/crates/ck_codegen/src/lib.rs` and 54 more.
Author
Owner

It said what I assumed. Reworded in src/rust/crates/ck_driver/tests/arena_return_run.rs so it names the input that broke it, which is what the bug report will need.

It said what I assumed. Reworded in `src/rust/crates/ck_driver/tests/arena_return_run.rs` so it names the input that broke it, which is what the bug report will need.
Author
Owner

It runs per token. The buffer behind rets lives on the struct in src/rust/crates/ck_codegen/src/lib.rs now and is cleared rather than reallocated.

It runs per token. The buffer behind `rets` lives on the struct in `src/rust/crates/ck_codegen/src/lib.rs` now and is cleared rather than reallocated.
Author
Owner

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

Split. The decision in `new` is a pure function now, with its own tests in `src/rust/crates/ck_driver/tests/alloc_rw_test_fn_run.rs`.
tvidal approved these changes 2026-04-07 09:30:00 +02:00
tvidal left a comment

Reads well. One question on src/rust/crates/ck_driver/tests/arena_return_run.rs, nothing blocking.

Reads well. One question on `src/rust/crates/ck_driver/tests/arena_return_run.rs`, nothing blocking.
@ -442,0 +460,4 @@
}
fb.ins().return_(&[]);
} else {
let mut rets = Vec::new();
Owner

rets allocates a fresh buffer per call. If this runs per frame or per token, can the buffer be reused?

`rets` allocates a fresh buffer per call. If this runs per frame or per token, can the buffer be reused?
@ -0,0 +144,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?, at src/rust/crates/ck_driver/tests/alloc_rw_test_fn_run.rs:147.

`new` reads, decides and writes in one function. The deciding part is the piece I would want to test on its own. Worth splitting?, at src/rust/crates/ck_driver/tests/alloc_rw_test_fn_run.rs:147.
@ -0,0 +117,4 @@
.arg("-d")
.arg(binary)
.output()
.expect("spawn `readelf`");
Owner

The expect message on expect is the only thing a user sees if this fires. Does it say what they should do, or only what we assumed?, at src/rust/crates/ck_driver/tests/arena_return_run.rs:120.

The expect message on `expect` is the only thing a user sees if this fires. Does it say what they should do, or only what we assumed?, at src/rust/crates/ck_driver/tests/arena_return_run.rs:120.
mdoorn approved these changes 2026-04-07 09:32:03 +02:00
mdoorn left a comment

Read it too. No objection to src/rust/crates/ck_driver/tests/arena_return_run.rs.

Read it too. No objection to `src/rust/crates/ck_driver/tests/arena_return_run.rs`.
The postfix `::` coercion arm folds the letter-led <value>::Type surface
(plan_values_02.md §1b) onto any completed postfix receiver — (a + b)::usize,
f()::T, a.b::T, xs[i]::T — where before only literals (8::i32) and lists
([1,2,3]::List) carried a coercion inline and bare idents (x::u8) rode the path
surface. Parser wraps the receiver in a CoercionExpr; resolve lowers it to a new
hir::ExprKind::Coerce { value }; the checker types the node at the target via the
ONE §1.4 coercion law (coerce_scalar_into, now shared with the local-path
surface); the Floor pass rides the ONE coerce entry (a scalar ireduce/sextend, an
identity retype a no-op). No new floor leaf, no per-receiver arm, no by-name probe
— the value-vs-access split (§1b) needs no parser decision, since a wrapped
postfix receiver is always a value and a bare path consumes its own :: segments.

- The stale OWNED-DEBT comment (value_turbofish.rs) claiming the letter form is
  unimplemented language-wide is deleted — replaced by an accurate one-liner.
- todo_effect/trap.md ratcheted down 3 lines: the new :: postfix arm removes 3
  match-recovery cascade diagnostics (0 new).

Proof: 5 parser goldens (call/member/index/paren-arith accepted; Suit::Hearts
still a path), 1 types golden (coercion applies clean on postfix receivers), and
the coerce_postfix runtime stone ((a+b)::u8 narrow, (a+b)::usize into a strict
usize param, src()::u8) exit-42, registered.
Two codegen completeness holes, both closed via the ONE observation that
every consumer of the FS4 width channel (`load`, `fcvt_from_sint`, and now
the width-coercion leaves) needs the call's RESULT type — which the Floor IR
already carries SUBSTITUTED for a monomorphized stamp.

A) Generic-param `Floor::load::<T>` in a stamp body (e.g. `Alloc::read<T>`
   over the cross-module boundary) had no FS4 width entry: `collect_mem_types`
   read the turbofish `::<T>` off the CST, which resolves to the generic
   param, and the source span is SHARED across every monomorphization. Now the
   width is the call's substituted result type, and `MemTypes` is keyed by
   `(span, stamp)` — each stamp's `FnId` disambiguating the shared template
   span; codegen looks up with the active body's `FnId`, falling back to the
   plain `(span, None)` entry. `UnitMainBody` gained `active_fn`. The E0743
   multi-word refusal now checks the substituted Floor IR type (the value
   stored / loaded), so a substituted multi-word `T` is still refused.
   Armed the previously-`#[ignore]`d `alloc_rw_cross_module_run` gate (exit
   42) and repaired its stale fixture (`64::<usize>` → the coercion `64::usize`,
   now E0747).

B) `Format::Convert` (`ireduce`/`sextend`/`uextend`) was a standing
   `Unsupported`. It now reads its target width (`<From, To>`'s `To` = the
   leaf's result type) off the same width channel and emits the named opcode
   verbatim (1:1). Added the `floor_width_ops` corpus stone + runtime gate
   (`ireduce::<Int,u8>` narrow, `uextend`/`sextend` of the same 0xFF byte →
   255 vs -1; exit 42) and its `RUNTIME_TESTED` entry.

Gates: fmt/clippy/build clean; architecture 21/21; workspace tests green.
A by-value return wider than the SysV register-return budget (2 words) can no
longer ride back in registers — Cranelift's legalizer rejects >2 register
returns. Implement the return-area convention, selected strictly by flattened
word COUNT (shape), never a type name:

- when ret_flat_words(ret).len() > RETURN_REGISTER_BUDGET, the callee's signature
  gains a hidden leading pointer param (the caller-allocated result frame) and
  returns void; the ValueWords Return stores the flattened words through it.
- every call site (value / effect / words-consumption) allocates the result
  frame, passes its base as the hidden leading arg, and binds/reads it back —
  emit_return_area / repack_register_words in value.rs, one law for all three.

An ordinary explicit pointer param (not ArgumentPurpose::StructReturn) — no ABI
magic, fully under our control, matching how record locals already thread a base
pointer; correct on both the run and test emission paths.

1- and 2-word returns are byte-identical (ret_area is None → the existing
register path is untouched). Existing suite green: 1360 passed, 3 pre-existing
soft-assert canaries FAILED as designed.

Proofs:
- wide_return: a 5-word record returned by value, exit 42, freestanding + a
  RUNTIME_TESTED _run.rs gate.
- arena_return: Alloc::arena_over / Alloc::reset (the 3-word Arena) now cross by
  value through the return area — write/read through the slice, cursor reset,
  exit 42, freestanding + registered. alloc_roundtrip's stale "NOT exercised"
  comment updated.

Scope note: Alloc::alloc (a named-tuple (| handle, arena |) return) stays dead —
blocked by the SEPARATE named-tuple slice (named-tuple fields cannot be
projected: by name → E0718 at resolve, by position → codegen unsupported; its
nested-record body does not lower), and arena_over-direct use trips a separate
pre-existing bug in constructing a record field from a record variable
(`Arena { region: r, .. }` drops region.size — proven on the pristine base).
Both are unrelated to this return-area ABI; the stone works around them lawfully.

user_fn.rs held under its R3 file cap (494) by splitting its unit tests to
user_fn_tests.rs; mock_state.rs / freestanding.rs held at their caps by trimming
comments. Arch test 21/21, fmt + clippy -D warnings clean.
The nominal-roster landing made every by-name type probe redundant; this
slice deletes the four that survived and locks them out with an error-level
architecture gate.

A) ck_types/lift.rs — `well_known_value_type` (the `match head { "String" =>
   .., "EmptyString" => .. }` fallback) deleted whole, with both call sites.
   `lift_scope_head` already resolves String/EmptyString through the
   project-global nominal roster BEFORE this arm was reached, so the function
   was a dead duplicate. Verdict: unreachable in production; the one in-crate
   unit test that reached it hit only the `string_id() == None` branch (an
   undeclared `String` in a fresh interner), where it returned `None` — bit-
   identical to deletion (empirically confirmed: instrumented panic, full
   suite green save that one branch which yields `Ty::Unknown` either way).

B) ck_types/lift/impl_head.rs — the identical "String"/"EmptyString" fallback
   in `resolve_head_type_id` deleted; the fn now stops at scope + nominal_id.
   Verdict: fully unreachable — no test reached the arm (instrumented panic,
   never fired).

C) ck_driver/compile/kernel.rs — `span_text(..) == "Region"` (kernel wiring by
   raw source text) re-expressed as real type resolution: the field's type is
   lifted and matched against Floor::Region's canonical nominal id via
   `is_floor_region`. The dead `span_text` helper is gone; the fn now takes
   scope + interner. Behavior change (the fix): a `type MyRegion = Region`
   binding that resolves to that id is now honored — the string probe missed it.

D) ck_driver/compile/state.rs — `is_word_int_ty(..) || span_text(..) ==
   "Region"` (the state-field width probe whose own comment called it "a floor-
   drift tell kept deliberately" pending cross-module resolution — since
   LANDED) re-expressed the same way through the shared `is_floor_region`. The
   field's checked type answers by id, never source text.

Gate: values V6 — bans the `== "Name"` / `!= "Name"` probe shape for
String/EmptyString/Region across EVERY production crate (V3 covered only the
ck_lower/ck_codegen ABI path), whitespace-normalized so a forward roster lookup
(`symbol("Region")`) and diagnostic strings stay legal. Allow-lists the ONE
sanctioned N1 name→id binding file (ck_resolve/src/lib.rs, values §5) with a
stale-entry guard. Red-proofed.

Gates: fmt clean, clippy -D warnings clean, build ok, workspace 1366 passed /
3 expected canaries, architecture 22/22 (21 + V6). Production Rust net -27 LOC.
`resolve_method_call` selected candidate traits via `traits_for(recv_ty.id)`
— "does this trait have ANY row for the receiver's type" — and never gated on
the full operand-pair coherence key. With `Add<u32,u32,u32>` + `Add<i32,i32,i32>`
declared (no cross-sign impl), `u32 + i32` found exactly one candidate trait via
the receiver's `u32` row, built a candidate whose `impl_for_inputs([u32,i32])`
MISSED, but the miss fell soft to `impl_id = None` / `ret = Ty::Unknown` (and
`assignable(_, Unknown)` is permissive) instead of being rejected — so the call
typechecked with ZERO diagnostics.

Fix: `build_candidate` flags `concrete_miss` — a concrete nominal receiver whose
NON-EMPTY, fully-determined operand-tuple key finds no impl row — and
`resolve_method_call` drops such candidates (`.filter(|c| !c.concrete_miss)`).
The match set then empties and `select_candidate` yields `NotFound`, firing the
ordinary E0728 family that `"a" + "b"` already produces. Three exclusions keep
the fail-soft discipline intact: a symbolic `Var` receiver (no impl by design),
an under-determined key (an operand still `Unknown`/`Var` upstream — no doubled
diagnostic), and an empty projected key (a trait whose type param never reaches a
method operand position, e.g. `Countable<T>`, keyed by the fallback subject — the
receiver-fit E0702 owns that case).

Proof: negative golden `tests/types/operator_add_mixed_sign_no_impl.cktest`
(trapped E0728 on `u32 + i32`) + a positive control (`u32 + u32` clean). The
heterogeneous tower, const-param Concat/Equals, and where-bound paths are
unaffected (a real impl → `impl_for_inputs` hits → not a miss). No latent site
was leaning on the permissive fallback (whole suite green, ratchet holds at 94).

Gates: fmt clean; clippy 0; build clean; workspace 0 Rust-level failures (3
soft-assert canaries echo as expected); architecture 21/21; conformance 2/2,
ratchet 94.
A `test fn` calling a generic (`Alloc::write::<u8>` / `Alloc::read::<u8>`) died
in codegen ("unsupported Floor IR form" at `Alloc.ck`'s `Floor::load::<T>`): the
test entry body lowered with `GenericStamps::default()`, so the generic Alloc
template lowered without a monomorphization stamp. The SAME code under `fn main`
worked. A test binary is not a special binary — the fix rides the ONE fn-main
mechanism, not a parallel test-only variant.

Three unifications on the test entry body, each the exact builder `fn main` uses:

1. LC6 stamps — `lower_one_test` now runs `stamps_for_body` over the test HIR
   (as `fn_main::lower_main_floor` does), retargeting each generic callee to its
   minted stamp and seeding `collect_user_fns` with the pending stamps (was an
   empty `Vec`). `Alloc::write::<u8>` in a `test fn` stamps like any fn-main call.

2. Full entry-body fact set — `lower_test_floor` now builds the real variant
   facts (LC2/LC5), record/mixed-union ABI layouts (FC-a/FC-b), raise facts
   (LC10), and R2c binder names, not `*::default()`. A `Region` returned by
   `Alloc::os_acquire` or passed by value crosses the test body's call boundaries
   by the same law its callees repack under (this was the second wall the repro
   hit, after the stamp wall).

3. ONE shared `MonoTable` — threaded from `lower_all_tests` (lowering) through
   `testfloor::emit_freestanding_tests` (cross-module emit), so a `test fn`'s
   entry-minted cross-module stamp is lowered via `stamp_origin` under the table
   that minted it, exactly as the fn-main path threads one `mono` end to end
   (was a fresh table per stage that forgot the entry-minted stamps).

The honest-error arm in `user_fns::resolve_work` stays (still reachable via the
actor / mock / receive / supervisor entry paths that thread no stamps); its
message and the module docs drop "test" from the un-lowered list.

Proof: new corpus app `tests/corpus/alloc_rw_test_fn` — a `test fn` does
`os_acquire` + `write::<u8>` + `read::<u8>` + `Assert::equal`, gated by
`alloc_rw_test_fn_run.rs` under the `ckc test` path, registered in
`RUNTIME_TESTED`. The failing shape is green; the whole suite stays green.
tvidal manually merged commit 22e8e420ea into master 2026-04-07 11:02:34 +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!75
No description provided.