diff --git a/src/types/checker/type_compat/declarations.rs b/src/types/checker/type_compat/declarations.rs index ec7b0415ef..5fa1c00a95 100644 --- a/src/types/checker/type_compat/declarations.rs +++ b/src/types/checker/type_compat/declarations.rs @@ -9,7 +9,7 @@ //! - Rules here define accepted programs, so PHP covariance, inheritance, and extension-specific constraints must stay explicit. use crate::errors::CompileError; -use crate::parser::ast::{Expr, TypeExpr}; +use crate::parser::ast::{Expr, ExprKind, StaticReceiver, TypeExpr}; use crate::types::{callable_wrapper_sig, ClassInfo, FunctionSig, PhpType}; use super::super::inference::syntactic::infer_expr_type_syntactic; @@ -184,6 +184,23 @@ impl Checker { context: &str, ) -> Result<(), CompileError> { if let Some(default_expr) = default_expr { + // An enum-case access used as a parameter default (`E $x = E::Case`) has the + // enum object type, but purely-syntactic inference types every `::` access as + // Str (it has no class table, and enum cases are populated after this schema + // pass). When the declared type is an enum/class object and the default is a + // scoped access naming that same type, accept it here; the later semantic pass + // validates that the case actually exists. + if let PhpType::Object(expected_class) = expected_ty { + if let ExprKind::ScopedConstantAccess { + receiver: StaticReceiver::Named(recv), + .. + } = &default_expr.kind + { + if recv.as_canonical() == *expected_class { + return Ok(()); + } + } + } let default_ty = infer_expr_type_syntactic(default_expr); self.require_compatible_arg_type(expected_ty, &default_ty, span, context)?; } diff --git a/tests/codegen/types/enums.rs b/tests/codegen/types/enums.rs index 0a5b9ff6bf..acf3a1c8d0 100644 --- a/tests/codegen/types/enums.rs +++ b/tests/codegen/types/enums.rs @@ -833,3 +833,14 @@ fn test_backed_int_enum_tryfrom_mixed() { ); assert_eq!(out, "HighnullLow"); } + +/// An enum-case parameter default (`Level $l = Level::Low`) types as the ENUM, not its backing +/// scalar, so the declared parameter type accepts the default and calls without the argument use +/// it. Byte-parity vs PHP 8.5. +#[test] +fn test_enum_case_parameter_default() { + let out = compile_and_run( + "value; } echo tag(), ':', tag(Level::High);", + ); + assert_eq!(out, "low:high"); +}