Skip to main content

shape_jit/ffi/call_method/
mod.rs

1// Heap allocation audit (PR-9 V8 Gap Closure):
2//   Category A (NaN-boxed returns): 2 sites
3//     jit_box(HK_JIT_OBJECT, ...) — group/groupBy result object
4//     jit_box(HK_ARRAY, ...) — group values inside object
5//   Category B (intermediate/consumed): 0 sites
6//   Category C (heap islands): 1 site (group/groupBy)
7//!
8//! Method Call FFI Functions for JIT
9//!
10//! Dispatches method calls on various types (array, string, object, series, etc.)
11//! Split into type-specific helper modules for maintainability.
12
13use crate::context::JITContext;
14use crate::jit_array::JitArray;
15use crate::nan_boxing::*;
16use shape_runtime::context::ExecutionContext;
17use std::collections::HashMap;
18
19// Module declarations
20pub mod array;
21pub mod duration;
22pub mod number;
23pub mod object;
24pub mod result;
25pub mod signal_builder;
26pub mod string;
27pub mod time;
28
29// Re-export the individual method handlers
30pub use array::call_array_method;
31pub use duration::call_duration_method;
32pub use number::call_number_method;
33pub use object::call_object_method;
34pub use result::call_result_method;
35pub use signal_builder::call_signalbuilder_method;
36pub use string::call_string_method;
37pub use time::call_time_method;
38
39// ============================================================================
40// User-Defined Method Support
41// ============================================================================
42
43/// Determine the type name of a JIT NaN-boxed receiver value.
44///
45/// For TypedObjects, uses the schema_id to look up the type name from the
46/// ExecutionContext's type_schema_registry. For other types, returns a static
47/// type name string.
48unsafe fn receiver_type_name(receiver_bits: u64, exec_ctx: &ExecutionContext) -> Option<String> {
49    use crate::ffi::typed_object::jit_typed_object_schema_id;
50
51    if is_number(receiver_bits) {
52        return Some("number".to_string());
53    }
54    if receiver_bits == TAG_BOOL_TRUE || receiver_bits == TAG_BOOL_FALSE {
55        return Some("bool".to_string());
56    }
57    if receiver_bits == TAG_NULL || receiver_bits == TAG_NONE {
58        return None;
59    }
60
61    match heap_kind(receiver_bits) {
62        Some(HK_STRING) => Some("string".to_string()),
63        Some(HK_ARRAY) => Some("Array".to_string()),
64        Some(HK_TYPED_OBJECT) => {
65            // Look up the schema name from the type_schema_registry
66            let schema_id = jit_typed_object_schema_id(receiver_bits);
67            if schema_id == 0 {
68                return None;
69            }
70            let registry = exec_ctx.type_schema_registry();
71            registry.get_by_id(schema_id).map(|s| s.name.clone())
72        }
73        Some(HK_JIT_OBJECT) => Some("object".to_string()),
74        Some(HK_DURATION) => Some("Duration".to_string()),
75        Some(HK_TIME) => Some("DateTime".to_string()),
76        _ => None,
77    }
78}
79
80/// Search the JITContext's function_names table for a function with the given
81/// UFCS name (e.g. "Point::distance") and return its index.
82unsafe fn find_function_by_name(ctx_ref: &JITContext, ufcs_name: &str) -> Option<usize> {
83    if ctx_ref.function_names_ptr.is_null() || ctx_ref.function_names_len == 0 {
84        return None;
85    }
86    let names = unsafe {
87        std::slice::from_raw_parts(ctx_ref.function_names_ptr, ctx_ref.function_names_len)
88    };
89    for (idx, name) in names.iter().enumerate() {
90        if name == ufcs_name {
91            return Some(idx);
92        }
93    }
94    None
95}
96
97/// Try to call a user-defined method from impl blocks via UFCS dispatch.
98///
99/// User-defined methods (from `extend` / `impl` blocks) are compiled as functions
100/// named `"TypeName::method_name"`. This function:
101/// 1. Determines the receiver type name from the NaN-boxed bits
102/// 2. Constructs the UFCS name `"TypeName::method_name"`
103/// 3. Looks up the function index in function_names
104/// 4. Calls the function via function_table, passing (receiver, ...args)
105/// 5. Returns the result as NaN-boxed u64
106///
107/// Returns Some(result) if the method was found and executed, None otherwise.
108unsafe fn try_call_user_method(
109    ctx: *const JITContext,
110    receiver_bits: u64,
111    method_name: &str,
112    args: &[u64],
113) -> Option<u64> {
114    let ctx_ref = unsafe { &*ctx };
115
116    // Need execution context to access the type schema registry
117    if ctx_ref.exec_context_ptr.is_null() {
118        return None;
119    }
120    let exec_ctx = unsafe { &*(ctx_ref.exec_context_ptr as *const ExecutionContext) };
121
122    // Determine the receiver's type name
123    let type_name = unsafe { receiver_type_name(receiver_bits, exec_ctx) }?;
124
125    // Construct UFCS function name: "TypeName::method_name"
126    let ufcs_name = format!("{}::{}", type_name, method_name);
127
128    // Look up the function index in the JIT function table
129    let func_idx = unsafe { find_function_by_name(ctx_ref, &ufcs_name) }?;
130
131    // Check that we have a valid function table entry
132    if ctx_ref.function_table.is_null() || func_idx >= ctx_ref.function_table_len {
133        return None;
134    }
135
136    // Read the raw pointer from the function table. A null entry means the
137    // function was not JIT-compiled (interpreted only).
138    let raw_fn_ptr = unsafe { *(ctx_ref.function_table as *const *const u8).add(func_idx) };
139    if raw_fn_ptr.is_null() {
140        return None;
141    }
142    let fn_ptr = unsafe { *ctx_ref.function_table.add(func_idx) };
143
144    // Push receiver + args onto the JIT stack for the function call.
145    // UFCS convention: first parameter is `self` (the receiver), then the rest.
146    let ctx_mut = unsafe { &mut *(ctx as *mut JITContext) };
147    ctx_mut.stack[ctx_mut.stack_ptr] = receiver_bits;
148    ctx_mut.stack_ptr += 1;
149    for &arg in args {
150        ctx_mut.stack[ctx_mut.stack_ptr] = arg;
151        ctx_mut.stack_ptr += 1;
152    }
153
154    // Call the JIT-compiled function
155    let _result_code = unsafe { fn_ptr(ctx_mut) };
156
157    // Pop result from stack
158    if ctx_mut.stack_ptr > 0 {
159        ctx_mut.stack_ptr -= 1;
160        Some(ctx_mut.stack[ctx_mut.stack_ptr])
161    } else {
162        Some(TAG_NULL)
163    }
164}
165
166// ============================================================================
167// Main Dispatcher
168// ============================================================================
169
170/// Call a method on a value
171/// Stack layout at call: [receiver, arg1, ..., argN, method_name, arg_count]
172/// The FFI pops values from ctx.stack and dispatches to the appropriate method
173pub extern "C" fn jit_call_method(ctx: *mut JITContext, stack_count: usize) -> u64 {
174    unsafe {
175        if ctx.is_null() || stack_count < 3 {
176            return TAG_NULL;
177        }
178
179        let ctx_ref = &mut *ctx;
180
181        // Pop arg_count from stack (number)
182        if ctx_ref.stack_ptr == 0 {
183            return TAG_NULL;
184        }
185        ctx_ref.stack_ptr -= 1;
186        let arg_count_bits = ctx_ref.stack[ctx_ref.stack_ptr];
187        let arg_count = if is_number(arg_count_bits) {
188            unbox_number(arg_count_bits) as usize
189        } else {
190            return TAG_NULL;
191        };
192
193        // Pop method_name from stack (string)
194        if ctx_ref.stack_ptr == 0 {
195            return TAG_NULL;
196        }
197        ctx_ref.stack_ptr -= 1;
198        let method_bits = ctx_ref.stack[ctx_ref.stack_ptr];
199        let method_name = if is_heap_kind(method_bits, HK_STRING) {
200            jit_unbox::<String>(method_bits).clone()
201        } else {
202            return method_bits; // Return non-string value as-is
203        };
204
205        // Pop args from stack
206        let mut args = Vec::with_capacity(arg_count);
207        for _ in 0..arg_count {
208            if ctx_ref.stack_ptr == 0 {
209                return TAG_NULL;
210            }
211            ctx_ref.stack_ptr -= 1;
212            args.push(ctx_ref.stack[ctx_ref.stack_ptr]);
213        }
214        args.reverse(); // Restore original order
215
216        // Pop receiver from stack
217        if ctx_ref.stack_ptr == 0 {
218            return TAG_NULL;
219        }
220        ctx_ref.stack_ptr -= 1;
221        let receiver_bits = ctx_ref.stack[ctx_ref.stack_ptr];
222
223        // Special-case higher-order methods that need callback execution
224        // Handle both arrays and series
225        if is_heap_kind(receiver_bits, HK_ARRAY) {
226            match method_name.as_str() {
227                "find" | "findIndex" | "some" | "every" | "filter" | "map" | "count" | "group"
228                | "groupBy" | "reduce" => {
229                    // These methods need callback execution via control functions
230                    if args.is_empty() {
231                        return TAG_NULL;
232                    }
233                    let predicate = args[0]; // The callback function
234
235                    let working_array_bits = receiver_bits;
236
237                    // Handle reduce separately (needs initial value)
238                    if method_name == "reduce" {
239                        let initial = if args.len() > 1 {
240                            args[1]
241                        } else {
242                            box_number(0.0)
243                        };
244                        // Push: [array, callback, initial, arg_count=3]
245                        ctx_ref.stack[ctx_ref.stack_ptr] = working_array_bits;
246                        ctx_ref.stack_ptr += 1;
247                        ctx_ref.stack[ctx_ref.stack_ptr] = predicate;
248                        ctx_ref.stack_ptr += 1;
249                        ctx_ref.stack[ctx_ref.stack_ptr] = initial;
250                        ctx_ref.stack_ptr += 1;
251                        ctx_ref.stack[ctx_ref.stack_ptr] = box_number(3.0);
252                        ctx_ref.stack_ptr += 1;
253                        return super::control::jit_control_reduce(ctx);
254                    }
255
256                    // Push array onto stack for other operations
257                    ctx_ref.stack[ctx_ref.stack_ptr] = working_array_bits;
258                    ctx_ref.stack_ptr += 1;
259                    // Push predicate onto stack
260                    ctx_ref.stack[ctx_ref.stack_ptr] = predicate;
261                    ctx_ref.stack_ptr += 1;
262                    // Push arg_count onto stack
263                    ctx_ref.stack[ctx_ref.stack_ptr] = box_number(2.0);
264                    ctx_ref.stack_ptr += 1;
265
266                    let result = match method_name.as_str() {
267                        "find" => super::control::jit_control_find(ctx),
268                        "findIndex" => super::control::jit_control_find_index(ctx),
269                        "some" => super::control::jit_control_some(ctx),
270                        "every" => super::control::jit_control_every(ctx),
271                        "filter" => super::control::jit_control_filter(ctx),
272                        "map" => super::control::jit_control_map(ctx),
273                        "count" => {
274                            // count(pred) = filter(pred).length
275                            let filtered = super::control::jit_control_filter(ctx);
276                            if is_heap_kind(filtered, HK_ARRAY) {
277                                let arr = jit_unbox::<JitArray>(filtered);
278                                return box_number(arr.len() as f64);
279                            }
280                            box_number(0.0)
281                        }
282                        "group" | "groupBy" => {
283                            // group(keyFn) - groups elements by the result of keyFn
284                            let elements = jit_unbox::<JitArray>(working_array_bits);
285
286                            let mut groups: HashMap<String, Vec<u64>> = HashMap::new();
287
288                            for (index, &value) in elements.iter().enumerate() {
289                                // Call predicate to get the key
290                                ctx_ref.stack[ctx_ref.stack_ptr] = predicate;
291                                ctx_ref.stack_ptr += 1;
292                                ctx_ref.stack[ctx_ref.stack_ptr] = value;
293                                ctx_ref.stack_ptr += 1;
294                                ctx_ref.stack[ctx_ref.stack_ptr] = box_number(index as f64);
295                                ctx_ref.stack_ptr += 1;
296                                ctx_ref.stack[ctx_ref.stack_ptr] = box_number(2.0);
297                                ctx_ref.stack_ptr += 1;
298
299                                let key_result = super::control::jit_call_value(ctx);
300
301                                // Convert key to string for HashMap
302                                let key = if is_heap_kind(key_result, HK_STRING) {
303                                    jit_unbox::<String>(key_result).clone()
304                                } else if is_number(key_result) {
305                                    format!("{}", unbox_number(key_result))
306                                } else if key_result == TAG_BOOL_TRUE {
307                                    "true".to_string()
308                                } else if key_result == TAG_BOOL_FALSE {
309                                    "false".to_string()
310                                } else {
311                                    "null".to_string()
312                                };
313
314                                groups.entry(key).or_default().push(value);
315                            }
316
317                            // AUDIT(C5): heap island — each group's Vec<u64> is jit_box'd
318                            // into a JitAlloc<JitArray>, then that u64 is stored as a
319                            // HashMap value. The HashMap itself is then jit_box'd into
320                            // a JitAlloc<HashMap>. The inner JitArray allocations escape
321                            // into the HashMap without GC tracking.
322                            // When GC feature enabled, route through gc_allocator.
323                            let mut obj: HashMap<String, u64> = HashMap::new();
324                            for (key, values) in groups {
325                                obj.insert(key, jit_box(HK_ARRAY, JitArray::from_vec(values)));
326                            }
327                            jit_box(HK_JIT_OBJECT, obj)
328                        }
329                        _ => TAG_NULL,
330                    };
331
332                    return result;
333                }
334                _ => {}
335            }
336        }
337
338        // Try built-in methods first
339        // Check for Result types (Ok/Err) before the heap kind match since they use sub-tags
340        let builtin_result = if is_ok_tag(receiver_bits) || is_err_tag(receiver_bits) {
341            call_result_method(receiver_bits, &method_name, &args)
342        } else if is_number(receiver_bits) {
343            call_number_method(receiver_bits, &method_name, &args)
344        } else if is_inline_function(receiver_bits) {
345            TAG_NULL // Functions don't have methods
346        } else {
347            match heap_kind(receiver_bits) {
348                Some(HK_ARRAY) => call_array_method(receiver_bits, &method_name, &args),
349                Some(HK_STRING) => call_string_method(receiver_bits, &method_name, &args),
350                Some(HK_JIT_OBJECT) => call_object_method(receiver_bits, &method_name, &args),
351                Some(HK_DURATION) => call_duration_method(receiver_bits, &method_name, &args),
352                Some(HK_COLUMN_REF) => TAG_NULL, // Series type removed
353                Some(HK_TIME) => call_time_method(receiver_bits, &method_name, &args),
354                Some(HK_JIT_SIGNAL_BUILDER) => {
355                    call_signalbuilder_method(receiver_bits, &method_name, &args)
356                }
357                _ => TAG_NULL,
358            }
359        };
360
361        // If built-in method returned NULL, try user-defined methods from TypeMethodRegistry
362        if builtin_result == TAG_NULL {
363            if let Some(user_result) = try_call_user_method(ctx, receiver_bits, &method_name, &args)
364            {
365                return user_result;
366            }
367        }
368
369        builtin_result
370    }
371}