Skip to main content

shape_value/v2/
decimal_obj.rs

1//! Refcounted, repr(C) Decimal carrier for v2 runtime.
2//!
3//! ## Memory layout (24 bytes)
4//!
5//! ```text
6//! Offset  Size  Field
7//! ------  ----  -----
8//!   0       8   header (HeapHeader — refcount at offset 0)
9//!   8      16   value (rust_decimal::Decimal — inline payload)
10//! ```
11//!
12//! Mirrors the `StringObj` precedent — `#[repr(C)]` 24-byte struct with
13//! `HeapHeader` at offset 0. `rust_decimal::Decimal` is `Copy + 16-byte`
14//! (4-byte flags + 12-byte mantissa per the crate's internal layout), so
15//! the payload lives inline with no nested allocation.
16//!
17//! ## Authority
18//!
19//! Per ADR-006 §2.7.24 Q25.A SUPERSEDED + R20 S2-prime audit deliverable
20//! (d) §4.1.D.1. `DecimalObj` is the per-element carrier for
21//! `TypedArray<*const DecimalObj>`, the v2-raw replacement for
22//! `TypedArrayData::Decimal(Arc<TypedBuffer<Arc<Decimal>>>)`.
23//!
24//! ## Refcount discipline
25//!
26//! The header refcount initializes to 1 on `new`. `HeapElement::release_elem`
27//! decrements via `v2_release`; on return-true, `Self::drop` deallocates the
28//! struct. No nested buffer (Decimal payload is inline), so `drop` only frees
29//! `Layout::new::<Self>()`.
30
31use super::heap_header::{HeapHeader, HEAP_KIND_V2_DECIMAL};
32use rust_decimal::Decimal;
33
34/// Refcounted, repr(C) Decimal carrier for v2 runtime.
35/// Total: 24 bytes (header 8 + value 16).
36#[repr(C)]
37pub struct DecimalObj {
38    pub header: HeapHeader,
39    pub value: Decimal,
40}
41
42impl DecimalObj {
43    /// Allocate a new DecimalObj wrapping the given Decimal value.
44    /// Returns a raw pointer with refcount initialized to 1.
45    pub fn new(value: Decimal) -> *mut Self {
46        let layout = std::alloc::Layout::new::<Self>();
47        let ptr = unsafe { std::alloc::alloc(layout) as *mut Self };
48        assert!(!ptr.is_null(), "allocation failed for DecimalObj");
49        unsafe {
50            (*ptr).header = HeapHeader::new(HEAP_KIND_V2_DECIMAL);
51            (*ptr).value = value;
52        }
53        ptr
54    }
55
56    /// Get the Decimal value.
57    ///
58    /// # Safety
59    /// `ptr` must point to a valid, live `DecimalObj`.
60    pub unsafe fn value(ptr: *const Self) -> Decimal {
61        unsafe { (*ptr).value }
62    }
63
64    /// Free the DecimalObj.
65    ///
66    /// # Safety
67    /// `ptr` must point to a valid `DecimalObj` with no remaining references.
68    /// Must not be called more than once on the same pointer.
69    pub unsafe fn drop(ptr: *mut Self) {
70        // No nested allocation; just dealloc the struct.
71        let layout = std::alloc::Layout::new::<Self>();
72        unsafe { std::alloc::dealloc(ptr as *mut u8, layout) };
73    }
74
75    /// Byte offset constants for JIT codegen.
76    pub const OFFSET_VALUE: usize = 8;
77}
78
79// Compile-time size + alignment assertions.
80// `rust_decimal::Decimal` is 16 bytes with 4-byte alignment (4-byte flags +
81// 12-byte mantissa); `HeapHeader` is 8 bytes with 4-byte alignment
82// (AtomicU32 refcount + u16 kind + u8 flags + u8 _pad). Combined struct is
83// 24 bytes with 4-byte alignment.
84const _: () = {
85    assert!(std::mem::size_of::<DecimalObj>() == 24);
86    assert!(std::mem::align_of::<DecimalObj>() == 4);
87};
88
89// HeapElement impl per ADR-006 §2.7.24 Q25.A SUPERSEDED + R20 S2-prime
90// audit deliverable (b) §4.1.B decision. Constrains `DecimalObj` to the
91// HeapHeader-at-offset-0 v2-raw element-carrier contract; enables
92// `TypedArray<*const DecimalObj>::drop_array_heap` per-T release dispatch
93// via compile-time monomorphization (no runtime NativeKind probe).
94unsafe impl super::heap_element::HeapElement for DecimalObj {
95    unsafe fn release_elem(ptr: *const Self) {
96        if unsafe { super::refcount::v2_release(&(*ptr).header) } {
97            unsafe { Self::drop(ptr as *mut Self) };
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use rust_decimal::prelude::FromPrimitive;
106
107    #[test]
108    fn test_size_of_decimal_obj() {
109        assert_eq!(std::mem::size_of::<DecimalObj>(), 24);
110        assert_eq!(std::mem::align_of::<DecimalObj>(), 4);
111    }
112
113    #[test]
114    fn test_create_and_read_decimal() {
115        unsafe {
116            let d = Decimal::from_f64(3.14).unwrap();
117            let ptr = DecimalObj::new(d);
118            assert_eq!(DecimalObj::value(ptr), d);
119            assert_eq!((*ptr).header.kind(), HEAP_KIND_V2_DECIMAL);
120            assert_eq!((*ptr).header.get_refcount(), 1);
121            DecimalObj::drop(ptr);
122        }
123    }
124
125    #[test]
126    fn test_create_zero_decimal() {
127        unsafe {
128            let d = Decimal::ZERO;
129            let ptr = DecimalObj::new(d);
130            assert_eq!(DecimalObj::value(ptr), Decimal::ZERO);
131            DecimalObj::drop(ptr);
132        }
133    }
134
135    #[test]
136    fn test_create_max_decimal() {
137        unsafe {
138            let d = Decimal::MAX;
139            let ptr = DecimalObj::new(d);
140            assert_eq!(DecimalObj::value(ptr), Decimal::MAX);
141            DecimalObj::drop(ptr);
142        }
143    }
144
145    #[test]
146    fn test_drop_does_not_leak() {
147        // Create and drop many DecimalObjs — under Miri/valgrind this would
148        // catch leaks.
149        unsafe {
150            for i in 0..200 {
151                let d = Decimal::from_f64(i as f64 * 0.123).unwrap_or(Decimal::ZERO);
152                let ptr = DecimalObj::new(d);
153                DecimalObj::drop(ptr);
154            }
155        }
156    }
157
158    #[test]
159    fn test_field_offsets() {
160        unsafe {
161            let ptr = DecimalObj::new(Decimal::ONE);
162            let base = ptr as usize;
163            let value_offset = &(*ptr).value as *const _ as usize - base;
164            assert_eq!(value_offset, DecimalObj::OFFSET_VALUE, "value must be at offset 8");
165            DecimalObj::drop(ptr);
166        }
167    }
168
169    #[test]
170    fn test_refcount_starts_at_one() {
171        unsafe {
172            let ptr = DecimalObj::new(Decimal::ONE);
173            assert_eq!((*ptr).header.get_refcount(), 1);
174            DecimalObj::drop(ptr);
175        }
176    }
177
178    #[test]
179    fn test_refcount_retain_release() {
180        use crate::v2::refcount::{v2_get_refcount, v2_release, v2_retain};
181        unsafe {
182            let ptr = DecimalObj::new(Decimal::ONE);
183            let header = &(*ptr).header as *const HeapHeader;
184
185            assert_eq!(v2_get_refcount(header), 1);
186            v2_retain(header);
187            assert_eq!(v2_get_refcount(header), 2);
188            assert!(!v2_release(header)); // 2 -> 1
189            assert_eq!(v2_get_refcount(header), 1);
190
191            // Don't release to zero here — use drop for cleanup.
192            DecimalObj::drop(ptr);
193        }
194    }
195
196    #[test]
197    fn test_heap_element_release_elem_to_zero() {
198        use crate::v2::heap_element::HeapElement;
199        unsafe {
200            // Allocate; release_elem from refcount 1 → 0 should deallocate.
201            let ptr = DecimalObj::new(Decimal::ONE);
202            DecimalObj::release_elem(ptr);
203            // ptr is dangling; we cannot dereference further. The valgrind /
204            // Miri pass confirms no leak.
205        }
206    }
207
208    #[test]
209    fn test_heap_element_release_elem_held_share() {
210        use crate::v2::heap_element::HeapElement;
211        use crate::v2::refcount::{v2_get_refcount, v2_retain};
212        unsafe {
213            let ptr = DecimalObj::new(Decimal::ONE);
214            let header = &(*ptr).header as *const HeapHeader;
215
216            v2_retain(header); // refcount = 2
217            DecimalObj::release_elem(ptr); // refcount = 1 (does not deallocate)
218            assert_eq!(v2_get_refcount(header), 1);
219
220            // Clean up the held share.
221            DecimalObj::drop(ptr);
222        }
223    }
224}