diff --git a/docs/internals/the-codegen.md b/docs/internals/the-codegen.md index 1639d0b3..d3882f90 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. 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 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-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/internals/the-runtime.md b/docs/internals/the-runtime.md index 76b8b79b..cc1ad1eb 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..d21ca5b3 100644 --- a/docs/php/functions.md +++ b/docs/php/functions.md @@ -155,8 +155,8 @@ $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(...)`. -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. +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 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()`. 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 308cb38b..7a310731 100644 --- a/examples/callbacks/main.php +++ b/examples/callbacks/main.php @@ -57,6 +57,53 @@ 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; + } + + 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_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 58cb9f2a..3433adc4 100644 --- a/src/codegen/builtins/arrays/array_reduce.rs +++ b/src/codegen/builtins/arrays/array_reduce.rs @@ -13,10 +13,9 @@ 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::parser::ast::Expr; use crate::types::PhpType; +use super::callback_env; pub fn emit( _name: &str, @@ -31,52 +30,53 @@ 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(_) - ); - if is_closure { - emit_expr(&args[1], emitter, ctx, data); - 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 = + 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 + 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); } + 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 - } - 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_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 Some(PhpType::Int) } diff --git a/src/codegen/builtins/arrays/array_walk.rs b/src/codegen/builtins/arrays/array_walk.rs index ebe6a769..5115e0ff 100644 --- a/src/codegen/builtins/arrays/array_walk.rs +++ b/src/codegen/builtins/arrays/array_walk.rs @@ -13,10 +13,9 @@ 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::parser::ast::Expr; use crate::types::PhpType; +use super::callback_env; pub fn emit( _name: &str, @@ -30,47 +29,45 @@ 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 - // -- resolve callback function address -- - let is_closure = matches!( - &args[1].kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ); - if is_closure { - emit_expr(&args[1], emitter, ctx, data); - 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 - } + // -- 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 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 + if !captures.is_empty() { + 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 { 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/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 06f649b5..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) @@ -129,6 +104,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 +121,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/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 b19b1323..7825e62f 100644 --- a/src/codegen/builtins/arrays/uasort.rs +++ b/src/codegen/builtins/arrays/uasort.rs @@ -9,15 +9,14 @@ //! - 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::parser::ast::Expr; use crate::types::PhpType; pub fn emit( @@ -31,6 +30,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 +41,35 @@ 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 is_closure = matches!( - &args[1].kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ); - 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 - } - } - } 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 - } - } - } 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 - } - } - } + 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 captures = + callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); // -- 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 + 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 Some(PhpType::Void) diff --git a/src/codegen/builtins/arrays/uksort.rs b/src/codegen/builtins/arrays/uksort.rs index 2c8a7e98..07a4e9d5 100644 --- a/src/codegen/builtins/arrays/uksort.rs +++ b/src/codegen/builtins/arrays/uksort.rs @@ -9,15 +9,14 @@ //! - 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::parser::ast::Expr; use crate::types::PhpType; pub fn emit( @@ -31,6 +30,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 +41,35 @@ 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 is_closure = matches!( - &args[1].kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ); - 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 - } - } - } 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 - } - } - } 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 - } - } - } + 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 captures = + callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); // -- 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 + 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 Some(PhpType::Void) diff --git a/src/codegen/builtins/arrays/usort.rs b/src/codegen/builtins/arrays/usort.rs index 2e3cafcb..ccd06396 100644 --- a/src/codegen/builtins/arrays/usort.rs +++ b/src/codegen/builtins/arrays/usort.rs @@ -9,15 +9,14 @@ //! - 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::parser::ast::Expr; use crate::types::PhpType; pub fn emit( @@ -31,6 +30,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 +41,35 @@ 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 is_closure = matches!( - &args[1].kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ); - 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 - } - } - } 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 - } - } - } 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 - } - } - } + 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 captures = + callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); // -- 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 + 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 Some(PhpType::Void) 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.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..c5ab92fa 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 = 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( @@ -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,24 @@ fn callee_sig_for_expr( _ => None, } } + +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/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; 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..f064f30f 100644 --- a/src/types/checker/builtins/callables.rs +++ b/src/types/checker/builtins/callables.rs @@ -16,6 +16,118 @@ 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", + )) +} + +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, @@ -32,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", @@ -63,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", @@ -82,31 +202,41 @@ 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)?; } - 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" => { 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)?; } - 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" => { @@ -116,17 +246,24 @@ 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)?; } - 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" => { @@ -139,18 +276,19 @@ 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(), _ => &[], }; 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, @@ -175,12 +313,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, @@ -196,12 +329,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, @@ -215,10 +343,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", )); } } @@ -237,12 +367,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, @@ -255,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() { @@ -267,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)?; @@ -326,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/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/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/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..afc012c1 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 { } } + pub(crate) 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/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#" 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( @@ -346,3 +373,332 @@ 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_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( + 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/array_builtins.rs b/tests/error_tests/array_builtins.rs index 55f06d19..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] @@ -402,17 +452,17 @@ fn test_indexed_array_unrelated_object_values_widen_to_mixed() { } #[test] -fn test_error_call_user_func_array_rejects_ref_callback_params() { +fn test_error_call_user_func_array_ref_callback_requires_literal_argument_array() { 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", ); }