Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/ir_lower/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/types/checker/functions/call_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` 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,
Expand Down
28 changes: 28 additions & 0 deletions tests/codegen/objects/classes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,3 +511,31 @@ echo $u->id();
);
assert_eq!(out, "7");
}

/// Array element covariance into a declared `array<Object>` 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#"<?php
interface CondI { public function label(): string; }
final class Leaf implements CondI { public function __construct(public string $x) {} public function label(): string { return "L:".$this->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<CondI> $conditions */
public function __construct(private array $conditions) {}
/** @param list<CondI> $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|");
}
Loading