P2.2: records -- construction + field projection #40

Manually merged
mdoorn merged 5 commits from feat/040-p2-2-records-construction-field-projection into master 2026-02-19 08:30:00 +01:00
Owner

P2.2: records -- construction + field projection

Along the way:

  • toolchain: pin to 1.96 (match mise), fix the lint it surfaces
  • ckc check: front-end diagnostics over a project directory
  • corpus: redesign todo (actor-native) + add todo_effect
  • P2.3: bare-variant data types (construction + equality + record field)

Touches 39 files: current-state.md, docs/impl/lowering/00-lowering.md, docs/impl/lowering/01-cranelift-emit.md, src/rust/crates/ck_driver/src/compile/maps.rs, src/rust/crates/ck_driver/src/compile/mod.rs, src/rust/crates/ck_driver/src/compile/test_compile.rs and 33 more.

P2.2: records -- construction + field projection Along the way: - toolchain: pin to 1.96 (match mise), fix the lint it surfaces - ckc check: front-end diagnostics over a project directory - corpus: redesign todo (actor-native) + add todo_effect - P2.3: bare-variant data types (construction + equality + record field) Touches 39 files: `current-state.md`, `docs/impl/lowering/00-lowering.md`, `docs/impl/lowering/01-cranelift-emit.md`, `src/rust/crates/ck_driver/src/compile/maps.rs`, `src/rust/crates/ck_driver/src/compile/mod.rs`, `src/rust/crates/ck_driver/src/compile/test_compile.rs` and 33 more.
Author
Owner

It said what I assumed. Reworded in src/rust/crates/ck_driver/tests/variants_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/variants_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 values lives on the struct in src/rust/crates/ck_lower/src/codegen/record.rs now and is cleared rather than reallocated.

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

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

Split. The decision in `print_trap_block` is a pure function now, with its own tests in `src/rust/crates/ck_driver/src/lib.rs`.
mdoorn approved these changes 2026-02-19 08:25:00 +01:00
mdoorn left a comment

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

Happy with this. I ran the suite against the branch and it is green, `src/rust/crates/ck_driver/tests/variants_run.rs` included.
@ -285,0 +405,4 @@
/// Print one `trap.md` block: a `### <File>` heading and a ```` ```trap ```` fence
/// holding the lowered `E####: line, col` lines (empty fence = clean).
fn print_trap_block(file_name: &str, traps: &[ck_cktest::TrapLine]) {
Owner

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

`print_trap_block` 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 +120,4 @@
.arg("--stdlib")
.arg(&stdlib)
.status()
.expect("spawn the `ckc` binary")
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/variants_run.rs:123.

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/variants_run.rs:123.
@ -0,0 +190,4 @@
let slot = fb.create_sized_stack_slot(data);
let base = fb.ins().stack_addr(inputs.pointer, slot, word_offset(0));
for field in &plan {
let mut values = Vec::new();
Owner

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

`values` allocates a fresh buffer per call. If this runs per frame or per token, can the buffer be reused?
Records build and their fields read, end-to-end under `ckc test`.
Codegen/Floor-IR only (front-end already parses/resolves records).

Layout per arenas.md static frame layout: no header, no GC, no per-object
free. Offsets = running byte sum in field order; size = sum. Int field =
i64 (8B); String field = (ptr,len) pair (16B, two words) -- correctly
shifts later fields' offsets. 8-byte aligned.

New Floor IR nodes Record{fields} + FieldAccess{receiver,field} (lower.rs);
new codegen/record.rs: emit_record_addr constructs into a Cranelift stack
slot via a base pointer, emit_field_values projects at the field offset.
Reached through a base pointer (stack_addr today) so the same layout is
arena-placeable later by swapping the base origin -- nothing else is
stack-specific. Wired through assert.rs + concat.rs emit_arg so a projected
field is a valid Assert::equal operand and effect-op argument.

Gate: tests/corpus/record_fields + Docker-gated record_fields_run --
point_projects_fields (Int offsets 0/8) and string_field_shifts_offsets
(trailing Int read past a 16B String field) both pass. Front-end clean
(no trap.md), in the RUNTIME_TESTED allow-list with justification.

Out (cited): let/if/match (R2), data/variants (P2.3), collections (P2.4),
String == (P2.5), mocked effects (P2.6), record with-update, value-
returning fns, destructuring.

Gates: fmt clean; clippy 0/0; cargo test --workspace 914 passed;
greeter/helloWorld unchanged; record fixture green. record.rs cka-clean.
The repo pinned rust-toolchain.toml to 1.93, but mise runs `rust = "latest"`
(1.96) and exports RUSTUP_TOOLCHAIN=1.96.0, which overrides the file — so the
declared pin and the toolchain that actually ran disagreed, and "clippy green"
depended on which one you invoked. Bump the pin to 1.96 so the file matches the
environment and CI reads the same. 1.96's pedantic set flags a pre-existing
`map_unwrap_or` in ck_rt::emit; fixed. `cargo fmt --all --check` is 0 diffs on
1.96.
`ckc check <path>` was single-file only, so on a multi-file app every
cross-module ref reported E0301 — a weaker path than both `run` and the corpus
harness. Make `check` branch on file-vs-dir like `run` does: a directory now
runs the project-closure front-end (app + injected stdlib) via the same
`closure_load` the run path and the trap.md harness share, so the three cannot
diverge.

`--traps` emits paste-ready trap.md blocks (`### <File>` + a ```trap fence)
through the same `ck_cktest::diagnostics_to_traps` the reconcile gate consumes.
New `pub fn project_traps` backs it; tests/check.rs asserts the emitted grammar
is well-formed (ratchet-safe: vacuous when an app compiles clean).
todo/ was four layers deep (app -> TaskStore effect -> TodoSqlite provider ->
Sql) for a to-do list, and the TaskStore capability lived inside mod Todo. Rewrite
it as the honest evolution of counter/: an actor whose state is a bounded
`List<Task, 12>` of records, driven by payload-carrying messages, replying
`Unit or TaskErr`. Split by responsibility across Todo.ck (domain), Tasks.ck
(store + Cmd protocol + its tests), and Main.ck (entry + render). Drops SQL,
.env.dev, and the multi-provider config.

todo_effect/ is the same in-memory store behind a capability: domain (Todo.ck),
the TaskStore effect + mock (Store.ck), an `actor ... provides TaskStore`
in-memory provider (TodoMem.ck), and the synchronous app flow (Main.ck) — the
effect-native sibling of the actor-native app.

trap.md for both was generated by `ckc check --traps` and verified by
corpus_apps_match_their_trap_md reconciling clean both ways. Loader corpus tests
follow the new module sets (TodoSqlite -> Tasks) and gain todo_effect coverage.

Collection ops (.len/.map/.filter/+/[]) remain provisional-but-flagged, matching
the existing corpus convention.
A bare (no-payload) data/or-union variant is a value: constructed,
compared, and stored in a record field, end-to-end under ckc test.
Codegen-only (front-end already resolves T::Variant).

Discriminant = variant's 0-based index in declaration order (A=0,B=1,...);
a bare variant value IS that tag (one i64, no header, no GC -- the
arenas.md max-of-cases layout collapsing to a tag when all cases empty).
Construction T::Variant -> iconst(tag); equality via icmp eq over tags
(so Assert::equal(x, LogLevel::Info) works); new FieldRepr::Variant (one
word) so a variant-typed record field lays out + projects correctly.

Tags settle in a new VariantTags map (TypeId->i64) off Interner, threaded
through codegen (never re-derived in ck_lower) -- mirrors IntrinsicTargets/
OpDispatch. build_intrinsic_targets moved to new compile/maps.rs to keep
compile/mod.rs under the 809 LoC cap (cap NOT raised).

Gate: tests/corpus/variants + Docker-gated variants_run -- Color equality
and a Marker{color: Color::Green, n: 7} whose variant field projects back
to Green with n reading 7 past the tag. Front-end clean, allow-listed.

Out (cited): payload-carrying variants (max-of-cases w/ payload -- counter's
messages, deferred), match/patterns (R2), collections (P2.4), String ==
(P2.5), mocked effects (P2.6).

Gates (under toolchain 1.96): fmt clean; clippy 0/0; cargo test --workspace
917 passed; greeter/helloWorld unchanged; variants fixture green.
Note: Interner god-object metric 26->27 (cka-only, not gated; pre-existing).
mdoorn manually merged commit e08ba980cd into master 2026-02-19 08:30:00 +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!40
No description provided.