Category
Regression: · silent wrong output (AGENTS.md §3 class) · child of #36188
Problem
Verified on master 4eed6a2785 in the pinned image (build/audit/v_dot1.php):
| program |
Zend |
AOT |
$s = ""; for ($i=0;$i<5;$i++) { $s .= "x"; } echo $s, "|", strlen($s); |
xxxxx|5 |
xxxxx|1 |
$t = ""; $t .= "ab"; $t .= "cd"; echo $t, "|", strlen($t); |
abcd|4 |
abcd|0 |
$u = "a"; for (...) { $u = $u . "x"; } echo $u, "|", strlen($u); |
axxxxx|6 |
axxxxx|2 |
The bytes are right (echo prints the full string) and the length is wrong: JitStrlen::lowerLength() (ext/types/JitStrlen.php:41-51) asks JitStringArg::compileTimeLiteral($arg) and, when it returns a string, emits constInt(strlen($literal)). compileTimeLiteral() (lib/JIT/JitStringArg.php) just returns $arg->compileTimeString. That annotation is stamped when a variable is initialised from a literal and is propagated across phis from one incoming arm ($phiVar->compileTimeString = $newVal->compileTimeString, lib/JIT.php:10902; also :10971, :11133, :11164, :11183) and cleared in only 12 places — so after a loop back-edge, a .=, or a re-assignment the variable still carries its first literal. strlen is one consumer; grep -rn compileTimeString lib/JIT.php lib/JIT/*.php ext finds 888 references in 353 files (json_encode, property-name lowering, str_* folds, include paths, class names…), each a potential wrong fold.
Consequence beyond the wrong number: the guard if (strlen($s) > 1000) { $s = substr($s, 500); } never fires, so a 300k-iteration append loop that Zend runs in 40 ms was killed at the 12 GB container cap under AOT during the audit (build/audit/run.log, micro m_string). This is very likely the same mechanism behind several "returns wrong value after loop" issues the fleet has been fixing one builtin at a time.
php-src reference
- Zend folds
strlen() only on a literal argument at compile time (zend_compile.c zend_compile_func_strlen: if (arg is ZEND_AST_ZVAL string)); a CV is never folded.
PHP implementation target
- Make
compileTimeString sound: it may only survive on KIND_VALUE temporaries produced directly from a literal (or a fold of literals) in the same basic block. Clear it on every assignment to a named variable, on every phi (unless all incoming arms carry the identical literal), on .=/concat, on by-ref binding, and on passing to a by-ref parameter. Best done centrally in Variable (setter that requires the producing Op to be a literal/constant) rather than at 888 sites.
- Add an opcode-corpus and differential guard: the three probes above plus loop/branch/phi shapes for
strlen, json_encode, str_repeat, substr, dynamic property names, include $path.
- Audit the 12 explicit clears for the same shape; remove them once the setter enforces the rule.
Repro
./script/docker-exec.sh -- bash -lc 'source script/php-env.sh && printf "<?php \$s=\"\"; for(\$i=0;\$i<5;\$i++){ \$s.=\"x\"; } echo \$s,\"|\",strlen(\$s),\"\\n\";" > build/sl.php && php bin/compile.php -o build/sl build/sl.php && ./build/sl && php build/sl.php'
Done when
Category
Regression:· silent wrong output (AGENTS.md §3 class) · child of #36188Problem
Verified on master
4eed6a2785in the pinned image (build/audit/v_dot1.php):$s = ""; for ($i=0;$i<5;$i++) { $s .= "x"; } echo $s, "|", strlen($s);xxxxx|5xxxxx|1$t = ""; $t .= "ab"; $t .= "cd"; echo $t, "|", strlen($t);abcd|4abcd|0$u = "a"; for (...) { $u = $u . "x"; } echo $u, "|", strlen($u);axxxxx|6axxxxx|2The bytes are right (echo prints the full string) and the length is wrong:
JitStrlen::lowerLength()(ext/types/JitStrlen.php:41-51) asksJitStringArg::compileTimeLiteral($arg)and, when it returns a string, emitsconstInt(strlen($literal)).compileTimeLiteral()(lib/JIT/JitStringArg.php) just returns$arg->compileTimeString. That annotation is stamped when a variable is initialised from a literal and is propagated across phis from one incoming arm ($phiVar->compileTimeString = $newVal->compileTimeString, lib/JIT.php:10902; also :10971, :11133, :11164, :11183) and cleared in only 12 places — so after a loop back-edge, a.=, or a re-assignment the variable still carries its first literal.strlenis one consumer;grep -rn compileTimeString lib/JIT.php lib/JIT/*.php extfinds 888 references in 353 files (json_encode, property-name lowering, str_* folds, include paths, class names…), each a potential wrong fold.Consequence beyond the wrong number: the guard
if (strlen($s) > 1000) { $s = substr($s, 500); }never fires, so a 300k-iteration append loop that Zend runs in 40 ms was killed at the 12 GB container cap under AOT during the audit (build/audit/run.log, microm_string). This is very likely the same mechanism behind several "returns wrong value after loop" issues the fleet has been fixing one builtin at a time.php-src reference
strlen()only on a literal argument at compile time (zend_compile.czend_compile_func_strlen:if (arg is ZEND_AST_ZVAL string)); a CV is never folded.PHP implementation target
compileTimeStringsound: it may only survive onKIND_VALUEtemporaries produced directly from a literal (or a fold of literals) in the same basic block. Clear it on every assignment to a named variable, on every phi (unless all incoming arms carry the identical literal), on.=/concat, on by-ref binding, and on passing to a by-ref parameter. Best done centrally inVariable(setter that requires the producingOpto be a literal/constant) rather than at 888 sites.strlen,json_encode,str_repeat,substr, dynamic property names,include $path.Repro
./script/docker-exec.sh -- bash -lc 'source script/php-env.sh && printf "<?php \$s=\"\"; for(\$i=0;\$i<5;\$i++){ \$s.=\"x\"; } echo \$s,\"|\",strlen(\$s),\"\\n\";" > build/sl.php && php bin/compile.php -o build/sl build/sl.php && ./build/sl && php build/sl.php'Done when
test/differential/cases/; them_stringmicro (300k appends with astrlenguard) completes in < 2 scompileTimeStringcan only be set from a literal-producing op (enforced by a setter + unit test);grep -c "compileTimeString = \$" lib/JIT.phppropagation-through-phi sites removedscript/differential-sweep.sh --aot --repeat 3unchanged; compliance name-set unchanged