diff --git a/ext/standard/JitDateMutation.php b/ext/standard/JitDateMutation.php index 340367e1679..22fc63c7dc5 100644 --- a/ext/standard/JitDateMutation.php +++ b/ext/standard/JitDateMutation.php @@ -728,6 +728,14 @@ private static function invokeObjectIntervalMutationForFunction( DateTimeSupport::MICROSECOND_PROPERTY, $i64->constInt($updated['microsecond'], true) ); + // format()/getTimestamp() prefer compileTimeDateTimeTimestamp (#33911). + // Refresh the stamp + dateTimeLocalInstants or the mutation is a silent no-op. + self::publishMutableDateTimeInstant( + $context, + $args[0], + $updated['timestamp'], + $instant['timezone'] + ); } return self::boxObjectPtr($context, $dtObj); @@ -787,12 +795,19 @@ private static function invokeObjectIntervalMutationForFunction( ); } self::writeLongProp($context, $object, $dtObj, $layout, DateTimeSupport::TS_PROPERTY, $newTs); + if (!$immutable) { + // Runtime delta — drop stale construct stamp so format reads the slot (#33911). + self::invalidateMutableDateTimeInstant($context, $args[0]); + } return self::boxObjectPtr($context, $dtObj); } if (null !== $intervalState) { self::applyKnownIntervalStateViaLlvm($context, $object, $dtObj, $intervalState, $layout, $add); + if (!$immutable) { + self::invalidateMutableDateTimeInstant($context, $args[0]); + } return self::boxObjectPtr($context, $dtObj); } @@ -809,10 +824,52 @@ private static function invokeObjectIntervalMutationForFunction( DateMutationRuntime::ensureLinked($context); self::applyIntervalToDateTimeObject($context, $dtObj, $intervalObj, $layout, $add); } + if (!$immutable) { + self::invalidateMutableDateTimeInstant($context, $args[0]); + } return self::boxObjectPtr($context, $dtObj); } + /** + * Keep format()/getTimestamp() compile-time stamps in sync after mutable add/sub (#33911). + */ + private static function publishMutableDateTimeInstant( + Context $context, + JITVariable $receiver, + int $timestamp, + string $timezone + ): void { + $receiver->compileTimeDateTimeTimestamp = $timestamp; + $receiver->compileTimeTimezoneName = $timezone; + foreach ($context->namedVariableBindings as $boundName => $bound) { + if ($bound !== $receiver) { + continue; + } + $bound->compileTimeDateTimeTimestamp = $timestamp; + $bound->compileTimeTimezoneName = $timezone; + $context->dateTimeLocalInstants[$boundName] = [ + 'timestamp' => $timestamp, + 'timezone' => $timezone, + ]; + } + } + + /** + * Drop construct-time stamps after a runtime LLVM add/sub so reads use __dt_timestamp (#33911). + */ + private static function invalidateMutableDateTimeInstant(Context $context, JITVariable $receiver): void + { + $receiver->compileTimeDateTimeTimestamp = null; + foreach ($context->namedVariableBindings as $boundName => $bound) { + if ($bound !== $receiver) { + continue; + } + $bound->compileTimeDateTimeTimestamp = null; + unset($context->dateTimeLocalInstants[$boundName]); + } + } + private static function boxObjectPtr(Context $context, Value $dtObj): Value { $ret = JitValueBox::alloc($context); diff --git a/test/repro/issue_33911_datetime_add_gettimestamp.php b/test/repro/issue_33911_datetime_add_gettimestamp.php new file mode 100644 index 00000000000..6692d14458a --- /dev/null +++ b/test/repro/issue_33911_datetime_add_gettimestamp.php @@ -0,0 +1,6 @@ +getTimestamp(), "\n"; +$d->add(new DateInterval('P1M')); +echo 'after_ts=', $d->getTimestamp(), "\n"; +echo 'after_fmt=', $d->format('Y-m-d'), "\n"; diff --git a/test/repro/issue_33911_datetime_add_sub_p1m.php b/test/repro/issue_33911_datetime_add_sub_p1m.php new file mode 100644 index 00000000000..2a0d2b03d4f --- /dev/null +++ b/test/repro/issue_33911_datetime_add_sub_p1m.php @@ -0,0 +1,13 @@ +add(new DateInterval('P1M')); +echo 'add=', $d->format('Y-m-d'), "\n"; + +$d2 = new DateTime('2020-03-15'); +$d2->sub(new DateInterval('P1M')); +echo 'sub=', $d2->format('Y-m-d'), "\n"; + +$i = DateInterval::createFromDateString('1 day + 2 hours'); +$dt = new DateTime('2020-01-01 00:00:00'); +$dt->add($i); +echo 'cfd=', $dt->format('Y-m-d H:i'), "\n"; diff --git a/test/unit/Issue33911DateTimeAddSubStampAotTest.php b/test/unit/Issue33911DateTimeAddSubStampAotTest.php new file mode 100644 index 00000000000..cdbba0dac26 --- /dev/null +++ b/test/unit/Issue33911DateTimeAddSubStampAotTest.php @@ -0,0 +1,86 @@ +assertStringContainsString('publishMutableDateTimeInstant', $source); + $this->assertStringContainsString('invalidateMutableDateTimeInstant', $source); + $this->assertStringContainsString('#33911', $source); + } + + public function testAotAddSubP1mMatchesZend(): void + { + if (!\PHPCompiler\LlvmToolchain::hasLibrary(dirname(__DIR__, 2))) { + $this->markTestSkipped('LLVM 9 toolchain not available'); + } + $root = dirname(__DIR__, 2); + $src = $root.'/test/repro/issue_33911_datetime_add_sub_p1m.php'; + $bin = sys_get_temp_dir().'/phpc_addsub_33911_'.getmypid().'.bin'; + $compile = 'PHP_COMPILER_HELPER_RUNTIME_O=0 '.escapeshellarg(PHP_BINARY).' ' + .escapeshellarg($root.'/bin/compile.php') + .' -o '.escapeshellarg($bin).' '.escapeshellarg($src).' 2>&1'; + exec($compile, $compileOut, $compileRc); + $this->assertSame(0, $compileRc, implode("\n", $compileOut)); + + $zendOut = []; + exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($src).' 2>&1', $zendOut, $zendRc); + $this->assertSame(0, $zendRc, implode("\n", $zendOut)); + $expected = implode("\n", $zendOut)."\n"; + + try { + for ($i = 0; $i < 5; ++$i) { + $runOut = []; + exec(escapeshellarg($bin).' 2>&1', $runOut, $runRc); + $this->assertSame(0, $runRc, 'run '.($i + 1).': '.implode("\n", $runOut)); + $this->assertSame($expected, implode("\n", $runOut)."\n", 'run '.($i + 1)); + } + } finally { + @unlink($bin); + } + } + + public function testAotAddGetTimestampMatchesZend(): void + { + if (!\PHPCompiler\LlvmToolchain::hasLibrary(dirname(__DIR__, 2))) { + $this->markTestSkipped('LLVM 9 toolchain not available'); + } + $root = dirname(__DIR__, 2); + $src = $root.'/test/repro/issue_33911_datetime_add_gettimestamp.php'; + $bin = sys_get_temp_dir().'/phpc_addts_33911_'.getmypid().'.bin'; + $compile = 'PHP_COMPILER_HELPER_RUNTIME_O=0 '.escapeshellarg(PHP_BINARY).' ' + .escapeshellarg($root.'/bin/compile.php') + .' -o '.escapeshellarg($bin).' '.escapeshellarg($src).' 2>&1'; + exec($compile, $compileOut, $compileRc); + $this->assertSame(0, $compileRc, implode("\n", $compileOut)); + + $zendOut = []; + exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($src).' 2>&1', $zendOut, $zendRc); + $this->assertSame(0, $zendRc, implode("\n", $zendOut)); + $expected = implode("\n", $zendOut)."\n"; + + try { + $runOut = []; + exec(escapeshellarg($bin).' 2>&1', $runOut, $runRc); + $this->assertSame(0, $runRc, implode("\n", $runOut)); + $this->assertSame($expected, implode("\n", $runOut)."\n"); + } finally { + @unlink($bin); + } + } +}