Skip to content

Inferred return types erase nullability for object/null ternary and match merges #494

Description

@mirchaemanuel

Summary

When a function's return type is inferred (not declared) and the returned expression merges an object with null — via ternary or match — the checker erases the nullability: the null path materializes as an object-typed value and gettype() reports object where PHP reports NULL.

<?php
function pick(int $n) {
    return $n === 0 ? new stdClass() : null;   // same with match($n) { 0 => new stdClass(), default => null }
}
echo gettype(pick(0)), "|", gettype(pick(1));

Declaring the return type (: mixed or : ?stdClass) makes both constructs behave correctly, so this is specifically the inferred-return boundary.

Mechanism

The checker types the null literal as Void, and both merge rules let the non-Void side win (the ternary inference keeps then_ty/the other branch; the match inference — after #492 — deliberately treats Void as a value-less pass-through so throw/null arms don't poison the merge). The inferred return type therefore comes out as plain Object("stdClass"), while the EIR lowering's merge temp is nullable (?Object), and the return coercion to the checker's Object erases the null.

Worse than the wrong gettype, the null path then flows on as a "valid" object: pick(1)->prop reads garbage (with an undefined-property warning) instead of PHP's "Attempt to read property on null" warning + NULL.

Expected

The inferred return type for these merges should preserve nullability (e.g. ?T / a Void|T union, or at minimum Mixed), so the null path survives the return boundary like it already does with a declared mixed return type.

Affects ternary and match identically; pre-existing (reproduced on main before the match work). Verified on macOS ARM64.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions