diff --git a/CHANGELOG.md b/CHANGELOG.md index 2188836cbe..c226001765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to elephc, a PHP-to-native compiler written in Rust. Releases are listed newest first. ## [Unreleased] +- Object-subtype declaration defaults are now validated after class and interface schemas are complete: parameters, methods, and constructor-promoted properties may use an implementing class or subclass instance as the default for an interface/base-class type, while unrelated object defaults are still rejected with a type error. - `htmlspecialchars()` / `htmlentities()` now accept the optional `$flags` and `$encoding` arguments (issue #506), and the `ENT_*` constants (`ENT_QUOTES`, `ENT_COMPAT`, `ENT_NOQUOTES`, `ENT_HTML401`, `ENT_HTML5`, `ENT_XHTML`, `ENT_XML1`, `ENT_SUBSTITUTE`, `ENT_IGNORE`) are defined with PHP's values. The escaper currently always applies `ENT_QUOTES` behavior. - Static interface methods (PHP 8.3+): an interface may declare `public static function` signatures; a concrete implementing class must provide a compatible public static method (abstract classes may defer to a concrete child), dispatched by class with no vtable slot. `#[\Override]` is accepted on the static implementation, including when the interface is implemented by an abstract parent class. - Deprecated `${var}` / `${expr}` string interpolation is now accepted by the lexer (issue #340), matching PHP 8.x's deprecated-but-working behavior. diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index 5d8af9517a..194084619b 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -40,7 +40,7 @@ use super::builtin_stdclass::inject_builtin_stdclass; use super::builtin_user_filter::inject_builtin_user_filter; use super::schema::{ build_class_info_recursive, build_enum_info, build_interface_info_recursive, - drop_unresolvable_attribute_arg_refs, + drop_unresolvable_attribute_arg_refs, validate_deferred_object_defaults, }; use super::yield_validation::validate_yield_contexts; use super::Checker; @@ -256,6 +256,11 @@ pub(super) fn check_types_impl( } } } + errors.extend(validate_deferred_object_defaults( + &checker, + &flattened_classes, + program, + )); // All class/interface/enum metadata now exists, so deferred symbolic // attribute-argument references can be checked for resolvability. Drop any // the EIR backend cannot lower (e.g. built-in `Attribute::TARGET_CLASS`) so diff --git a/src/types/checker/schema/classes/properties.rs b/src/types/checker/schema/classes/properties.rs index 88d46ab65b..5287c1b435 100644 --- a/src/types/checker/schema/classes/properties.rs +++ b/src/types/checker/schema/classes/properties.rs @@ -94,7 +94,7 @@ fn apply_static_property( } let ty = if let Some(declared_ty) = declared_ty { - checker.validate_declared_default_type( + checker.validate_schema_declared_default_type( &declared_ty, prop.default.as_ref(), prop.span, @@ -248,7 +248,7 @@ fn apply_instance_property( } let ty = if let Some(declared_ty) = resolve_property_declared_type(checker, &class.name, prop)? { - checker.validate_declared_default_type( + checker.validate_schema_declared_default_type( &declared_ty, prop.default.as_ref(), prop.span, @@ -341,7 +341,7 @@ fn apply_instance_property_redeclaration( )?; let ty = if let Some(declared_ty) = declared_ty { - checker.validate_declared_default_type( + checker.validate_schema_declared_default_type( &declared_ty, prop.default.as_ref(), prop.span, diff --git a/src/types/checker/schema/defaults.rs b/src/types/checker/schema/defaults.rs new file mode 100644 index 0000000000..17be7dc2db --- /dev/null +++ b/src/types/checker/schema/defaults.rs @@ -0,0 +1,191 @@ +//! Purpose: +//! Revalidates object-typed declaration defaults after class-like schemas are complete. +//! Covers class/interface methods and declared instance/static property defaults. +//! +//! Called from: +//! - `crate::types::checker::driver::check_types_impl()` after enum schema construction. +//! +//! Key details: +//! - The initial schema pass cannot reliably resolve inheritance or interface relationships. +//! - Only deferred Object-to-Object pairs are revisited; scalar checks stay in the initial pass. + +use crate::errors::CompileError; +use crate::names::php_symbol_key; +use crate::parser::ast::{Expr, Program, StmtKind}; +use crate::types::{traits::FlattenedClass, FunctionSig, PhpType}; + +use super::super::{infer_expr_type_syntactic, Checker}; + +/// Validates every object-typed default deferred during class-like schema construction. +/// Returns all incompatibilities so the driver can aggregate them with other schema errors. +pub(crate) fn validate_deferred_object_defaults( + checker: &Checker, + flattened_classes: &[FlattenedClass], + program: &Program, +) -> Vec { + let mut errors = Vec::new(); + + for class in flattened_classes { + validate_class_defaults(checker, &class.name, &mut errors); + } + for stmt in program { + if let StmtKind::EnumDecl { name, .. } = &stmt.kind { + validate_class_defaults(checker, name, &mut errors); + } + } + + for stmt in program { + let StmtKind::InterfaceDecl { name, .. } = &stmt.kind else { + continue; + }; + let Some(interface_info) = checker.interfaces.get(name) else { + continue; + }; + let interface_key = php_symbol_key(name); + for method_key in &interface_info.method_order { + let is_declared_here = interface_info + .method_declaring_interfaces + .get(method_key) + .is_some_and(|declaring| php_symbol_key(declaring) == interface_key); + if !is_declared_here { + continue; + } + let Some(signature) = interface_info.methods.get(method_key) else { + continue; + }; + validate_signature_object_defaults(checker, signature, "Method", &mut errors); + } + } + + errors +} + +/// Revalidates local method and property defaults for one source-declared class or enum. +fn validate_class_defaults(checker: &Checker, class_name: &str, errors: &mut Vec) { + let Some(class_info) = checker.classes.get(class_name) else { + return; + }; + validate_class_property_defaults(checker, class_name, class_info, errors); + + for method in &class_info.method_decls { + let method_key = php_symbol_key(&method.name); + let signature = if method.is_static { + class_info.static_methods.get(&method_key) + } else { + class_info.methods.get(&method_key) + }; + let Some(signature) = signature else { + continue; + }; + validate_signature_object_defaults(checker, signature, "Method", errors); + } +} + +/// Revalidates local declared instance and static property defaults for one class. +fn validate_class_property_defaults( + checker: &Checker, + class_name: &str, + class_info: &crate::types::ClassInfo, + errors: &mut Vec, +) { + for (index, (property_name, expected_ty)) in class_info.properties.iter().enumerate() { + let is_local_declared_property = class_info.declared_properties.contains(property_name) + && class_info + .property_declaring_classes + .get(property_name) + .is_some_and(|declaring| declaring == class_name); + if !is_local_declared_property { + continue; + } + let Some(default) = class_info.defaults.get(index).and_then(Option::as_ref) else { + continue; + }; + validate_object_default( + checker, + expected_ty, + default, + &format!("Property {}::${} default", class_name, property_name), + errors, + ); + } + + for (index, (property_name, expected_ty)) in class_info.static_properties.iter().enumerate() { + let is_local_declared_property = class_info + .declared_static_properties + .contains(property_name) + && class_info + .static_property_declaring_classes + .get(property_name) + .is_some_and(|declaring| declaring == class_name); + if !is_local_declared_property { + continue; + } + let Some(default) = class_info + .static_defaults + .get(index) + .and_then(Option::as_ref) + else { + continue; + }; + validate_object_default( + checker, + expected_ty, + default, + &format!("Static property {}::${} default", class_name, property_name), + errors, + ); + } +} + +/// Revalidates object defaults for declared parameters in one resolved callable signature. +fn validate_signature_object_defaults( + checker: &Checker, + signature: &FunctionSig, + callable_kind: &str, + errors: &mut Vec, +) { + for (index, ((param_name, expected_ty), default)) in signature + .params + .iter() + .zip(signature.defaults.iter()) + .enumerate() + { + if !signature + .declared_params + .get(index) + .copied() + .unwrap_or(false) + { + continue; + } + let Some(default) = default.as_ref() else { + continue; + }; + validate_object_default( + checker, + expected_ty, + default, + &format!("{} parameter ${}", callable_kind, param_name), + errors, + ); + } +} + +/// Checks one deferred default when both its declared and syntactic types are objects. +fn validate_object_default( + checker: &Checker, + expected_ty: &PhpType, + default: &Expr, + context: &str, + errors: &mut Vec, +) { + let default_ty = infer_expr_type_syntactic(default); + if !matches!(expected_ty, PhpType::Object(_)) || !matches!(default_ty, PhpType::Object(_)) { + return; + } + if let Err(error) = + checker.require_compatible_arg_type(expected_ty, &default_ty, default.span, context) + { + errors.extend(error.flatten()); + } +} diff --git a/src/types/checker/schema/mod.rs b/src/types/checker/schema/mod.rs index 0e14b20774..f30a635970 100644 --- a/src/types/checker/schema/mod.rs +++ b/src/types/checker/schema/mod.rs @@ -10,11 +10,13 @@ pub(crate) mod validation; mod attribute_refs; +mod defaults; mod interfaces; mod classes; mod enums; pub(crate) use attribute_refs::drop_unresolvable_attribute_arg_refs; +pub(crate) use defaults::validate_deferred_object_defaults; pub(crate) use interfaces::*; pub(crate) use classes::*; pub(crate) use enums::*; diff --git a/src/types/checker/schema/validation.rs b/src/types/checker/schema/validation.rs index e92eb7e451..bb459cd709 100644 --- a/src/types/checker/schema/validation.rs +++ b/src/types/checker/schema/validation.rs @@ -62,7 +62,7 @@ pub(crate) fn build_method_sig( method.params.iter().zip(params.iter()) { if type_ann.is_some() { - checker.validate_declared_default_type( + checker.validate_schema_declared_default_type( resolved_ty, default.as_ref(), method.span, diff --git a/src/types/checker/type_compat/declarations.rs b/src/types/checker/type_compat/declarations.rs index ba183a4dd4..ec7b0415ef 100644 --- a/src/types/checker/type_compat/declarations.rs +++ b/src/types/checker/type_compat/declarations.rs @@ -190,6 +190,26 @@ impl Checker { Ok(()) } + /// Validates a declaration default while class-like schema metadata is still being built. + /// Object-to-object checks are deferred because inheritance and interface relationships are + /// incomplete during this phase; every other type pair is validated immediately. + pub(crate) fn validate_schema_declared_default_type( + &self, + expected_ty: &PhpType, + default_expr: Option<&Expr>, + span: crate::span::Span, + context: &str, + ) -> Result<(), CompileError> { + if let Some(default_expr) = default_expr { + let default_ty = infer_expr_type_syntactic(default_expr); + if matches!(expected_ty, PhpType::Object(_)) && matches!(default_ty, PhpType::Object(_)) + { + return Ok(()); + } + } + self.validate_declared_default_type(expected_ty, default_expr, span, context) + } + /// Builds the initial parameter type list for a function declaration, resolving type hints, /// validating defaults, and inferring types for untyped parameters. Adds variadic parameter /// type as `PhpType::Array(Int)` if the function is variadic. diff --git a/tests/codegen/objects/constructor_promotion.rs b/tests/codegen/objects/constructor_promotion.rs index 749651857f..336d31e6e0 100644 --- a/tests/codegen/objects/constructor_promotion.rs +++ b/tests/codegen/objects/constructor_promotion.rs @@ -157,3 +157,31 @@ echo $box->value; ); assert_eq!(out, "7:9"); } + +/// Verifies that an interface-typed promoted property accepts a concrete implementing-class +/// default after schema construction, and that the default instance dispatches normally. +#[test] +fn test_interface_promoted_property_accepts_object_default() { + let out = compile_and_run( + r#"v(); } +} +function read_value(I $x = new N()): string { return $x->v(); } +$c = new C(); +echo $c->x->v(); +echo ":"; +echo $c->read(); +echo ":"; +echo read_value(); +"#, + ); + assert_eq!(out, "n:n:n"); +} diff --git a/tests/error_tests/type_system.rs b/tests/error_tests/type_system.rs index 4711d2a200..146a5d1694 100644 --- a/tests/error_tests/type_system.rs +++ b/tests/error_tests/type_system.rs @@ -447,6 +447,21 @@ $box = new Box("bad"); ); } +/// Verifies that an unrelated object default is rejected after class relationships are known. +#[test] +fn test_error_promoted_property_rejects_incompatible_object_default() { + expect_error( + r#"