From 6fffa99eaa9a709fc9e74aaeb74a5d116b771318 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 15 May 2026 21:42:40 +0200 Subject: [PATCH 1/5] feat: extend captured callable parity --- docs/internals/the-codegen.md | 6 +- docs/internals/the-runtime.md | 6 +- docs/php/functions.md | 4 +- examples/callbacks/main.php | 35 +++ src/codegen/builtins/arrays/array_reduce.rs | 47 +++- src/codegen/builtins/arrays/array_walk.rs | 44 +++- src/codegen/builtins/arrays/uasort.rs | 83 +++--- src/codegen/builtins/arrays/uksort.rs | 83 +++--- src/codegen/builtins/arrays/usort.rs | 83 +++--- src/codegen/expr.rs | 3 +- src/codegen/expr/calls.rs | 14 +- src/codegen/expr/calls/first_class.rs | 50 +++- src/codegen/expr/calls/indirect.rs | 114 +++----- src/codegen/functions/locals.rs | 17 +- src/codegen/runtime/arrays/array_reduce.rs | 22 +- src/codegen/runtime/arrays/array_walk.rs | 40 ++- src/codegen/runtime/arrays/usort.rs | 36 ++- src/types/checker/builtins/callables.rs | 3 - src/types/checker/callables/captures.rs | 88 ------- src/types/checker/callables/first_class.rs | 6 - src/types/checker/callables/mod.rs | 1 - src/types/checker/inference/ops.rs | 75 +++++- tests/codegen/oop/callables/methods.rs | 278 +++++++++++++++++++- tests/error_tests/misc/functions.rs | 56 ++-- 24 files changed, 809 insertions(+), 385 deletions(-) delete mode 100644 src/types/checker/callables/captures.rs diff --git a/docs/internals/the-codegen.md b/docs/internals/the-codegen.md index 1639d0b3..77582d37 100644 --- a/docs/internals/the-codegen.md +++ b/docs/internals/the-codegen.md @@ -505,11 +505,11 @@ When a closure variable is called (`$fn(1, 2)`), the codegen: ### Closures as callback arguments -Built-in functions like `array_map`, `array_filter`, `array_reduce`, `array_walk`, and `usort` accept callback values. The callback function pointer is passed in a register (like any other `Callable` argument) and the runtime routine calls it via `blr`. +Built-in functions like `array_map`, `array_filter`, `array_reduce`, `array_walk`, `usort`, `uksort`, and `uasort` accept callback values. The callback function pointer is passed in a register (like any other `Callable` argument) and the runtime routine calls it via `blr`. -For captured closures passed through `array_map` / `array_filter`, codegen builds a temporary callback environment containing the original closure pointer plus its hidden `use (...)` values. The runtime passes that environment to a generated callback wrapper, and the wrapper re-materializes the original visible arguments plus hidden captures before calling the closure. `call_user_func()` and `call_user_func_array()` do not need a runtime loop, so they append the hidden capture arguments directly at the indirect call site. +For captured closures passed through callback runtimes such as `array_map`, `array_filter`, `array_reduce`, `array_walk`, `usort`, `uksort`, and `uasort`, codegen builds a temporary callback environment containing the original closure pointer plus its hidden `use (...)` values. The runtime passes that environment to a generated callback wrapper, and the wrapper re-materializes the original visible arguments plus hidden captures before calling the closure. `call_user_func()` and `call_user_func_array()` do not need a runtime loop, so they append the hidden capture arguments directly at the indirect call site. -First-class callable wrappers reuse this hidden argument path when the callable target carries context. `$obj->method(...)` records the receiver variable as a hidden capture and the wrapper calls that method with the visible arguments. `static::method(...)` records the forwarded called-class id, or `$this` in an instance method, so late static binding is preserved for direct callable calls and for callback paths that forward an environment, such as `array_map`, `array_filter`, `call_user_func`, and `call_user_func_array`. +First-class callable wrappers reuse this hidden argument path when the callable target carries context. `$obj->method(...)` records the receiver as a hidden capture; non-local receiver expressions are evaluated once into a hidden temporary before wrapper creation. `static::method(...)` records the forwarded called-class id, or `$this` in an instance method, so late static binding is preserved for direct callable calls and for callback paths that forward an environment. ## Generator codegen diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 58b05f17..8cb0c1e0 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -272,9 +272,9 @@ See [Memory Model](memory-model.md) for the hash table memory layout. | `__rt_array_map` | Apply callback to each scalar element, return new array; an optional third argument carries a captured-closure environment for generated callback wrappers | | `__rt_array_map_str` | Apply callback to each scalar or string element and return a string array; an optional third argument carries a captured-closure environment | | `__rt_array_filter` | Filter scalar elements where callback returns truthy; an optional third argument carries a captured-closure environment | -| `__rt_array_reduce` | Reduce array to single value via callback | -| `__rt_array_walk` | Call callback on each element (side-effects) | -| `__rt_usort` | Sort array using user comparison callback | +| `__rt_array_reduce` | Reduce array to single value via callback; an optional fourth argument carries a captured-callback environment | +| `__rt_array_walk` | Call callback on each element (side-effects); an optional third argument carries a captured-callback environment | +| `__rt_usort` | Sort array using user comparison callback; an optional third argument carries a captured-callback environment | ### Reference counting diff --git a/docs/php/functions.md b/docs/php/functions.md index bf353968..1da64f2c 100644 --- a/docs/php/functions.md +++ b/docs/php/functions.md @@ -156,7 +156,7 @@ $double = MathBox::double(...); ``` Supported: user-defined function names, extern function names, `ClassName::method(...)`, `self::method(...)`, `parent::method(...)`, and the registered builtin wrappers `strlen(...)`, `count(...)`, `buffer_len(...)`, `intval(...)`, `strtolower(...)`, `strtoupper(...)`, `ucfirst(...)`, `lcfirst(...)`, `strrev(...)`, `addslashes(...)`, `stripslashes(...)`, `nl2br(...)`, `bin2hex(...)`, `hex2bin(...)`, `htmlspecialchars(...)`, `htmlentities(...)`, `html_entity_decode(...)`, `urlencode(...)`, `urldecode(...)`, `rawurlencode(...)`, `rawurldecode(...)`, `base64_encode(...)`, `base64_decode(...)`, `json_encode(...)`, `json_decode(...)`, `json_validate(...)`, `json_last_error(...)`, `json_last_error_msg(...)`, `array_sum(...)`, and `array_product(...)`. -Also supported: `static::method(...)` inside class methods, preserving late static binding for direct callable calls, and `$obj->method(...)` / `$this->method(...)` with a stable object receiver variable. +Also supported: `static::method(...)` inside class methods, preserving late static binding for direct callable calls, and `$obj->method(...)` / `$this->method(...)` with either a local receiver variable or a non-local receiver expression such as `(new Greeter("Hi "))->greet(...)`. ```php hello(...); echo $hello("Ada"); // Hello Ada ``` -Captured first-class callable targets (`static::method(...)` and `$obj->method(...)`) can be called directly through a local callable variable. They can also be passed to callback paths that forward captured callable environments, including `array_map()`, `array_filter()`, `call_user_func()`, and `call_user_func_array()`. Runtime helpers without callback environments (`array_reduce()`, `array_walk()`, `usort()`, `uksort()`, and `uasort()`) still reject captured method/static targets. Immediate expression calls such as `($obj->method(...))()` are also rejected; assign the callable to a local variable before calling it. +Captured first-class callable targets (`static::method(...)` and `$obj->method(...)`) can be called directly through a local callable variable or as an immediate callable expression such as `($obj->method(...))("Ada")`. They can also be passed to callback paths that forward captured callable environments, including `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `usort()`, `uksort()`, `uasort()`, `call_user_func()`, and `call_user_func_array()`. Nullsafe first-class callable syntax (`$obj?->method(...)`) is not supported yet. ## Global variables diff --git a/examples/callbacks/main.php b/examples/callbacks/main.php index 308cb38b..8bc436e3 100644 --- a/examples/callbacks/main.php +++ b/examples/callbacks/main.php @@ -57,6 +57,41 @@ public function bracket(string $value): string { echo "\n"; echo "method callable call_user_func_array: " . call_user_func_array($format, ["cb"]) . "\n"; +class OffsetCallbacks { + public function add_offset($carry, $item) { + return $carry + $item + 10; + } + + public function show_shifted($item) { + echo ($item + 5) . " "; + } + + public function descending($a, $b) { + return $b - $a; + } +} + +$offsets = new OffsetCallbacks(); +echo "method callable array_reduce: " . array_reduce([1, 2], $offsets->add_offset(...), 0) . "\n"; +echo "method callable array_walk: "; +array_walk([1, 2], $offsets->show_shifted(...)); +echo "\n"; +$method_sorted = [1, 3, 2]; +usort($method_sorted, $offsets->descending(...)); +echo "method callable usort: "; +foreach ($method_sorted as $v) { echo $v . " "; } +echo "\n"; +$method_key_sorted = [1, 3, 2]; +uksort($method_key_sorted, $offsets->descending(...)); +echo "method callable uksort: "; +foreach ($method_key_sorted as $v) { echo $v . " "; } +echo "\n"; +$method_value_sorted = [1, 3, 2]; +uasort($method_value_sorted, $offsets->descending(...)); +echo "method callable uasort: "; +foreach ($method_value_sorted as $v) { echo $v . " "; } +echo "\n"; + class Labeler { public static function current() { $label = static::name(...); diff --git a/src/codegen/builtins/arrays/array_reduce.rs b/src/codegen/builtins/arrays/array_reduce.rs index 58cb9f2a..38e1b13f 100644 --- a/src/codegen/builtins/arrays/array_reduce.rs +++ b/src/codegen/builtins/arrays/array_reduce.rs @@ -13,10 +13,10 @@ use crate::codegen::context::Context; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; use crate::names::function_symbol; use crate::parser::ast::{Expr, ExprKind}; use crate::types::PhpType; +use super::callback_env; pub fn emit( _name: &str, @@ -31,14 +31,21 @@ pub fn emit( let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); let initial_arg_reg = abi::int_arg_reg_name(emitter.target, 2); + let env_arg_reg = abi::int_arg_reg_name(emitter.target, 3); + let source_elem_ty = match crate::codegen::functions::infer_contextual_type(&args[0], ctx) { + PhpType::Array(elem_ty) => elem_ty.codegen_repr(), + _ => PhpType::Int, + }; // -- evaluate the callback argument (may be a string literal or closure) -- let is_closure = matches!( &args[1].kind, ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) ); + let mut inline_captures = Vec::new(); if is_closure { emit_expr(&args[1], emitter, ctx, data); + inline_captures = callback_env::callback_captures(&args[1], ctx); abi::emit_push_reg(emitter, result_reg); // save the synthesized callback address on the temporary stack } @@ -59,6 +66,37 @@ pub fn emit( let label = function_symbol(&func_name); abi::emit_symbol_address(emitter, call_reg, &label); // materialize the callback function address in the nested-call scratch register } + let captures = if is_closure { + inline_captures + } else { + callback_env::callback_captures(&args[1], ctx) + }; + + if !captures.is_empty() { + abi::emit_pop_reg(emitter, result_reg); // recover the source array pointer before building the capture environment + if is_closure { + abi::emit_pop_reg(emitter, call_reg); // recover the original closure entry point for env slot zero + } + let wrapper = callback_env::emit_captured_callback_env( + call_reg, + result_reg, + &captures, + vec![PhpType::Int, source_elem_ty], + emitter, + ctx, + ); + + // -- evaluate initial value (third arg) -- + emit_expr(&args[2], emitter, ctx, data); + emitter.instruction(&format!("mov {}, {}", initial_arg_reg, result_reg)); // place the initial accumulator in the third runtime argument register + + callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); + abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); + callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); + abi::emit_call_label(emitter, "__rt_array_reduce"); // call the callback-driven reduce runtime helper with a capture environment + abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); + return Some(PhpType::Int); + } // -- evaluate initial value (third arg) -- emit_expr(&args[2], emitter, ctx, data); @@ -72,11 +110,8 @@ pub fn emit( abi::emit_pop_reg(emitter, array_arg_reg); // pop the source array pointer into the second runtime argument register emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the callback function address into the first runtime argument register } - if emitter.target.arch == Arch::X86_64 { - abi::emit_call_label(emitter, "__rt_array_reduce"); // call the x86_64 callback-driven reduce runtime helper - } else { - emitter.instruction("bl __rt_array_reduce"); // call the ARM64 callback-driven reduce runtime helper - } + abi::emit_load_int_immediate(emitter, env_arg_reg, 0); + abi::emit_call_label(emitter, "__rt_array_reduce"); // call the callback-driven reduce runtime helper Some(PhpType::Int) } diff --git a/src/codegen/builtins/arrays/array_walk.rs b/src/codegen/builtins/arrays/array_walk.rs index ebe6a769..5e2cb50b 100644 --- a/src/codegen/builtins/arrays/array_walk.rs +++ b/src/codegen/builtins/arrays/array_walk.rs @@ -13,10 +13,10 @@ use crate::codegen::context::Context; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; use crate::names::function_symbol; use crate::parser::ast::{Expr, ExprKind}; use crate::types::PhpType; +use super::callback_env; pub fn emit( _name: &str, @@ -30,9 +30,14 @@ pub fn emit( let result_reg = abi::int_result_reg(emitter); let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); + let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); // -- evaluate the array argument (first arg) -- - emit_expr(&args[0], emitter, ctx, data); + let arr_ty = emit_expr(&args[0], emitter, ctx, data); + let source_elem_ty = match &arr_ty { + PhpType::Array(elem_ty) => elem_ty.codegen_repr(), + _ => PhpType::Int, + }; // -- save array pointer -- abi::emit_push_reg(emitter, result_reg); // push the source array pointer onto the temporary stack @@ -42,8 +47,10 @@ pub fn emit( &args[1].kind, ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) ); + let mut inline_captures = Vec::new(); if is_closure { emit_expr(&args[1], emitter, ctx, data); + inline_captures = callback_env::callback_captures(&args[1], ctx); abi::emit_push_reg(emitter, result_reg); // save the synthesized callback address on the temporary stack } else if let ExprKind::Variable(var_name) = &args[1].kind { let var = ctx.variables.get(var_name).expect("undefined callback variable"); @@ -57,20 +64,41 @@ pub fn emit( let label = function_symbol(&func_name); abi::emit_symbol_address(emitter, call_reg, &label); // materialize the callback function address in the nested-call scratch register } + let captures = if is_closure { + inline_captures + } else { + callback_env::callback_captures(&args[1], ctx) + }; // -- place callback and array pointer into the runtime argument registers -- - if is_closure { + if !captures.is_empty() { + if is_closure { + abi::emit_pop_reg(emitter, call_reg); // recover the original closure entry point for env slot zero + } + abi::emit_pop_reg(emitter, result_reg); // recover the source array pointer before building the capture environment + let wrapper = callback_env::emit_captured_callback_env( + call_reg, + result_reg, + &captures, + vec![source_elem_ty], + emitter, + ctx, + ); + callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); + abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); + callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); + abi::emit_call_label(emitter, "__rt_array_walk"); // call the callback-driven walk runtime helper with a capture environment + abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); + return Some(PhpType::Void); + } else if is_closure { abi::emit_pop_reg(emitter, callback_arg_reg); // pop the synthesized callback address into the first runtime argument register abi::emit_pop_reg(emitter, array_arg_reg); // pop the source array pointer into the second runtime argument register } else { abi::emit_pop_reg(emitter, array_arg_reg); // pop the source array pointer into the second runtime argument register emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the callback function address into the first runtime argument register } - if emitter.target.arch == Arch::X86_64 { - abi::emit_call_label(emitter, "__rt_array_walk"); // call the x86_64 callback-driven walk runtime helper - } else { - emitter.instruction("bl __rt_array_walk"); // call the ARM64 callback-driven walk runtime helper - } + abi::emit_load_int_immediate(emitter, env_arg_reg, 0); + abi::emit_call_label(emitter, "__rt_array_walk"); // call the callback-driven walk runtime helper Some(PhpType::Void) } diff --git a/src/codegen/builtins/arrays/uasort.rs b/src/codegen/builtins/arrays/uasort.rs index b19b1323..6210b559 100644 --- a/src/codegen/builtins/arrays/uasort.rs +++ b/src/codegen/builtins/arrays/uasort.rs @@ -9,13 +9,13 @@ //! - Callback lowering must preserve PHP source evaluation order, captures, and callable return ownership. use crate::codegen::abi; +use super::callback_env; use super::ensure_unique_arg::emit_ensure_unique_arg; use super::store_mutating_arg::emit_store_mutating_arg; use crate::codegen::context::Context; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; use crate::names::function_symbol; use crate::parser::ast::{Expr, ExprKind}; use crate::types::PhpType; @@ -31,6 +31,10 @@ pub fn emit( // -- evaluate the array argument (first arg) -- let arr_ty = emit_expr(&args[0], emitter, ctx, data); + let elem_ty = match &arr_ty { + PhpType::Array(elem_ty) => elem_ty.codegen_repr(), + _ => PhpType::Int, + }; emit_ensure_unique_arg(emitter, &arr_ty); emit_store_mutating_arg(emitter, ctx, &args[0]); @@ -38,56 +42,71 @@ pub fn emit( abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the array pointer while the callback address is resolved for the target ABI // -- resolve callback function address -- + let call_reg = abi::nested_call_reg(emitter); + let result_reg = abi::int_result_reg(emitter); + let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); + let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); + let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); let is_closure = matches!( &args[1].kind, ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) ); + let mut inline_captures = Vec::new(); if is_closure { emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move the resolved closure callback address into the first SysV uasort() runtime argument register - } - Arch::AArch64 => { - emitter.instruction("mov x0, x0"); // keep the resolved closure callback address in the first AArch64 uasort() runtime argument register - } - } + inline_captures = callback_env::callback_captures(&args[1], ctx); + emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // keep the resolved closure callback address in the nested-call scratch register } else if let ExprKind::Variable(var_name) = &args[1].kind { let var = ctx.variables.get(var_name).expect("undefined callback variable"); let offset = var.stack_offset; - match emitter.target.arch { - Arch::X86_64 => { - abi::load_at_offset(emitter, "rdi", offset); // load the callback address from the variable slot into the first SysV uasort() runtime argument register - } - Arch::AArch64 => { - abi::load_at_offset(emitter, "x0", offset); // load the callback address from the variable slot into the first AArch64 uasort() runtime argument register - } - } + abi::load_at_offset(emitter, call_reg, offset); // load the callback address from the variable slot into the nested-call scratch register } else { let func_name = match &args[1].kind { ExprKind::StringLiteral(name) => name.clone(), _ => panic!("uasort() callback must be a string literal, callable expression, or callable variable"), }; let label = function_symbol(&func_name); - match emitter.target.arch { - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rdi", &label); // materialize the comparator function address in the first SysV uasort() runtime argument register - } - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x0", &label); // materialize the comparator function address in the first AArch64 uasort() runtime argument register - } - } + abi::emit_symbol_address(emitter, call_reg, &label); // materialize the comparator function address in the nested-call scratch register } + let captures = if is_closure { + inline_captures + } else { + callback_env::callback_captures(&args[1], ctx) + }; // -- call runtime: callback_addr + array_ptr -- - match emitter.target.arch { - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "rsi"); // restore the array pointer into the second SysV uasort() runtime argument register - } - Arch::AArch64 => { - abi::emit_pop_reg(emitter, "x1"); // restore the array pointer into the second AArch64 uasort() runtime argument register - } + if !captures.is_empty() { + abi::emit_pop_reg(emitter, result_reg); // recover the array pointer before building the comparator capture environment + let wrapper = callback_env::emit_captured_callback_env( + call_reg, + result_reg, + &captures, + vec![elem_ty.clone(), elem_ty], + emitter, + ctx, + ); + callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); + abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); + callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); + abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a captured comparator wrapper + abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); + return Some(PhpType::Void); + } + abi::emit_pop_reg(emitter, array_arg_reg); // restore the array pointer into the second runtime argument register + if is_closure { + emitter.instruction(&format!("mov {}, {}", callback_arg_reg, result_reg)); // move the resolved closure callback address into the first runtime argument register + } else if let ExprKind::Variable(var_name) = &args[1].kind { + let var = ctx.variables.get(var_name).expect("undefined callback variable"); + abi::load_at_offset(emitter, callback_arg_reg, var.stack_offset); // load the callback address from the variable slot into the first runtime argument register + } else { + let func_name = match &args[1].kind { + ExprKind::StringLiteral(name) => name.clone(), + _ => panic!("uasort() callback must be a string literal, callable expression, or callable variable"), + }; + let label = function_symbol(&func_name); + abi::emit_symbol_address(emitter, callback_arg_reg, &label); // materialize the comparator function address in the first runtime argument register } + abi::emit_load_int_immediate(emitter, env_arg_reg, 0); abi::emit_call_label(emitter, "__rt_usort"); // call the target-aware runtime helper that sorts the indexed array using the comparator callback Some(PhpType::Void) diff --git a/src/codegen/builtins/arrays/uksort.rs b/src/codegen/builtins/arrays/uksort.rs index 2c8a7e98..e8d1935c 100644 --- a/src/codegen/builtins/arrays/uksort.rs +++ b/src/codegen/builtins/arrays/uksort.rs @@ -9,13 +9,13 @@ //! - Callback lowering must preserve PHP source evaluation order, captures, and callable return ownership. use crate::codegen::abi; +use super::callback_env; use super::ensure_unique_arg::emit_ensure_unique_arg; use super::store_mutating_arg::emit_store_mutating_arg; use crate::codegen::context::Context; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; use crate::names::function_symbol; use crate::parser::ast::{Expr, ExprKind}; use crate::types::PhpType; @@ -31,6 +31,10 @@ pub fn emit( // -- evaluate the array argument (first arg) -- let arr_ty = emit_expr(&args[0], emitter, ctx, data); + let elem_ty = match &arr_ty { + PhpType::Array(elem_ty) => elem_ty.codegen_repr(), + _ => PhpType::Int, + }; emit_ensure_unique_arg(emitter, &arr_ty); emit_store_mutating_arg(emitter, ctx, &args[0]); @@ -38,56 +42,71 @@ pub fn emit( abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the array pointer while the callback address is resolved for the target ABI // -- resolve callback function address -- + let call_reg = abi::nested_call_reg(emitter); + let result_reg = abi::int_result_reg(emitter); + let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); + let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); + let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); let is_closure = matches!( &args[1].kind, ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) ); + let mut inline_captures = Vec::new(); if is_closure { emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move the resolved closure callback address into the first SysV uksort() runtime argument register - } - Arch::AArch64 => { - emitter.instruction("mov x0, x0"); // keep the resolved closure callback address in the first AArch64 uksort() runtime argument register - } - } + inline_captures = callback_env::callback_captures(&args[1], ctx); + emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // keep the resolved closure callback address in the nested-call scratch register } else if let ExprKind::Variable(var_name) = &args[1].kind { let var = ctx.variables.get(var_name).expect("undefined callback variable"); let offset = var.stack_offset; - match emitter.target.arch { - Arch::X86_64 => { - abi::load_at_offset(emitter, "rdi", offset); // load the callback address from the variable slot into the first SysV uksort() runtime argument register - } - Arch::AArch64 => { - abi::load_at_offset(emitter, "x0", offset); // load the callback address from the variable slot into the first AArch64 uksort() runtime argument register - } - } + abi::load_at_offset(emitter, call_reg, offset); // load the callback address from the variable slot into the nested-call scratch register } else { let func_name = match &args[1].kind { ExprKind::StringLiteral(name) => name.clone(), _ => panic!("uksort() callback must be a string literal, callable expression, or callable variable"), }; let label = function_symbol(&func_name); - match emitter.target.arch { - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rdi", &label); // materialize the comparator function address in the first SysV uksort() runtime argument register - } - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x0", &label); // materialize the comparator function address in the first AArch64 uksort() runtime argument register - } - } + abi::emit_symbol_address(emitter, call_reg, &label); // materialize the comparator function address in the nested-call scratch register } + let captures = if is_closure { + inline_captures + } else { + callback_env::callback_captures(&args[1], ctx) + }; // -- call runtime: callback_addr + array_ptr -- - match emitter.target.arch { - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "rsi"); // restore the array pointer into the second SysV uksort() runtime argument register - } - Arch::AArch64 => { - abi::emit_pop_reg(emitter, "x1"); // restore the array pointer into the second AArch64 uksort() runtime argument register - } + if !captures.is_empty() { + abi::emit_pop_reg(emitter, result_reg); // recover the array pointer before building the comparator capture environment + let wrapper = callback_env::emit_captured_callback_env( + call_reg, + result_reg, + &captures, + vec![elem_ty.clone(), elem_ty], + emitter, + ctx, + ); + callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); + abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); + callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); + abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a captured comparator wrapper + abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); + return Some(PhpType::Void); + } + abi::emit_pop_reg(emitter, array_arg_reg); // restore the array pointer into the second runtime argument register + if is_closure { + emitter.instruction(&format!("mov {}, {}", callback_arg_reg, result_reg)); // move the resolved closure callback address into the first runtime argument register + } else if let ExprKind::Variable(var_name) = &args[1].kind { + let var = ctx.variables.get(var_name).expect("undefined callback variable"); + abi::load_at_offset(emitter, callback_arg_reg, var.stack_offset); // load the callback address from the variable slot into the first runtime argument register + } else { + let func_name = match &args[1].kind { + ExprKind::StringLiteral(name) => name.clone(), + _ => panic!("uksort() callback must be a string literal, callable expression, or callable variable"), + }; + let label = function_symbol(&func_name); + abi::emit_symbol_address(emitter, callback_arg_reg, &label); // materialize the comparator function address in the first runtime argument register } + abi::emit_load_int_immediate(emitter, env_arg_reg, 0); abi::emit_call_label(emitter, "__rt_usort"); // call the target-aware runtime helper that sorts the indexed array using the comparator callback Some(PhpType::Void) diff --git a/src/codegen/builtins/arrays/usort.rs b/src/codegen/builtins/arrays/usort.rs index 2e3cafcb..f7387b2a 100644 --- a/src/codegen/builtins/arrays/usort.rs +++ b/src/codegen/builtins/arrays/usort.rs @@ -9,13 +9,13 @@ //! - Callback lowering must preserve PHP source evaluation order, captures, and callable return ownership. use crate::codegen::abi; +use super::callback_env; use super::ensure_unique_arg::emit_ensure_unique_arg; use super::store_mutating_arg::emit_store_mutating_arg; use crate::codegen::context::Context; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; use crate::names::function_symbol; use crate::parser::ast::{Expr, ExprKind}; use crate::types::PhpType; @@ -31,6 +31,10 @@ pub fn emit( // -- evaluate the array argument (first arg) -- let arr_ty = emit_expr(&args[0], emitter, ctx, data); + let elem_ty = match &arr_ty { + PhpType::Array(elem_ty) => elem_ty.codegen_repr(), + _ => PhpType::Int, + }; emit_ensure_unique_arg(emitter, &arr_ty); emit_store_mutating_arg(emitter, ctx, &args[0]); @@ -38,56 +42,71 @@ pub fn emit( abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the array pointer while the callback address is resolved for the target ABI // -- resolve callback function address -- + let call_reg = abi::nested_call_reg(emitter); + let result_reg = abi::int_result_reg(emitter); + let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); + let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); + let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); let is_closure = matches!( &args[1].kind, ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) ); + let mut inline_captures = Vec::new(); if is_closure { emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move the resolved closure callback address into the first SysV usort() runtime argument register - } - Arch::AArch64 => { - emitter.instruction("mov x0, x0"); // keep the resolved closure callback address in the first AArch64 usort() runtime argument register - } - } + inline_captures = callback_env::callback_captures(&args[1], ctx); + emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // keep the resolved closure callback address in the nested-call scratch register } else if let ExprKind::Variable(var_name) = &args[1].kind { let var = ctx.variables.get(var_name).expect("undefined callback variable"); let offset = var.stack_offset; - match emitter.target.arch { - Arch::X86_64 => { - abi::load_at_offset(emitter, "rdi", offset); // load the callback address from the variable slot into the first SysV usort() runtime argument register - } - Arch::AArch64 => { - abi::load_at_offset(emitter, "x0", offset); // load the callback address from the variable slot into the first AArch64 usort() runtime argument register - } - } + abi::load_at_offset(emitter, call_reg, offset); // load the callback address from the variable slot into the nested-call scratch register } else { let func_name = match &args[1].kind { ExprKind::StringLiteral(name) => name.clone(), _ => panic!("usort() callback must be a string literal, callable expression, or callable variable"), }; let label = function_symbol(&func_name); - match emitter.target.arch { - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rdi", &label); // materialize the comparator function address in the first SysV usort() runtime argument register - } - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x0", &label); // materialize the comparator function address in the first AArch64 usort() runtime argument register - } - } + abi::emit_symbol_address(emitter, call_reg, &label); // materialize the comparator function address in the nested-call scratch register } + let captures = if is_closure { + inline_captures + } else { + callback_env::callback_captures(&args[1], ctx) + }; // -- call runtime: callback_addr + array_ptr -- - match emitter.target.arch { - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "rsi"); // restore the array pointer into the second SysV usort() runtime argument register - } - Arch::AArch64 => { - abi::emit_pop_reg(emitter, "x1"); // restore the array pointer into the second AArch64 usort() runtime argument register - } + if !captures.is_empty() { + abi::emit_pop_reg(emitter, result_reg); // recover the array pointer before building the comparator capture environment + let wrapper = callback_env::emit_captured_callback_env( + call_reg, + result_reg, + &captures, + vec![elem_ty.clone(), elem_ty], + emitter, + ctx, + ); + callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); + abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); + callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); + abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a captured comparator wrapper + abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); + return Some(PhpType::Void); + } + abi::emit_pop_reg(emitter, array_arg_reg); // restore the array pointer into the second runtime argument register + if is_closure { + emitter.instruction(&format!("mov {}, {}", callback_arg_reg, result_reg)); // move the resolved closure callback address into the first runtime argument register + } else if let ExprKind::Variable(var_name) = &args[1].kind { + let var = ctx.variables.get(var_name).expect("undefined callback variable"); + abi::load_at_offset(emitter, callback_arg_reg, var.stack_offset); // load the callback address from the variable slot into the first runtime argument register + } else { + let func_name = match &args[1].kind { + ExprKind::StringLiteral(name) => name.clone(), + _ => panic!("usort() callback must be a string literal, callable expression, or callable variable"), + }; + let label = function_symbol(&func_name); + abi::emit_symbol_address(emitter, callback_arg_reg, &label); // materialize the comparator function address in the first runtime argument register } + abi::emit_load_int_immediate(emitter, env_arg_reg, 0); abi::emit_call_label(emitter, "__rt_usort"); // call the target-aware runtime helper that sorts the indexed array using the comparator callback Some(PhpType::Void) diff --git a/src/codegen/expr.rs b/src/codegen/expr.rs index cedcae2c..4edf2781 100644 --- a/src/codegen/expr.rs +++ b/src/codegen/expr.rs @@ -183,7 +183,8 @@ pub fn emit_expr( calls::emit_closure_call(var, args, emitter, ctx, data) } ExprKind::ExprCall { callee, args } => { - calls::emit_expr_call(callee, args, emitter, ctx, data) + let loaded_callee_ty = emit_expr(callee, emitter, ctx, data); + calls::emit_loaded_expr_call(callee, args, &loaded_callee_ty, emitter, ctx, data) } ExprKind::ConstRef(name) => { let (value, ty) = match ctx.constants.get(name.as_str()) { diff --git a/src/codegen/expr/calls.rs b/src/codegen/expr/calls.rs index 6bfb2bc0..ad79ff43 100644 --- a/src/codegen/expr/calls.rs +++ b/src/codegen/expr/calls.rs @@ -65,16 +65,6 @@ pub(super) fn emit_closure_call( closure::emit_closure_call(var, args, emitter, ctx, data) } -pub(super) fn emit_expr_call( - callee: &Expr, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - indirect::emit_expr_call(callee, args, emitter, ctx, data) -} - pub(super) fn emit_loaded_expr_call( callee: &Expr, args: &[Expr], @@ -102,6 +92,10 @@ pub(crate) fn first_class_callable_sig( first_class::first_class_callable_sig(target, ctx) } +pub(crate) fn first_class_method_receiver_temp_name(span: Span) -> String { + first_class::method_receiver_temp_name(span) +} + pub(crate) fn pipe_value_temp_name(span: Span) -> String { format!("__elephc_pipe_value_{}_{}", span.line, span.col) } diff --git a/src/codegen/expr/calls/first_class.rs b/src/codegen/expr/calls/first_class.rs index b7291155..d472c6ed 100644 --- a/src/codegen/expr/calls/first_class.rs +++ b/src/codegen/expr/calls/first_class.rs @@ -9,17 +9,22 @@ //! - Callable metadata and argument signatures must stay synchronized with type checking and runtime dispatch. use crate::codegen::abi; -use crate::codegen::context::{Context, DeferredClosure}; +use crate::codegen::context::{Context, DeferredClosure, HeapOwnership}; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::names::Name; use crate::parser::ast::{CallableTarget, Expr, ExprKind, StaticReceiver, Stmt, StmtKind}; +use crate::span::Span; use crate::types::{callable_wrapper_sig, first_class_callable_builtin_sig, FunctionSig, PhpType}; const FCC_CALLED_CLASS_ID_PARAM: &str = "__elephc_fcc_called_class_id"; const FCC_THIS_PARAM: &str = "__elephc_fcc_this"; const FCC_RECEIVER_PARAM: &str = "__elephc_fcc_receiver"; +pub(crate) fn method_receiver_temp_name(span: Span) -> String { + format!("__elephc_fcc_receiver_{}_{}", span.line, span.col) +} + fn resolved_static_callable_target(receiver: &StaticReceiver, ctx: &Context) -> Option { match receiver { StaticReceiver::Named(name) => Some(StaticReceiver::Named(name.clone())), @@ -124,7 +129,12 @@ fn unsupported_fcc_diagnostic(target: &CallableTarget) -> String { } } -fn capture_for_method_receiver(object: &Expr, ctx: &Context) -> Option<(String, PhpType)> { +fn capture_for_method_receiver( + object: &Expr, + emitter: &mut Emitter, + ctx: &mut Context, + data: &mut DataSection, +) -> Option<(String, PhpType)> { match &object.kind { ExprKind::Variable(name) => { let ty = ctx @@ -138,14 +148,40 @@ fn capture_for_method_receiver(object: &Expr, ctx: &Context) -> Option<(String, .variables .get("this") .map(|var| ("this".to_string(), var.ty.clone())), - _ => None, + _ => { + let temp_name = method_receiver_temp_name(object.span); + let receiver_static_ty = crate::codegen::functions::infer_contextual_type(object, ctx); + let receiver_ty = crate::codegen::expr::emit_expr(object, emitter, ctx, data); + if receiver_ty.is_refcounted() + && super::super::expr_result_heap_ownership(object) != HeapOwnership::Owned + { + abi::emit_incref_if_refcounted(emitter, &receiver_ty); + } + let Some(temp_offset) = ctx.variables.get(&temp_name).map(|info| info.stack_offset) else { + emitter.comment(&format!( + "WARNING: missing first-class callable receiver temp ${}", + temp_name + )); + return None; + }; + abi::emit_store(emitter, &receiver_ty, temp_offset); + ctx.update_var_type_static_and_ownership( + &temp_name, + receiver_ty.codegen_repr(), + receiver_static_ty, + HeapOwnership::local_owner_for_type(&receiver_ty), + ); + Some((temp_name, receiver_ty)) + } } } fn normalized_target_and_captures( target: &CallableTarget, sig: &FunctionSig, - ctx: &Context, + emitter: &mut Emitter, + ctx: &mut Context, + data: &mut DataSection, ) -> Option<( CallableTarget, Vec<(String, PhpType)>, @@ -183,7 +219,7 @@ fn normalized_target_and_captures( } }, CallableTarget::Method { object, method } => { - let capture = capture_for_method_receiver(object, ctx)?; + let capture = capture_for_method_receiver(object, emitter, ctx, data)?; let hidden_name = unique_hidden_param(FCC_RECEIVER_PARAM, sig); let hidden_ty = capture.1.clone(); Some(( @@ -264,7 +300,7 @@ pub(super) fn emit_first_class_callable( target: &CallableTarget, emitter: &mut Emitter, ctx: &mut Context, - _data: &mut DataSection, + data: &mut DataSection, ) -> PhpType { let Some(sig) = first_class_callable_sig(target, ctx) else { emitter.comment("WARNING: unsupported first-class callable target"); @@ -273,7 +309,7 @@ pub(super) fn emit_first_class_callable( }; let Some((normalized_target, captures, hidden_params)) = - normalized_target_and_captures(target, &sig, ctx) + normalized_target_and_captures(target, &sig, emitter, ctx, data) else { emitter.comment(&unsupported_fcc_diagnostic(target)); abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); diff --git a/src/codegen/expr/calls/indirect.rs b/src/codegen/expr/calls/indirect.rs index 36e10ecc..0366fadd 100644 --- a/src/codegen/expr/calls/indirect.rs +++ b/src/codegen/expr/calls/indirect.rs @@ -16,80 +16,6 @@ use crate::types::PhpType; use super::args; -pub(super) fn emit_expr_call( - callee: &Expr, - args_exprs: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("call expression result"); - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - super::super::save_concat_offset_before_nested_call(emitter, ctx); - } - - let callee_sig = callee_sig_for_expr(callee, ctx); - - let emitted_args = args::emit_pushed_call_args( - args_exprs, - callee_sig.as_ref(), - args::regular_param_count(callee_sig.as_ref(), args_exprs.len()), - "indirect ref arg", - true, - emitter, - ctx, - data, - ); - let arg_types = emitted_args.arg_types; - - let _callee_ty = super::super::emit_expr(callee, emitter, ctx, data); - let call_reg = crate::codegen::abi::nested_call_reg(emitter); - let result_reg = match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => "x0", - crate::codegen::platform::Arch::X86_64 => "rax", - }; - emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // preserve the computed callable address in the nested-call scratch register - crate::codegen::abi::emit_push_reg(emitter, call_reg); - - let assignments = - crate::codegen::abi::build_outgoing_arg_assignments_for_target(emitter.target, &arg_types, 0); - - crate::codegen::abi::emit_pop_reg(emitter, call_reg); - let overflow_bytes = crate::codegen::abi::materialize_outgoing_args(emitter, &assignments); - - let ret_ty = callee_sig - .as_ref() - .map(|sig| sig.return_type.clone()) - .unwrap_or_else(|| match &callee.kind { - ExprKind::Closure { - return_type: Some(type_ann), - .. - } => crate::codegen::functions::codegen_static_type(type_ann, ctx), - ExprKind::Closure { body, .. } => { - crate::types::checker::infer_return_type_syntactic(body) - } - _ => PhpType::Int, - }); - - if !save_concat_before_args { - super::super::save_concat_offset_before_nested_call(emitter, ctx); - } - crate::codegen::abi::emit_call_reg(emitter, call_reg); - if save_concat_before_args { - crate::codegen::abi::emit_release_temporary_stack(emitter, overflow_bytes); - crate::codegen::abi::emit_release_temporary_stack(emitter, emitted_args.source_temp_bytes); - super::super::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } else { - super::super::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - crate::codegen::abi::emit_release_temporary_stack(emitter, overflow_bytes); - crate::codegen::abi::emit_release_temporary_stack(emitter, emitted_args.source_temp_bytes); - } - - ret_ty -} - pub(super) fn emit_loaded_expr_call( callee: &Expr, args_exprs: &[Expr], @@ -106,6 +32,7 @@ pub(super) fn emit_loaded_expr_call( } let callee_sig = callee_sig_for_expr(callee, ctx); + let captures = captures_for_expr_call_callee(callee, ctx); crate::codegen::abi::emit_push_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // save the already-evaluated callable below later arguments let emitted_args = args::emit_pushed_call_args( @@ -118,11 +45,12 @@ pub(super) fn emit_loaded_expr_call( ctx, data, ); - let arg_types = emitted_args.arg_types; + let mut arg_types = emitted_args.arg_types; let call_reg = crate::codegen::abi::nested_call_reg(emitter); let arg_temp_bytes = args::pushed_temp_bytes(&arg_types) + emitted_args.source_temp_bytes; crate::codegen::abi::emit_load_temporary_stack_slot(emitter, call_reg, arg_temp_bytes); + push_captures_as_hidden_args(&captures, emitter, ctx, &mut arg_types); let assignments = crate::codegen::abi::build_outgoing_arg_assignments_for_target(emitter.target, &arg_types, 0); @@ -177,3 +105,39 @@ fn callee_sig_for_expr( _ => None, } } + +fn captures_for_expr_call_callee(callee: &Expr, ctx: &mut Context) -> Vec<(String, PhpType)> { + match &callee.kind { + ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) => ctx + .deferred_closures + .last() + .map(|closure| closure.captures.clone()) + .unwrap_or_default(), + ExprKind::Variable(name) => { + ctx.mark_fcc_used(name); + ctx.closure_captures.get(name).cloned().unwrap_or_default() + } + _ => Vec::new(), + } +} + +fn push_captures_as_hidden_args( + captures: &[(String, PhpType)], + emitter: &mut Emitter, + ctx: &Context, + arg_types: &mut Vec, +) { + for (capture_name, capture_ty) in captures { + emitter.comment(&format!("push callable capture ${}", capture_name)); + let Some(capture_info) = ctx.variables.get(capture_name) else { + emitter.comment(&format!( + "WARNING: captured callable variable ${} not found", + capture_name + )); + continue; + }; + crate::codegen::abi::emit_load(emitter, capture_ty, capture_info.stack_offset); + args::push_arg_value(emitter, capture_ty); + arg_types.push(capture_ty.clone()); + } +} diff --git a/src/codegen/functions/locals.rs b/src/codegen/functions/locals.rs index cfd8e0a6..abe3ef7f 100644 --- a/src/codegen/functions/locals.rs +++ b/src/codegen/functions/locals.rs @@ -9,7 +9,7 @@ //! - Any lowering path that introduces storage must be represented here before stack offsets are assigned. use crate::codegen::context::Context; -use crate::parser::ast::{Expr, ExprKind, InstanceOfTarget, StmtKind}; +use crate::parser::ast::{CallableTarget, Expr, ExprKind, InstanceOfTarget, StmtKind}; use crate::types::{FunctionSig, PhpType}; use super::types::{codegen_declared_type, codegen_static_type, infer_local_type}; @@ -358,6 +358,21 @@ fn collect_assignment_expr_vars(expr: &Expr, ctx: &mut Context, sig: &FunctionSi } } } + ExprKind::FirstClassCallable(CallableTarget::Method { object, .. }) => { + collect_assignment_expr_vars(object, ctx, sig); + if !matches!(&object.kind, ExprKind::Variable(_) | ExprKind::This) { + let temp_name = + crate::codegen::expr::calls::first_class_method_receiver_temp_name(object.span); + if !ctx.variables.contains_key(&temp_name) { + let static_ty = infer_local_type(object, sig, Some(ctx)); + ctx.alloc_var_with_static_type( + &temp_name, + static_ty.codegen_repr(), + static_ty, + ); + } + } + } _ => {} } } diff --git a/src/codegen/runtime/arrays/array_reduce.rs b/src/codegen/runtime/arrays/array_reduce.rs index 845844fd..a99b13fb 100644 --- a/src/codegen/runtime/arrays/array_reduce.rs +++ b/src/codegen/runtime/arrays/array_reduce.rs @@ -12,7 +12,7 @@ use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; /// array_reduce: reduce an integer array to a single value using a callback. -/// Input: x0 = callback function address, x1 = source array pointer, x2 = initial value +/// Input: x0 = callback function address, x1 = source array pointer, x2 = initial value, x3 = optional callback environment pointer /// Output: x0 = accumulated result /// The callback receives (accumulator, element) and returns the new accumulator. pub fn emit_array_reduce(emitter: &mut Emitter) { @@ -31,6 +31,7 @@ pub fn emit_array_reduce(emitter: &mut Emitter) { emitter.instruction("add x29, sp, #48"); // set up new frame pointer emitter.instruction("stp x19, x20, [sp, #32]"); // save callee-saved x19, x20 emitter.instruction("str x21, [sp, #24]"); // save callee-saved x21 + emitter.instruction("str x3, [sp, #16]"); // save optional callback environment pointer to stack emitter.instruction("mov x19, x0"); // x19 = callback address (callee-saved) emitter.instruction("str x1, [sp, #0]"); // save source array pointer to stack emitter.instruction("mov x21, x2"); // x21 = accumulator = initial value @@ -53,8 +54,12 @@ pub fn emit_array_reduce(emitter: &mut Emitter) { emitter.instruction("add x2, x2, #24"); // skip header to data region emitter.instruction("ldr x1, [x2, x20, lsl #3]"); // x1 = source[i] (element) emitter.instruction("mov x0, x21"); // x0 = accumulator + emitter.instruction("ldr x9, [sp, #16]"); // load optional callback environment pointer + emitter.instruction("cbz x9, __rt_array_reduce_call"); // keep legacy two-argument callback ABI when no environment is present + emitter.instruction("mov x2, x9"); // pass capture environment after accumulator and element // -- call callback(accumulator, element) -- + emitter.label("__rt_array_reduce_call"); emitter.instruction("blr x19"); // call callback → result in x0 emitter.instruction("mov x21, x0"); // accumulator = callback result @@ -84,20 +89,26 @@ fn emit_array_reduce_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push r12"); // preserve the callback address register because the reduce loop calls through it repeatedly emitter.instruction("push r13"); // preserve the source-index register because the loop keeps it live across callback invocations emitter.instruction("push r14"); // preserve the accumulator register because the loop keeps it live across callback invocations + emitter.instruction("push r15"); // preserve the optional callback environment pointer across callback invocations emitter.instruction("sub rsp, 16"); // reserve local slots for the source array pointer and source length emitter.instruction("mov r12, rdi"); // keep the callback address in a callee-saved register across the reduce loop - emitter.instruction("mov QWORD PTR [rbp - 32], rsi"); // save the source array pointer so the loop can reload it after callback calls + emitter.instruction("mov QWORD PTR [rbp - 40], rsi"); // save the source array pointer so the loop can reload it after callback calls emitter.instruction("mov r14, rdx"); // keep the current accumulator in a callee-saved register across every callback invocation + emitter.instruction("mov r15, rcx"); // keep the optional callback environment pointer across every callback invocation emitter.instruction("mov r10, QWORD PTR [rsi]"); // load the source array length from the first field of the array header - emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save the source array length for loop termination checks + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // save the source array length for loop termination checks emitter.instruction("xor r13d, r13d"); // start the source index at zero before reducing the source array emitter.label("__rt_array_reduce_loop"); - emitter.instruction("cmp r13, QWORD PTR [rbp - 40]"); // stop once the source index reaches the saved source-array length + emitter.instruction("cmp r13, QWORD PTR [rbp - 48]"); // stop once the source index reaches the saved source-array length emitter.instruction("jge __rt_array_reduce_done"); // finish reduction once every source element has been folded into the accumulator - emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the source array pointer after the previous callback invocation + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the source array pointer after the previous callback invocation emitter.instruction("mov rsi, QWORD PTR [r10 + r13 * 8 + 24]"); // load the current source element into the second SysV integer argument register emitter.instruction("mov rdi, r14"); // move the current accumulator into the first SysV integer argument register + emitter.instruction("test r15, r15"); // check whether this runtime call carries a callback capture environment + emitter.instruction("jz __rt_array_reduce_call_linux_x86_64"); // keep legacy two-argument callback ABI when no environment is present + emitter.instruction("mov rdx, r15"); // pass capture environment after accumulator and element + emitter.label("__rt_array_reduce_call_linux_x86_64"); emitter.instruction("call r12"); // invoke the user callback with (accumulator, element) and read the new accumulator from rax emitter.instruction("mov r14, rax"); // update the live accumulator with the callback result before the next iteration emitter.instruction("add r13, 1"); // advance the source index after folding the current element @@ -106,6 +117,7 @@ fn emit_array_reduce_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_array_reduce_done"); emitter.instruction("mov rax, r14"); // move the final accumulator into the x86_64 integer return register emitter.instruction("add rsp, 16"); // release the reduce local bookkeeping slots before restoring callee-saved registers + emitter.instruction("pop r15"); // restore the caller callback-environment callee-saved register emitter.instruction("pop r14"); // restore the caller accumulator callee-saved register emitter.instruction("pop r13"); // restore the caller source-index callee-saved register emitter.instruction("pop r12"); // restore the caller callback callee-saved register diff --git a/src/codegen/runtime/arrays/array_walk.rs b/src/codegen/runtime/arrays/array_walk.rs index f2cd6ca7..bd5c83af 100644 --- a/src/codegen/runtime/arrays/array_walk.rs +++ b/src/codegen/runtime/arrays/array_walk.rs @@ -12,7 +12,7 @@ use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; /// array_walk: call a callback on each element of an integer array (no return value). -/// Input: x0 = callback function address, x1 = source array pointer +/// Input: x0 = callback function address, x1 = source array pointer, x2 = optional callback environment pointer /// Output: none (void) pub fn emit_array_walk(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { @@ -25,10 +25,11 @@ pub fn emit_array_walk(emitter: &mut Emitter) { emitter.label_global("__rt_array_walk"); // -- set up stack frame, save callee-saved registers -- - emitter.instruction("sub sp, sp, #48"); // allocate 48 bytes on the stack - emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #32"); // set up new frame pointer - emitter.instruction("stp x19, x20, [sp, #16]"); // save callee-saved x19, x20 + emitter.instruction("sub sp, sp, #64"); // allocate 64 bytes on the stack + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // set up new frame pointer + emitter.instruction("stp x19, x20, [sp, #32]"); // save callee-saved x19, x20 + emitter.instruction("str x2, [sp, #16]"); // save optional callback environment pointer to stack emitter.instruction("mov x19, x0"); // x19 = callback address (callee-saved) emitter.instruction("str x1, [sp, #0]"); // save source array pointer to stack @@ -49,8 +50,12 @@ pub fn emit_array_walk(emitter: &mut Emitter) { emitter.instruction("ldr x1, [sp, #0]"); // reload source array pointer emitter.instruction("add x1, x1, #24"); // skip header to data region emitter.instruction("ldr x0, [x1, x20, lsl #3]"); // x0 = source[i] + emitter.instruction("ldr x9, [sp, #16]"); // load optional callback environment pointer + emitter.instruction("cbz x9, __rt_array_walk_call"); // keep legacy one-argument callback ABI when no environment is present + emitter.instruction("mov x1, x9"); // pass capture environment as the wrapper's second argument // -- call callback with element (ignore return value) -- + emitter.label("__rt_array_walk_call"); emitter.instruction("blr x19"); // call callback(element) // -- advance loop -- @@ -61,9 +66,9 @@ pub fn emit_array_walk(emitter: &mut Emitter) { emitter.label("__rt_array_walk_done"); // -- tear down stack frame and return -- - emitter.instruction("ldp x19, x20, [sp, #16]"); // restore callee-saved x19, x20 - emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #48"); // deallocate stack frame + emitter.instruction("ldp x19, x20, [sp, #32]"); // restore callee-saved x19, x20 + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // deallocate stack frame emitter.instruction("ret"); // return (void) } @@ -76,24 +81,31 @@ fn emit_array_walk_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the saved source array and source length emitter.instruction("push r12"); // preserve the callback address register because the walk loop calls through it repeatedly emitter.instruction("push r13"); // preserve the source-index register because the loop keeps it live across callback invocations - emitter.instruction("sub rsp, 16"); // reserve local slots for the source array pointer and source length + emitter.instruction("push r14"); // preserve the optional callback environment pointer across callback invocations + emitter.instruction("sub rsp, 24"); // reserve local slots for the source array pointer and source length emitter.instruction("mov r12, rdi"); // keep the callback address in a callee-saved register across the walk loop - emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // save the source array pointer so the loop can reload it after callback calls + emitter.instruction("mov r14, rdx"); // keep the optional callback environment pointer across every callback invocation + emitter.instruction("mov QWORD PTR [rbp - 32], rsi"); // save the source array pointer so the loop can reload it after callback calls emitter.instruction("mov r10, QWORD PTR [rsi]"); // load the source array length from the first field of the array header - emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // save the source array length for loop termination checks + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save the source array length for loop termination checks emitter.instruction("xor r13d, r13d"); // start the source index at zero before walking the source array emitter.label("__rt_array_walk_loop"); - emitter.instruction("cmp r13, QWORD PTR [rbp - 32]"); // stop once the source index reaches the saved source-array length + emitter.instruction("cmp r13, QWORD PTR [rbp - 40]"); // stop once the source index reaches the saved source-array length emitter.instruction("jge __rt_array_walk_done"); // finish walking once every source element has been passed to the callback - emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the source array pointer after the previous callback invocation + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the source array pointer after the previous callback invocation emitter.instruction("mov rdi, QWORD PTR [r10 + r13 * 8 + 24]"); // load the current source element into the first SysV integer argument register + emitter.instruction("test r14, r14"); // check whether this runtime call carries a callback capture environment + emitter.instruction("jz __rt_array_walk_call_linux_x86_64"); // keep legacy one-argument callback ABI when no environment is present + emitter.instruction("mov rsi, r14"); // pass capture environment as the wrapper's second argument + emitter.label("__rt_array_walk_call_linux_x86_64"); emitter.instruction("call r12"); // invoke the user callback with the current source element and ignore the scalar return value emitter.instruction("add r13, 1"); // advance the source index after visiting the current element emitter.instruction("jmp __rt_array_walk_loop"); // continue walking until the whole source array has been visited emitter.label("__rt_array_walk_done"); - emitter.instruction("add rsp, 16"); // release the walk local bookkeeping slots before restoring callee-saved registers + emitter.instruction("add rsp, 24"); // release the walk local bookkeeping slots before restoring callee-saved registers + emitter.instruction("pop r14"); // restore the caller callback-environment callee-saved register emitter.instruction("pop r13"); // restore the caller source-index callee-saved register emitter.instruction("pop r12"); // restore the caller callback callee-saved register emitter.instruction("pop rbp"); // restore the caller frame pointer before returning void diff --git a/src/codegen/runtime/arrays/usort.rs b/src/codegen/runtime/arrays/usort.rs index 9e590da1..1e7dd80a 100644 --- a/src/codegen/runtime/arrays/usort.rs +++ b/src/codegen/runtime/arrays/usort.rs @@ -12,7 +12,7 @@ use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; /// usort: sort an integer array in-place using a user-defined comparison callback. -/// Input: x0 = callback function address, x1 = array pointer +/// Input: x0 = callback function address, x1 = array pointer, x2 = optional callback environment pointer /// Output: none (sorts in place) /// The callback receives (a, b) and returns negative/zero/positive for ordering. /// Uses bubble sort for simplicity. @@ -27,11 +27,12 @@ pub fn emit_usort(emitter: &mut Emitter) { emitter.label_global("__rt_usort"); // -- set up stack frame, save callee-saved registers -- - emitter.instruction("sub sp, sp, #64"); // allocate 64 bytes on the stack - emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #48"); // set up new frame pointer - emitter.instruction("stp x19, x20, [sp, #32]"); // save callee-saved x19, x20 - emitter.instruction("stp x21, x22, [sp, #16]"); // save callee-saved x21, x22 + emitter.instruction("sub sp, sp, #80"); // allocate 80 bytes on the stack + emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #64"); // set up new frame pointer + emitter.instruction("stp x19, x20, [sp, #48]"); // save callee-saved x19, x20 + emitter.instruction("stp x21, x22, [sp, #32]"); // save callee-saved x21, x22 + emitter.instruction("str x2, [sp, #16]"); // save optional callback environment pointer to stack emitter.instruction("mov x19, x0"); // x19 = callback address (callee-saved) emitter.instruction("str x1, [sp, #0]"); // save array pointer to stack @@ -57,11 +58,13 @@ pub fn emit_usort(emitter: &mut Emitter) { emitter.instruction("ldr x0, [x9, x22, lsl #3]"); // x0 = data[j] (first element) emitter.instruction("add x10, x22, #1"); // x10 = j + 1 emitter.instruction("ldr x1, [x9, x10, lsl #3]"); // x1 = data[j+1] (second element) - - // -- save data base pointer and element values for potential swap -- - emitter.instruction("str x9, [sp, #8]"); // save data base pointer + emitter.instruction("str x9, [sp, #8]"); // save data base pointer before the callback can clobber x9 + emitter.instruction("ldr x11, [sp, #16]"); // load optional callback environment pointer + emitter.instruction("cbz x11, __rt_usort_call"); // keep legacy two-argument comparator ABI when no environment is present + emitter.instruction("mov x2, x11"); // pass capture environment after the compared pair // -- call comparator callback(a, b) -- + emitter.label("__rt_usort_call"); emitter.instruction("blr x19"); // call callback(data[j], data[j+1]) → x0=result // -- if result > 0, swap elements -- @@ -90,10 +93,10 @@ pub fn emit_usort(emitter: &mut Emitter) { emitter.label("__rt_usort_done"); // -- tear down stack frame and return -- - emitter.instruction("ldp x21, x22, [sp, #16]"); // restore callee-saved x21, x22 - emitter.instruction("ldp x19, x20, [sp, #32]"); // restore callee-saved x19, x20 - emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #64"); // deallocate stack frame + emitter.instruction("ldp x21, x22, [sp, #32]"); // restore callee-saved x21, x22 + emitter.instruction("ldp x19, x20, [sp, #48]"); // restore callee-saved x19, x20 + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #80"); // deallocate stack frame emitter.instruction("ret"); // return (void, array sorted in place) } @@ -109,8 +112,10 @@ fn emit_usort_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push r13"); // preserve the indexed-array pointer across nested comparator calls emitter.instruction("push r14"); // preserve the indexed-array length across nested comparator calls emitter.instruction("push r15"); // preserve the swapped flag across nested comparator calls + emitter.instruction("sub rsp, 16"); // reserve a local slot for the optional callback environment pointer emitter.instruction("mov r12, rdi"); // preserve the comparator callback address in a callee-saved register for the whole bubble-sort pass emitter.instruction("mov r13, rsi"); // preserve the indexed-array pointer in a callee-saved register for the whole bubble-sort pass + emitter.instruction("mov QWORD PTR [rbp - 48], rdx"); // save optional callback environment pointer for captured comparator wrappers emitter.instruction("mov r14, QWORD PTR [r13]"); // load the indexed-array logical length once before the bubble-sort passes begin emitter.instruction("cmp r14, 2"); // does the indexed array contain fewer than two elements? emitter.instruction("jl __rt_usort_done_linux_x86_64"); // arrays of length zero or one are already sorted @@ -128,6 +133,10 @@ fn emit_usort_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdi, QWORD PTR [r10 + rbx * 8]"); // load the left comparator argument from the current indexed-array slot emitter.instruction("lea r11, [rbx + 1]"); // derive the right adjacent slot index before loading the second comparator argument emitter.instruction("mov rsi, QWORD PTR [r10 + r11 * 8]"); // load the right comparator argument from the adjacent indexed-array slot + emitter.instruction("cmp QWORD PTR [rbp - 48], 0"); // check whether this runtime call carries a callback capture environment + emitter.instruction("je __rt_usort_call_linux_x86_64"); // keep legacy two-argument comparator ABI when no environment is present + emitter.instruction("mov rdx, QWORD PTR [rbp - 48]"); // pass capture environment after the compared pair + emitter.label("__rt_usort_call_linux_x86_64"); emitter.instruction("call r12"); // invoke the user comparator callback on the current adjacent indexed-array pair emitter.instruction("cmp rax, 0"); // did the comparator report that the current adjacent pair is already ordered? emitter.instruction("jle __rt_usort_noswap_linux_x86_64"); // skip the swap path when the comparator says the left element should stay before the right element @@ -148,6 +157,7 @@ fn emit_usort_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jnz __rt_usort_outer_linux_x86_64"); // repeat another bubble-sort pass while at least one adjacent pair was swapped emitter.label("__rt_usort_done_linux_x86_64"); + emitter.instruction("add rsp, 16"); // release the comparator environment slot before restoring callee-saved registers emitter.instruction("pop r15"); // restore the saved swapped-flag register after the x86_64 usort() helper finishes emitter.instruction("pop r14"); // restore the saved indexed-array length register after the x86_64 usort() helper finishes emitter.instruction("pop r13"); // restore the saved indexed-array pointer register after the x86_64 usort() helper finishes diff --git a/src/types/checker/builtins/callables.rs b/src/types/checker/builtins/callables.rs index 55035a7f..77209bc4 100644 --- a/src/types/checker/builtins/callables.rs +++ b/src/types/checker/builtins/callables.rs @@ -82,7 +82,6 @@ pub(super) fn check_builtin( "array_reduce() takes exactly 3 arguments", )); } - checker.reject_captured_first_class_callable_callback(&args[1], span, "array_reduce")?; for arg in args { checker.infer_type(arg, env)?; } @@ -99,7 +98,6 @@ pub(super) fn check_builtin( if args.len() != 2 { return Err(CompileError::new(span, "array_walk() takes exactly 2 arguments")); } - checker.reject_captured_first_class_callable_callback(&args[1], span, "array_walk")?; for arg in args { checker.infer_type(arg, env)?; } @@ -116,7 +114,6 @@ pub(super) fn check_builtin( &format!("{}() takes exactly 2 arguments", name), )); } - checker.reject_captured_first_class_callable_callback(&args[1], span, name)?; for arg in args { checker.infer_type(arg, env)?; } diff --git a/src/types/checker/callables/captures.rs b/src/types/checker/callables/captures.rs deleted file mode 100644 index 808db97d..00000000 --- a/src/types/checker/callables/captures.rs +++ /dev/null @@ -1,88 +0,0 @@ -//! Purpose: -//! Type-checks callable captures behavior. -//! Infers callable signatures and validates invocation details that affect later lowering and optimizer effects. -//! -//! Called from: -//! - `crate::types::checker::callables` -//! - `crate::types::checker::inference` -//! -//! Key details: -//! - Closure captures, first-class callable syntax, and extern calls must agree with shared call argument planning. - -use crate::errors::CompileError; -use crate::parser::ast::{CallableTarget, Expr, ExprKind, StaticReceiver}; - -use super::super::Checker; - -impl Checker { -pub(crate) fn first_class_callable_target_needs_runtime_capture( - target: &CallableTarget, - ) -> bool { - matches!( - target, - CallableTarget::Method { .. } - | CallableTarget::StaticMethod { - receiver: StaticReceiver::Static, - .. - } - ) - } - - pub(crate) fn reject_captured_first_class_callable_callback( - &self, - callback: &Expr, - span: crate::span::Span, - builtin: &str, - ) -> Result<(), CompileError> { - let target = match &callback.kind { - ExprKind::FirstClassCallable(target) => Some(target), - ExprKind::Variable(var_name) => self.first_class_callable_targets.get(var_name), - _ => None, - }; - if target.is_some_and(Self::first_class_callable_target_needs_runtime_capture) { - return Err(CompileError::new( - span, - &format!( - "{}() does not support captured first-class callable targets yet", - builtin - ), - )); - } - Ok(()) - } - - pub(crate) fn expr_call_callee_needs_runtime_capture(&self, callee: &Expr) -> bool { - match &callee.kind { - ExprKind::Closure { captures, .. } => !captures.is_empty(), - ExprKind::FirstClassCallable(target) => { - Self::first_class_callable_target_needs_runtime_capture(target) - } - ExprKind::Variable(var_name) => { - self.callable_captures - .get(var_name) - .is_some_and(|captures| !captures.is_empty()) - || self - .first_class_callable_targets - .get(var_name) - .is_some_and(Self::first_class_callable_target_needs_runtime_capture) - } - ExprKind::Assignment { value, .. } => { - self.expr_call_callee_needs_runtime_capture(value) - } - ExprKind::Ternary { - then_expr, - else_expr, - .. - } => { - self.expr_call_callee_needs_runtime_capture(then_expr) - || self.expr_call_callee_needs_runtime_capture(else_expr) - } - ExprKind::ShortTernary { value, default } - | ExprKind::NullCoalesce { value, default } => { - self.expr_call_callee_needs_runtime_capture(value) - || self.expr_call_callee_needs_runtime_capture(default) - } - _ => false, - } - } -} diff --git a/src/types/checker/callables/first_class.rs b/src/types/checker/callables/first_class.rs index 05a67852..6350a45a 100644 --- a/src/types/checker/callables/first_class.rs +++ b/src/types/checker/callables/first_class.rs @@ -157,12 +157,6 @@ impl Checker { Ok(Self::callable_wrapper_sig(&effective_sig)) } CallableTarget::Method { object, method } => { - if !matches!(&object.kind, ExprKind::Variable(_) | ExprKind::This) { - return Err(CompileError::new( - span, - "First-class method callable requires a variable or $this receiver", - )); - } let object_ty = self.infer_type(object, env)?; match object_ty { PhpType::Object(class_name) => { diff --git a/src/types/checker/callables/mod.rs b/src/types/checker/callables/mod.rs index 8a82c8c2..0802f250 100644 --- a/src/types/checker/callables/mod.rs +++ b/src/types/checker/callables/mod.rs @@ -8,7 +8,6 @@ //! Key details: //! - Callable targets must preserve parameter order, capture ownership expectations, and builtin alias resolution. -mod captures; mod closures; mod extern_calls; mod first_class; diff --git a/src/types/checker/inference/ops.rs b/src/types/checker/inference/ops.rs index 32ff2e4c..cbee8df7 100644 --- a/src/types/checker/inference/ops.rs +++ b/src/types/checker/inference/ops.rs @@ -10,7 +10,9 @@ use crate::errors::CompileError; use crate::names::Name; -use crate::parser::ast::{BinOp, Expr, ExprKind, InstanceOfTarget, Stmt, TypeExpr}; +use crate::parser::ast::{ + BinOp, CallableTarget, Expr, ExprKind, InstanceOfTarget, StaticReceiver, Stmt, TypeExpr, +}; use crate::types::{merge_array_key_types, FunctionSig, PhpType, TypeEnv}; use super::super::Checker; @@ -397,10 +399,10 @@ impl Checker { ), )); } - if self.expr_call_callee_needs_runtime_capture(callee) { + if self.expr_call_complex_callee_needs_runtime_capture(callee) { return Err(CompileError::new( expr.span, - "Direct calls of captured callable expressions are not supported yet", + "Direct calls of complex captured callable expressions are not supported yet", )); } match &callee.kind { @@ -487,6 +489,73 @@ impl Checker { } } + fn expr_call_complex_callee_needs_runtime_capture(&self, callee: &Expr) -> bool { + match &callee.kind { + ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) | ExprKind::Variable(_) => { + false + } + ExprKind::Assignment { value, .. } => self.expr_produces_captured_callable(value), + ExprKind::Ternary { + then_expr, + else_expr, + .. + } => { + self.expr_produces_captured_callable(then_expr) + || self.expr_produces_captured_callable(else_expr) + } + ExprKind::ShortTernary { value, default } + | ExprKind::NullCoalesce { value, default } => { + self.expr_produces_captured_callable(value) + || self.expr_produces_captured_callable(default) + } + _ => false, + } + } + + fn expr_produces_captured_callable(&self, expr: &Expr) -> bool { + match &expr.kind { + ExprKind::Closure { captures, .. } => !captures.is_empty(), + ExprKind::FirstClassCallable(target) => { + Self::first_class_callable_target_needs_runtime_capture(target) + } + ExprKind::Variable(var_name) => { + self.callable_captures + .get(var_name) + .is_some_and(|captures| !captures.is_empty()) + || self + .first_class_callable_targets + .get(var_name) + .is_some_and(Self::first_class_callable_target_needs_runtime_capture) + } + ExprKind::Assignment { value, .. } => self.expr_produces_captured_callable(value), + ExprKind::Ternary { + then_expr, + else_expr, + .. + } => { + self.expr_produces_captured_callable(then_expr) + || self.expr_produces_captured_callable(else_expr) + } + ExprKind::ShortTernary { value, default } + | ExprKind::NullCoalesce { value, default } => { + self.expr_produces_captured_callable(value) + || self.expr_produces_captured_callable(default) + } + _ => false, + } + } + + fn first_class_callable_target_needs_runtime_capture(target: &CallableTarget) -> bool { + matches!( + target, + CallableTarget::Method { .. } + | CallableTarget::StaticMethod { + receiver: StaticReceiver::Static, + .. + } + ) + } + /// Type-checks the PHP 8.5 pipe operator: `value |> callable`. /// /// Semantics: `callable` must evaluate to a callable, and the result is diff --git a/tests/codegen/oop/callables/methods.rs b/tests/codegen/oop/callables/methods.rs index 9eca8e20..00ce2faf 100644 --- a/tests/codegen/oop/callables/methods.rs +++ b/tests/codegen/oop/callables/methods.rs @@ -1,5 +1,6 @@ //! Purpose: -//! Integration or regression tests for end-to-end codegen coverage of object-oriented PHP, callables methods, including first class callable instance method call user func with capture, first class callable inline instance method call user func with capture, and first class callable instance method call user func array with capture. +//! Integration or regression tests for object-oriented callable codegen, including captured method +//! and static first-class callables used through direct calls, indirect calls, and callback builtins. //! //! Called from: //! - `cargo test` through Rust's test harness. @@ -346,3 +347,278 @@ ChildMapper::run(); ); assert_eq!(out, "11:12|21:22"); } + +#[test] +fn test_first_class_callable_instance_method_array_reduce_with_capture() { + let out = compile_and_run( + r#"add_offset(...); +echo array_reduce([1, 2], $fn, 0); +"#, + ); + assert_eq!(out, "23"); +} + +#[test] +fn test_first_class_callable_instance_method_array_walk_with_capture() { + let out = compile_and_run( + r#"show(...)); +"#, + ); + assert_eq!(out, "6:7:"); +} + +#[test] +fn test_first_class_callable_instance_method_usort_with_capture() { + let out = compile_and_run( + r#"desc(...)); +foreach ($values as $value) { + echo $value; +} +"#, + ); + assert_eq!(out, "321"); +} + +#[test] +fn test_first_class_callable_instance_method_uksort_with_capture() { + let out = compile_and_run( + r#"desc(...)); +foreach ($values as $value) { + echo $value; +} +"#, + ); + assert_eq!(out, "321"); +} + +#[test] +fn test_first_class_callable_instance_method_uasort_with_capture() { + let out = compile_and_run( + r#"asc(...)); +foreach ($values as $value) { + echo $value; +} +"#, + ); + assert_eq!(out, "123"); +} + +#[test] +fn test_first_class_callable_static_late_bound_array_reduce_with_capture() { + let out = compile_and_run( + r#"greet(...))("Ada"); +"#, + ); + assert_eq!(out, "Hi Ada"); +} + +#[test] +fn test_direct_first_class_callable_expr_call_evaluates_receiver_before_args() { + let out = compile_and_run( + r#"greet(...))(name_arg()); +"#, + ); + assert_eq!(out, "receiver:arg:Ada"); +} + +#[test] +fn test_parenthesized_captured_first_class_callable_variable_expr_call() { + let out = compile_and_run( + r#"apply(...); +echo ($fn)(5); +"#, + ); + assert_eq!(out, "12"); +} + +#[test] +fn test_first_class_callable_non_local_method_receiver() { + let out = compile_and_run( + r#"prefix . $name; + } +} + +$fn = (new Greeter("Hi "))->greet(...); +echo $fn("Ada"); +"#, + ); + assert_eq!(out, "Hi Ada"); +} diff --git a/tests/error_tests/misc/functions.rs b/tests/error_tests/misc/functions.rs index 242b239d..a3c23f82 100644 --- a/tests/error_tests/misc/functions.rs +++ b/tests/error_tests/misc/functions.rs @@ -34,58 +34,36 @@ fn test_error_first_class_callable_method_requires_object_receiver() { } #[test] -fn test_error_first_class_callable_method_requires_stable_receiver() { - expect_error( - "greet(...);", - "First-class method callable requires a variable or $this receiver", - ); -} - -#[test] -fn test_error_direct_expr_call_rejects_captured_first_class_method_callable() { - expect_error( - "greet(...))();", - "Direct calls of captured callable expressions are not supported yet", - ); -} - -#[test] -fn test_error_direct_expr_call_rejects_captured_static_first_class_callable() { - expect_error( - "greet(...); echo ($f)();", - "Direct calls of captured callable expressions are not supported yet", + "add(...); array_reduce([1, 2], $f, 0);", - "array_reduce() does not support captured first-class callable targets yet", + "inc(...) : $counter->inc(...))(1); +"#, + "Direct calls of complex captured callable expressions are not supported yet", ); } From 42172def43563fdf3f108a6cc4ee0f6b3751f4d0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 15 May 2026 22:10:16 +0200 Subject: [PATCH 2/5] feat: finish callable parity follow-up --- docs/internals/the-codegen.md | 2 +- docs/internals/the-parser.md | 2 +- docs/php/functions.md | 4 +- examples/callbacks/main.php | 12 ++ .../builtins/arrays/call_user_func_array.rs | 24 +++ src/types/checker/builtins/callables.rs | 51 +++---- src/types/signatures.rs | 144 +++++++++++++++++- .../codegen/callables/constants_and_system.rs | 53 +++++++ .../oop/callables/functions_and_builtins.rs | 48 ++++++ tests/error_tests/array_builtins.rs | 12 +- tests/error_tests/misc/functions.rs | 4 +- 11 files changed, 310 insertions(+), 46 deletions(-) diff --git a/docs/internals/the-codegen.md b/docs/internals/the-codegen.md index 77582d37..d3882f90 100644 --- a/docs/internals/the-codegen.md +++ b/docs/internals/the-codegen.md @@ -507,7 +507,7 @@ When a closure variable is called (`$fn(1, 2)`), the codegen: Built-in functions like `array_map`, `array_filter`, `array_reduce`, `array_walk`, `usort`, `uksort`, and `uasort` accept callback values. The callback function pointer is passed in a register (like any other `Callable` argument) and the runtime routine calls it via `blr`. -For captured closures passed through callback runtimes such as `array_map`, `array_filter`, `array_reduce`, `array_walk`, `usort`, `uksort`, and `uasort`, codegen builds a temporary callback environment containing the original closure pointer plus its hidden `use (...)` values. The runtime passes that environment to a generated callback wrapper, and the wrapper re-materializes the original visible arguments plus hidden captures before calling the closure. `call_user_func()` and `call_user_func_array()` do not need a runtime loop, so they append the hidden capture arguments directly at the indirect call site. +For captured closures passed through callback runtimes such as `array_map`, `array_filter`, `array_reduce`, `array_walk`, `usort`, `uksort`, and `uasort`, codegen builds a temporary callback environment containing the original closure pointer plus its hidden `use (...)` values. The runtime passes that environment to a generated callback wrapper, and the wrapper re-materializes the original visible arguments plus hidden captures before calling the closure. `call_user_func()` and `call_user_func_array()` do not need a runtime loop, so they append the hidden capture arguments directly at the indirect call site. When `call_user_func_array()` targets a by-reference callback and receives a literal argument array, codegen passes frame-slot addresses for variable elements in by-reference positions instead of loading array payload values. First-class callable wrappers reuse this hidden argument path when the callable target carries context. `$obj->method(...)` records the receiver as a hidden capture; non-local receiver expressions are evaluated once into a hidden temporary before wrapper creation. `static::method(...)` records the forwarded called-class id, or `$this` in an instance method, so late static binding is preserved for direct callable calls and for callback paths that forward an environment. diff --git a/docs/internals/the-parser.md b/docs/internals/the-parser.md index 1e43733e..a2b38cc1 100644 --- a/docs/internals/the-parser.md +++ b/docs/internals/the-parser.md @@ -85,7 +85,7 @@ Things that have a value: | `NullsafePropertyAccess { object, property }` | `$p?->x` | Nullsafe property access via `?->` | | `StaticPropertyAccess { receiver, property }` | `Point::$count`, `self::$count`, `parent::$count`, `static::$count` | Class-scoped property access via `::`, where `receiver` is a named class, `Self_`, `Static`, or `Parent` | | `MethodCall { object, method, args }` | `$p->move(1, 2)` | Instance method call | -| `NullsafeMethodCall { object, method, args }` | `$p?->move(1, 2)` | Nullsafe instance method call; `?->method(...)` cannot form a first-class callable | +| `NullsafeMethodCall { object, method, args }` | `$p?->move(1, 2)` | Nullsafe instance method call; PHP rejects `?->method(...)` closure creation, so elephc reports `Cannot combine nullsafe operator with Closure creation` for that form | | `StaticMethodCall { receiver, method, args }` | `Point::origin()`, `self::boot()`, `parent::boot()`, `static::boot()` | Static-style call via `::`, where `receiver` is a named class, `Self_`, `Static`, or `Parent` | | `FirstClassCallable(CallableTarget)` | `strlen(...)`, `Tools\fmt(...)`, `Math::twice(...)` | PHP-style first-class callable syntax; the target is preserved structurally instead of being parsed as a call | | `This` | `$this` | Reference to the current object inside a method | diff --git a/docs/php/functions.md b/docs/php/functions.md index 1da64f2c..d21ca5b3 100644 --- a/docs/php/functions.md +++ b/docs/php/functions.md @@ -155,7 +155,7 @@ $triple = triple(...); $double = MathBox::double(...); ``` -Supported: user-defined function names, extern function names, `ClassName::method(...)`, `self::method(...)`, `parent::method(...)`, and the registered builtin wrappers `strlen(...)`, `count(...)`, `buffer_len(...)`, `intval(...)`, `strtolower(...)`, `strtoupper(...)`, `ucfirst(...)`, `lcfirst(...)`, `strrev(...)`, `addslashes(...)`, `stripslashes(...)`, `nl2br(...)`, `bin2hex(...)`, `hex2bin(...)`, `htmlspecialchars(...)`, `htmlentities(...)`, `html_entity_decode(...)`, `urlencode(...)`, `urldecode(...)`, `rawurlencode(...)`, `rawurldecode(...)`, `base64_encode(...)`, `base64_decode(...)`, `json_encode(...)`, `json_decode(...)`, `json_validate(...)`, `json_last_error(...)`, `json_last_error_msg(...)`, `array_sum(...)`, and `array_product(...)`. +Supported: user-defined function names, extern function names, `ClassName::method(...)`, `self::method(...)`, `parent::method(...)`, and registered builtin wrappers. Builtin wrapper coverage includes common string transforms and searches (`strlen(...)`, `trim(...)`, `substr(...)`, `str_contains(...)`), casts and type checks (`intval(...)`, `floatval(...)`, `gettype(...)`, `is_int(...)`), math helpers (`abs(...)`, `sqrt(...)`, `round(...)`), JSON helpers, and array helpers including `count(...)`, `array_sum(...)`, `array_product(...)`, and by-reference mutators such as `sort(...)`. Also supported: `static::method(...)` inside class methods, preserving late static binding for direct callable calls, and `$obj->method(...)` / `$this->method(...)` with either a local receiver variable or a non-local receiver expression such as `(new Greeter("Hi "))->greet(...)`. ```php @@ -171,7 +171,7 @@ $hello = $greeter->hello(...); echo $hello("Ada"); // Hello Ada ``` -Captured first-class callable targets (`static::method(...)` and `$obj->method(...)`) can be called directly through a local callable variable or as an immediate callable expression such as `($obj->method(...))("Ada")`. They can also be passed to callback paths that forward captured callable environments, including `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `usort()`, `uksort()`, `uasort()`, `call_user_func()`, and `call_user_func_array()`. Nullsafe first-class callable syntax (`$obj?->method(...)`) is not supported yet. +Captured first-class callable targets (`static::method(...)` and `$obj->method(...)`) can be called directly through a local callable variable or as an immediate callable expression such as `($obj->method(...))("Ada")`. They can also be passed to callback paths that forward captured callable environments, including `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `usort()`, `uksort()`, `uasort()`, `call_user_func()`, and `call_user_func_array()`. For by-reference callback parameters, `call_user_func_array()` supports literal argument arrays whose by-reference positions are variables, such as `call_user_func_array($cb, [$value])`. PHP disallows nullsafe first-class callable syntax (`$obj?->method(...)`), and elephc reports the same error. ## Global variables diff --git a/examples/callbacks/main.php b/examples/callbacks/main.php index 8bc436e3..7a310731 100644 --- a/examples/callbacks/main.php +++ b/examples/callbacks/main.php @@ -57,6 +57,18 @@ public function bracket(string $value): string { echo "\n"; echo "method callable call_user_func_array: " . call_user_func_array($format, ["cb"]) . "\n"; +function bump(&$value) { + $value = $value + 1; +} + +$bump = bump(...); +$counter_value = 10; +call_user_func_array($bump, [$counter_value]); +echo "call_user_func_array by-ref: " . $counter_value . "\n"; + +$trim = trim(...); +echo "builtin callable trim: " . $trim(" ready ") . "\n"; + class OffsetCallbacks { public function add_offset($carry, $item) { return $carry + $item + 10; diff --git a/src/codegen/builtins/arrays/call_user_func_array.rs b/src/codegen/builtins/arrays/call_user_func_array.rs index 06f649b5..5ed9c9c6 100644 --- a/src/codegen/builtins/arrays/call_user_func_array.rs +++ b/src/codegen/builtins/arrays/call_user_func_array.rs @@ -129,6 +129,10 @@ pub fn emit( _ => PhpType::Int, }; let elem_size = args::array_element_stride(&elem_ty); + let literal_arg_elems = match &args[1].kind { + ExprKind::ArrayLiteral(elems) => Some(elems.as_slice()), + _ => None, + }; emitter.instruction(&format!("mov {}, {}", array_reg, abi::int_result_reg(emitter))); // preserve the callback-argument array pointer across element boxing abi::emit_load_from_address(emitter, len_reg, array_reg, 0); // load callback-argument array length @@ -142,6 +146,26 @@ pub fn emit( visible_param_count }; for i in 0..regular_param_count { + let is_ref = sig.ref_params.get(i).copied().unwrap_or(false); + if is_ref { + if let Some(Expr { + kind: ExprKind::Variable(var_name), + .. + }) = literal_arg_elems.and_then(|elems| elems.get(i)) + { + if !args::emit_ref_arg_variable_address( + var_name, + "call_user_func_array ref arg", + emitter, + ctx, + ) { + panic!("call_user_func_array() by-reference callback argument variable not found"); + } + args::push_arg_value(emitter, &PhpType::Int); + arg_types.push(PhpType::Int); + continue; + } + } let has_default = sig.defaults.get(i).and_then(|d| d.as_ref()).is_some(); let target_ty = if args::declared_target_ty(Some(&sig), i).is_some() || has_default { sig.params.get(i).map(|(_, ty)| ty) diff --git a/src/types/checker/builtins/callables.rs b/src/types/checker/builtins/callables.rs index 77209bc4..6261f028 100644 --- a/src/types/checker/builtins/callables.rs +++ b/src/types/checker/builtins/callables.rs @@ -16,6 +16,23 @@ use super::super::Checker; type BuiltinResult = Result, CompileError>; +fn validate_call_user_func_array_ref_args( + sig: &crate::types::FunctionSig, + arg_array: &Expr, + span: crate::span::Span, +) -> Result<(), CompileError> { + if !sig.ref_params.iter().any(|is_ref| *is_ref) { + return Ok(()); + } + if matches!(arg_array.kind, ExprKind::ArrayLiteral(_)) { + return Ok(()); + } + Err(CompileError::new( + span, + "call_user_func_array() requires a literal argument array when the callback has pass-by-reference parameters", + )) +} + pub(super) fn check_builtin( checker: &mut Checker, name: &str, @@ -142,12 +159,7 @@ pub(super) fn check_builtin( _ => &[], }; let sig = checker.specialize_first_class_callable_target(target, elems, span, env)?; - if sig.ref_params.iter().any(|is_ref| *is_ref) { - return Err(CompileError::new( - span, - "call_user_func_array() does not support pass-by-reference callback parameters yet", - )); - } + validate_call_user_func_array_ref_args(&sig, &args[1], span)?; if let ExprKind::ArrayLiteral(elems) = &args[1].kind { let ret_ty = checker.check_known_callable_call( &sig, @@ -172,12 +184,7 @@ pub(super) fn check_builtin( checker .closure_return_types .insert(var_name.clone(), sig.return_type.clone()); - if sig.ref_params.iter().any(|is_ref| *is_ref) { - return Err(CompileError::new( - span, - "call_user_func_array() does not support pass-by-reference callback parameters yet", - )); - } + validate_call_user_func_array_ref_args(&sig, &args[1], span)?; if let ExprKind::ArrayLiteral(elems) = &args[1].kind { let ret_ty = checker.check_known_callable_call( &sig, @@ -193,12 +200,7 @@ pub(super) fn check_builtin( } if let ExprKind::StringLiteral(cb_name) = &args[0].kind { if let Some(sig) = checker.functions.get(cb_name.as_str()).cloned() { - if sig.ref_params.iter().any(|is_ref| *is_ref) { - return Err(CompileError::new( - span, - "call_user_func_array() does not support pass-by-reference callback parameters yet", - )); - } + validate_call_user_func_array_ref_args(&sig, &args[1], span)?; if let ExprKind::ArrayLiteral(elems) = &args[1].kind { let ret_ty = checker.check_known_callable_call( &sig, @@ -212,10 +214,12 @@ pub(super) fn check_builtin( return Ok(Some(sig.return_type.clone())); } if let Some(decl) = checker.fn_decls.get(cb_name.as_str()).cloned() { - if decl.ref_params.iter().any(|is_ref| *is_ref) { + if decl.ref_params.iter().any(|is_ref| *is_ref) + && !matches!(args[1].kind, ExprKind::ArrayLiteral(_)) + { return Err(CompileError::new( span, - "call_user_func_array() does not support pass-by-reference callback parameters yet", + "call_user_func_array() requires a literal argument array when the callback has pass-by-reference parameters", )); } } @@ -234,12 +238,7 @@ pub(super) fn check_builtin( } } if let Some(sig) = checker.resolve_expr_callable_sig(&args[0], env)? { - if sig.ref_params.iter().any(|is_ref| *is_ref) { - return Err(CompileError::new( - span, - "call_user_func_array() does not support pass-by-reference callback parameters yet", - )); - } + validate_call_user_func_array_ref_args(&sig, &args[1], span)?; if let ExprKind::ArrayLiteral(elems) = &args[1].kind { let ret_ty = checker.check_known_callable_call( &sig, diff --git a/src/types/signatures.rs b/src/types/signatures.rs index f8acb7b1..d1ba4068 100644 --- a/src/types/signatures.rs +++ b/src/types/signatures.rs @@ -362,25 +362,151 @@ pub(crate) fn first_class_callable_builtin_sig(name: &str) -> Option Option { match name { - "intval" => Some(typed_first_class_builtin_sig( + "time" | "json_last_error" => Some(typed_first_class_builtin_sig( + name, + &[], + PhpType::Int, + )), + "phpversion" | "getcwd" | "sys_get_temp_dir" | "json_last_error_msg" => { + Some(typed_first_class_builtin_sig(name, &[], PhpType::Str)) + } + "pi" => Some(typed_first_class_builtin_sig(name, &[], PhpType::Float)), + "intval" | "ord" => Some(typed_first_class_builtin_sig( name, &[PhpType::Str], PhpType::Int, )), + "floatval" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Mixed], + PhpType::Float, + )), + "boolval" | "is_bool" | "is_null" | "is_float" | "is_int" | "is_iterable" + | "is_string" | "is_numeric" | "is_nan" | "is_finite" | "is_infinite" + | "ctype_alpha" | "ctype_digit" | "ctype_alnum" | "ctype_space" => { + Some(typed_first_class_builtin_sig(name, &[PhpType::Mixed], PhpType::Bool)) + } + "gettype" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Mixed], + PhpType::Str, + )), "strtolower" | "strtoupper" | "ucfirst" | "lcfirst" | "strrev" | "addslashes" | "stripslashes" | "nl2br" | "bin2hex" | "hex2bin" | "htmlspecialchars" | "htmlentities" | "html_entity_decode" | "urlencode" | "urldecode" | "rawurlencode" | "rawurldecode" | "base64_encode" - | "base64_decode" | "json_last_error_msg" => Some(typed_first_class_builtin_sig( + | "base64_decode" | "trim" | "ltrim" | "rtrim" | "ucwords" | "substr" + | "str_repeat" | "strstr" | "str_replace" | "str_ireplace" | "explode" + | "implode" | "substr_replace" | "str_pad" | "str_split" | "wordwrap" + | "sprintf" | "hash" | "md5" | "sha1" | "number_format" | "chr" => { + Some(typed_first_class_builtin_sig( + name, + &[PhpType::Str], + PhpType::Str, + )) + } + "strpos" | "strrpos" | "strcmp" | "strcasecmp" => Some(typed_first_class_builtin_sig( name, &[PhpType::Str], - PhpType::Str, + PhpType::Int, + )), + "str_contains" | "str_starts_with" | "str_ends_with" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Str, PhpType::Str], + PhpType::Bool, + )), + "array_keys" | "array_values" | "array_reverse" | "array_unique" | "array_rand" => { + Some(typed_first_class_builtin_sig( + name, + &[PhpType::Array(Box::new(PhpType::Mixed))], + PhpType::Array(Box::new(PhpType::Mixed)), + )) + } + "array_chunk" | "array_pad" | "array_fill" | "array_slice" | "array_diff" + | "array_intersect" | "range" => return_typed_first_class_builtin_sig( + name, + PhpType::Array(Box::new(PhpType::Mixed)), + ), + "array_flip" | "array_combine" | "array_fill_keys" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Array(Box::new(PhpType::Mixed))], + PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(PhpType::Mixed), + }, )), "array_sum" | "array_product" => Some(typed_first_class_builtin_sig( name, &[PhpType::Array(Box::new(PhpType::Int))], PhpType::Int, )), + "array_key_exists" | "in_array" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Mixed, PhpType::Array(Box::new(PhpType::Mixed))], + PhpType::Bool, + )), + "array_search" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Mixed, PhpType::Array(Box::new(PhpType::Mixed)), PhpType::Bool], + PhpType::Mixed, + )), + "array_pop" | "array_shift" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Array(Box::new(PhpType::Mixed))], + PhpType::Mixed, + )), + "array_push" | "array_unshift" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Array(Box::new(PhpType::Mixed)), PhpType::Mixed], + PhpType::Void, + )), + "sort" | "rsort" | "shuffle" | "natsort" | "natcasesort" | "asort" + | "arsort" | "ksort" | "krsort" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Array(Box::new(PhpType::Mixed))], + PhpType::Void, + )), + "is_file" | "is_dir" | "is_readable" | "is_writable" | "is_writeable" + | "is_executable" | "is_link" | "file_exists" | "fnmatch" | "chmod" | "chown" + | "chgrp" | "touch" | "ftruncate" | "fflush" | "fsync" | "fdatasync" + | "symlink" | "link" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Str], + PhpType::Bool, + )), + "file_get_contents" | "file" | "filesize" | "filemtime" | "fileatime" + | "filectime" | "fileperms" | "fileowner" | "filegroup" | "fileinode" + | "filetype" | "stat" | "lstat" | "basename" | "dirname" | "realpath" + | "pathinfo" | "readlink" | "linkinfo" | "tempnam" => { + let mut sig = builtin_call_sig(name)?; + sig.return_type = PhpType::Mixed; + sig.declared_return = true; + Some(sig) + } + "abs" | "min" | "max" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Mixed], + PhpType::Mixed, + )), + "floor" | "ceil" | "sqrt" | "sin" | "cos" | "tan" | "asin" | "acos" | "atan" + | "sinh" | "cosh" | "tanh" | "log2" | "log10" | "exp" | "deg2rad" + | "rad2deg" | "microtime" | "log" | "atan2" | "hypot" | "pow" | "fmod" + | "fdiv" | "round" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Mixed], + PhpType::Float, + )), + "intdiv" | "rand" | "mt_rand" | "random_int" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Int, PhpType::Int], + PhpType::Int, + )), + "date" | "php_uname" | "readline" => { + let mut sig = builtin_call_sig(name)?; + sig.return_type = PhpType::Str; + sig.declared_return = true; + Some(sig) + } "json_encode" => Some(typed_first_class_builtin_sig( name, &[PhpType::Mixed, PhpType::Int, PhpType::Int], @@ -396,11 +522,6 @@ fn general_first_class_callable_builtin_sig(name: &str) -> Option { &[PhpType::Str, PhpType::Int, PhpType::Int], PhpType::Bool, )), - "json_last_error" => Some(typed_first_class_builtin_sig( - name, - &[], - PhpType::Int, - )), _ => None, } } @@ -421,6 +542,13 @@ fn typed_first_class_builtin_sig( sig } +fn return_typed_first_class_builtin_sig(name: &str, return_type: PhpType) -> Option { + let mut sig = builtin_call_sig(name)?; + sig.return_type = return_type; + sig.declared_return = true; + Some(sig) +} + fn fixed(params: &[&str]) -> FunctionSig { make_sig(params, vec![None; params.len()], None) } diff --git a/tests/codegen/callables/constants_and_system.rs b/tests/codegen/callables/constants_and_system.rs index c0c65bbd..81b0fff7 100644 --- a/tests/codegen/callables/constants_and_system.rs +++ b/tests/codegen/callables/constants_and_system.rs @@ -281,6 +281,59 @@ fn test_call_user_func_array_variadic_float_tail_count() { assert_eq!(out, "2"); } +#[test] +fn test_call_user_func_array_first_class_callable_preserves_by_ref_params() { + let out = compile_and_run( + r#"bump(...); +$value = 5; +call_user_func_array($f, [$value]); +echo $value; +"#, + ); + assert_eq!(out, "7"); +} + // -- v0.8 constants -- #[test] diff --git a/tests/codegen/oop/callables/functions_and_builtins.rs b/tests/codegen/oop/callables/functions_and_builtins.rs index c9ec5900..81ddee3f 100644 --- a/tests/codegen/oop/callables/functions_and_builtins.rs +++ b/tests/codegen/oop/callables/functions_and_builtins.rs @@ -68,6 +68,54 @@ echo $sum([2, 3, 5]); assert_eq!(out, "10"); } +#[test] +fn test_first_class_callable_builtin_trim() { + let out = compile_and_run( + r#" Date: Sat, 16 May 2026 11:08:57 +0200 Subject: [PATCH 3/5] refactor: centralize callback callable codegen --- src/codegen/builtins/arrays/array_filter.rs | 122 +++++------------- src/codegen/builtins/arrays/array_map.rs | 114 +++++----------- src/codegen/builtins/arrays/array_reduce.rs | 51 ++------ src/codegen/builtins/arrays/array_walk.rs | 39 +----- src/codegen/builtins/arrays/call_user_func.rs | 41 ++---- .../builtins/arrays/call_user_func_array.rs | 41 ++---- src/codegen/builtins/arrays/callback_env.rs | 36 ++++-- src/codegen/builtins/arrays/uasort.rs | 45 +------ src/codegen/builtins/arrays/uksort.rs | 45 +------ src/codegen/builtins/arrays/usort.rs | 45 +------ src/codegen/callables.rs | 66 ++++++++++ src/codegen/expr/calls/indirect.rs | 17 +-- src/codegen/mod.rs | 1 + 13 files changed, 194 insertions(+), 469 deletions(-) create mode 100644 src/codegen/callables.rs diff --git a/src/codegen/builtins/arrays/array_filter.rs b/src/codegen/builtins/arrays/array_filter.rs index e00869be..ac7dcde0 100644 --- a/src/codegen/builtins/arrays/array_filter.rs +++ b/src/codegen/builtins/arrays/array_filter.rs @@ -14,8 +14,7 @@ use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::expr::emit_expr; use crate::codegen::platform::Arch; -use crate::names::function_symbol; -use crate::parser::ast::{Expr, ExprKind}; +use crate::parser::ast::Expr; use crate::types::PhpType; use super::callback_env; @@ -33,106 +32,45 @@ pub fn emit( let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); - // -- evaluate the callback argument (may be a string literal or closure) -- - let is_closure = matches!( - &args[1].kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ); - let mut inline_captures = Vec::new(); - if is_closure { - emit_expr(&args[1], emitter, ctx, data); - inline_captures = callback_env::callback_captures(&args[1], ctx); - abi::emit_push_reg(emitter, result_reg); // save the synthesized callback address on the temporary stack - } - // -- evaluate the array argument (first arg) -- let arr_ty = emit_expr(&args[0], emitter, ctx, data); let uses_refcounted_runtime = filter_uses_payload_runtime(&arr_ty); - // -- save array pointer, then resolve the callback address into the target scratch register -- + // -- save array pointer, then evaluate the callback argument -- abi::emit_push_reg(emitter, result_reg); // push the source array pointer onto the temporary stack - if is_closure { - } else if let ExprKind::Variable(var_name) = &args[1].kind { - let var = ctx.variables.get(var_name).expect("undefined callback variable"); - let offset = var.stack_offset; - abi::load_at_offset(emitter, call_reg, offset); // load the callback address from the callable variable slot - } else { - let func_name = match &args[1].kind { - ExprKind::StringLiteral(name) => name.clone(), - _ => panic!("array_filter() callback must be a string literal, callable expression, or callable variable"), - }; - let label = function_symbol(&func_name); - abi::emit_symbol_address(emitter, call_reg, &label); // materialize the callback function address in the nested-call scratch register - } - let captures = if is_closure { - inline_captures - } else { - callback_env::callback_captures(&args[1], ctx) - }; + let captures = + callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); // -- place callback and array pointer into the runtime argument registers -- - if is_closure { - if captures.is_empty() { - abi::emit_pop_reg(emitter, array_arg_reg); // pop the source array pointer into the second runtime argument register - abi::emit_pop_reg(emitter, callback_arg_reg); // pop the synthesized callback address into the first runtime argument register - abi::emit_load_int_immediate(emitter, env_arg_reg, 0); - } else { - abi::emit_pop_reg(emitter, result_reg); // recover the source array pointer before building the capture environment - abi::emit_pop_reg(emitter, call_reg); // recover the original closure entry point for env slot zero - let wrapper = callback_env::emit_captured_callback_env( - call_reg, - result_reg, - &captures, - vec![filter_elem_type(&arr_ty)], - emitter, - ctx, - ); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - let runtime_label = if uses_refcounted_runtime { - "__rt_array_filter_refcounted" - } else { - "__rt_array_filter" - }; - abi::emit_call_label(emitter, runtime_label); - abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); - return match arr_ty { - PhpType::Array(elem_ty) => Some(PhpType::Array(elem_ty)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - }; - } + if captures.is_empty() { + abi::emit_pop_reg(emitter, array_arg_reg); // pop the source array pointer into the second runtime argument register + emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the callback function address into the first runtime argument register + abi::emit_load_int_immediate(emitter, env_arg_reg, 0); } else { - if captures.is_empty() { - abi::emit_pop_reg(emitter, array_arg_reg); // pop the source array pointer into the second runtime argument register - emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the callback function address into the first runtime argument register - abi::emit_load_int_immediate(emitter, env_arg_reg, 0); + abi::emit_pop_reg(emitter, result_reg); // recover the source array pointer before building the capture environment + let wrapper = callback_env::emit_captured_callback_env( + call_reg, + result_reg, + &captures, + vec![filter_elem_type(&arr_ty)], + emitter, + ctx, + ); + callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); + abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); + callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); + let runtime_label = if uses_refcounted_runtime { + "__rt_array_filter_refcounted" } else { - abi::emit_pop_reg(emitter, result_reg); // recover the source array pointer before building the capture environment - let wrapper = callback_env::emit_captured_callback_env( - call_reg, - result_reg, - &captures, - vec![filter_elem_type(&arr_ty)], - emitter, - ctx, - ); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - let runtime_label = if uses_refcounted_runtime { - "__rt_array_filter_refcounted" - } else { - "__rt_array_filter" - }; - abi::emit_call_label(emitter, runtime_label); - abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); - return match arr_ty { - PhpType::Array(elem_ty) => Some(PhpType::Array(elem_ty)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - }; - } + "__rt_array_filter" + }; + abi::emit_call_label(emitter, runtime_label); + abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); + return match arr_ty { + PhpType::Array(elem_ty) => Some(PhpType::Array(elem_ty)), + _ => Some(PhpType::Array(Box::new(PhpType::Int))), + }; } let runtime_label = if uses_refcounted_runtime { diff --git a/src/codegen/builtins/arrays/array_map.rs b/src/codegen/builtins/arrays/array_map.rs index 3307db25..eaa458f5 100644 --- a/src/codegen/builtins/arrays/array_map.rs +++ b/src/codegen/builtins/arrays/array_map.rs @@ -13,8 +13,7 @@ use crate::codegen::context::Context; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::expr::emit_expr; -use crate::names::function_symbol; -use crate::parser::ast::{Expr, ExprKind}; +use crate::parser::ast::Expr; use crate::types::PhpType; use super::array_map_callback_returns_str::callback_returns_str; use super::callback_env; @@ -40,98 +39,43 @@ pub fn emit( _ => PhpType::Int, }; - // -- evaluate the callback argument (may be a string literal or closure) -- - let is_closure = matches!( - &args[0].kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ); - let mut inline_captures = Vec::new(); - if is_closure { - emit_expr(&args[0], emitter, ctx, data); - inline_captures = callback_env::callback_captures(&args[0], ctx); - abi::emit_push_reg(emitter, result_reg); // save the synthesized callback address on the temporary stack - } + // -- evaluate the callback argument first, matching PHP source order -- + let captures = + callback_env::materialize_callback_address(&args[0], call_reg, emitter, ctx, data); + abi::emit_push_reg(emitter, call_reg); // save the callback address across mapped-array evaluation // -- evaluate the array argument -- let _arr_ty = emit_expr(&args[1], emitter, ctx, data); - // -- save array pointer, load callback address into the target nested-call scratch register -- + // -- save array pointer before preparing runtime arguments -- abi::emit_push_reg(emitter, result_reg); // push the array pointer onto the temporary stack - if is_closure { - } else if let ExprKind::Variable(var_name) = &args[0].kind { - let var = ctx.variables.get(var_name).expect("undefined callback variable"); - let offset = var.stack_offset; - abi::load_at_offset(emitter, call_reg, offset); // load the callback address from the callable variable slot - } else { - let func_name = match &args[0].kind { - ExprKind::StringLiteral(name) => name.clone(), - _ => panic!("array_map() callback must be a string literal, callable expression, or callable variable"), - }; - let label = function_symbol(&func_name); - abi::emit_symbol_address(emitter, call_reg, &label); // materialize the callback function address in the nested-call scratch register - } - let captures = if is_closure { - inline_captures + if captures.is_empty() { + abi::emit_pop_reg(emitter, array_arg_reg); // pop the mapped array pointer into the second runtime argument register + abi::emit_pop_reg(emitter, callback_arg_reg); // pop the callback address into the first runtime argument register + abi::emit_load_int_immediate(emitter, env_arg_reg, 0); } else { - callback_env::callback_captures(&args[0], ctx) - }; - - if is_closure { - if captures.is_empty() { - abi::emit_pop_reg(emitter, array_arg_reg); // pop the mapped array pointer into the second runtime argument register - abi::emit_pop_reg(emitter, callback_arg_reg); // pop the synthesized callback address into the first runtime argument register - abi::emit_load_int_immediate(emitter, env_arg_reg, 0); - } else { - abi::emit_pop_reg(emitter, result_reg); // recover the mapped array pointer before building the capture environment - abi::emit_pop_reg(emitter, call_reg); // recover the original closure entry point for env slot zero - let wrapper = callback_env::emit_captured_callback_env( - call_reg, - result_reg, - &captures, - vec![source_elem_ty.clone()], - emitter, - ctx, - ); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - if returns_str { - abi::emit_call_label(emitter, "__rt_array_map_str"); // call the string-producing array_map runtime helper - abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); - return Some(PhpType::Array(Box::new(PhpType::Str))); - } - abi::emit_call_label(emitter, "__rt_array_map"); // call the scalar array_map runtime helper + abi::emit_pop_reg(emitter, result_reg); // recover the mapped array pointer before building the capture environment + abi::emit_pop_reg(emitter, call_reg); // recover the callback entry point for env slot zero + let wrapper = callback_env::emit_captured_callback_env( + call_reg, + result_reg, + &captures, + vec![source_elem_ty.clone()], + emitter, + ctx, + ); + callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); + abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); + callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); + if returns_str { + abi::emit_call_label(emitter, "__rt_array_map_str"); // call the string-producing array_map runtime helper abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); - return Some(PhpType::Array(Box::new(PhpType::Int))); - } - } else { - if captures.is_empty() { - abi::emit_pop_reg(emitter, array_arg_reg); // pop the mapped array pointer into the second runtime argument register - emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the callback function address into the first runtime argument register - abi::emit_load_int_immediate(emitter, env_arg_reg, 0); - } else { - abi::emit_pop_reg(emitter, result_reg); // recover the mapped array pointer before building the capture environment - let wrapper = callback_env::emit_captured_callback_env( - call_reg, - result_reg, - &captures, - vec![source_elem_ty.clone()], - emitter, - ctx, - ); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - if returns_str { - abi::emit_call_label(emitter, "__rt_array_map_str"); // call the string-producing array_map runtime helper - abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); - return Some(PhpType::Array(Box::new(PhpType::Str))); - } - abi::emit_call_label(emitter, "__rt_array_map"); // call the scalar array_map runtime helper - abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); - return Some(PhpType::Array(Box::new(PhpType::Int))); + return Some(PhpType::Array(Box::new(PhpType::Str))); } + abi::emit_call_label(emitter, "__rt_array_map"); // call the scalar array_map runtime helper + abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); + return Some(PhpType::Array(Box::new(PhpType::Int))); } if returns_str { diff --git a/src/codegen/builtins/arrays/array_reduce.rs b/src/codegen/builtins/arrays/array_reduce.rs index 38e1b13f..3433adc4 100644 --- a/src/codegen/builtins/arrays/array_reduce.rs +++ b/src/codegen/builtins/arrays/array_reduce.rs @@ -13,8 +13,7 @@ use crate::codegen::context::Context; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::expr::emit_expr; -use crate::names::function_symbol; -use crate::parser::ast::{Expr, ExprKind}; +use crate::parser::ast::Expr; use crate::types::PhpType; use super::callback_env; @@ -37,46 +36,15 @@ pub fn emit( _ => PhpType::Int, }; - // -- evaluate the callback argument (may be a string literal or closure) -- - let is_closure = matches!( - &args[1].kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ); - let mut inline_captures = Vec::new(); - if is_closure { - emit_expr(&args[1], emitter, ctx, data); - inline_captures = callback_env::callback_captures(&args[1], ctx); - abi::emit_push_reg(emitter, result_reg); // save the synthesized callback address on the temporary stack - } - - // -- evaluate the array argument, then resolve the callback address into the target scratch register -- + // -- evaluate the array argument, then the callback argument -- emit_expr(&args[0], emitter, ctx, data); abi::emit_push_reg(emitter, result_reg); // push the source array pointer onto the temporary stack - if is_closure { - } else if let ExprKind::Variable(var_name) = &args[1].kind { - let var = ctx.variables.get(var_name).expect("undefined callback variable"); - let offset = var.stack_offset; - abi::load_at_offset(emitter, call_reg, offset); // load the callback address from the callable variable slot - } else { - let func_name = match &args[1].kind { - ExprKind::StringLiteral(name) => name.clone(), - _ => panic!("array_reduce() callback must be a string literal, callable expression, or callable variable"), - }; - let label = function_symbol(&func_name); - abi::emit_symbol_address(emitter, call_reg, &label); // materialize the callback function address in the nested-call scratch register - } - let captures = if is_closure { - inline_captures - } else { - callback_env::callback_captures(&args[1], ctx) - }; + let captures = + callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); if !captures.is_empty() { abi::emit_pop_reg(emitter, result_reg); // recover the source array pointer before building the capture environment - if is_closure { - abi::emit_pop_reg(emitter, call_reg); // recover the original closure entry point for env slot zero - } let wrapper = callback_env::emit_captured_callback_env( call_reg, result_reg, @@ -98,18 +66,15 @@ pub fn emit( return Some(PhpType::Int); } + abi::emit_push_reg(emitter, call_reg); // save the callback address across initial-value evaluation + // -- evaluate initial value (third arg) -- emit_expr(&args[2], emitter, ctx, data); emitter.instruction(&format!("mov {}, {}", initial_arg_reg, result_reg)); // place the initial accumulator in the third runtime argument register // -- place callback and array pointer into the runtime argument registers -- - if is_closure { - abi::emit_pop_reg(emitter, array_arg_reg); // pop the source array pointer into the second runtime argument register - abi::emit_pop_reg(emitter, callback_arg_reg); // pop the synthesized callback address into the first runtime argument register - } else { - abi::emit_pop_reg(emitter, array_arg_reg); // pop the source array pointer into the second runtime argument register - emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the callback function address into the first runtime argument register - } + abi::emit_pop_reg(emitter, callback_arg_reg); // restore the callback function address into the first runtime argument register + abi::emit_pop_reg(emitter, array_arg_reg); // pop the source array pointer into the second runtime argument register abi::emit_load_int_immediate(emitter, env_arg_reg, 0); abi::emit_call_label(emitter, "__rt_array_reduce"); // call the callback-driven reduce runtime helper diff --git a/src/codegen/builtins/arrays/array_walk.rs b/src/codegen/builtins/arrays/array_walk.rs index 5e2cb50b..5115e0ff 100644 --- a/src/codegen/builtins/arrays/array_walk.rs +++ b/src/codegen/builtins/arrays/array_walk.rs @@ -13,8 +13,7 @@ use crate::codegen::context::Context; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::expr::emit_expr; -use crate::names::function_symbol; -use crate::parser::ast::{Expr, ExprKind}; +use crate::parser::ast::Expr; use crate::types::PhpType; use super::callback_env; @@ -42,39 +41,12 @@ pub fn emit( // -- save array pointer -- abi::emit_push_reg(emitter, result_reg); // push the source array pointer onto the temporary stack - // -- resolve callback function address -- - let is_closure = matches!( - &args[1].kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ); - let mut inline_captures = Vec::new(); - if is_closure { - emit_expr(&args[1], emitter, ctx, data); - inline_captures = callback_env::callback_captures(&args[1], ctx); - abi::emit_push_reg(emitter, result_reg); // save the synthesized callback address on the temporary stack - } else if let ExprKind::Variable(var_name) = &args[1].kind { - let var = ctx.variables.get(var_name).expect("undefined callback variable"); - let offset = var.stack_offset; - abi::load_at_offset(emitter, call_reg, offset); // load the callback address from the callable variable slot - } else { - let func_name = match &args[1].kind { - ExprKind::StringLiteral(name) => name.clone(), - _ => panic!("array_walk() callback must be a string literal, callable expression, or callable variable"), - }; - let label = function_symbol(&func_name); - abi::emit_symbol_address(emitter, call_reg, &label); // materialize the callback function address in the nested-call scratch register - } - let captures = if is_closure { - inline_captures - } else { - callback_env::callback_captures(&args[1], ctx) - }; + // -- evaluate the callback argument and resolve its function address -- + let captures = + callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); // -- place callback and array pointer into the runtime argument registers -- if !captures.is_empty() { - if is_closure { - abi::emit_pop_reg(emitter, call_reg); // recover the original closure entry point for env slot zero - } abi::emit_pop_reg(emitter, result_reg); // recover the source array pointer before building the capture environment let wrapper = callback_env::emit_captured_callback_env( call_reg, @@ -90,9 +62,6 @@ pub fn emit( abi::emit_call_label(emitter, "__rt_array_walk"); // call the callback-driven walk runtime helper with a capture environment abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); return Some(PhpType::Void); - } else if is_closure { - abi::emit_pop_reg(emitter, callback_arg_reg); // pop the synthesized callback address into the first runtime argument register - abi::emit_pop_reg(emitter, array_arg_reg); // pop the source array pointer into the second runtime argument register } else { abi::emit_pop_reg(emitter, array_arg_reg); // pop the source array pointer into the second runtime argument register emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the callback function address into the first runtime argument register diff --git a/src/codegen/builtins/arrays/call_user_func.rs b/src/codegen/builtins/arrays/call_user_func.rs index 3c0cc86e..dedf98b5 100644 --- a/src/codegen/builtins/arrays/call_user_func.rs +++ b/src/codegen/builtins/arrays/call_user_func.rs @@ -11,10 +11,8 @@ use crate::codegen::context::Context; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; use crate::codegen::expr::calls::args; use crate::codegen::abi; -use crate::names::function_symbol; use crate::parser::ast::{Expr, ExprKind}; use crate::types::{FunctionSig, PhpType}; use super::callback_env; @@ -33,43 +31,22 @@ pub fn emit( crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); } let call_reg = abi::nested_call_reg(emitter); - let result_reg = match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => "x0", - crate::codegen::platform::Arch::X86_64 => "rax", - }; // -- resolve callback function address -- let is_callable_expr = matches!( &args[0].kind, ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) ); - let mut sig: Option = None; - let mut captures: Vec<(String, PhpType)> = Vec::new(); - if is_callable_expr { - emit_expr(&args[0], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // move the synthesized callback address into the nested-call scratch register - if let Some(deferred) = ctx.deferred_closures.last() { - sig = Some(deferred.sig.clone()); - captures = deferred.captures.clone(); - } - } else if let ExprKind::Variable(var_name) = &args[0].kind { - ctx.mark_fcc_used(var_name); - let var = ctx.variables.get(var_name).expect("undefined callback variable"); - let offset = var.stack_offset; - abi::load_at_offset(emitter, call_reg, offset); // load the callback address from the callable variable slot - if let Some(closure_sig) = ctx.closure_sigs.get(var_name) { - sig = Some(closure_sig.clone()); - } - captures = ctx.closure_captures.get(var_name).cloned().unwrap_or_default(); + let precomputed_sig = crate::codegen::callables::callable_sig(&args[0], ctx); + let captures = + callback_env::materialize_callback_address(&args[0], call_reg, emitter, ctx, data); + let sig: Option = if is_callable_expr { + ctx.deferred_closures + .last() + .map(|deferred| deferred.sig.clone()) } else { - let func_name = match &args[0].kind { - ExprKind::StringLiteral(name) => name.clone(), - _ => panic!("call_user_func() callback must be a string literal, callable expression, or callable variable"), - }; - let label = function_symbol(&func_name); - sig = ctx.functions.get(&func_name).cloned(); - abi::emit_symbol_address(emitter, call_reg, &label); - } + precomputed_sig + }; let ret_ty = sig .as_ref() .map(|sig| sig.return_type.clone()) diff --git a/src/codegen/builtins/arrays/call_user_func_array.rs b/src/codegen/builtins/arrays/call_user_func_array.rs index 5ed9c9c6..b27de110 100644 --- a/src/codegen/builtins/arrays/call_user_func_array.rs +++ b/src/codegen/builtins/arrays/call_user_func_array.rs @@ -14,7 +14,6 @@ use crate::codegen::emit::Emitter; use crate::codegen::expr::emit_expr; use crate::codegen::expr::calls::args; use crate::codegen::abi; -use crate::names::function_symbol; use crate::parser::ast::{Expr, ExprKind}; use crate::types::PhpType; use super::callback_env; @@ -68,7 +67,6 @@ pub fn emit( crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); } let call_reg = abi::nested_call_reg(emitter); - let result_reg = abi::int_result_reg(emitter); let (array_reg, len_reg, tail_count_reg, tail_index_reg, index_reg, offset_reg, data_reg, peek_reg, array_new_capacity_reg, array_new_elem_size_reg, len_store_reg) = match emitter.target.arch { crate::codegen::platform::Arch::AArch64 => ( @@ -80,43 +78,20 @@ pub fn emit( }; // -- resolve callback function address and signature -- - let is_callable_expr = matches!( + let precomputed_sig = crate::codegen::callables::callable_sig(&args[0], ctx); + let captures = + callback_env::materialize_callback_address(&args[0], call_reg, emitter, ctx, data); + let sig = if matches!( &args[0].kind, ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ); - let mut captures: Vec<(String, PhpType)> = Vec::new(); - let sig = if is_callable_expr { - emit_expr(&args[0], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // move the synthesized callback address into the nested-call scratch register - let deferred = ctx - .deferred_closures + ) { + ctx.deferred_closures .last() - .expect("call_user_func_array: missing synthesized callable signature"); - captures = deferred.captures.clone(); - deferred + .expect("call_user_func_array: missing synthesized callable signature") .sig .clone() - } else if let ExprKind::Variable(var_name) = &args[0].kind { - ctx.mark_fcc_used(var_name); - let var = ctx.variables.get(var_name).expect("undefined callback variable"); - let offset = var.stack_offset; - abi::load_at_offset(emitter, call_reg, offset); // load the callback address from the callable variable slot - captures = ctx.closure_captures.get(var_name).cloned().unwrap_or_default(); - ctx.closure_sigs - .get(var_name) - .expect("call_user_func_array: callable variable signature not found") - .clone() } else { - let func_name = match &args[0].kind { - ExprKind::StringLiteral(name) => name.clone(), - _ => panic!("call_user_func_array() callback must be a string literal, callable expression, or callable variable"), - }; - let label = function_symbol(&func_name); - abi::emit_symbol_address(emitter, call_reg, &label); - ctx.functions - .get(&func_name) - .expect("call_user_func_array: function not found") - .clone() + precomputed_sig.expect("call_user_func_array: callable signature not found") }; // Evaluate the array argument (second arg) diff --git a/src/codegen/builtins/arrays/callback_env.rs b/src/codegen/builtins/arrays/callback_env.rs index 8676133f..a588f2e6 100644 --- a/src/codegen/builtins/arrays/callback_env.rs +++ b/src/codegen/builtins/arrays/callback_env.rs @@ -3,14 +3,18 @@ //! Owns hidden capture materialization and deferred wrapper metadata for emitted callbacks. //! //! Called from: -//! - `crate::codegen::builtins::arrays::{array_filter,array_map,call_user_func,call_user_func_array}::emit()`. +//! - Array callback builtins such as `array_map()`, `array_filter()`, `array_reduce()`, and sort/walk helpers. +//! - Dynamic-call builtins such as `call_user_func()` and `call_user_func_array()`. //! //! Key details: //! - Capture slots must preserve source-call evaluation order and ABI argument layout for wrapper calls. use crate::codegen::abi; use crate::codegen::context::{Context, DeferredCallbackWrapper}; +use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; +use crate::codegen::expr::emit_expr; +use crate::names::function_symbol; use crate::parser::ast::{Expr, ExprKind}; use crate::types::PhpType; @@ -20,18 +24,30 @@ pub(super) struct CallbackEnv { pub(super) array_slot_offset: usize, } -pub(super) fn callback_captures(callback: &Expr, ctx: &mut Context) -> Vec<(String, PhpType)> { +pub(super) fn materialize_callback_address( + callback: &Expr, + call_reg: &str, + emitter: &mut Emitter, + ctx: &mut Context, + data: &mut DataSection, +) -> Vec<(String, PhpType)> { match &callback.kind { - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) => ctx - .deferred_closures - .last() - .map(|closure| closure.captures.clone()) - .unwrap_or_default(), + ExprKind::StringLiteral(name) => { + let label = function_symbol(name); + abi::emit_symbol_address(emitter, call_reg, &label); + Vec::new() + } ExprKind::Variable(name) => { - ctx.mark_fcc_used(name); - ctx.closure_captures.get(name).cloned().unwrap_or_default() + let var = ctx.variables.get(name).expect("undefined callback variable"); + abi::load_at_offset(emitter, call_reg, var.stack_offset); // load the callback address from the callable variable slot + crate::codegen::callables::callable_captures(callback, ctx) + } + _ => { + emit_expr(callback, emitter, ctx, data); + let result_reg = abi::int_result_reg(emitter); + emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // keep the evaluated callback address in the nested-call scratch register + crate::codegen::callables::callable_captures(callback, ctx) } - _ => Vec::new(), } } diff --git a/src/codegen/builtins/arrays/uasort.rs b/src/codegen/builtins/arrays/uasort.rs index 6210b559..7825e62f 100644 --- a/src/codegen/builtins/arrays/uasort.rs +++ b/src/codegen/builtins/arrays/uasort.rs @@ -16,8 +16,7 @@ use crate::codegen::context::Context; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::expr::emit_expr; -use crate::names::function_symbol; -use crate::parser::ast::{Expr, ExprKind}; +use crate::parser::ast::Expr; use crate::types::PhpType; pub fn emit( @@ -47,32 +46,8 @@ pub fn emit( let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); - let is_closure = matches!( - &args[1].kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ); - let mut inline_captures = Vec::new(); - if is_closure { - emit_expr(&args[1], emitter, ctx, data); - inline_captures = callback_env::callback_captures(&args[1], ctx); - emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // keep the resolved closure callback address in the nested-call scratch register - } else if let ExprKind::Variable(var_name) = &args[1].kind { - let var = ctx.variables.get(var_name).expect("undefined callback variable"); - let offset = var.stack_offset; - abi::load_at_offset(emitter, call_reg, offset); // load the callback address from the variable slot into the nested-call scratch register - } else { - let func_name = match &args[1].kind { - ExprKind::StringLiteral(name) => name.clone(), - _ => panic!("uasort() callback must be a string literal, callable expression, or callable variable"), - }; - let label = function_symbol(&func_name); - abi::emit_symbol_address(emitter, call_reg, &label); // materialize the comparator function address in the nested-call scratch register - } - let captures = if is_closure { - inline_captures - } else { - callback_env::callback_captures(&args[1], ctx) - }; + let captures = + callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); // -- call runtime: callback_addr + array_ptr -- if !captures.is_empty() { @@ -93,19 +68,7 @@ pub fn emit( return Some(PhpType::Void); } abi::emit_pop_reg(emitter, array_arg_reg); // restore the array pointer into the second runtime argument register - if is_closure { - emitter.instruction(&format!("mov {}, {}", callback_arg_reg, result_reg)); // move the resolved closure callback address into the first runtime argument register - } else if let ExprKind::Variable(var_name) = &args[1].kind { - let var = ctx.variables.get(var_name).expect("undefined callback variable"); - abi::load_at_offset(emitter, callback_arg_reg, var.stack_offset); // load the callback address from the variable slot into the first runtime argument register - } else { - let func_name = match &args[1].kind { - ExprKind::StringLiteral(name) => name.clone(), - _ => panic!("uasort() callback must be a string literal, callable expression, or callable variable"), - }; - let label = function_symbol(&func_name); - abi::emit_symbol_address(emitter, callback_arg_reg, &label); // materialize the comparator function address in the first runtime argument register - } + emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the resolved comparator address into the first runtime argument register abi::emit_load_int_immediate(emitter, env_arg_reg, 0); abi::emit_call_label(emitter, "__rt_usort"); // call the target-aware runtime helper that sorts the indexed array using the comparator callback diff --git a/src/codegen/builtins/arrays/uksort.rs b/src/codegen/builtins/arrays/uksort.rs index e8d1935c..07a4e9d5 100644 --- a/src/codegen/builtins/arrays/uksort.rs +++ b/src/codegen/builtins/arrays/uksort.rs @@ -16,8 +16,7 @@ use crate::codegen::context::Context; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::expr::emit_expr; -use crate::names::function_symbol; -use crate::parser::ast::{Expr, ExprKind}; +use crate::parser::ast::Expr; use crate::types::PhpType; pub fn emit( @@ -47,32 +46,8 @@ pub fn emit( let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); - let is_closure = matches!( - &args[1].kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ); - let mut inline_captures = Vec::new(); - if is_closure { - emit_expr(&args[1], emitter, ctx, data); - inline_captures = callback_env::callback_captures(&args[1], ctx); - emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // keep the resolved closure callback address in the nested-call scratch register - } else if let ExprKind::Variable(var_name) = &args[1].kind { - let var = ctx.variables.get(var_name).expect("undefined callback variable"); - let offset = var.stack_offset; - abi::load_at_offset(emitter, call_reg, offset); // load the callback address from the variable slot into the nested-call scratch register - } else { - let func_name = match &args[1].kind { - ExprKind::StringLiteral(name) => name.clone(), - _ => panic!("uksort() callback must be a string literal, callable expression, or callable variable"), - }; - let label = function_symbol(&func_name); - abi::emit_symbol_address(emitter, call_reg, &label); // materialize the comparator function address in the nested-call scratch register - } - let captures = if is_closure { - inline_captures - } else { - callback_env::callback_captures(&args[1], ctx) - }; + let captures = + callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); // -- call runtime: callback_addr + array_ptr -- if !captures.is_empty() { @@ -93,19 +68,7 @@ pub fn emit( return Some(PhpType::Void); } abi::emit_pop_reg(emitter, array_arg_reg); // restore the array pointer into the second runtime argument register - if is_closure { - emitter.instruction(&format!("mov {}, {}", callback_arg_reg, result_reg)); // move the resolved closure callback address into the first runtime argument register - } else if let ExprKind::Variable(var_name) = &args[1].kind { - let var = ctx.variables.get(var_name).expect("undefined callback variable"); - abi::load_at_offset(emitter, callback_arg_reg, var.stack_offset); // load the callback address from the variable slot into the first runtime argument register - } else { - let func_name = match &args[1].kind { - ExprKind::StringLiteral(name) => name.clone(), - _ => panic!("uksort() callback must be a string literal, callable expression, or callable variable"), - }; - let label = function_symbol(&func_name); - abi::emit_symbol_address(emitter, callback_arg_reg, &label); // materialize the comparator function address in the first runtime argument register - } + emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the resolved comparator address into the first runtime argument register abi::emit_load_int_immediate(emitter, env_arg_reg, 0); abi::emit_call_label(emitter, "__rt_usort"); // call the target-aware runtime helper that sorts the indexed array using the comparator callback diff --git a/src/codegen/builtins/arrays/usort.rs b/src/codegen/builtins/arrays/usort.rs index f7387b2a..ccd06396 100644 --- a/src/codegen/builtins/arrays/usort.rs +++ b/src/codegen/builtins/arrays/usort.rs @@ -16,8 +16,7 @@ use crate::codegen::context::Context; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::expr::emit_expr; -use crate::names::function_symbol; -use crate::parser::ast::{Expr, ExprKind}; +use crate::parser::ast::Expr; use crate::types::PhpType; pub fn emit( @@ -47,32 +46,8 @@ pub fn emit( let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); - let is_closure = matches!( - &args[1].kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ); - let mut inline_captures = Vec::new(); - if is_closure { - emit_expr(&args[1], emitter, ctx, data); - inline_captures = callback_env::callback_captures(&args[1], ctx); - emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // keep the resolved closure callback address in the nested-call scratch register - } else if let ExprKind::Variable(var_name) = &args[1].kind { - let var = ctx.variables.get(var_name).expect("undefined callback variable"); - let offset = var.stack_offset; - abi::load_at_offset(emitter, call_reg, offset); // load the callback address from the variable slot into the nested-call scratch register - } else { - let func_name = match &args[1].kind { - ExprKind::StringLiteral(name) => name.clone(), - _ => panic!("usort() callback must be a string literal, callable expression, or callable variable"), - }; - let label = function_symbol(&func_name); - abi::emit_symbol_address(emitter, call_reg, &label); // materialize the comparator function address in the nested-call scratch register - } - let captures = if is_closure { - inline_captures - } else { - callback_env::callback_captures(&args[1], ctx) - }; + let captures = + callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); // -- call runtime: callback_addr + array_ptr -- if !captures.is_empty() { @@ -93,19 +68,7 @@ pub fn emit( return Some(PhpType::Void); } abi::emit_pop_reg(emitter, array_arg_reg); // restore the array pointer into the second runtime argument register - if is_closure { - emitter.instruction(&format!("mov {}, {}", callback_arg_reg, result_reg)); // move the resolved closure callback address into the first runtime argument register - } else if let ExprKind::Variable(var_name) = &args[1].kind { - let var = ctx.variables.get(var_name).expect("undefined callback variable"); - abi::load_at_offset(emitter, callback_arg_reg, var.stack_offset); // load the callback address from the variable slot into the first runtime argument register - } else { - let func_name = match &args[1].kind { - ExprKind::StringLiteral(name) => name.clone(), - _ => panic!("usort() callback must be a string literal, callable expression, or callable variable"), - }; - let label = function_symbol(&func_name); - abi::emit_symbol_address(emitter, callback_arg_reg, &label); // materialize the comparator function address in the first runtime argument register - } + emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the resolved comparator address into the first runtime argument register abi::emit_load_int_immediate(emitter, env_arg_reg, 0); abi::emit_call_label(emitter, "__rt_usort"); // call the target-aware runtime helper that sorts the indexed array using the comparator callback diff --git a/src/codegen/callables.rs b/src/codegen/callables.rs new file mode 100644 index 00000000..ad58a335 --- /dev/null +++ b/src/codegen/callables.rs @@ -0,0 +1,66 @@ +//! Purpose: +//! Shares callable metadata lookups used by indirect calls and callback builtins. +//! Centralizes capture and signature discovery so callable codegen paths stay aligned. +//! +//! Called from: +//! - `crate::codegen::expr::calls` +//! - `crate::codegen::builtins::arrays` +//! +//! Key details: +//! - Complex callable expressions can only expose captures when their runtime shape is statically direct. +//! - Branch-shaped callable signatures are reused only when every branch has the same call contract. + +use crate::codegen::context::Context; +use crate::parser::ast::{Expr, ExprKind}; +use crate::types::{FunctionSig, PhpType}; + +pub(crate) fn callable_captures(callback: &Expr, ctx: &mut Context) -> Vec<(String, PhpType)> { + match &callback.kind { + ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) => ctx + .deferred_closures + .last() + .map(|closure| closure.captures.clone()) + .unwrap_or_default(), + ExprKind::Variable(name) => { + ctx.mark_fcc_used(name); + ctx.closure_captures.get(name).cloned().unwrap_or_default() + } + _ => Vec::new(), + } +} + +pub(crate) fn callable_sig(callback: &Expr, ctx: &Context) -> Option { + match &callback.kind { + ExprKind::StringLiteral(name) => ctx.functions.get(name).cloned(), + ExprKind::Variable(name) => ctx.closure_sigs.get(name).cloned(), + ExprKind::FirstClassCallable(target) => { + crate::codegen::expr::calls::first_class_callable_sig(target, ctx) + } + ExprKind::ArrayAccess { array, .. } => { + if let ExprKind::Variable(name) = &array.kind { + ctx.closure_sigs.get(name).cloned() + } else { + None + } + } + ExprKind::Assignment { value, .. } => callable_sig(value, ctx), + ExprKind::Ternary { + then_expr, + else_expr, + .. + } => matching_branch_sig(then_expr, else_expr, ctx), + ExprKind::ShortTernary { value, default } + | ExprKind::NullCoalesce { value, default } => matching_branch_sig(value, default, ctx), + _ => None, + } +} + +fn matching_branch_sig(left: &Expr, right: &Expr, ctx: &Context) -> Option { + let left_sig = callable_sig(left, ctx)?; + let right_sig = callable_sig(right, ctx)?; + if left_sig == right_sig { + Some(left_sig) + } else { + None + } +} diff --git a/src/codegen/expr/calls/indirect.rs b/src/codegen/expr/calls/indirect.rs index 0366fadd..c5ab92fa 100644 --- a/src/codegen/expr/calls/indirect.rs +++ b/src/codegen/expr/calls/indirect.rs @@ -32,7 +32,7 @@ pub(super) fn emit_loaded_expr_call( } let callee_sig = callee_sig_for_expr(callee, ctx); - let captures = captures_for_expr_call_callee(callee, ctx); + let captures = crate::codegen::callables::callable_captures(callee, ctx); crate::codegen::abi::emit_push_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // save the already-evaluated callable below later arguments let emitted_args = args::emit_pushed_call_args( @@ -106,21 +106,6 @@ fn callee_sig_for_expr( } } -fn captures_for_expr_call_callee(callee: &Expr, ctx: &mut Context) -> Vec<(String, PhpType)> { - match &callee.kind { - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) => ctx - .deferred_closures - .last() - .map(|closure| closure.captures.clone()) - .unwrap_or_default(), - ExprKind::Variable(name) => { - ctx.mark_fcc_used(name); - ctx.closure_captures.get(name).cloned().unwrap_or_default() - } - _ => Vec::new(), - } -} - fn push_captures_as_hidden_args( captures: &[(String, PhpType)], emitter: &mut Emitter, diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index e7f6681c..accb493d 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -10,6 +10,7 @@ mod abi; mod builtins; +mod callables; mod class_methods; pub mod context; mod data_section; From 29c8197ce30d0073039643213f9c7a64b2df9776 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 16 May 2026 11:09:18 +0200 Subject: [PATCH 4/5] fix: validate callback callable signatures --- src/types/checker/builtins/callables.rs | 217 +++++++++++++++++++----- src/types/checker/callables/closures.rs | 29 ++++ src/types/checker/inference/ops.rs | 2 +- 3 files changed, 209 insertions(+), 39 deletions(-) diff --git a/src/types/checker/builtins/callables.rs b/src/types/checker/builtins/callables.rs index 6261f028..f064f30f 100644 --- a/src/types/checker/builtins/callables.rs +++ b/src/types/checker/builtins/callables.rs @@ -33,6 +33,101 @@ fn validate_call_user_func_array_ref_args( )) } +fn dummy_arg_for_array_scalar_elem(arr_ty: &PhpType, span: crate::span::Span) -> Expr { + let elem_ty = match arr_ty { + PhpType::Array(elem_ty) => elem_ty.as_ref(), + PhpType::AssocArray { value, .. } => value.as_ref(), + _ => &PhpType::Int, + }; + match elem_ty { + PhpType::Str => Expr::new(ExprKind::StringLiteral(String::new()), span), + PhpType::Float => Expr::new(ExprKind::FloatLiteral(0.0), span), + PhpType::Bool => Expr::new(ExprKind::BoolLiteral(false), span), + _ => Expr::new(ExprKind::IntLiteral(0), span), + } +} + +fn check_callback_builtin_call( + checker: &mut Checker, + callback: &Expr, + callback_args: &[Expr], + span: crate::span::Span, + env: &TypeEnv, + label: &str, +) -> Result { + if checker.expr_call_complex_callee_needs_runtime_capture(callback) { + return Err(CompileError::new( + callback.span, + &format!( + "{} does not support complex expressions that select captured callables at runtime", + label + ), + )); + } + + if let ExprKind::FirstClassCallable(target) = &callback.kind { + let sig = checker.specialize_first_class_callable_target(target, callback_args, span, env)?; + return checker.check_known_callable_call(&sig, callback_args, span, env, label); + } + + if let ExprKind::Variable(var_name) = &callback.kind { + if let Some(target) = checker.first_class_callable_targets.get(var_name).cloned() { + let sig = + checker.specialize_first_class_callable_target(&target, callback_args, span, env)?; + checker.callable_sigs.insert(var_name.clone(), sig.clone()); + checker + .closure_return_types + .insert(var_name.clone(), sig.return_type.clone()); + return checker.check_known_callable_call(&sig, callback_args, span, env, label); + } + } + + if let ExprKind::StringLiteral(cb_name) = &callback.kind { + if let Some(sig) = checker.functions.get(cb_name.as_str()).cloned() { + return checker.check_known_callable_call(&sig, callback_args, span, env, label); + } + if let Some(decl) = checker.fn_decls.get(cb_name.as_str()).cloned() { + let effective_arg_count = callback_args.len(); + let required = decl.defaults.iter().filter(|default| default.is_none()).count(); + if decl.variadic.is_some() { + if effective_arg_count < required { + return Err(CompileError::new( + span, + &format!( + "Function '{}' expects at least {} arguments, got {}", + cb_name, required, effective_arg_count + ), + )); + } + } else if effective_arg_count < required || effective_arg_count > decl.params.len() { + return Err(CompileError::new( + span, + &format!( + "Function '{}' expects {} arguments, got {}", + cb_name, + Checker::format_fixed_or_range_arity(required, decl.params.len()), + effective_arg_count + ), + )); + } + // Keep function-variant discovery, but do not treat scalar dummy args + // as authoritative parameter types for callbacks over refcounted arrays. + let _ = checker.check_function_call(cb_name, callback_args, span, env); + return Ok(PhpType::Int); + } + return checker.check_function_call(cb_name, callback_args, span, env); + } + + if let Some(sig) = checker.resolve_expr_callable_sig(callback, env)? { + return checker.check_known_callable_call(&sig, callback_args, span, env, label); + } + + Err(CompileError::new( + callback.span, + &format!("{} must have a statically known callable signature", label), + )) +} + pub(super) fn check_builtin( checker: &mut Checker, name: &str, @@ -49,20 +144,20 @@ pub(super) fn check_builtin( checker.infer_type(arg, env)?; } let arr_ty = checker.infer_type(&args[1], env)?; - if let ExprKind::StringLiteral(cb_name) = &args[0].kind { - if let PhpType::Array(ref elem_ty) = arr_ty { - let dummy_arg = match elem_ty.as_ref() { - PhpType::Str => Expr::new(ExprKind::StringLiteral(String::new()), span), - PhpType::Float => Expr::new(ExprKind::FloatLiteral(0.0), span), - PhpType::Bool => Expr::new(ExprKind::BoolLiteral(false), span), - _ => Expr::new(ExprKind::IntLiteral(0), span), - }; - let dummy_args = vec![dummy_arg]; - let _ = checker.check_function_call(cb_name, &dummy_args, span, env); - } - } match arr_ty { - PhpType::Array(elem_ty) => Ok(Some(PhpType::Array(elem_ty))), + PhpType::Array(elem_ty) => { + let arr_ty = PhpType::Array(elem_ty.clone()); + let dummy_args = vec![dummy_arg_for_array_scalar_elem(&arr_ty, span)]; + check_callback_builtin_call( + checker, + &args[0], + &dummy_args, + span, + env, + "array_map() callback", + )?; + Ok(Some(PhpType::Array(elem_ty))) + } _ => Err(CompileError::new( span, "array_map() second argument must be array", @@ -80,12 +175,20 @@ pub(super) fn check_builtin( checker.infer_type(arg, env)?; } let arr_ty = checker.infer_type(&args[0], env)?; - if let ExprKind::StringLiteral(cb_name) = &args[1].kind { - let dummy_args = vec![Expr::new(ExprKind::IntLiteral(0), span)]; - let _ = checker.check_function_call(cb_name, &dummy_args, span, env); - } match arr_ty { - PhpType::Array(elem_ty) => Ok(Some(PhpType::Array(elem_ty))), + PhpType::Array(elem_ty) => { + let arr_ty = PhpType::Array(elem_ty.clone()); + let dummy_args = vec![dummy_arg_for_array_scalar_elem(&arr_ty, span)]; + check_callback_builtin_call( + checker, + &args[1], + &dummy_args, + span, + env, + "array_filter() callback", + )?; + Ok(Some(PhpType::Array(elem_ty))) + } _ => Err(CompileError::new( span, "array_filter() first argument must be array", @@ -102,13 +205,19 @@ pub(super) fn check_builtin( for arg in args { checker.infer_type(arg, env)?; } - if let ExprKind::StringLiteral(cb_name) = &args[1].kind { - let dummy_args = vec![ - Expr::new(ExprKind::IntLiteral(0), span), - Expr::new(ExprKind::IntLiteral(0), span), - ]; - let _ = checker.check_function_call(cb_name, &dummy_args, span, env); - } + let arr_ty = checker.infer_type(&args[0], env)?; + let dummy_args = vec![ + Expr::new(ExprKind::IntLiteral(0), span), + dummy_arg_for_array_scalar_elem(&arr_ty, span), + ]; + check_callback_builtin_call( + checker, + &args[1], + &dummy_args, + span, + env, + "array_reduce() callback", + )?; Ok(Some(PhpType::Int)) } "array_walk" => { @@ -118,10 +227,16 @@ pub(super) fn check_builtin( for arg in args { checker.infer_type(arg, env)?; } - if let ExprKind::StringLiteral(cb_name) = &args[1].kind { - let dummy_args = vec![Expr::new(ExprKind::IntLiteral(0), span)]; - let _ = checker.check_function_call(cb_name, &dummy_args, span, env); - } + let arr_ty = checker.infer_type(&args[0], env)?; + let dummy_args = vec![dummy_arg_for_array_scalar_elem(&arr_ty, span)]; + check_callback_builtin_call( + checker, + &args[1], + &dummy_args, + span, + env, + "array_walk() callback", + )?; Ok(Some(PhpType::Void)) } "usort" | "uksort" | "uasort" => { @@ -134,13 +249,21 @@ pub(super) fn check_builtin( for arg in args { checker.infer_type(arg, env)?; } - if let ExprKind::StringLiteral(cb_name) = &args[1].kind { - let dummy_args = vec![ - Expr::new(ExprKind::IntLiteral(0), span), - Expr::new(ExprKind::IntLiteral(0), span), - ]; - let _ = checker.check_function_call(cb_name, &dummy_args, span, env); - } + let arr_ty = checker.infer_type(&args[0], env)?; + let cmp_arg = if name == "uksort" { + Expr::new(ExprKind::IntLiteral(0), span) + } else { + dummy_arg_for_array_scalar_elem(&arr_ty, span) + }; + let dummy_args = vec![cmp_arg.clone(), cmp_arg]; + check_callback_builtin_call( + checker, + &args[1], + &dummy_args, + span, + env, + &format!("{}() callback", name), + )?; Ok(Some(PhpType::Void)) } "call_user_func_array" => { @@ -153,6 +276,12 @@ pub(super) fn check_builtin( for arg in args { checker.infer_type(arg, env)?; } + if checker.expr_call_complex_callee_needs_runtime_capture(&args[0]) { + return Err(CompileError::new( + args[0].span, + "call_user_func_array() callback does not support complex expressions that select captured callables at runtime", + )); + } if let ExprKind::FirstClassCallable(target) = &args[0].kind { let elems = match &args[1].kind { ExprKind::ArrayLiteral(elems) => elems.as_slice(), @@ -251,7 +380,10 @@ pub(super) fn check_builtin( } return Ok(Some(sig.return_type.clone())); } - Ok(Some(PhpType::Int)) + Err(CompileError::new( + args[0].span, + "call_user_func_array() callback must have a statically known callable signature", + )) } "call_user_func" => { if args.is_empty() { @@ -263,6 +395,12 @@ pub(super) fn check_builtin( for arg in args { checker.infer_type(arg, env)?; } + if checker.expr_call_complex_callee_needs_runtime_capture(&args[0]) { + return Err(CompileError::new( + args[0].span, + "call_user_func() callback does not support complex expressions that select captured callables at runtime", + )); + } if let ExprKind::FirstClassCallable(target) = &args[0].kind { let sig = checker.specialize_first_class_callable_target(target, &args[1..], span, env)?; @@ -322,7 +460,10 @@ pub(super) fn check_builtin( )?; return Ok(Some(ret_ty)); } - Ok(Some(PhpType::Int)) + Err(CompileError::new( + args[0].span, + "call_user_func() callback must have a statically known callable signature", + )) } "class_alias" => { if args.len() < 2 || args.len() > 3 { diff --git a/src/types/checker/callables/closures.rs b/src/types/checker/callables/closures.rs index 09279ab2..77d5f754 100644 --- a/src/types/checker/callables/closures.rs +++ b/src/types/checker/callables/closures.rs @@ -207,7 +207,36 @@ impl Checker { .resolve_first_class_callable_sig(target, expr.span, env) .map(Some), ExprKind::Variable(var_name) => Ok(self.callable_sigs.get(var_name).cloned()), + ExprKind::Assignment { value, .. } => self.resolve_expr_callable_sig(value, env), + ExprKind::Ternary { + then_expr, + else_expr, + .. + } => self.resolve_matching_branch_callable_sig(then_expr, else_expr, env), + ExprKind::ShortTernary { value, default } + | ExprKind::NullCoalesce { value, default } => { + self.resolve_matching_branch_callable_sig(value, default, env) + } _ => Ok(None), } } + + fn resolve_matching_branch_callable_sig( + &mut self, + left: &Expr, + right: &Expr, + env: &TypeEnv, + ) -> Result, CompileError> { + let Some(left_sig) = self.resolve_expr_callable_sig(left, env)? else { + return Ok(None); + }; + let Some(right_sig) = self.resolve_expr_callable_sig(right, env)? else { + return Ok(None); + }; + if left_sig == right_sig { + Ok(Some(left_sig)) + } else { + Ok(None) + } + } } diff --git a/src/types/checker/inference/ops.rs b/src/types/checker/inference/ops.rs index cbee8df7..afc012c1 100644 --- a/src/types/checker/inference/ops.rs +++ b/src/types/checker/inference/ops.rs @@ -489,7 +489,7 @@ impl Checker { } } - fn expr_call_complex_callee_needs_runtime_capture(&self, callee: &Expr) -> bool { + pub(crate) fn expr_call_complex_callee_needs_runtime_capture(&self, callee: &Expr) -> bool { match &callee.kind { ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) | ExprKind::Variable(_) => { false From 26a1e4211e165c202733ec99365a94d133bc17d0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 16 May 2026 11:09:30 +0200 Subject: [PATCH 5/5] test: cover callback callable edge cases --- tests/codegen/oop/callables/methods.rs | 80 ++++++++++++++++++++++++++ tests/error_tests/array_builtins.rs | 50 ++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/tests/codegen/oop/callables/methods.rs b/tests/codegen/oop/callables/methods.rs index 00ce2faf..be3697ca 100644 --- a/tests/codegen/oop/callables/methods.rs +++ b/tests/codegen/oop/callables/methods.rs @@ -219,6 +219,32 @@ foreach ($values as $value) { assert_eq!(out, "2:3:4"); } +#[test] +fn test_array_filter_evaluates_array_before_method_callable_receiver() { + let out = compile_and_run( + r#" 1; + } +} + +$values = array_filter(values(), (new FilterBox())->keep(...)); +echo count($values); +"#, + ); + assert_eq!(out, "array:receiver:1"); +} + #[test] fn test_first_class_callable_instance_method_preserves_by_ref_params() { let out = compile_and_run( @@ -366,6 +392,60 @@ echo array_reduce([1, 2], $fn, 0); assert_eq!(out, "23"); } +#[test] +fn test_array_reduce_evaluates_args_left_to_right_for_method_callable() { + let out = compile_and_run( + r#"add(...), initial()); +"#, + ); + assert_eq!(out, "array:receiver:initial:3"); +} + +#[test] +fn test_array_filter_accepts_complex_noncaptured_callable_expression() { + let out = compile_and_run( + r#" 2; +} + +$use_even = true; +$values = array_filter([1, 2, 3, 4], $use_even ? keep_even(...) : keep_big(...)); +echo count($values); +foreach ($values as $value) { + echo ":"; + echo $value; +} +"#, + ); + assert_eq!(out, "2:2:4"); +} + #[test] fn test_first_class_callable_instance_method_array_walk_with_capture() { let out = compile_and_run( diff --git a/tests/error_tests/array_builtins.rs b/tests/error_tests/array_builtins.rs index 0ad0f18b..3a27ee1c 100644 --- a/tests/error_tests/array_builtins.rs +++ b/tests/error_tests/array_builtins.rs @@ -360,6 +360,24 @@ fn test_error_uasort_wrong_args() { expect_error(r#"cmp(...)); +"#, + "Method BadComparator::cmp expects 1 arguments, got 2", + ); +} + #[test] fn test_error_list_unpack_non_array() { expect_error("keep(...) : $box->keep(...)); +"#, + "array_filter() callback does not support complex expressions", + ); +} + // --- v0.8 system function errors --- #[test]