diff --git a/src/Php/ArgInfo.php b/src/Php/ArgInfo.php index b26bd6b7..680f1d33 100644 --- a/src/Php/ArgInfo.php +++ b/src/Php/ArgInfo.php @@ -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; diff --git a/src/Php/Entity/ArrayInitPlan.php b/src/Php/Entity/ArrayInitPlan.php new file mode 100644 index 00000000..2a6d817e --- /dev/null +++ b/src/Php/Entity/ArrayInitPlan.php @@ -0,0 +1,28 @@ +expr = $expr; + $this->init = $init; + $this->clean = $clean; + } + + public function requiresRuntimeInit(): bool + { + return $this->init !== '' || $this->clean !== ''; + } +} diff --git a/src/Php/Entity/PropertyDef.php b/src/Php/Entity/PropertyDef.php index 42090819..7448d4f6 100644 --- a/src/Php/Entity/PropertyDef.php +++ b/src/Php/Entity/PropertyDef.php @@ -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 = ''; diff --git a/src/Php/Preprocessor.php b/src/Php/Preprocessor.php index f8dcd561..4d2f3e35 100644 --- a/src/Php/Preprocessor.php +++ b/src/Php/Preprocessor.php @@ -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; } diff --git a/src/Php/Translator.php b/src/Php/Translator.php index 5935734f..0f571bbf 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -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) { diff --git a/src/gen_stub.php b/src/gen_stub.php index 5e82c458..c4b7a8ac 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -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, diff --git a/tests/aot/class/readonly-2.phpt b/tests/aot/class/readonly-2.phpt new file mode 100644 index 00000000..7e37f314 --- /dev/null +++ b/tests/aot/class/readonly-2.phpt @@ -0,0 +1,38 @@ +--TEST-- +Readonly Classes (PHP 8.2+) +--FILE-- + '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