From e11b62a5117a06f2d846974635f9a4879db156bf Mon Sep 17 00:00:00 2001 From: PurHur Date: Tue, 25 Aug 2026 12:56:35 +0000 Subject: [PATCH 1/2] php-in-php: lazy ObOutputRuntime at ValueEchoHelper (#34695) Drop ensureMinimal/ensureFull always-on NestedJIT ob_* ABI; echo/print paths ensureLinked before __phpc_ob_echo_* lookup (peer #34642 / #32122). Co-authored-by: Cursor --- lib/JIT/Context.php | 8 +- lib/JIT/ValueEchoHelper.php | 5 ++ ...zyDomGcProgressAssertRuntimeShrinkTest.php | 17 ++-- ...eLazyHtmlErrorHandlerRuntimeShrinkTest.php | 15 +++- ...eLazyHtmlspecialcharsRuntimeShrinkTest.php | 14 ++- ...andaloneLazyLastErrorRuntimeShrinkTest.php | 8 +- ...tandaloneLazyObOutputRuntimeShrinkTest.php | 86 +++++++++++++++++++ ...loneLazyReturnPendingRuntimeShrinkTest.php | 15 +++- ...lStandaloneLazyStdlibRuntimeShrinkTest.php | 15 +++- ...daloneLazyStringTriggerErrorShrinkTest.php | 15 +++- test/unit/ObOutputRuntimeShrinkTest.php | 14 ++- 11 files changed, 186 insertions(+), 26 deletions(-) create mode 100644 test/unit/ContextMinimalStandaloneLazyObOutputRuntimeShrinkTest.php diff --git a/lib/JIT/Context.php b/lib/JIT/Context.php index 02dd1106b12..39c35bcb6d1 100644 --- a/lib/JIT/Context.php +++ b/lib/JIT/Context.php @@ -2317,7 +2317,10 @@ private function ensureMinimalUserStandaloneBodies(): void // JitReturnPending always-on removed (#34621): TryCatchHelper / emitPendingReturnResume // already ensureLinked before lookup (peer #34612). JitHelperAbiBridge restores insert // mid-{main}. Leftover Context NestedJIT vs Runtime ABI drift mints *.1 (#31894 / #32122). - Builtin\ObOutputRuntime::ensureLinked($this); + // ObOutput always-on removed (#34695): ValueEchoHelper / ValueEchoRuntime / + // StringVarDump / ObOutput / StreamReadRuntime already ensureLinked before + // __phpc_ob_echo_* lookup (peer #34642). Leftover Context NestedJIT vs Runtime ABI + // drift mints ob_*.1 (#31894 / #32122). Thin hello-world must not NestedJIT ob during init. // StringRandomBytes / Utf8Latin1 / RewriteVars / Define / StrContains / StatPath / // FileGetContents / MetaTags / HashCrypto / MbNumericEntity / Readfile / Bin2hex / // Addslashes / Stripslashes / FilePutContents / IniRuntime always-on removed (#34578): @@ -2367,7 +2370,8 @@ private function ensureFullStandaloneBodies(): void Builtin\AssertFail::ensureStandaloneBodies($this); Builtin\AssertOptionsRuntime::ensureStandaloneBodies($this); Builtin\JitReturnPending::ensureStandaloneBodies($this); - Builtin\ObOutputRuntime::ensureLinked($this); + // ObOutput always-on removed (#34695): ValueEchoRuntime::ensureLinked → ObOutput + // (and ValueEchoHelper call sites). Do not re-add before ValueEcho here. Builtin\ValueEchoRuntime::ensureLinked($this); Builtin\CliArgvRuntime::ensureStandaloneBodies($this); // Nested-JIT string helpers: lazy via ensureLinked during spine/thin init (#14472, #20571). diff --git a/lib/JIT/ValueEchoHelper.php b/lib/JIT/ValueEchoHelper.php index 044fe694c7c..99dc40a0685 100644 --- a/lib/JIT/ValueEchoHelper.php +++ b/lib/JIT/ValueEchoHelper.php @@ -11,6 +11,7 @@ namespace PHPCompiler\JIT; +use PHPCompiler\JIT\Builtin\ObOutputRuntime; use PHPCompiler\JIT\Builtin\ValueEchoRuntime; use PHPCompiler\JIT\IncDecResourceProvenance; use PHPCfg\Operand; @@ -23,6 +24,8 @@ final class ValueEchoHelper public static function echoLiteral(Context $context, string $literal): void { + // Lazy ob_* ABI — Context ensureMinimal must not NestedJIT ObOutput (#34695 / peer #34642). + ObOutputRuntime::ensureLinked($context); $charPtr = $context->getTypeFromString('char*'); $context->builder->call( $context->lookupFunction('__phpc_ob_echo_cstr'), @@ -42,6 +45,7 @@ public static function echoNativeLong( ?Operand $sourceOperand = null ): void { + ObOutputRuntime::ensureLinked($context); Builtin\StringDir::ensureLinked($context); $tag = 'enl'.(string) ++self::$seq; $i64 = $context->getTypeFromString('int64'); @@ -189,6 +193,7 @@ public static function echoObjectVariable(Context $context, Variable $objectVar, public static function echoStringVariable(Context $context, Variable $stringVar): void { + ObOutputRuntime::ensureLinked($context); $argValue = $context->helper->loadValue($stringVar); $offset = $context->structFieldIndex($argValue, 'length'); $__str__length = $context->builder->load( diff --git a/test/unit/ContextMinimalStandaloneLazyDomGcProgressAssertRuntimeShrinkTest.php b/test/unit/ContextMinimalStandaloneLazyDomGcProgressAssertRuntimeShrinkTest.php index b858d6fc68b..7de19b947ae 100644 --- a/test/unit/ContextMinimalStandaloneLazyDomGcProgressAssertRuntimeShrinkTest.php +++ b/test/unit/ContextMinimalStandaloneLazyDomGcProgressAssertRuntimeShrinkTest.php @@ -38,18 +38,25 @@ public function testEnsureMinimalDropsEagerDomGcProgressAssert(): void ); } - // Essentials for thin echo / error / argv / getenv surface stay. - // HtmlEntities / Decode / ErrorHandler / ExceptionHandler dropped in #34612 (peer this test). - // LastError dropped in #34631 (peer this test). + // Essentials for thin argv / getenv surface stay (#34695 dropped ObOutput; + // #34641 dropped StringTriggerError). foreach ([ - 'ObOutputRuntime::ensureLinked($this)', - 'StringTriggerError::ensureStandaloneBodies($this)', 'CliArgvRuntime::ensureStandaloneBodies($this)', 'EnvLocalRuntime::ensureLinked($this)', 'SuperglobalNameRuntime::ensureLinked($this)', ] as $keep) { $this->assertStringContainsString($keep, $minimalBody, "keep {$keep} in minimal (#34605)"); } + $this->assertStringNotContainsString( + 'ObOutputRuntime::ensureLinked($this)', + $minimalBody, + 'ensureMinimal must not eagerly ObOutputRuntime (#34695)' + ); + $this->assertStringNotContainsString( + 'StringTriggerError::ensureStandaloneBodies($this)', + $minimalBody, + 'ensureMinimal must not eagerly StringTriggerError (#34641)' + ); $this->assertStringNotContainsString( 'LastErrorRuntime::ensureStandaloneBodies($this)', $minimalBody, diff --git a/test/unit/ContextMinimalStandaloneLazyHtmlErrorHandlerRuntimeShrinkTest.php b/test/unit/ContextMinimalStandaloneLazyHtmlErrorHandlerRuntimeShrinkTest.php index c7eb7ad6956..c75782b0f9f 100644 --- a/test/unit/ContextMinimalStandaloneLazyHtmlErrorHandlerRuntimeShrinkTest.php +++ b/test/unit/ContextMinimalStandaloneLazyHtmlErrorHandlerRuntimeShrinkTest.php @@ -37,17 +37,26 @@ public function testEnsureMinimalDropsEagerHtmlErrorHandler(): void ); } - // Essentials for thin echo / error / argv / getenv surface stay. + // Essentials for thin argv / getenv surface stay (#34695 dropped ObOutput; + // #34641 dropped StringTriggerError). // LastError dropped in #34631 (peer this test). foreach ([ - 'ObOutputRuntime::ensureLinked($this)', - 'StringTriggerError::ensureStandaloneBodies($this)', 'CliArgvRuntime::ensureStandaloneBodies($this)', 'EnvLocalRuntime::ensureLinked($this)', 'SuperglobalNameRuntime::ensureLinked($this)', ] as $keep) { $this->assertStringContainsString($keep, $minimalBody, "keep {$keep} in minimal (#34612)"); } + $this->assertStringNotContainsString( + 'ObOutputRuntime::ensureLinked($this)', + $minimalBody, + 'ensureMinimal must not eagerly ObOutputRuntime (#34695)' + ); + $this->assertStringNotContainsString( + 'StringTriggerError::ensureStandaloneBodies($this)', + $minimalBody, + 'ensureMinimal must not eagerly StringTriggerError (#34641)' + ); $this->assertStringNotContainsString( 'LastErrorRuntime::ensureStandaloneBodies($this)', $minimalBody, diff --git a/test/unit/ContextMinimalStandaloneLazyHtmlspecialcharsRuntimeShrinkTest.php b/test/unit/ContextMinimalStandaloneLazyHtmlspecialcharsRuntimeShrinkTest.php index a0c08149609..46f5a1b1e77 100644 --- a/test/unit/ContextMinimalStandaloneLazyHtmlspecialcharsRuntimeShrinkTest.php +++ b/test/unit/ContextMinimalStandaloneLazyHtmlspecialcharsRuntimeShrinkTest.php @@ -30,10 +30,8 @@ public function testEnsureMinimalDropsEagerHtmlspecialchars(): void 'ensureMinimalUserStandaloneBodies must not eagerly StringHtmlspecialchars (#34642)' ); - // Essentials for thin echo / error / argv / getenv surface stay. + // Essentials for thin argv / getenv / bridges stay (#34695 dropped ObOutput). foreach ([ - 'ObOutputRuntime::ensureLinked($this)', - 'StringTriggerError::ensureStandaloneBodies($this)', 'CliArgvRuntime::ensureStandaloneBodies($this)', 'EnvLocalRuntime::ensureLinked($this)', 'SuperglobalNameRuntime::ensureLinked($this)', @@ -42,6 +40,16 @@ public function testEnsureMinimalDropsEagerHtmlspecialchars(): void ] as $keep) { $this->assertStringContainsString($keep, $minimalBody, "keep {$keep} in minimal (#34642)"); } + $this->assertStringNotContainsString( + 'ObOutputRuntime::ensureLinked($this)', + $minimalBody, + 'ensureMinimal must not eagerly ObOutputRuntime (#34695)' + ); + $this->assertStringNotContainsString( + 'StringTriggerError::ensureStandaloneBodies($this)', + $minimalBody, + 'ensureMinimal must not eagerly StringTriggerError (#34641)' + ); } public function testCallSiteEnsuresBeforeLookup(): void diff --git a/test/unit/ContextMinimalStandaloneLazyLastErrorRuntimeShrinkTest.php b/test/unit/ContextMinimalStandaloneLazyLastErrorRuntimeShrinkTest.php index 46085ebcde9..05fe074da82 100644 --- a/test/unit/ContextMinimalStandaloneLazyLastErrorRuntimeShrinkTest.php +++ b/test/unit/ContextMinimalStandaloneLazyLastErrorRuntimeShrinkTest.php @@ -30,9 +30,8 @@ public function testEnsureMinimalDropsEagerLastError(): void 'ensureMinimalUserStandaloneBodies must not eagerly LastErrorRuntime (#34631)' ); - // Essentials for thin echo / error / argv / getenv surface stay. + // Essentials for thin argv / getenv / bridges stay (#34695 dropped ObOutput). foreach ([ - 'ObOutputRuntime::ensureLinked($this)', 'CliArgvRuntime::ensureStandaloneBodies($this)', 'EnvLocalRuntime::ensureLinked($this)', 'SuperglobalNameRuntime::ensureLinked($this)', @@ -41,6 +40,11 @@ public function testEnsureMinimalDropsEagerLastError(): void ] as $keep) { $this->assertStringContainsString($keep, $minimalBody, "keep {$keep} in minimal (#34631)"); } + $this->assertStringNotContainsString( + 'ObOutputRuntime::ensureLinked($this)', + $minimalBody, + 'ensureMinimal must not eagerly ObOutputRuntime (#34695)' + ); $this->assertStringNotContainsString( 'StringTriggerError::ensureStandaloneBodies($this)', diff --git a/test/unit/ContextMinimalStandaloneLazyObOutputRuntimeShrinkTest.php b/test/unit/ContextMinimalStandaloneLazyObOutputRuntimeShrinkTest.php new file mode 100644 index 00000000000..dbb9d2a76e2 --- /dev/null +++ b/test/unit/ContextMinimalStandaloneLazyObOutputRuntimeShrinkTest.php @@ -0,0 +1,86 @@ +assertStringContainsString('#34695', $context); + $minimalPos = strpos($context, 'private function ensureMinimalUserStandaloneBodies'); + $this->assertNotFalse($minimalPos); + $minimalEnd = strpos($context, 'private function ensureBootstrapAotStandaloneBodies', $minimalPos); + $this->assertNotFalse($minimalEnd); + $minimalBody = substr($context, $minimalPos, $minimalEnd - $minimalPos); + + $this->assertStringNotContainsString( + 'ObOutputRuntime::ensureLinked($this)', + $minimalBody, + 'ensureMinimalUserStandaloneBodies must not eagerly ObOutputRuntime (#34695)' + ); + + // Essentials for thin argv / getenv / bridges stay. + foreach ([ + 'CliArgvRuntime::ensureStandaloneBodies($this)', + 'EnvLocalRuntime::ensureLinked($this)', + 'SuperglobalNameRuntime::ensureLinked($this)', + 'ExceptionBridge::ensureStandaloneBodies($this)', + 'ErrorBridge::ensureStandaloneBodies($this)', + ] as $keep) { + $this->assertStringContainsString($keep, $minimalBody, "keep {$keep} in minimal (#34695)"); + } + + // ensureFull must not re-add ObOutput before ValueEcho (ValueEcho → ObOutput). + $fullPos = strpos($context, 'private function ensureFullStandaloneBodies'); + $this->assertNotFalse($fullPos); + $fullEnd = strpos($context, 'private function ', $fullPos + 1); + $fullHead = false === $fullEnd + ? substr($context, $fullPos, 2500) + : substr($context, $fullPos, min(2500, $fullEnd - $fullPos)); + $this->assertStringContainsString('ValueEchoRuntime::ensureLinked($this)', $fullHead); + $this->assertStringNotContainsString( + 'ObOutputRuntime::ensureLinked($this)', + $fullHead, + 'ensureFull must not eagerly ObOutput before ValueEcho (#34695)' + ); + } + + public function testValueEchoHelperEnsuresBeforeObLookup(): void + { + $source = (string) file_get_contents(__DIR__.'/../../lib/JIT/ValueEchoHelper.php'); + $this->assertStringContainsString('#34695', $source); + foreach (['echoLiteral', 'echoNativeLong', 'echoStringVariable'] as $method) { + $pos = strpos($source, 'public static function '.$method); + $this->assertNotFalse($pos, $method); + $next = strpos($source, 'public static function ', $pos + 10); + $body = false === $next + ? substr($source, $pos) + : substr($source, $pos, $next - $pos); + $this->assertStringContainsString( + 'ObOutputRuntime::ensureLinked($context)', + $body, + $method.' must ensure ObOutput before __phpc_ob_echo_* (#34695)' + ); + } + } + + public function testNoNewRuntimeCForMinimalObOutputLazy(): void + { + $runtimeDir = dirname(__DIR__, 2).'/lib/AOT/runtime'; + $this->assertFileDoesNotExist( + $runtimeDir.'/ob_output.c', + 'must not add ob_output.c for #34695 — PHP JIT bridges only' + ); + } +} diff --git a/test/unit/ContextMinimalStandaloneLazyReturnPendingRuntimeShrinkTest.php b/test/unit/ContextMinimalStandaloneLazyReturnPendingRuntimeShrinkTest.php index 217a15a4e23..1142c1ebfda 100644 --- a/test/unit/ContextMinimalStandaloneLazyReturnPendingRuntimeShrinkTest.php +++ b/test/unit/ContextMinimalStandaloneLazyReturnPendingRuntimeShrinkTest.php @@ -30,11 +30,10 @@ public function testEnsureMinimalDropsEagerJitReturnPending(): void 'ensureMinimalUserStandaloneBodies must not eagerly JitReturnPending (#34621)' ); - // Essentials for thin echo / error / argv / getenv surface stay. + // Essentials for thin argv / getenv / bridges stay (#34695 dropped ObOutput; + // #34641 dropped StringTriggerError). // LastError dropped in #34631 (peer this test). foreach ([ - 'ObOutputRuntime::ensureLinked($this)', - 'StringTriggerError::ensureStandaloneBodies($this)', 'CliArgvRuntime::ensureStandaloneBodies($this)', 'EnvLocalRuntime::ensureLinked($this)', 'SuperglobalNameRuntime::ensureLinked($this)', @@ -43,6 +42,16 @@ public function testEnsureMinimalDropsEagerJitReturnPending(): void ] as $keep) { $this->assertStringContainsString($keep, $minimalBody, "keep {$keep} in minimal (#34621)"); } + $this->assertStringNotContainsString( + 'ObOutputRuntime::ensureLinked($this)', + $minimalBody, + 'ensureMinimal must not eagerly ObOutputRuntime (#34695)' + ); + $this->assertStringNotContainsString( + 'StringTriggerError::ensureStandaloneBodies($this)', + $minimalBody, + 'ensureMinimal must not eagerly StringTriggerError (#34641)' + ); $this->assertStringNotContainsString( 'LastErrorRuntime::ensureStandaloneBodies($this)', $minimalBody, diff --git a/test/unit/ContextMinimalStandaloneLazyStdlibRuntimeShrinkTest.php b/test/unit/ContextMinimalStandaloneLazyStdlibRuntimeShrinkTest.php index 4f730805836..151b72d3a18 100644 --- a/test/unit/ContextMinimalStandaloneLazyStdlibRuntimeShrinkTest.php +++ b/test/unit/ContextMinimalStandaloneLazyStdlibRuntimeShrinkTest.php @@ -49,16 +49,25 @@ public function testEnsureMinimalDropsEagerStdlibBatch(): void ); } - // Essentials for thin echo / error / argv / getenv surface stay. + // Essentials for thin argv / getenv surface stay (#34695 dropped ObOutput; + // #34641 dropped StringTriggerError). foreach ([ - 'ObOutputRuntime::ensureLinked($this)', - 'StringTriggerError::ensureStandaloneBodies($this)', 'CliArgvRuntime::ensureStandaloneBodies($this)', 'EnvLocalRuntime::ensureLinked($this)', 'SuperglobalNameRuntime::ensureLinked($this)', ] as $keep) { $this->assertStringContainsString($keep, $minimalBody, "keep {$keep} in minimal (#34578)"); } + $this->assertStringNotContainsString( + 'ObOutputRuntime::ensureLinked($this)', + $minimalBody, + 'ensureMinimal must not eagerly ObOutputRuntime (#34695)' + ); + $this->assertStringNotContainsString( + 'StringTriggerError::ensureStandaloneBodies($this)', + $minimalBody, + 'ensureMinimal must not eagerly StringTriggerError (#34641)' + ); } public function testCallSitesEnsureBeforeLookup(): void diff --git a/test/unit/ContextMinimalStandaloneLazyStringTriggerErrorShrinkTest.php b/test/unit/ContextMinimalStandaloneLazyStringTriggerErrorShrinkTest.php index 2e266f6bbe0..31915753f9b 100644 --- a/test/unit/ContextMinimalStandaloneLazyStringTriggerErrorShrinkTest.php +++ b/test/unit/ContextMinimalStandaloneLazyStringTriggerErrorShrinkTest.php @@ -30,10 +30,9 @@ public function testEnsureMinimalDropsEagerStringTriggerError(): void 'ensureMinimalUserStandaloneBodies must not eagerly StringTriggerError (#34641)' ); - // Essentials for thin echo / argv / getenv / bridges stay. + // Essentials for thin argv / getenv / bridges stay (#34695 dropped ObOutput; + // #34642 dropped StringHtmlspecialchars). foreach ([ - 'StringHtmlspecialchars::ensureStandaloneBodies($this)', - 'ObOutputRuntime::ensureLinked($this)', 'CliArgvRuntime::ensureStandaloneBodies($this)', 'EnvLocalRuntime::ensureLinked($this)', 'SuperglobalNameRuntime::ensureLinked($this)', @@ -42,6 +41,16 @@ public function testEnsureMinimalDropsEagerStringTriggerError(): void ] as $keep) { $this->assertStringContainsString($keep, $minimalBody, "keep {$keep} in minimal (#34641)"); } + $this->assertStringNotContainsString( + 'ObOutputRuntime::ensureLinked($this)', + $minimalBody, + 'ensureMinimal must not eagerly ObOutputRuntime (#34695)' + ); + $this->assertStringNotContainsString( + 'StringHtmlspecialchars::ensureStandaloneBodies($this)', + $minimalBody, + 'ensureMinimal must not eagerly StringHtmlspecialchars (#34642)' + ); // Full standalone still links StringTriggerError before AssertFail / LastError. $fullPos = strpos($context, 'private function ensureFullStandaloneBodies'); diff --git a/test/unit/ObOutputRuntimeShrinkTest.php b/test/unit/ObOutputRuntimeShrinkTest.php index 5ed93726143..90cd0a78737 100644 --- a/test/unit/ObOutputRuntimeShrinkTest.php +++ b/test/unit/ObOutputRuntimeShrinkTest.php @@ -75,10 +75,20 @@ public function testRuntimeOwnerDeclaresObAbiModuleLocally(): void ); } - public function testContextStillEnsureLinksObOutputForStandalone(): void + public function testContextDropsAlwaysOnObOutputForMinimalStandalone(): void { $ctx = (string) file_get_contents(__DIR__.'/../../lib/JIT/Context.php'); - $this->assertStringContainsString('ObOutputRuntime::ensureLinked($this)', $ctx); + $this->assertStringContainsString('#34695', $ctx); + $minimalPos = strpos($ctx, 'private function ensureMinimalUserStandaloneBodies'); + $this->assertNotFalse($minimalPos); + $minimalEnd = strpos($ctx, 'private function ensureBootstrapAotStandaloneBodies', $minimalPos); + $this->assertNotFalse($minimalEnd); + $minimalBody = substr($ctx, $minimalPos, $minimalEnd - $minimalPos); + $this->assertStringNotContainsString( + 'ObOutputRuntime::ensureLinked($this)', + $minimalBody, + 'ensureMinimal must not eagerly ObOutput (#34695 / peer #33862)' + ); } public function testNoRuntimeCForObOutputAbi(): void From 6f2153bc0012439cc45462b7e764b35174f48bfb Mon Sep 17 00:00:00 2001 From: PurHur Date: Tue, 25 Aug 2026 13:07:05 +0000 Subject: [PATCH 2/2] AOT: ensure ObOutput before bare ECHO string/bool paths (#34695) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Concat smoke failed looking up __phpc_ob_echo_substr — JIT.php ECHO TYPE_STRING/NATIVE_BOOL bypassed ValueEchoHelper. Co-authored-by: Cursor --- lib/JIT.php | 3 +++ ...alStandaloneLazyObOutputRuntimeShrinkTest.php | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/JIT.php b/lib/JIT.php index 9cdd719d138..889aa98004f 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -11275,6 +11275,8 @@ private function compileBlockInternal( ); break; case Variable::TYPE_STRING: + // Lazy ob_* — bare __phpc_ob_echo_* lookups below (#34695). + JIT\Builtin\ObOutputRuntime::ensureLinked($this->context); if ($arg->kind === Variable::KIND_VALUE && 'i8*' === $this->context->getStringFromType($arg->value->typeOf()) ) { @@ -11329,6 +11331,7 @@ private function compileBlockInternal( ); break; case Variable::TYPE_NATIVE_BOOL: + JIT\Builtin\ObOutputRuntime::ensureLinked($this->context); $boolVal = $this->context->helper->loadValue($arg); $charPtr = $this->context->getTypeFromString('char*'); $trueBlock = JIT\BasicBlockHelper::append($this->context, 'echo_bool_true'); diff --git a/test/unit/ContextMinimalStandaloneLazyObOutputRuntimeShrinkTest.php b/test/unit/ContextMinimalStandaloneLazyObOutputRuntimeShrinkTest.php index dbb9d2a76e2..e24d3c7a3f2 100644 --- a/test/unit/ContextMinimalStandaloneLazyObOutputRuntimeShrinkTest.php +++ b/test/unit/ContextMinimalStandaloneLazyObOutputRuntimeShrinkTest.php @@ -75,6 +75,22 @@ public function testValueEchoHelperEnsuresBeforeObLookup(): void } } + public function testJitEchoOpcodeEnsuresBeforeBareObLookup(): void + { + $jit = (string) file_get_contents(__DIR__.'/../../lib/JIT.php'); + // Concat/string echo uses bare __phpc_ob_echo_substr in the ECHO opcode switch (#34695). + $this->assertMatchesRegularExpression( + '/case Variable::TYPE_STRING:\s*\/\/ Lazy ob_\* — bare __phpc_ob_echo_\* lookups below \(#34695\)\.\s*JIT\\\\Builtin\\\\ObOutputRuntime::ensureLinked/s', + $jit, + 'JIT ECHO TYPE_STRING must ensureLinked before bare __phpc_ob_echo_* (#34695)' + ); + $this->assertMatchesRegularExpression( + '/case Variable::TYPE_NATIVE_BOOL:\s*JIT\\\\Builtin\\\\ObOutputRuntime::ensureLinked/s', + $jit, + 'JIT ECHO TYPE_NATIVE_BOOL must ensureLinked before bare __phpc_ob_echo_cstr (#34695)' + ); + } + public function testNoNewRuntimeCForMinimalObOutputLazy(): void { $runtimeDir = dirname(__DIR__, 2).'/lib/AOT/runtime';