Skip to content

ArrayAccess 的 ??= miss 分支错误调用 offsetGet 而非 offsetSet,默认值未写入 #70

Description

@hafung

Environment

  • TypePHP: current upstream/master at a6cd38a
  • Zend baselines: PHP 8.5.6 (Linux) and PHP 8.5.10 ZTS (Windows)
  • AOT backend: Windows MSVC, freshly built and executed at both -O0 and -O3

Minimal reproducer

<?php

declare(strict_types=1);

final class Bag implements ArrayAccess
{
    public array $data = [];

    public function offsetExists(mixed $offset): bool
    {
        echo "exists:$offset\n";
        return array_key_exists($offset, $this->data);
    }

    public function offsetGet(mixed $offset): mixed
    {
        echo "get:$offset\n";
        return $this->data[$offset] ?? null;
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        echo "set:$offset=$value\n";
        $this->data[$offset] = $value;
    }

    public function offsetUnset(mixed $offset): void
    {
        unset($this->data[$offset]);
    }
}

function main(): void
{
    $bag = new Bag();
    $result = ($bag['service'] ??= 42);

    var_dump($result, $bag->data);
}

For Zend, invoke main() from a small runner. TypePHP binary mode invokes it directly.

Expected (Zend 8.5.6 and 8.5.10)

exists:service
set:service=42
int(42)
array(1) {
  ["service"]=>
  int(42)
}

Actual TypePHP output (-O0 and -O3)

exists:service
get:service
int(42)
array(0) {
}

The assignment expression returns the RHS, but the default is not stored.

Semantic invariant and failure mechanism

For an ArrayAccess target, ??= must use Zend's dimension-operation sequence:

  • missing offset: offsetExists() then offsetSet();
  • present non-null offset: offsetExists() then offsetGet(), without evaluating the RHS;
  • offsetExists() === true and offsetGet() === null: evaluate the RHS and call offsetSet().

The generated C++ for the reproducer is:

result = (php::exists(bag, {{php::ArrayDimFetch, php::Var(get_str(2))}})
    ? bag.item(get_str(2), true)
    : (bag.item(get_str(2), true) = 42LL));

bag.item(..., true) dispatches to offsetGet() for this userland ArrayAccess object. Assigning to that returned value mutates a temporary instead of dispatching to offsetSet().

At the compiler level, parseAssignOpCoalesce() uses the same parseWritableIdentifier() result for the selected read and the miss-branch write. That is valid for a real array bucket but not for an ArrayAccess operation. Ordinary assignment already distinguishes these paths in parseAssignArrayDim() and emits offsetSet() for object containers.

A robust boundary appears to require separate presence/read/write lowering (or reuse of the shared assignment-target emitter), while preserving the target stabilization and lazy-RHS ordering added by #47.

Relevance matrix

Dimension Result
Userland ArrayAccess, missing key broken: calls offsetGet(), never offsetSet()
Userland ArrayAccess, offsetExists() === true, offsetGet() === null broken: returns null and skips the default/write
Userland ArrayAccess, present non-null key correct: offsetExists() then offsetGet()
Concrete class and ArrayAccess-typed parameter both reproduce
Assignment result used / unused storage is missing in both contexts
Side-effecting receiver/key evaluated once after #47; the wrong write dispatch remains
Built-in ArrayObject stores the value, but the same lowering emits an unexpected undefined-key warning on a miss
Ordinary PHP arrays correct after #47; true bucket lvalues are unaffected
Native Class ArrayAccess explicitly rejected by the Native object boundary; out of scope
std containers separate native lowering; out of scope
Optimization level reproduced with fresh O0 and O3 executables
Backend Windows MSVC executed; generated lowering is backend-neutral, but Linux AOT was not executed locally because this checkout lacks Linux libphp/phpx runtime libraries

docs/en/INCOMPATIBLE_PHP_FEATURES.md does not list ordinary Zend ArrayAccess coalesce assignment as unsupported.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions