Summary
Null coalescing does not select its default when a missing array access has a statically known Str result:
<?php
$a = [
['', 'word0'],
['', 'word1'],
];
echo '[' . ($a[7][1] ?? 'dflt') . "]\n";
echo '[' . ($a[1][7] ?? 'dflt') . "]\n";
echo '[' . ($a[1][0] ?? 'dflt') . "]\n";
PHP distinguishes the two missing reads from the real empty string and prints:
On current elephc, the already non-crashing second-index miss $a[1][7] ?? 'dflt' yields '' instead of 'dflt'. PR #533 makes the first-index miss $a[7][1] safe instead of dereferencing the null-container sentinel, but it then exposes the same pre-existing semantic gap: that expression also yields ''.
The coalescing expressions must remain silent, matching PHP.
Root cause
?? lowers its left-hand access silently and then tests the result with IsNull. In src/codegen/lower_inst/predicates.rs::emit_is_null_result(), statically known PhpType::Str values fall through to constant false.
A missed read of a statically-string slot is therefore indistinguishable at the null check from a legitimate empty string. By the time IsNull runs, the missing/null information has been erased.
Current main at the time of filing: 0b26415c9b.
Design scope
A fix must preserve a distinction between:
- a missing/null string-valued read, for which
?? selects the default; and
- a real empty string, for which
?? keeps ''.
One possible direction is to give Str an explicit null representation. Another is to preserve missingness explicitly through silent access/coalesce lowering. Either approach needs a broad audit because strings use multiple storage classes and ownership contracts.
At minimum, audit:
- persistent and literal empty strings;
- concat/scratch-backed strings;
- heap-owned strings, including owned zero-length allocations;
- casts and builtin-produced strings;
- array/hash/property/string-offset miss paths;
- local, argument, return, property, and array-element storage;
- string ABI materialization and release/refcount helpers on every supported target.
The implementation must not reinterpret legitimate empty strings as null, create invalid sentinel pointers for string consumers, or introduce leaks/double releases.
Regression coverage
Related
Summary
Null coalescing does not select its default when a missing array access has a statically known
Strresult:PHP distinguishes the two missing reads from the real empty string and prints:
On current elephc, the already non-crashing second-index miss
$a[1][7] ?? 'dflt'yields''instead of'dflt'. PR #533 makes the first-index miss$a[7][1]safe instead of dereferencing the null-container sentinel, but it then exposes the same pre-existing semantic gap: that expression also yields''.The coalescing expressions must remain silent, matching PHP.
Root cause
??lowers its left-hand access silently and then tests the result withIsNull. Insrc/codegen/lower_inst/predicates.rs::emit_is_null_result(), statically knownPhpType::Strvalues fall through to constant false.A missed read of a statically-string slot is therefore indistinguishable at the null check from a legitimate empty string. By the time
IsNullruns, the missing/null information has been erased.Current
mainat the time of filing:0b26415c9b.Design scope
A fix must preserve a distinction between:
??selects the default; and??keeps''.One possible direction is to give
Stran explicit null representation. Another is to preserve missingness explicitly through silent access/coalesce lowering. Either approach needs a broad audit because strings use multiple storage classes and ownership contracts.At minimum, audit:
The implementation must not reinterpret legitimate empty strings as null, create invalid sentinel pointers for string consumers, or introduce leaks/double releases.
Regression coverage
$a[7][1] ?? 'dflt'returns'dflt'silently after fix(codegen): guard container consumers against the null-container sentinel (chained-read miss segfault) #533.$a[1][7] ?? 'dflt'returns'dflt'silently.--ir-opt=onand--ir-opt=off.Related