From 28c8342d18a6723f37559dfe2c712cc1d59468b4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 16 May 2026 22:07:49 +0200 Subject: [PATCH 1/3] feat: complete oop property parity v2 --- ROADMAP.md | 2 +- docs/internals/the-parser.md | 6 +- docs/internals/the-type-checker.md | 2 + docs/php/classes.md | 29 +- examples/abstract-properties/main.php | 4 +- examples/interfaces/main.php | 6 +- src/codegen/expr/calls/args/common.rs | 81 ++++++ src/codegen/expr/calls/args/emit.rs | 14 +- src/codegen/expr/calls/args/mod.rs | 2 +- src/codegen/expr/calls/args/named/temps.rs | 11 +- src/codegen/runtime/data/user.rs | 1 + src/conditional/stmts.rs | 18 +- src/magic_constants/walker/stmts.rs | 5 + src/name_resolver/declarations.rs | 2 + src/optimize/control/dce.rs | 2 + src/optimize/control/fold.rs | 2 + src/optimize/control/prune/statements.rs | 2 + src/optimize/fold/expr.rs | 1 + src/optimize/propagate/stmt.rs | 2 + src/optimize/propagate/stmt/declarations.rs | 1 + src/optimize/tests/fold.rs | 1 + src/parser/ast/mod.rs | 2 +- src/parser/ast/oop.rs | 24 ++ src/parser/ast/stmt.rs | 1 + src/parser/stmt/oop/body.rs | 239 ++++++++++++++-- src/parser/stmt/oop/method_params.rs | 32 +-- src/resolver/engine.rs | 3 +- src/resolver/stmt_exprs.rs | 9 + src/types/checker/builtin_interfaces.rs | 11 + src/types/checker/builtin_json.rs | 1 + .../checker/builtin_types/declarations.rs | 3 + src/types/checker/builtin_types/exception.rs | 5 +- src/types/checker/builtin_types/reflection.rs | 1 + src/types/checker/driver/mod.rs | 2 + .../checker/schema/classes/interfaces.rs | 270 +++++++++++++++++- .../checker/schema/classes/properties.rs | 138 +++++++-- src/types/checker/schema/classes/state.rs | 8 +- src/types/checker/schema/enums.rs | 1 + src/types/checker/schema/interfaces.rs | 185 +++++++++++- src/types/mod.rs | 1 + src/types/schema.rs | 22 ++ src/types/traits/merge.rs | 10 + .../codegen/objects/constructor_promotion.rs | 36 +++ tests/codegen/oop/abstract_properties.rs | 20 +- tests/codegen/oop/interfaces.rs | 64 +++++ tests/codegen/oop/traits.rs | 23 ++ tests/error_tests/misc/classes.rs | 125 ++++++-- tests/parser_tests/classes/declarations.rs | 22 ++ tests/parser_tests/classes/modifiers.rs | 33 +++ 49 files changed, 1351 insertions(+), 134 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 173f44f3..9e5e80da 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -449,7 +449,7 @@ runtime helpers, and standard-library surfaces. - [x] JSON pretty-print optimization — inline indent emission inside each container encoder (assoc, array_int/str/dynamic, object) and retire the `__rt_json_pretty_apply` post-processor. Eliminates the second buffer walk for JSON_PRETTY_PRINT workloads. Multi-day refactor completed with a `_json_indent_depth` BSS slot, balanced normal-path formatting depth maintenance, reset-at-entry protection across throws, and bytewise PHP cross-check coverage on representative payloads. - [x] `is_callable()` runtime fallback — handle non-literal strings, `[$obj, "method"]` arrays, and objects implementing `__invoke`. The string-literal + Callable-typed compile-time path is already in place. - [x] Case-insensitive user-function lookup — `function_exists("USER_FN")` and `is_callable("USER_FN")` accept any case for user functions through a shared lookup path, matching PHP's function-name rules. -- [ ] OOP property parity v2 — finish PHP 8.4 property-hook contracts, including interface properties and abstract properties in traits; cover `readonly static` properties, instance property redeclaration rules, and the remaining by-reference constructor-promotion gaps (`readonly` and default values) +- [x] OOP property parity v2 — PHP 8.4 property-hook contracts now cover interface properties and abstract properties in traits/classes; `readonly static` remains rejected like PHP, instance property redeclaration validates hook get/set contracts, and by-reference constructor promotion now covers `readonly` runtime fatals plus default-value reference cells. ### Standard PHP Library (SPL) diff --git a/docs/internals/the-parser.md b/docs/internals/the-parser.md index a2b38cc1..e71c3d3f 100644 --- a/docs/internals/the-parser.md +++ b/docs/internals/the-parser.md @@ -137,7 +137,7 @@ Each `Stmt` also carries a source `span` and an `attributes` list. The list is p | `ClassDecl { name, extends, implements, is_abstract, is_final, is_readonly_class, trait_uses, properties, constants, methods }` | `final readonly class Point extends Shape implements Named { use NamedTrait; ... }` | | `EnumDecl { name, backing_type, cases }` | `enum Status: int { case Ok = 1; case Err = 2; }` | | `PackedClassDecl { name, fields }` | `packed class Vec2 { public float $x; public float $y; }` | -| `InterfaceDecl { name, extends, methods, constants }` | `interface Named extends Stringable { public function name(): string; }` | +| `InterfaceDecl { name, extends, properties, methods, constants }` | `interface Named extends Stringable { public string $name { get; } public function name(): string; }` | | `TraitDecl { name, trait_uses, properties, constants, methods }` | `trait Named { public const KIND = "name"; ... }` | | `PropertyAssign { object, property, value }` | `$p->x = 10;` | | `StaticPropertyAssign { receiver, property, value }` | `Counter::$count = 10;`, `self::$count = 10;` | @@ -150,7 +150,7 @@ Each `Stmt` also carries a source `span` and an `attributes` list. The list is p | `ExternGlobalDecl { name, c_type }` | `extern global ptr $environ;` — the declared type is a C-facing `CType`, not a `PhpType` | | `ExprStmt(Expr)` | `my_func();` (expression used as statement) | -Constructor property promotion is normalized during class-body parsing. A parameter such as `public int $id` in `__construct` becomes a `ClassProperty` plus a synthetic leading `PropertyAssign` statement equivalent to `$this->id = $id;`. By-reference promoted parameters preserve a `by_ref` flag on the generated property so codegen can bind the property slot to the referenced argument. Later passes otherwise see ordinary properties and ordinary constructor assignments. +Constructor property promotion is normalized during class-body parsing. A parameter such as `public int $id` in `__construct` becomes a `ClassProperty` plus a synthetic leading `PropertyAssign` statement equivalent to `$this->id = $id;`. Parameter defaults stay on the constructor signature rather than `ClassProperty.default`, matching PHP's distinction between promoted parameter defaults and property defaults. By-reference promoted parameters preserve a `by_ref` flag on the generated property so codegen can bind the property slot to the referenced argument or to a heap reference cell when a default value is used. Later passes otherwise see ordinary properties and ordinary constructor assignments. ### Statement dispatch @@ -227,7 +227,7 @@ forms such as `?T|U` and normalize accepted declarations. | `AttributeGroup` | `attributes`, `span` | One bracketed attribute group. Declaration sites can carry one or more groups. | | `EnumCaseDecl` | `name`, `value`, `span`, `attributes` | A backed or unit enum case declaration, with declaration-level attributes preserved in the AST. | | `ClassConst` | `name`, `visibility`, `is_final`, `value`, `span`, `attributes` | A class, interface, or trait constant declaration. | -| `ClassProperty` | `name`, `visibility`, `type_expr`, `readonly`, `is_final`, `is_static`, `by_ref`, `default`, `span`, `attributes` | A property declaration inside a class or trait, optionally carrying a parsed property type declaration, static-property marker, by-reference promotion marker, or declaration-level attributes | +| `ClassProperty` | `name`, `visibility`, `type_expr`, `hooks`, `readonly`, `is_final`, `is_static`, `is_abstract`, `by_ref`, `default`, `span`, `attributes` | A property declaration inside a class, trait, or interface, optionally carrying a parsed property type declaration, hook contract, static-property marker, by-reference promotion marker, or declaration-level attributes | | `ClassMethod` | `name`, `visibility`, `is_static`, `is_abstract`, `is_final`, `has_body`, `params`, `variadic`, `return_type`, `body`, `span`, `attributes` | A method declaration inside a class, trait, or interface | | `CatchClause` | `exception_types`, `variable`, `body` | A catch arm. `exception_types` supports both single-type and PHP-style multi-catch (`TypeA | TypeB`), and `variable` is optional for PHP 8-style `catch (Exception)` | | `StaticReceiver` | `Named(Name)`, `Self_`, `Static`, `Parent` | Left-hand side of `ClassName::method()`, `self::method()`, `static::method()`, and `parent::method()` | diff --git a/docs/internals/the-type-checker.md b/docs/internals/the-type-checker.md index 58be2590..37070b7b 100644 --- a/docs/internals/the-type-checker.md +++ b/docs/internals/the-type-checker.md @@ -367,6 +367,8 @@ When checking property writes, explicitly declared property types stay fixed. De Constructor-promoted properties reach the checker as ordinary class properties plus synthetic constructor assignments produced by the parser. This lets promoted parameter type hints, defaults, visibility, readonly checks, and by-reference parameter validation reuse the same `FunctionSig`, property metadata, and constructor-to-property mapping used by handwritten constructor assignments. The checker records by-reference promoted properties in `reference_properties`, which codegen uses to store an alias pointer instead of an owned property value. +PHP 8.4 property hook contracts are represented as abstract property requirements on class metadata. Interface properties and abstract trait/class properties record separate get and set type obligations: get contracts are covariant, set contracts are contravariant, and get+set contracts are effectively invariant for ordinary properties. Concrete classes clear those abstract requirements when they redeclare a compatible instance property. + When checking method calls, it verifies the method exists, enforces method visibility (`public`, subclass-visible `protected`, declaring-class-only `private`), validates argument count and types against the method's `FunctionSig`, resolves `parent::method()` against the immediate parent class, resolves `self::method()` against the current lexical class, and accepts `static::method()` as a late-static-bound static call against the current class hierarchy. First-class callable validation uses the same method metadata for `static::method(...)` and stable object receiver targets such as `$obj->method(...)`. When checking `new ClassName(...)`, it also rejects interfaces and abstract classes before codegen. diff --git a/docs/php/classes.md b/docs/php/classes.md index 4f6be8b9..a31a3fad 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -44,9 +44,22 @@ class Product implements Named { public function label() { return strtoupper($this->name()); } } ``` -- signature-only methods, no bodies, no properties +- signature-only methods and PHP 8.4 property hook contracts; method and hook bodies are not allowed in interfaces - interface inheritance flattened transitively with cycle detection +Interface properties must be hooked contracts. A concrete class can satisfy a `{ get; }` contract with a public readable property, a `{ set; }` contract with a public writable property, or both with an invariant public property. Get-only contracts allow covariant concrete types; set-only contracts allow contravariant concrete types. + +```php +value = 3; echo $value; // 3 ``` -Current limitations for by-reference promotion: the promoted property cannot be `readonly`, and by-reference promoted parameters cannot use default values yet. +By-reference promoted parameters may also have defaults. If no argument is passed, elephc creates a private reference cell for the default value; if a variable is passed, the promoted property aliases that variable. `readonly` by-reference promoted properties are rejected at compile time because construction would have to bind an indirect mutable alias to a readonly slot. ## Instance methods and $this Virtual dispatch for overrides. @@ -372,7 +383,7 @@ Same parameter count, same pass-by-reference positions, same default layout, sam ## Traits Flattened at compile time. Support: `use Trait;`, multiple traits, `insteadof`, `as`, trait properties, static trait methods. -Abstract properties in traits are not yet supported; use an abstract base class for property requirements until PHP 8.4 property-hook contracts are implemented. +Traits may declare abstract hooked property contracts. A concrete class using the trait must satisfy the contract directly or inherit it through an abstract base class that is later completed by a concrete child. ## Property access `->` for properties and methods. @@ -588,7 +599,7 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa ## Limitations - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. -- No `readonly` or default-valued by-reference promoted properties +- Property hook bodies are not implemented; elephc supports hook contracts only. - Shadowing a private parent property with a same-named child property is not yet supported (PHP gives them separate slots; elephc uses one slot per name) - Class constants must be literal-or-foldable expressions; `self::OTHER + 1` style recursive references are not supported. - Anonymous classes (`new class { ... }`) are not yet supported. diff --git a/examples/abstract-properties/main.php b/examples/abstract-properties/main.php index 7d2b2f17..50689ba8 100644 --- a/examples/abstract-properties/main.php +++ b/examples/abstract-properties/main.php @@ -1,8 +1,8 @@ name . " has " . $this->sides . " sides"; diff --git a/examples/interfaces/main.php b/examples/interfaces/main.php index 3a9f723c..a2d1fd79 100644 --- a/examples/interfaces/main.php +++ b/examples/interfaces/main.php @@ -1,6 +1,8 @@ name; } } diff --git a/src/codegen/expr/calls/args/common.rs b/src/codegen/expr/calls/args/common.rs index b16a8c24..4b173ceb 100644 --- a/src/codegen/expr/calls/args/common.rs +++ b/src/codegen/expr/calls/args/common.rs @@ -146,6 +146,87 @@ pub(crate) fn push_expr_arg( pushed_ty } +pub(crate) fn push_non_variable_ref_arg_address( + arg: &Expr, + target_ty: Option<&PhpType>, + emitter: &mut Emitter, + ctx: &mut Context, + data: &mut DataSection, +) -> PhpType { + let pushed_ty = push_expr_arg(arg, target_ty, emitter, ctx, data); + abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 16); + abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate a stable 16-byte by-reference cell for a default or temporary argument + let cell_reg = abi::symbol_scratch_reg(emitter); + emitter.instruction(&format!("mov {}, {}", cell_reg, abi::int_result_reg(emitter))); // keep the allocated reference cell address while storing the initial value + store_pushed_value_to_ref_cell(emitter, cell_reg, &pushed_ty); + abi::emit_push_reg(emitter, cell_reg); + PhpType::Int +} + +fn store_pushed_value_to_ref_cell(emitter: &mut Emitter, cell_reg: &str, val_ty: &PhpType) { + let temp_reg = abi::temp_int_reg(emitter.target); + match val_ty.codegen_repr() { + PhpType::Bool + | PhpType::Int + | PhpType::Callable + | PhpType::Pointer(_) + | PhpType::Buffer(_) + | PhpType::Packed(_) => { + abi::emit_pop_reg(emitter, temp_reg); + abi::emit_store_to_address(emitter, temp_reg, cell_reg, 0); + abi::emit_store_zero_to_address(emitter, cell_reg, 8); + } + PhpType::Resource(_) => { + abi::emit_pop_reg(emitter, temp_reg); + abi::emit_store_to_address(emitter, temp_reg, cell_reg, 0); + abi::emit_load_int_immediate(emitter, temp_reg, 9); + abi::emit_store_to_address(emitter, temp_reg, cell_reg, 8); + } + PhpType::Mixed | PhpType::Union(_) | PhpType::Iterable => { + abi::emit_pop_reg(emitter, temp_reg); + abi::emit_store_to_address(emitter, temp_reg, cell_reg, 0); + abi::emit_load_int_immediate(emitter, temp_reg, 7); + abi::emit_store_to_address(emitter, temp_reg, cell_reg, 8); + } + PhpType::Array(_) => { + abi::emit_pop_reg(emitter, temp_reg); + abi::emit_store_to_address(emitter, temp_reg, cell_reg, 0); + abi::emit_load_int_immediate(emitter, temp_reg, 4); + abi::emit_store_to_address(emitter, temp_reg, cell_reg, 8); + } + PhpType::AssocArray { .. } => { + abi::emit_pop_reg(emitter, temp_reg); + abi::emit_store_to_address(emitter, temp_reg, cell_reg, 0); + abi::emit_load_int_immediate(emitter, temp_reg, 5); + abi::emit_store_to_address(emitter, temp_reg, cell_reg, 8); + } + PhpType::Object(_) => { + abi::emit_pop_reg(emitter, temp_reg); + abi::emit_store_to_address(emitter, temp_reg, cell_reg, 0); + abi::emit_load_int_immediate(emitter, temp_reg, 6); + abi::emit_store_to_address(emitter, temp_reg, cell_reg, 8); + } + PhpType::Float => { + abi::emit_pop_float_reg(emitter, abi::float_result_reg(emitter)); + abi::emit_store_to_address(emitter, abi::float_result_reg(emitter), cell_reg, 0); + abi::emit_store_zero_to_address(emitter, cell_reg, 8); + } + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(emitter); + abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); + abi::emit_push_reg(emitter, cell_reg); + abi::emit_call_label(emitter, "__rt_str_persist"); // detach temporary string storage before putting it in the reference cell + abi::emit_pop_reg(emitter, cell_reg); + abi::emit_store_to_address(emitter, ptr_reg, cell_reg, 0); + abi::emit_store_to_address(emitter, len_reg, cell_reg, 8); + } + PhpType::Void | PhpType::Never => { + abi::emit_store_zero_to_address(emitter, cell_reg, 0); + abi::emit_store_zero_to_address(emitter, cell_reg, 8); + } + } +} + fn should_release_owned_mixed_after_arg_coerce( arg: &Expr, source_ty: &PhpType, diff --git a/src/codegen/expr/calls/args/emit.rs b/src/codegen/expr/calls/args/emit.rs index ba37a1ce..616fadd8 100644 --- a/src/codegen/expr/calls/args/emit.rs +++ b/src/codegen/expr/calls/args/emit.rs @@ -14,7 +14,10 @@ use crate::parser::ast::{Expr, ExprKind}; use crate::types::call_args; use crate::types::{FunctionSig, PhpType}; -use super::common::{call_target_ty, emit_ref_arg_variable_address, push_arg_value, push_expr_arg}; +use super::common::{ + call_target_ty, emit_ref_arg_variable_address, push_arg_value, + push_expr_arg, push_non_variable_ref_arg_address, +}; use super::named; use super::normalize::{has_named_args, prepare_call_args}; use super::spread::{emit_spread_into_named_params, emit_spread_tail_variadic_array_arg}; @@ -124,7 +127,7 @@ pub(crate) fn emit_pushed_non_variadic_args( all_args: &[Expr], sig: Option<&FunctionSig>, ref_arg_context_label: &str, - retain_non_variable_ref_args: bool, + _retain_non_variable_ref_args: bool, coerce_inferred_params: bool, emitter: &mut Emitter, ctx: &mut Context, @@ -144,13 +147,10 @@ pub(crate) fn emit_pushed_non_variadic_args( if !emit_ref_arg_variable_address(var_name, ref_arg_context_label, emitter, ctx) { continue; } + push_arg_value(emitter, &PhpType::Int); } else { - let source_ty = super::super::super::emit_expr(arg, emitter, ctx, data); - if retain_non_variable_ref_args { - super::super::super::retain_borrowed_heap_arg(emitter, arg, &source_ty); - } + push_non_variable_ref_arg_address(arg, target_ty, emitter, ctx, data); } - push_arg_value(emitter, &PhpType::Int); arg_types.push(PhpType::Int); } else { let pushed_ty = push_expr_arg(arg, target_ty, emitter, ctx, data); diff --git a/src/codegen/expr/calls/args/mod.rs b/src/codegen/expr/calls/args/mod.rs index 7b5788c1..c28fc420 100644 --- a/src/codegen/expr/calls/args/mod.rs +++ b/src/codegen/expr/calls/args/mod.rs @@ -26,7 +26,7 @@ pub(crate) use array_elements::{ }; pub(crate) use common::{ coerce_current_value_to_target, declared_target_ty, emit_ref_arg_variable_address, - push_arg_value, push_expr_arg, + push_arg_value, push_expr_arg, push_non_variable_ref_arg_address, }; pub(crate) use emit::emit_pushed_call_args; pub(crate) use named::pushed_temp_bytes; diff --git a/src/codegen/expr/calls/args/named/temps.rs b/src/codegen/expr/calls/args/named/temps.rs index b6f76c2a..1017eec4 100644 --- a/src/codegen/expr/calls/args/named/temps.rs +++ b/src/codegen/expr/calls/args/named/temps.rs @@ -15,6 +15,7 @@ use crate::types::{FunctionSig, PhpType}; use super::super::{ declared_target_ty, emit_ref_arg_variable_address, push_arg_value, push_expr_arg, + push_non_variable_ref_arg_address, }; pub(super) fn push_source_temp_type(source_temp_types: &mut Vec, ty: PhpType) -> usize { @@ -29,7 +30,7 @@ pub(super) fn emit_source_temp_arg( sig: &FunctionSig, param_idx: Option, ref_arg_context_label: &str, - retain_non_variable_ref_args: bool, + _retain_non_variable_ref_args: bool, source_temp_types: &mut Vec, emitter: &mut Emitter, ctx: &mut Context, @@ -42,13 +43,11 @@ pub(super) fn emit_source_temp_arg( let pushed_ty = if is_ref { if let ExprKind::Variable(var_name) = &arg.kind { emit_ref_arg_variable_address(var_name, ref_arg_context_label, emitter, ctx); + push_arg_value(emitter, &PhpType::Int); } else { - let source_ty = super::super::super::super::emit_expr(arg, emitter, ctx, data); - if retain_non_variable_ref_args { - super::super::super::super::retain_borrowed_heap_arg(emitter, arg, &source_ty); - } + let target_ty = param_idx.and_then(|idx| declared_target_ty(Some(sig), idx)); + push_non_variable_ref_arg_address(arg, target_ty, emitter, ctx, data); } - push_arg_value(emitter, &PhpType::Int); PhpType::Int } else { let target_ty = param_idx.and_then(|idx| declared_target_ty(Some(sig), idx)); diff --git a/src/codegen/runtime/data/user.rs b/src/codegen/runtime/data/user.rs index 3f225bd0..c760d1e6 100644 --- a/src/codegen/runtime/data/user.rs +++ b/src/codegen/runtime/data/user.rs @@ -812,6 +812,7 @@ mod tests { readonly_properties: HashSet::new(), reference_properties: HashSet::new(), abstract_properties: HashSet::new(), + abstract_property_hooks: HashMap::new(), static_properties: Vec::new(), static_defaults: Vec::new(), static_property_declaring_classes: HashMap::new(), diff --git a/src/conditional/stmts.rs b/src/conditional/stmts.rs index ada06828..e89167e6 100644 --- a/src/conditional/stmts.rs +++ b/src/conditional/stmts.rs @@ -217,7 +217,14 @@ fn rewrite_stmt_kind(kind: StmtKind, defines: &HashSet) -> StmtKind { is_final, is_readonly_class, trait_uses, - properties, + properties: properties + .into_iter() + .map(|mut property| { + property.default = + property.default.map(|expr| rewrite_expr(expr, defines)); + property + }) + .collect(), methods: methods .into_iter() .map(|mut method| { @@ -252,11 +259,20 @@ fn rewrite_stmt_kind(kind: StmtKind, defines: &HashSet) -> StmtKind { StmtKind::InterfaceDecl { name, extends, + properties, methods, constants, } => StmtKind::InterfaceDecl { name, extends, + properties: properties + .into_iter() + .map(|mut property| { + property.default = + property.default.map(|expr| rewrite_expr(expr, defines)); + property + }) + .collect(), methods, constants, }, diff --git a/src/magic_constants/walker/stmts.rs b/src/magic_constants/walker/stmts.rs index 54e983b9..c5cdde98 100644 --- a/src/magic_constants/walker/stmts.rs +++ b/src/magic_constants/walker/stmts.rs @@ -307,11 +307,16 @@ pub(super) fn walk_stmt(stmt: Stmt, pass: &mut P) -> Stmt { StmtKind::InterfaceDecl { name, extends, + properties, methods, constants, } => StmtKind::InterfaceDecl { name, extends, + properties: properties + .into_iter() + .map(|p| walk_class_property(p, pass)) + .collect(), methods: methods .into_iter() .map(|m| walk_class_method(m, pass)) diff --git a/src/name_resolver/declarations.rs b/src/name_resolver/declarations.rs index 11847a51..b199dc75 100644 --- a/src/name_resolver/declarations.rs +++ b/src/name_resolver/declarations.rs @@ -144,6 +144,7 @@ pub(super) fn resolve_decl_stmt( StmtKind::InterfaceDecl { name, extends, + properties, methods, constants, } => { @@ -157,6 +158,7 @@ pub(super) fn resolve_decl_stmt( resolved_name(resolved_class_name(name, namespace, imports, symbols)) }) .collect(), + properties: resolve_properties(properties, namespace, imports, symbols), methods: resolved_methods, constants: resolve_class_consts(constants, namespace, imports, symbols), }, diff --git a/src/optimize/control/dce.rs b/src/optimize/control/dce.rs index 1ec54dc8..65beff6d 100644 --- a/src/optimize/control/dce.rs +++ b/src/optimize/control/dce.rs @@ -469,12 +469,14 @@ fn dce_stmt_with_guards(stmt: Stmt, guards: &GuardState) -> Vec { StmtKind::InterfaceDecl { name, extends, + properties, methods, constants, } => vec![Stmt { kind: StmtKind::InterfaceDecl { name, extends, + properties, methods: methods .into_iter() .map(dce_method_without_context) diff --git a/src/optimize/control/fold.rs b/src/optimize/control/fold.rs index b702be52..229b73a1 100644 --- a/src/optimize/control/fold.rs +++ b/src/optimize/control/fold.rs @@ -215,11 +215,13 @@ pub(crate) fn fold_stmt(stmt: Stmt) -> Stmt { StmtKind::InterfaceDecl { name, extends, + properties, methods, constants, } => StmtKind::InterfaceDecl { name, extends, + properties: properties.into_iter().map(fold_property).collect(), methods: methods.into_iter().map(fold_method).collect(), constants, }, diff --git a/src/optimize/control/prune/statements.rs b/src/optimize/control/prune/statements.rs index da3d40e4..97790d35 100644 --- a/src/optimize/control/prune/statements.rs +++ b/src/optimize/control/prune/statements.rs @@ -314,12 +314,14 @@ pub(crate) fn prune_stmt(stmt: Stmt) -> Vec { StmtKind::InterfaceDecl { name, extends, + properties, methods, constants, } => vec![Stmt { kind: StmtKind::InterfaceDecl { name, extends, + properties, methods: methods .into_iter() .map(prune_method_without_context) diff --git a/src/optimize/fold/expr.rs b/src/optimize/fold/expr.rs index d97ed2a2..9247c8fe 100644 --- a/src/optimize/fold/expr.rs +++ b/src/optimize/fold/expr.rs @@ -32,6 +32,7 @@ pub(in crate::optimize) fn fold_property(property: ClassProperty) -> ClassProper name: property.name, visibility: property.visibility, type_expr: property.type_expr, + hooks: property.hooks, readonly: property.readonly, is_final: property.is_final, is_static: property.is_static, diff --git a/src/optimize/propagate/stmt.rs b/src/optimize/propagate/stmt.rs index 3e26dd44..1475558e 100644 --- a/src/optimize/propagate/stmt.rs +++ b/src/optimize/propagate/stmt.rs @@ -309,6 +309,7 @@ pub(crate) fn propagate_stmt(stmt: Stmt, env: ConstantEnv) -> (Stmt, ConstantEnv StmtKind::InterfaceDecl { name, extends, + properties, methods, constants, } => ( @@ -316,6 +317,7 @@ pub(crate) fn propagate_stmt(stmt: Stmt, env: ConstantEnv) -> (Stmt, ConstantEnv StmtKind::InterfaceDecl { name, extends, + properties: properties.into_iter().map(propagate_property).collect(), methods: methods.into_iter().map(propagate_method).collect(), constants, }, diff --git a/src/optimize/propagate/stmt/declarations.rs b/src/optimize/propagate/stmt/declarations.rs index a9821985..6c802ce4 100644 --- a/src/optimize/propagate/stmt/declarations.rs +++ b/src/optimize/propagate/stmt/declarations.rs @@ -31,6 +31,7 @@ pub(super) fn propagate_property(property: ClassProperty) -> ClassProperty { name: property.name, visibility: property.visibility, type_expr: property.type_expr, + hooks: property.hooks, readonly: property.readonly, is_final: property.is_final, is_static: property.is_static, diff --git a/src/optimize/tests/fold.rs b/src/optimize/tests/fold.rs index deb5214d..77ad8507 100644 --- a/src/optimize/tests/fold.rs +++ b/src/optimize/tests/fold.rs @@ -80,6 +80,7 @@ fn test_fold_string_concat_and_property_default() { name: "label".to_string(), visibility: Visibility::Public, type_expr: None, + hooks: crate::parser::ast::PropertyHooks::none(), readonly: false, is_final: false, is_static: false, diff --git a/src/parser/ast/mod.rs b/src/parser/ast/mod.rs index 1bc22d98..dbd035e6 100644 --- a/src/parser/ast/mod.rs +++ b/src/parser/ast/mod.rs @@ -22,7 +22,7 @@ pub use ffi::{CType, ExternField, ExternParam, PackedField}; pub use operators::BinOp; pub use oop::{ Attribute, AttributeGroup, ClassConst, ClassMethod, ClassProperty, EnumCaseDecl, - TraitAdaptation, TraitUse, Visibility, + PropertyHooks, TraitAdaptation, TraitUse, Visibility, }; pub use stmt::{CatchClause, Program, Stmt, StmtKind, UseItem, UseKind}; pub use types::TypeExpr; diff --git a/src/parser/ast/oop.rs b/src/parser/ast/oop.rs index cbd3e0ae..e1794819 100644 --- a/src/parser/ast/oop.rs +++ b/src/parser/ast/oop.rs @@ -72,6 +72,27 @@ pub enum Visibility { Private, } +#[derive(Debug, Clone, Default, PartialEq)] +pub struct PropertyHooks { + pub get: bool, + pub set: bool, + pub get_by_ref: bool, +} + +impl PropertyHooks { + pub fn none() -> Self { + Self::default() + } + + pub fn any(&self) -> bool { + self.get || self.set || self.get_by_ref + } + + pub fn requires_get(&self) -> bool { + self.get || self.get_by_ref + } +} + #[derive(Debug, Clone)] pub struct TraitUse { pub trait_names: Vec, @@ -106,6 +127,7 @@ pub struct ClassProperty { pub name: String, pub visibility: Visibility, pub type_expr: Option, + pub hooks: PropertyHooks, pub readonly: bool, pub is_final: bool, pub is_static: bool, @@ -121,11 +143,13 @@ impl PartialEq for ClassProperty { fn eq(&self, other: &Self) -> bool { self.name == other.name && self.visibility == other.visibility && self.type_expr == other.type_expr + && self.hooks == other.hooks && self.readonly == other.readonly && self.is_final == other.is_final && self.is_static == other.is_static && self.is_abstract == other.is_abstract && self.by_ref == other.by_ref + && self.default == other.default && self.attributes == other.attributes } } diff --git a/src/parser/ast/stmt.rs b/src/parser/ast/stmt.rs index bd3aba42..0ebe85f9 100644 --- a/src/parser/ast/stmt.rs +++ b/src/parser/ast/stmt.rs @@ -207,6 +207,7 @@ pub enum StmtKind { InterfaceDecl { name: String, extends: Vec, + properties: Vec, methods: Vec, constants: Vec, }, diff --git a/src/parser/stmt/oop/body.rs b/src/parser/stmt/oop/body.rs index bf3c568b..81d7e8d3 100644 --- a/src/parser/stmt/oop/body.rs +++ b/src/parser/stmt/oop/body.rs @@ -11,7 +11,8 @@ use crate::errors::CompileError; use crate::lexer::Token; use crate::parser::ast::{ - ClassConst, ClassMethod, ClassProperty, Stmt, StmtKind, TraitUse, TypeExpr, Visibility, + ClassConst, ClassMethod, ClassProperty, PropertyHooks, Stmt, StmtKind, TraitUse, + TypeExpr, Visibility, }; use crate::parser::expr::parse_expr; use crate::span::Span; @@ -60,7 +61,7 @@ pub(in crate::parser::stmt) fn parse_interface_decl( &Token::LBrace, "Expected '{' after interface name", )?; - let (methods, constants) = parse_interface_body(tokens, pos)?; + let (properties, methods, constants) = parse_interface_body(tokens, pos)?; expect_token( tokens, pos, @@ -72,6 +73,7 @@ pub(in crate::parser::stmt) fn parse_interface_decl( StmtKind::InterfaceDecl { name, extends, + properties, methods, constants, }, @@ -243,16 +245,56 @@ pub(in crate::parser::stmt) fn parse_class_like_body( "Static properties cannot be readonly", )); } + let prop_name = prop_name.clone(); + *pos += 1; + if properties.iter().any(|property| property.name == prop_name) { + return Err(CompileError::new( + member_span, + &format!("Cannot redeclare property ${}", prop_name), + )); + } + let default = if *pos < tokens.len() && tokens[*pos].0 == Token::Assign { + *pos += 1; + Some(parse_expr(tokens, pos)?) + } else { + None + }; + let hooks = parse_property_hooks(tokens, pos, member_span)?; + if modifiers.is_abstract && default.is_some() { + return Err(CompileError::new( + member_span, + &format!("Abstract property ${} cannot have a default value", prop_name), + )); + } + if modifiers.is_abstract && !hooks.any() { + return Err(CompileError::new( + member_span, + "Only hooked properties may be declared abstract", + )); + } + if modifiers.is_static && hooks.any() { + return Err(CompileError::new( + member_span, + "Cannot declare hooks for static property", + )); + } + if modifiers.is_readonly && hooks.any() { + return Err(CompileError::new( + member_span, + "Hooked properties cannot be readonly", + )); + } + if hooks.any() && default.is_some() { + return Err(CompileError::new( + member_span, + "Hooked properties cannot have a default value", + )); + } if modifiers.is_abstract { - if !enclosing_is_abstract { - let message = if owner_kind == "trait" { - "Abstract properties in traits are not yet supported" - } else { - "Abstract properties can only be declared in abstract classes" - }; + if owner_kind != "trait" && !enclosing_is_abstract { return Err(CompileError::new( member_span, - message, + "Abstract properties can only be declared in abstract classes", )); } if modifiers.is_static { @@ -273,32 +315,17 @@ pub(in crate::parser::stmt) fn parse_class_like_body( "Private abstract properties are not supported", )); } - } - let prop_name = prop_name.clone(); - *pos += 1; - if properties.iter().any(|property| property.name == prop_name) { - return Err(CompileError::new( - member_span, - &format!("Cannot redeclare property ${}", prop_name), - )); - } - let default = if *pos < tokens.len() && tokens[*pos].0 == Token::Assign { - *pos += 1; - Some(parse_expr(tokens, pos)?) - } else { - None - }; - if modifiers.is_abstract && default.is_some() { + } else if hooks.any() { return Err(CompileError::new( member_span, - &format!("Abstract property ${} cannot have a default value", prop_name), + "Non-abstract property hook must have a body", )); } - expect_semicolon(tokens, pos)?; properties.push(ClassProperty { name: prop_name, visibility: modifiers.visibility, type_expr, + hooks, readonly: modifiers.is_readonly, is_final: modifiers.is_final, is_static: modifiers.is_static, @@ -493,7 +520,8 @@ fn parse_class_like_method( fn parse_interface_body( tokens: &[(Token, Span)], pos: &mut usize, -) -> Result<(Vec, Vec), CompileError> { +) -> Result<(Vec, Vec, Vec), CompileError> { + let mut properties = Vec::new(); let mut methods = Vec::new(); let mut constants = Vec::new(); @@ -550,10 +578,79 @@ fn parse_interface_body( }); continue; } + let type_expr = parse_optional_property_type(tokens, pos, member_span)?; + if let Some(Token::Variable(prop_name)) = tokens.get(*pos).map(|(t, _)| t.clone()) { + if modifiers.is_abstract { + return Err(CompileError::new( + member_span, + "Property in interface cannot be explicitly abstract", + )); + } + if modifiers.visibility != Visibility::Public { + return Err(CompileError::new( + member_span, + "Property in interface cannot be protected or private", + )); + } + if modifiers.is_final { + return Err(CompileError::new( + member_span, + "Interface properties cannot be final", + )); + } + let prop_name = prop_name.clone(); + *pos += 1; + if properties.iter().any(|property: &ClassProperty| property.name == prop_name) { + return Err(CompileError::new( + member_span, + &format!("Cannot redeclare interface property ${}", prop_name), + )); + } + if *pos < tokens.len() && tokens[*pos].0 == Token::Assign { + return Err(CompileError::new( + member_span, + "Interface properties cannot have a default value", + )); + } + let hooks = parse_property_hooks(tokens, pos, member_span)?; + if !hooks.any() { + return Err(CompileError::new( + member_span, + "Interfaces may only include hooked properties", + )); + } + if modifiers.is_static { + return Err(CompileError::new( + member_span, + "Cannot declare hooks for static property", + )); + } + if modifiers.is_readonly { + return Err(CompileError::new( + member_span, + "Hooked properties cannot be readonly", + )); + } + properties.push(ClassProperty { + name: prop_name, + visibility: Visibility::Public, + type_expr, + hooks, + readonly: false, + is_final: false, + is_static: false, + is_abstract: true, + by_ref: false, + default: None, + span: member_span, + attributes: member_attributes, + }); + continue; + } if tokens[*pos].0 != Token::Function { return Err(CompileError::new( member_span, - "Interfaces may only contain method or constant declarations", + "Interfaces may only contain method, property, or constant declarations", )); } let (mut method, promoted_properties) = parse_class_like_method( @@ -575,5 +672,87 @@ fn parse_interface_body( methods.push(method); } - Ok((methods, constants)) + Ok((properties, methods, constants)) +} + +fn parse_property_hooks( + tokens: &[(Token, Span)], + pos: &mut usize, + span: Span, +) -> Result { + if *pos < tokens.len() && tokens[*pos].0 == Token::Semicolon { + *pos += 1; + return Ok(PropertyHooks::none()); + } + if !matches!(tokens.get(*pos).map(|(t, _)| t), Some(Token::LBrace)) { + return Err(CompileError::new( + span, + "Expected ';' or property hook block after property declaration", + )); + } + *pos += 1; + + let mut hooks = PropertyHooks::none(); + while *pos < tokens.len() && !matches!(tokens[*pos].0, Token::RBrace | Token::Eof) { + let hook_span = tokens[*pos].1; + let get_by_ref = if tokens[*pos].0 == Token::Ampersand { + *pos += 1; + true + } else { + false + }; + let hook_name = match tokens.get(*pos).map(|(t, _)| t) { + Some(Token::Identifier(name)) => name.clone(), + _ => { + return Err(CompileError::new( + hook_span, + "Expected property hook name", + )) + } + }; + *pos += 1; + + if !matches!(tokens.get(*pos).map(|(t, _)| t), Some(Token::Semicolon)) { + return Err(CompileError::new( + hook_span, + "Property hook bodies are not supported yet", + )); + } + *pos += 1; + + if hook_name.eq_ignore_ascii_case("get") { + if hooks.requires_get() { + return Err(CompileError::new(hook_span, "Duplicate get property hook")); + } + hooks.get = !get_by_ref; + hooks.get_by_ref = get_by_ref; + } else if hook_name.eq_ignore_ascii_case("set") { + if get_by_ref { + return Err(CompileError::new( + hook_span, + "Set property hook cannot return by reference", + )); + } + if hooks.set { + return Err(CompileError::new(hook_span, "Duplicate set property hook")); + } + hooks.set = true; + } else { + return Err(CompileError::new( + hook_span, + &format!("Unknown property hook '{}'", hook_name), + )); + } + } + + expect_token( + tokens, + pos, + &Token::RBrace, + "Expected '}' at end of property hook block", + )?; + if !hooks.any() { + return Err(CompileError::new(span, "Expected property hook declaration")); + } + Ok(hooks) } diff --git a/src/parser/stmt/oop/method_params.rs b/src/parser/stmt/oop/method_params.rs index c5da32ff..4f2c3ce0 100644 --- a/src/parser/stmt/oop/method_params.rs +++ b/src/parser/stmt/oop/method_params.rs @@ -10,7 +10,9 @@ use crate::errors::CompileError; use crate::lexer::Token; -use crate::parser::ast::{ClassProperty, Expr, ExprKind, Stmt, StmtKind, TypeExpr, Visibility}; +use crate::parser::ast::{ + ClassProperty, Expr, ExprKind, PropertyHooks, Stmt, StmtKind, TypeExpr, Visibility, +}; use crate::parser::expr::parse_expr; use crate::span::Span; @@ -63,17 +65,12 @@ pub(super) fn parse_method_params( } else { None }; - let is_ref = if *pos < tokens.len() && tokens[*pos].0 == Token::Ampersand { - if promotion.as_ref().is_some_and(|(_, readonly, _)| *readonly) { - return Err(CompileError::new( - span, - "Readonly promoted by-reference properties are not supported", - )); - } + let (is_ref, ref_span) = if *pos < tokens.len() && tokens[*pos].0 == Token::Ampersand { + let ref_span = tokens[*pos].1; *pos += 1; - true + (true, Some(ref_span)) } else { - false + (false, None) }; if *pos < tokens.len() && tokens[*pos].0 == Token::Ellipsis { if promotion.is_some() { @@ -109,22 +106,25 @@ pub(super) fn parse_method_params( } else { None }; - if is_ref && promotion.is_some() && default.is_some() { - return Err(CompileError::new( - span, - "Promoted by-reference properties cannot use default values yet", - )); - } if let Some((visibility, readonly, property_span)) = promotion { + if readonly && is_ref { + return Err(CompileError::new( + ref_span.unwrap_or(property_span), + "Readonly promoted property cannot be by-reference", + )); + } promoted_properties.push(ClassProperty { name: n.clone(), visibility, type_expr: type_ann.clone(), + hooks: PropertyHooks::none(), readonly, is_final: false, is_static: false, is_abstract: false, by_ref: is_ref, + // PHP keeps constructor-promotion defaults on the parameter, + // not on the promoted property's default metadata. default: None, span: property_span, attributes: Vec::new(), diff --git a/src/resolver/engine.rs b/src/resolver/engine.rs index 36f91d3c..9aa347f1 100644 --- a/src/resolver/engine.rs +++ b/src/resolver/engine.rs @@ -380,7 +380,7 @@ pub(super) fn resolve_stmts( stmt.attributes.clone(), )); } - StmtKind::InterfaceDecl { name, extends, methods, + StmtKind::InterfaceDecl { name, extends, properties, methods, constants, } => { let methods = resolve_methods( @@ -395,6 +395,7 @@ pub(super) fn resolve_stmts( StmtKind::InterfaceDecl { name: name.clone(), extends: extends.clone(), + properties: properties.clone(), methods, constants: constants.clone(), }, diff --git a/src/resolver/stmt_exprs.rs b/src/resolver/stmt_exprs.rs index d9af5180..cc2aac02 100644 --- a/src/resolver/stmt_exprs.rs +++ b/src/resolver/stmt_exprs.rs @@ -392,11 +392,20 @@ pub(super) fn resolve_stmt_exprs( StmtKind::InterfaceDecl { name, extends, + properties, methods, constants, } => StmtKind::InterfaceDecl { name, extends, + properties: resolve_properties( + properties, + base_dir, + declared_once, + include_chain, + state, + function_variants, + )?, methods: resolve_method_exprs( methods, base_dir, diff --git a/src/types/checker/builtin_interfaces.rs b/src/types/checker/builtin_interfaces.rs index 3ff2ce4d..6751fe59 100644 --- a/src/types/checker/builtin_interfaces.rs +++ b/src/types/checker/builtin_interfaces.rs @@ -62,6 +62,7 @@ pub(crate) fn inject_builtin_interfaces( InterfaceDeclInfo { name: "Iterator".to_string(), extends: vec!["Traversable".to_string()], + properties: Vec::new(), methods: vec![ builtin_interface_method("current", TypeExpr::Named(Name::unqualified("mixed"))), builtin_interface_method("key", TypeExpr::Named(Name::unqualified("mixed"))), @@ -79,6 +80,7 @@ pub(crate) fn inject_builtin_interfaces( InterfaceDeclInfo { name: "IteratorAggregate".to_string(), extends: vec!["Traversable".to_string()], + properties: Vec::new(), methods: vec![builtin_interface_method( "getIterator", TypeExpr::Named(Name::unqualified("Traversable")), @@ -93,6 +95,7 @@ pub(crate) fn inject_builtin_interfaces( InterfaceDeclInfo { name: "ArrayAccess".to_string(), extends: Vec::new(), + properties: Vec::new(), methods: vec![ builtin_interface_method_with_params( "offsetExists", @@ -125,6 +128,7 @@ pub(crate) fn inject_builtin_interfaces( InterfaceDeclInfo { name: "Countable".to_string(), extends: Vec::new(), + properties: Vec::new(), methods: vec![builtin_interface_method("count", TypeExpr::Int)], span: crate::span::Span::dummy(), constants: Vec::new(), @@ -136,6 +140,7 @@ pub(crate) fn inject_builtin_interfaces( InterfaceDeclInfo { name: "OuterIterator".to_string(), extends: vec!["Iterator".to_string()], + properties: Vec::new(), methods: vec![builtin_interface_method( "getInnerIterator", TypeExpr::Nullable(Box::new(TypeExpr::Named(Name::unqualified("Iterator")))), @@ -150,6 +155,7 @@ pub(crate) fn inject_builtin_interfaces( InterfaceDeclInfo { name: "RecursiveIterator".to_string(), extends: vec!["Iterator".to_string()], + properties: Vec::new(), methods: vec![ builtin_interface_method( "getChildren", @@ -169,6 +175,7 @@ pub(crate) fn inject_builtin_interfaces( InterfaceDeclInfo { name: "SeekableIterator".to_string(), extends: vec!["Iterator".to_string()], + properties: Vec::new(), methods: vec![builtin_interface_method_with_params( "seek", vec![("offset", TypeExpr::Int)], @@ -184,6 +191,7 @@ pub(crate) fn inject_builtin_interfaces( InterfaceDeclInfo { name: "SplObserver".to_string(), extends: Vec::new(), + properties: Vec::new(), methods: vec![builtin_interface_method_with_params( "update", vec![( @@ -202,6 +210,7 @@ pub(crate) fn inject_builtin_interfaces( InterfaceDeclInfo { name: "SplSubject".to_string(), extends: Vec::new(), + properties: Vec::new(), methods: vec![ builtin_interface_method_with_params( "attach", @@ -231,6 +240,7 @@ pub(crate) fn inject_builtin_interfaces( InterfaceDeclInfo { name: "Stringable".to_string(), extends: Vec::new(), + properties: Vec::new(), methods: vec![builtin_interface_method("__toString", TypeExpr::Str)], span: crate::span::Span::dummy(), constants: Vec::new(), @@ -268,6 +278,7 @@ fn marker_interface(name: &str) -> InterfaceDeclInfo { InterfaceDeclInfo { name: name.to_string(), extends: Vec::new(), + properties: Vec::new(), methods: Vec::new(), span: crate::span::Span::dummy(), constants: Vec::new(), diff --git a/src/types/checker/builtin_json.rs b/src/types/checker/builtin_json.rs index 707a88b8..75d022a9 100644 --- a/src/types/checker/builtin_json.rs +++ b/src/types/checker/builtin_json.rs @@ -50,6 +50,7 @@ pub(crate) fn inject_builtin_json_interfaces( InterfaceDeclInfo { name: "JsonSerializable".to_string(), extends: Vec::new(), + properties: Vec::new(), methods: vec![json_serialize_method()], span: crate::span::Span::dummy(), constants: Vec::new(), diff --git a/src/types/checker/builtin_types/declarations.rs b/src/types/checker/builtin_types/declarations.rs index 62fa7e08..4435590d 100644 --- a/src/types/checker/builtin_types/declarations.rs +++ b/src/types/checker/builtin_types/declarations.rs @@ -25,6 +25,7 @@ use super::fiber::builtin_fiber_methods; pub(crate) struct InterfaceDeclInfo { pub name: String, pub extends: Vec, + pub properties: Vec, pub methods: Vec, pub span: crate::span::Span, pub constants: Vec, @@ -35,6 +36,7 @@ impl Clone for InterfaceDeclInfo { InterfaceDeclInfo { name: self.name.clone(), extends: self.extends.clone(), + properties: self.properties.clone(), methods: self.methods.clone(), span: self.span, constants: self.constants.clone(), @@ -74,6 +76,7 @@ pub(crate) fn inject_builtin_throwables( InterfaceDeclInfo { name: "Throwable".to_string(), extends: Vec::new(), + properties: Vec::new(), methods: vec![builtin_throwable_get_message_method()], span: crate::span::Span::dummy(), constants: Vec::new(), diff --git a/src/types/checker/builtin_types/exception.rs b/src/types/checker/builtin_types/exception.rs index 70696945..6f911dea 100644 --- a/src/types/checker/builtin_types/exception.rs +++ b/src/types/checker/builtin_types/exception.rs @@ -11,7 +11,8 @@ use crate::names::php_symbol_key; use crate::parser::ast::{ - ClassMethod, ClassProperty, Expr, ExprKind, Stmt, StmtKind, TypeExpr, Visibility, + ClassMethod, ClassProperty, Expr, ExprKind, PropertyHooks, Stmt, StmtKind, TypeExpr, + Visibility, }; use crate::types::PhpType; @@ -22,6 +23,7 @@ pub(super) fn builtin_exception_message_property() -> ClassProperty { name: "message".to_string(), visibility: Visibility::Public, type_expr: Some(TypeExpr::Str), + hooks: PropertyHooks::none(), readonly: false, is_final: false, is_static: false, @@ -100,6 +102,7 @@ pub(super) fn builtin_exception_code_property() -> ClassProperty { name: "code".to_string(), visibility: Visibility::Protected, type_expr: Some(TypeExpr::Int), + hooks: PropertyHooks::none(), readonly: false, is_final: false, is_static: false, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 6b29d38d..e1ad3b77 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -112,6 +112,7 @@ fn builtin_property( name: name.to_string(), visibility, type_expr, + hooks: crate::parser::ast::PropertyHooks::none(), readonly: false, is_final: false, is_static: false, diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index 13f03a8e..5d05ff3b 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -78,6 +78,7 @@ pub(super) fn check_types_impl( if let StmtKind::InterfaceDecl { name, extends, + properties, methods, constants, } = &stmt.kind @@ -104,6 +105,7 @@ pub(super) fn check_types_impl( .iter() .map(|name| name.as_str().to_string()) .collect(), + properties: properties.clone(), methods: methods.clone(), span: stmt.span, constants: constants.clone(), diff --git a/src/types/checker/schema/classes/interfaces.rs b/src/types/checker/schema/classes/interfaces.rs index b13a5eea..62230aaf 100644 --- a/src/types/checker/schema/classes/interfaces.rs +++ b/src/types/checker/schema/classes/interfaces.rs @@ -13,8 +13,9 @@ use std::collections::{HashMap, HashSet}; use crate::errors::CompileError; use crate::names::php_symbol_key; use crate::parser::ast::Visibility; +use crate::span::Span; use crate::types::traits::FlattenedClass; -use crate::types::PhpType; +use crate::types::{PhpType, PropertyHookContract}; use super::super::super::Checker; use super::super::validation::{ @@ -93,6 +94,23 @@ pub(super) fn validate_interface_contracts( building, )?; } + for property_name in &interface_info.property_order { + let contract = interface_info + .properties + .get(property_name) + .expect("type checker bug: missing interface property contract"); + validate_interface_property( + state, + class, + &interface_name, + property_name, + contract, + class_map, + checker, + next_class_id, + building, + )?; + } } Ok(()) } @@ -136,8 +154,13 @@ pub(super) fn ensure_concrete_class_implements_abstracts( .get(prop_name) .cloned() .unwrap_or_else(|| class.name.clone()); + let span = state + .abstract_property_hooks + .get(prop_name) + .map(|contract| contract.span) + .unwrap_or_else(Span::dummy); return Err(CompileError::new( - crate::span::Span::dummy(), + span, &format!( "Concrete class {} must declare abstract property {}::${}", class.name, declaring_class, prop_name @@ -272,3 +295,246 @@ fn validate_interface_method( } Ok(()) } + +fn validate_interface_property( + state: &mut ClassBuildState, + class: &FlattenedClass, + interface_name: &str, + property_name: &str, + contract: &PropertyHookContract, + class_map: &HashMap, + checker: &mut Checker, + next_class_id: &mut u64, + building: &mut HashSet, +) -> Result<(), CompileError> { + if state + .static_property_declaring_classes + .contains_key(property_name) + { + return Err(CompileError::new( + class_property_span(class, property_name, contract.span), + &format!( + "Cannot use static property to satisfy interface contract: {}::${}", + class.name, property_name + ), + )); + } + if !state.property_declaring_classes.contains_key(property_name) { + if class.is_abstract { + defer_interface_property_contract(state, class, interface_name, property_name, contract); + return Ok(()); + } + return Err(CompileError::new( + contract.span, + &format!( + "Class {} must implement interface property {}::${}", + class.name, interface_name, property_name + ), + )); + } + + if state.abstract_properties.contains(property_name) { + if class.is_abstract { + state + .abstract_property_hooks + .entry(property_name.to_string()) + .or_insert_with(|| contract.clone()); + return Ok(()); + } + return Ok(()); + } + + if state.property_visibilities.get(property_name) != Some(&Visibility::Public) { + return Err(CompileError::new( + class_property_span(class, property_name, contract.span), + &format!( + "Interface property implementation must be public: {}::${}", + class.name, property_name + ), + )); + } + + let actual_ty = instance_property_type_for_contract(state, property_name); + ensure_object_type_known(&actual_ty, &class.name, class_map, checker, next_class_id, building)?; + if let Some(required_get) = contract.get_type.as_ref() { + ensure_object_type_known( + required_get, + &class.name, + class_map, + checker, + next_class_id, + building, + )?; + if !checker.type_accepts(required_get, &actual_ty) { + return Err(CompileError::new( + class_property_span(class, property_name, contract.span), + &format!( + "Type of {}::${} must be compatible with get property contract {} from interface {}", + class.name, property_name, required_get, interface_name + ), + )); + } + } + if let Some(required_set) = contract.set_type.as_ref() { + ensure_object_type_known( + required_set, + &class.name, + class_map, + checker, + next_class_id, + building, + )?; + if state.readonly_properties.contains(property_name) { + return Err(CompileError::new( + class_property_span(class, property_name, contract.span), + &format!( + "Readonly property {}::${} cannot satisfy set property contract from interface {}", + class.name, property_name, interface_name + ), + )); + } + if !checker.type_accepts(&actual_ty, required_set) { + return Err(CompileError::new( + class_property_span(class, property_name, contract.span), + &format!( + "Type of {}::${} must accept set property contract {} from interface {}", + class.name, property_name, required_set, interface_name + ), + )); + } + } + Ok(()) +} + +fn class_property_span( + class: &FlattenedClass, + property_name: &str, + fallback: Span, +) -> Span { + class + .properties + .iter() + .find(|property| property.name == property_name) + .map(|property| property.span) + .unwrap_or(fallback) +} + +fn ensure_object_type_known( + ty: &PhpType, + current_class: &str, + class_map: &HashMap, + checker: &mut Checker, + next_class_id: &mut u64, + building: &mut HashSet, +) -> Result<(), CompileError> { + match ty { + PhpType::Object(name) if name != current_class + && class_map.contains_key(name) + && !checker.classes.contains_key(name) => + { + super::build_class_info_recursive(name, class_map, checker, next_class_id, building)?; + } + PhpType::Union(members) => { + for member in members { + ensure_object_type_known( + member, + current_class, + class_map, + checker, + next_class_id, + building, + )?; + } + } + PhpType::Array(inner) => { + ensure_object_type_known( + inner, + current_class, + class_map, + checker, + next_class_id, + building, + )?; + } + PhpType::AssocArray { key, value } => { + ensure_object_type_known( + key, + current_class, + class_map, + checker, + next_class_id, + building, + )?; + ensure_object_type_known( + value, + current_class, + class_map, + checker, + next_class_id, + building, + )?; + } + _ => {} + } + Ok(()) +} + +fn defer_interface_property_contract( + state: &mut ClassBuildState, + class: &FlattenedClass, + interface_name: &str, + property_name: &str, + contract: &PropertyHookContract, +) { + if !state.property_declaring_classes.contains_key(property_name) { + let slot_index = state.prop_types.len(); + let ty = contract + .get_type + .as_ref() + .or(contract.set_type.as_ref()) + .cloned() + .unwrap_or(PhpType::Mixed); + state.prop_types.push((property_name.to_string(), ty)); + state + .property_offsets + .insert(property_name.to_string(), 8 + slot_index * 16); + state.defaults.push(None); + } + state + .property_declaring_classes + .insert(property_name.to_string(), interface_name.to_string()); + state + .property_visibilities + .insert(property_name.to_string(), Visibility::Public); + state.abstract_properties.insert(property_name.to_string()); + state + .abstract_property_hooks + .insert(property_name.to_string(), contract.clone()); + state + .property_attribute_names + .entry(property_name.to_string()) + .or_default(); + state + .property_attribute_args + .entry(property_name.to_string()) + .or_default(); + state + .property_declaring_classes + .entry(property_name.to_string()) + .or_insert_with(|| class.name.clone()); +} + +fn instance_property_type_for_contract( + state: &ClassBuildState, + property_name: &str, +) -> PhpType { + if !state.declared_properties.contains(property_name) { + return PhpType::Mixed; + } + state + .prop_types + .iter() + .find(|(name, _)| name == property_name) + .map(|(_, ty)| ty.clone()) + .unwrap_or(PhpType::Mixed) +} diff --git a/src/types/checker/schema/classes/properties.rs b/src/types/checker/schema/classes/properties.rs index eba8441f..e523f959 100644 --- a/src/types/checker/schema/classes/properties.rs +++ b/src/types/checker/schema/classes/properties.rs @@ -16,6 +16,7 @@ use crate::types::PhpType; use super::super::super::{infer_expr_type_syntactic, Checker}; use super::super::validation::visibility_rank; +use super::super::interfaces::{build_property_contract, merge_property_contract}; use super::state::{collect_attribute_args, collect_attribute_names, ClassBuildState}; pub(super) fn apply_properties( @@ -192,10 +193,13 @@ fn apply_instance_property( ), )); } - if prop.by_ref && class.is_readonly_class { + if prop.by_ref && (class.is_readonly_class || prop.readonly) { return Err(CompileError::new( prop.span, - "Readonly promoted by-reference properties are not supported", + &format!( + "Readonly promoted property cannot be by-reference: {}::${}", + class.name, prop.name + ), )); } if let Some(parent_declaring_class) = @@ -261,6 +265,10 @@ fn apply_instance_property( // when a concrete child overrides an inherited abstract slot. if prop.is_abstract { state.abstract_properties.insert(prop.name.clone()); + let contract = build_property_contract(checker, &class.name, prop)?; + state + .abstract_property_hooks + .insert(prop.name.clone(), contract); } Ok(()) } @@ -282,12 +290,13 @@ fn apply_instance_property_redeclaration( )); } let declared_ty = resolve_property_declared_type(checker, &class.name, prop)?; - validate_instance_property_override( - state, - class, - prop, - declared_ty.as_ref(), - parent_declaring_class, + validate_instance_property_override( + state, + class, + checker, + prop, + declared_ty.as_ref(), + parent_declaring_class, )?; let ty = if let Some(declared_ty) = declared_ty { @@ -322,8 +331,27 @@ fn apply_instance_property_redeclaration( } if prop.is_abstract { state.abstract_properties.insert(prop.name.clone()); + let mut contract = build_property_contract(checker, &class.name, prop)?; + if let Some(existing) = state.abstract_property_hooks.get(&prop.name) { + merge_property_contract( + &mut contract, + existing, + checker, + prop.span, + &class.name, + &prop.name, + "redeclaring abstract property", + )?; + } + state + .abstract_property_hooks + .insert(prop.name.clone(), contract); } else { state.abstract_properties.remove(&prop.name); + state.abstract_property_hooks.remove(&prop.name); + } + if prop.by_ref { + state.reference_properties.insert(prop.name.clone()); } Ok(()) } @@ -331,6 +359,7 @@ fn apply_instance_property_redeclaration( fn validate_instance_property_override( state: &ClassBuildState, class: &FlattenedClass, + checker: &Checker, prop: &ClassProperty, declared_ty: Option<&PhpType>, parent_declaring_class: &str, @@ -361,16 +390,28 @@ fn validate_instance_property_override( )); } - let parent_declared = state.declared_properties.contains(&prop.name); - validate_property_type_invariance( - parent_declared, - || inherited_instance_property_type(state, &prop.name), - declared_ty, - &class.name, - &prop.name, - parent_declaring_class, - prop.span, - )?; + let parent_abstract = state.abstract_properties.contains(&prop.name); + if parent_abstract { + validate_abstract_property_contract( + state, + checker, + class, + prop, + declared_ty, + parent_declaring_class, + )?; + } else { + let parent_declared = state.declared_properties.contains(&prop.name); + validate_property_type_invariance( + parent_declared, + || inherited_instance_property_type(state, &prop.name), + declared_ty, + &class.name, + &prop.name, + parent_declaring_class, + prop.span, + )?; + } let parent_readonly = state.readonly_properties.contains(&prop.name); let child_readonly = class.is_readonly_class || prop.readonly; @@ -395,7 +436,6 @@ fn validate_instance_property_override( )); } - let parent_abstract = state.abstract_properties.contains(&prop.name); if !parent_abstract && prop.is_abstract { return Err(CompileError::new( prop.span, @@ -409,6 +449,66 @@ fn validate_instance_property_override( Ok(()) } +fn validate_abstract_property_contract( + state: &ClassBuildState, + checker: &Checker, + class: &FlattenedClass, + prop: &ClassProperty, + declared_ty: Option<&PhpType>, + parent_declaring_class: &str, +) -> Result<(), CompileError> { + let Some(contract) = state.abstract_property_hooks.get(&prop.name) else { + return Ok(()); + }; + if prop.is_abstract { + let mut child_contract = build_property_contract(checker, &class.name, prop)?; + merge_property_contract( + &mut child_contract, + contract, + checker, + prop.span, + &class.name, + &prop.name, + "redeclaring abstract property", + )?; + return Ok(()); + } + + let actual_ty = declared_ty.cloned().unwrap_or(PhpType::Mixed); + if let Some(required_get) = contract.get_type.as_ref() { + if !checker.type_accepts(required_get, &actual_ty) { + return Err(CompileError::new( + prop.span, + &format!( + "Type of {}::${} must be compatible with get property contract {} from {}", + class.name, prop.name, required_get, parent_declaring_class + ), + )); + } + } + if let Some(required_set) = contract.set_type.as_ref() { + if class.is_readonly_class || prop.readonly { + return Err(CompileError::new( + prop.span, + &format!( + "Readonly property {}::${} cannot satisfy set property contract from {}", + class.name, prop.name, parent_declaring_class + ), + )); + } + if !checker.type_accepts(&actual_ty, required_set) { + return Err(CompileError::new( + prop.span, + &format!( + "Type of {}::${} must accept set property contract {} from {}", + class.name, prop.name, required_set, parent_declaring_class + ), + )); + } + } + Ok(()) +} + fn inherited_static_property_type(state: &ClassBuildState, property: &str) -> PhpType { state .static_prop_types diff --git a/src/types/checker/schema/classes/state.rs b/src/types/checker/schema/classes/state.rs index 707f7ea8..cc578113 100644 --- a/src/types/checker/schema/classes/state.rs +++ b/src/types/checker/schema/classes/state.rs @@ -13,7 +13,7 @@ use std::collections::{HashMap, HashSet}; use crate::errors::CompileError; use crate::parser::ast::{Expr, Visibility}; use crate::types::traits::FlattenedClass; -use crate::types::{ClassInfo, FunctionSig, PhpType}; +use crate::types::{ClassInfo, FunctionSig, PhpType, PropertyHookContract}; #[derive(Default)] pub(super) struct ClassBuildState { @@ -28,6 +28,7 @@ pub(super) struct ClassBuildState { pub(super) readonly_properties: HashSet, pub(super) reference_properties: HashSet, pub(super) abstract_properties: HashSet, + pub(super) abstract_property_hooks: HashMap, pub(super) static_prop_types: Vec<(String, PhpType)>, pub(super) static_defaults: Vec>, pub(super) static_property_declaring_classes: HashMap, @@ -107,6 +108,7 @@ impl ClassBuildState { readonly_properties: self.readonly_properties, reference_properties: self.reference_properties, abstract_properties: self.abstract_properties, + abstract_property_hooks: self.abstract_property_hooks, static_properties: self.static_prop_types, static_defaults: self.static_defaults, static_property_declaring_classes: self.static_property_declaring_classes, @@ -255,6 +257,10 @@ impl ClassBuildState { if parent.abstract_properties.contains(name) { self.abstract_properties.insert(name.clone()); } + if let Some(contract) = parent.abstract_property_hooks.get(name) { + self.abstract_property_hooks + .insert(name.clone(), contract.clone()); + } } } diff --git a/src/types/checker/schema/enums.rs b/src/types/checker/schema/enums.rs index 0fbce973..b59a8f5d 100644 --- a/src/types/checker/schema/enums.rs +++ b/src/types/checker/schema/enums.rs @@ -283,6 +283,7 @@ pub(crate) fn build_enum_info( readonly_properties, reference_properties, abstract_properties: HashSet::new(), + abstract_property_hooks: HashMap::new(), static_properties: Vec::new(), static_defaults: Vec::new(), static_property_declaring_classes: HashMap::new(), diff --git a/src/types/checker/schema/interfaces.rs b/src/types/checker/schema/interfaces.rs index 645e933c..cdcbe92b 100644 --- a/src/types/checker/schema/interfaces.rs +++ b/src/types/checker/schema/interfaces.rs @@ -12,8 +12,8 @@ use std::collections::{HashMap, HashSet}; use crate::errors::CompileError; use crate::names::php_symbol_key; -use crate::parser::ast::Visibility; -use crate::types::InterfaceInfo; +use crate::parser::ast::{ClassProperty, Visibility}; +use crate::types::{InterfaceInfo, PhpType, PropertyHookContract}; use super::super::Checker; use super::super::InterfaceDeclInfo; @@ -56,6 +56,8 @@ pub(crate) fn build_interface_info_recursive( let mut method_declaring_interfaces = HashMap::new(); let mut method_order = Vec::new(); let mut method_slots = HashMap::new(); + let mut properties = HashMap::new(); + let mut property_order = Vec::new(); for parent_name in &interface.extends { if class_map.contains_key(parent_name) { @@ -113,6 +115,55 @@ pub(crate) fn build_interface_info_recursive( method_slots.insert(method_name.clone(), slot); method_order.push(method_name.clone()); } + for property_name in &parent_info.property_order { + let parent_contract = parent_info + .properties + .get(property_name) + .expect("type checker bug: missing interface parent property contract"); + if let Some(existing_contract) = properties.get_mut(property_name) { + merge_property_contract( + existing_contract, + parent_contract, + checker, + interface.span, + &interface.name, + property_name, + "combining interface parent", + )?; + continue; + } + properties.insert(property_name.clone(), parent_contract.clone()); + property_order.push(property_name.clone()); + } + } + + let mut direct_property_names = HashSet::new(); + for property in &interface.properties { + if !direct_property_names.insert(property.name.clone()) { + return Err(CompileError::new( + property.span, + &format!( + "Duplicate interface property declaration: {}::${}", + interface.name, property.name + ), + )); + } + validate_interface_property_syntax(&interface.name, property)?; + let contract = build_property_contract(checker, &interface.name, property)?; + if let Some(existing_contract) = properties.get_mut(&property.name) { + merge_property_contract( + existing_contract, + &contract, + checker, + property.span, + &interface.name, + &property.name, + "redeclaring interface", + )?; + } else { + properties.insert(property.name.clone(), contract); + property_order.push(property.name.clone()); + } } let mut direct_method_keys = HashSet::new(); @@ -203,6 +254,8 @@ pub(crate) fn build_interface_info_recursive( InterfaceInfo { interface_id: *next_interface_id, parents: interface.extends.clone(), + properties, + property_order, methods, method_declaring_interfaces, method_order, @@ -214,3 +267,131 @@ pub(crate) fn build_interface_info_recursive( building.remove(interface_name); Ok(()) } + +fn validate_interface_property_syntax( + interface_name: &str, + property: &ClassProperty, +) -> Result<(), CompileError> { + if property.visibility != Visibility::Public { + return Err(CompileError::new( + property.span, + &format!( + "Interface properties must be public: {}::${}", + interface_name, property.name + ), + )); + } + if property.is_static { + return Err(CompileError::new( + property.span, + &format!( + "Interface property {}::${} cannot be static", + interface_name, property.name + ), + )); + } + if property.readonly { + return Err(CompileError::new( + property.span, + &format!( + "Hooked properties cannot be readonly: {}::${}", + interface_name, property.name + ), + )); + } + if !property.hooks.any() { + return Err(CompileError::new( + property.span, + &format!( + "Interfaces may only include hooked properties: {}::${}", + interface_name, property.name + ), + )); + } + Ok(()) +} + +pub(crate) fn build_property_contract( + checker: &Checker, + declaring_type: &str, + property: &ClassProperty, +) -> Result { + let property_ty = match property.type_expr.as_ref() { + Some(type_expr) => checker.resolve_declared_property_type_hint( + type_expr, + property.span, + &format!("Property {}::${}", declaring_type, property.name), + )?, + None => PhpType::Mixed, + }; + Ok(PropertyHookContract { + get_type: property + .hooks + .requires_get() + .then(|| property_ty.clone()), + set_type: property.hooks.set.then_some(property_ty), + get_by_ref: property.hooks.get_by_ref, + declaring_type: declaring_type.to_string(), + span: property.span, + }) +} + +pub(crate) fn merge_property_contract( + existing: &mut PropertyHookContract, + incoming: &PropertyHookContract, + checker: &Checker, + span: crate::span::Span, + owner_name: &str, + property_name: &str, + context: &str, +) -> Result<(), CompileError> { + if let Some(incoming_get) = incoming.get_type.as_ref() { + match existing.get_type.as_ref() { + Some(existing_get) if checker.type_accepts(existing_get, incoming_get) => { + existing.get_type = Some(incoming_get.clone()); + existing.get_by_ref |= incoming.get_by_ref; + existing.span = incoming.span; + } + Some(existing_get) if checker.type_accepts(incoming_get, existing_get) => { + existing.get_by_ref |= incoming.get_by_ref; + } + Some(existing_get) => { + return Err(CompileError::new( + span, + &format!( + "Incompatible get property contract when {}: {}::${} requires {}, conflicting with {}", + context, owner_name, property_name, incoming_get, existing_get + ), + )) + } + None => { + existing.get_type = Some(incoming_get.clone()); + existing.get_by_ref = incoming.get_by_ref; + existing.span = incoming.span; + } + } + } + if let Some(incoming_set) = incoming.set_type.as_ref() { + match existing.set_type.as_ref() { + Some(existing_set) if checker.type_accepts(existing_set, incoming_set) => {} + Some(existing_set) if checker.type_accepts(incoming_set, existing_set) => { + existing.set_type = Some(incoming_set.clone()); + existing.span = incoming.span; + } + Some(existing_set) => { + return Err(CompileError::new( + span, + &format!( + "Incompatible set property contract when {}: {}::${} requires {}, conflicting with {}", + context, owner_name, property_name, incoming_set, existing_set + ), + )) + } + None => { + existing.set_type = Some(incoming_set.clone()); + existing.span = incoming.span; + } + } + } + Ok(()) +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 320ba492..b6dcf014 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -31,6 +31,7 @@ pub use result::{check_with_target, CheckResult}; pub use schema::{ AttrArgValue, ClassInfo, EnumCaseInfo, EnumCaseValue, EnumInfo, ExternClassInfo, ExternFieldInfo, ExternFunctionSig, InterfaceInfo, PackedClassInfo, PackedFieldInfo, + PropertyHookContract, }; pub(crate) use signatures::{ builtin_call_sig, callable_wrapper_sig, first_class_callable_builtin_sig, diff --git a/src/types/schema.rs b/src/types/schema.rs index c8bb008f..2b9ac872 100644 --- a/src/types/schema.rs +++ b/src/types/schema.rs @@ -12,6 +12,7 @@ use std::collections::{HashMap, HashSet}; use crate::parser::ast::{ClassMethod, Expr, Visibility}; +use crate::span::Span; use super::{FunctionSig, PhpType}; @@ -26,10 +27,30 @@ pub enum AttrArgValue { Str(String), } +#[derive(Debug, Clone)] +pub struct PropertyHookContract { + pub get_type: Option, + pub set_type: Option, + pub get_by_ref: bool, + pub declaring_type: String, + pub span: Span, +} + +impl PartialEq for PropertyHookContract { + fn eq(&self, other: &Self) -> bool { + self.get_type == other.get_type + && self.set_type == other.set_type + && self.get_by_ref == other.get_by_ref + && self.declaring_type == other.declaring_type + } +} + #[derive(Debug, Clone)] pub struct InterfaceInfo { pub interface_id: u64, pub parents: Vec, + pub properties: HashMap, + pub property_order: Vec, pub methods: HashMap, pub method_declaring_interfaces: HashMap, pub method_order: Vec, @@ -85,6 +106,7 @@ pub struct ClassInfo { pub readonly_properties: HashSet, pub reference_properties: HashSet, pub abstract_properties: HashSet, + pub abstract_property_hooks: HashMap, pub static_properties: Vec<(String, PhpType)>, pub static_defaults: Vec>, pub static_property_declaring_classes: HashMap, diff --git a/src/types/traits/merge.rs b/src/types/traits/merge.rs index e697fb58..8979f405 100644 --- a/src/types/traits/merge.rs +++ b/src/types/traits/merge.rs @@ -49,6 +49,15 @@ pub(super) fn merge_property_into( .position(|existing| existing.name == property.name) { let existing = &merged[index]; + if existing.hooks.any() || property.hooks.any() { + return Err(CompileError::new( + span, + &format!( + "{} has incompatible duplicate hooked property '{}'", + owner_label, property.name + ), + )); + } if properties_compatible(existing, &property) { if replace_compatible_existing { merged[index] = property; @@ -129,6 +138,7 @@ pub(super) fn merge_imported_method_set( fn properties_compatible(left: &ClassProperty, right: &ClassProperty) -> bool { left.visibility == right.visibility && left.type_expr == right.type_expr + && left.hooks == right.hooks && left.readonly == right.readonly && left.is_static == right.is_static && left.is_abstract == right.is_abstract diff --git a/tests/codegen/objects/constructor_promotion.rs b/tests/codegen/objects/constructor_promotion.rs index 4694d9c1..738a2496 100644 --- a/tests/codegen/objects/constructor_promotion.rs +++ b/tests/codegen/objects/constructor_promotion.rs @@ -88,3 +88,39 @@ echo $name; ); assert_eq!(out, "Grace"); } + +#[test] +fn test_constructor_promoted_by_ref_property_uses_default_reference_cell() { + let out = compile_and_run( + r#"value; +$box->value = 4; +echo ":"; +echo $box->value; +"#, + ); + assert_eq!(out, "1:4"); +} + +#[test] +fn test_constructor_promoted_by_ref_property_with_default_still_links_variable_arg() { + let out = compile_and_run( + r#"value = 7; +echo $value; +$value = 9; +echo ":"; +echo $box->value; +"#, + ); + assert_eq!(out, "7:9"); +} diff --git a/tests/codegen/oop/abstract_properties.rs b/tests/codegen/oop/abstract_properties.rs index c2f6e38e..96efa565 100644 --- a/tests/codegen/oop/abstract_properties.rs +++ b/tests/codegen/oop/abstract_properties.rs @@ -15,7 +15,7 @@ fn test_abstract_property_concrete_child_declares_default() { let out = compile_and_run( r#"value; @@ -169,7 +169,7 @@ fn test_abstract_readonly_property_concretized() { let out = compile_and_run( r#"id; +"#, + ); + assert_eq!(out, "42"); +} + +#[test] +fn test_interface_set_property_contract_allows_contravariant_type() { + let out = compile_and_run( + r#"pet = new Dog(); +echo $kennel->pet instanceof Animal; +"#, + ); + assert_eq!(out, "1"); +} + +#[test] +fn test_abstract_class_can_defer_interface_property_to_child() { + let out = compile_and_run( + r#"name; +"#, + ); + assert_eq!(out, "widget"); +} diff --git a/tests/codegen/oop/traits.rs b/tests/codegen/oop/traits.rs index 2dda2648..bdb4eb2e 100644 --- a/tests/codegen/oop/traits.rs +++ b/tests/codegen/oop/traits.rs @@ -150,3 +150,26 @@ echo $demo->reveal(); ); assert_eq!(out, "hello"); } + +#[test] +fn test_abstract_trait_property_can_be_satisfied_by_concrete_child() { + let out = compile_and_run( + r#"value; +"#, + ); + assert_eq!(out, "9"); +} diff --git a/tests/error_tests/misc/classes.rs b/tests/error_tests/misc/classes.rs index c0d43b7d..63e5bd9c 100644 --- a/tests/error_tests/misc/classes.rs +++ b/tests/error_tests/misc/classes.rs @@ -147,26 +147,26 @@ fn test_error_constructor_promotion_rejects_variadic() { } #[test] -fn test_error_constructor_promotion_rejects_readonly_by_reference() { +fn test_error_constructor_promotion_by_reference_requires_variable_arg() { expect_error( - "value = $value; } }", + "Readonly property Box::$value cannot satisfy set property contract", + ); +} + +#[test] +fn test_error_interface_property_missing_uses_contract_span() { + let err = check_source_full( + r#" 0); +} + +#[test] +fn test_error_interface_property_type_mismatch_uses_implementation_span() { + let err = check_source_full( + r#" 0); +} + +#[test] +fn test_error_deferred_interface_property_uses_contract_span() { + let err = check_source_full( + r#" 0); } #[test] fn test_error_abstract_property_with_default() { expect_error( - " { + assert_eq!(name, "HasName"); + assert_eq!(properties.len(), 1); + assert_eq!(properties[0].name, "name"); + assert!(properties[0].is_abstract); + assert!(properties[0].hooks.get); + assert!(properties[0].hooks.set); + } + other => panic!("Expected InterfaceDecl, got {:?}", other), + } +} + #[test] fn test_parse_new_self() { let stmts = parse_source(" panic!("Expected ClassDecl with promoted properties, got {:?}", other), } } + +#[test] +fn test_parse_abstract_property_hook_contract() { + let stmts = parse_source( + " { + assert_eq!(properties.len(), 1); + assert_eq!(properties[0].name, "value"); + assert!(properties[0].is_abstract); + assert!(properties[0].hooks.get); + assert!(properties[0].hooks.set); + } + other => panic!("Expected ClassDecl, got {:?}", other), + } +} + +#[test] +fn test_parse_trait_abstract_property_hook_contract() { + let stmts = parse_source( + " { + assert_eq!(properties.len(), 1); + assert_eq!(properties[0].name, "value"); + assert!(properties[0].is_abstract); + assert!(properties[0].hooks.get_by_ref); + } + other => panic!("Expected TraitDecl, got {:?}", other), + } +} From 04d78003ef78c4b7627551ec5a6374cfe534edbe Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 16 May 2026 23:06:55 +0200 Subject: [PATCH 2/3] docs: sync release documentation --- ROADMAP.md | 2 +- docs/internals/architecture.md | 1 + docs/internals/memory-model.md | 4 ++-- docs/internals/the-runtime.md | 38 ++++++++++++++++++++++++++++++++-- docs/php/operators.md | 5 ++--- 5 files changed, 42 insertions(+), 8 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9e5e80da..07fe9c70 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -449,7 +449,7 @@ runtime helpers, and standard-library surfaces. - [x] JSON pretty-print optimization — inline indent emission inside each container encoder (assoc, array_int/str/dynamic, object) and retire the `__rt_json_pretty_apply` post-processor. Eliminates the second buffer walk for JSON_PRETTY_PRINT workloads. Multi-day refactor completed with a `_json_indent_depth` BSS slot, balanced normal-path formatting depth maintenance, reset-at-entry protection across throws, and bytewise PHP cross-check coverage on representative payloads. - [x] `is_callable()` runtime fallback — handle non-literal strings, `[$obj, "method"]` arrays, and objects implementing `__invoke`. The string-literal + Callable-typed compile-time path is already in place. - [x] Case-insensitive user-function lookup — `function_exists("USER_FN")` and `is_callable("USER_FN")` accept any case for user functions through a shared lookup path, matching PHP's function-name rules. -- [x] OOP property parity v2 — PHP 8.4 property-hook contracts now cover interface properties and abstract properties in traits/classes; `readonly static` remains rejected like PHP, instance property redeclaration validates hook get/set contracts, and by-reference constructor promotion now covers `readonly` runtime fatals plus default-value reference cells. +- [x] OOP property parity v2 — PHP 8.4 property-hook contracts now cover interface properties and abstract properties in traits/classes; `readonly static` remains rejected like PHP, instance property redeclaration validates hook get/set contracts, and by-reference constructor promotion now rejects readonly aliases at compile time while supporting default-value reference cells. ### Standard PHP Library (SPL) diff --git a/docs/internals/architecture.md b/docs/internals/architecture.md index cb0da47e..8610fa40 100644 --- a/docs/internals/architecture.md +++ b/docs/internals/architecture.md @@ -307,6 +307,7 @@ src/ │ ├── x86_minimal.rs Minimal x86_64 runtime slice for the Linux x86_64 target │ ├── strings/ itoa, concat, resource display, ftoa, sprintf, md5, sha1, str_persist, ... (58 files) │ ├── arrays/ heap_alloc, heap_free, array_free_deep, array_grow, hash_grow, hash_*, mixed boxing/freeing, mixed instanceof, sort, usort, refcount, gc/decref dispatch, ... (119 files) +│ ├── callables/ Runtime `is_callable()` fallback for dynamic strings, arrays, hashes, objects, and Mixed values (2 files) │ ├── io/ fopen, fgets, fread, stat, scandir, ... (30 files) │ ├── buffers/ buffer_new, buffer_len, bounds_fail, use_after_free helpers (5 files incl. mod.rs) │ ├── exceptions.rs Exception runtime module root / re-exports diff --git a/docs/internals/memory-model.md b/docs/internals/memory-model.md index 5dee5862..7364b6c4 100644 --- a/docs/internals/memory-model.md +++ b/docs/internals/memory-model.md @@ -25,7 +25,7 @@ This page explains where every value lives in memory at runtime. │ String buffer │ _concat_buf: 64KB, scratch pad │ (temporary string results) │ Reset at each statement ├─────────────────────────────┤ -│ I/O buffers │ _cstr_buf: 4KB × 2, _eof_flags: 256B +│ I/O buffers │ _cstr_buf/_cstr_buf2: 4KB each, _eof_flags: 256B │ (C-string conversion, EOF) │ ├─────────────────────────────┤ │ Runtime metadata (BSS) │ _concat_off, _global_argc/_argv, @@ -535,7 +535,7 @@ The naming pattern comes from `static_property_symbol(...)`. Inherited static pr | Static vars | 24 bytes per `static $var` (`16 + 8 init flag`) | Grows with number of declared static locals | | Static properties | 16 bytes per effective declaring class static property | Grows with number of declared and redeclared static properties | | Array capacity | Fixed at creation until grow/re-hash logic runs | Fatal error: "array capacity exceeded" if a hard limit is hit | -| C-string buffers | 4KB each (×2) | Long converted paths/strings are truncated to buffer size | +| C-string buffers | `_cstr_buf`, `_cstr_buf2` = 4KB each | Long converted paths/strings are truncated to buffer size | | EOF flags | 256 bytes | Max 256 simultaneous file descriptors | | Data section | No fixed limit | Grows with number of unique literals | diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index d1449154..78ed64d6 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -5,7 +5,7 @@ sidebar: order: 8 --- -**Source:** `src/codegen/runtime/` — `mod.rs`, `emitters.rs`, `data/`, `x86_minimal.rs`, `strings/`, `arrays/`, `buffers/`, `exceptions.rs`, `exceptions/`, `io/`, `objects/`, `system/`, `pointers/`, `fibers/`, `generators/` +**Source:** `src/codegen/runtime/` — `mod.rs`, `emitters.rs`, `data/`, `x86_minimal.rs`, `strings/`, `arrays/`, `buffers/`, `callables/`, `exceptions.rs`, `exceptions/`, `io/`, `objects/`, `system/`, `pointers/`, `fibers/`, `generators/` The runtime is a collection of **hand-written assembly routines** that handle operations too complex for inline code generation. When the [code generator](the-codegen.md) needs to convert an integer to a string or concatenate two strings, it emits a `bl __rt_itoa` or `bl __rt_concat` — a call to a runtime routine. @@ -124,6 +124,8 @@ Each routine follows the same pattern — inputs in registers, output in standar | Routine | What it does | Input | Output | |---|---|---|---| | `__rt_strcopy` | Copy string into concat buffer | `x1`/`x2` | `x1`/`x2` | +| `__rt_str_to_number` | Parse a PHP numeric string for loose comparison and numeric-string casts | `x1`/`x2` | numeric payload + success flag | +| `__rt_str_loose_eq` | Compare two strings using PHP loose-comparison numeric-string rules before falling back to bytes | two strings | `x0` (0 or 1) | | `__rt_strtolower` | Lowercase conversion | `x1`/`x2` | `x1`/`x2` | | `__rt_strtoupper` | Uppercase conversion | `x1`/`x2` | `x1`/`x2` | | `__rt_trim` | Strip whitespace (no args) or chars in mask | `x1`/`x2` | `x1`/`x2` | @@ -168,9 +170,26 @@ Each routine follows the same pattern — inputs in registers, output in standar | `__rt_hash` | Hash with algorithm | algo + data | `x1`/`x2` | | `__rt_sscanf` | Parse string with format | str + format | `x0` (array ptr) | +## Callable routines + +**Source:** `src/codegen/runtime/callables/` (2 files) + +These routines implement the runtime fallback path for `is_callable()` when the argument is not a compile-time literal or statically known callable value. They consult generated metadata for builtins, user functions, public methods, public static methods, and `__invoke` objects. + +| Routine | What it does | Input | Output | +|---|---|---|---| +| `__rt_is_callable_string` | Resolve a string as a builtin, active user function, or `Class::method` static-method callable | `x1`/`x2` = string | `x0` = bool | +| `__rt_is_callable_method_name` | Check whether an object exposes a public method with the supplied name | object pointer + method string | `x0` = bool | +| `__rt_is_callable_static_method_name` | Check whether a class string exposes a public static method with the supplied name | class string + method string | `x0` = bool | +| `__rt_is_callable_object` | Check object callability through public `__invoke` metadata | object pointer | `x0` = bool | +| `__rt_is_callable_array` | Validate indexed callable arrays such as `[$obj, "method"]` or `[ClassName::class, "method"]` | array pointer | `x0` = bool | +| `__rt_is_callable_assoc` | Validate associative callable-array payloads produced through boxed or dynamic data paths | hash pointer | `x0` = bool | +| `__rt_is_callable_mixed` | Unbox a Mixed value and dispatch string, array, hash, or object callable checks | mixed pointer | `x0` = bool | +| `__rt_is_callable_heap` | Dispatch callable checks from a raw heap pointer by inspecting its heap-kind tag | heap pointer | `x0` = bool | + ## Array routines -**Source:** `src/codegen/runtime/arrays/` (118 files) +**Source:** `src/codegen/runtime/arrays/` (119 files) ### Core allocation @@ -186,6 +205,7 @@ Each routine follows the same pattern — inputs in registers, output in standar | `__rt_heap_kind` | Return the uniform heap-kind tag for a heap-backed pointer | `x0` = pointer | `x0` = kind | | `__rt_array_new` | Create indexed array with header | `x0` = capacity, `x1` = elem_size | `x0` = array ptr | | `__rt_array_clone_shallow` | Clone indexed array storage for copy-on-write splitting, retaining nested heap children as needed | `x0` = array | `x0` = new array | +| `__rt_array_to_mixed` | Convert an indexed array's live slots to boxed Mixed cells and stamp the array metadata as mixed | `x0` = array | `x0` = same array | | `__rt_array_ensure_unique` | Split a shared indexed array before mutation | `x0` = array | `x0` = unique array | | `__rt_array_grow` | Ensure uniqueness, double array capacity, copy elements, free old unique storage | `x0` = array | `x0` = new array | | `__rt_array_free_deep` | Free array storage and release nested heap-backed elements | `x0` = array | — | @@ -263,6 +283,7 @@ See [Memory Model](memory-model.md) for the hash table memory layout. | `__rt_array_column` | Extract column from array of assoc arrays (int values) | | `__rt_array_column_ref` | Extract column of retained heap-backed values (arrays / hashes / objects) | | `__rt_array_column_str` | Extract column from array of assoc arrays (string values) | +| `__rt_array_column_mixed` | Extract column values as boxed Mixed cells for heterogeneous input payloads | | `__rt_range` | Generate integer range array | | `__rt_shuffle` / `__rt_array_rand` | Randomize order / pick random | | `__rt_random_u32` / `__rt_random_uniform` | Target-aware random primitives used by `rand()`, `random_int()`, `shuffle()`, and `array_rand()` | @@ -366,6 +387,8 @@ The `json_encode` implementation uses **type-aware dispatch** — the codegen ca | `__rt_json_encode_stdclass` | Encode the dynamic-property hash backing `stdClass`, preserving `{}` for empty instances | `x0` = stdClass hash ptr | `x1`/`x2` = JSON string | | `__rt_json_decode` | String-only compatibility helper used by string decode paths; trims outer whitespace and unescapes quoted JSON strings including surrogate-aware `\uXXXX` sequences | `x1`/`x2` = JSON string | `x1`/`x2` = decoded string | | `__rt_json_decode_mixed` | Checked structural recursive decoder that returns boxed `Mixed` cells for null, bool, int, float, string, indexed arrays, associative arrays, and stdClass objects depending on `_json_decode_assoc`; records syntax/depth/UTF-16 errors and returns 0 on malformed input | `x1`/`x2` = JSON string | `x0` = Mixed* or 0 | +| `__rt_json_decode_mixed_array_real` | Recursive array parser used by `json_decode_mixed` once the outer `[` token is known | parser cursor + JSON bounds | boxed Mixed array | +| `__rt_json_decode_mixed_object_real` | Recursive object parser used by `json_decode_mixed` once the outer `{` token is known; returns assoc hash or stdClass payload based on decode mode | parser cursor + JSON bounds | boxed Mixed object/hash | | `__rt_json_skip_ws` | Shared RFC 8259 whitespace skipper used by `json_decode_mixed` and its recursive array/object parsers; advances a caller-owned cursor to the next token or caller-supplied limit | JSON slice pointer, exclusive limit, cursor | updated cursor | | `__rt_json_validate` | Standalone RFC 8259 validator used by `json_validate()`; scalar validator helpers are also reused by `json_decode_mixed` for strings and numbers | `x1`/`x2` = JSON string | `x0` = 1 valid / 0 invalid | | `__rt_json_depth_enter` / `__rt_json_depth_exit` | Maintain `_json_active_depth` and compare against `_json_depth_limit` for recursive encode/decode/validate walks | global JSON state | status / updated state | @@ -399,6 +422,10 @@ These routines handle file and filesystem operations through target-aware libc/s | `__rt_fgets` | Read line from file descriptor | | `__rt_feof` | Check end-of-file flag for a file descriptor | | `__rt_fread` | Read N bytes from file descriptor | +| `__rt_readfile` | Open a path, stream contents to stdout, and return copied byte count, `-1` on read failure, or a false sentinel on open failure | +| `__rt_fpassthru` | Stream the remaining bytes from an existing descriptor to stdout and return copied byte count or `-1` on read failure | +| `__rt_flock` | Call libc `flock()`, translating PHP's `LOCK_UN` constant and exposing would-block state for the optional output parameter | +| `__rt_tmpfile` | Create an anonymous temporary file descriptor through `mkstemp()` plus immediate unlink | | `__rt_file_get_contents` | Read entire file into string, or return a null pointer after emitting a suppressible warning on failure | | `__rt_file_put_contents` | Write string to file (create/truncate) | | `__rt_file` | Read file into array of lines | @@ -411,6 +438,9 @@ These routines handle file and filesystem operations through target-aware libc/s | `__rt_stat_array` / `__rt_lstat_array` / `__rt_fstat_array` | Build PHP-compatible stat arrays with numeric and string keys, returning a null pointer for codegen to box as `false` on failure | | `__rt_unlink` / `__rt_mkdir` / `__rt_rmdir` / `__rt_chdir` | Filesystem path operations via libc/syscalls | | `__rt_rename` / `__rt_copy` | Two-path filesystem helpers using dual C-string scratch buffers | +| `__rt_symlink` / `__rt_link` | Create symbolic or hard links through libc | +| `__rt_readlink` | Read a symbolic-link target into a heap-backed string, with null output for PHP `false` on failure | +| `__rt_linkinfo` | Return `lstat()` device metadata for a link path, or PHP's `-1` failure sentinel | | `__rt_getcwd` | Get current working directory | | `__rt_scandir` | List directory contents into array | | `__rt_glob` | Pattern-match filenames | @@ -501,8 +531,11 @@ These helpers back the built-in `Generator` class. Generator functions emit a he | `__rt_gen_key` | Return an owned ref to the boxed Mixed key from the most recent yield | `GeneratorFrame*` | boxed `mixed` key | | `__rt_gen_valid` | Report whether the generator is not terminated | `GeneratorFrame*` | bool | | `__rt_gen_next` | Resume the state machine past the current yield unless terminated | `GeneratorFrame*` | — | +| `__rt_gen_next_done` | Shared global return label used after `next()` skips or completes a resume | `GeneratorFrame*` | — | | `__rt_gen_send` | Store a boxed Mixed sent value, then resume the state machine | `GeneratorFrame*`, boxed `mixed` value | boxed `mixed` payload | +| `__rt_gen_send_done` | Shared global return label used after `send()` skips or completes a resume | `GeneratorFrame*` | boxed `mixed` payload | | `__rt_gen_rewind` | Run the generator to its first yield once | `GeneratorFrame*` | — | +| `__rt_gen_rewind_done` | Shared global return label used when `rewind()` has already run or just finished | `GeneratorFrame*` | — | | `__rt_gen_throw` | Mark the generator terminated and throw through the normal exception runtime | `GeneratorFrame*`, throwable object | does not return | | `__rt_gen_get_return` | Return an owned ref to the boxed terminal return value | `GeneratorFrame*` | boxed `mixed` payload | @@ -541,6 +574,7 @@ pub fn emit_runtime(emitter: &mut Emitter) { // diagnostics: runtime warning emission and @ suppression state // strings: itoa, resource display/stdout, ftoa, concat, atoi, equality, formatting, trim/mask, // search/replace, explode/implode, hashing, encoding, sscanf, ... + // callables: dynamic is_callable() fallback for strings, arrays, hashes, objects, and Mixed // system: argv, time, getenv, shell, date/mktime/strtotime, JSON, regex // exceptions: cleanup walk, catch matching, throw/rethrow helpers // arrays: heap alloc/free, array/hash helpers, sort, callbacks, refcount diff --git a/docs/php/operators.md b/docs/php/operators.md index a1c60de7..d514642b 100644 --- a/docs/php/operators.md +++ b/docs/php/operators.md @@ -262,11 +262,10 @@ The right-hand side may be any expression that evaluates to a callable: The callable must accept the piped value as its first parameter; remaining parameters must be optional or variadic. By-reference parameters are not supported on the pipe target. -First-class callable creation requires a simple receiver: `$obj->method(...)` and `$this->method(...)` are accepted, but receivers with intermediate property accesses or call results (`$obj->inner->method(...)`, `(getThing())->method(...)`) are not yet captured by the optimizer. Use a temporary variable in those cases: +First-class callable creation accepts local receivers such as `$obj->method(...)` and `$this->method(...)`, plus non-local receiver expressions such as `(new Greeter())->greet(...)` or `(getThing())->method(...)`. The receiver expression is evaluated when the callable is created, then captured in the generated wrapper. ```php -$inner = $obj->inner; -$cb = $inner->method(...); +$cb = (new Greeter())->greet(...); $value |> $cb; ``` From cf116cd89fb066b2d40431393a5b37695e2e8b7c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 16 May 2026 23:21:21 +0200 Subject: [PATCH 3/3] fix: avoid clobbering by-ref scalar slots --- .../stmt/assignments/properties/storage.rs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/codegen/stmt/assignments/properties/storage.rs b/src/codegen/stmt/assignments/properties/storage.rs index baa2d68c..e193e1f1 100644 --- a/src/codegen/stmt/assignments/properties/storage.rs +++ b/src/codegen/stmt/assignments/properties/storage.rs @@ -149,6 +149,8 @@ pub(super) fn store_referenced_value( } else { abi::temp_int_reg(emitter.target) }; + // Reference targets may be one-word local slots. Only strings have a + // guaranteed second word in both local slots and default heap ref cells. match val_ty { PhpType::Bool | PhpType::Int @@ -158,42 +160,30 @@ pub(super) fn store_referenced_value( | PhpType::Packed(_) => { abi::emit_pop_reg(emitter, temp_reg); abi::emit_store_to_address(emitter, temp_reg, pointer_reg, 0); - abi::emit_store_zero_to_address(emitter, pointer_reg, 8); } PhpType::Resource(_) => { abi::emit_pop_reg(emitter, temp_reg); abi::emit_store_to_address(emitter, temp_reg, pointer_reg, 0); - abi::emit_load_int_immediate(emitter, temp_reg, 9); - abi::emit_store_to_address(emitter, temp_reg, pointer_reg, 8); } PhpType::Mixed | PhpType::Union(_) | PhpType::Iterable => { abi::emit_pop_reg(emitter, temp_reg); abi::emit_store_to_address(emitter, temp_reg, pointer_reg, 0); - abi::emit_load_int_immediate(emitter, temp_reg, 7); - abi::emit_store_to_address(emitter, temp_reg, pointer_reg, 8); } PhpType::Array(_) => { abi::emit_pop_reg(emitter, temp_reg); abi::emit_store_to_address(emitter, temp_reg, pointer_reg, 0); - abi::emit_load_int_immediate(emitter, temp_reg, 4); - abi::emit_store_to_address(emitter, temp_reg, pointer_reg, 8); } PhpType::AssocArray { .. } => { abi::emit_pop_reg(emitter, temp_reg); abi::emit_store_to_address(emitter, temp_reg, pointer_reg, 0); - abi::emit_load_int_immediate(emitter, temp_reg, 5); - abi::emit_store_to_address(emitter, temp_reg, pointer_reg, 8); } PhpType::Object(_) => { abi::emit_pop_reg(emitter, temp_reg); abi::emit_store_to_address(emitter, temp_reg, pointer_reg, 0); - abi::emit_load_int_immediate(emitter, temp_reg, 6); - abi::emit_store_to_address(emitter, temp_reg, pointer_reg, 8); } PhpType::Float => { abi::emit_pop_float_reg(emitter, abi::float_result_reg(emitter)); abi::emit_store_to_address(emitter, abi::float_result_reg(emitter), pointer_reg, 0); - abi::emit_store_zero_to_address(emitter, pointer_reg, 8); } PhpType::Str => { let (ptr_reg, len_reg) = abi::string_result_regs(emitter); @@ -206,7 +196,6 @@ pub(super) fn store_referenced_value( } PhpType::Void | PhpType::Never => { abi::emit_store_zero_to_address(emitter, pointer_reg, 0); - abi::emit_store_zero_to_address(emitter, pointer_reg, 8); } } }