lift: a name is found the same way wherever it is asked for #219

Manually merged
mdoorn merged 3 commits from feat/219-lift-a-name-is-found-the-same-way-wherever-it into master 2026-09-04 08:35:30 +02:00
Owner

lift: a name is found the same way wherever it is asked for

Along the way:

  • plan: the queue, measured — what landed, what is open, and what the constraints cost
  • newtype: a rep is read the same way whether it is written or read back

Touches 12 files: plan.md, src/rust/crates/ck_driver/tests/capability.rs, src/rust/crates/ck_types/src/field_access.rs, src/rust/crates/ck_types/src/lift.rs, src/rust/crates/ck_types/src/newtype_ctor.rs, tests/capability/money_add_operator_does_not_lower.cktest and 6 more.

lift: a name is found the same way wherever it is asked for Along the way: - plan: the queue, measured — what landed, what is open, and what the constraints cost - newtype: a rep is read the same way whether it is written or read back Touches 12 files: `plan.md`, `src/rust/crates/ck_driver/tests/capability.rs`, `src/rust/crates/ck_types/src/field_access.rs`, `src/rust/crates/ck_types/src/lift.rs`, `src/rust/crates/ck_types/src/newtype_ctor.rs`, `tests/capability/money_add_operator_does_not_lower.cktest` and 6 more.
hkowalski approved these changes 2026-09-04 08:00:00 +02:00
hkowalski left a comment

Read it too. No objection to src/rust/crates/ck_types/src/field_access.rs.

Read it too. No objection to `src/rust/crates/ck_types/src/field_access.rs`.
mdoorn requested changes 2026-09-04 08:30:00 +02:00
Dismissed
mdoorn left a comment

Two things inline. The one in src/rust/crates/ck_types/src/field_access.rs is the one I care about.

Two things inline. The one in `src/rust/crates/ck_types/src/field_access.rs` is the one I care about.
@ -239,1 +242,3 @@
return None;
return interner
.newtype_scalar_target(id)
.map(|leaf| Ty::nominal(leaf, Vec::new()));
Owner

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

`map` allocates a fresh buffer per call. If this runs per frame or per token, can the buffer be reused?
@ -70,0 +116,4 @@
/// One rule otherwise, read off the declaration's own parameter list — no type
/// name, and no per-parameter *kind* (a tag and a size are treated alike; the
/// question is only whether the bare name fills the slot).
fn under_determined(id: TypeId, ret: &Ty, interner: &Interner) -> bool {
Owner

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

`under_determined` reads, decides and writes in one function. The deciding part is the piece I would want to test on its own. Worth splitting?
mdoorn approved these changes 2026-09-04 08:30:00 +02:00
mdoorn left a comment

Good. The second pass on src/rust/crates/ck_types/src/field_access.rs is what I hoped for.

Good. The second pass on `src/rust/crates/ck_types/src/field_access.rs` is what I hoped for.
Author
Owner

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

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

Split. The decision in under_determined is a pure function now, with its own tests in src/rust/crates/ck_types/src/newtype_ctor.rs.

Split. The decision in `under_determined` is a pure function now, with its own tests in `src/rust/crates/ck_types/src/newtype_ctor.rs`.
Author
Owner

Updated. The point about src/rust/crates/ck_types/src/field_access.rs turned out to be a real bug, so thank you.

Updated. The point about `src/rust/crates/ck_types/src/field_access.rs` turned out to be a real bug, so thank you.
The merge of five branches surfaced a regression none of them could see alone:
eleven `money_*` / `tag_newtype_*` cells began reporting E0742 on correct code.

`Money<USD>` — where `USD` is a nullary variant of the parameter's declared
`data Currency` — is a concrete TAG, not an unbound name. It broke because the
binder slice closed the head-resolution blind spot (a stdlib head missed in an
app module, so the whole value-slot check was silently skipped and a field
absorbed to unbounded) by giving `resolve_head_type_id` a project-roster
fallback after module scope. `resolve_named_type` did not get the same
fallback. So the HEAD `Money` resolved globally while the PARAMETER's declared
`Currency` still stopped at scope, `value_slot_tag` could not see `USD` as a
variant, and a legal tag was diagnosed as a free binder.

Closing the head hole was right; asking for two names two different ways was
not. `resolve_named_type` now ends in the same roster lookup, so what CHECKS
and what LIFTS cannot tell different stories about one program — which is the
invariant `param_args`' module doc already claimed and no longer had.

Measured after the fix, not argued:

  - `Money<USD>`                     clean (was E0742)
  - `List<byte, n>`, n free          E0742  — the free-binder rule holds
  - `List<byte, WOBBLY>`, unfoldable E0742  — the head blind spot stays closed
  - `Pair2<1 + 1, Int>`              clean  — the lead defect stays fixed

Also: Grammar.ckx's M1/M2 sites moved (List.ck:272→285, 222→235). Values are
unchanged at 3 and 4; the binder slice added `<n: Size>` to five signatures and
shifted the lines under them. `grammar_structure` caught it and printed the
corrected rows, which is the reconciliation working.

Worth recording why this reached a merge at all: `--test capability` is the only
harness that runs these cells, and it was off-limits to every agent on thermal
grounds (it runs all 437 cells with container executions, no per-cell
selection). Each agent verified honestly inside the limits it was given, and
the limits had a hole exactly the shape of this bug.
Not a roadmap; CLAUDE.md §1 still governs and this repo works demand-driven.
This is the queue plus the facts that were expensive to establish, so the next
session does not re-derive them: docs/ exists in neither repo, the gate had no
--no-fail-fast, --test capability has no per-cell selection, a 200-item list
literal checks clean, and composes clauses are never read back.

Every number is measured at 0659629d and dated. A number with no measurement
behind it does not belong in the file.
`ck_types::field_access::newtype_rep_ty` claimed in its own doc to be "the ONE
rep-type law shared by both convention surfaces" — the `v.value` accessor and
the `N(r)` constructor — and answered only ONE of the two rep shapes a `newtype`
can declare. The named-tuple rep (`newtype String<size> = (| position, len |)`)
came out of it; the scalar rep (`newtype DateSpan = i64`) did not. The accessor
covered for that with a second arm of its own; the constructor had none, so a
scalar-rep newtype had no constructor type at all.

The cost was not a diagnostic. `DateSpan(42)` synthesized `Unknown`, the
coherence probe needs a `Ty::Nominal` operand to project its input-position key
from (`method_call.rs:632`), so the operand pair was dropped, the driver settled
no impl for the call (`mono::method_impls_for_body`), and `lower_method_call`
fell through to the spanned `Unsupported` codegen refuses. `a + DateSpan(42)`
therefore did not lower while `let s = DateSpan(42); a + s` did — the operand's
FORM decided, which an operand's form must never do. Both shapes now come out of
the one function, and the accessor keeps only the accessor-name reservation.

The exactness this buys is the other direction too: `f(DateSpan(1))` against a
`String` parameter is now E0702 where it used to check clean and reach the
Cranelift verifier.

Ratchets: `BROKEN_CELL_COUNT` 54 -> 49, `CELL_COUNT` 437 -> 438.

Six cells flip to WORKS; only ONE is earned by this change:

  · newtype_ctor_call_as_an_operator_operand   <- this commit

The other five were ALREADY green at b5668540 and had merely never been re-run,
because `--test capability` is the only harness that executes them and it has
not run since the slices that fixed them merged (plan.md §1, §6):

  · money_add_operator_does_not_lower
  · money_equals_operator_does_not_lower
  · money_show_does_not_lower
  · money_value_projection_does_not_lower
  · tag_newtype_operator_across_a_module_boundary

`money_add_operator_does_not_lower`'s tail changes from a bare `42` to `c.value`
so that it observes the FOLDED SUM (21 + 21 minor units) instead of proving only
that the program compiled. Its own comment block had said to do exactly this
once `.value` answered on a tag-parameterised newtype, which it now does.

`Types.ck`'s KNOWN GAP note — "an app-side `Show::show(m)` on a TAG-parameterised
type lowers Unsupported today" — is retired in this commit, which is what
`money_show_does_not_lower` was written to force.

One new cell, BROKEN: `tag_newtype_ctor_result_is_under_determined`. A
constructor on a newtype with an UNFILLED generic parameter (`Money::<USD>(1)`)
still has no RESULT type. Inference here is synthesis-only by design, so neither
the turbofish nor the slot reaches the node, and naming the zero-argument head
`Money` would be a WRONG answer rather than a partial one — nominal
assignability is invariant, so it refuses the `Money<USD>` slot the value is
legitimately bound to (measured: E0722 out of `impl Add<Money<USD>, …>`'s own
body). The under-determined result therefore stays fail-soft `Unknown`, scoped
to the scalar rep shape, with the reason written at `under_determined`: the
named-tuple shape must keep its bare head or twelve `list_*` cells stop lowering
(`List.ck` spells its own slots bare), and nothing in the two declarations
separates them — only their consumers' spelling does. That is an asymmetry, it
is measured, and it is pinned rather than left in prose.

Verified without the capability harness (not this session's to run): all 438
cells materialized and taken to `ckc check` + `ckc disassemble` before and
after — exactly one cell moves, plus the new one; all 143 project directories
under tests/{corpus,corpus_test_run,corpus_parked} byte-identical at `ckc check`;
the six flipped cells linked freestanding and executed natively, each exit 42.
`--test conformance`, `--test architecture`, `-p ck_types`, `-p ck_lower`,
`-p ck_resolve`, `-p ck_cktest` green. `cargo fmt --check` clean;
`clippy --workspace --all-targets -D warnings` 0 warnings, 0 errors.
mdoorn manually merged commit abf27c7b1c into master 2026-09-04 08:35:30 +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!219
No description provided.