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
113    Bool,
114    /// String reference (Arc<String> raw pointer)
115    String,
116    /// v2-raw `*const StringObj` carrier reference. ADR-006 §2.7.5
117    /// amendment (Wave 2 Agent B W12-StringV2-DecimalV2-NativeKind-additions,
118    /// 2026-05-14): new heap-pointer variant introduced for v2-raw
119    /// `Array<string>` element read paths per
120    /// `TypedArray<*const StringObj>` (Wave 2 §A2 producer migration).
121    /// Slot bits store `ptr as u64` where `ptr: *const StringObj` —
122    /// retain/release uses `v2_retain` / `v2_release` against the
123    /// `HeapHeader` at offset 0 of `StringObj` (NOT `Arc::increment_strong_count`
124    /// — `StringObj` is a manually-allocated `repr(C)` carrier with its
125    /// own refcount discipline per `v2/refcount.rs`, not an `Arc<String>`).
126    ///
127    /// **Parallel-discriminator note** (CLAUDE.md §Parallel-implementation
128    /// across producer/consumer carrier-shape boundaries): this variant
129    /// is a per-carrier-shape discriminator distinct from `NativeKind::String`
130    /// (`Arc<String>` carrier); the two are structurally distinct
131    /// (`StringObj` is a `repr(C)` 24-byte HeapHeader-equipped struct,
132    /// `Arc<String>` is a Rust-managed `Arc<T>` allocation). Mixing the
133    /// two carriers under the same NativeKind discriminator is the H-b
134    /// defection refused per the audit §H.2; the H-c decision (option
135    /// adopted at §H.4 + supervisor §P.1 ratification 2026-05-14) gives
136    /// each carrier its own NativeKind variant explicitly.
137    StringV2,
138    /// v2-raw `*const DecimalObj` carrier reference. ADR-006 §2.7.5
139    /// amendment (Wave 2 Agent B W12-StringV2-DecimalV2-NativeKind-additions,
140    /// 2026-05-14): new heap-pointer variant introduced for v2-raw
141    /// `Array<decimal>` element read paths per
142    /// `TypedArray<*const DecimalObj>` (Wave 2 §A2 producer migration).
143    /// Slot bits store `ptr as u64` where `ptr: *const DecimalObj` —
144    /// retain/release uses `v2_retain` / `v2_release` against the
145    /// `HeapHeader` at offset 0 of `DecimalObj` (NOT
146    /// `Arc::increment_strong_count` against an `Arc<rust_decimal::Decimal>`
147    /// — `DecimalObj` is a manually-allocated `repr(C)` carrier per
148    /// `v2/decimal_obj.rs` + `v2/refcount.rs`).
149    ///
150    /// **Parallel-discriminator note** (CLAUDE.md §Parallel-implementation
151    /// across producer/consumer carrier-shape boundaries): this variant
152    /// is a per-carrier-shape discriminator distinct from
153    /// `NativeKind::Ptr(HeapKind::Decimal)` (`Arc<rust_decimal::Decimal>`
154    /// carrier); the two are structurally distinct (`DecimalObj` is a
155    /// `repr(C)` 24-byte HeapHeader-equipped struct, `Arc<Decimal>` is
156    /// a Rust-managed `Arc<T>` allocation). Same H-c decision rationale
157    /// as `StringV2`.
158    DecimalV2,
159    /// Heap pointer (`Arc<HeapValue>` raw pointer) whose `HeapValue`
160    /// discriminant is `kind`. The marshal/wire/snapshot layer dispatches
161    /// on `kind` to project the slot to its typed shape — it does not
162    /// probe the heap object's self-reported discriminant in production
163    /// (`(*hv).kind() == kind` is a debug-only sanity check).
164    ///
165    /// Watchlist (`docs/defections.md` 2026-05-06 — HeapKind trim +
166    /// Ptr extension): do NOT add parametric `NativeKind::Result(..)`,
167    /// `NativeKind::Option(..)`, or `NativeKind::JsonValue` variants
168    /// when stdlib mass migration hits those returns. The strict-typed
169    /// answer is `HeapKind::TypedObject` plus a per-instantiation
170    /// schema_id from the function's registered `ConcreteType`. Adding
171    /// parametric NativeKind variants re-creates heterogeneous-by-default
172    /// sum types at the discriminator level — the same defection
173    /// pattern as the rejected `enum SlotValue { Int, Float, Bool, Heap }`.
174    ///
175    // ADR-005 names the general principle this watchlist applies at
176    // the proof layer: HeapValue is the single discriminator for
177    // heap-resident values; layers above take Arc<HeapValue> and dispatch
178    // on HeapValue::kind(). See docs/adr/005-typed-slot-construction.md.
179    Ptr(HeapKind),
180    // NativeKind::Dynamic and NativeKind::Unknown deleted by the strict-typing
181    // bulldozer (commit 128cb8a). There is no dynamic-typed slot. Every slot
182    // has a proven NativeKind at compile time or it's a compile error.
183    // Default impl also deleted — call sites must commit to a concrete
184    // kind, not rely on "Unknown means I haven't decided yet".
185}
186
187impl NativeKind {
188    #[inline]
189    pub fn is_integer(self) -> bool {
190        matches!(
191            self,
192            Self::Int8
193                | Self::UInt8
194                | Self::Int16
195                | Self::UInt16
196                | Self::Int32
197                | Self::UInt32
198                | Self::Int64
199                | Self::UInt64
200                | Self::IntSize
201                | Self::UIntSize
202        )
203    }
204
205    #[inline]
206    pub fn is_nullable_integer(self) -> bool {
207        matches!(
208            self,
209            Self::NullableInt8
210                | Self::NullableUInt8
211                | Self::NullableInt16
212                | Self::NullableUInt16
213                | Self::NullableInt32
214                | Self::NullableUInt32
215                | Self::NullableInt64
216                | Self::NullableUInt64
217                | Self::NullableIntSize
218                | Self::NullableUIntSize
219        )
220    }
221
222    #[inline]
223    pub fn is_integer_family(self) -> bool {
224        self.is_integer() || self.is_nullable_integer()
225    }
226
227    #[inline]
228    pub fn is_default_int_family(self) -> bool {
229        matches!(self, Self::Int64 | Self::NullableInt64)
230    }
231
232    #[inline]
233    pub fn is_float_family(self) -> bool {
234        // Round 19 S1.5 (2026-05-14): Float32 joins the floating
235        // family. No `NullableFloat32` sibling at this amendment per
236        // the scope-bounded "F32 + Char additions only" disposition.
237        matches!(self, Self::Float64 | Self::NullableFloat64 | Self::Float32)
238    }
239
240    /// Whether this is the `Char` scalar (per ADR-006 §2.7.5
241    /// amendment, Round 19 S1.5). Note: this does NOT include the
242    /// pre-amendment `NativeKind::Ptr(HeapKind::Char)` carrier label
243    /// — callers that want to recognize both shapes must check
244    /// `is_char_family()` instead. The scalar-only predicate exists
245    /// because `Char` is a non-heap scalar at the §Q8 carrier-API
246    /// layer (no `Arc<T>` payload, refcount-equivalent to other
247    /// 4-byte scalars).
248    #[inline]
249    pub fn is_char_scalar(self) -> bool {
250        matches!(self, Self::Char)
251    }
252
253    #[inline]
254    pub fn is_numeric_family(self) -> bool {
255        self.is_integer_family() || self.is_float_family()
256    }
257
258    #[inline]
259    pub fn is_pointer_sized_integer(self) -> bool {
260        matches!(
261            self,
262            Self::IntSize | Self::UIntSize | Self::NullableIntSize | Self::NullableUIntSize
263        )
264    }
265
266    #[inline]
267    pub fn is_signed_integer(self) -> Option<bool> {
268        if matches!(
269            self,
270            Self::Int8
271                | Self::NullableInt8
272                | Self::Int16
273                | Self::NullableInt16
274                | Self::Int32
275                | Self::NullableInt32
276                | Self::Int64
277                | Self::NullableInt64
278                | Self::IntSize
279                | Self::NullableIntSize
280        ) {
281            Some(true)
282        } else if matches!(
283            self,
284            Self::UInt8
285                | Self::NullableUInt8
286                | Self::UInt16
287                | Self::NullableUInt16
288                | Self::UInt32
289                | Self::NullableUInt32
290                | Self::UInt64
291                | Self::NullableUInt64
292                | Self::UIntSize
293                | Self::NullableUIntSize
294        ) {
295            Some(false)
296        } else {
297            None
298        }
299    }
300
301    #[inline]
302    pub fn integer_bit_width(self) -> Option<u16> {
303        match self {
304            Self::Int8 | Self::UInt8 | Self::NullableInt8 | Self::NullableUInt8 => Some(8),
305            Self::Int16 | Self::UInt16 | Self::NullableInt16 | Self::NullableUInt16 => Some(16),
306            Self::Int32 | Self::UInt32 | Self::NullableInt32 | Self::NullableUInt32 => Some(32),
307            Self::Int64 | Self::UInt64 | Self::NullableInt64 | Self::NullableUInt64 => Some(64),
308            Self::IntSize | Self::UIntSize | Self::NullableIntSize | Self::NullableUIntSize => {
309                Some(usize::BITS as u16)
310            }
311            _ => None,
312        }
313    }
314
315    /// Whether values of this kind carry a refcounted heap pointer.
316    ///
317    /// Post-strict-typing (ADR-006 §2.7.5 / §2.7.6 / Q8), the kind IS the
318    /// discriminator that decides refcount semantics — there is no
319    /// tag-bit probing. Heap kinds are `String` (Arc<String> raw pointer)
320    /// and `Ptr(HeapKind::*)` (Arc<HeapValue> raw pointer). All
321    /// numeric / bool kinds — including their nullable variants — are
322    /// raw scalars and do NOT carry a refcount, regardless of Cranelift
323    /// storage width (an `Int64` slot is a raw `i64`, not a NaN-boxed
324    /// ValueWord; the deleted ValueWord ABI is what made the W-series
325    /// `is_native_slot` predicate exclude Int64).
326    ///
327    /// Used by `shape-jit/src/mir_compiler/ownership.rs` to gate
328    /// `jit_arc_retain` / `jit_arc_release` emission. The kind-blind
329    /// fall-through ("if kind isn't proven, assume heap and retain")
330    /// the prior W-series MIR emitter took is forbidden under §2.7.7
331    /// #9 — when kind isn't proven, surface-and-stop is the principled
332    /// response, not a Bool-default-like silent retain.
333    #[inline]
334    pub fn is_refcounted(self) -> bool {
335        // Wave 2 Agent B (ADR-006 §2.7.5 amendment, 2026-05-14): StringV2
336        // / DecimalV2 are v2-raw heap-pointer carriers per the §H.4 H-c
337        // decision — refcount via `v2_retain` / `v2_release` against the
338        // HeapHeader at offset 0 of the StringObj / DecimalObj target.
339        matches!(
340            self,
341            Self::String | Self::StringV2 | Self::DecimalV2 | Self::Ptr(_)
342        )
343    }
344
345    #[inline]
346    pub fn non_nullable(self) -> Self {
347        match self {
348            Self::NullableFloat64 => Self::Float64,
349            Self::NullableInt8 => Self::Int8,
350            Self::NullableUInt8 => Self::UInt8,
351            Self::NullableInt16 => Self::Int16,
352            Self::NullableUInt16 => Self::UInt16,
353            Self::NullableInt32 => Self::Int32,
354            Self::NullableUInt32 => Self::UInt32,
355            Self::NullableInt64 => Self::Int64,
356            Self::NullableUInt64 => Self::UInt64,
357            Self::NullableIntSize => Self::IntSize,
358            Self::NullableUIntSize => Self::UIntSize,
359            other => other,
360        }
361    }
362
363    #[inline]
364    pub fn with_nullability(self, nullable: bool) -> Self {
365        if !nullable {
366            return self.non_nullable();
367        }
368        match self.non_nullable() {
369            Self::Float64 => Self::NullableFloat64,
370            Self::Int8 => Self::NullableInt8,
371            Self::UInt8 => Self::NullableUInt8,
372            Self::Int16 => Self::NullableInt16,
373            Self::UInt16 => Self::NullableUInt16,
374            Self::Int32 => Self::NullableInt32,
375            Self::UInt32 => Self::NullableUInt32,
376            Self::Int64 => Self::NullableInt64,
377            Self::UInt64 => Self::NullableUInt64,
378            Self::IntSize => Self::NullableIntSize,
379            Self::UIntSize => Self::NullableUIntSize,
380            other => other,
381        }
382    }
383
384    pub fn combine_integer_hints(lhs: Self, rhs: Self) -> Option<Self> {
385        let lhs_bits = lhs.integer_bit_width()?;
386        let rhs_bits = rhs.integer_bit_width()?;
387        let bits = lhs_bits.max(rhs_bits);
388        let signed = lhs.is_signed_integer()? || rhs.is_signed_integer()?;
389        let nullable = lhs.is_nullable_integer() || rhs.is_nullable_integer();
390        let keep_pointer_size = bits == usize::BITS as u16
391            && (lhs.is_pointer_sized_integer() || rhs.is_pointer_sized_integer());
392        let base = if keep_pointer_size {
393            if signed {
394                Self::IntSize
395            } else {
396                Self::UIntSize
397            }
398        } else {
399            match (bits, signed) {
400                (8, true) => Self::Int8,
401                (8, false) => Self::UInt8,
402                (16, true) => Self::Int16,
403                (16, false) => Self::UInt16,
404                (32, true) => Self::Int32,
405                (32, false) => Self::UInt32,
406                (64, true) => Self::Int64,
407                (64, false) => Self::UInt64,
408                _ => return None,
409            }
410        };
411        Some(base.with_nullability(nullable))
412    }
413}