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}