diff --git a/ROADMAP.md b/ROADMAP.md index c93bcc9e..2e70477f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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) diff --git a/docs/php/types.md b/docs/php/types.md index badfb4f5..d4ef10a7 100644 --- a/docs/php/types.md +++ b/docs/php/types.md @@ -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 | diff --git a/examples/callbacks/main.php b/examples/callbacks/main.php index 26d78f2d..fdd9bb97 100644 --- a/examples/callbacks/main.php +++ b/examples/callbacks/main.php @@ -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"; } diff --git a/src/codegen/builtins/arrays/call_user_func.rs b/src/codegen/builtins/arrays/call_user_func.rs index dedf98b5..e2bd849f 100644 --- a/src/codegen/builtins/arrays/call_user_func.rs +++ b/src/codegen/builtins/arrays/call_user_func.rs @@ -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, @@ -25,6 +26,33 @@ pub fn emit( data: &mut DataSection, ) -> Option { 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 { diff --git a/src/codegen/builtins/arrays/call_user_func_array.rs b/src/codegen/builtins/arrays/call_user_func_array.rs index b27de110..ecbcd816 100644 --- a/src/codegen/builtins/arrays/call_user_func_array.rs +++ b/src/codegen/builtins/arrays/call_user_func_array.rs @@ -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 { @@ -61,6 +62,35 @@ pub fn emit( data: &mut DataSection, ) -> Option { 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 { diff --git a/src/codegen/builtins/arrays/callback_env.rs b/src/codegen/builtins/arrays/callback_env.rs index a588f2e6..b21bbfad 100644 --- a/src/codegen/builtins/arrays/callback_env.rs +++ b/src/codegen/builtins/arrays/callback_env.rs @@ -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, @@ -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() } diff --git a/src/codegen/builtins/arrays/function_exists.rs b/src/codegen/builtins/arrays/function_exists.rs index 869b48d3..aed58a68 100644 --- a/src/codegen/builtins/arrays/function_exists.rs +++ b/src/codegen/builtins/arrays/function_exists.rs @@ -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], @@ -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) diff --git a/src/codegen/builtins/callable_lookup.rs b/src/codegen/builtins/callable_lookup.rs new file mode 100644 index 00000000..882791de --- /dev/null +++ b/src/codegen/builtins/callable_lookup.rs @@ -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 { + 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 +where + I: IntoIterator, +{ + let key = php_symbol_key(name); + names + .into_iter() + .find(|candidate| php_symbol_key(candidate) == key) + .cloned() +} diff --git a/src/codegen/builtins/mod.rs b/src/codegen/builtins/mod.rs index 3a761273..9302eac0 100644 --- a/src/codegen/builtins/mod.rs +++ b/src/codegen/builtins/mod.rs @@ -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; diff --git a/src/codegen/builtins/types/is_callable.rs b/src/codegen/builtins/types/is_callable.rs index 49eb58d7..b09da213 100644 --- a/src/codegen/builtins/types/is_callable.rs +++ b/src/codegen/builtins/types/is_callable.rs @@ -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 @@ -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); diff --git a/src/codegen/callables.rs b/src/codegen/callables.rs index ad58a335..0c26d438 100644 --- a/src/codegen/callables.rs +++ b/src/codegen/callables.rs @@ -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 @@ -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 { 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) diff --git a/src/types/checker/builtins/callables.rs b/src/types/checker/builtins/callables.rs index f064f30f..36f7eb6b 100644 --- a/src/types/checker/builtins/callables.rs +++ b/src/types/checker/builtins/callables.rs @@ -12,6 +12,7 @@ use crate::errors::CompileError; use crate::parser::ast::{Expr, ExprKind}; use crate::types::{PhpType, TypeEnv}; +use super::canonical_builtin_function_name; use super::super::Checker; type BuiltinResult = Result, CompileError>; @@ -328,6 +329,32 @@ pub(super) fn check_builtin( } } if let ExprKind::StringLiteral(cb_name) = &args[0].kind { + if let Some(extern_name) = checker.canonical_extern_function_name_folded(cb_name) { + if let ExprKind::ArrayLiteral(elems) = &args[1].kind { + let ret_ty = + checker.check_extern_function_call(&extern_name, elems, span, env)?; + return Ok(Some(ret_ty)); + } + if let Some(sig) = checker.functions.get(extern_name.as_str()).cloned() { + return Ok(Some(sig.return_type)); + } + } + if let Some(builtin_name) = canonical_builtin_function_name(cb_name) { + if let ExprKind::ArrayLiteral(elems) = &args[1].kind { + if let Some(ret_ty) = + checker.check_builtin(&builtin_name, elems, span, env)? + { + return Ok(Some(ret_ty)); + } + } + if let Some(sig) = crate::types::first_class_callable_builtin_sig(&builtin_name) + { + return Ok(Some(sig.return_type)); + } + } + let cb_name = checker + .canonical_function_name_folded(cb_name) + .unwrap_or_else(|| cb_name.clone()); if let Some(sig) = checker.functions.get(cb_name.as_str()).cloned() { validate_call_user_func_array_ref_args(&sig, &args[1], span)?; if let ExprKind::ArrayLiteral(elems) = &args[1].kind { @@ -353,7 +380,7 @@ pub(super) fn check_builtin( } } if let ExprKind::ArrayLiteral(elems) = &args[1].kind { - let ret_ty = checker.check_function_call(cb_name, elems, span, env)?; + let ret_ty = checker.check_function_call(&cb_name, elems, span, env)?; return Ok(Some(ret_ty)); } if let Some(decl) = checker.fn_decls.get(cb_name.as_str()).cloned() { @@ -362,7 +389,7 @@ pub(super) fn check_builtin( .iter() .map(|_| Expr::new(ExprKind::IntLiteral(0), span)) .collect(); - let ret_ty = checker.check_function_call(cb_name, &dummy_args, span, env)?; + let ret_ty = checker.check_function_call(&cb_name, &dummy_args, span, env)?; return Ok(Some(ret_ty)); } } @@ -436,6 +463,21 @@ pub(super) fn check_builtin( } } if let ExprKind::StringLiteral(cb_name) = &args[0].kind { + if let Some(extern_name) = checker.canonical_extern_function_name_folded(cb_name) { + let ret_ty = + checker.check_extern_function_call(&extern_name, &args[1..], span, env)?; + return Ok(Some(ret_ty)); + } + if let Some(builtin_name) = canonical_builtin_function_name(cb_name) { + if let Some(ret_ty) = + checker.check_builtin(&builtin_name, &args[1..], span, env)? + { + return Ok(Some(ret_ty)); + } + } + let cb_name = checker + .canonical_function_name_folded(cb_name) + .unwrap_or_else(|| cb_name.clone()); if let Some(sig) = checker.functions.get(cb_name.as_str()).cloned() { let ret_ty = checker.check_known_callable_call( &sig, @@ -447,7 +489,7 @@ pub(super) fn check_builtin( return Ok(Some(ret_ty)); } let cb_args = args[1..].to_vec(); - let ret_ty = checker.check_function_call(cb_name, &cb_args, span, env)?; + let ret_ty = checker.check_function_call(&cb_name, &cb_args, span, env)?; return Ok(Some(ret_ty)); } if let Some(sig) = checker.resolve_expr_callable_sig(&args[0], env)? { @@ -566,6 +608,9 @@ pub(super) fn check_builtin( } checker.infer_type(&args[0], env)?; if let ExprKind::StringLiteral(cb_name) = &args[0].kind { + let cb_name = checker + .canonical_function_name_folded(cb_name) + .unwrap_or_else(|| cb_name.clone()); if checker.fn_decls.contains_key(cb_name.as_str()) && !checker.functions.contains_key(cb_name.as_str()) { @@ -575,12 +620,12 @@ pub(super) fn check_builtin( .iter() .map(|_| Expr::new(ExprKind::IntLiteral(0), span)) .collect(); - let _ = checker.check_function_call(cb_name, &dummy_args, span, env); + let _ = checker.check_function_call(&cb_name, &dummy_args, span, env); } } else if checker.function_variant_groups.contains_key(cb_name.as_str()) && !checker.functions.contains_key(cb_name.as_str()) { - let _ = checker.ensure_function_variant_group_signature(cb_name, span); + let _ = checker.ensure_function_variant_group_signature(&cb_name, span); } } Ok(Some(PhpType::Bool)) diff --git a/src/types/checker/driver/functions.rs b/src/types/checker/driver/functions.rs index 59a9d536..41e92155 100644 --- a/src/types/checker/driver/functions.rs +++ b/src/types/checker/driver/functions.rs @@ -8,7 +8,7 @@ //! Key details: //! - Phase order controls diagnostics, available declarations, required libraries, and function-local environments. -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use crate::errors::CompileError; use crate::names::php_symbol_key; @@ -111,6 +111,16 @@ impl Checker { .any(|existing| php_symbol_key(existing) == key) } + pub(crate) fn canonical_function_name_folded(&self, name: &str) -> Option { + folded_map_key(&self.functions, name) + .or_else(|| folded_map_key(&self.function_variant_groups, name)) + .or_else(|| folded_map_key(&self.fn_decls, name)) + } + + pub(crate) fn canonical_extern_function_name_folded(&self, name: &str) -> Option { + folded_map_key(&self.extern_functions, name) + } + pub(super) fn resolve_unchecked_functions(&mut self, errors: &mut Vec) { let unchecked: Vec = self .fn_decls @@ -244,3 +254,10 @@ impl Checker { })) } } + +fn folded_map_key(map: &HashMap, name: &str) -> Option { + let key = php_symbol_key(name); + map.keys() + .find(|existing| php_symbol_key(existing) == key) + .cloned() +} diff --git a/src/types/checker/functions/resolution/mod.rs b/src/types/checker/functions/resolution/mod.rs index 22d91ee9..1cf7714b 100644 --- a/src/types/checker/functions/resolution/mod.rs +++ b/src/types/checker/functions/resolution/mod.rs @@ -42,6 +42,14 @@ impl Checker { span: crate::span::Span, caller_env: &TypeEnv, ) -> Result { + if let Some(extern_name) = self.canonical_extern_function_name_folded(name) { + return self.check_extern_function_call(&extern_name, args, span, caller_env); + } + let canonical_name = self + .canonical_function_name_folded(name) + .unwrap_or_else(|| name.to_string()); + let name = canonical_name.as_str(); + if let Some(sig) = self.functions.get(name).cloned() { if let Some(reason) = sig.deprecation.as_deref() { let message = if reason.is_empty() { diff --git a/tests/codegen/arrays/callbacks.rs b/tests/codegen/arrays/callbacks.rs index 525d009a..c9db374c 100644 --- a/tests/codegen/arrays/callbacks.rs +++ b/tests/codegen/arrays/callbacks.rs @@ -220,6 +220,12 @@ echo call_user_func("sum9", 1, 2, 3, 4, 5, 6, 7, 8, 9); assert_eq!(out, "45"); } +#[test] +fn test_call_user_func_string_builtin_callback() { + let out = compile_and_run(r#"