From 5c8f4d032f158188f613083f749f720e7e5b23d2 Mon Sep 17 00:00:00 2001 From: Chad Peppers Date: Sat, 11 Jul 2026 08:54:57 -0500 Subject: [PATCH 1/2] feat: resolve the object pseudo-type and Closure type-hint --- src/name_resolver/names.rs | 2 +- src/types/checker/type_compat/declarations.rs | 16 ++++++++++++++++ src/types/checker/type_compat/pointers.rs | 6 ++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/name_resolver/names.rs b/src/name_resolver/names.rs index 003dde2bf6..dee5ee66fd 100644 --- a/src/name_resolver/names.rs +++ b/src/name_resolver/names.rs @@ -82,7 +82,7 @@ pub(super) fn resolve_type_expr( } TypeExpr::Named(name) => { let raw = name.as_str(); - if matches!(raw, "array" | "mixed" | "callable" | "void") { + if matches!(raw, "array" | "mixed" | "callable" | "void" | "object" | "Closure") { TypeExpr::Named(name.clone()) } else { TypeExpr::Named(resolved_name(resolve_special_or_class_name( diff --git a/src/types/checker/type_compat/declarations.rs b/src/types/checker/type_compat/declarations.rs index ec7b0415ef..d4a8999bba 100644 --- a/src/types/checker/type_compat/declarations.rs +++ b/src/types/checker/type_compat/declarations.rs @@ -102,6 +102,12 @@ impl Checker { &format!("{} cannot use type never", context), )); } + // PHP permits the `Closure` class as a property type even though it forbids the bare + // `callable` pseudo-type. Closures resolve to Callable internally, so let the `Closure` + // spelling through the callable restriction (a `callable` property is still rejected). + if Self::type_expr_names_closure(type_expr) { + return Ok(ty); + } if Self::type_contains_callable(&ty) { return Err(CompileError::new( span, @@ -111,6 +117,16 @@ impl Checker { Ok(ty) } + /// Returns true if `type_expr` is the built-in `Closure` class name — permitted as a property + /// type, unlike the bare `callable` pseudo-type that closures resolve to internally. + fn type_expr_names_closure(type_expr: &TypeExpr) -> bool { + matches!( + type_expr, + TypeExpr::Named(name) + if name.as_str().trim_start_matches('\\').eq_ignore_ascii_case("Closure") + ) + } + /// Returns true if `ty` is or contains a `PhpType::Callable` anywhere in its structure. fn type_contains_callable(ty: &PhpType) -> bool { match ty { diff --git a/src/types/checker/type_compat/pointers.rs b/src/types/checker/type_compat/pointers.rs index be6af3547c..fc09526739 100644 --- a/src/types/checker/type_compat/pointers.rs +++ b/src/types/checker/type_compat/pointers.rs @@ -119,6 +119,12 @@ impl Checker { "callable" => Ok(PhpType::Callable), "void" => Ok(PhpType::Void), "array" => Ok(PhpType::Array(Box::new(PhpType::Mixed))), + // The built-in `Closure` class types any closure/arrow-fn value; closures + // resolve to Callable internally, so `Closure` type-hints resolve to Callable. + "closure" => Ok(PhpType::Callable), + // The generic `object` pseudo-type accepts any object; elephc has no classless + // object variant, so it resolves to Mixed (which boxes any value, objects too). + "object" => Ok(PhpType::Mixed), // Relative class types only survive to this point when used outside a class // body; inside a class they are rewritten to the enclosing class beforehand. relative @ ("self" | "static" | "parent") => Err(CompileError::new( From c1170525ffae914c98575d4377cdb4d7c98e2bd1 Mon Sep 17 00:00:00 2001 From: Chad Peppers Date: Sat, 11 Jul 2026 08:55:22 -0500 Subject: [PATCH 2/2] test: object pseudo-type and Closure hint resolve end-to-end --- tests/codegen/oop/misc.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/codegen/oop/misc.rs b/tests/codegen/oop/misc.rs index 32b46055ca..4906f687d2 100644 --- a/tests/codegen/oop/misc.rs +++ b/tests/codegen/oop/misc.rs @@ -102,3 +102,14 @@ fn test_example_v017_trio_compiles_and_runs() { let out = compile_and_run(include_str!("../../../examples/v017-trio/main.php")); assert_eq!(out, "health:[ok]:missing"); } + +/// The `object` built-in pseudo-type and the `Closure` type-hint resolve as types (param, +/// return, property positions) instead of being treated as namespaced class names. +/// Byte-parity vs PHP 8.5. +#[test] +fn test_object_pseudo_type_and_closure_hint_resolve() { + let out = compile_and_run( + "v : '?'; } function run(Closure $f): string { return $f(); } echo tag(new T('t')), ':', run(function (): string { return 'c'; });", + ); + assert_eq!(out, "t:c"); +}