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
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ runtime helpers, and standard-library surfaces.
- [x] JSON decoder optimization — fused the `__rt_json_validate` pre-pass into `__rt_json_decode_mixed` for `json_decode()`. The wrapper now calls the checked structural decoder directly; the decoder trims the input once, validates scalar strings/numbers at the point where they are decoded, enforces depth around containers, records syntax/depth/UTF-16 errors internally, and returns null-on-error for the PHP-facing wrapper. `json_validate()` keeps the standalone RFC 8259 validator surface.
- [x] JSON encoder optimization — extended the `_json_active_flags` callee-saved-register cache to `__rt_json_encode_assoc` and `__rt_json_encode_array_dynamic` (`x19` ARM64 / `r15` x86_64). The recursive encoder chain now preserves that cache: `__rt_json_encode_object` no longer clobbers ARM64 `x19`, and the x86_64 string encoder keeps `r15` dedicated to cached flags during UTF-8 decoding.
- [x] JSON pretty-print optimization — inline indent emission inside each container encoder (assoc, array_int/str/dynamic, object) and retire the `__rt_json_pretty_apply` post-processor. Eliminates the second buffer walk for JSON_PRETTY_PRINT workloads. Multi-day refactor completed with a `_json_indent_depth` BSS slot, balanced normal-path formatting depth maintenance, reset-at-entry protection across throws, and bytewise PHP cross-check coverage on representative payloads.
- [ ] `is_callable()` runtime fallback — handle non-literal strings, `[$obj, "method"]` arrays, and objects implementing `__invoke`. The string-literal + Callable-typed compile-time path is already in place.
- [x] `is_callable()` runtime fallback — handle non-literal strings, `[$obj, "method"]` arrays, and objects implementing `__invoke`. The string-literal + Callable-typed compile-time path is already in place.
- [ ] Case-insensitive user-function lookup — `function_exists("USER_FN")` and `is_callable("USER_FN")` currently require the exact case. PHP accepts any case for user functions too; tighten the lookup table (shared by both builtins).

### Standard PHP Library (SPL)
Expand Down
2 changes: 1 addition & 1 deletion docs/php/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ Aliases: `(integer)`, `(double)`, `(real)`, `(boolean)`.
| `is_numeric()` | `is_numeric($val): bool` | Returns true if int or float |
| `is_bool()` | `is_bool($val): bool` | Returns true if bool |
| `is_iterable()` | `is_iterable($val): bool` | Returns true if array or Traversable-compatible iterable |
| `is_callable()` | `is_callable($val): bool` | Returns true for closures, first-class callables, and string literals naming a known builtin or user function. Non-literal strings, `[$obj, "method"]` arrays, and `__invoke` objects are tracked as follow-ups. |
| `is_callable()` | `is_callable($val): bool` | Returns true for closures, first-class callables, strings naming known builtins, user functions, or public static methods (`"Class::method"`), `[$obj, "method"]` arrays with public methods, `[ClassName::class, "method"]` static method arrays, and objects with public `__invoke()`. |
| `is_nan()` | `is_nan($val): bool` | Returns true if NAN |
| `is_finite()` | `is_finite($val): bool` | Returns true if not INF/NAN |
| `is_infinite()` | `is_infinite($val): bool` | Returns true if INF or -INF |
Expand Down
32 changes: 32 additions & 0 deletions examples/callbacks/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,35 @@ public static function name() {
if (!function_exists("nonexistent")) {
echo "function 'nonexistent' does not exist\n";
}

// is_callable: dynamic strings, method arrays, static method arrays, and invokable objects
class Runner {
public function run() {
return "running";
}
}

class InvokableRunner {
public function __invoke() {
return "invoked";
}
}

class StaticRunner {
public static function run() {
return "static";
}
}

$callback_name = "double";
$static_callback_name = "StaticRunner::run";
$runner = new Runner();
$method_callback = [$runner, "run"];
$static_method_callback = [StaticRunner::class, "run"];
$invokable_runner = new InvokableRunner();

echo "is_callable dynamic string: " . (is_callable($callback_name) ? "yes" : "no") . "\n";
echo "is_callable static string: " . (is_callable($static_callback_name) ? "yes" : "no") . "\n";
echo "is_callable method array: " . (is_callable($method_callback) ? "yes" : "no") . "\n";
echo "is_callable static method array: " . (is_callable($static_method_callback) ? "yes" : "no") . "\n";
echo "is_callable invokable object: " . (is_callable($invokable_runner) ? "yes" : "no") . "\n";
73 changes: 57 additions & 16 deletions src/codegen/builtins/types/is_callable.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
//! Purpose:
//! Emits codegen for `is_callable()`.
//! Handles compile-time callable shapes and known literal function names while preserving side effects for dynamic values.
//! Handles compile-time callable shapes and delegates dynamic PHP callable forms to runtime helpers.
//!
//! Called from:
//! - `crate::codegen::builtins::types::emit()` when lowering type/introspection builtins.
//!
//! Key details:
//! - Runtime callable lookup is intentionally conservative; unsupported dynamic callable shapes evaluate then return false.
//! - Runtime fallback covers non-literal strings, callable arrays, invokable objects, Mixed, and erased iterables.

use crate::codegen::abi;
use crate::codegen::context::Context;
Expand All @@ -21,10 +21,8 @@ use crate::types::PhpType;
///
/// Static evaluation when the argument's compile-time type is Callable
/// (closures, first-class callables) or a string literal that resolves
/// to a known builtin / user function. Non-literal strings, arrays, and
/// other dynamic shapes return false here pending runtime lookup
/// (PHP also accepts `[$obj, "method"]` pairs and objects implementing
/// `__invoke` — those routes are tracked as follow-ups).
/// to a known builtin / user function. Dynamic strings, callable arrays,
/// objects, and type-erased payloads route to runtime metadata lookup.
pub fn emit(
_name: &str,
args: &[Expr],
Expand All @@ -39,18 +37,61 @@ pub fn emit(
// function ⇒ true, else false. Evaluating the literal expression
// has no side effects, so we skip emit_expr.
if let ExprKind::StringLiteral(name) = &args[0].kind {
let known = ctx.functions.contains_key(name)
|| is_supported_builtin_function(name)
|| ctx.function_variant_groups.contains(name);
let val: i64 = if known { 1 } else { 0 };
abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), val);
return Some(PhpType::Bool);
if !name.contains("::") {
let known = ctx.functions.contains_key(name)
|| is_supported_builtin_function(name)
|| ctx.function_variant_groups.contains(name);
let val: i64 = if known { 1 } else { 0 };
abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), val);
return Some(PhpType::Bool);
}
}

// Otherwise evaluate the expression for side effects and decide
// statically based on its compile-time type.
let ty = emit_expr(&args[0], emitter, ctx, data);
let val: i64 = if ty == PhpType::Callable { 1 } else { 0 };
abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), val);
match ty.codegen_repr() {
PhpType::Callable => {
abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 1);
}
PhpType::Str => emit_dynamic_string_lookup(emitter),
PhpType::Array(_) => {
emit_pointer_lookup(emitter, "__rt_is_callable_array"); // inspect indexed array shape for callable arrays
}
PhpType::AssocArray { .. } => {
emit_pointer_lookup(emitter, "__rt_is_callable_assoc"); // inspect hash shape for numeric 0/1 callable-array entries
}
PhpType::Object(_) => {
emit_pointer_lookup(emitter, "__rt_is_callable_object"); // check whether the object's runtime class exposes public __invoke
}
PhpType::Mixed | PhpType::Union(_) => {
emit_pointer_lookup(emitter, "__rt_is_callable_mixed"); // unwrap Mixed and dispatch to the dynamic callable checks
}
PhpType::Iterable => {
emit_pointer_lookup(emitter, "__rt_is_callable_heap"); // inspect erased iterable heap kind before choosing array/object fallback
}
_ => {
abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0);
}
}
Some(PhpType::Bool)
}

fn emit_pointer_lookup(emitter: &mut Emitter, label: &str) {
if emitter.target.arch == crate::codegen::platform::Arch::X86_64 {
emitter.instruction("mov rdi, rax"); // move pointer-shaped result into SysV helper argument 0
}
abi::emit_call_label(emitter, label); // call the selected pointer-shaped runtime callable fallback
}

fn emit_dynamic_string_lookup(emitter: &mut Emitter) {
match emitter.target.arch {
crate::codegen::platform::Arch::AArch64 => {
emitter.instruction("mov x0, x1"); // move dynamic string pointer into runtime helper argument 0
emitter.instruction("mov x1, x2"); // move dynamic string length into runtime helper argument 1
}
crate::codegen::platform::Arch::X86_64 => {
emitter.instruction("mov rdi, rax"); // move dynamic string pointer into SysV helper argument 0
emitter.instruction("mov rsi, rdx"); // move dynamic string length into SysV helper argument 1
}
}
abi::emit_call_label(emitter, "__rt_is_callable_string"); // resolve dynamic function-name string against builtin and user metadata
}
6 changes: 6 additions & 0 deletions src/codegen/main_emission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ pub(super) fn emit_main_and_finalize(
finish_user_asm(
emitter,
data,
functions,
function_variant_groups,
all_global_var_names,
all_static_vars,
interfaces,
Expand Down Expand Up @@ -318,6 +320,8 @@ fn emit_gc_stats(emitter: &mut Emitter, data: &mut DataSection) {
fn finish_user_asm(
emitter: Emitter,
data: DataSection,
functions: &HashMap<String, FunctionSig>,
function_variant_groups: &HashSet<String>,
all_global_var_names: &HashSet<String>,
all_static_vars: &HashMap<(String, String), PhpType>,
interfaces: &HashMap<String, InterfaceInfo>,
Expand All @@ -329,6 +333,8 @@ fn finish_user_asm(
let user_data = runtime::emit_runtime_data_user(
all_global_var_names,
all_static_vars,
functions,
function_variant_groups,
interfaces,
classes,
enums,
Expand Down
Loading
Loading