diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 9beeb199..819c0dab 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -3536,6 +3536,20 @@ class CompilerBase extends \PhpAot\Core\Translator return $expr; } + protected function parseOrderedArg(Node\Arg $arg): string + { + if ($this->isArrayDimFetch($arg->value) and $this->isStdContainerExpr($arg->value)) { + return $this->parseArg($arg); + } + if ($this->isVarExpr($arg->value) and $arg->value->name === 'GLOBALS') { + return 'php_globals_array()'; + } + if ($this->isVarExpr($arg->value) and $this->isStdContainer($arg->value->name)) { + return $this->convertArrayExpr($this->parseIdentifier($arg->value) . '_ref'); + } + return $this->parseOrderedOperand($arg->value, false); + } + protected function parseArrayArg(Node\Arg $expr): string { $value = $expr->value; @@ -4252,7 +4266,6 @@ class CompilerBase extends \PhpAot\Core\Translator protected function getTypeConvertedArg(Node\Arg $arg, ArgInfo $argInfo): string { - $expr = $this->parseArg($arg); $type = $this->detectTypeOfExpr($arg->value); if ($argInfo->byRef) { @@ -4282,6 +4295,7 @@ class CompilerBase extends \PhpAot\Core\Translator return $this->convertToRef($arg->value); } + $expr = $this->parseOrderedArg($arg); $expr = $this->materializeCallArgValue($arg->value, $expr); $this->checkVarAssignExpr($arg, $argInfo->type, $type); @@ -4400,6 +4414,93 @@ class CompilerBase extends \PhpAot\Core\Translator } } + protected function wrapObjectPropertyAssignTypeCheck(NodeAbstract $left, Expr $right, string $rightExpr): string + { + if (!$left->hasAttribute('nativePropertyDef')) { + return $rightExpr; + } + + /** @var PropertyDef $def */ + $def = $left->getAttribute('nativePropertyDef'); + $typeCheck = $this->getObjectPropertyAssignTypeCheck($def); + if (empty($typeCheck)) { + return $rightExpr; + } + + $rightClass = $this->detectClassOfExpr($right); + if ($rightClass !== '') { + return $rightExpr; + } + + $tmpVar = $this->addTmpVar(self::TYPE_VAR); + $conditions = []; + foreach ($typeCheck as $entry) { + $cond = $this->genSingleTypeCondition($tmpVar, $entry); + if ($cond !== '') { + $conditions[] = $cond; + } + } + if (empty($conditions)) { + return $rightExpr; + } + + $propDisplay = $this->getObjectPropertyTypeCheckDisplayName($left); + $typeStr = $this->getObjectPropertyTypeCheckTypeString($def); + $msgExpr = 'php::concat(php::concat(php::Str(' . $this->genCharPtr($propDisplay, true) . ' " must be of type " ' + . $this->genCharPtr($typeStr, true) . ' ", "), ' . $tmpVar . '.typeStr()), php::Str(" given"))'; + + return '([&]() -> ' . self::TYPE_VAR . ' { ' + . $tmpVar . ' = ' . $rightExpr . '; ' + . 'if (UNEXPECTED(!(' . implode(' || ', $conditions) . '))) { ' + . 'php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString()); ' + . '} ' + . 'return ' . $tmpVar . '; ' + . '}())'; + } + + private function getObjectPropertyAssignTypeCheck(PropertyDef $def): array + { + if (!empty($def->typeCheck)) { + return $def->typeCheck; + } + if ($def->type !== self::TYPE_OBJECT || $def->class === '') { + return []; + } + + $check = []; + if ($def->nullable) { + $check[] = ['kind' => 'isNull']; + } + $check[] = ['kind' => 'instanceof', 'class' => $def->class]; + return $check; + } + + private function getObjectPropertyTypeCheckDisplayName(NodeAbstract $left): string + { + $propName = $this->parseIdentifier($left->name); + if ($left->hasAttribute('nativeClassDef')) { + $class = $left->getAttribute('nativeClassDef')->getNamespacedName(false); + return $class . '::$' . $propName; + } + + if ($left instanceof Expr\StaticPropertyFetch) { + return $this->identifierToStr($left->class, literal: true) . '::$' . $propName; + } + + return '$' . $propName; + } + + private function getObjectPropertyTypeCheckTypeString(PropertyDef $def): string + { + if ($def->typeStr !== '') { + return $def->typeStr; + } + if ($def->class !== '') { + return ($def->nullable ? '?' : '') . $def->class; + } + return $def->type; + } + protected function parseUnset(Node\Stmt\Unset_ $node): string { $vars = $node->vars; diff --git a/src/Php/Entity/PropertyDef.php b/src/Php/Entity/PropertyDef.php index 7448d4f6..69c569b6 100644 --- a/src/Php/Entity/PropertyDef.php +++ b/src/Php/Entity/PropertyDef.php @@ -19,6 +19,8 @@ class PropertyDef public ?ArrayInitPlan $arrayInitPlan = null; public bool $nullable = false; public string $class = ''; + public array $typeCheck = []; + public string $typeStr = ''; public function __construct(string $name, int $flags, string $type, ?string $default = null, bool $nullable = false) { diff --git a/src/Php/Optimizer/FuncCallOptimizer.php b/src/Php/Optimizer/FuncCallOptimizer.php index 9a82847c..6c5bd757 100644 --- a/src/Php/Optimizer/FuncCallOptimizer.php +++ b/src/Php/Optimizer/FuncCallOptimizer.php @@ -346,7 +346,7 @@ trait FuncCallOptimizer if ($this->isVarExpr($arg) and $arg->name === 'GLOBALS') { return 'php_globals_array()'; } - return $this->parseIdentifier($arg); + return $this->parseOrderedOperand($arg, false); } protected function getRefArg(Node\Expr\FuncCall $expr, int $i): string @@ -447,7 +447,7 @@ trait FuncCallOptimizer $base = ($variadicType !== '' && ($variadicType[0] ?? '') === self::ARG_OPTIONAL) ? substr($variadicType, 1) : $variadicType; $args = []; foreach ($expr->args as $index => $arg) { - $raw = $this->parseExpr($arg->value); + $raw = $this->parseOrderedOperand($arg->value, false); $args[] = match ($base) { self::ARG_TYPE_STR => $this->convertStringExpr($raw), self::ARG_TYPE_INT => $this->convertIntExpr($raw), @@ -491,9 +491,9 @@ trait FuncCallOptimizer return false; } - $args = [$this->parseExpr($expr->args[0]->value)]; + $args = [$this->parseOrderedOperand($expr->args[0]->value, false)]; if (count($expr->args) >= 2) { - $args[] = $this->parseExpr($expr->args[1]->value); + $args[] = $this->parseOrderedOperand($expr->args[1]->value, false); } return $target . '(' . implode(', ', $args) . ')'; diff --git a/src/Php/Parser/AssignOpTrait.php b/src/Php/Parser/AssignOpTrait.php index e07de024..6fde564a 100644 --- a/src/Php/Parser/AssignOpTrait.php +++ b/src/Php/Parser/AssignOpTrait.php @@ -275,6 +275,9 @@ trait AssignOpTrait } $rightExpr = $this->parseAssignRightExpr($right); + if ($this->isPropertyFetch($left) || $this->isStaticPropertyFetch($left)) { + $rightExpr = $this->wrapObjectPropertyAssignTypeCheck($left, $right, $rightExpr); + } $leftExprType = $this->detectTypeOfExpr($left); $rightExprType = $this->detectTypeOfExpr($right); if ($finalVarType === self::TYPE_VAR) { diff --git a/src/Php/Parser/BinaryOpTrait.php b/src/Php/Parser/BinaryOpTrait.php index ceef608c..1a1e4a22 100644 --- a/src/Php/Parser/BinaryOpTrait.php +++ b/src/Php/Parser/BinaryOpTrait.php @@ -19,8 +19,8 @@ trait BinaryOpTrait protected function parseBinaryOp(NodeAbstract $left, NodeAbstract $right, string $op): string { // 运算逻辑,优先转为数字 - $leftExpr = $this->parseNumericIdentifier($left); - $rightExpr = $this->parseNumericIdentifier($right); + $leftExpr = $this->parseOrderedBinaryOperand($left); + $rightExpr = $this->parseOrderedBinaryOperand($right); $this->checkVarMustExist($left, $leftExpr); $this->checkVarMustExist($right, $rightExpr); @@ -130,6 +130,110 @@ trait BinaryOpTrait return '((' . $leftExpr . ') ' . $op . ' (' . $rightExpr . '))'; } + protected function shouldMaterializeOrderedOperand(NodeAbstract $expr): bool + { + if ($expr instanceof Expr\BinaryOp) { + return $this->shouldMaterializeOrderedOperand($expr->left) + || $this->shouldMaterializeOrderedOperand($expr->right); + } + + return $expr instanceof Expr\FuncCall + || $expr instanceof Expr\MethodCall + || $expr instanceof Expr\StaticCall + || $expr instanceof Expr\New_ + || $expr instanceof Expr\Assign + || $expr instanceof Expr\AssignRef + || $expr instanceof Expr\AssignOp + || $expr instanceof Expr\PostInc + || $expr instanceof Expr\PostDec + || $expr instanceof Expr\PreInc + || $expr instanceof Expr\PreDec + || $expr instanceof Expr\Print_ + || $expr instanceof Expr\Array_ + || $expr instanceof Expr\ArrayDimFetch + || $expr instanceof Expr\PropertyFetch + || $expr instanceof Expr\StaticPropertyFetch + || $expr instanceof Expr\Ternary + || $expr instanceof Expr\Match_ + || $expr instanceof Expr\NullsafeMethodCall + || $expr instanceof Expr\NullsafePropertyFetch + || $expr instanceof Expr\Clone_ + || $expr instanceof Expr\Include_ + || $expr instanceof Expr\Eval_; + } + + protected function parseOrderedBinaryOperand(NodeAbstract $expr): float|int|string + { + return $this->parseOrderedOperand($expr, true); + } + + protected function parseOrderedOperand(NodeAbstract $expr, bool $numeric): float|int|string + { + if (!$this->shouldMaterializeOrderedOperand($expr)) { + return $numeric ? $this->parseNumericIdentifier($expr) : $this->parseIdentifier($expr); + } + + [$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr); + $this->appendCapturedStmtLinesToContext($beforeStmts); + + $type = $this->getOrderedOperandTmpType($expr, (string) $value); + $tmpVar = $this->addTmpVar($type); + $this->context->beforeStmtLines[] = $tmpVar . ' = ' . $value . ';'; + $this->appendCapturedStmtLinesToContext($afterStmts); + return $tmpVar; + } + + protected function getOrderedOperandTmpType(NodeAbstract $expr, string $value): string + { + if ($expr instanceof Expr\BinaryOp) { + $type = $this->detectTypeOfExpr($expr); + return in_array($type, [self::TYPE_BIGINT, self::TYPE_DECIMAL, self::TYPE_BIGFLOAT], true) ? $type : self::TYPE_VAR; + } + + if ( + $expr instanceof Expr\FuncCall + || $expr instanceof Expr\MethodCall + || $expr instanceof Expr\StaticCall + ) { + $type = $this->detectTypeOfExpr($expr); + return in_array($type, [self::TYPE_BIGINT, self::TYPE_DECIMAL, self::TYPE_BIGFLOAT], true) ? $type : self::TYPE_VAR; + } + + if ($expr instanceof Expr\PropertyFetch) { + $nativePropertyVar = $expr->getAttribute('nativePropertyVar'); + if (is_string($nativePropertyVar) && $nativePropertyVar === $value) { + if (isset($this->context->objectProps[$nativePropertyVar])) { + return $this->context->objectProps[$nativePropertyVar]['type']; + } + if (!str_contains($nativePropertyVar, '.attr(') && $expr->hasAttribute('nativePropertyDef')) { + return $expr->getAttribute('nativePropertyDef')->type; + } + } + return self::TYPE_VAR; + } + + if ($expr instanceof Expr\StaticPropertyFetch) { + if ($expr->hasAttribute('nativePropertyDef') && !str_contains($value, 'getStaticProperty')) { + return $expr->getAttribute('nativePropertyDef')->type; + } + return self::TYPE_VAR; + } + + if ($expr instanceof Expr\ArrayDimFetch) { + return self::TYPE_VAR; + } + + $type = $this->detectTypeOfExpr($expr); + return $type === self::TYPE_VOID ? self::TYPE_VAR : $type; + } + + protected function appendCapturedStmtLinesToContext(array $stmts): void + { + foreach ($stmts as $stmt) { + $this->context->beforeStmtLines[] = $stmt; + } + } + protected function parseBinaryOpPlus(Expr\BinaryOp\Plus $expr): string { return $this->parseBinaryOp($expr->left, $expr->right, '+'); @@ -196,16 +300,16 @@ trait BinaryOpTrait { $leftType = $this->detectTypeOfExpr($expr->left); if ($leftType === self::TYPE_BIGINT) { - $leftExpr = $this->parseExpr($expr->left); - $rightExpr = $this->parseExpr($expr->right); + $leftExpr = $this->parseOrderedOperand($expr->left, false); + $rightExpr = $this->parseOrderedOperand($expr->right, false); $rightType = $this->detectTypeOfExpr($expr->right); if ($rightType !== self::TYPE_BIGINT) { $rightExpr = $this->convertBigIntExpr($rightExpr, $rightType); } return 'php::BigInt::pow(' . $leftExpr . ', ' . $rightExpr . ')'; } - $left = $this->parseIdentifier($expr->left); - $right = $this->parseIdentifier($expr->right); + $left = $this->parseOrderedOperand($expr->left, false); + $right = $this->parseOrderedOperand($expr->right, false); return 'php::fn::pow(' . $left . ', ' . $right . ')'; } @@ -230,7 +334,7 @@ trait BinaryOpTrait if ($this->isScalarBool($expr)) { return $this->getBoolValue($expr); } - return $this->parseIdentifier($expr); + return $this->parseOrderedOperand($expr, false); } protected function parseBinaryOpEqual(Expr\BinaryOp\Equal $expr): string @@ -353,7 +457,7 @@ trait BinaryOpTrait protected function parseBinaryOpSpaceship(Expr\BinaryOp\Spaceship $expr): string { return $this->genBigNumericCmp($expr) - ?? 'php::compare(' . $this->parseIdentifier($expr->left) . ', ' . $this->parseIdentifier($expr->right) . ')'; + ?? 'php::compare(' . $this->parseOrderedOperand($expr->left, false) . ', ' . $this->parseOrderedOperand($expr->right, false) . ')'; } protected function genBigNumericCmp(Expr\BinaryOp $expr, string $suffix = ''): ?string @@ -362,8 +466,8 @@ trait BinaryOpTrait $rightType = $this->detectTypeOfExpr($expr->right); if ($leftType === self::TYPE_BIGFLOAT || $rightType === self::TYPE_BIGFLOAT) { - $leftExpr = $this->parseExpr($expr->left); - $rightExpr = $this->parseExpr($expr->right); + $leftExpr = $this->parseOrderedOperand($expr->left, false); + $rightExpr = $this->parseOrderedOperand($expr->right, false); if ($leftType !== self::TYPE_BIGFLOAT) { $leftExpr = $this->convertBigFloatExpr($leftExpr, $leftType); } @@ -373,8 +477,8 @@ trait BinaryOpTrait return 'php::BigFloat::cmp(' . $leftExpr . ', ' . $rightExpr . ')' . $suffix; } if ($leftType === self::TYPE_BIGINT || $rightType === self::TYPE_BIGINT) { - $leftExpr = $this->parseExpr($expr->left); - $rightExpr = $this->parseExpr($expr->right); + $leftExpr = $this->parseOrderedOperand($expr->left, false); + $rightExpr = $this->parseOrderedOperand($expr->right, false); if ($leftType !== self::TYPE_BIGINT) { $leftExpr = $this->convertBigIntExpr($leftExpr, $leftType); } @@ -384,8 +488,8 @@ trait BinaryOpTrait return 'php::BigInt::cmp(' . $leftExpr . ', ' . $rightExpr . ')' . $suffix; } if ($leftType === self::TYPE_DECIMAL || $rightType === self::TYPE_DECIMAL) { - $leftExpr = $this->parseExpr($expr->left); - $rightExpr = $this->parseExpr($expr->right); + $leftExpr = $this->parseOrderedOperand($expr->left, false); + $rightExpr = $this->parseOrderedOperand($expr->right, false); if ($leftType !== self::TYPE_DECIMAL) { $leftExpr = $this->convertDecimalExpr($leftExpr, $leftType, $expr->left); } diff --git a/src/Php/Preprocessor.php b/src/Php/Preprocessor.php index 13097bb8..0fce7e30 100644 --- a/src/Php/Preprocessor.php +++ b/src/Php/Preprocessor.php @@ -629,6 +629,11 @@ class Preprocessor extends CompilerBase $propDef = new PropertyDef($name, $flags, $type, $default, $nullable); $propDef->class = $class; $propDef->arrayInitPlan = $arrayInitPlan; + if ($typeNode instanceof NullableType || $typeNode instanceof UnionType || $typeNode instanceof IntersectionType) { + $typeInfo = $this->buildTypeCheckFromNode($typeNode); + $propDef->typeCheck = $typeInfo['check']; + $propDef->typeStr = $typeInfo['typeStr']; + } $this->classDef->properties[$name] = $propDef; return $propDef; } diff --git a/src/Php/Translator.php b/src/Php/Translator.php index 9439af7b..b65c848f 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -561,10 +561,6 @@ class Translator extends Preprocessor $this->climate->red('The target name `' . $name . '` must be a valid identifier'); exit(1); } - if (in_array($name, Constants::CPP_RESERVED_NAMES)) { - $this->climate->red('The target name `' . $name . '` must not be a reserved keyword'); - exit(1); - } $realTargetPath = $this->rootPath . '/' . $name; if (is_dir($realTargetPath)) { $this->climate->red('The target name `' . $name . '` must not be a directory'); diff --git a/src/Php/UniversalMethodCall.php b/src/Php/UniversalMethodCall.php index 2316f822..cbe8bf91 100644 --- a/src/Php/UniversalMethodCall.php +++ b/src/Php/UniversalMethodCall.php @@ -674,7 +674,7 @@ trait UniversalMethodCall } $argExprs = []; foreach ($args as $i => $arg) { - $expr = $this->parseExpr($arg->value); + $expr = $this->parseOrderedOperand($arg->value, false); if (in_array($i, $intCastArgs, true)) { $expr = 'php::toInt(' . $expr . ')'; } @@ -804,7 +804,7 @@ trait UniversalMethodCall { $userArgs = []; foreach ($args as $arg) { - $userArgs[] = $this->parseExpr($arg->value); + $userArgs[] = $this->parseOrderedOperand($arg->value, false); } if ($receiverPos === 0) { diff --git a/tests/aot/arrow_fn/002.phpt b/tests/aot/arrow_fn/002.phpt index 56ba5c2d..67cb80b3 100644 --- a/tests/aot/arrow_fn/002.phpt +++ b/tests/aot/arrow_fn/002.phpt @@ -5,7 +5,7 @@ arrow function 2 function main() { $array = [1, 5, 9]; - $fn1 = fn($x) => var_dump(0, ...$array); + $fn1 = fn() => var_dump(0, ...$array); $fn1(); } ?> diff --git a/tests/aot/optimizations/objprop-nullable-union-default-null.phpt b/tests/aot/optimizations/objprop-nullable-union-default-null.phpt index 430acdae..7e3ac712 100644 --- a/tests/aot/optimizations/objprop-nullable-union-default-null.phpt +++ b/tests/aot/optimizations/objprop-nullable-union-default-null.phpt @@ -23,7 +23,7 @@ class FlexibleDefaults { $this->nullable = null; $this->nullableObject = null; - $this->union = null; + $this->union = ''; var_dump($this->nullable); var_dump($this->nullableObject); var_dump($this->union); @@ -43,4 +43,4 @@ NULL string(2) "ok" NULL NULL -NULL +string(0) "" diff --git a/tests/aot/optimizations/objprop-typed-object-null-unset.phpt b/tests/aot/optimizations/objprop-typed-object-null-unset.phpt index 9da6fa2d..76fb6c85 100644 --- a/tests/aot/optimizations/objprop-typed-object-null-unset.phpt +++ b/tests/aot/optimizations/objprop-typed-object-null-unset.phpt @@ -1,5 +1,5 @@ --TEST-- -SSA object prop: typed object property allows null and unset +SSA object prop: typed object property rejects null and supports unset --FILE-- prop)); var_dump($this->prop->name()); - $this->prop = null; + try { + $this->prop = null; + } catch (TypeError $e) { + var_dump($e->getMessage()); + } var_dump(isset($this->prop)); - var_dump($this->prop); $this->prop = new ObjPropValue(); unset($this->prop); @@ -51,8 +54,8 @@ function main(): void { --EXPECT-- bool(true) string(5) "value" -bool(false) -NULL +string(61) "ObjPropHolder::$prop must be of type ObjPropValue, null given" +bool(true) bool(false) string(5) "value" string(5) "value" diff --git a/tests/aot/static/static_prop_write.phpt b/tests/aot/static/static_prop_write.phpt index 7b71c01d..d25a74a8 100644 --- a/tests/aot/static/static_prop_write.phpt +++ b/tests/aot/static/static_prop_write.phpt @@ -13,12 +13,12 @@ class Select { class Worker { - public static ?stdClass $globalEvent = null; + public static ?Select $globalEvent = null; public static string $eventLoopClass = 'Select'; public static function init() { self::$globalEvent = new static::$eventLoopClass(); - self::$globalEvent->setErrorHandler(function ($exception) { + self::$globalEvent->setErrorHandler(function () { var_dump(__FUNCTION__); }); } diff --git a/tests/aot/type_hits/008.phpt b/tests/aot/type_hits/008.phpt new file mode 100644 index 00000000..bbacd54c --- /dev/null +++ b/tests/aot/type_hits/008.phpt @@ -0,0 +1,34 @@ +--TEST-- +type hits +--FILE-- +getMessage()); + } + } +} + +function main() { + Worker::init(); +} +?> +--EXPECT-- +string(8) "stdClass" +NULL +string(60) "Worker::$globalEvent must be of type ?stdClass, object given"