shape_jit/ffi/stack_kind_code.rs
1//! JIT-side parallel-kind track encoding (ADR-006 §2.7.7 / Q9).
2//!
3//! This module is the JIT-tier analog of the VM-side
4//! `Vec<NativeKind>` parallel track on `crates/shape-vm/src/executor/vm_impl/
5//! stack.rs::VmStack`. The JIT stores its parallel kind track as a
6//! contiguous `[u8; 512]` array (`JITContext.stack_kinds`) so Cranelift
7//! codegen can write a kind in lockstep with each data push via a plain
8//! `iconst(types::I8, code)` + byte store, mirroring the existing 8-byte
9//! data store at `JITContext.stack[i]`.
10//!
11//! The encoding is the §2.7.5 cross-crate stable-FFI raw-pair shape: each
12//! `NativeKind` value (including all `Ptr(HeapKind::*)` arms) maps to a
13//! single byte. The producing site stamps the code from the call signature
14//! at JIT-compile time; the consuming FFI side decodes the byte back to
15//! `NativeKind` and dispatches per ADR-006 §2.7.6 / Q8. No tag-bit decode
16//! on the raw u64, no `is_heap()` probe — the kind IS the discriminator.
17//!
18//! Forbidden under §2.7.7:
19//!
20//! - `Option<NativeKind>` in the track — every push has a known kind from
21//! the producing call signature; if the kind genuinely isn't known the
22//! answer is `surface-and-stop`, not `None`. The `SENTINEL` value is the
23//! uninitialized-slot pattern set by `JITContext::default()`; reading it
24//! at a pop site is a kind-source gap (forbidden #9).
25//! - `Vec<KindedSlot>` for the stack — same §2.7.5 rule the VM-side rules
26//! out; storage stays raw u64 + parallel byte track.
27//! - 16-byte slot — would conflict with the §2.1 8-byte invariant.
28//! - Encoding `Ptr(HeapKind)` arms via a tag-bit probe on the slot's u64 —
29//! the deleted ValueWord `tag_bits` dispatch (CLAUDE.md "Forbidden
30//! Patterns" #4). Kind comes from the producing call signature, period.
31
32use shape_value::{HeapKind, NativeKind};
33
34/// Sentinel byte for uninitialized stack slots (matches
35/// `JITContext::default()` initialization). Reading this at a pop site is
36/// a kind-source gap per ADR-006 §2.7.7 #9 — surface, do not Bool-default.
37pub const SENTINEL: u8 = 255;
38
39// ── Scalar / nullable scalar codes (1-byte tag, no payload) ────────────
40// Codes 0..63 are reserved for non-Ptr `NativeKind` variants. Codes
41// 128..(128 + HeapKind ordinals) are `Ptr(HeapKind)` arms — same shape as
42// the §2.7.6 / Q8 dispatch table's "heap arm range" convention but encoded
43// as a single byte for the parallel track.
44//
45// The decoder dispatches by range first: `< 128` selects a scalar arm,
46// `>= 128 && < 255` selects a `Ptr(HeapKind)` arm via
47// `(code - 128) as HeapKind`. 255 is `SENTINEL`.
48
49pub const C_FLOAT64: u8 = 0;
50pub const C_NULLABLE_FLOAT64: u8 = 1;
51pub const C_INT8: u8 = 2;
52pub const C_NULLABLE_INT8: u8 = 3;
53pub const C_UINT8: u8 = 4;
54pub const C_NULLABLE_UINT8: u8 = 5;
55pub const C_INT16: u8 = 6;
56pub const C_NULLABLE_INT16: u8 = 7;
57pub const C_UINT16: u8 = 8;
58pub const C_NULLABLE_UINT16: u8 = 9;
59pub const C_INT32: u8 = 10;
60pub const C_NULLABLE_INT32: u8 = 11;
61pub const C_UINT32: u8 = 12;
62pub const C_NULLABLE_UINT32: u8 = 13;
63pub const C_INT64: u8 = 14;
64pub const C_NULLABLE_INT64: u8 = 15;
65pub const C_UINT64: u8 = 16;
66pub const C_NULLABLE_UINT64: u8 = 17;
67pub const C_INTSIZE: u8 = 18;
68pub const C_NULLABLE_INTSIZE: u8 = 19;
69pub const C_UINTSIZE: u8 = 20;
70pub const C_NULLABLE_UINTSIZE: u8 = 21;
71pub const C_BOOL: u8 = 22;
72pub const C_STRING: u8 = 23;
73// Round 19 S1.5 W12-nativekind-scalar-additions (2026-05-14):
74// ADR-006 §2.7.5 amendment adds F32 + Char as 4-byte scalar variants.
75// Codes allocated contiguously at the next free scalar slots (24, 25);
76// still well below `PTR_BASE = 128`.
77pub const C_FLOAT32: u8 = 24;
78pub const C_CHAR: u8 = 25;
79// Wave 2 Agent B W12-StringV2-DecimalV2-NativeKind-additions (2026-05-14):
80// ADR-006 §2.7.5 amendment adds StringV2 + DecimalV2 as v2-raw heap-pointer
81// scalar codes for `Array<string>` / `Array<decimal>` element read paths.
82// Codes allocated contiguously after Round 19 S1.5 F32 + Char (24, 25); still
83// well below `PTR_BASE = 128`. The codes carry the v2-raw carrier-shape
84// discrimination at the JIT FFI parallel-kind track per §2.7.7 / Q9.
85pub const C_STRING_V2: u8 = 26;
86pub const C_DECIMAL_V2: u8 = 27;
87// R5b-2-bool-null-sentinel-cluster (ADR-006 §2.7 + §2.7.5 + §2.7.7/Q9,
88// 2026-05-19): `NativeKind::Null` is the canonical absence-of-value
89// sentinel discriminator. Code allocated contiguously after Wave 2's
90// StringV2 / DecimalV2 (26, 27); still well below `PTR_BASE = 128`.
91pub const C_NULL: u8 = 28;
92
93/// Base code for `Ptr(HeapKind::*)` arms. Each `HeapKind` ordinal is
94/// added to `PTR_BASE` to produce the byte. With HeapKind ordinals 0..=28
95/// today the Ptr-range is `128..=156`, well clear of the scalar codes
96/// `0..=23` and the `SENTINEL = 255`.
97pub const PTR_BASE: u8 = 128;
98
99/// Encode a `NativeKind` as its single-byte parallel-track code.
100///
101/// Stamped at JIT-compile time from the producing call signature
102/// (`MIR::infer_slot_kinds` / `operand_slot_kind` / similar §2.7.5
103/// kind-source paths). Never derived from a raw u64 bit pattern — the
104/// `Ptr(HeapKind)` arms encode the producing-site kind, not a runtime tag
105/// probe.
106#[inline]
107pub const fn encode(kind: NativeKind) -> u8 {
108 match kind {
109 NativeKind::Float64 => C_FLOAT64,
110 NativeKind::NullableFloat64 => C_NULLABLE_FLOAT64,
111 NativeKind::Int8 => C_INT8,
112 NativeKind::NullableInt8 => C_NULLABLE_INT8,
113 NativeKind::UInt8 => C_UINT8,
114 NativeKind::NullableUInt8 => C_NULLABLE_UINT8,
115 NativeKind::Int16 => C_INT16,
116 NativeKind::NullableInt16 => C_NULLABLE_INT16,
117 NativeKind::UInt16 => C_UINT16,
118 NativeKind::NullableUInt16 => C_NULLABLE_UINT16,
119 NativeKind::Int32 => C_INT32,
120 NativeKind::NullableInt32 => C_NULLABLE_INT32,
121 NativeKind::UInt32 => C_UINT32,
122 NativeKind::NullableUInt32 => C_NULLABLE_UINT32,
123 NativeKind::Int64 => C_INT64,
124 NativeKind::NullableInt64 => C_NULLABLE_INT64,
125 NativeKind::UInt64 => C_UINT64,
126 NativeKind::NullableUInt64 => C_NULLABLE_UINT64,
127 NativeKind::IntSize => C_INTSIZE,
128 NativeKind::NullableIntSize => C_NULLABLE_INTSIZE,
129 NativeKind::UIntSize => C_UINTSIZE,
130 NativeKind::NullableUIntSize => C_NULLABLE_UINTSIZE,
131 NativeKind::Bool => C_BOOL,
132 NativeKind::String => C_STRING,
133 // Round 19 S1.5 W12-nativekind-scalar-additions (2026-05-14).
134 NativeKind::Float32 => C_FLOAT32,
135 NativeKind::Char => C_CHAR,
136 // Wave 2 Agent B W12-StringV2-DecimalV2-NativeKind-additions
137 // (2026-05-14).
138 NativeKind::StringV2 => C_STRING_V2,
139 NativeKind::DecimalV2 => C_DECIMAL_V2,
140 // R5b-2-bool-null-sentinel-cluster (ADR-006 §2.7 + §2.7.7/Q9,
141 // 2026-05-19): canonical absence-of-value discriminator code.
142 NativeKind::Null => C_NULL,
143 NativeKind::Ptr(hk) => PTR_BASE.wrapping_add(hk as u8),
144 }
145}
146
147/// Decode a single-byte parallel-track code back to its `NativeKind`.
148///
149/// Returns `None` for `SENTINEL` (uninitialized) and any reserved code
150/// not in the encoded range — both indicate a kind-source gap and the
151/// consumer must surface, not Bool-default (ADR-006 §2.7.7 #9).
152#[inline]
153pub fn decode(code: u8) -> Option<NativeKind> {
154 match code {
155 SENTINEL => None,
156 C_FLOAT64 => Some(NativeKind::Float64),
157 C_NULLABLE_FLOAT64 => Some(NativeKind::NullableFloat64),
158 C_INT8 => Some(NativeKind::Int8),
159 C_NULLABLE_INT8 => Some(NativeKind::NullableInt8),
160 C_UINT8 => Some(NativeKind::UInt8),
161 C_NULLABLE_UINT8 => Some(NativeKind::NullableUInt8),
162 C_INT16 => Some(NativeKind::Int16),
163 C_NULLABLE_INT16 => Some(NativeKind::NullableInt16),
164 C_UINT16 => Some(NativeKind::UInt16),
165 C_NULLABLE_UINT16 => Some(NativeKind::NullableUInt16),
166 C_INT32 => Some(NativeKind::Int32),
167 C_NULLABLE_INT32 => Some(NativeKind::NullableInt32),
168 C_UINT32 => Some(NativeKind::UInt32),
169 C_NULLABLE_UINT32 => Some(NativeKind::NullableUInt32),
170 C_INT64 => Some(NativeKind::Int64),
171 C_NULLABLE_INT64 => Some(NativeKind::NullableInt64),
172 C_UINT64 => Some(NativeKind::UInt64),
173 C_NULLABLE_UINT64 => Some(NativeKind::NullableUInt64),
174 C_INTSIZE => Some(NativeKind::IntSize),
175 C_NULLABLE_INTSIZE => Some(NativeKind::NullableIntSize),
176 C_UINTSIZE => Some(NativeKind::UIntSize),
177 C_NULLABLE_UINTSIZE => Some(NativeKind::NullableUIntSize),
178 C_BOOL => Some(NativeKind::Bool),
179 C_STRING => Some(NativeKind::String),
180 // Round 19 S1.5 W12-nativekind-scalar-additions (2026-05-14).
181 C_FLOAT32 => Some(NativeKind::Float32),
182 C_CHAR => Some(NativeKind::Char),
183 // Wave 2 Agent B W12-StringV2-DecimalV2-NativeKind-additions
184 // (2026-05-14).
185 C_STRING_V2 => Some(NativeKind::StringV2),
186 C_DECIMAL_V2 => Some(NativeKind::DecimalV2),
187 // R5b-2-bool-null-sentinel-cluster (ADR-006 §2.7 + §2.7.7/Q9,
188 // 2026-05-19): canonical absence-of-value discriminator code.
189 C_NULL => Some(NativeKind::Null),
190 c if c >= PTR_BASE && c < SENTINEL => {
191 decode_heap_kind(c - PTR_BASE).map(NativeKind::Ptr)
192 }
193 _ => None,
194 }
195}
196
197/// Decode a `HeapKind` ordinal byte back to its variant. Mirror of the
198/// encoding side — must stay in lockstep with `heap_variants.rs`'s
199/// `#[repr(u8)]` ordinal table.
200#[inline]
201fn decode_heap_kind(ord: u8) -> Option<HeapKind> {
202 // Safe transmute is not available for non-exhaustive enum ordinals;
203 // do an explicit dispatch matching `crates/shape-value/src/heap_variants.rs`.
204 Some(match ord {
205 0 => HeapKind::String,
206 1 => HeapKind::TypedObject,
207 2 => HeapKind::Closure,
208 3 => HeapKind::Decimal,
209 4 => HeapKind::BigInt,
210 5 => HeapKind::DataTable,
211 6 => HeapKind::Future,
212 7 => HeapKind::TaskGroup,
213 8 => HeapKind::TypedArray,
214 9 => HeapKind::Temporal,
215 10 => HeapKind::TableView,
216 11 => HeapKind::Content,
217 12 => HeapKind::Instant,
218 13 => HeapKind::IoHandle,
219 14 => HeapKind::NativeScalar,
220 15 => HeapKind::NativeView,
221 16 => HeapKind::Char,
222 17 => HeapKind::HashMap,
223 18 => HeapKind::FilterExpr,
224 19 => HeapKind::Reference,
225 20 => HeapKind::SharedCell,
226 21 => HeapKind::HashSet,
227 22 => HeapKind::Iterator,
228 23 => HeapKind::Deque,
229 24 => HeapKind::Channel,
230 25 => HeapKind::PriorityQueue,
231 26 => HeapKind::Range,
232 27 => HeapKind::Result,
233 28 => HeapKind::Option,
234 29 => HeapKind::TraitObject,
235 30 => HeapKind::Mutex,
236 31 => HeapKind::Atomic,
237 32 => HeapKind::Lazy,
238 33 => HeapKind::ModuleFn,
239 _ => return None,
240 })
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn scalar_roundtrip() {
249 for kind in [
250 NativeKind::Float64,
251 NativeKind::Int64,
252 NativeKind::UInt64,
253 NativeKind::Bool,
254 NativeKind::String,
255 NativeKind::Int32,
256 NativeKind::NullableInt64,
257 ] {
258 let code = encode(kind);
259 assert!(code < PTR_BASE, "scalar code {} must be < PTR_BASE", code);
260 assert_eq!(decode(code), Some(kind));
261 }
262 }
263
264 #[test]
265 fn ptr_roundtrip() {
266 for hk in [
267 HeapKind::Closure,
268 HeapKind::TypedArray,
269 HeapKind::TypedObject,
270 HeapKind::HashMap,
271 HeapKind::Result,
272 HeapKind::Option,
273 HeapKind::String,
274 ] {
275 let kind = NativeKind::Ptr(hk);
276 let code = encode(kind);
277 assert!(
278 code >= PTR_BASE && code < SENTINEL,
279 "Ptr code {} must be in [PTR_BASE, SENTINEL)",
280 code
281 );
282 assert_eq!(decode(code), Some(kind));
283 }
284 }
285
286 #[test]
287 fn sentinel_decodes_to_none() {
288 assert_eq!(decode(SENTINEL), None);
289 }
290}