feat(plan_values A4): float width conversion via value turbofish #58

Manually merged
dferreira merged 6 commits from feat/058-feat-plan-values-a4-float-width-conversion-via into master 2026-03-13 15:25:14 +01:00
Owner

feat(plan_values A4): float width conversion via value turbofish

Along the way:

  • docs(plan_values): f8/f16 exist as distinct types with a lowered f32 repr
  • feat(plan_values A4b): f8/f16 realized as a lowered f32 repr
  • fix(plan_values A4c): lowercase scalar pin resolution + retire A4's hollow gate
  • feat(plan_values A5a): a float crosses a fn boundary as one scalar word
  • docs(plan_values): record Phase A state + the actor-boundary limitation

Touches 49 files: docs/current-state.md, plan_values.md, src/rust/crates/ck_codegen/src/binding.rs, src/rust/crates/ck_codegen/src/strings.rs, src/rust/crates/ck_codegen/src/user_fn.rs, src/rust/crates/ck_codegen/src/value.rs and 43 more.

feat(plan_values A4): float width conversion via value turbofish Along the way: - docs(plan_values): f8/f16 exist as distinct types with a lowered f32 repr - feat(plan_values A4b): f8/f16 realized as a lowered f32 repr - fix(plan_values A4c): lowercase scalar pin resolution + retire A4's hollow gate - feat(plan_values A5a): a float crosses a fn boundary as one scalar word - docs(plan_values): record Phase A state + the actor-boundary limitation Touches 49 files: `docs/current-state.md`, `plan_values.md`, `src/rust/crates/ck_codegen/src/binding.rs`, `src/rust/crates/ck_codegen/src/strings.rs`, `src/rust/crates/ck_codegen/src/user_fn.rs`, `src/rust/crates/ck_codegen/src/value.rs` and 43 more.
Author
Owner

It said what I assumed. Reworded in src/rust/crates/ck_driver/tests/float_narrow_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/float_narrow_run.rs` so it names the input that broke it, which is what the bug report will need.
Author
Owner

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

Split. The decision in `find_near_exe` is a pure function now, with its own tests in `src/rust/crates/ck_driver/tests/float_fn_cross_run.rs`.
mdoorn approved these changes 2026-03-13 14:17:07 +01:00
mdoorn left a comment

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

Read it too. No objection to `src/rust/crates/ck_driver/tests/float_narrow_run.rs`.
lnguyen approved these changes 2026-03-13 15:20:14 +01:00
lnguyen left a comment

Approving. I would have split src/rust/crates/ck_driver/tests/float_narrow_run.rs differently, but not enough to hold this up.

Approving. I would have split `src/rust/crates/ck_driver/tests/float_narrow_run.rs` differently, but not enough to hold this up.
@ -0,0 +204,4 @@
}
/// Search the test executable's ancestor directories for a sibling file `name`.
fn find_near_exe(name: &str) -> Option<PathBuf> {
Owner

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

`find_near_exe` 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 +95,4 @@
}
// (5) low KB.
let size = std::fs::metadata(&binary).expect("binary metadata").len();
Owner

The expect message on size is the only thing a user sees if this fires. Does it say what they should do, or only what we assumed?

The expect message on `size` is the only thing a user sees if this fires. Does it say what they should do, or only what we assumed?
`expr::<T>` on a float value whose width differs from T is a width conversion
(`let n = a::<f32>` narrows f64 -> f32; `n::<f64>` widens). Rides the EXISTING
value-turbofish surface — no `as` cast, no invented Convert trait. Zero new
floor leaves; ISA row-set stays 23.

- ck_types: `synth_local_ref` — a float local pinned to a float width synthesizes
  as the TARGET width (the conversion result), so `n == 12.0::<f32>` ties
  same-width. Type-directed on `Primitive::is_float`, never a `match type_name`.
- ck_lower: new scalar-tier `ExprKind::FloatConvert { from, to, value }`; the pass
  wraps the ref (source width = local's checked width, target = the reference's
  checked width); a same-width pin folds to identity. Decomposed into
  `lower/convert_lower.rs`.
- ck_codegen: `apply_float_convert` emits a single Cranelift `fdemote` (narrow) /
  `fpromote` (widen) at the primitive-op tier — no leaf. Float scalar concern
  extracted to `value::emit_float_scalar` to hold the fn cap (no cap raise).

Gate: `tests/corpus/float_convert` narrows 12.0->f32, widens back (bit-exact
f64->f32->f64 round-trip), narrows 0.5->f32, each proven against a pinned
exactly-representable literal -> exit 42 freestanding. Verified to discriminate
(12.0 -> 12.5 yields exit 1).

Honest deferrals (recorded, not faked): f8 has no IEEE-8 machine type; f16's x86
conversion needs soft-float libcalls (__truncsfhf2/__extendhfsf2) a freestanding
binary cannot link — both are an honest backend error. f32/f64 are the native
SSE widths the gate proves. Annotated `let x: T = e` is a separate pre-existing
codegen gap (fails for Int too) — the corpus uses manifest inference.

Gates: fmt clean, clippy 0, cargo test --workspace exit 0, container exits 42
(discriminates), ISA 23, caps unchanged.
Owner decision (2026-07-08): f8 and f16 must EXIST as first-class manifest types
(f8 is not f16 is not f32 is not f64), but their machine representation is a
lowered f32 (native SSE) — so every op rides native float instructions with no
soft-float libcall (a freestanding binary can't link __truncsfhf2 &c., and there
is no IEEE-8 machine type). Memory efficiency is explicitly not a goal here.

- §3a: record the lock — ValueRepr::Float(F8)/Float(F16) map to Cranelift F32;
  f8<->f16<->f32 conversions are repr-identity, f8/f16<->f64 use fdemote/fpromote.
- §10: A4's f8/f16 deferral is superseded; add A4b (f8/f16 realized as lowered
  f32 — completes Repr+Literal+Arith+Conversion for the narrow widths); A5's
  done-when notes f8/f16 cross as their lowered f32 repr.
f8 and f16 now EXIST as distinct manifest types (f8 is not f16 is not f32) with
a lowered f32 machine repr — native SSE, no soft-float libcall, freestanding.
Memory efficiency is explicitly not a goal (per the §3a lock).

- ck_lower/layout.rs: FloatWidth::byte_width is the machine-repr footprint
  (F8/F16/F32 = 4 bytes, F64 = 8), retiring A3's 1/2-byte declared widths.
- ck_lower/lower/convert_lower.rs: a same-repr pin (f8<->f16<->f32) folds to
  identity (no FloatConvert node); only a differing repr (...<->f64) converts.
- ck_codegen/value.rs: repr_types collapses the four Float arms into
  float_repr_type (F8/F16/F32 -> Cranelift F32, F64 -> F64).
- ck_codegen/strings.rs: emit_float_const emits f32const for F8/F16/F32.
- ck_codegen/binding.rs: apply_float_convert = repr-identity when widths share
  the f32 repr, else fdemote/fpromote (f32<->f64). Removed dead float_ir_type.

Gate: tests/corpus/float_narrow proves f8 fadd, f16 fadd, f8/f16->f64 fpromote,
f64->f8/f16 fdemote against pinned exactly-representable literals -> exit 42,
freestanding (nm -u empty; no __truncsfhf2/__extendhfsf2/libc). Discriminates
(mutations give distinct non-42 codes). A missing widen/narrow would reinterpret
f32 bits as f64 garbage and fail, so the gate is not trivially satisfiable.

Finding (documented, addressed next): Primitive::from_name is uppercase-only, so
LOWERCASE float turbofish (::<f32>) silently falls back to F64 — which made A4's
float_convert gate pass TRIVIALLY (its conversions folded to identity). This
slice uses the working uppercase ::<F8>..::<F64> spelling; the lowercase-
resolution gap and A4's weak gate are the next slice.

Gates: fmt clean, clippy 0, cargo test --workspace exit 0, float_narrow exits 42
freestanding no-libcall, ISA 23, caps unchanged.
Two real defects, found when A4b's agent read the CLIF and saw A4's conversions
folding to identity:

1. Primitive::from_name was uppercase-only ("F32"), so the canonical LOWERCASE
   turbofish `::<f32>` (docs/types.md; the owner's `i::<f32>` surface) failed to
   resolve and silently fell back to F64. `from_name`/`name` now use the
   canonical lowercase fixed-width spellings (f8/f16/f32/f64, i8..u64); Int/Bool/
   Char stay capitalized; the round-trip test is updated.
2. An unresolvable value-turbofish pin silently defaulted to F64. It is now a
   hard error (E0739). Generic ::<T>, nominal ::<Region>, and non-float scalars
   still resolve — only an unresolvable scalar pin errors.

Consequence fixed: A4's float_convert gate had been passing TRIVIALLY (lowercase
::<f32> folded to identity, everything stayed f64, 12.0==12.0). It is rewritten
as a precision-loss gate: a 0.1 f64->f32->f64 round-trip must break `== 0.1`
(a real fdemote/fpromote loses precision), plus an exact 0.5 direction that must
hold — discriminating in both directions, not trivially satisfiable.

Migrated every uppercase SCALAR turbofish pin to lowercase (u8_roundtrip,
float_narrow, primes*, bytes_roundtrip, stdlib Bytes/Memory/Meta, the Size=u16
fixtures, numeric_literal_turbofish); nominal ::<Region>/::<Int> and generic
::<T> untouched.

Proof lowercase now really converts: `let a=0.1; let n=a::<f32>; let b=n::<f64>;
b==0.1` -> exit 42 (equality broken by the real round-trip; was 99 under the
defect). Stale ::<F32> and bogus ::<f99> both -> E0739.

Gates: fmt clean, clippy 0, cargo test --workspace exit 0, float_convert/
float_narrow/u8_roundtrip all exit 42, ISA 23, caps unchanged. No codegen leaf
touched — pure resolution + gate rewrite.
A float now passes as a user-fn param and comes back as a return, byte-exact —
the single-word crossing (a float is one word: f64=8B, f8/f16/f32 share the
lowered f32 4B word per A4b), distinct from the multi-word POD case (A6). Two
one-arm edits, no new leaf, ISA stays 23, no `match type_name`.

- ck_driver/user_fns.rs: `is_scalar_return` — the restriction behind the error
  "a called user function returns a value outside this slice's frame (Unit / Int
  / floor handle / union only…)" — gains a guarded `Ty::Primitive(p) if
  p.is_float()` arm, so a float return joins the in-frame set.
- ck_codegen/user_fn.rs: `abi_types` gains `Ty::Primitive(p) if p.is_float()` ->
  `repr_types(ValueRepr::Float(FloatWidth::from_primitive(p)))`, reusing the one
  repr law. user_fn_signature / bind_params / ret_types / emit_user_call_value
  all route through abi_types, so param passing, block-param binding, and the
  single-word return fall out uniformly. Type-directed on float width.

Param arithmetic (`x + x` on an f64 param) folds to native fadd with no extra
wiring — the fold gates on the operand's declared type, which a param carries.

Gate: tests/corpus/float_fn_cross proves (a) id(x: f64) round-trips a float,
(b) dbl { x + x } returns 2*x, (c) an f32 param/return crosses (lowercase
1.25::<f32>) -> exit 42 freestanding. Verified to discriminate: input a=3.0
breaks id(a)==2.5 -> exit 1; dropping the + in dbl -> exit 2.

Gates: fmt clean, clippy 0, cargo test --workspace exit 0, container exits 42
(discriminates), ISA 23, caps unchanged.
Add §0 (Current state & limitations, read-first) as the handoff for the next
agent:
- What landed: the f* float family Phase A (A1/A3/A4/A4b/A4c/A5a with commits);
  A2 removed (precision is a Show operation arg).
- The one limitation: actor-boundary crossing is blocked for EVERY value type —
  target.send does not lower (SendUnsupported); actor messaging is the runtime
  plan's MA3c-3, not a values slice. We cannot spin up/message actors yet, so
  ladder rung 6 + slices A5b/A7 are deferred until send lowering lands. Recorded,
  not faked.
- Handoff: the other value types (A6 multi-word POD ABI core, then §3b integers,
  §3c Char, §3d String, §2 composites) each follow the same six-rung ladder,
  deferring rung 6. Plus the cross-cutting rules this run established (lowercase
  pins/E0739, one generic law, ISA frozen at 23, gates must discriminate, caps
  are signals not targets, the pre-existing annotated-let gap).
dferreira manually merged commit 0d4898dabb into master 2026-03-13 15:25:14 +01: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!58
No description provided.