Skip to main content

shape_value/
native_kind.rs

1//! `NativeKind`: the single discriminator for typed values at every ABI exit.
2//!
3//! Used by:
4//! - `shape-vm` compile-time proof: `prove_native_kind() -> Result<NativeKind, ProofGap>`
5//! - Marshal layer (`shape-runtime::typed_module_exports`): `(u64 bits, NativeKind kind)` paired
6//! - Wire/snapshot serialization: `slot_to_wire(bits, kind, ctx)`
7//! - JIT FFI boundary
8//!
9//! Previously named `SlotKind`; renamed and moved out of `shape-vm/type_tracking.rs`
10//! into the foundational `shape-value` crate during the strict-typing Phase 2b
11//! marshal-layer landing. The single-discriminator rule prevents the two-parallel-
12//! discriminator drift trap (see `docs/defections.md` 2026-05-06 — Phase 2b).
13//!
14//! `NativeKind::Dynamic` and `NativeKind::Unknown` are both deleted — the bulldozer
15//! removed them per the strict-typed plan. Every slot has a proven kind at compile
16//! time or it's a compile error. There is no fallback variant.
17
18use crate::heap_value::HeapKind;
19use serde::{Deserialize, Serialize};
20
21/// Storage discriminator for a single 8-byte typed slot.
22///
23/// Each variant identifies which native type the slot's `u64` raw bits
24/// represent, including width and nullability for integers and float.
25/// Boolean has no width variant. `String` is special-cased as the most
26/// common heap shape (an `Arc<String>` raw pointer); all other heap-
27/// allocated shapes use `Ptr(HeapKind)` carrying the surviving
28/// `HeapValue` discriminant. The kind tells the marshal/wire/snapshot
29/// layer which `HeapValue` arm the bits decode to without probing the
30/// bits themselves.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub enum NativeKind {
33    /// Plain f64 value (direct float operations)
34    Float64,
35    /// Nullable f64 using NaN sentinel (Option<f64>)
36    /// IEEE 754: NaN + x = NaN, so null propagates automatically
37    NullableFloat64,
38    /// Plain f32 value (4-byte single-precision float). ADR-006 §2.7.5
39    /// amendment (Round 19 S1.5 W12-nativekind-scalar-additions,
40    /// 2026-05-14): non-parametric scalar variant introduced for
41    /// `Array<f32>` v2-raw producer paths. `f32` is `Copy + 4-byte` and
42    /// fits the v2-raw `TypedArray<T>` flat-struct shape without Arc
43    /// wrapping. Slot bits store the `f32` zero-extended into the low 32
44    /// bits of the 8-byte slot via `f32::to_bits` (zero-extended).
45    Float32,
46    /// Plain `char` value (4-byte Unicode scalar). ADR-006 §2.7.5
47    /// amendment (Round 19 S1.5 W12-nativekind-scalar-additions,
48    /// 2026-05-14): non-parametric scalar variant introduced for
49    /// `Array<char>` v2-raw producer paths. `char` is `Copy + 4-byte`
50    /// (UTF-32 scalar-value subset of `u32`) and fits the v2-raw
51    /// `TypedArray<T>` flat-struct shape without Arc wrapping. Slot bits
52    /// store the codepoint as `c as u32` zero-extended into the low 32
53    /// bits of the 8-byte slot.
54    ///
55    /// **Parallel-discriminator note** (CLAUDE.md §Parallel-implementation
56    /// across producer/consumer carrier-shape boundaries): the existing
57    /// `NativeKind::Ptr(HeapKind::Char)` carrier remains in source for
58    /// inline-char-value paths that push char values directly to the
59    /// VM stack without going through the §2.7.6/Q8 `KindedSlot::from_char`
60    /// constructor. `NativeKind::Char` is the scalar-bucket carrier (the
61    /// canonical §2.7.6/Q8 constructor target); `NativeKind::Ptr(HeapKind::Char)`
62    /// is a per-element carrier label for the inline-codepoint payload
63    /// pattern. Both labels are read-side-equivalent (slot bits in both
64    /// shapes are `c as u32` zero-extended), but consumer dispatch sites
65    /// MUST handle either label exhaustively for correctness — the
66    /// `NativeKind::Char` arm is the §Q8 carrier-API target, the
67    /// `Ptr(HeapKind::Char)` arm is the pre-amendment inline-payload
68    /// pattern. A future sub-cluster (cluster-1 hardening) folds the
69    /// `Ptr(HeapKind::Char)` arms into `NativeKind::Char` exhaustively
70    /// once the `HeapKind::Char` label can be retired.
71    Char,
72    /// Plain i8 value
73    Int8,
74    /// Nullable i8 value
75    NullableInt8,
76    /// Plain u8 value
77    UInt8,
78    /// Nullable u8 value
79    NullableUInt8,
80    /// Plain i16 value
81    Int16,
82    /// Nullable i16 value
83    NullableInt16,
84    /// Plain u16 value
85    UInt16,
86    /// Nullable u16 value
87    NullableUInt16,
88    /// Plain i32 value
89    Int32,
90    /// Nullable i32 value
91    NullableInt32,
92    /// Plain u32 value
93    UInt32,
94    /// Nullable u32 value
95    NullableUInt32,
96    /// Plain i64 value
97    Int64,
98    /// Nullable i64 value
99    NullableInt64,
100    /// Plain u64 value
101    UInt64,
102    /// Nullable u64 value
103    NullableUInt64,
104    /// Plain isize value
105    IntSize,
106    /// Nullable isize value
107    NullableIntSize,
108    /// Plain usize value
109    UIntSize,
110    /// Nullable usize value
111    NullableUIntSize,
112    /// Boolean value. Slot bits: `0` = `false`, `1` = `true`. Bool slots
113    /// NEVER carry the null/unit sentinel — null is a distinct variant
114    /// (`NativeKind::Null`) per the R5b-2-bool-null-sentinel disposition
115    /// (ADR-006 §2.7 carrier-semantics + §2.7.7/Q9 parallel-kind track,
116    /// 2026-05-19): pre-disposition `(0u64, NativeKind::Bool)` was the
117    /// canonical null sentinel, which collided with legitimate `false`
118    /// bool values (both encoded as bits=0). The collision caused
119    /// VM-only divergence per W14.2-G6 SURFACE-G6-BOOL-NULL +
120    /// SURFACE-G6-LET-ONLY-BODY + SURFACE-G6-NONE-OUTPUT-ADAPTER.
121    /// Post-disposition: Bool slots carry only `{0, 1}` bits; null is
122    /// pushed with `NativeKind::Null` discriminator (kind from the
123    /// §2.7.7/Q9 parallel-kind track), restoring the kind-discriminator
124    /// soundness invariant.
125    Bool,
126    /// Unit / null sentinel. Non-parametric scalar variant indicating
127    /// the slot carries the absence-of-value marker (`None` / `null` /
128    /// implicit unit return). R5b-2-bool-null-sentinel-cluster
129    /// (ADR-006 §2.7 carrier semantics + §2.7.5 producer-side stamp +
130    /// §2.7.7/Q9 parallel-kind track, 2026-05-19): introduced to fix
131    /// the §2.7 `(0u64, NativeKind::Bool)` null-sentinel ⇔ `false`
132    /// bool bit-pattern collision surfaced by W14.2-G6.
133    ///
134    /// Slot bits are unspecified and ignored — the kind alone carries
135    /// the absence signal. Producers use `0u64` by convention so
136    /// uninitialized stack-pad bytes (already zero per
137    /// `push_kinded_slow`) project to the same wire shape under
138    /// `NativeKind::Null` if accidentally observed (defense in depth).
139    ///
140    /// Clone/drop: no-op (no `Arc<T>` payload, no refcount work) —
141    /// mirrors the inline-scalar arm of `clone_with_kind` /
142    /// `drop_with_kind` per ADR-006 §2.7.7/Q9 + §2.7.6/Q8 dispatch
143    /// tables.
144    ///
145    /// Wire projection: `WireValue::Null` (mirrors the `NullableFloat64`
146    /// NaN-sentinel arm at `slot_to_wire`).
147    ///
148    /// `is_null_kinded`: returns `true` for any `(_, NativeKind::Null)`
149    /// pair regardless of bits — kind IS the discriminator per
150    /// §2.7.7/Q9 (not "kind PLUS bit pattern of data slot", which was
151    /// the pre-disposition shape that the §2.7 collision exposed as
152    /// unsound).
153    Null,
154    /// String reference (Arc<String> raw pointer)
155    String,
156    /// v2-raw `*const StringObj` carrier reference. ADR-006 §2.7.5
157    /// amendment (Wave 2 Agent B W12-StringV2-DecimalV2-NativeKind-additions,
158    /// 2026-05-14): new heap-pointer variant introduced for v2-raw
159    /// `Array<string>` element read paths per
160    /// `TypedArray<*const StringObj>` (Wave 2 §A2 producer migration).
161    /// Slot bits store `ptr as u64` where `ptr: *const StringObj` —
162    /// retain/release uses `v2_retain` / `v2_release` against the
163    /// `HeapHeader` at offset 0 of `StringObj` (NOT `Arc::increment_strong_count`
164    /// — `StringObj` is a manually-allocated `repr(C)` carrier with its
165    /// own refcount discipline per `v2/refcount.rs`, not an `Arc<String>`).
166    ///
167    /// **Parallel-discriminator note** (CLAUDE.md §Parallel-implementation
168    /// across producer/consumer carrier-shape boundaries): this variant
169    /// is a per-carrier-shape discriminator distinct from `NativeKind::String`
170    /// (`Arc<String>` carrier); the two are structurally distinct
171    /// (`StringObj` is a `repr(C)` 24-byte HeapHeader-equipped struct,
172    /// `Arc<String>` is a Rust-managed `Arc<T>` allocation). Mixing the
173    /// two carriers under the same NativeKind discriminator is the H-b
174    /// defection refused per the audit §H.2; the H-c decision (option
175    /// adopted at §H.4 + supervisor §P.1 ratification 2026-05-14) gives
176    /// each carrier its own NativeKind variant explicitly.
177    StringV2,
178    /// v2-raw `*const DecimalObj` carrier reference. ADR-006 §2.7.5
179    /// amendment (Wave 2 Agent B W12-StringV2-DecimalV2-NativeKind-additions,
180    /// 2026-05-14): new heap-pointer variant introduced for v2-raw
181    /// `Array<decimal>` element read paths per
182    /// `TypedArray<*const DecimalObj>` (Wave 2 §A2 producer migration).
183    /// Slot bits store `ptr as u64` where `ptr: *const DecimalObj` —
184    /// retain/release uses `v2_retain` / `v2_release` against the
185    /// `HeapHeader` at offset 0 of `DecimalObj` (NOT
186    /// `Arc::increment_strong_count` against an `Arc<rust_decimal::Decimal>`
187    /// — `DecimalObj` is a manually-allocated `repr(C)` carrier per
188    /// `v2/decimal_obj.rs` + `v2/refcount.rs`).
189    ///
190    /// **Parallel-discriminator note** (CLAUDE.md §Parallel-implementation
191    /// across producer/consumer carrier-shape boundaries): this variant
192    /// is a per-carrier-shape discriminator distinct from
193    /// `NativeKind::Ptr(HeapKind::Decimal)` (`Arc<rust_decimal::Decimal>`
194    /// carrier); the two are structurally distinct (`DecimalObj` is a
195    /// `repr(C)` 24-byte HeapHeader-equipped struct, `Arc<Decimal>` is
196    /// a Rust-managed `Arc<T>` allocation). Same H-c decision rationale
197    /// as `StringV2`.
198    DecimalV2,
199    /// Heap pointer (`Arc<HeapValue>` raw pointer) whose `HeapValue`
200    /// discriminant is `kind`. The marshal/wire/snapshot layer dispatches
201    /// on `kind` to project the slot to its typed shape — it does not
202    /// probe the heap object's self-reported discriminant in production
203    /// (`(*hv).kind() == kind` is a debug-only sanity check).
204    ///
205    /// Watchlist (`docs/defections.md` 2026-05-06 — HeapKind trim +
206    /// Ptr extension): do NOT add parametric `NativeKind::Result(..)`,
207    /// `NativeKind::Option(..)`, or `NativeKind::JsonValue` variants
208    /// when stdlib mass migration hits those returns. The strict-typed
209    /// answer is `HeapKind::TypedObject` plus a per-instantiation
210    /// schema_id from the function's registered `ConcreteType`. Adding
211    /// parametric NativeKind variants re-creates heterogeneous-by-default
212    /// sum types at the discriminator level — the same defection
213    /// pattern as the rejected `enum SlotValue { Int, Float, Bool, Heap }`.
214    ///
215    // ADR-005 names the general principle this watchlist applies at
216    // the proof layer: HeapValue is the single discriminator for
217    // heap-resident values; layers above take Arc<HeapValue> and dispatch
218    // on HeapValue::kind(). See docs/adr/005-typed-slot-construction.md.
219    Ptr(HeapKind),
220    // NativeKind::Dynamic and NativeKind::Unknown deleted by the strict-typing
221    // bulldozer (commit 128cb8a). There is no dynamic-typed slot. Every slot
222    // has a proven NativeKind at compile time or it's a compile error.
223    // Default impl also deleted — call sites must commit to a concrete
224    // kind, not rely on "Unknown means I haven't decided yet".
225}
226
227impl NativeKind {
228    #[inline]
229    pub fn is_integer(self) -> bool {
230        matches!(
231            self,
232            Self::Int8
233                | Self::UInt8
234                | Self::Int16
235                | Self::UInt16
236                | Self::Int32
237                | Self::UInt32
238                | Self::Int64
239                | Self::UInt64
240                | Self::IntSize
241                | Self::UIntSize
242        )
243    }
244
245    #[inline]
246    pub fn is_nullable_integer(self) -> bool {
247        matches!(
248            self,
249            Self::NullableInt8
250                | Self::NullableUInt8
251                | Self::NullableInt16
252                | Self::NullableUInt16
253                | Self::NullableInt32
254                | Self::NullableUInt32
255                | Self::NullableInt64
256                | Self::NullableUInt64
257                | Self::NullableIntSize
258                | Self::NullableUIntSize
259        )
260    }
261
262    #[inline]
263    pub fn is_integer_family(self) -> bool {
264        self.is_integer() || self.is_nullable_integer()
265    }
266
267    #[inline]
268    pub fn is_default_int_family(self) -> bool {
269        matches!(self, Self::Int64 | Self::NullableInt64)
270    }
271
272    #[inline]
273    pub fn is_float_family(self) -> bool {
274        // Round 19 S1.5 (2026-05-14): Float32 joins the floating
275        // family. No `NullableFloat32` sibling at this amendment per
276        // the scope-bounded "F32 + Char additions only" disposition.
277        matches!(self, Self::Float64 | Self::NullableFloat64 | Self::Float32)
278    }
279
280    /// Whether this is the `Char` scalar (per ADR-006 §2.7.5
281    /// amendment, Round 19 S1.5). Note: this does NOT include the
282    /// pre-amendment `NativeKind::Ptr(HeapKind::Char)` carrier label
283    /// — callers that want to recognize both shapes must check
284    /// `is_char_family()` instead. The scalar-only predicate exists
285    /// because `Char` is a non-heap scalar at the §Q8 carrier-API
286    /// layer (no `Arc<T>` payload, refcount-equivalent to other
287    /// 4-byte scalars).
288    #[inline]
289    pub fn is_char_scalar(self) -> bool {
290        matches!(self, Self::Char)
291    }
292
293    #[inline]
294    pub fn is_numeric_family(self) -> bool {
295        self.is_integer_family() || self.is_float_family()
296    }
297
298    #[inline]
299    pub fn is_pointer_sized_integer(self) -> bool {
300        matches!(
301            self,
302            Self::IntSize | Self::UIntSize | Self::NullableIntSize | Self::NullableUIntSize
303        )
304    }
305
306    #[inline]
307    pub fn is_signed_integer(self) -> Option<bool> {
308        if matches!(
309            self,
310            Self::Int8
311                | Self::NullableInt8
312                | Self::Int16
313                | Self::NullableInt16
314                | Self::Int32
315                | Self::NullableInt32
316                | Self::Int64
317                | Self::NullableInt64
318                | Self::IntSize
319                | Self::NullableIntSize
320        ) {
321            Some(true)
322        } else if matches!(
323            self,
324            Self::UInt8
325                | Self::NullableUInt8
326                | Self::UInt16
327                | Self::NullableUInt16
328                | Self::UInt32
329                | Self::NullableUInt32
330                | Self::UInt64
331                | Self::NullableUInt64
332                | Self::UIntSize
333                | Self::NullableUIntSize
334        ) {
335            Some(false)
336        } else {
337            None
338        }
339    }
340
341    #[inline]
342    pub fn integer_bit_width(self) -> Option<u16> {
343        match self {
344            Self::Int8 | Self::UInt8 | Self::NullableInt8 | Self::NullableUInt8 => Some(8),
345            Self::Int16 | Self::UInt16 | Self::NullableInt16 | Self::NullableUInt16 => Some(16),
346            Self::Int32 | Self::UInt32 | Self::NullableInt32 | Self::NullableUInt32 => Some(32),
347            Self::Int64 | Self::UInt64 | Self::NullableInt64 | Self::NullableUInt64 => Some(64),
348            Self::IntSize | Self::UIntSize | Self::NullableIntSize | Self::NullableUIntSize => {
349                Some(usize::BITS as u16)
350            }
351            _ => None,
352        }
353    }
354
355    /// Whether values of this kind carry a refcounted heap pointer.
356    ///
357    /// Post-strict-typing (ADR-006 §2.7.5 / §2.7.6 / Q8), the kind IS the
358    /// discriminator that decides refcount semantics — there is no
359    /// tag-bit probing. Heap kinds are `String` (Arc<String> raw pointer)
360    /// and `Ptr(HeapKind::*)` (Arc<HeapValue> raw pointer). All
361    /// numeric / bool kinds — including their nullable variants — are
362    /// raw scalars and do NOT carry a refcount, regardless of Cranelift
363    /// storage width (an `Int64` slot is a raw `i64`, not a NaN-boxed
364    /// ValueWord; the deleted ValueWord ABI is what made the W-series
365    /// `is_native_slot` predicate exclude Int64).
366    ///
367    /// Used by `shape-jit/src/mir_compiler/ownership.rs` to gate
368    /// `jit_arc_retain` / `jit_arc_release` emission. The kind-blind
369    /// fall-through ("if kind isn't proven, assume heap and retain")
370    /// the prior W-series MIR emitter took is forbidden under §2.7.7
371    /// #9 — when kind isn't proven, surface-and-stop is the principled
372    /// response, not a Bool-default-like silent retain.
373    #[inline]
374    pub fn is_refcounted(self) -> bool {
375        // Wave 2 Agent B (ADR-006 §2.7.5 amendment, 2026-05-14): StringV2
376        // / DecimalV2 are v2-raw heap-pointer carriers per the §H.4 H-c
377        // decision — refcount via `v2_retain` / `v2_release` against the
378        // HeapHeader at offset 0 of the StringObj / DecimalObj target.
379        matches!(
380            self,
381            Self::String | Self::StringV2 | Self::DecimalV2 | Self::Ptr(_)
382        )
383    }
384
385    #[inline]
386    pub fn non_nullable(self) -> Self {
387        match self {
388            Self::NullableFloat64 => Self::Float64,
389            Self::NullableInt8 => Self::Int8,
390            Self::NullableUInt8 => Self::UInt8,
391            Self::NullableInt16 => Self::Int16,
392            Self::NullableUInt16 => Self::UInt16,
393            Self::NullableInt32 => Self::Int32,
394            Self::NullableUInt32 => Self::UInt32,
395            Self::NullableInt64 => Self::Int64,
396            Self::NullableUInt64 => Self::UInt64,
397            Self::NullableIntSize => Self::IntSize,
398            Self::NullableUIntSize => Self::UIntSize,
399            other => other,
400        }
401    }
402
403    #[inline]
404    pub fn with_nullability(self, nullable: bool) -> Self {
405        if !nullable {
406            return self.non_nullable();
407        }
408        match self.non_nullable() {
409            Self::Float64 => Self::NullableFloat64,
410            Self::Int8 => Self::NullableInt8,
411            Self::UInt8 => Self::NullableUInt8,
412            Self::Int16 => Self::NullableInt16,
413            Self::UInt16 => Self::NullableUInt16,
414            Self::Int32 => Self::NullableInt32,
415            Self::UInt32 => Self::NullableUInt32,
416            Self::Int64 => Self::NullableInt64,
417            Self::UInt64 => Self::NullableUInt64,
418            Self::IntSize => Self::NullableIntSize,
419            Self::UIntSize => Self::NullableUIntSize,
420            other => other,
421        }
422    }
423
424    pub fn combine_integer_hints(lhs: Self, rhs: Self) -> Option<Self> {
425        let lhs_bits = lhs.integer_bit_width()?;
426        let rhs_bits = rhs.integer_bit_width()?;
427        let bits = lhs_bits.max(rhs_bits);
428        let signed = lhs.is_signed_integer()? || rhs.is_signed_integer()?;
429        let nullable = lhs.is_nullable_integer() || rhs.is_nullable_integer();
430        let keep_pointer_size = bits == usize::BITS as u16
431            && (lhs.is_pointer_sized_integer() || rhs.is_pointer_sized_integer());
432        let base = if keep_pointer_size {
433            if signed {
434                Self::IntSize
435            } else {
436                Self::UIntSize
437            }
438        } else {
439            match (bits, signed) {
440                (8, true) => Self::Int8,
441                (8, false) => Self::UInt8,
442                (16, true) => Self::Int16,
443                (16, false) => Self::UInt16,
444                (32, true) => Self::Int32,
445                (32, false) => Self::UInt32,
446                (64, true) => Self::Int64,
447                (64, false) => Self::UInt64,
448                _ => return None,
449            }
450        };
451        Some(base.with_nullability(nullable))
452    }
453}