feat(php): 添加数组初始化计划支持并修复构造函数提升属性默认值处理

- 引入 ArrayInitPlan 实体类用于处理数组初始化计划
- 修复构造函数参数提升属性的默认值归属问题
- 为数组类型的参数默认值添加运行时初始化支持
- 实现数组初始化计划的构建和包装逻辑
- 添加默认参数辅助函数生成功能
- 重构静态属性和实例属性的初始化处理逻辑
- 添加 readonly 类测试用例以验证功能正确性
pull/2/head
韩天峰 2 months ago
parent 5ed79e9451
commit 0dc846a4fc
  1. 2
      src/Php/ArgInfo.php
  2. 28
      src/Php/Entity/ArrayInitPlan.php
  3. 3
      src/Php/Entity/PropertyDef.php
  4. 74
      src/Php/Preprocessor.php
  5. 129
      src/Php/Translator.php
  6. 4
      src/gen_stub.php
  7. 38
      tests/aot/class/readonly-2.phpt

@ -8,6 +8,7 @@
namespace PhpAot\Php;
use PhpAot\Php\Entity\ArrayInitPlan;
use PhpParser\Node\Expr;
use PhpParser\NodeAbstract;
@ -16,6 +17,7 @@ class ArgInfo
public string $name;
public string $type;
public string $default = '';
public ?ArrayInitPlan $arrayInitPlan = null;
public ?Expr $defaultValue = null;
public string $class = '';
public bool $byRef = false;

@ -0,0 +1,28 @@
<?php
/**
* This file is part of Swoole-Compiler(AOT).
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace PhpAot\Php\Entity;
class ArrayInitPlan
{
public string $expr;
public string $init;
public string $clean;
public function __construct(string $expr, string $init = '', string $clean = '')
{
$this->expr = $expr;
$this->init = $init;
$this->clean = $clean;
}
public function requiresRuntimeInit(): bool
{
return $this->init !== '' || $this->clean !== '';
}
}

@ -16,8 +16,7 @@ class PropertyDef
public string $type;
public int $flags;
public ?string $default = null;
public string $defaultInit = '';
public string $defaultClean = '';
public ?ArrayInitPlan $arrayInitPlan = null;
public bool $nullable = false;
public string $class = '';

@ -9,6 +9,7 @@
namespace PhpAot\Php;
use MJS\TopSort\Implementations\StringSort;
use PhpAot\Php\Entity\ArrayInitPlan;
use PhpAot\Php\Entity\ClassDef;
use PhpAot\Php\Entity\ConstantDef;
use PhpAot\Php\Entity\FunctionDef;
@ -256,7 +257,10 @@ class Preprocessor extends CompilerBase
}
$name = $this->parseIdentifier($param->var);
$nullable = $param->type instanceof NullableType;
$this->addClassProperty($name, $param->flags, $param->type, $param->default, $nullable, $param);
// Promoted property defaults belong to the constructor parameter,
// not to the property default table. The property itself must stay
// uninitialized until __construct assigns it.
$this->addClassProperty($name, $param->flags, $param->type, null, $nullable, $param);
}
if ($param->variadic) {
if ($i !== $last) {
@ -293,6 +297,9 @@ class Preprocessor extends CompilerBase
$list[] = $this->genArgumentDeclaration($argInfo);
}
if ($param->default) {
$arrayInitPlan = $param->default instanceof Node\Expr\Array_
? $this->buildLiteralArrayInitPlan($param->default)
: null;
if ($param->byRef) {
if ($this->isEmptyArray($param->default)) {
$argInfo->default = 'php::getEmptyArrayRef()';
@ -300,11 +307,15 @@ class Preprocessor extends CompilerBase
} elseif ($this->isNull($param->default)) {
$argInfo->default = 'nullptr';
$argInfo->defaultValue = null;
} elseif ($arrayInitPlan) {
$argInfo->default = 'php::newReference(' . $arrayInitPlan->expr . ')';
$argInfo->arrayInitPlan = $arrayInitPlan;
} else {
$argInfo->default = 'php::newReference(' . $this->parseParamDefaultValue($param->default) . ')';
}
} else {
$argInfo->default = $this->parseParamDefaultValue($param->default);
$argInfo->default = $arrayInitPlan ? $arrayInitPlan->expr : $this->parseParamDefaultValue($param->default);
$argInfo->arrayInitPlan = $arrayInitPlan;
$argInfo->defaultValue = $param->default;
}
$defaultValueCount++;
@ -500,6 +511,35 @@ class Preprocessor extends CompilerBase
return $code;
}
protected function buildLiteralArrayInitPlan(Node\Expr\Array_ $defaultNode): ArrayInitPlan
{
$localVarCount = count($this->context->localVars);
$beforeStmtCount = count($this->context->beforeStmtLines);
$afterStmtCount = count($this->context->afterStmtLines);
$expr = $this->parseIdentifier($defaultNode);
$init = '';
$clean = '';
$newLocalVars = array_slice($this->context->localVars, $localVarCount, null, true);
$newBeforeStmtLines = array_slice($this->context->beforeStmtLines, $beforeStmtCount);
$newAfterStmtLines = array_slice($this->context->afterStmtLines, $afterStmtCount);
if ($newLocalVars) {
$init .= $this->genLocalVarDecl($newLocalVars);
$this->context->localVars = array_slice($this->context->localVars, 0, $localVarCount, true);
}
if ($newBeforeStmtLines) {
$init .= implode(PHP_EOL, $newBeforeStmtLines) . PHP_EOL;
$this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $beforeStmtCount);
}
if ($newAfterStmtLines) {
$clean .= implode(PHP_EOL, $newAfterStmtLines) . PHP_EOL;
$this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $afterStmtCount);
}
return new ArrayInitPlan($expr, $init, $clean);
}
protected function getMethodName(Node\Stmt\ClassMethod $v): string
{
return $this->parseIdentifier($v->name);
@ -553,15 +593,16 @@ class Preprocessor extends CompilerBase
$flags = $this->parseModifiers($flags);
$class = '';
$type = $this->parseTypeDecl($typeNode, self::DECL_TYPE_OF_PROPERTY, $class);
$localVarCount = count($this->context->localVars);
$beforeStmtCount = count($this->context->beforeStmtLines);
$afterStmtCount = count($this->context->afterStmtLines);
$default = null;
$arrayInitPlan = null;
if ($defaultNode !== null) {
$default = $this->parseIdentifier($defaultNode);
if ($defaultNode->getType() == 'Expr_Array') {
if ($defaultNode instanceof Node\Expr\Array_) {
$type = self::TYPE_ARRAY;
$arrayInitPlan = $this->buildLiteralArrayInitPlan($defaultNode);
$default = $arrayInitPlan->expr;
} else {
$default = $this->parseIdentifier($defaultNode);
}
}
@ -571,24 +612,7 @@ class Preprocessor extends CompilerBase
$propDef = new PropertyDef($name, $flags, $type, $default, $nullable);
$propDef->class = $class;
if (($flags & Modifiers::STATIC) && $defaultNode !== null) {
$newLocalVars = array_slice($this->context->localVars, $localVarCount, null, true);
$newBeforeStmtLines = array_slice($this->context->beforeStmtLines, $beforeStmtCount);
$newAfterStmtLines = array_slice($this->context->afterStmtLines, $afterStmtCount);
if ($newLocalVars) {
$propDef->defaultInit .= $this->genLocalVarDecl($newLocalVars);
$this->context->localVars = array_slice($this->context->localVars, 0, $localVarCount, true);
}
if ($newBeforeStmtLines) {
$propDef->defaultInit .= implode(PHP_EOL, $newBeforeStmtLines) . PHP_EOL;
$this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $beforeStmtCount);
}
if ($newAfterStmtLines) {
$propDef->defaultClean .= implode(PHP_EOL, $newAfterStmtLines) . PHP_EOL;
$this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $afterStmtCount);
}
}
$propDef->arrayInitPlan = $arrayInitPlan;
$this->classDef->properties[$name] = $propDef;
return $propDef;
}

@ -11,6 +11,7 @@ namespace PhpAot\Php;
use MJS\TopSort\Implementations\StringSort;
use PhpAot\Php\Analysis\SsaBuilder;
use PhpAot\Php\Backend\CompilerFactory;
use PhpAot\Php\Entity\ArrayInitPlan;
use PhpAot\Php\Entity\ClassDef;
use PhpAot\Php\Entity\ClassLikeDef;
use PhpAot\Php\Entity\ConstantDef;
@ -50,15 +51,75 @@ class Translator extends Preprocessor
// Windows 资源文件配置(图标、版本信息等)
protected array $resourceConfig = [];
// 类静态属性初始值
protected array $defaultStaticPropertyList = [];
// 类属性初始值
protected array $defaultPropertyList = [];
protected bool $useRegisterSymbolsFn = false;
protected const string MODULE_NAME_PREFIX = 'app_';
protected function isConstructorNativeFunction(FunctionDef $func): bool
{
return $func->method && str_ends_with($func->name, self::NAMESPACE_SEPARATOR . '__construct');
}
protected function getDefaultArgumentType(ArgInfo $argInfo): string
{
$type = $argInfo->type;
if ($type === self::TYPE_STREAM || $type === self::TYPE_BOX) {
return self::TYPE_VAR;
}
return $type;
}
protected function getDefaultArgumentHelperName(FunctionDef $func, ArgInfo $argInfo): string
{
return self::PREFIX . 'default_arg_' . $func->name . '_' . $argInfo->name;
}
protected function genDefaultArgumentExpr(FunctionDef $func, ArgInfo $argInfo): string
{
if (!$argInfo->arrayInitPlan || !$argInfo->arrayInitPlan->requiresRuntimeInit()) {
return $argInfo->default;
}
return $this->getDefaultArgumentHelperName($func, $argInfo) . '()';
}
protected function wrapArrayInitPlan(ArrayInitPlan $plan, string $body): string
{
if (!$plan->requiresRuntimeInit()) {
return $body;
}
return "do {\n" . $plan->init . $body . $plan->clean . "} while (0);\n";
}
protected function genDefaultArgumentHelpers(): string
{
$code = '';
foreach ($this->functions as $func) {
foreach ($func->argInfoList as $argInfo) {
$plan = $argInfo->arrayInitPlan;
if (!$plan || !$plan->requiresRuntimeInit()) {
continue;
}
$type = $this->getDefaultArgumentType($argInfo);
$helper = $this->getDefaultArgumentHelperName($func, $argInfo);
$code .= 'static inline ' . $type . ' ' . $helper . "() {\n";
$code .= $plan->init;
if ($plan->clean) {
$code .= $type . ' retval = ' . $plan->expr . ';' . PHP_EOL;
$code .= $plan->clean;
$code .= 'return retval;' . PHP_EOL;
} else {
$code .= 'return ' . $plan->expr . ';' . PHP_EOL;
}
$code .= '}' . PHP_EOL;
}
}
return $code ? $code . PHP_EOL : '';
}
public function __construct(string $rootPath)
{
parent::__construct($rootPath);
@ -695,15 +756,16 @@ CODE;
}
$code .= '// static property ' . PHP_EOL;
foreach ($this->defaultStaticPropertyList as $prop) {
if ($prop->init || $prop->clean) {
$code .= "do {\n";
$code .= $prop->init;
$code .= 'php::setStaticProperty(' . $this->genCharPtr($prop->class, true) . ', ' . $this->genCharPtr($prop->name) . ', ' . $prop->default . ');' . PHP_EOL;
$code .= $prop->clean;
$code .= "} while (0);\n";
} else {
$code .= 'php::setStaticProperty(' . $this->genCharPtr($prop->class, true) . ', ' . $this->genCharPtr($prop->name) . ', ' . $prop->default . ');' . PHP_EOL;
foreach ($this->classes as $classDef) {
foreach ($classDef->properties as $property) {
if (!$property->isStatic() || !$property->arrayInitPlan || !$property->default) {
continue;
}
$statement = 'php::setStaticProperty('
. $this->genCharPtr($classDef->getNamespacedName(false), true) . ', '
. $this->genCharPtr($property->name) . ', '
. $property->arrayInitPlan->expr . ');' . PHP_EOL;
$code .= $this->wrapArrayInitPlan($property->arrayInitPlan, $statement);
}
}
@ -1396,6 +1458,7 @@ CODE;
$literalStringsCount = count($this->literalStrings);
$code .= 'extern ' . self::TYPE_STR . ' ' . self::LITERAL_STRINGS . '[' . $literalStringsCount . '];' . PHP_EOL;
}
$code .= $this->genDefaultArgumentHelpers();
foreach ($this->functions as $name => $func) {
$code .= 'extern ' . $func->returnType . ' ' . self::PREFIX . $name . '(';
@ -1410,8 +1473,8 @@ CODE;
$arg = self::TYPE_ARRAY . ' ' . $argInfo->name . ' = {}';
} else {
$arg = $this->genArgumentDeclaration($argInfo);
if ($argInfo->default) {
$arg .= ' = ' . $argInfo->default;
if ($argInfo->default && !$this->isConstructorNativeFunction($func)) {
$arg .= ' = ' . $this->genDefaultArgumentExpr($func, $argInfo);
}
}
$list[] = $arg;
@ -1483,12 +1546,10 @@ CODE;
$code .= $classDef->ctorInit;
$code .= "auto obj = create_object_{$className}(class_type);\n";
foreach ($classDef->properties as $property) {
$fullPropName = $classDef->getNamespacedName(false) . '::' . $property->name;
if (isset($this->defaultPropertyList[$fullPropName])) {
$code .= "do {\n";
$code .= "auto value = {$this->defaultPropertyList[$fullPropName]};\n";
$code .= 'zend_update_property(obj->ce, obj, ' . $this->genZendStrl($property->name) . ", value.ptr());\n";
$code .= "} while(0);\n";
if (!$property->isStatic() && $property->arrayInitPlan && $property->default) {
$body = "auto value = {$property->arrayInitPlan->expr};\n";
$body .= 'zend_update_property(obj->ce, obj, ' . $this->genZendStrl($property->name) . ", value.ptr());\n";
$code .= $this->wrapArrayInitPlan($property->arrayInitPlan, $body);
}
}
$code .= $classDef->ctorClean;
@ -2364,10 +2425,11 @@ CODE;
$cppCode .= '}' . PHP_EOL;
} else {
if ($argInfo->default) {
$defaultExpr = $this->genDefaultArgumentExpr($functionDef, $argInfo);
if ($argInfo->byRef) {
$argExpr = 'php::getCallArgByRef(' . $k . ', ' . $argInfo->default . ')';
$argExpr = 'php::getCallArgByRef(' . $k . ', ' . $defaultExpr . ')';
} else {
$argExpr = 'php::getCallArg(' . $k . ', ' . $argInfo->default . ')';
$argExpr = 'php::getCallArg(' . $k . ', ' . $defaultExpr . ')';
}
} else {
if ($argInfo->byRef) {
@ -2378,7 +2440,7 @@ CODE;
$argExpr = 'php::getCallArg(' . $k . ')';
}
}
$cppType = ($argInfo->type === self::TYPE_STREAM || $argInfo->type === self::TYPE_BOX) ? self::TYPE_VAR : $argInfo->type;
$cppType = $this->getDefaultArgumentType($argInfo);
$expr = $this->convertExprFromType($argInfo->type, $argExpr);
$cppCode .= $this->getIndent() . $cppType . ' ' . $var . ' = ' . $expr . ';' . PHP_EOL;
}
@ -2435,21 +2497,8 @@ CODE;
if ($classDef instanceof ClassDef) {
$arrayPropCount = 0;
foreach ($classDef->properties as $property) {
if ($property->type === self::TYPE_ARRAY and $property->default and $property->default !== self::TYPE_ARRAY . '{}') {
$fullClassName = $classDef->getNamespacedName(false);
$fullPropName = $fullClassName . '::' . $property->name;
if ($property->isStatic()) {
$prop = new \stdClass();
$prop->class = $fullClassName;
$prop->name = $property->name;
$prop->default = $property->default;
$prop->init = $property->defaultInit;
$prop->clean = $property->defaultClean;
$this->defaultStaticPropertyList[$fullPropName] = $prop;
} else {
$this->defaultPropertyList[$fullPropName] = $property->default;
$arrayPropCount++;
}
if ($property->type === self::TYPE_ARRAY && $property->arrayInitPlan && $property->default && !$property->isStatic()) {
$arrayPropCount++;
}
}
if ($arrayPropCount > 0) {

@ -4877,7 +4877,9 @@ function parseFunctionLike(
$foundVariadic = false;
foreach ($func->getParams() as $i => $param) {
if ($param->isPromoted()) {
$propertyItem = new Stmt\PropertyProperty($param->var->name, $param->default);
// For constructor promotion, the parameter default belongs to the
// constructor argument, not to the promoted property itself.
$propertyItem = new Stmt\PropertyProperty($param->var->name, null);
$property = new Stmt\Property($param->flags, [$propertyItem], $param->getAttributes(), $param->type, $param->attrGroups);
$propertyInfos[] = parseProperty(
$name->className,

@ -0,0 +1,38 @@
--TEST--
Readonly Classes (PHP 8.2+)
--FILE--
<?php
enum T { case A; case M; }
readonly class Q {
public function __construct(
public string $t,
public array $o = [],
public array $a = [1 => 'yes', 'next' => 'no', 2 => 'maybe'],
public T $type = T::A,
) {}
}
function main(): void {
$questions = [
new Q("1+1=2?", ['yes', 'no'], ['yes']),
new Q("哪些是数字?", ['1', '2', 'a', 'b'], ['1', '2'], T::M),
];
foreach ($questions as $i => $q) {
$num = $i + 1;
$typeText = $q->type === T::A ? '单选' : '多选';
echo "第{$num}题 [{$typeText}] {$q->t}\n";
echo "选项: " . implode(', ', $q->o) . "\n";
echo "答案: " . implode(', ', $q->a) . "\n";
}
}
?>
--EXPECT--
第1题 [单选] 1+1=2?
选项: yes, no
答案: yes
第2题 [多选] 哪些是数字?
选项: 1, 2, a, b
答案: 1, 2
Loading…
Cancel
Save