Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand.
| `str_ends_with` | yes | yes | yes | standard | AOT PHPT |
| `str_pad` | yes | yes | yes | standard | AOT PHPT |
| `str_repeat` | yes | yes | yes | standard | AOT PHPT |
| `str_replace` | yes | no | no | standard | doc: VM only; not implemented for JIT in this compiler build |
| `str_replace` | yes | yes | yes | standard | JIT PHPT; AOT PHPT |
| `str_split` | yes | no | no | standard | doc: VM only; not implemented for JIT in this compiler build |
| `str_starts_with` | yes | yes | yes | standard | AOT PHPT |
| `strcmp` | yes | yes | yes | standard | |
Expand Down
92 changes: 92 additions & 0 deletions ext/standard/JitStrReplace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

/**
* LLVM JIT/AOT helper for str_replace() — strstr loop with slice + concat (byte-safe subset).
*/

namespace PHPCompiler\ext\standard;

use PHPCompiler\JIT\BasicBlockHelper;
use PHPCompiler\JIT\Context;
use PHPLLVM\Builder;
use PHPLLVM\Value;

final class JitStrReplace
{
public static function replace(Context $context, Value $search, Value $replace, Value $subject): Value
{
$map = $context->structFieldMap['__string__'];
$searchLen = $context->builder->load(
$context->builder->structGep($search, $map['length'])
);
$subjectLen = $context->builder->load(
$context->builder->structGep($subject, $map['length'])
);
$subjectPtr = $context->builder->structGep($subject, $map['value']);
$searchPtr = $context->builder->structGep($search, $map['value']);

$i64 = $context->getTypeFromString('int64');
$zero = $i64->constInt(0, false);

$resultSlot = $context->builder->alloca(
$context->getTypeFromString('__string__*'),
1,
'str_replace_result'
);
$offsetSlot = $context->builder->alloca($i64, 1, 'str_replace_offset');
$emptyResult = $context->builder->call($context->lookupFunction('__string__alloc'), $zero);
$context->builder->store($emptyResult, $resultSlot);
$context->builder->store($zero, $offsetSlot);

$loopHead = BasicBlockHelper::append($context, 'str_replace_head');
$loopBody = BasicBlockHelper::append($context, 'str_replace_body');
$tailBlock = BasicBlockHelper::append($context, 'str_replace_tail');
$doneBlock = BasicBlockHelper::append($context, 'str_replace_done');
$context->builder->branch($loopHead);

$context->builder->positionAtEnd($loopHead);
$offset = $context->builder->load($offsetSlot);
$pastEnd = $context->builder->icmp(Builder::INT_SGE, $offset, $subjectLen);
$context->builder->branchIf($pastEnd, $doneBlock, $loopBody);

$context->builder->positionAtEnd($loopBody);
$searchFrom = $context->builder->gep($subjectPtr, $offset);
$found = $context->builder->call(
$context->lookupFunction('strstr'),
$searchFrom,
$searchPtr
);
$null = $context->getTypeFromString('int8*')->constNull();
$notFound = $context->builder->icmp(Builder::INT_EQ, $found, $null);
$matchBlock = BasicBlockHelper::append($context, 'str_replace_match');
$context->builder->branchIf($notFound, $tailBlock, $matchBlock);

$context->builder->positionAtEnd($matchBlock);
$foundInt = $context->builder->ptrToInt($found, $i64);
$baseInt = $context->builder->ptrToInt($subjectPtr, $i64);
$pos = $context->builder->sub($foundInt, $baseInt);
$prefixLen = $context->builder->sub($pos, $offset);
$prefix = string_trim::jitCopySlice($context, $subject, $subjectPtr, $offset, $prefixLen);
$acc = $context->builder->load($resultSlot);
$withPrefix = JitStringConcat::concat($context, $acc, $prefix);
$withReplace = JitStringConcat::concat($context, $withPrefix, $replace);
$context->builder->store($withReplace, $resultSlot);
$newOffset = $context->builder->add($pos, $searchLen);
$context->builder->store($newOffset, $offsetSlot);
$context->builder->branch($loopHead);

$context->builder->positionAtEnd($tailBlock);
$offset = $context->builder->load($offsetSlot);
$tailLen = $context->builder->sub($subjectLen, $offset);
$tail = string_trim::jitCopySlice($context, $subject, $subjectPtr, $offset, $tailLen);
$acc = $context->builder->load($resultSlot);
$context->builder->store(JitStringConcat::concat($context, $acc, $tail), $resultSlot);
$context->builder->branch($doneBlock);

$context->builder->positionAtEnd($doneBlock);

return $context->builder->load($resultSlot);
}
}
34 changes: 32 additions & 2 deletions ext/standard/str_replace.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
use PHPLLVM\Value;

/**
* str_replace() with string search, replace, and subject (subset of PHP; VM only).
* str_replace() with string search, replace, and subject (subset of PHP; LLVM JIT/AOT).
*/
final class str_replace extends Internal
{
Expand Down Expand Up @@ -50,6 +50,36 @@ public function execute(Frame $frame): void

public function call(Context $context, JITVariable ...$args): Value
{
throw new \LogicException('str_replace() is not implemented for JIT in this compiler build');
$this->context = $context;
if (3 !== \count($args)) {
throw new \LogicException('str_replace() requires exactly three arguments in this compiler build');
}
foreach ($args as $arg) {
if (JITVariable::TYPE_STRING !== $arg->type && JITVariable::TYPE_VALUE !== $arg->type) {
throw new \LogicException('str_replace() requires string arguments in this compiler build');
}
}

return JitStrReplace::replace(
$context,
self::jitStringArg($context, $args[0]),
self::jitStringArg($context, $args[1]),
self::jitStringArg($context, $args[2])
);
}

private static function jitStringArg(Context $context, JITVariable $arg): Value
{
if (JITVariable::TYPE_STRING === $arg->type) {
return $context->helper->loadValue($arg);
}
if (JITVariable::TYPE_VALUE === $arg->type) {
return $context->builder->call(
$context->lookupFunction('__value__readString'),
$arg->value
);
}

throw new \LogicException('str_replace() requires string arguments in this compiler build');
}
}
11 changes: 11 additions & 0 deletions test/compliance/cases/stdlib/str_replace_jit.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--TEST--
stdlib str_replace() JIT
--FILE--
<?php
echo str_replace('o', '0', 'foo'), "\n";
echo str_replace('ab', 'X', 'cab abc'), "\n";
echo str_replace('z', '', 'no match'), "\n";
--EXPECT--
f00
cX Xc
no match
11 changes: 11 additions & 0 deletions test/fixtures/aot/cases/str_replace.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--TEST--
AOT: str_replace() for string search/replace/subject (LLVM)
--FILE--
<?php
echo str_replace('o', '0', 'foo'), "\n";
echo str_replace('ab', 'X', 'cab abc'), "\n";
echo str_replace('z', '', 'no match'), "\n";
--EXPECT--
f00
cX Xc
no match
Loading