From 4a3375db6696f596a6dfd6d7f20791e8773c7efe Mon Sep 17 00:00:00 2001 From: Chad Peppers Date: Thu, 9 Jul 2026 13:40:14 -0500 Subject: [PATCH] fix(types): list destructuring accepts a union/assoc RHS carrying an array member `list()` / `[$a, $b] = ...` destructuring only accepted a plain `Array(T)` RHS and errored on everything else. A `?array` method return narrowed by `=== null` still types as `Union[Array, Void]` at the destructure (and an associative-array or Mixed union is an array at runtime in well-typed code). Accept an `AssocArray` RHS and a union that carries an array-like member; their elements bind as Mixed (adaptive access), the trust posture PHP applies at runtime. Co-Authored-By: Claude Opus 4.8 --- .../checker/stmt_check/assignments/locals.rs | 23 +++++++++++++++ tests/codegen/array_basics.rs | 29 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/types/checker/stmt_check/assignments/locals.rs b/src/types/checker/stmt_check/assignments/locals.rs index 173e1ef988..f9bdca6e0c 100644 --- a/src/types/checker/stmt_check/assignments/locals.rs +++ b/src/types/checker/stmt_check/assignments/locals.rs @@ -738,6 +738,29 @@ pub(super) fn check_list_unpack( update_list_unpack_callable_metadata(checker, var, value, &unpack_ty); } } + // An associative-array RHS, or a union that carries an array-like member + // (a null-narrowed `?array` return leaves `Union[Array, Void]`; a Mixed + // union), is an array at runtime in well-typed code. Its elements bind as + // Mixed (adaptive access), the same trust posture PHP applies at runtime. + PhpType::AssocArray { .. } => { + for var in vars { + env.insert(var.clone(), PhpType::Mixed); + update_list_unpack_callable_metadata(checker, var, value, &PhpType::Mixed); + } + } + PhpType::Union(members) + if members.iter().any(|member| { + matches!( + member.codegen_repr(), + PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Mixed + ) + }) => + { + for var in vars { + env.insert(var.clone(), PhpType::Mixed); + update_list_unpack_callable_metadata(checker, var, value, &PhpType::Mixed); + } + } _ => { return Err(CompileError::new( span, diff --git a/tests/codegen/array_basics.rs b/tests/codegen/array_basics.rs index 5978c4a262..9f1429022f 100644 --- a/tests/codegen/array_basics.rs +++ b/tests/codegen/array_basics.rs @@ -1014,3 +1014,32 @@ fn test_in_array_strict_distinguishes_bool_int_membership() { ); assert_eq!(out, "101010"); } + +/// List/array destructuring accepts a union RHS that carries an array member: a `?array` +/// method return, narrowed by `=== null`, still types as `Union[Array, Void]` at the +/// destructure, and its elements bind as Mixed (adaptive). Byte-parity vs PHP 8.5. +#[test] +fn test_list_unpack_null_narrowed_array_return() { + let out = compile_and_run( + r#"mk($n); + if ($entry === null) { continue; } + [$key, $value] = $entry; + $out .= $key . "=" . $value . ";"; + } + return $out; + } +} +echo (new R())->run(); +"#, + ); + assert_eq!(out, "k1=v1;k2=v2;"); +}