Skip to main content

shape_jit/ffi/
data.rs

1// Heap allocation audit (PR-9 V8 Gap Closure):
2//   Category A (NaN-boxed returns): 1 site
3//     jit_box(HK_TIME, ...) — jit_get_row_timestamp
4//   Category B (intermediate/consumed): 0 sites
5//   Category C (heap islands): 0 sites
6//!
7//! Generic DataFrame FFI Functions for JIT
8//!
9//! Industry-agnostic functions for accessing DataFrame rows and fields.
10//! Column indices are resolved at compile time from field names.
11
12use super::super::context::JITContext;
13use super::jit_kinds::*;
14use super::value_ffi::*;
15
16// ============================================================================
17// Generic Field Access (by compile-time column index)
18// ============================================================================
19
20/// Get a field value from the current or offset row by column index.
21///
22/// This is the primary generic data access function.
23/// Column indices are resolved at compile time from field names.
24///
25/// # Arguments
26/// * `ctx` - JIT execution context
27/// * `row_offset` - Offset from current_row (0 = current, -1 = previous, etc.)
28/// * `column_index` - Compile-time resolved column index
29///
30/// # Returns
31/// NaN-boxed f64 value, or TAG_NULL if out of bounds
32pub extern "C" fn jit_get_field(ctx: *mut JITContext, row_offset: i32, column_index: u32) -> u64 {
33    unsafe {
34        if ctx.is_null() {
35            return TAG_NULL;
36        }
37        let ctx_ref = &*ctx;
38
39        // Calculate absolute row index
40        let row_signed = ctx_ref.current_row as i32 + row_offset;
41        if row_signed < 0 || row_signed as usize >= ctx_ref.row_count {
42            return TAG_NULL;
43        }
44        let row_idx = row_signed as usize;
45
46        // Check column bounds
47        if column_index as usize >= ctx_ref.column_count {
48            return TAG_NULL;
49        }
50
51        // Check if column_ptrs is valid
52        if ctx_ref.column_ptrs.is_null() {
53            return TAG_NULL;
54        }
55
56        // Get the column pointer
57        let col_ptr = *ctx_ref.column_ptrs.add(column_index as usize);
58        if col_ptr.is_null() {
59            return TAG_NULL;
60        }
61
62        // Get the value
63        let value = *col_ptr.add(row_idx);
64        box_number(value)
65    }
66}
67
68// ============================================================================
69// Row Reference Operations (lightweight, no data copy)
70// ============================================================================
71
72/// Create a lightweight row reference (just stores the row index).
73///
74/// This allows passing row references without copying data.
75/// The row index is stored in the NaN-boxed payload.
76///
77/// # Arguments
78/// * `ctx` - JIT execution context
79/// * `row_offset` - Offset from current_row (0 = current, -1 = previous, etc.)
80///
81/// # Returns
82/// TAG_INT with row index in payload, or TAG_NULL if out of bounds
83pub extern "C" fn jit_get_row_ref(ctx: *mut JITContext, row_offset: i32) -> u64 {
84    unsafe {
85        if ctx.is_null() {
86            return TAG_NULL;
87        }
88        let ctx_ref = &*ctx;
89
90        // Calculate absolute row index
91        let row_signed = ctx_ref.current_row as i32 + row_offset;
92        if row_signed < 0 || row_signed as usize >= ctx_ref.row_count {
93            return TAG_NULL;
94        }
95        let row_idx = row_signed as usize;
96
97        // Return a lightweight row reference (just the index)
98        box_data_row(row_idx)
99    }
100}
101
102/// Get a field value from a row reference.
103///
104/// # Arguments
105/// * `ctx` - JIT execution context
106/// * `row_ref` - TAG_INT value with row index in payload
107/// * `column_index` - Compile-time resolved column index
108///
109/// # Returns
110/// NaN-boxed f64 value, or TAG_NULL if invalid
111pub extern "C" fn jit_row_get_field(ctx: *mut JITContext, row_ref: u64, column_index: u32) -> u64 {
112    unsafe {
113        if ctx.is_null() {
114            return TAG_NULL;
115        }
116        let ctx_ref = &*ctx;
117
118        // Validate row reference tag
119        if !is_data_row(row_ref) {
120            return TAG_NULL;
121        }
122
123        // Extract row index from payload
124        let row_idx = unbox_data_row(row_ref);
125        if row_idx >= ctx_ref.row_count {
126            return TAG_NULL;
127        }
128
129        // Check column bounds
130        if column_index as usize >= ctx_ref.column_count {
131            return TAG_NULL;
132        }
133
134        // Check if column_ptrs is valid
135        if ctx_ref.column_ptrs.is_null() {
136            return TAG_NULL;
137        }
138
139        // Get the column pointer
140        let col_ptr = *ctx_ref.column_ptrs.add(column_index as usize);
141        if col_ptr.is_null() {
142            return TAG_NULL;
143        }
144
145        // Get the value
146        let value = *col_ptr.add(row_idx);
147        box_number(value)
148    }
149}
150
151/// Get the timestamp for a data row.
152///
153/// # Arguments
154/// * `ctx` - JIT execution context
155/// * `row_offset` - Offset from current_row
156///
157/// # Returns
158/// TAG_TIME with timestamp, or TAG_NULL if unavailable
159pub extern "C" fn jit_get_row_timestamp(ctx: *mut JITContext, row_offset: i32) -> u64 {
160    unsafe {
161        if ctx.is_null() {
162            return TAG_NULL;
163        }
164        let ctx_ref = &*ctx;
165
166        // Calculate absolute row index
167        let row_signed = ctx_ref.current_row as i32 + row_offset;
168        if row_signed < 0 || row_signed as usize >= ctx_ref.row_count {
169            return TAG_NULL;
170        }
171        let row_idx = row_signed as usize;
172
173        // Get timestamp from timestamps_ptr
174        if ctx_ref.timestamps_ptr.is_null() {
175            return TAG_NULL;
176        }
177
178        let timestamp = *ctx_ref.timestamps_ptr.add(row_idx);
179        // Return as heap-allocated time value
180        unified_box(HK_TIME, timestamp)
181    }
182}
183
184// ============================================================================
185// Row Count and Current Row Access
186// ============================================================================
187
188/// Get the total number of rows in the DataFrame.
189pub extern "C" fn jit_get_row_count(ctx: *mut JITContext) -> u64 {
190    unsafe {
191        if ctx.is_null() {
192            return box_number(0.0);
193        }
194        let ctx_ref = &*ctx;
195        box_number(ctx_ref.row_count as f64)
196    }
197}
198
199/// Get the current row index.
200pub extern "C" fn jit_get_current_row(ctx: *mut JITContext) -> u64 {
201    unsafe {
202        if ctx.is_null() {
203            return box_number(0.0);
204        }
205        let ctx_ref = &*ctx;
206        box_number(ctx_ref.current_row as f64)
207    }
208}
209
210// ============================================================================
211// Typed Column Access (LoadCol* opcodes)
212// ============================================================================
213
214/// Load an f64 value from a column by index and row reference.
215///
216/// # Arguments
217/// * `ctx` - JIT execution context (provides column_ptrs)
218/// * `col_id` - Column index
219/// * `row_ref` - TAG_INT with row index, or any value (uses current_row)
220///
221/// # Returns
222/// NaN-boxed f64 value, or TAG_NULL if out of bounds
223pub extern "C" fn jit_load_col_f64(ctx: *mut JITContext, col_id: u32, row_ref: u64) -> u64 {
224    unsafe {
225        if ctx.is_null() {
226            return TAG_NULL;
227        }
228        let ctx_ref = &*ctx;
229
230        let row_idx = if is_data_row(row_ref) {
231            unbox_data_row(row_ref)
232        } else {
233            ctx_ref.current_row
234        };
235
236        if row_idx >= ctx_ref.row_count || col_id as usize >= ctx_ref.column_count {
237            return TAG_NULL;
238        }
239        if ctx_ref.column_ptrs.is_null() {
240            return TAG_NULL;
241        }
242
243        let col_ptr = *ctx_ref.column_ptrs.add(col_id as usize);
244        if col_ptr.is_null() {
245            return TAG_NULL;
246        }
247
248        let value = *col_ptr.add(row_idx);
249        box_number(value)
250    }
251}
252
253/// Load an i64 value from a column (stored as f64, cast back to integer).
254///
255/// Returns NaN-boxed f64 (integer values are represented as f64 in the JIT).
256pub extern "C" fn jit_load_col_i64(ctx: *mut JITContext, col_id: u32, row_ref: u64) -> u64 {
257    unsafe {
258        if ctx.is_null() {
259            return TAG_NULL;
260        }
261        let ctx_ref = &*ctx;
262
263        let row_idx = if is_data_row(row_ref) {
264            unbox_data_row(row_ref)
265        } else {
266            ctx_ref.current_row
267        };
268
269        if row_idx >= ctx_ref.row_count || col_id as usize >= ctx_ref.column_count {
270            return TAG_NULL;
271        }
272        if ctx_ref.column_ptrs.is_null() {
273            return TAG_NULL;
274        }
275
276        let col_ptr = *ctx_ref.column_ptrs.add(col_id as usize);
277        if col_ptr.is_null() {
278            return TAG_NULL;
279        }
280
281        // Read as f64 (JIT stores all numerics as f64), truncate to integer
282        let value = *col_ptr.add(row_idx);
283        box_number(value.trunc())
284    }
285}
286
287/// Load a boolean value from a column (stored as f64: 0.0=false, else true).
288///
289/// Returns TAG_BOOL_TRUE or TAG_BOOL_FALSE.
290pub extern "C" fn jit_load_col_bool(ctx: *mut JITContext, col_id: u32, row_ref: u64) -> u64 {
291    unsafe {
292        if ctx.is_null() {
293            return TAG_NULL;
294        }
295        let ctx_ref = &*ctx;
296
297        let row_idx = if is_data_row(row_ref) {
298            unbox_data_row(row_ref)
299        } else {
300            ctx_ref.current_row
301        };
302
303        if row_idx >= ctx_ref.row_count || col_id as usize >= ctx_ref.column_count {
304            return TAG_NULL;
305        }
306        if ctx_ref.column_ptrs.is_null() {
307            return TAG_NULL;
308        }
309
310        let col_ptr = *ctx_ref.column_ptrs.add(col_id as usize);
311        if col_ptr.is_null() {
312            return TAG_NULL;
313        }
314
315        let value = *col_ptr.add(row_idx);
316        if value != 0.0 {
317            TAG_BOOL_TRUE
318        } else {
319            TAG_BOOL_FALSE
320        }
321    }
322}
323
324/// Load a string value from a column.
325///
326/// Not yet implemented — string columns require Arrow-backed buffer access.
327/// Returns TAG_NULL as a placeholder.
328pub extern "C" fn jit_load_col_str(_ctx: *mut JITContext, _col_id: u32, _row_ref: u64) -> u64 {
329    // TODO: Implement when JITContext supports Arrow-backed string columns
330    TAG_NULL
331}
332
333/// Stub for eval_data_datetime_ref - not yet implemented
334///
335/// Evaluates a data datetime reference expression.
336/// This is a placeholder that returns TAG_NULL.
337pub extern "C" fn jit_eval_data_datetime_ref(_ctx: *mut JITContext, _expr: u64) -> u64 {
338    // TODO: Implement datetime reference evaluation
339    TAG_NULL
340}
341
342/// Stub for eval_data_relative - not yet implemented
343///
344/// Evaluates a relative data access expression.
345/// This is a placeholder that returns TAG_NULL.
346pub extern "C" fn jit_eval_data_relative(_ctx: *mut JITContext, _expr: u64, _offset: i32) -> u64 {
347    // TODO: Implement relative data access
348    TAG_NULL
349}
350
351// ============================================================================
352// Type-Specialized Field Access (JIT Optimization)
353// ============================================================================
354
355/// Get a field from a typed object using precomputed offset.
356///
357/// This is the JIT optimization for typed field access. When the compiler
358/// knows an object's type at compile time, it precomputes the field offset
359/// and emits this instruction instead of a dynamic property lookup.
360///
361/// Performance: ~2ns (direct memory access)
362///
363/// # Arguments
364/// * `obj` - NaN-boxed TypedObject (TAG_TYPED_OBJECT)
365/// * `type_id` - Expected type schema ID (for type guard)
366/// * `_field_idx` - Field index (unused - offset is used instead)
367/// * `offset` - Precomputed byte offset for direct access
368///
369/// # Returns
370/// NaN-boxed field value
371///
372/// # Panics
373/// Panics if obj is not a TypedObject or has a schema mismatch.
374/// This indicates a type system bug - the type checker should guarantee
375/// that typed field access only occurs on correctly-typed objects.
376pub extern "C" fn jit_get_field_typed(obj: u64, type_id: u64, field_idx: u64, offset: u64) -> u64 {
377    #[allow(unused_imports)]
378    use crate::ffi::object::conversion::{jit_bits_to_nanboxed, nanboxed_to_jit_bits};
379
380    // Fast path: JIT-allocated TypedObject with direct offset access (~2ns)
381    if is_typed_object(obj) {
382        let ptr = unbox_typed_object(obj) as *const super::typed_object::TypedObject;
383        if !ptr.is_null() {
384            return unsafe {
385                if type_id != 0 && (*ptr).schema_id != type_id as u32 {
386                    TAG_NULL
387                } else {
388                    (*ptr).get_field(offset as usize)
389                }
390            };
391        }
392    }
393
394    // Slow path: VM-allocated object (Arc<HeapValue>).
395    //
396    // PHASE_2C / SURFACE (ADR-006 §2.7.4 / §2.7.5): pre-strict-typing
397    // the slow path called `ValueWord::clone_from_bits(obj)` and
398    // `vw.as_typed_object()` to decode an `Arc<HeapValue>` from raw
399    // bits, then routed through `slots[idx].as_value_word(is_heap)` to
400    // re-encode the field as a `ValueWord`. Both ends are the deleted
401    // kind-blind W-series helpers (`ValueWord::clone_from_bits` decodes
402    // a kind from `tag_bits`; `as_value_word(is_heap)` is the §2.7.7 #4
403    // is_heap-probe shape).
404    //
405    // The strict-typing rebuild target reads the field via
406    // `Arc<TypedObjectStorage>` (single-discriminator HeapValue per
407    // ADR-005 §1) directly from the JIT-stamped slot kind, returning a
408    // typed scalar where the JIT-emitted call signature carries the
409    // expected `NativeKind`. Until that lands, the slow path returns
410    // TAG_NULL — the fast path above handles JIT-allocated TypedObject
411    // (the production path for JIT-emitted typed field reads), so this
412    // surfaces only when VM-side construction crosses into JIT code.
413    //
414    // Forbidden under any rebuild: `tag_bits`-decode classification of
415    // `obj` (CLAUDE.md "Forbidden Patterns"); `as_value_word(is_heap)`
416    // re-encoding (deleted §2.7.7 #4 shape); Bool-default fallback when
417    // schema_id is unknown.
418    let _ = field_idx;
419    TAG_NULL
420}
421
422/// Set a field on a typed object using precomputed offset.
423///
424/// This is the JIT optimization for typed field set. Similar to get,
425/// when the compiler knows the type, it precomputes the offset.
426///
427/// Performance: ~2ns (direct memory access)
428///
429/// # Arguments
430/// * `obj` - NaN-boxed TypedObject to modify (TAG_TYPED_OBJECT)
431/// * `value` - NaN-boxed value to set
432/// * `type_id` - Expected type schema ID (for type guard)
433/// * `_field_idx` - Field index (unused - offset is used instead)
434/// * `offset` - Precomputed byte offset for direct access
435///
436/// # Returns
437/// The modified object (same object reference)
438///
439/// # Panics
440/// Panics if obj is not a TypedObject or has a schema mismatch.
441/// This indicates a type system bug - the type checker should guarantee
442/// that typed field access only occurs on correctly-typed objects.
443pub extern "C" fn jit_set_field_typed(
444    obj: u64,
445    value: u64,
446    type_id: u64,
447    _field_idx: u64,
448    offset: u64,
449) -> u64 {
450    // Fast path: JIT-allocated TypedObject
451    if is_typed_object(obj) {
452        let ptr = unbox_typed_object(obj) as *mut super::typed_object::TypedObject;
453        if !ptr.is_null() {
454            return unsafe {
455                if type_id != 0 && (*ptr).schema_id != type_id as u32 {
456                    obj // schema mismatch — return unchanged
457                } else {
458                    let old_bits = (*ptr).get_field(offset as usize);
459                    super::gc::jit_write_barrier(old_bits, value);
460                    (*ptr).set_field(offset as usize, value);
461                    obj
462                }
463            };
464        }
465    }
466
467    // Slow path: VM-allocated object — return unchanged.
468    // VM objects should be mutated through the trampoline VM, not directly.
469    obj
470}