Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/internals/the-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/internals/the-parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
6 changes: 3 additions & 3 deletions docs/internals/the-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions docs/php/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<?php
Expand All @@ -171,7 +171,7 @@ $hello = $greeter->hello(...);
echo $hello("Ada"); // Hello Ada
```

Captured first-class callable targets (`static::method(...)` and `$obj->method(...)`) can be called directly through a local callable variable. 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

Expand Down
47 changes: 47 additions & 0 deletions examples/callbacks/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(...);
Expand Down
122 changes: 30 additions & 92 deletions src/codegen/builtins/arrays/array_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 {
Expand Down
Loading
Loading