From baca561891b38b0cdb84775e2cbf5da69da2241d Mon Sep 17 00:00:00 2001 From: Chad Peppers Date: Wed, 1 Jul 2026 12:57:41 -0500 Subject: [PATCH 1/2] fix: type enum-case parameter defaults as the enum, not the backing scalar A parameter default that is an enum-case access (`function f(E $x = E::Case)`) has the enum object type. elephc's default-value validation inferred the default's type purely syntactically, and syntactic inference types every `::` access as Str (it has no class table, and enum cases are populated only after the schema pass runs). So an enum-typed parameter with an enum-case default was rejected: "Method parameter $x expects Object(E), got Str". When the declared parameter type is an object type and the default is a scoped-constant access naming that same type, accept it in validate_declared_default_type; the later semantic pass still validates that the named case exists. The check is ordering-independent (a name comparison, not an enum-table lookup) and falls through to the existing syntactic check for every other default. Discovered compiling a real strict-types codebase: a constructor `public HeadAssetMode $mode = HeadAssetMode::Default` would not compile. Test: an enum-case default flows through ->value both when the default is taken and when an explicit case is passed. (cherry picked from commit e3ab06298d77ea7d5e0f06f1e2ab581a27313dbd) (cherry picked from commit 95cd8677dc9798408272880415c656729f165680) --- src/types/checker/type_compat/declarations.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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)?; } From a7144e63cb8a3a2741a6b260717b480166e053bb Mon Sep 17 00:00:00 2001 From: Chad Peppers Date: Sun, 5 Jul 2026 21:45:41 -0500 Subject: [PATCH 2/2] test: enum-case parameter defaults type as the enum Co-Authored-By: Claude Fable 5 --- tests/codegen/types/enums.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) 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"); +}