From 96155f023ea82cdebac82f2aa7553919654943e1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 15 May 2026 22:05:41 +0200 Subject: [PATCH 1/2] fix: make user function string lookup case-insensitive --- ROADMAP.md | 2 +- docs/php/types.md | 2 +- examples/callbacks/main.php | 7 +++- .../builtins/arrays/function_exists.rs | 23 ++++++----- src/codegen/builtins/callable_lookup.rs | 41 +++++++++++++++++++ src/codegen/builtins/mod.rs | 1 + src/codegen/builtins/types/is_callable.rs | 7 ++-- tests/codegen/case_insensitive_symbols.rs | 3 +- 8 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 src/codegen/builtins/callable_lookup.rs diff --git a/ROADMAP.md b/ROADMAP.md index 18eb9331..52de7f49 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -448,7 +448,7 @@ runtime helpers, and standard-library surfaces. - [ ] JSON encoder optimization — extend the `_json_active_flags` callee-saved-register cache (already shipped for `__rt_json_encode_str`) to `__rt_json_encode_assoc` and `__rt_json_encode_array_dynamic`. Blocked by `__rt_json_encode_object` clobbering x19 internally without saving; needs an ABI-preservation audit of every helper in the recursive encoder chain before x19/r15 can be relied upon as a long-lived cache. - [ ] 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: needs a `_json_indent_depth` BSS slot, careful invariant maintenance across throws, and bytewise PHP cross-check on ~10 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. -- [ ] 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 8d950d9c..be5b75cd 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, 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, and string literals case-insensitively naming a known builtin or user function. Non-literal strings, `[$obj, "method"]` arrays, and `__invoke` objects are tracked as follow-ups. | | `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 308cb38b..22000f17 100644 --- a/examples/callbacks/main.php +++ b/examples/callbacks/main.php @@ -76,10 +76,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/function_exists.rs b/src/codegen/builtins/arrays/function_exists.rs index 869b48d3..2109c373 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,18 @@ 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::AlwaysAvailable) => { + 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..06d63bf6 --- /dev/null +++ b/src/codegen/builtins/callable_lookup.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Resolves string-literal function names used by callable/introspection builtins. +//! Shares PHP case-insensitive lookup between `function_exists()` and `is_callable()`. +//! +//! Called from: +//! - `crate::codegen::builtins::arrays::function_exists` +//! - `crate::codegen::builtins::types::is_callable` +//! +//! Key details: +//! - Include-discovered function variants stay distinguishable so `function_exists()` can keep runtime load-order behavior. + +use crate::codegen::context::Context; +use crate::names::php_symbol_key; +use crate::types::checker::builtins::is_supported_builtin_function; + +pub(super) enum FunctionLookup { + AlwaysAvailable, + IncludeVariant(String), +} + +pub(super) fn lookup_function(ctx: &Context, name: &str) -> Option { + lookup_folded(ctx.function_variant_groups.iter(), name) + .map(FunctionLookup::IncludeVariant) + .or_else(|| { + (is_supported_builtin_function(name) + || lookup_folded(ctx.functions.keys(), name).is_some() + || lookup_folded(ctx.extern_functions.keys(), name).is_some()) + .then_some(FunctionLookup::AlwaysAvailable) + }) +} + +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..2f17582c 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; +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 bef92395..e8d29aca 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 @@ -39,9 +40,7 @@ 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 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/tests/codegen/case_insensitive_symbols.rs b/tests/codegen/case_insensitive_symbols.rs index 58d094fe..4deaca10 100644 --- a/tests/codegen/case_insensitive_symbols.rs +++ b/tests/codegen/case_insensitive_symbols.rs @@ -95,10 +95,11 @@ function FormatName(string $name): string { } echo FUNCTION_EXISTS("formatname") ? "Y:" : "N:"; +echo IS_CALLABLE("FORMATNAME") ? "C:" : "N:"; echo CALL_USER_FUNC("formatname", "ada"); "#, ); - assert_eq!(out, "Y:ADA"); + assert_eq!(out, "Y:C:ADA"); } #[test] From 0868dd3d7de3f80e0a45f4cf9ffde88faa89e29b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 15 May 2026 23:23:00 +0200 Subject: [PATCH 2/2] fix: align callable string lookup fallbacks --- src/codegen/builtins/arrays/call_user_func.rs | 39 +++++++++++++ .../builtins/arrays/call_user_func_array.rs | 41 ++++++++++++++ .../builtins/arrays/function_exists.rs | 6 +- src/codegen/builtins/callable_lookup.rs | 28 ++++++---- src/types/checker/builtins/callables.rs | 55 +++++++++++++++++-- src/types/checker/driver/functions.rs | 19 ++++++- src/types/checker/functions/resolution/mod.rs | 8 +++ tests/codegen/arrays/callbacks.rs | 6 ++ .../codegen/callables/constants_and_system.rs | 6 ++ tests/codegen/case_insensitive_symbols.rs | 5 +- tests/codegen/ffi/extern_calls.rs | 22 ++++++++ tests/error_tests/callables.rs | 8 +++ 12 files changed, 222 insertions(+), 21 deletions(-) diff --git a/src/codegen/builtins/arrays/call_user_func.rs b/src/codegen/builtins/arrays/call_user_func.rs index 3c0cc86e..b7f2116f 100644 --- a/src/codegen/builtins/arrays/call_user_func.rs +++ b/src/codegen/builtins/arrays/call_user_func.rs @@ -18,6 +18,7 @@ use crate::names::function_symbol; 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, @@ -27,6 +28,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 { @@ -66,6 +94,17 @@ pub fn emit( ExprKind::StringLiteral(name) => name.clone(), _ => panic!("call_user_func() callback must be a string literal, callable expression, or callable variable"), }; + let func_name = match lookup_function(ctx, &func_name) { + Some(FunctionLookup::UserFunction(name)) + | Some(FunctionLookup::IncludeVariant(name)) => name, + Some(FunctionLookup::Builtin(name)) | Some(FunctionLookup::Extern(name)) => { + panic!( + "call_user_func() direct string callback '{}' should have been emitted directly", + name + ); + } + None => func_name, + }; let label = function_symbol(&func_name); sig = ctx.functions.get(&func_name).cloned(); abi::emit_symbol_address(emitter, call_reg, &label); diff --git a/src/codegen/builtins/arrays/call_user_func_array.rs b/src/codegen/builtins/arrays/call_user_func_array.rs index 06f649b5..c75751d9 100644 --- a/src/codegen/builtins/arrays/call_user_func_array.rs +++ b/src/codegen/builtins/arrays/call_user_func_array.rs @@ -18,6 +18,7 @@ use crate::names::function_symbol; 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 { @@ -62,6 +63,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 { @@ -111,6 +141,17 @@ pub fn emit( ExprKind::StringLiteral(name) => name.clone(), _ => panic!("call_user_func_array() callback must be a string literal, callable expression, or callable variable"), }; + let func_name = match lookup_function(ctx, &func_name) { + Some(FunctionLookup::UserFunction(name)) + | Some(FunctionLookup::IncludeVariant(name)) => name, + Some(FunctionLookup::Builtin(name)) | Some(FunctionLookup::Extern(name)) => { + panic!( + "call_user_func_array() string callback '{}' requires a literal argument array for builtin or extern lowering", + name + ); + } + None => func_name, + }; let label = function_symbol(&func_name); abi::emit_symbol_address(emitter, call_reg, &label); ctx.functions diff --git a/src/codegen/builtins/arrays/function_exists.rs b/src/codegen/builtins/arrays/function_exists.rs index 2109c373..aed58a68 100644 --- a/src/codegen/builtins/arrays/function_exists.rs +++ b/src/codegen/builtins/arrays/function_exists.rs @@ -40,7 +40,11 @@ pub fn emit( emit_variant_function_exists(&variant_name, emitter, data); return Some(PhpType::Bool); } - Some(FunctionLookup::AlwaysAvailable) => { + Some( + FunctionLookup::Builtin(_) + | FunctionLookup::Extern(_) + | FunctionLookup::UserFunction(_), + ) => { abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 1); } None => { diff --git a/src/codegen/builtins/callable_lookup.rs b/src/codegen/builtins/callable_lookup.rs index 06d63bf6..c1ba6f72 100644 --- a/src/codegen/builtins/callable_lookup.rs +++ b/src/codegen/builtins/callable_lookup.rs @@ -1,32 +1,36 @@ //! Purpose: //! Resolves string-literal function names used by callable/introspection builtins. -//! Shares PHP case-insensitive lookup between `function_exists()` and `is_callable()`. +//! 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-discovered function variants stay distinguishable so `function_exists()` can keep runtime load-order behavior. +//! - 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::is_supported_builtin_function; +use crate::types::checker::builtins::canonical_builtin_function_name; pub(super) enum FunctionLookup { - AlwaysAvailable, + Builtin(String), + Extern(String), + UserFunction(String), IncludeVariant(String), } pub(super) fn lookup_function(ctx: &Context, name: &str) -> Option { - lookup_folded(ctx.function_variant_groups.iter(), name) - .map(FunctionLookup::IncludeVariant) - .or_else(|| { - (is_supported_builtin_function(name) - || lookup_folded(ctx.functions.keys(), name).is_some() - || lookup_folded(ctx.extern_functions.keys(), name).is_some()) - .then_some(FunctionLookup::AlwaysAvailable) - }) + 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 diff --git a/src/types/checker/builtins/callables.rs b/src/types/checker/builtins/callables.rs index 55035a7f..b333711c 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>; @@ -195,6 +196,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() { if sig.ref_params.iter().any(|is_ref| *is_ref) { return Err(CompileError::new( @@ -223,7 +250,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() { @@ -232,7 +259,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)); } } @@ -302,6 +329,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, @@ -313,7 +355,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)? { @@ -429,6 +471,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()) { @@ -438,12 +483,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#"