shape_jit/ffi/object/conversion.rs
1//! JIT-FFI bits ↔ runtime-tier carrier conversions (ADR-006 §2.7.5).
2//!
3//! Pre-strict-typing this module bridged the JIT's NaN-boxed `u64`
4//! representation and the VM's `ValueWord` (the v1 dynamic tagged word).
5//! The Phase-2 bulldozer deleted both the `ValueWord` type and the
6//! `tag_bits::*` discriminator family.
7//!
8//! ## Carrier shape (§2.7.5 stamp-at-compile-time)
9//!
10//! Per ADR-006 §2.7.5 the JIT-FFI surface is `(u64, NativeKind)` — raw
11//! bits plus a parallel kind companion. The kind is stamped at JIT
12//! compile time from the call signature; it is **never decoded from the
13//! bits**. Consumers that need a runtime-tier carrier wrap the pair as
14//! `KindedSlot::new(ValueSlot::from_raw(bits), kind)` per §2.7.6/Q7 when
15//! crossing into runtime-tier dispatch.
16//!
17//! ## What this module does (W11-jit-carrier-conversion close)
18//!
19//! These functions are the bookkeeping layer between the JIT call
20//! signature's separate `bits: u64` and `kind: NativeKind` parameters
21//! (the §2.7.5 stable-FFI raw-pair shape) and the in-Rust `JitFfiCarrier`
22//! tuple alias `(u64, NativeKind)`. The body is intentionally trivial —
23//! per §2.7.5 the conversion is *identity*: raw bits stay raw bits, and
24//! the kind companion is whatever the caller's static stamp passed in.
25//! No decode, no probe, no `is_heap()` classification — the kind is
26//! authoritative because the caller proved it at JIT compile time.
27//!
28//! ## What is forbidden
29//!
30//! - **Decoding kind from `bits`** (CLAUDE.md "Forbidden Patterns" #4,
31//! ADR-006 §2.7.7 #4 — the deleted `tag_bits` dispatch).
32//! - **`is_heap()` / `is_tagged()` probe** to classify (§2.7.7 #7).
33//! - **Bool-default fallback** when the caller didn't supply a kind
34//! (forbidden #9 — surface-and-stop instead).
35//! - **`ValueWord` resurrection under any name** — the body's
36//! pre-bulldozer shape decoded `Arc<HeapValue>` from raw bits via
37//! `ValueWord::clone_from_bits` and re-encoded per-arm via
38//! `ValueWord::as_heap_ref` / `tag_bits::TAG_HEAP`; all deleted.
39//!
40//! ## Historical context (pre-bulldozer body, what's gone)
41//!
42//! The deleted pipeline ran:
43//! 1. `nanboxed_to_jit_bits(&ValueWord)` decoded `ValueWord` per arm
44//! (each arm a `tag_bits::TAG_*` discriminator), produced JIT bits.
45//! 2. `jit_bits_to_nanboxed(bits)` re-decoded raw bits via the same
46//! `tag_bits` machinery, constructed a `ValueWord` via
47//! `ValueWord::from_*` constructors, returned it.
48//! 3. The `UNIFIED_HEAP_REFS` thread-local accumulator captured retained
49//! `Arc<HeapValue>` shares for `drain_unified_heap_refs()` to release
50//! at the end of each JIT call.
51//!
52//! All three steps are gone. The retain/release path now flows through
53//! `clone_with_kind` / `drop_with_kind` at the producing call site
54//! (ADR-006 §2.7.7), driven by the parallel-kind track — no global
55//! drain accumulator.
56
57use crate::ffi::jit_kinds::JitFfiCarrier;
58use shape_value::NativeKind;
59
60/// Per-JIT-call ref-drain hook. Retained as a named export for the JIT
61/// executor's per-call epilogue (`executor.rs`). The pre-bulldozer body
62/// drained the deleted `UNIFIED_HEAP_REFS` thread-local that the deleted
63/// `nanboxed_to_jit_bits` pushed `Arc<HeapValue>` retain-shares into.
64///
65/// Per ADR-006 §2.7.5 the post-call lifecycle is dispatched through
66/// `clone_with_kind` / `drop_with_kind` at the producing call site (the
67/// parallel-kind track in `crates/shape-vm/src/executor/vm_impl/stack.rs`),
68/// not via a global accumulator. The hook is a no-op — there is nothing
69/// to drain.
70#[inline]
71pub fn drain_unified_heap_refs() {
72 // ADR-006 §2.7.5 / §2.7.7: post-call retain/release is dispatched
73 // per-slot through the parallel-kind track at the producing call
74 // site. No global accumulator exists; nothing to drain.
75}
76
77// ============================================================================
78// JIT-FFI bits → runtime-tier carrier (`(u64, NativeKind)` pair)
79// ============================================================================
80
81/// Pack raw JIT-FFI bits + their static `NativeKind` stamp into a
82/// `JitFfiCarrier` per ADR-006 §2.7.5.
83///
84/// The kind comes from the JIT call signature's stamp — the caller's
85/// emitter proved it at JIT compile time. This function does **not**
86/// decode the kind from `bits`: per §2.7.7 #4 / #7, runtime kind
87/// discrimination from a raw bit pattern is the deleted ValueWord
88/// `tag_bits` shape and is forbidden.
89///
90/// Consumers assemble a `KindedSlot` from the returned pair via
91/// `KindedSlot::new(ValueSlot::from_raw(bits), kind)` per §2.7.6/Q7
92/// when they need a runtime-tier carrier.
93#[inline]
94pub fn jit_bits_to_nanboxed(bits: u64, kind: NativeKind) -> JitFfiCarrier {
95 // §2.7.5: identity pack. Bits stay raw; kind is the caller's stamp.
96 (bits, kind)
97}
98
99/// Variant of `jit_bits_to_nanboxed` with `JITContext` access, retained
100/// for downstream call sites (`async_ops`, `control`, `generic_builtin`,
101/// `data_access`, etc.) that need function-name resolution from the
102/// JIT context. The ctx pointer is plumbing for caller-side lookups
103/// (function table, function names) — the carrier itself is the same
104/// `(bits, kind)` pair.
105#[inline]
106pub fn jit_bits_to_nanboxed_with_ctx(
107 bits: u64,
108 kind: NativeKind,
109 _ctx: *const super::super::super::context::JITContext,
110) -> JitFfiCarrier {
111 // §2.7.5: same identity pack as `jit_bits_to_nanboxed`. The `_ctx`
112 // parameter is preserved for downstream signature compatibility but
113 // unused here — function-name resolution happens at the caller
114 // before passing `(bits, kind)` to this function.
115 (bits, kind)
116}
117
118// ============================================================================
119// TypedScalar ↔ JIT bits Conversion (still kind-flat — no carrier change)
120// ============================================================================
121
122/// Convert JIT NaN-boxed bits to a `TypedScalar` with an optional type hint.
123///
124/// `TypedScalar` is a kind-tagged scalar carrier (`ScalarKind` field) that
125/// the producing site populates from the `NativeKind` hint per ADR-006
126/// §2.7.5; this is the carrier shape for scalar-FFI returns and does not
127/// participate in the deleted `ValueWord` dispatch. The hint comes from
128/// the JIT-emitted `FrameDescriptor`'s slot-kind track per §2.7.7/Q9.
129pub fn jit_bits_to_typed_scalar(
130 bits: u64,
131 hint: Option<shape_vm::NativeKind>,
132) -> shape_value::TypedScalar {
133 use crate::ffi::value_ffi::{
134 TAG_BOOL_FALSE, TAG_BOOL_TRUE, TAG_NONE, TAG_NULL, TAG_UNIT, is_number, unbox_number,
135 };
136 use shape_value::TypedScalar;
137 use shape_vm::NativeKind;
138
139 if is_number(bits) {
140 let f = unbox_number(bits);
141 if let Some(h) = hint {
142 match h {
143 NativeKind::Int8 | NativeKind::NullableInt8 => return TypedScalar::i8(f as i8),
144 NativeKind::UInt8 | NativeKind::NullableUInt8 => return TypedScalar::u8(f as u8),
145 NativeKind::Int16 | NativeKind::NullableInt16 => return TypedScalar::i16(f as i16),
146 NativeKind::UInt16 | NativeKind::NullableUInt16 => {
147 return TypedScalar::u16(f as u16);
148 }
149 NativeKind::Int32 | NativeKind::NullableInt32 => return TypedScalar::i32(f as i32),
150 NativeKind::UInt32 | NativeKind::NullableUInt32 => {
151 return TypedScalar::u32(f as u32);
152 }
153 NativeKind::Int64 | NativeKind::NullableInt64 => return TypedScalar::i64(f as i64),
154 NativeKind::UInt64 | NativeKind::NullableUInt64 => {
155 return TypedScalar::u64(f as u64);
156 }
157 NativeKind::Float64 | NativeKind::NullableFloat64 => {
158 return TypedScalar::f64_from_bits(bits);
159 }
160 _ => {
161 // Bool / String / Boxed / etc. fall through to the
162 // generic-number branch (the bits already encode an
163 // f64).
164 }
165 }
166 }
167 return TypedScalar::f64_from_bits(bits);
168 }
169
170 if bits == TAG_BOOL_TRUE {
171 return TypedScalar::bool(true);
172 }
173 if bits == TAG_BOOL_FALSE {
174 return TypedScalar::bool(false);
175 }
176 if bits == TAG_NULL || bits == TAG_NONE {
177 return TypedScalar::none();
178 }
179 if bits == TAG_UNIT {
180 return TypedScalar::unit();
181 }
182
183 // Non-scalar (heap pointer, function, etc.) — return None sentinel. The
184 // kinded entry-point per ADR-006 §2.7.5/§2.7.10 takes the receiver's
185 // NativeKind from the call signature for heap-shaped slots.
186 TypedScalar::none()
187}
188
189/// Convert a `TypedScalar` to JIT NaN-boxed bits. Integer kinds box as
190/// `box_number(value as f64)` since the JIT's Cranelift IR uses f64 for
191/// all numeric operations internally — this is JIT-internal scalar
192/// encoding, not `tag_bits` dispatch.
193pub fn typed_scalar_to_jit_bits(ts: &shape_value::TypedScalar) -> u64 {
194 use crate::ffi::value_ffi::{TAG_BOOL_FALSE, TAG_BOOL_TRUE, TAG_NULL, TAG_UNIT, box_number};
195 use shape_value::ScalarKind;
196
197 match ts.kind {
198 ScalarKind::I8 | ScalarKind::I16 | ScalarKind::I32 | ScalarKind::I64 => {
199 box_number(ts.payload_lo as i64 as f64)
200 }
201 ScalarKind::U8 | ScalarKind::U16 | ScalarKind::U32 | ScalarKind::U64 => {
202 box_number(ts.payload_lo as f64)
203 }
204 ScalarKind::I128 | ScalarKind::U128 => box_number(ts.payload_lo as i64 as f64),
205 ScalarKind::F64 | ScalarKind::F32 => ts.payload_lo, // already f64 bits
206 ScalarKind::Bool => {
207 if ts.payload_lo != 0 {
208 TAG_BOOL_TRUE
209 } else {
210 TAG_BOOL_FALSE
211 }
212 }
213 ScalarKind::None => TAG_NULL,
214 ScalarKind::Unit => TAG_UNIT,
215 }
216}
217
218// ============================================================================
219// Runtime-tier carrier → JIT NaN-boxed bits
220// ============================================================================
221
222/// Unpack a `JitFfiCarrier` back to raw JIT-FFI bits per ADR-006 §2.7.5.
223///
224/// The carrier is `(bits, kind)` — the bits are already in the JIT's
225/// raw 8-byte slot form per §2.7.5 stable-FFI rule. Unpacking is a
226/// projection, not a re-encoding: the producing caller (the one that
227/// constructed the carrier via `jit_bits_to_nanboxed`) already supplied
228/// the canonical JIT-side bit pattern. The kind companion is consumed
229/// by the caller's dispatch shell at the JIT-internal call site
230/// (`op_get_prop` / `op_call_method` / etc. emitter), not re-decoded
231/// here.
232///
233/// **Forbidden alternatives**:
234/// - Per-arm re-encoding via `tag_bits::TAG_HEAP` / `make_tagged` on the
235/// `NativeKind::Ptr(HeapKind::*)` arms — that is the deleted ValueWord
236/// pipeline (CLAUDE.md "Forbidden Patterns" #4).
237/// - `is_heap()` probe on `bits` to classify before re-encoding —
238/// §2.7.7 #7 forbidden.
239/// - Stamping kind from a side-table keyed by `bits` — same forbidden
240/// shape; kind is on the carrier struct, not derived from the bits.
241#[inline]
242pub fn nanboxed_to_jit_bits(carrier: &JitFfiCarrier) -> u64 {
243 // §2.7.5: identity unpack. The bits ARE the JIT-side representation;
244 // no re-encoding step exists under strict typing.
245 //
246 // Kind on the carrier is consumed by the caller's dispatch shell
247 // for stack-slot retain/release accounting at the JIT-internal
248 // call site (the producing emitter knows the kind statically and
249 // emits the matching `clone_with_kind` / `drop_with_kind` epilogue
250 // in JIT IR). We do NOT touch refcounts here — that would alias-
251 // share the slot's ownership in a way the dispatch shell would
252 // either leak (if we cloned) or double-free (if we dropped).
253 let _kind = carrier.1;
254 carrier.0
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use shape_value::{HeapKind, NativeKind};
261
262 #[test]
263 fn jit_bits_to_nanboxed_packs_bits_and_kind_identity() {
264 // §2.7.5: pack is a trivial tuple constructor.
265 let bits = 0xDEAD_BEEF_CAFE_BABEu64;
266 let carrier = jit_bits_to_nanboxed(bits, NativeKind::Int64);
267 assert_eq!(carrier.0, bits);
268 assert_eq!(carrier.1, NativeKind::Int64);
269 }
270
271 #[test]
272 fn jit_bits_to_nanboxed_preserves_heap_kind() {
273 let bits = 0x1234_5678_9ABCu64; // arbitrary
274 let carrier = jit_bits_to_nanboxed(bits, NativeKind::Ptr(HeapKind::TypedObject));
275 assert_eq!(carrier.0, bits);
276 assert_eq!(carrier.1, NativeKind::Ptr(HeapKind::TypedObject));
277 }
278
279 #[test]
280 fn nanboxed_to_jit_bits_unpacks_identity() {
281 // §2.7.5: unpack returns the bits unchanged; kind is consumed at
282 // the caller's dispatch shell (not here).
283 let bits = 0xABCD_1234_5678_DEFFu64;
284 let carrier: JitFfiCarrier = (bits, NativeKind::Float64);
285 assert_eq!(nanboxed_to_jit_bits(&carrier), bits);
286 }
287
288 #[test]
289 fn roundtrip_bits_through_carrier() {
290 // pack → unpack is identity on bits per §2.7.5.
291 for kind in [
292 NativeKind::Int64,
293 NativeKind::Float64,
294 NativeKind::Bool,
295 NativeKind::String,
296 NativeKind::Ptr(HeapKind::TypedObject),
297 NativeKind::Ptr(HeapKind::TypedArray),
298 NativeKind::Ptr(HeapKind::HashMap),
299 ] {
300 let bits = 0x1122_3344_5566_7788u64;
301 let carrier = jit_bits_to_nanboxed(bits, kind);
302 assert_eq!(carrier.1, kind);
303 assert_eq!(nanboxed_to_jit_bits(&carrier), bits);
304 }
305 }
306
307 #[test]
308 fn drain_unified_heap_refs_is_noop() {
309 // §2.7.5: no global accumulator to drain — refcounts are
310 // dispatched per-slot through the parallel-kind track at the
311 // producing call site.
312 drain_unified_heap_refs();
313 drain_unified_heap_refs();
314 }
315}