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}