diff --git a/docs/internals/builtins/array/count.md b/docs/internals/builtins/array/count.md index b25bacadc1..74a4e5442f 100644 --- a/docs/internals/builtins/array/count.md +++ b/docs/internals/builtins/array/count.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/count.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/count.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:439](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L439) (`lower_count`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:442](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L442) (`lower_count`) - **Function symbol**: `lower_count()` diff --git a/docs/internals/builtins/class/class_exists.md b/docs/internals/builtins/class/class_exists.md index cc3671b2f7..20fde3f23f 100644 --- a/docs/internals/builtins/class/class_exists.md +++ b/docs/internals/builtins/class/class_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/class_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:293](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L293) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:296](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L296) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` diff --git a/docs/internals/builtins/class/enum_exists.md b/docs/internals/builtins/class/enum_exists.md index 4d97bdcb0a..bc7c097301 100644 --- a/docs/internals/builtins/class/enum_exists.md +++ b/docs/internals/builtins/class/enum_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/enum_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/enum_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:293](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L293) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:296](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L296) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` diff --git a/docs/internals/builtins/class/function_exists.md b/docs/internals/builtins/class/function_exists.md index e72e15dfbb..dee10064c8 100644 --- a/docs/internals/builtins/class/function_exists.md +++ b/docs/internals/builtins/class/function_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/function_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/function_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:276](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L276) (`lower_function_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:279](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L279) (`lower_function_exists`) - **Function symbol**: `lower_function_exists()` diff --git a/docs/internals/builtins/class/interface_exists.md b/docs/internals/builtins/class/interface_exists.md index 11a13350ab..86bade85d0 100644 --- a/docs/internals/builtins/class/interface_exists.md +++ b/docs/internals/builtins/class/interface_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/interface_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/interface_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:293](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L293) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:296](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L296) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` diff --git a/docs/internals/builtins/class/trait_exists.md b/docs/internals/builtins/class/trait_exists.md index 93e606d30c..40ac0a02fe 100644 --- a/docs/internals/builtins/class/trait_exists.md +++ b/docs/internals/builtins/class/trait_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/trait_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/trait_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:293](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L293) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:296](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L296) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` diff --git a/docs/internals/builtins/misc/defined.md b/docs/internals/builtins/misc/defined.md index ba5a89d8ad..502a191a96 100644 --- a/docs/internals/builtins/misc/defined.md +++ b/docs/internals/builtins/misc/defined.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/defined.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/defined.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:261](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L261) (`lower_defined`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:264](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L264) (`lower_defined`) - **Function symbol**: `lower_defined()` diff --git a/docs/internals/builtins/misc/empty.md b/docs/internals/builtins/misc/empty.md index 8d6543af38..865f61a3ee 100644 --- a/docs/internals/builtins/misc/empty.md +++ b/docs/internals/builtins/misc/empty.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:618](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L618) (`lower_empty`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:621](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L621) (`lower_empty`) - **Function symbol**: `lower_empty()` diff --git a/docs/internals/builtins/misc/phpversion.md b/docs/internals/builtins/misc/phpversion.md index 0d0b423615..dddb960274 100644 --- a/docs/internals/builtins/misc/phpversion.md +++ b/docs/internals/builtins/misc/phpversion.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/phpversion.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/phpversion.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:251](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L251) (`lower_phpversion`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:254](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L254) (`lower_phpversion`) - **Function symbol**: `lower_phpversion()` diff --git a/docs/internals/builtins/string/strlen.md b/docs/internals/builtins/string/strlen.md index 991c85188a..716aa9ca3e 100644 --- a/docs/internals/builtins/string/strlen.md +++ b/docs/internals/builtins/string/strlen.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strlen.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:493](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L493) (`lower_strlen`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:496](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L496) (`lower_strlen`) - **Function symbol**: `lower_strlen()` diff --git a/docs/internals/builtins/type/boolval.md b/docs/internals/builtins/type/boolval.md index 9db1506fb8..ca4705b17b 100644 --- a/docs/internals/builtins/type/boolval.md +++ b/docs/internals/builtins/type/boolval.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/boolval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/boolval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:586](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L586) (`lower_boolval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:589](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L589) (`lower_boolval`) - **Function symbol**: `lower_boolval()` diff --git a/docs/internals/builtins/type/floatval.md b/docs/internals/builtins/type/floatval.md index feda4bba04..572fda9155 100644 --- a/docs/internals/builtins/type/floatval.md +++ b/docs/internals/builtins/type/floatval.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/floatval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/floatval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:556](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L556) (`lower_floatval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:559](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L559) (`lower_floatval`) - **Function symbol**: `lower_floatval()` diff --git a/docs/internals/builtins/type/intval.md b/docs/internals/builtins/type/intval.md index dbaef413b1..52da2322dc 100644 --- a/docs/internals/builtins/type/intval.md +++ b/docs/internals/builtins/type/intval.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/intval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/intval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:523](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L523) (`lower_intval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:526](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L526) (`lower_intval`) - **Function symbol**: `lower_intval()` diff --git a/docs/internals/builtins/type/is_array.md b/docs/internals/builtins/type/is_array.md index aaf5a53e50..2f66d63239 100644 --- a/docs/internals/builtins/type/is_array.md +++ b/docs/internals/builtins/type/is_array.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_array.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1002](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1002) (`lower_is_array`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1005](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1005) (`lower_is_array`) - **Function symbol**: `lower_is_array()` diff --git a/docs/internals/builtins/type/is_bool.md b/docs/internals/builtins/type/is_bool.md index aa91c078e6..bd5c4a9125 100644 --- a/docs/internals/builtins/type/is_bool.md +++ b/docs/internals/builtins/type/is_bool.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_bool.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_bool.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:736](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L736) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:739](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L739) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/docs/internals/builtins/type/is_callable.md b/docs/internals/builtins/type/is_callable.md index 2a2d5cf3d7..45754870c7 100644 --- a/docs/internals/builtins/type/is_callable.md +++ b/docs/internals/builtins/type/is_callable.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_callable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_callable.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:319](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L319) (`lower_is_callable`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:322](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L322) (`lower_is_callable`) - **Function symbol**: `lower_is_callable()` diff --git a/docs/internals/builtins/type/is_float.md b/docs/internals/builtins/type/is_float.md index 280ed86bab..9d69d02aa2 100644 --- a/docs/internals/builtins/type/is_float.md +++ b/docs/internals/builtins/type/is_float.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_float.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_float.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:736](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L736) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:739](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L739) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/docs/internals/builtins/type/is_int.md b/docs/internals/builtins/type/is_int.md index 951c5da4b3..45eaef5632 100644 --- a/docs/internals/builtins/type/is_int.md +++ b/docs/internals/builtins/type/is_int.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_int.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_int.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:736](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L736) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:739](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L739) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/docs/internals/builtins/type/is_iterable.md b/docs/internals/builtins/type/is_iterable.md index fdc8b3277b..9c8f08c835 100644 --- a/docs/internals/builtins/type/is_iterable.md +++ b/docs/internals/builtins/type/is_iterable.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_iterable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_iterable.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:794](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L794) (`lower_is_iterable`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:797](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L797) (`lower_is_iterable`) - **Function symbol**: `lower_is_iterable()` diff --git a/docs/internals/builtins/type/is_null.md b/docs/internals/builtins/type/is_null.md index adbf86dfc0..51cc646186 100644 --- a/docs/internals/builtins/type/is_null.md +++ b/docs/internals/builtins/type/is_null.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_null.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:992](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L992) (`lower_is_null_builtin`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:995](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L995) (`lower_is_null_builtin`) - **Function symbol**: `lower_is_null_builtin()` diff --git a/docs/internals/builtins/type/is_object.md b/docs/internals/builtins/type/is_object.md index db8bf7064b..552788afd2 100644 --- a/docs/internals/builtins/type/is_object.md +++ b/docs/internals/builtins/type/is_object.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_object.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_object.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1017](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1017) (`lower_is_object`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1020](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1020) (`lower_is_object`) - **Function symbol**: `lower_is_object()` diff --git a/docs/internals/builtins/type/is_scalar.md b/docs/internals/builtins/type/is_scalar.md index c2717fc7aa..8ba7c0b7e4 100644 --- a/docs/internals/builtins/type/is_scalar.md +++ b/docs/internals/builtins/type/is_scalar.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_scalar.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_scalar.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1033](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1033) (`lower_is_scalar`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1036](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1036) (`lower_is_scalar`) - **Function symbol**: `lower_is_scalar()` diff --git a/docs/internals/builtins/type/is_string.md b/docs/internals/builtins/type/is_string.md index 64cd46c5de..4b47b3934b 100644 --- a/docs/internals/builtins/type/is_string.md +++ b/docs/internals/builtins/type/is_string.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_string.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:736](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L736) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:739](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L739) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/docs/php/control-structures.md b/docs/php/control-structures.md index 1c920b420f..6f38cfedbf 100644 --- a/docs/php/control-structures.md +++ b/docs/php/control-structures.md @@ -205,6 +205,16 @@ echo $result; // two If no arm matches and there is no `default`, elephc aborts with a fatal runtime error. +Arms may produce values of different types (objects, arrays, strings, ints, `null`), +and an arm may be a `throw` expression. When the arm types are heterogeneous, the +result is stored as a boxed `mixed` value and each value-producing arm keeps its +own runtime type, matching PHP; a `null` arm keeps the merged result nullable, so +returning such a match from a function with an inferred return type preserves the +null. Exception: arms whose types share one runtime representation (two array +types with different element types, or `int` and `bool`) merge to that +representation, which can change an arm value's observable type — see +[Known incompatibilities with PHP](types.md#known-incompatibilities-with-php). + ## try / catch / finally / throw ```php diff --git a/docs/php/types.md b/docs/php/types.md index 864c6765b8..76db5baa51 100644 --- a/docs/php/types.md +++ b/docs/php/types.md @@ -240,6 +240,7 @@ Narrowing applies to function and method parameters. A parameter whose call site - Plain array numeric casts (`(int)$array`, `(float)$array`) follow elephc's existing array cast semantics (return the element count rather than PHP's `0`/`1`). Direct `iterable` numeric casts use PHP's empty/non-empty `0`/`1` semantics. - `__destruct` runs when an object's refcount reaches zero (scope exit, reassignment, `unset`, program end), matching PHP's timing, but **object resurrection is not supported**: re-storing `$this` so the object would outlive the destructor does not keep it alive — the object is still freed once `__destruct` returns. - Under the compatibility `--null-repr=sentinel` opt-out, the integer `9223372036854775806` (`PHP_INT_MAX - 1`) collides with elephc's internal null marker in unboxed scalar slots and is misread as `null` by `echo`, `var_dump()`, `is_null()`, `??`, and related null checks. The default tagged null representation does not have this collision: the full 64-bit integer range round-trips. +- `match` (and ternary) arms whose types share one runtime representation merge to it instead of each arm keeping its own type: an `int` arm together with a `bool` arm collapses to one representation (`match($n) { 1 => 42, default => true }` yields `bool(true)` where PHP keeps `int(42)`), and two indexed-array arms with different element types merge to one element type (`match($n) { 1 => [1, 2], default => ["a", "b"] }` reads the int-array arm's elements through the string-array type). Arms of otherwise distinct types — object, array, string, int, float, `null` — each keep their own runtime type, matching PHP. - Variable variables (`$$name`, `${$expr}`) are not supported. elephc allocates each local to a fixed compile-time stack slot and keeps no per-frame variable-name table, so a variable whose name is computed at runtime cannot be resolved. Use an array keyed by the dynamic name instead. - `serialize()`/`unserialize()` cover scalars, arrays, and objects (including the `__serialize`/`__unserialize`/`__sleep`/`__wakeup` magic methods and `r:`/`R:` object back-references) byte-for-byte compatibly with PHP. Remaining gaps: a cyclic reference inside an object's own properties resolves to `null` on `unserialize()` (serialization handles cycles), the deprecated `Serializable` interface (`C:` wire form) is unsupported, writing a property of an unserialized object held in a `Mixed` does not persist (a separate `Mixed` property-write limitation), and `unserialize()` does not emit PHP's `E_WARNING` / `E_NOTICE` on malformed input — it just returns `false`. diff --git a/scripts/docs/builtin_registry.json b/scripts/docs/builtin_registry.json index 8f97ec8891..c7e458c03d 100644 --- a/scripts/docs/builtin_registry.json +++ b/scripts/docs/builtin_registry.json @@ -3332,7 +3332,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_boolval", - "codegen_line": 586, + "codegen_line": 589, "notes": [ "Lowers `boolval()` using the same concrete scalar PHP truthiness rules as `IsTruthy`." ], @@ -4104,7 +4104,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 293, + "codegen_line": 296, "notes": [ "Lowers AOT class/interface/enum existence checks for literal names." ], @@ -4528,7 +4528,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_count", - "codegen_line": 439, + "codegen_line": 442, "notes": [ "Lowers `count(array)` for concrete array values by reading the runtime length header.", "Called from `crate::builtins::array::count` (the registry home) via a thin wrapper.", @@ -4938,7 +4938,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_defined", - "codegen_line": 261, + "codegen_line": 264, "notes": [ "Lowers `defined(\"NAME\")` for compile-time string constant names." ], @@ -5172,7 +5172,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_empty", - "codegen_line": 618, + "codegen_line": 621, "notes": [ "Lowers `empty()` for concrete scalar and array-like operands." ], @@ -5211,7 +5211,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 293, + "codegen_line": 296, "notes": [ "Lowers AOT class/interface/enum existence checks for literal names." ], @@ -6295,7 +6295,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_floatval", - "codegen_line": 556, + "codegen_line": 559, "notes": [ "Lowers `floatval()` for concrete scalar operands." ], @@ -7094,7 +7094,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_function_exists", - "codegen_line": 276, + "codegen_line": 279, "notes": [ "Lowers `function_exists(\"name\")` for compile-time string names.", "Recognizes user functions, externs, catalog builtins, and the date/time procedural aliases", @@ -9234,7 +9234,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 293, + "codegen_line": 296, "notes": [ "Lowers AOT class/interface/enum existence checks for literal names." ], @@ -9278,7 +9278,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_intval", - "codegen_line": 523, + "codegen_line": 526, "notes": [ "Lowers `intval()` for concrete scalar operands." ], @@ -9409,7 +9409,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_array", - "codegen_line": 1002, + "codegen_line": 1005, "notes": [ "Lowers `is_array()`: true for statically-known arrays/hashes, or a boxed Mixed/Union value", "whose runtime tag is an indexed (4) or associative (5) array. An `iterable`-typed value is", @@ -9448,7 +9448,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 736, + "codegen_line": 739, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -9485,7 +9485,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_callable", - "codegen_line": 319, + "codegen_line": 322, "notes": [ "Lowers `is_callable(value)` through static lookup or runtime callable-shape helpers." ], @@ -9687,7 +9687,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 736, + "codegen_line": 739, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -9761,7 +9761,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 736, + "codegen_line": 739, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -9798,7 +9798,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_iterable", - "codegen_line": 794, + "codegen_line": 797, "notes": [ "Lowers `is_iterable()` for concrete values and boxed Mixed payloads." ], @@ -9914,7 +9914,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_null_builtin", - "codegen_line": 992, + "codegen_line": 995, "notes": [ "Lowers `is_null()` for concrete scalar values and boxed Mixed payloads." ], @@ -9988,7 +9988,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_object", - "codegen_line": 1017, + "codegen_line": 1020, "notes": [ "Lowers `is_object()`: true for statically-known objects, or a boxed Mixed/Union value whose", "runtime tag is an object (6)." @@ -10104,7 +10104,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_scalar", - "codegen_line": 1033, + "codegen_line": 1036, "notes": [ "Lowers `is_scalar()`: true for int/float/string/bool, a non-null tagged scalar, or a boxed", "Mixed/Union value whose runtime tag is int (0), string (1), float (2), or bool (3). Null,", @@ -10143,7 +10143,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 736, + "codegen_line": 739, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -12129,7 +12129,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_phpversion", - "codegen_line": 251, + "codegen_line": 254, "notes": [ "Lowers `phpversion()` as the compiler package version string." ], @@ -17521,7 +17521,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_strlen", - "codegen_line": 493, + "codegen_line": 496, "notes": [ "Lowers `strlen()` by coercing string-like values and returning the byte length." ], @@ -18355,7 +18355,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 293, + "codegen_line": 296, "notes": [ "Lowers AOT class/interface/enum existence checks for literal names." ], diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 43b62538b6..b138b85d1b 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -130,7 +130,10 @@ pub(crate) fn lower_gettype(ctx: &mut FunctionContext<'_>, inst: &Instruction) - ensure_arg_count(inst, "gettype", 1)?; let value = expect_operand(inst, 0)?; let ty = ctx.raw_value_php_type(value)?; - if matches!(ty, PhpType::TaggedScalar) { + // Dispatch on the codegen representation: a nullable-int union stores an + // inline tagged scalar, not a boxed Mixed cell, so unboxing it would read + // a non-pointer payload as a heap cell and crash. + if matches!(ty.codegen_repr(), PhpType::TaggedScalar) { emit_tagged_scalar_gettype(ctx, value)?; return store_if_result(ctx, inst); } diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 4867521749..fd59d42d74 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -7124,7 +7124,7 @@ fn lower_match( expr: &Expr, ) -> LoweredValue { let subject = lower_expr(ctx, subject); - let result_type = fallback_expr_type(expr); + let result_type = match_merge_result_type(ctx, arms, default, expr); let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); let merge = ctx.builder.create_named_block("match.merge", Vec::new()); @@ -10213,6 +10213,33 @@ fn branch_merge_result_type( wider_type_for_merge(&fallback_ty, &branch_ty.codegen_repr()) } +/// Chooses a match hidden-temp type by merging every arm result type, so +/// heterogeneous arms (e.g. object/array/string) materialize a Mixed temp +/// boxed per arm instead of coercing all arms to one unified scalar type. +fn match_merge_result_type( + ctx: &LoweringContext<'_, '_>, + arms: &[(Vec, Expr)], + default: Option<&Expr>, + expr: &Expr, +) -> PhpType { + let mut merged: Option = None; + for result in arms.iter().map(|(_, result)| result).chain(default) { + let arm_ty = materialized_expr_type_for_merge(ctx, result); + merged = Some(match merged { + Some(acc) => nullable_aware_branch_merge_type(&acc, &arm_ty), + None => arm_ty, + }); + } + let Some(merged) = merged else { + return fallback_expr_type(expr); + }; + if php_type_allows_null(&merged) { + return merged; + } + let fallback_ty = fallback_expr_type(expr).codegen_repr(); + wider_type_for_merge(&fallback_ty, &merged.codegen_repr()) +} + /// Chooses a ternary branch merge type without erasing PHP null branches. fn nullable_aware_branch_merge_type(left: &PhpType, right: &PhpType) -> PhpType { if php_type_allows_null(left) || php_type_allows_null(right) { @@ -10256,6 +10283,9 @@ fn materialized_expr_type_for_merge(ctx: &LoweringContext<'_, '_>, expr: &Expr) else_expr, .. } => branch_merge_result_type(ctx, then_expr, else_expr, expr), + ExprKind::Match { arms, default, .. } => { + match_merge_result_type(ctx, arms, default.as_deref(), expr) + } ExprKind::ShortTernary { value, default } => { let value_ty = materialized_expr_type_for_merge(ctx, value).codegen_repr(); let default_ty = materialized_expr_type_for_merge(ctx, default).codegen_repr(); diff --git a/src/types/checker/inference/expr/mod.rs b/src/types/checker/inference/expr/mod.rs index 07b8c4ad5b..ed5007274b 100644 --- a/src/types/checker/inference/expr/mod.rs +++ b/src/types/checker/inference/expr/mod.rs @@ -130,21 +130,23 @@ impl Checker { default, } => { self.infer_type(subject, env)?; - let mut result_ty = None; + let mut result_ty: Option = None; for (conditions, result) in arms { for c in conditions { self.infer_type(c, env)?; } - let ty = self.infer_type(result, env)?; - if result_ty.is_none() { - result_ty = Some(ty); - } + let ty = self.match_arm_result_type(result, env)?; + result_ty = Some(match result_ty { + Some(acc) => merge_match_arm_result_type(acc, ty), + None => ty, + }); } if let Some(d) = default { - let ty = self.infer_type(d, env)?; - if result_ty.is_none() { - result_ty = Some(ty); - } + let ty = self.match_arm_result_type(d, env)?; + result_ty = Some(match result_ty { + Some(acc) => merge_match_arm_result_type(acc, ty), + None => ty, + }); } Ok(result_ty.unwrap_or(PhpType::Void)) } @@ -697,6 +699,21 @@ impl Checker { .unwrap_or(PhpType::Mixed) } + /// Infers a match arm result type for arm merging. Throw arms produce no + /// value, so their checker type (`Void`, shared with `null`) is normalized + /// to `Never` here: the merge must distinguish "arm never yields" (defer + /// to the other arms) from "arm yields null" (keep the merge nullable). + fn match_arm_result_type( + &mut self, + result: &Expr, + env: &TypeEnv, + ) -> Result { + let ty = self.infer_type(result, env)?; + if matches!(result.kind, ExprKind::Throw(_)) { + return Ok(PhpType::Never); + } + Ok(ty) + } } /// Returns `true` if `index` is a valid string offset index for a string receiver. @@ -712,3 +729,43 @@ fn is_valid_string_offset_index(index: &Expr, idx_ty: &PhpType) -> bool { if crate::types::parse_php_string_offset_literal(value).is_some() ) } + +/// Merges two match arm result types: identical arms keep their type, +/// `Never`-typed arms (`throw`, normalized at the call site) defer to the +/// other arm's type, `Void`-typed arms (checker `null`) keep the merge +/// nullable so the null arm's value survives return-type-driven coercion +/// (mirroring the lowered temp's nullable-aware merge), and any other +/// heterogeneous pair widens to `Mixed` so each arm's runtime value survives +/// instead of being coerced to the first arm's type. +fn merge_match_arm_result_type(acc: PhpType, next: PhpType) -> PhpType { + if acc == next { + return acc; + } + if acc == PhpType::Never { + return next; + } + if next == PhpType::Never { + return acc; + } + if acc == PhpType::Void { + return nullable_match_arm_type(next); + } + if next == PhpType::Void { + return nullable_match_arm_type(acc); + } + PhpType::Mixed +} + +/// Widens a match arm type to also admit PHP null, for merges where another +/// arm is a `null` literal. +fn nullable_match_arm_type(ty: PhpType) -> PhpType { + match ty { + PhpType::Mixed => PhpType::Mixed, + PhpType::Union(members) if members.contains(&PhpType::Void) => PhpType::Union(members), + PhpType::Union(mut members) => { + members.push(PhpType::Void); + PhpType::Union(members) + } + other => PhpType::Union(vec![other, PhpType::Void]), + } +} diff --git a/tests/codegen/control_flow.rs b/tests/codegen/control_flow.rs index 43c8cf8402..040cb8b6d1 100644 --- a/tests/codegen/control_flow.rs +++ b/tests/codegen/control_flow.rs @@ -21,5 +21,7 @@ mod assignments; mod nulls; #[path = "control_flow/ternary.rs"] mod ternary; +#[path = "control_flow/match_expressions.rs"] +mod match_expressions; #[path = "control_flow/closures.rs"] mod closures; diff --git a/tests/codegen/control_flow/match_expressions.rs b/tests/codegen/control_flow/match_expressions.rs new file mode 100644 index 0000000000..d91bd4448a --- /dev/null +++ b/tests/codegen/control_flow/match_expressions.rs @@ -0,0 +1,402 @@ +//! Purpose: +//! Integration and regression tests for match expressions with heterogeneous +//! arm result types (object/array/string/int/null mixes) and homogeneous arms. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Regression coverage for issue #488: heterogeneous match arms must merge +//! to a Mixed hidden temp (boxed per arm) instead of coercing every arm to +//! one scalar-biased unified type, which fatally cast object arms to string. +//! - `gettype` probes assert PHP's per-arm type preservation across arms. + +use super::*; + +/// Regression test for issue #488: a match whose arms produce an object, an +/// array, and a string must preserve each arm's value instead of fatally +/// coercing the object arm to string. +#[test] +fn test_match_heterogeneous_object_array_string_arms() { + let out = compile_and_run( + r#" new stdClass(), + 1 => [1, 2], + default => "s", + }; +} +$count = 0; +for ($i = 0; $i < 40; $i++) { + $v = pick($i); + $count++; +} +echo $count; +"#, + ); + assert_eq!(out, "40"); +} + +/// Tests that object and string match arms each keep their own runtime type +/// (PHP prints "object|string"), in both arm orders. +#[test] +fn test_match_object_and_string_arms_preserve_types() { + let out = compile_and_run( + r#" new stdClass(), + default => "s", + }; +} +echo gettype(pick(0)), "|", gettype(pick(1)); +"#, + ); + assert_eq!(out, "object|string"); + let out = compile_and_run( + r#" "s", + default => new stdClass(), + }; +} +echo gettype(pick(0)), "|", gettype(pick(1)); +"#, + ); + assert_eq!(out, "string|object"); +} + +/// Tests that object and array match arms are not silently unified: PHP +/// reports "object|array" for the two reached arms. +#[test] +fn test_match_object_and_array_arms_preserve_types() { + let out = compile_and_run( + r#" new stdClass(), + default => [1, 2], + }; +} +echo gettype(pick(0)), "|", gettype(pick(1)); +"#, + ); + assert_eq!(out, "object|array"); +} + +/// Tests that an object arm mixed with an int arm compiles and preserves both +/// runtime types (previously failed with an unsupported object-to-int cast). +#[test] +fn test_match_object_and_int_arms_preserve_types() { + let out = compile_and_run( + r#" new stdClass(), + default => 7, + }; +} +echo gettype(pick(0)), "|", gettype(pick(1)); +"#, + ); + assert_eq!(out, "object|integer"); +} + +/// Tests that array and int match arms keep their own types instead of the +/// int arm silently absorbing the array arm (or vice versa by arm order). +#[test] +fn test_match_array_and_int_arms_preserve_types() { + let out = compile_and_run( + r#" [1, 2], + default => 7, + }; +} +echo gettype(pick(0)), "|", gettype(pick(1)); +"#, + ); + assert_eq!(out, "array|integer"); +} + +/// Tests that string and int match arms keep their own types instead of the +/// string arm absorbing the int arm into a unified string temp. +#[test] +fn test_match_string_and_int_arms_preserve_types() { + let out = compile_and_run( + r#" "s", + default => 7, + }; +} +echo gettype(pick(0)), "|", gettype(pick(1)); +"#, + ); + assert_eq!(out, "string|integer"); +} + +/// Tests that object and null match arms preserve both results: PHP reports +/// "object|NULL" instead of coercing the null arm into an object. +#[test] +fn test_match_object_and_null_arms_preserve_types() { + let out = compile_and_run( + r#" new stdClass(), + default => null, + }; +} +echo gettype(pick(0)), "|", gettype(pick(1)); +"#, + ); + assert_eq!(out, "object|NULL"); +} + +/// Tests a heterogeneous match consumed at the top level (no enclosing +/// function or declared return type): the fatal object-to-string cast +/// happened inside the match arm itself, before any return coercion. +#[test] +fn test_match_heterogeneous_arms_top_level() { + let out = compile_and_run( + r#" new stdClass(), + default => "s", +}; +echo gettype($v); +"#, + ); + assert_eq!(out, "object"); +} + +/// Tests a heterogeneous match returned from a function with no declared +/// return type: the checker's inferred match type must agree with the +/// lowered Mixed merge instead of erroring on an unsupported cast. +#[test] +fn test_match_heterogeneous_arms_inferred_return_type() { + let out = compile_and_run( + r#" new stdClass(), + default => "s", + }; +} +echo gettype(pick(0)), "|", gettype(pick(1)); +"#, + ); + assert_eq!(out, "object|string"); +} + +/// Tests that homogeneous string arms keep working (the merged temp stays a +/// plain string, no boxing regression for same-typed arms). +#[test] +fn test_match_homogeneous_string_arms() { + let out = compile_and_run( + r#" "zero", + 1 => "one", + default => "many", +}; +"#, + ); + assert_eq!(out, "one"); +} + +/// Tests that a throw-only default arm does not widen the merged match type: +/// the value arms stay strings and the non-throw path prints normally. +#[test] +fn test_match_throw_default_arm_keeps_value_arm_type() { + let out = compile_and_run( + r#" "zero", + 1 => "one", + default => throw new Exception("nope"), + }; +} +echo pick(0), "|", pick(1); +"#, + ); + assert_eq!(out, "zero|one"); +} + +/// Regression test for the gettype() dispatch on a match-produced nullable +/// int: the hidden temp is an inline tagged scalar (`null|int`), which the +/// gettype() emitter previously unboxed as a boxed Mixed cell and crashed. +#[test] +fn test_match_int_and_null_arms_gettype() { + let out = compile_and_run( + r#" 1, + default => null, +}; +echo gettype($v), "|"; +$w = match($argc) { + 99 => 1, + default => null, +}; +echo gettype($w); +"#, + ); + assert_eq!(out, "integer|NULL"); +} + +/// Tests that a null arm survives a return with an inferred type: the checker +/// must infer a nullable merge (like the lowered temp) instead of dropping the +/// null arm and coercing its value to the other arm's type. +#[test] +fn test_match_null_and_string_arms_inferred_return_keeps_null() { + let out = compile_and_run( + r#" null, + default => "s", + }; +} +echo gettype(pick(0)), "|", gettype(pick(1)); +"#, + ); + assert_eq!(out, "NULL|string"); +} + +/// Tests the mirror arm order with an int value arm: the inferred return type +/// must stay nullable so the null default is not coerced to int 0. +#[test] +fn test_match_int_and_null_default_inferred_return_keeps_null() { + let out = compile_and_run( + r#" 7, + default => null, + }; +} +echo gettype(pick(0)), "|", gettype(pick(1)); +"#, + ); + assert_eq!(out, "integer|NULL"); +} + +/// Tests that heterogeneous arm values survive being consumed (not just type +/// probed): a corrupted boxed payload with a correct tag would pass the +/// gettype tests but fail here. +#[test] +fn test_match_heterogeneous_arm_values_consumed() { + let out = compile_and_run( + r#" "s", + default => 7, + }; +} +echo pick(0), "|", pick(1); +"#, + ); + assert_eq!(out, "s|7"); +} + +/// Tests that float and int match arms keep their own types and values: the +/// (Int, Float) pair must widen to Mixed, not unify to float. +#[test] +fn test_match_float_and_int_arms_preserve_types_and_values() { + let out = compile_and_run( + r#" 6.5, + default => 8, + }; +} +echo gettype(pick(0)), "|", gettype(pick(1)), "|", pick(0), "|", pick(1); +"#, + ); + assert_eq!(out, "double|integer|6.5|8"); +} + +/// Tests a heterogeneous match nested as another match's arm result: the +/// inner match must contribute its merged (Mixed) type to the outer merge +/// instead of a scalar-biased syntactic fallback re-introducing the #488 +/// fatal cast for nested arms. +#[test] +fn test_match_nested_heterogeneous_match_arm() { + let out = compile_and_run( + r#" match(0) { + 0 => new stdClass(), + default => 1, + }, + default => "s", + }; +} +echo gettype(pick(0)), "|", gettype(pick(1)); +"#, + ); + assert_eq!(out, "object|string"); +} + +/// Pins the documented int/bool arm-merge incompatibility (see "Known +/// incompatibilities with PHP" in docs/php/types.md): int and bool arms share +/// one runtime representation, so both arms observe it — PHP would print +/// "integer|boolean" here. If this test starts failing with PHP's output, the +/// incompatibility got fixed: update the docs entry alongside. +#[test] +fn test_match_int_and_bool_arms_merge_documented_divergence() { + let out = compile_and_run( + r#" 42, + default => true, +}; +$w = match($argc) { + 99 => 42, + default => true, +}; +echo gettype($r), "|", gettype($w); +"#, + ); + assert_eq!(out, "boolean|boolean"); +} + +/// Tests that heterogeneous scalar match arms stay heap-balanced: the boxed +/// per-arm values and the hidden Mixed temp must release cleanly. +#[test] +fn test_match_heterogeneous_scalar_arms_heap_clean() { + let out = compile_and_run_with_heap_debug( + r#" "s", + default => 7, + }; +} +$count = 0; +for ($i = 0; $i < 20; $i++) { + $v = pick($i); + $count++; +} +echo $count; +"#, + ); + assert_eq!(out.stdout, "20"); + assert!( + out.stderr.contains("HEAP DEBUG: leak summary: clean"), + "expected clean heap summary, got: {}", + out.stderr + ); +} diff --git a/tests/codegen/control_flow/ternary.rs b/tests/codegen/control_flow/ternary.rs index 07b9e5d4c1..e2602d7f0d 100644 --- a/tests/codegen/control_flow/ternary.rs +++ b/tests/codegen/control_flow/ternary.rs @@ -30,6 +30,22 @@ fn test_ternary_int() { assert_eq!(out, "7"); } +/// Regression test for gettype() on a ternary-produced nullable int: the +/// merge temp is an inline tagged scalar (`null|int`), which the gettype() +/// emitter previously unboxed as a boxed Mixed cell and crashed. +#[test] +fn test_ternary_int_null_gettype() { + let out = compile_and_run( + r#"