Skip to content

A literal array may hold a hole; static_variables may not (#260) - #261

Merged
EdmondDantes merged 1 commit into
mainfrom
260-sparse-literal-array
Aug 20, 2026
Merged

A literal array may hold a hole; static_variables may not (#260)#261
EdmondDantes merged 1 commit into
mainfrom
260-sparse-literal-array

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

Closes #260.

op_array_emalloc_copy_array rebuilds two different tables and asserted an
invariant that holds for one of them.

Static variables need every bucket kept: BIND_LEXICAL reaches a captured
variable by a byte offset into arData, computed at compile time, so a rebuild
that dropped a slot would shift every offset past it. The compiler seeds every
slot there, and the assert is right — it just belonged to that caller.

A literal array carries no such promise. [0 => 'a', 2 => 'b'] compiles to a
packed hash whose slot 1 is UNDEF, so nNumUsed is 3 while the element count is
2, and every debug build aborted the moment a task closure held one:

php: ext/async/thread.c:2342: op_array_emalloc_copy_array:
Assertion `zend_hash_num_elements(src) == src->nNumUsed' failed.

The rebuild itself was already correct for that case — it iterates by key, and
the keys are what the VM reads back — so this moves the assert to the
static_variables branch of op_array_to_emalloc and leaves the literal path
alone.

Reproduction

Twelve lines, no server involved:

spawn(function() {
    $pool = new ThreadPool(1);
    $future = $pool->submit(function () {
        $sparse = [0 => 'a', 2 => 'b'];
        return implode(',', $sparse);
    });
    echo await($future), "\n";
    $pool->close();
});

Aborts on main, prints a,b with this patch. It is tests/thread_pool/103-task_sparse_literal_array.phpt, which also covers a nested sparse array and string keys beside it.

What it was breaking

Every debug CI job of true-async/server since the merges of #254/#255/#256
earlier today, through tests/phpt/websocket/054-topics-pool-unsubscribe.phpt
a pool worker loading a handler closure. Server commit 5392ff7 was green at
09:11 UTC and the same commit failed at 12:50 UTC with no change on its side.

ext/async/tests: 1104 passed, 0 failed. true-async/server tests/phpt
against this build: 390 passed, 0 failed, both on a debug ZTS build.

#260)

op_array_emalloc_copy_array rebuilds two different tables, and the invariant
it asserted holds for one of them. Static variables need every bucket kept,
because BIND_LEXICAL reaches a captured variable by a byte offset into arData
and a dropped slot shifts every offset past it. A literal array carries no
such promise: [0 => 'a', 2 => 'b'] compiles to a packed hash whose slot 1 is
UNDEF, so nNumUsed is one past the element count and every debug build
aborted when a task closure held one.

The rebuild itself was already right — it iterates by key, and the keys are
what the VM reads — so the assert moves to the caller whose offsets depend on
it. Test 103-task_sparse_literal_array, which aborts without this.
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@EdmondDantes
EdmondDantes merged commit b1cf690 into main Aug 20, 2026
9 checks passed
@EdmondDantes
EdmondDantes deleted the 260-sparse-literal-array branch August 20, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Worker-pool op_array copy asserts on a hash with holes since today's main

1 participant