Skip to content
Open
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 src/name_resolver/names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
16 changes: 16 additions & 0 deletions src/types/checker/type_compat/declarations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions src/types/checker/type_compat/pointers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
11 changes: 11 additions & 0 deletions tests/codegen/oop/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
"<?php final class T { public function __construct(public string $v) {} } function tag(object $o): string { return $o instanceof T ? $o->v : '?'; } function run(Closure $f): string { return $f(); } echo tag(new T('t')), ':', run(function (): string { return 'c'; });",
);
assert_eq!(out, "t:c");
}
Loading