fix(compiler):注入finally块到return路径并修复属性注册

pull/13/head
韩天峰 2 months ago
parent b6d0488ea6
commit c94be9c6a9
  1. 78
      src/Php/CompilerBase.php
  2. 12
      src/Php/Translator.php
  3. 24
      src/gen_stub.php
  4. 32
      tests/aot/anon_class/constructor-state-method.phpt
  5. 40
      tests/aot/attribute/function-attributes.phpt
  6. 52
      tests/aot/attribute/parameter-property-attributes.phpt
  7. 27
      tests/aot/basic/unset-dim-side-effects.phpt
  8. 43
      tests/aot/dynamic_call/new-dynamic-unpack-call-order.phpt
  9. 36
      tests/aot/empty/isset-empty-dim-side-effects.phpt
  10. 29
      tests/aot/exception/finally-catch-return.phpt
  11. 33
      tests/aot/exception/finally-nested-return.phpt
  12. 45
      tests/aot/exception/finally-return-throw-side-effects.phpt
  13. 31
      tests/aot/functions/array-push-unpack-side-effects.phpt
  14. 24
      tests/aot/functions/preg-match-output-ref-reuse.phpt

@ -6444,14 +6444,14 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
{
$code = $this->parseBeforeStmtLines() . PHP_EOL;
$code .= 'try {';
$stmts = $v->stmts;
$finally = $v->finally;
$stmts = $finally ? $this->injectFinallyBeforeReturn($v->stmts, $finally->stmts) : $v->stmts;
$code .= PHP_EOL;
$code .= $this->parseBlockStmts($stmts);
$code .= $this->getIndent() . '}' . PHP_EOL;
$catches = $v->catches;
$finally = $v->finally;
$exVar = $this->genTmpVarName();
$this->addLocalVar($exVar, self::TYPE_VAR);
@ -6461,7 +6461,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if ($catches) {
$this->indentLevel++;
foreach ($catches as $catch) {
$code .= $this->parseCatch($catch, $exVar);
$code .= $this->parseCatch($catch, $exVar, $finally?->stmts ?? []);
}
$this->indentLevel--;
}
@ -6476,7 +6476,74 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
return $code;
}
protected function parseCatch(Node\Stmt\Catch_ $catch, string $exVar): string
protected function injectFinallyBeforeReturn(array $stmts, array $finallyStmts): array
{
$result = [];
foreach ($stmts as $stmt) {
if ($stmt instanceof Node\Stmt\Return_) {
if ($stmt->expr) {
$tmpVar = $this->addTmpVar(self::TYPE_VAR);
$result[] = new Node\Stmt\Expression(new Expr\Assign(new Variable($tmpVar), $stmt->expr));
array_push($result, ...$this->cloneStmtList($finallyStmts));
$result[] = new Node\Stmt\Return_(new Variable($tmpVar));
continue;
}
array_push($result, ...$this->cloneStmtList($finallyStmts));
$result[] = $stmt;
continue;
}
$result[] = $this->injectFinallyBeforeReturnInStmt($stmt, $finallyStmts);
}
return $result;
}
protected function injectFinallyBeforeReturnInStmt(Node\Stmt $stmt, array $finallyStmts): Node\Stmt
{
if ($stmt instanceof Node\Stmt\If_) {
$stmt = clone $stmt;
$stmt->stmts = $this->injectFinallyBeforeReturn($stmt->stmts, $finallyStmts);
foreach ($stmt->elseifs as $index => $elseIf) {
$elseIf = clone $elseIf;
$elseIf->stmts = $this->injectFinallyBeforeReturn($elseIf->stmts, $finallyStmts);
$stmt->elseifs[$index] = $elseIf;
}
if ($stmt->else) {
$stmt->else = clone $stmt->else;
$stmt->else->stmts = $this->injectFinallyBeforeReturn($stmt->else->stmts, $finallyStmts);
}
return $stmt;
}
if ($stmt instanceof Node\Stmt\For_
|| $stmt instanceof Node\Stmt\Foreach_
|| $stmt instanceof Node\Stmt\While_
|| $stmt instanceof Node\Stmt\Do_
) {
$stmt = clone $stmt;
$stmt->stmts = $this->injectFinallyBeforeReturn($stmt->stmts, $finallyStmts);
return $stmt;
}
if ($stmt instanceof Node\Stmt\Switch_) {
$stmt = clone $stmt;
foreach ($stmt->cases as $index => $case) {
$case = clone $case;
$case->stmts = $this->injectFinallyBeforeReturn($case->stmts, $finallyStmts);
$stmt->cases[$index] = $case;
}
return $stmt;
}
return $stmt;
}
protected function cloneStmtList(array $stmts): array
{
return array_map(static fn (Node\Stmt $stmt): Node\Stmt => clone $stmt, $stmts);
}
protected function parseCatch(Node\Stmt\Catch_ $catch, string $exVar, array $finallyStmts = []): string
{
$types = $catch->types;
$var = $catch->var ? $this->parseIdentifier($catch->var) : $this->genTmpVarName();
@ -6503,7 +6570,8 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$code .= ') {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . "{$exVar} = php::null;" . PHP_EOL;
$code .= $this->parseStmts($catch->stmts);
$stmts = $finallyStmts ? $this->injectFinallyBeforeReturn($catch->stmts, $finallyStmts) : $catch->stmts;
$code .= $this->parseStmts($stmts);
$this->indentLevel--;
$code .= $this->getIndent() . '}';

@ -2503,11 +2503,13 @@ CODE;
$this->climate->info('generate arginfo file: ' . $this->getRelativePath($file));
generateStubFile($file, $this->getIncludeDir() . '/' . $headerFile, true);
if ($this->useRegisterSymbolsFn) {
preg_match('/php_(.*)_arginfo.h/', $headerFile, $matches);
$registerSymbolFn = 'register_' . $matches[1] . '_symbols';
$registerSymbol = PHP_EOL . 'static void ' . $registerSymbolFn . '(int module_number)' . PHP_EOL;
if (str_contains(file_get_contents($this->getBuildDir() . '/include/' . $headerFile), $registerSymbol)) {
$headerCode = file_get_contents($this->getBuildDir() . '/include/' . $headerFile);
$needsAttributeSymbols = str_contains($headerCode, 'zend_add_function_attribute(')
|| str_contains($headerCode, 'zend_add_parameter_attribute(')
|| str_contains($headerCode, 'zend_add_global_constant_attribute(');
if ($this->useRegisterSymbolsFn || $needsAttributeSymbols) {
if (preg_match('/\bstatic\s+void\s+(register_[A-Za-z0-9_]+_symbols)\s*\(\s*int\s+module_number\s*\)/', $headerCode, $matches)) {
$registerSymbolFn = $matches[1];
$this->registerSymbols[] = $registerSymbolFn;
}
}

@ -5467,20 +5467,9 @@ function generateArgInfoCode(
}
}
if ($attributeInitializationCode !== "" || !empty($fileInfo->constInfos)) {
if ($attributeInitializationCode !== "") {
$code .= "\nstatic void register_{$stubFilenameWithoutExtension}_symbols(int module_number)\n";
$code .= "{\n";
$code .= generateCodeWithConditions(
$fileInfo->constInfos,
'',
static fn (ConstInfo $constInfo): string => $constInfo->getDeclaration($allConstInfos)
);
if ($attributeInitializationCode !== "" && $fileInfo->constInfos) {
$code .= "\n";
}
$code .= $attributeInitializationCode;
$code .= "}\n";
}
@ -5550,8 +5539,9 @@ function generateFunctionAttributeInitialization(iterable $funcInfos, array $all
}
foreach ($funcInfo->attributes as $key => $attribute) {
$functionLookup = "(zend_function *) zend_hash_str_find_ptr($functionTable, \"" . $funcInfo->name->getNameForAttributes() . "\", sizeof(\"" . $funcInfo->name->getNameForAttributes() . "\") - 1)";
$code .= $attribute->generateCode(
"zend_add_function_attribute(zend_hash_str_find_ptr($functionTable, \"" . $funcInfo->name->getNameForAttributes() . "\", sizeof(\"" . $funcInfo->name->getNameForAttributes() . "\") - 1)",
"zend_add_function_attribute($functionLookup",
"func_" . $funcInfo->name->getNameForAttributes() . "_$key",
$allConstInfos,
$phpVersionIdMinimumCompatibility,
@ -5561,8 +5551,9 @@ function generateFunctionAttributeInitialization(iterable $funcInfos, array $all
foreach ($funcInfo->args as $index => $arg) {
foreach ($arg->attributes as $key => $attribute) {
$functionLookup = "(zend_function *) zend_hash_str_find_ptr($functionTable, \"" . $funcInfo->name->getNameForAttributes() . "\", sizeof(\"" . $funcInfo->name->getNameForAttributes() . "\") - 1)";
$code .= $attribute->generateCode(
"zend_add_parameter_attribute(zend_hash_str_find_ptr($functionTable, \"" . $funcInfo->name->getNameForAttributes() . "\", sizeof(\"" . $funcInfo->name->getNameForAttributes() . "\") - 1), $index",
"zend_add_parameter_attribute($functionLookup, $index",
"func_{$funcInfo->name->getNameForAttributes()}_arg{$index}_$key",
$allConstInfos,
$phpVersionIdMinimumCompatibility,
@ -5615,12 +5606,7 @@ function generateGlobalConstantAttributeInitialization(
$constName = str_replace('\\', '\\\\', $constInfo->name->__toString());
$constVarName = 'const_' . $constName;
// The entire attribute block will be conditional if PHP < 8.5 is
// supported, but also if PHP < 8.5 is supported we need to search
// for the constant; see GH-19029
if ($isConditional) {
$code .= "\tzend_constant *$constVarName = zend_hash_str_find_ptr(EG(zend_constants), \"" . $constName . "\", sizeof(\"" . $constName . "\") - 1);\n";
}
foreach ($constInfo->attributes as $key => $attribute) {
$code .= $attribute->generateCode(
"zend_add_global_constant_attribute($constVarName",

@ -0,0 +1,32 @@
--TEST--
anonymous class stores constructor state and uses it in methods
--FILE--
<?php
abstract class AnonStateVisitor
{
abstract public function visit(string $name): string;
}
function main(): void
{
$prefixes = ['node' => 'Node', 'leaf' => 'Leaf'];
$visitor = new class($prefixes) extends AnonStateVisitor {
public function __construct(private array $prefixes)
{
}
public function visit(string $name): string
{
return ($this->prefixes[$name] ?? 'Unknown') . ':' . $name;
}
};
var_dump($visitor->visit('node'));
var_dump($visitor->visit('missing'));
}
?>
--EXPECT--
string(9) "Node:node"
string(15) "Unknown:missing"

@ -0,0 +1,40 @@
--TEST--
function attributes are available through reflection
--FILE--
<?php
#[Attribute(Attribute::TARGET_FUNCTION)]
class AotFunctionMeta
{
public function __construct(public string $name, public int $priority = 0)
{
}
}
#[AotFunctionMeta('handler', 10)]
function attributed_function(): string
{
return 'ok';
}
function main(): void
{
$func = new ReflectionFunction('attributed_function');
$attrs = $func->getAttributes(AotFunctionMeta::class);
var_dump($attrs[0]->getName());
var_dump($attrs[0]->getArguments());
var_dump($attrs[0]->newInstance()->name);
var_dump(attributed_function());
}
?>
--EXPECT--
string(15) "AotFunctionMeta"
array(2) {
[0]=>
string(7) "handler"
[1]=>
int(10)
}
string(7) "handler"
string(2) "ok"

@ -0,0 +1,52 @@
--TEST--
function parameter and property attributes are available through reflection
--FILE--
<?php
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
class AotMeta
{
public function __construct(public string $name, public int $order = 0)
{
}
}
class AttributeHolder
{
#[AotMeta('property', 1)]
public string $value = 'ok';
}
function attributed_parameter(#[AotMeta('parameter', 2)] string $value): string
{
return $value;
}
function main(): void
{
$func = new ReflectionFunction('attributed_parameter');
$paramAttrs = $func->getParameters()[0]->getAttributes(AotMeta::class);
var_dump($paramAttrs[0]->getName());
var_dump($paramAttrs[0]->getArguments());
$prop = new ReflectionProperty(AttributeHolder::class, 'value');
$propAttrs = $prop->getAttributes(AotMeta::class);
var_dump($propAttrs[0]->getName());
var_dump($propAttrs[0]->getArguments());
}
?>
--EXPECT--
string(7) "AotMeta"
array(2) {
[0]=>
string(9) "parameter"
[1]=>
int(2)
}
string(7) "AotMeta"
array(2) {
[0]=>
string(8) "property"
[1]=>
int(1)
}

@ -0,0 +1,27 @@
--TEST--
unset evaluates array dimension expressions left to right
--FILE--
<?php
function unset_key(string $key): string
{
echo "unset-key:$key\n";
return $key;
}
function main(): void
{
$items = ['a' => 1, 'b' => 2, 'c' => 3];
unset($items[unset_key('a')], $items[unset_key('c')]);
var_dump($items);
}
?>
--EXPECT--
unset-key:a
unset-key:c
array(1) {
["b"]=>
int(2)
}

@ -0,0 +1,43 @@
--TEST--
dynamic new with unpacked constructor args then method call preserves order
--FILE--
<?php
class DynamicNewOrder
{
public function __construct(public string $name)
{
echo "ctor:$name\n";
}
public function run(string $suffix): string
{
echo "run:$suffix\n";
return $this->name . ':' . $suffix;
}
}
function make_new_args(): array
{
echo "new-args\n";
return ['object'];
}
function make_call_arg(): string
{
echo "call-arg\n";
return 'method';
}
function main(): void
{
$class = DynamicNewOrder::class;
var_dump((new $class(...make_new_args()))->run(make_call_arg()));
}
?>
--EXPECT--
new-args
ctor:object
call-arg
run:method
string(13) "object:method"

@ -0,0 +1,36 @@
--TEST--
isset and empty evaluate array dimension expressions in order
--FILE--
<?php
function dim_key(string $key): string
{
echo "key:$key\n";
return $key;
}
function main(): void
{
$data = [
'user' => ['name' => 'Alice'],
'zero' => 0,
];
var_dump(isset($data[dim_key('user')][dim_key('name')]));
var_dump(isset($data[dim_key('missing')][dim_key('nested')]));
var_dump(empty($data[dim_key('zero')]));
var_dump(empty($data[dim_key('missing')][dim_key('empty-nested')]));
}
?>
--EXPECT--
key:user
key:name
bool(true)
key:missing
key:nested
bool(false)
key:zero
bool(true)
key:missing
key:empty-nested
bool(true)

@ -0,0 +1,29 @@
--TEST--
finally runs for return inside catch without changing return value
--FILE--
<?php
class FinallyCatchReturnException extends Exception {}
function catch_finally_return(): string
{
$state = "before";
try {
throw new FinallyCatchReturnException("failure");
} catch (FinallyCatchReturnException $e) {
$state = "catch:" . $e->getMessage();
return $state;
} finally {
echo "finally:$state\n";
$state = "finally";
}
}
function main(): void
{
var_dump(catch_finally_return());
}
?>
--EXPECT--
finally:catch:failure
string(13) "catch:failure"

@ -0,0 +1,33 @@
--TEST--
finally runs for return nested in if/else branches without changing return value
--FILE--
<?php
function nested_finally_return(int $value): string
{
$state = "start";
try {
if ($value > 0) {
$state .= ":positive";
return $state;
} else {
$state .= ":negative";
return $state;
}
} finally {
echo "finally:$state\n";
$state .= ":finally";
}
}
function main(): void
{
var_dump(nested_finally_return(1));
var_dump(nested_finally_return(-1));
}
?>
--EXPECT--
finally:start:positive
string(14) "start:positive"
finally:start:negative
string(14) "start:negative"

@ -0,0 +1,45 @@
--TEST--
finally side effects run before return and throw leave the frame
--FILE--
<?php
class FinallySideEffectException extends Exception {}
function finally_return_case(): string
{
try {
echo "try-return\n";
return "returned";
} finally {
echo "finally-return\n";
}
}
function finally_throw_case(): void
{
try {
echo "try-throw\n";
throw new FinallySideEffectException("thrown");
} finally {
echo "finally-throw\n";
}
}
function main(): void
{
var_dump(finally_return_case());
try {
finally_throw_case();
} catch (FinallySideEffectException $e) {
var_dump($e->getMessage());
}
}
?>
--EXPECT--
try-return
finally-return
string(8) "returned"
try-throw
finally-throw
string(6) "thrown"

@ -0,0 +1,31 @@
--TEST--
array_push with unpacked values mutates first argument once
--FILE--
<?php
function make_push_values(): array
{
echo "make-values\n";
return [2, 3];
}
function main(): void
{
$items = [1];
$count = array_push($items, ...make_push_values());
var_dump($count);
var_dump($items);
}
?>
--EXPECT--
make-values
int(3)
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}

@ -0,0 +1,24 @@
--TEST--
preg_match output array can be reused through references
--FILE--
<?php
function main(): void
{
preg_match('/(foo)(bar)/', 'foobar', $matches);
$first =& $matches[1];
$first = strtoupper($first);
var_dump($matches);
}
?>
--EXPECT--
array(3) {
[0]=>
string(6) "foobar"
[1]=>
&string(3) "FOO"
[2]=>
string(3) "bar"
}
Loading…
Cancel
Save