feat(parser): 添加对象属性赋值类型检查和二元运算操作数处理

- 实现对象属性赋值时的类型验证检查机制
- 添加静态属性赋值的类型安全保护
- 引入有序操作数解析以支持复杂表达式计算
- 增加对函数调用参数的类型安全检查
- 优化二元运算符操作数的类型转换处理
- 添加测试用例验证类型检查功能正确性
- 扩展属性定义实体以支持类型检查配置
- 实现操作数材料化以处理副作用表达式
pull/5/head
韩天峰 2 months ago
parent cff27c7c79
commit 54bca6942e
  1. 103
      src/Php/CompilerBase.php
  2. 2
      src/Php/Entity/PropertyDef.php
  3. 8
      src/Php/Optimizer/FuncCallOptimizer.php
  4. 3
      src/Php/Parser/AssignOpTrait.php
  5. 132
      src/Php/Parser/BinaryOpTrait.php
  6. 5
      src/Php/Preprocessor.php
  7. 4
      src/Php/Translator.php
  8. 4
      src/Php/UniversalMethodCall.php
  9. 2
      tests/aot/arrow_fn/002.phpt
  10. 4
      tests/aot/optimizations/objprop-nullable-union-default-null.phpt
  11. 13
      tests/aot/optimizations/objprop-typed-object-null-unset.phpt
  12. 4
      tests/aot/static/static_prop_write.phpt
  13. 34
      tests/aot/type_hits/008.phpt

@ -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;

@ -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)
{

@ -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) . ')';

@ -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) {

@ -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);
}

@ -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;
}

@ -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');

@ -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) {

@ -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();
}
?>

@ -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) ""

@ -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--
<?php
use native_types;
@ -28,9 +28,12 @@ class ObjPropHolder {
var_dump(isset($this->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"

@ -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__);
});
}

@ -0,0 +1,34 @@
--TEST--
type hits
--FILE--
<?php
class Select {
public $errorHandler = null;
}
class Worker
{
public static ?stdClass $globalEvent = null;
public static string $eventLoopClass = 'Select';
public static function init() {
self::$globalEvent = new stdClass();
var_dump(get_class(self::$globalEvent));
self::$globalEvent = null;
var_dump(self::$globalEvent);
try {
self::$globalEvent = new static::$eventLoopClass();
} catch (TypeError $e) {
var_dump($e->getMessage());
}
}
}
function main() {
Worker::init();
}
?>
--EXPECT--
string(8) "stdClass"
NULL
string(60) "Worker::$globalEvent must be of type ?stdClass, object given"
Loading…
Cancel
Save