fix: raise count() TypeError on a null-container Mixed receiver instead of segfaulting#616
Conversation
…iver count() on a boxed Mixed cell whose payload is the null-container sentinel — produced by a missed array read forwarded through a ternary merge, which boxes the sentinel as an indexed-array cell — dereferenced the sentinel past a plain-zero guard and segfaulted (issue illegalstudio#602). __rt_mixed_count now recognizes every null form of the receiver (a null pointer, the in-band sentinel as the cell or as a tag-4/5 payload, and a boxed null cell tag 8) and returns a null signal; the count() call site turns that signal into PHP's TypeError: count(): Argument illegalstudio#1 ($value) must be of type Countable|array, null given, matching the concrete-array path. Real containers, including empty ones, still count correctly and the catchable error path stays heap-clean. Guards land on both AArch64 and x86_64. Claude-Session: https://claude.ai/code/session_01HRvbcjPHa3kAojdCjNZL5L
|
The remaining count() parity gap mentioned in the description (non-null scalars in a mixed cell silently counting as 0, where PHP raises TypeError with the scalar type in the message) is now tracked as #617 — the machinery from this PR makes it a small follow-up, but it changes a test-locked behavior so it's kept separate. |
|
CI caught a real interaction here: extending the TypeError to plain null receivers breaks the deliberate BUG-0 CLI convention that off-web superglobals count as 0 ( |
The initial illegalstudio#602 fix over-reached: routing a plain null-pointer receiver and a boxed null cell (tag 8) to the count() TypeError broke the test-locked off-web convention that count($_SERVER) returns 0 (an uninitialized superglobal reaches __rt_mixed_count as a zero cell pointer), regressing bug0_cli_read_of_server_superglobal_before_assignment_does_not_crash. Narrow the guard to the actual crash: only the in-band NULL_SENTINEL — carried as the cell itself or as a tag-4/5 payload pointer (a missed refcounted read boxed as an array) — is guarded before the dereference and signaled back so the call site raises PHP's TypeError. A plain null pointer, a boxed null cell, a null payload, and every other non-countable tag keep returning 0 as before. Full PHP parity for real null (which raises a TypeError) is tracked with the non-container-scalar divergence in issue illegalstudio#617. Both AArch64 and x86_64. test_mixed_count_real_null_cell_matches_sentinel becomes test_mixed_count_real_null_cell_is_legacy_zero, locking count(json_decode("null")) === 0; the sentinel uncaught/catchable/heap-clean tests are unchanged. Claude-Session: https://claude.ai/code/session_01HRvbcjPHa3kAojdCjNZL5L
Regenerate the builtin catalog after the count() null-container guard shifted lower_empty's line reference in src/codegen/lower_inst/builtins.rs. No builtin signature, lowering strategy, or area assignment changed; only the source line link moves (1077 -> 1099). Produced by `python3 scripts/docs/extract_builtins.py --render --force`. Claude-Session: https://claude.ai/code/session_01HRvbcjPHa3kAojdCjNZL5L
|
Correction pushed ( |
Summary
count()on a boxedmixedreceiver whose payload is the null-container sentinel segfaulted(exit 139) instead of raising PHP's
TypeError. A missed array read forwarded through aternary merge (
$argc == 1 ? $rows[5] : ["a", "b"]) is boxed as an indexed-arraymixedcell whose payload pointer is the in-band
NULL_SENTINEL;__rt_mixed_countdereferencedthat payload past a plain-zero guard.
Repro
Run with no arguments (missed-read arm):
Warning: Undefined array key 5, then SIGSEGV (exit 139).Fatal error: Uncaught TypeError: count(): Argument #1 ($value) must be of type Countable|array, null given(exit 1) — matching PHP's diagnosticin elephc's uncaught-exception format, and catchable as a
TypeError.Root cause
The
count()lowering for a boxedMixedreceiver delegated to__rt_mixed_count, whosetag-4/5 payload read (
ldr x0, [x9]/mov rax, [r10]) was guarded only against a zeropointer. The non-zero in-band
NULL_SENTINELslipped through and was dereferenced — the samehole family as #526/#533 (read-side hardening), #585/PR #591 (get/set helpers), and
#592/PR #600 (append path). The concrete-array
count()arm already raised this exactTypeErrorviaemit_type_error; theMixedarm lacked the equivalent guard.Fix
__rt_mixed_countnow recognizes every null form of the receiver — a null pointer, thein-band sentinel as the cell or as a tag-4/5 payload (checked before the dereference, per
fix(codegen): guard container consumers against the null-container sentinel (chained-read miss segfault) #533), and a boxed null cell (tag 8) — and returns
NULL_SENTINELas a "receiver was anull container" signal. Other non-countable tags still return
0.count()call site turns that signal into PHP'sTypeErrorvia the existingemit_type_error, matching the concrete-array path. A plain0result (an empty container)passes through unchanged, so the sentinel signal never collides with a legitimate count.
Real null (
count(json_decode("null")), tag 8) previously returned0silently; it nowraises the identical
TypeError, so the sentinel and real null behave the same way and bothmatch PHP. Non-null non-countable scalars (e.g.
count(json_decode("42"))) still return0,unchanged (tracked separately as a quiet PHP-parity divergence).
Tests
New regression file
tests/codegen/regressions/mixed_count_sentinel.rs:count()prints the PHPTypeErrorfatal and exits with failure;TypeError;mixedcell still counts as0;Focused slices green:
mixed_count_sentinel(6),count(67),mixed(286),sentinel(43),
miss(119, including the #526 array-miss controls), sentinels lib unit tests (5).Fixes #602
https://claude.ai/code/session_01HRvbcjPHa3kAojdCjNZL5L