subetha-cxc 0.1.12

MMF-backed cross-process IPC primitives for SubEtha: SharedRing, SharedHashMap, SharedRWLock, SharedSemaphore, SharedLRUCache, OwnerLease, HeartbeatTable, plus 30+ more. One byte layout serves cross-thread, cross-process, and disk-persistent.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# SharedNaNValue

![Rust](https://img.shields.io/badge/Rust-1.96+-orange?logo=rust)
![Edition](https://img.shields.io/badge/Edition-2024-blue)
![Slot Size](https://img.shields.io/badge/SharedNaNValue-8_bytes-success)
![Variants](https://img.shields.io/badge/variants-f64_%7C_i32_%7C_u32_%7C_bool_%7C_nil_%7C_OffsetPtr-informational)
![Scope](https://img.shields.io/badge/Scope-cross--process-brightgreen)

A 64-bit NaN-boxed value that packs `f64 | i32 | u32 | bool | nil
| OffsetPtr<T>` into a single `u64` slot via the IEEE-754 NaN bit
patterns the FPU never produces during normal computation. Same
shape as V8 / SpiderMonkey NaN boxing but cross-process safe:
the `OffsetPtr` variant carries a 32-bit INDEX into a
`SharedRegion`, not a virtual address, so the same `u64` bit
pattern resolves to the same logical pointer in every process
that has the region mapped.

> **The "8-byte heterogeneous value slot" primitive.** Rust's
> idiomatic alternative is `enum { F64(f64), I32(i32), ... }`
> which pays a 1-byte discriminant + alignment padding = 16
> bytes per slot (the bench measures it at exactly 2x larger).
> Storing 1 M values: NaN-boxed = 8 MiB, enum = 16 MiB. The
> storage density is the architectural lever for cross-process
> shared collections.

**Constraints (read first):**

- **Boxed-prefix `0xFFF8_0000_0000_0000`.** Sign bit 1 + all-ones
  exponent + quiet-NaN bit. The FPU's normal-computation NaNs use
  sign 0, so the prefix doesn't collide; any NaN passed to
  `from_f64` is CANONICALISED to the positive canonical qNaN
  (`0x7FF8_0000_0000_0000`) so the stored bits never look boxed.
- **3-bit tag field (8 slots; 6 used, 2 reserved).**
  Adding a new tag value requires reserving one of the
  remaining slots; no expansion path beyond 8.
- **48-bit payload.** `i32` and `u32` use 32 bits;
  `OffsetPtr` uses 32 bits (max region size = 4 G slots);
  `bool` uses 1 bit; remaining payload bits are zero.
- **`OffsetPtr<T>` is type-erased in the slot.** The `T`
  parameter is dropped at construction; callers
  reconstruct any `T` at extraction time. This is what makes the
  slot uniform-size across heterogeneous payloads but it
  defeats compile-time type safety on the pointer.
- **No payload type check on extraction.**
  `as_i32`, `as_f64`, etc. return `None` if the tag doesn't
  match. The caller is responsible for knowing what they stored.
- **`from_f64(NaN)` is lossy.** Any NaN input
  (positive, negative, signaling, quiet, with specific payload
  bits) is collapsed to `CANONICAL_QNAN`. The original NaN bit
  pattern is NOT recoverable.
- **`#[repr(C)]` 8-byte slot.** Layout-stable for
  cross-process bit patterns.
- **Boolean payload uses only the lowest bit**.
  Other bits in the 48-bit payload area are zero. If you
  construct a SharedNaNValue from raw bits with tag=bool and
  non-zero payload, `as_bool()` returns based on bit 0 only.
- **Reserved tags 6 and 7 are present**.
  Construct via `from_raw(pack(6, 0))`; `type_tag()` returns
  `NaNValueType::Reserved(6)`. No accessor exists today; reserved
  for caller-defined extensions or later-added tagged variants.

---

## Table of contents

- [What it is]#what-it-is
- [The NaN-boxing encoding]#the-nan-boxing-encoding
- [Tag values]#tag-values
- [Layout]#layout
- [API at a glance]#api-at-a-glance
- [Worked examples]#worked-examples
- [Benchmark results]#benchmark-results
- [Use case patterns]#use-case-patterns
- [Known limitations (verified)]#known-limitations-verified
- [Common pitfalls]#common-pitfalls

---

## What it is

```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub struct SharedNaNValue {
    raw: u64,
}
```

One `u64` slot. The high 13 bits act as the discriminator: if they
match the boxed-prefix `0xFFF8_0000_0000_0000`, the next 3 bits
hold the tag (`nil | i32 | u32 | bool | OffsetPtr | reserved`) and
the low 48 bits hold the payload. If the high 13 bits do NOT match
the prefix, the whole `u64` is interpreted as an IEEE-754 `f64`
bit pattern.

The 8-byte size is the storage-density win. Rust's enum
alternative is 16 bytes (1-byte discriminant + padding to
8-byte-align the f64 variant). The bench measures both at exactly
those sizes.

---

## The NaN-boxing encoding

IEEE-754 binary64 reserves all bit patterns with `exponent =
0x7FF` and any nonzero mantissa for NaN values. There are 2^52 - 1
distinct NaN bit patterns, roughly 4.5e15 of them. Real FPUs only
generate two canonical forms (positive qNaN, negative qNaN) plus
the all-zero-payload variants. Every other NaN bit pattern is
"unused" by normal float math.

NaN-boxing claims those unused patterns to store typed payloads:

```text
bit  63        bit 51   bits 50-48   bits 47-0
  +-------+-----------+------------+----------------+
  | sign  | exp 0x7FF | qNaN | tag |    payload     |
  | =1    | (all 1s)  | =1   | (3) |     (48)       |
  +-------+-----------+------------+----------------+
```

The prefix `sign=1 + exp=0x7FF + qNaN=1` is
`0xFFF8_0000_0000_0000`. Real-computation NaNs use `sign=0`, so
they don't collide with the boxed prefix. To be safe against
caller-supplied NaNs (e.g. from manual
`f64::from_bits(0xFFF8_FFFF_FFFF_FFFF)`), `from_f64(NaN)`
canonicalises the bit pattern to `0x7FF8_0000_0000_0000`
(positive canonical qNaN, NOT boxed).

`✶ Insight ────────────────────────────────`

The NaN-box pattern is what makes interpreters like V8 and
SpiderMonkey efficient: every JavaScript value (number / string
ptr / object ptr / undefined / null / boolean) fits in 8 bytes.
Adding cross-process semantics is the additional move:
SharedNaNValue stores INDICES (OffsetPtr) instead of virtual
addresses, so the 8-byte bit pattern is portable across
processes that share the SharedRegion.

`──────────────────────────────────────────`

---

## Tag values

| Tag | Meaning | Payload encoding |
|---|---|---|
| 0 | `Nil` | payload bits ignored (all 0) |
| 1 | `I32` | low 32 bits |
| 2 | `U32` | low 32 bits |
| 3 | `Bool` | low 1 bit (0 = false, 1 = true) |
| 4 | `OffsetPtr` | low 32 bits = SharedRegion index |
| 5 | reserved (TaggedOffsetPtr) | - |
| 6, 7 | reserved | - |

Three bits = 8 values; 6 are used today, 2 stay reserved as
caller-defined extension slots. The `NaNValueType::Reserved(u64)`
variant exposes the raw tag if you query a slot with a reserved
tag, so existing readers don't crash on tags they don't
understand.

---

## Layout

```mermaid
flowchart LR
    A[raw u64] --> B{high 13 bits = boxed prefix}
    B -->|no| F[interpret as f64 via from_bits]
    B -->|yes| T[extract tag bits 48-50]
    T --> P[payload bits 0-47]
    T --> N{tag value}
    N -->|0| N0[Nil]
    N -->|1| N1[I32 payload as i32]
    N -->|2| N2[U32 payload as u32]
    N -->|3| N3[Bool payload bit 0]
    N -->|4| N4[OffsetPtr index]
    N -->|5,6,7| NR[Reserved tag]
    classDef store fill:#dceefb,stroke:#1f4e79,color:#000
    classDef test fill:#fff2cc,stroke:#7f6000,color:#000
    classDef variant fill:#d5e8d4,stroke:#2d5d2d,color:#000
    classDef reserved fill:#f8cecc,stroke:#a52626,color:#000
    class A store
    class B,N test
    class F,N0,N1,N2,N3,N4,T,P variant
    class NR reserved
```

`#[repr(C)]` 8-byte slot; `Vec<SharedNaNValue>` has the same
layout as `Vec<u64>`. Eight slots per cache line.

---

## API at a glance

```rust
use subetha_cxc::SharedNaNValue;
use subetha_cxc::shared_region::OffsetPtr;

// Constructors
let nil  = SharedNaNValue::NIL;
let i    = SharedNaNValue::from_i32(-42);
let u    = SharedNaNValue::from_u32(0xFFFF_FFFF);
let b    = SharedNaNValue::from_bool(true);
let f    = SharedNaNValue::from_f64(2.5);
let ptr  = SharedNaNValue::from_offset_ptr(OffsetPtr::<u64>::new(99));
let raw  = SharedNaNValue::from_raw(0x7FF8_0000_0000_0000);  // raw bits

// Type queries
assert!(i.is_i32());
let kind = i.type_tag();  // NaNValueType::I32

// Extractors (return Option; None on type mismatch)
let v: Option<i32> = i.as_i32();
let v: Option<f64> = f.as_f64();
let v: Option<OffsetPtr<u64>> = ptr.as_offset_ptr();

// Round-trip via raw bits (cross-process safe)
let bytes: u64 = i.raw();
let restored = SharedNaNValue::from_raw(bytes);
assert_eq!(i, restored);
```

`SharedNaNValue` is `Copy + Default + Hash + Eq + Ord`-via-raw,
suitable as a hash-map value or set element. The default value
is `NIL`.

---

## Worked examples

### Heterogeneous shared hash map

```rust
use subetha_cxc::{SharedHashMap, SharedNaNValue};

let m: SharedHashMap<u32, SharedNaNValue>
    = SharedHashMap::create(&path, 16)?;

m.insert(0, SharedNaNValue::from_i32(42))?;
m.insert(1, SharedNaNValue::from_f64(2.5))?;
m.insert(2, SharedNaNValue::from_bool(true))?;
m.insert(3, SharedNaNValue::NIL)?;
m.insert(4, SharedNaNValue::from_offset_ptr::<u64>(OffsetPtr::new(99)))?;

// Each value is exactly 8 bytes; no per-variant padding.
// Cross-process: another process opens the map and sees the
// same canonical bit patterns.
```

### Reconstructing OffsetPtr<T> with any T

The `T` parameter is type-erased in the slot, so the caller can
re-attach any T at extraction time:

```rust
let p: OffsetPtr<u64> = OffsetPtr::new(0xABCD);
let n = SharedNaNValue::from_offset_ptr(p);

// Later, extract as a different T (the slot just holds the index).
struct Foo { x: u32 }
let p2: OffsetPtr<Foo> = n.as_offset_ptr().unwrap();
assert_eq!(p2.index, 0xABCD);
```

This is the cost of the uniform 8-byte slot: compile-time T
discipline is the caller's responsibility.

---

## Benchmark results

Bench: `crates/subetha-cxc/benches/shared_nan_value.rs`. Four
contender groups; SharedNaNValue vs Rust enum baseline.

### Construction

| Op | SharedNaNValue | Rust enum | Ratio |
|---|---|---|---|
| `construct_i32` | **1.09 ns** | 2.99 ns | **2.7x faster** |
| `construct_f64` | **1.23 ns** | 3.69 ns | **3.0x faster** |

The construct path is simple shift + OR for SharedNaNValue; the
enum path writes the discriminant byte AND the payload, then
returns the larger 16-byte value (more work + more bytes to
zero).

### Extraction

| Op | SharedNaNValue | Rust enum | Ratio |
|---|---|---|---|
| `extract_i32` | **9.29 ns** | 9.28 ns | parity |
| `extract_f64` | **1.83 ns** | 4.98 ns | **2.7x faster** |

i32 extraction is at parity: both paths check the discriminator
and mask the low bits. f64 extraction is faster on the NaN-box
because there's no tag check (the boxed-prefix test fails AND
the whole u64 IS the f64 bit pattern), whereas the enum path
walks the discriminant.

### Storage size

| Type | Size |
|---|---|
| SharedNaNValue | **8 bytes** |
| Rust enum (6 variants) | **16 bytes** |

**Exactly 2x larger for the enum.** The discriminant is 1 byte
but Rust aligns the enum to 8 bytes for the `f64(f64)` variant,
yielding a 16-byte total. For a million-element collection that's
8 MiB savings.

### Batch sum (i32 filter over 4 096 heterogeneous slots)

| Contender | Time | Notes |
|---|---|---|
| `batch_sum_i32_4096/mmf` | **2.09 us** | 4 K slots, 1/4 are i32. |
| `batch_sum_i32_4096/enum` | **2.18 us** | 4 K slots, 1/4 are i32. |

Within 4% of each other. Filter-then-sum is dominated by the
extraction logic at each slot; the storage density doesn't help
here because the 4 K slots fit in L1 either way (32 KiB for
NaN-box vs 64 KiB for enum, both L1-resident on commodity
hardware). The win compounds for collections that EXCEED
L1 / L2 / L3 boundaries.

**The bench audit (rule 3b) confirms the bench is fair:** each
contender does the same logical work (construct, extract, filter,
sum) with the only difference being the storage representation.
Both paths are inlined; both compile to similar shift+mask
sequences. The numbers are an honest comparison of representation
overhead, not algorithmic differences.

---

## Use case patterns

| Pattern | Use SharedNaNValue for | Why |
|---|---|---|
| **Cross-process scripting** | Dynamic-language values shared across processes | The 8-byte slot survives MMF transit; pointers are indices not VAs. |
| **Heterogeneous config maps** | `SharedHashMap<K, SharedNaNValue>` | Per-value compactness; mixed int/float/bool/ptr in one map. |
| **Tagged-union slots in shared state** | Replacement for `enum` in cross-process structs | 8-byte slot vs 16-byte enum slot. |
| **Weakly-typed message payloads** | Event queues where the schema varies per event | Sender stores typed; receiver dispatches on `type_tag()`. |
| **Interpreter value representations** | JavaScript-shape values for embedded language | Same shape as V8 / SpiderMonkey but cross-process. |

---

## Known limitations (verified)

All confirmed against the source or the bench:

- **NaN payloads are LOSSY.** `from_f64(NaN)` canonicalises to
  `0x7FF8_0000_0000_0000`. The specific NaN
  bit pattern (sNaN vs qNaN, NaN payload bits) is not preserved.
  Test `f64_nan_canonicalised` exercises this.
- **OffsetPtr's `T` parameter is type-erased.**
  The caller can reconstruct as any `T`. Test
  `offset_ptr_phantom_type_erased_then_reconstructed` exercises
  this. Type safety is the caller's responsibility.
- **3-bit tag field = 8 slots, 2 reserved.**
  Adding a 9th value type requires restructuring the
  encoding.
- **48-bit payload caps `OffsetPtr` index at 2^32 (u32 max).**
  `p.index as u64` is the encoding. The full 48-bit payload is
  NOT used by `from_offset_ptr`; only the low 32 bits.
- **No `from_negative_f64_nan` path.** A caller that constructs
  a NaN with `sign=1` and passes it via `from_raw(...)` (bypassing
  `from_f64`) WILL produce a value that decodes as "boxed". This
  is a constraint on `from_raw` callers: validate before
  insertion or rely on `from_f64` to canonicalise.
- **`raw()` returns the underlying u64.** Cross-process callers
  must agree on endianness if they transit the value via raw
  bytes outside the MMF (e.g. over a network socket). On a single
  host, all processes share endianness.
- **Reserved tags decode as `NaNValueType::Reserved(u64)`.**
  No accessor is exposed for reserved-tag payloads;
  callers using reserved tags must use `from_raw` and inspect
  manually.

---

## Common pitfalls

- **Don't store an `OffsetPtr` whose index exceeds `u32::MAX`.**
  The encoding masks to 32 bits; high bits are silently dropped.
- **Don't use `from_raw(...)` with hand-constructed bit patterns
  unless you understand the boxed prefix.** A `from_raw(0xFFF8_...)`
  with `tag = 0` IS a `Nil`; a `from_raw(0xFFF8_...)` with a
  reserved tag returns `NaNValueType::Reserved`.
- **Don't expect to round-trip a specific NaN payload through
  `from_f64`.** Use `from_raw(specific_nan_bits)` if you need
  to preserve the bit pattern. Be aware that doing so may
  collide with the boxed prefix and decode as a boxed value.
- **Don't assume `as_i32()` works on a `from_u32(...)` slot.** The
  tag is checked; a u32 value returns `None` from `as_i32()`. Use
  `type_tag()` to inspect first if you don't know the variant.
- **Don't mix `OffsetPtr<T1>` and `OffsetPtr<T2>` in the same
  slot without caller-tracked T discipline.** The slot only holds
  the index; T is reconstructed at extraction time. If the caller
  reconstructs with the wrong T, subsequent dereferences are UB.
- **Don't rely on `Hash` for cross-process maps if the OffsetPtr
  variant changes T at extraction.** The hash is over the raw u64
  and is stable regardless of T, but downstream usage that
  re-extracts may yield different T's.
- **Don't expect the storage-density win in L1-resident batch
  workloads.** The bench shows parity at 4 K slots. The win is
  on collections that exceed L1 / L2 / L3 boundaries (1 M+ slots
  on commodity hardware).

---