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 @@ -448,7 +448,7 @@ runtime helpers, and standard-library surfaces.
- [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.
- [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).
- [x] Case-insensitive user-function lookup — `function_exists("USER_FN")` and `is_callable("USER_FN")` accept any case for user functions through a shared lookup path, matching PHP's function-name rules.

### 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, 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_callable()` | `is_callable($val): bool` | Returns true for closures, first-class callables, strings case-insensitively 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
7 changes: 5 additions & 2 deletions examples/callbacks/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,13 @@ public static function name() {

echo "static callable: " . Labeler::current() . "/" . LoudLabeler::current() . "\n";

// function_exists: check if a function is defined
if (function_exists("double")) {
// Function string lookups are case-insensitive, like PHP.
if (function_exists("DOUBLE")) {
echo "function 'double' exists\n";
}
if (is_callable("DoUbLe")) {
echo "function 'double' is callable\n";
}
if (!function_exists("nonexistent")) {
echo "function 'nonexistent' does not exist\n";
}
Expand Down
28 changes: 28 additions & 0 deletions src/codegen/builtins/arrays/call_user_func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::codegen::abi;
use crate::parser::ast::{Expr, ExprKind};
use crate::types::{FunctionSig, PhpType};
use super::callback_env;
use super::super::callable_lookup::{lookup_function, FunctionLookup};

pub fn emit(
_name: &str,
Expand All @@ -25,6 +26,33 @@ pub fn emit(
data: &mut DataSection,
) -> Option<PhpType> {
emitter.comment("call_user_func()");
if let ExprKind::StringLiteral(name) = &args[0].kind {
match lookup_function(ctx, name) {
Some(FunctionLookup::Extern(extern_name)) => {
return Some(crate::codegen::ffi::emit_extern_call(
&extern_name,
&args[1..],
args[0].span,
emitter,
ctx,
data,
));
}
Some(FunctionLookup::Builtin(builtin_name)) => {
if let Some(ret_ty) = crate::codegen::builtins::emit_builtin_call(
&builtin_name,
&args[1..],
args[0].span,
emitter,
ctx,
data,
) {
return Some(ret_ty);
}
}
Some(FunctionLookup::UserFunction(_)) | Some(FunctionLookup::IncludeVariant(_)) | None => {}
}
}
let save_concat_before_args =
emitter.target.arch == crate::codegen::platform::Arch::X86_64;
if save_concat_before_args {
Expand Down
30 changes: 30 additions & 0 deletions src/codegen/builtins/arrays/call_user_func_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::codegen::abi;
use crate::parser::ast::{Expr, ExprKind};
use crate::types::PhpType;
use super::callback_env;
use super::super::callable_lookup::{lookup_function, FunctionLookup};

fn emit_array_value_type_stamp(emitter: &mut Emitter, array_reg: &str, elem_ty: &PhpType) {
let value_type_tag = match elem_ty {
Expand Down Expand Up @@ -61,6 +62,35 @@ pub fn emit(
data: &mut DataSection,
) -> Option<PhpType> {
emitter.comment("call_user_func_array()");
if let (ExprKind::StringLiteral(name), ExprKind::ArrayLiteral(elems)) =
(&args[0].kind, &args[1].kind)
{
match lookup_function(ctx, name) {
Some(FunctionLookup::Extern(extern_name)) => {
return Some(crate::codegen::ffi::emit_extern_call(
&extern_name,
elems,
args[0].span,
emitter,
ctx,
data,
));
}
Some(FunctionLookup::Builtin(builtin_name)) => {
if let Some(ret_ty) = crate::codegen::builtins::emit_builtin_call(
&builtin_name,
elems,
args[0].span,
emitter,
ctx,
data,
) {
return Some(ret_ty);
}
}
Some(FunctionLookup::UserFunction(_)) | Some(FunctionLookup::IncludeVariant(_)) | None => {}
}
}
let save_concat_before_args =
emitter.target.arch == crate::codegen::platform::Arch::X86_64;
if save_concat_before_args {
Expand Down
9 changes: 8 additions & 1 deletion src/codegen/builtins/arrays/callback_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ use crate::names::function_symbol;
use crate::parser::ast::{Expr, ExprKind};
use crate::types::PhpType;

use super::super::callable_lookup::{lookup_function, FunctionLookup};

pub(super) struct CallbackEnv {
pub(super) wrapper_label: String,
pub(super) env_bytes: usize,
Expand All @@ -33,7 +35,12 @@ pub(super) fn materialize_callback_address(
) -> Vec<(String, PhpType)> {
match &callback.kind {
ExprKind::StringLiteral(name) => {
let label = function_symbol(name);
let resolved_name = match lookup_function(ctx, name) {
Some(FunctionLookup::UserFunction(name))
| Some(FunctionLookup::IncludeVariant(name)) => name,
_ => name.clone(),
};
let label = function_symbol(&resolved_name);
abi::emit_symbol_address(emitter, call_reg, &label);
Vec::new()
}
Expand Down
27 changes: 17 additions & 10 deletions src/codegen/builtins/arrays/function_exists.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ use crate::codegen::abi;
use crate::codegen::platform::Arch;
use crate::names::function_variant_active_symbol;
use crate::parser::ast::{Expr, ExprKind};
use crate::types::checker::builtins::is_supported_builtin_function;
use crate::types::PhpType;

use super::super::callable_lookup::{lookup_function, FunctionLookup};

pub fn emit(
_name: &str,
args: &[Expr],
Expand All @@ -33,16 +34,22 @@ pub fn emit(
_ => panic!("function_exists() argument must be a string literal"),
};

if ctx.function_variant_groups.contains(&func_name) {
emit_variant_function_exists(&func_name, emitter, data);
return Some(PhpType::Bool);
}

// -- emit constant true/false based on whether function is known --
if ctx.functions.contains_key(&func_name) || is_supported_builtin_function(&func_name) {
abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 1);
} else {
abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0);
match lookup_function(ctx, &func_name) {
Some(FunctionLookup::IncludeVariant(variant_name)) => {
emit_variant_function_exists(&variant_name, emitter, data);
return Some(PhpType::Bool);
}
Some(
FunctionLookup::Builtin(_)
| FunctionLookup::Extern(_)
| FunctionLookup::UserFunction(_),
) => {
abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 1);
}
None => {
abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0);
}
}

Some(PhpType::Bool)
Expand Down
45 changes: 45 additions & 0 deletions src/codegen/builtins/callable_lookup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! Purpose:
//! Resolves string-literal function names used by callable/introspection builtins.
//! Shares PHP case-insensitive lookup between string-callback and introspection builtins.
//!
//! Called from:
//! - `crate::codegen::builtins::arrays::function_exists`
//! - `crate::codegen::builtins::types::is_callable`
//!
//! Key details:
//! - Include variants, externs, builtins, and user functions stay distinguishable so callers can choose the right lowering path.

use crate::codegen::context::Context;
use crate::names::php_symbol_key;
use crate::types::checker::builtins::canonical_builtin_function_name;

pub(crate) enum FunctionLookup {
Builtin(String),
Extern(String),
UserFunction(String),
IncludeVariant(String),
}

pub(crate) fn lookup_function(ctx: &Context, name: &str) -> Option<FunctionLookup> {
if let Some(name) = lookup_folded(ctx.function_variant_groups.iter(), name) {
return Some(FunctionLookup::IncludeVariant(name));
}
if let Some(name) = lookup_folded(ctx.extern_functions.keys(), name) {
return Some(FunctionLookup::Extern(name));
}
if let Some(name) = lookup_folded(ctx.functions.keys(), name) {
return Some(FunctionLookup::UserFunction(name));
}
canonical_builtin_function_name(name).map(FunctionLookup::Builtin)
}

fn lookup_folded<'a, I>(names: I, name: &str) -> Option<String>
where
I: IntoIterator<Item = &'a String>,
{
let key = php_symbol_key(name);
names
.into_iter()
.find(|candidate| php_symbol_key(candidate) == key)
.cloned()
}
1 change: 1 addition & 0 deletions src/codegen/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//! - Builtin names arrive after type/catalog resolution, including PHP case-insensitive and namespace fallback behavior.

mod arrays;
pub(crate) mod callable_lookup;
mod io;
mod math;
mod pointers;
Expand Down
7 changes: 3 additions & 4 deletions src/codegen/builtins/types/is_callable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ use crate::codegen::data_section::DataSection;
use crate::codegen::emit::Emitter;
use crate::codegen::expr::emit_expr;
use crate::parser::ast::{Expr, ExprKind};
use crate::types::checker::builtins::is_supported_builtin_function;
use crate::types::PhpType;

use super::super::callable_lookup::lookup_function;

/// is_callable(value): bool
///
/// Static evaluation when the argument's compile-time type is Callable
Expand All @@ -38,9 +39,7 @@ pub fn emit(
// has no side effects, so we skip emit_expr.
if let ExprKind::StringLiteral(name) = &args[0].kind {
if !name.contains("::") {
let known = ctx.functions.contains_key(name)
|| is_supported_builtin_function(name)
|| ctx.function_variant_groups.contains(name);
let known = lookup_function(ctx, name).is_some();
let val: i64 = if known { 1 } else { 0 };
abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), val);
return Some(PhpType::Bool);
Expand Down
8 changes: 7 additions & 1 deletion src/codegen/callables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use crate::codegen::context::Context;
use crate::parser::ast::{Expr, ExprKind};
use crate::types::{FunctionSig, PhpType};

use super::builtins::callable_lookup::{lookup_function, FunctionLookup};

pub(crate) fn callable_captures(callback: &Expr, ctx: &mut Context) -> Vec<(String, PhpType)> {
match &callback.kind {
ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) => ctx
Expand All @@ -31,7 +33,11 @@ pub(crate) fn callable_captures(callback: &Expr, ctx: &mut Context) -> Vec<(Stri

pub(crate) fn callable_sig(callback: &Expr, ctx: &Context) -> Option<FunctionSig> {
match &callback.kind {
ExprKind::StringLiteral(name) => ctx.functions.get(name).cloned(),
ExprKind::StringLiteral(name) => match lookup_function(ctx, name) {
Some(FunctionLookup::UserFunction(name))
| Some(FunctionLookup::IncludeVariant(name)) => ctx.functions.get(&name).cloned(),
_ => 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)
Expand Down
Loading
Loading