From 78d6c4afbf467b14d6f4417dff1fa22fbdcd0f8a Mon Sep 17 00:00:00 2001 From: Chad Peppers Date: Thu, 9 Jul 2026 13:32:37 -0500 Subject: [PATCH] feat(types): array element covariance into a declared array parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A PHP `array` parameter is a bare `array` — PHP does NOT enforce the `@param array` element type at runtime, so the declared element type is a hint, not a hard bound. - `types_compatible` accepts `Array(Mixed)` (a spread literal `[...$list, $x]` or an adaptive local) and an element-compatible array in either covariance direction for a declared `Array(T)` parameter — mirroring the whole-value `(_, Mixed) => true` trust posture, one container level down. - `array_literal_element_type_for_ir` now types a method-call element (`[$this->mk()]`) as `Mixed` instead of the syntactic `Int` fallback. The old fallback stored an object-returning method's result in an `Array(Int)` and int-cast it at the element store (a hard backend error). Co-Authored-By: Claude Opus 4.8 --- src/ir_lower/expr/mod.rs | 10 +++++++ .../checker/functions/call_validation.rs | 12 ++++++++ tests/codegen/objects/classes.rs | 28 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 71a2f70ede..bbe20dd885 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -6907,6 +6907,16 @@ fn array_literal_element_type_for_ir( property, ) .unwrap_or_else(|| ir_array_storage_type(infer_expr_type_syntactic(item))), + // A method-call element has no resolved storage type here (unlike a free + // `FunctionCall`, whose signature is looked up above) — the syntactic + // fallback reduces it to `Int`, so an object-returning method (`[$this->mk()]`) + // would be stored in an `Array(Int)` and int-cast at the element store (a + // hard backend error). Store method-call elements as `Mixed`: an + // adaptive-boxed array is run-clean for objects and every other return type, + // and matches the `Array(Mixed)` covariance the call boundary now accepts. + ExprKind::MethodCall { .. } + | ExprKind::NullsafeMethodCall { .. } + | ExprKind::StaticMethodCall { .. } => PhpType::Mixed, _ => ir_array_storage_type(infer_expr_type_syntactic(item)), } } diff --git a/src/types/checker/functions/call_validation.rs b/src/types/checker/functions/call_validation.rs index 9ed0c0a682..7bd286c259 100644 --- a/src/types/checker/functions/call_validation.rs +++ b/src/types/checker/functions/call_validation.rs @@ -206,6 +206,18 @@ impl Checker { PhpType::AssocArray { key, value }, PhpType::Array(_) | PhpType::AssocArray { .. }, ) if **key == PhpType::Mixed && **value == PhpType::Mixed => true, + // Array element covariance for a declared `Array(T)` parameter. The PHP + // parameter is a bare `array` and PHP does NOT enforce the `@param + // array` element type at runtime, so the declared element type is a + // hint, not a hard bound. Accept an `Array(Mixed)` (a spread literal + // `[...$list, $x]` or an adaptive local — codegen boxes the elements + // adaptively) or an element-compatible array in either covariance + // direction. This mirrors the whole-value `(_, Mixed) => true` trust + // posture, one container level down. + (PhpType::Array(expected_elem), PhpType::Array(actual_elem)) => { + Self::types_compatible(expected_elem, actual_elem) + || Self::types_compatible(actual_elem, expected_elem) + } (PhpType::Float, PhpType::Int | PhpType::Bool | PhpType::False | PhpType::Void) => true, (PhpType::Int, PhpType::Bool | PhpType::False | PhpType::Void) => true, (PhpType::Bool, PhpType::Int | PhpType::Void) => true, diff --git a/tests/codegen/objects/classes.rs b/tests/codegen/objects/classes.rs index ecae889c3e..9d163256bf 100644 --- a/tests/codegen/objects/classes.rs +++ b/tests/codegen/objects/classes.rs @@ -511,3 +511,31 @@ echo $u->id(); ); assert_eq!(out, "7"); } + +/// Array element covariance into a declared `array` parameter. A spread literal +/// `[...$list, $x]` types as `Array(Mixed)`, and a method-call element `[$this->mk()]` types as +/// `Array(Mixed)` too (its storage type is Mixed, not the syntactic `Int` that would int-cast +/// the object at the element store) — both must be accepted for a declared object-element array +/// and run byte-parity through a fluent builder. +#[test] +fn test_array_element_covariance_spread_and_method_call() { + let out = compile_and_run( + r#"x; } } +final class Group implements CondI { public function __construct(public string $op) {} public function label(): string { return "G:".$this->op; } } +final class Builder { + /** @param list $conditions */ + public function __construct(private array $conditions) {} + /** @param list $conditions */ + private function withConditions(array $conditions): self { return new self($conditions); } + private function mkGroup(string $op): Group { return new Group($op); } + public function addLeaf(Leaf $c): self { return $this->withConditions([...$this->conditions, $c]); } + public function addGroup(string $op): self { return $this->withConditions([$this->mkGroup($op)]); } + public function render(): string { $o = ""; foreach ($this->conditions as $c) { $o .= $c->label() . "|"; } return $o; } +} +echo (new Builder([new Leaf("a")]))->addLeaf(new Leaf("b"))->addGroup("AND")->render(); +"#, + ); + assert_eq!(out, "G:AND|"); +}