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}