TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

510 lines
23 KiB

<?php
namespace TypePhp\Parser;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\NodeAbstract;
use TypePhp\Entity\ArgInfo;
use TypePhp\Entity\TypedArrayPropertyWritePlan;
use TypePhp\Transform\CompileTimeAttribute;
use TypePhp\Type;
/** Compiler-enforced PHP-array contracts. Storage and COW remain php::Array's. */
trait TypedArrayTrait
{
protected function parseTypedArrayPropertyDefinition(Node\Stmt\Property $property): ?array
{
foreach (['StdList' => 'list', 'StdDict' => 'dict'] as $name => $kind) {
$attribute = CompileTimeAttribute::find($property, $name);
if ($attribute !== null) {
$this->validateStdAttributeType($property, $name, 'array');
return $this->parseTypedArrayDefinition($kind, $attribute->args, $attribute);
}
}
return null;
}
protected function prepareTypedArrayPropertyDirectWrite(
Expr\ArrayDimFetch $left,
Expr $right,
string $value,
): ?TypedArrayPropertyWritePlan {
$definition = $this->getNativePropertyDef($left->var)?->typedArray;
if ($definition === null) {
return null;
}
$value = $this->guardTypedArrayValue($definition, $right, $value);
if ($left->dim === null) {
if ($definition['kind'] !== 'list') {
$this->fatalError($left, 'StdDict properties do not support append writes');
}
return new TypedArrayPropertyWritePlan(true, null, $value);
}
$key = $this->guardTypedArrayValue($definition, $left->dim, $this->parseExprAsValue($left->dim), true);
return new TypedArrayPropertyWritePlan(false, $key, $value);
}
protected function parseTypedArrayDefinition(string $kind, array $args, NodeAbstract $owner): array
{
$count = $kind === 'list' ? 1 : 2;
if (count($args) !== $count) {
$this->fatalError($owner, "Std{$kind} expects {$count} type argument(s)");
}
foreach ($args as $arg) {
if ($arg->name !== null || $arg->unpack || $arg->byRef) {
$this->fatalError($arg, 'Typed arrays require positional type arguments');
}
}
$key = $kind === 'list' ? Type::INT : $this->parseStdMapKeyType($args[0]->value, 'std::dict');
$value = $this->parseStdValueTypeInfo($args[$count - 1]->value, 'std::' . $kind);
if (!in_array($value['type'], [Type::INT, Type::FLOAT, Type::BOOL, Type::STR, Type::ARRAY, Type::OBJECT, Type::VAR], true)) {
$this->fatalError($owner, 'Typed PHP arrays only support PHP value types or ClassName::class');
}
if ($this->isNativeObjectClass($value['class'] ?? '')) {
$this->fatalError($owner, 'Typed PHP arrays cannot hold Native objects');
}
// Integer-key lists/dicts share PHP storage; only list grants append.
return ['kind' => $kind, 'keyType' => $key, 'type' => $value['type'], 'class' => $value['class']];
}
protected function parseTypedArrayFactoryDefinition(string $kind, Expr\StaticCall $call): array
{
$initializer = $this->getStdValueInitializer($call);
if ($initializer === null) {
return $this->parseTypedArrayDefinition($kind, $call->args, $call);
}
$inferred = $this->inferStdFlatInitializer(
$initializer,
'std::' . $kind,
$kind === 'list' ? 'integer' : 'map',
);
if (!in_array($inferred['type'], [
Type::INT,
Type::FLOAT,
Type::BOOL,
Type::STR,
Type::ARRAY,
Type::OBJECT,
], true)) {
$this->fatalError($call, 'Typed PHP array value initialization only supports concrete PHP value types');
}
if ($this->isNativeObjectClass($inferred['class'] ?? '')) {
$this->fatalError($call, 'Typed PHP arrays cannot hold Native objects');
}
return [
'kind' => $kind,
'keyType' => $kind === 'list' ? Type::INT : $inferred['keyType'],
'type' => $inferred['type'],
'class' => $inferred['class'],
];
}
protected function parseTypedArrayParameterDefinition(Node\Param $param): ?array
{
foreach (['StdList' => 'list', 'StdDict' => 'dict'] as $name => $kind) {
$attribute = CompileTimeAttribute::find($param, $name);
if ($attribute === null) {
continue;
}
$this->validateStdAttributeType($param, $name, 'array');
if ($param->variadic || $param->default !== null || $param->isPromoted()) {
$this->fatalError($param, $name . ' does not support variadic, defaulted or promoted parameters');
}
return $this->parseTypedArrayDefinition($kind, $attribute->args, $attribute);
}
return null;
}
protected function getTypedArrayDefinition(NodeAbstract $expr): ?array
{
if ($expr instanceof Expr\ErrorSuppress) {
return $this->getTypedArrayDefinition($expr->expr);
}
if ($expr instanceof Expr\Assign || $expr instanceof Expr\AssignRef) {
return $this->getTypedArrayDefinition($expr->expr);
}
if ($expr instanceof Expr\MethodCall && $expr->name instanceof Node\Identifier) {
$kind = match ($expr->name->name) {
'toStdList' => 'list',
'toStdDict' => 'dict',
default => null,
};
if ($kind !== null) {
return $this->parseTypedArrayDefinition($kind, $expr->args, $expr);
}
}
if ($expr instanceof Expr\StaticCall && $expr->class instanceof Node\Name
&& $expr->name instanceof Node\Identifier && $this->isStdClassExpr($expr->class)
&& in_array(strtolower($expr->name->name), ['list', 'dict'], true)) {
return $this->parseTypedArrayFactoryDefinition(strtolower($expr->name->name), $expr);
}
if ($this->isVarExpr($expr)) {
return $this->context->typedArrays[$this->parseIdentifier($expr)] ?? null;
}
return null;
}
protected function parseTypedArrayConversionCall(Expr\MethodCall $call): string
{
$kind = $call->name->toString() === 'toStdList' ? 'list' : 'dict';
$definition = $this->parseTypedArrayDefinition($kind, $call->args, $call);
if ($this->isVarExpr($call->var)) {
$this->assertStdContainerDoesNotEscapeNativeObjects($call, $this->parseIdentifier($call->var));
}
// A matching typed array already satisfies the contract. Preserve
// ordinary PHP array assignment and its copy-on-write behavior.
if ($this->getTypedArrayDefinition($call->var) === $definition) {
return $this->parseExprAsValue($call->var);
}
$stdContainer = $this->isVarExpr($call->var)
&& $this->isStdContainer($this->parseIdentifier($call->var));
if ($this->detectTypeOfExpr($call->var) === Type::ARRAY && !$stdContainer) {
$source = $this->parseExprAsValue($call->var);
} else {
$class = $this->detectClassOfExpr($call->var);
if ($this->isNativeObjectClass($class)) {
// Native objects use their declared toArray() method; their
// pointer cannot be passed to PHPX's dynamic conversion.
$source = $this->parseExprAsValue(new Expr\MethodCall($call->var, new Node\Identifier('toArray')));
} else {
$source = 'php::toArray(' . $this->parseExprAsValue($call->var) . ')';
}
}
$valueType = match ($definition['type']) {
Type::INT => 'Int',
Type::FLOAT => 'Float',
Type::BOOL => 'Bool',
Type::STR => 'String',
Type::ARRAY => 'Array',
Type::OBJECT => 'Object',
Type::VAR => 'Any',
};
$valueClass = $definition['class'] !== null && $definition['class'] !== ''
? $this->getClassEntryPtr($definition['class'])
: 'nullptr';
return 'php::toTypedArray(' . $source . ', '
. ($definition['keyType'] === Type::STR ? 'true' : 'false') . ', '
. 'php::TypedArrayValueType::' . $valueType . ', ' . $valueClass . ')';
}
protected function getTypedArrayAccessDefinition(NodeAbstract $expr): ?array
{
return $expr instanceof Expr\ArrayDimFetch ? $this->getTypedArrayDefinition($expr->var) : null;
}
protected function assertTypedArrayReferenceForbidden(NodeAbstract $expr): void
{
while ($expr instanceof Expr\ArrayDimFetch) {
$expr = $expr->var;
}
if ($this->getTypedArrayDefinition($expr) !== null) {
$this->fatalError($expr, 'Typed arrays cannot escape through std::ref(), toRef(), or element references');
}
}
protected function parseTypedArrayAssignment(Expr $left, Expr $right): ?string
{
if ($this->getTypedArrayAccessDefinition($left) !== null) {
return $this->parseTypedArrayWrite($left, $right);
}
// Nested writes would bypass the declared outer element type.
if ($left instanceof Expr\ArrayDimFetch && $left->var instanceof Expr\ArrayDimFetch
&& $this->getTypedArrayAccessDefinition($left->var) !== null) {
$this->fatalError($left, 'Nested typed-array writes must replace a checked element');
}
if (!$this->isVarExpr($left)) {
return null;
}
$name = $this->parseWritableIdentifier($left);
$factory = $right instanceof Expr\StaticCall && $right->class instanceof Node\Name
&& $right->name instanceof Node\Identifier && $this->isStdClassExpr($right->class)
&& in_array(strtolower($right->name->name), ['list', 'dict'], true);
$definition = $factory
? $this->parseTypedArrayFactoryDefinition(strtolower($right->name->name), $right)
: $this->getTypedArrayDefinition($right);
$existing = $this->context->typedArrays[$name] ?? null;
if ($definition === null && $existing === null) {
return null;
}
if ($existing !== null && $definition !== $existing) {
$this->fatalError($right, 'Typed array assignment requires an identical list/dict contract');
}
if ($this->hasScopeGlobalVar($name) || $this->hasStaticVar($name)
|| isset($this->context->varTypeDegradations[$name])) {
$this->fatalError($left, 'Typed arrays require fixed function-local storage and cannot be captured by reference');
}
if ($factory && $this->hasVar($name)) {
$this->fatalError($left, 'Typed array factories require a new variable');
}
if ($existing === null) {
if ($this->hasVar($name) && $this->getRawVarType($name) !== Type::ARRAY) {
$this->fatalError($left, 'Cannot erase a typed array into dynamic or reference storage');
}
// Do not retrofit a contract onto an already-live ordinary array:
// existing aliases or previous paths may still write untyped values.
if ($this->hasVar($name)) {
$this->fatalError($left, 'Typed array propagation requires a new variable');
}
$this->addLocalVar($name, Type::ARRAY);
$this->context->typedArrays[$name] = $definition;
}
if ($factory) {
$initializer = $this->getStdValueInitializer($right);
return $name . ' = ' . ($initializer === null ? 'php::Array{}' : $this->parseArray($initializer));
}
return $name . ' = ' . $this->parseExprAsValue($right);
}
protected function guardTypedArrayValue(array $def, Expr $expr, string $code, bool $key = false): string
{
$this->assertExprCanBeUsedAsValue($expr, 'typed array element');
$expected = $key ? $def['keyType'] : $def['type'];
$class = $key ? '' : ($def['class'] ?? '');
$actual = Type::getReferencedType($this->detectTypeOfExpr($expr));
if ($this->isNativeObjectClass($this->detectClassOfExpr($expr))) {
$this->fatalError($expr, 'Native objects cannot be stored in typed PHP arrays');
}
if ($key && $actual === Type::VAR) {
// Dynamic keys need a strict check, not a coercing conversion.
// These are internal PHPX helpers, not user keyword methods.
return ($expected === Type::INT ? 'php::toIntExact(' : 'php::toStringExact(') . $code . ')';
}
if (!$key && $expected === Type::VAR) {
return $code;
}
if ($class !== '') {
$actualClass = $this->detectClassOfExpr($expr);
if ($actualClass === '' || !$this->isObjectClassStaticallyAssignableTo($actualClass, $class)) {
$this->fatalError($expr, 'Typed array value must be an instance of ' . $class);
}
if ($actual !== Type::OBJECT) {
$this->fatalError($expr, 'Typed array value must be an instance of ' . $class);
}
return $code;
}
if ($actual !== $expected) {
$this->fatalError($expr, 'Typed array ' . ($key ? 'key' : 'value') . ' must have type ' . $expected);
}
if ($expected === Type::INT && $this->varIntTypes && $this->exprCanOverflowInt($expr)) {
$this->fatalError($expr, 'Typed array integer expressions that may widen require an explicit toInt() or native integer conversion');
}
return $code;
}
protected function convertTypedArrayRead(array $def, string $code): string
{
// Recover the statically proven type; no Exact checks or element scans.
return $this->convertExprFromType($def['type'], $code);
}
protected function parseTypedArrayKey(Expr\ArrayDimFetch $expr, bool $write = false): string
{
$def = $this->getTypedArrayAccessDefinition($expr);
if ($expr->dim === null) {
if (!$write || $def['kind'] !== 'list') {
$this->fatalError($expr, 'Only typed lists support append writes');
}
return '';
}
$key = $this->guardTypedArrayValue($def, $expr->dim, $this->parseExprAsValue($expr->dim), true);
return $key;
}
protected function parseTypedArrayRead(Expr\ArrayDimFetch $expr): string
{
$def = $this->getTypedArrayAccessDefinition($expr);
return $this->convertTypedArrayRead($def, $this->parseTypedArrayRawRead($expr));
}
protected function parseTypedArrayRawRead(Expr\ArrayDimFetch $expr): string
{
$array = $this->parseIdentifier($expr->var);
$key = $this->parseTypedArrayKey($expr);
return $array . '.offsetGet(' . $key . ')';
}
protected function parseTypedArrayPresence(Expr\ArrayDimFetch $expr, string $op, bool $getValue): string
{
$raw = $this->parseTypedArrayRawRead($expr);
if ($getValue) {
$result = $this->addTmpVar(Type::VAR);
$expr->setAttribute('chainOpResult', $result);
$raw = '(' . $result . ' = ' . $raw . ')';
}
return match ($op) {
self::OP_ISSET => 'php::exists(' . $raw . ')',
self::OP_EMPTY => 'php::empty(' . $raw . ')',
self::OP_NOT_EMPTY => 'php::notEmpty(' . $raw . ')',
default => $this->getChainedFunc($op) . '(' . $raw . ')',
};
}
protected function parseTypedArrayKeyExistsCall(Expr\FuncCall $call): ?string
{
if (count($call->args) !== 2) {
return null;
}
$arguments = [];
foreach ($call->args as $index => $argument) {
if ($argument->unpack || $argument->byRef) {
return null;
}
$name = $argument->name?->name ?? ($index === 0 ? 'key' : 'array');
if (!in_array($name, ['key', 'array'], true) || isset($arguments[$name])) {
return null;
}
$arguments[$name] = $argument->value;
}
if (!isset($arguments['key'], $arguments['array'])
|| $this->getTypedArrayDefinition($arguments['array']) === null) {
return null;
}
$definition = $this->getTypedArrayDefinition($arguments['array']);
// Snapshot source arguments in order, including named arguments.
$values = [];
foreach ($arguments as $name => $argument) {
if ($name === 'key') {
$key = $this->guardTypedArrayValue($definition, $argument, $this->parseExprAsValue($argument), true);
$values[$name] = $this->addTmpVar($definition['keyType']);
$this->context->beforeStmtLines[] = $values[$name] . ' = ' . $key . ';';
} else {
$values[$name] = $this->parseOrderedOperand($argument, false, true);
}
}
// Existing PHPX lookup retains PHP numeric-string normalization.
return $values['array'] . '.exists(' . $values['key'] . ')';
}
protected function parseTypedArrayForeach(Foreach_ $node, array $def): string
{
if ($node->byRef) {
$this->fatalError($node, 'Typed array elements cannot escape through foreach references');
}
$array = $this->parseIdentifier($node->expr);
$iterator = $this->genTmpVarName();
$assignments = '';
foreach ([[$node->keyVar, $def['keyType'], '', 'key'],
[$node->valueVar, $def['type'], $def['class'] ?? '', 'value']] as [$target, $type, $class, $part]) {
if ($target === null) {
continue;
}
if (!$this->isVarExpr($target)) {
$this->fatalError($target, 'Typed array foreach requires simple typed key/value variables');
}
$this->assertImmutableMutationTarget($target);
$name = $this->parseWritableIdentifier($target);
if (isset($this->context->varTypeDegradations[$name])) {
$this->fatalError($target, 'Typed foreach variables cannot be captured by reference');
}
if ($this->hasVar($name) && ($this->getRawVarType($name) !== $type
|| ($class !== '' && $this->getDeclaredObjectType($name) !== $class))) {
$this->fatalError($target, 'Typed array foreach variable must have the declared key/value type');
}
if (!$this->hasVar($name)) {
$this->addLocalVar($name, $type);
if ($class !== '') {
$this->addObject($name, $class);
}
}
// PHP normalizes numeric string keys to integers. Recover the
// declared key type here, without changing PHPX or array storage.
$assignments .= $this->getIndent() . ' ' . $name . ' = '
. $this->convertExprFromType($type, $iterator . '.' . $part . '()') . ';' . PHP_EOL;
}
$scope = $this->class ? $this->getLocalClassEntryPtr($this->getFullClassName()) : 'nullptr';
$code = '{' . PHP_EOL . $this->getIndent() . 'php::ForeachIterator ' . $iterator
. '{' . $array . ', false, ' . $scope . '};' . PHP_EOL
. $this->getIndent() . 'while (' . $iterator . '.next()) {' . PHP_EOL . $assignments;
$this->indentLevel++;
$body = $this->parseForeachBody($node);
$this->indentLevel--;
return $code . $this->parseBeforeStmtLines() . $body . $this->getIndent() . '}' . PHP_EOL
. $this->getIndent() . '}' . PHP_EOL;
}
protected function parseTypedArrayWrite(Expr\ArrayDimFetch $left, Expr $right): string
{
$def = $this->getTypedArrayAccessDefinition($left);
$array = $this->parseWritableIdentifier($left->var);
$key = $this->parseTypedArrayKey($left, true);
// Snapshot a key before lowering a RHS that might hoist side effects.
if ($key !== '') {
$keyVar = $this->addTmpVar($def['keyType']);
$this->context->beforeStmtLines[] = $keyVar . ' = ' . $key . ';';
$key = $keyVar;
}
$value = $this->guardTypedArrayValue($def, $right, $this->parseExprAsValue($right));
$result = $this->addTmpVar(Type::VAR);
// An assignment expression yields the assigned value, not void.
return '[&]() -> php::Var { ' . $result . ' = ' . $value . '; '
. $this->genTypedArrayStore($array, $key, $result) . '; return ' . $result . '; }()';
}
private function genTypedArrayStore(string $array, string $key, string $value): string
{
if ($key === '') {
return $array . '.appendValue(' . $value . ')';
}
return $array . '.offsetSet(' . $key . ', ' . $value . ')';
}
protected function parseTypedArrayUnset(Expr\ArrayDimFetch $expr): string
{
$array = $this->parseWritableIdentifier($expr->var);
$key = $this->parseTypedArrayKey($expr);
return $array . '.offsetUnset(' . $key . ')';
}
protected function assertTypedArrayArgument(Node\Arg $arg, ?ArgInfo $parameter, bool $byRef, bool $project): void
{
$expr = $arg->value;
if ($this->isReferenceWrapperCall($expr)) {
$expr = $this->unwrapReferenceWrapperCall($expr, $arg);
$this->assertTypedArrayReferenceForbidden($expr);
}
$def = $this->getTypedArrayDefinition($expr);
$expected = $parameter?->typedArray;
if ($expected !== null && $def !== $expected) {
$this->fatalError($arg, 'Typed array parameter requires an identical list/dict contract');
}
if ($def !== null && ($arg->unpack || ($project && $expected === null) || ($byRef && $expected === null))) {
$this->fatalError($arg, 'Typed arrays require matching annotated TypePHP parameters or read-only PHP calls');
}
if ($byRef && $expected === null) {
$this->assertTypedArrayReferenceForbidden($expr);
}
}
protected function validateTypedArrayDynamicArgument(Node\Arg $arg, string $function, string $class, int $index): void
{
$parameter = $arg->name !== null
? $this->getAotCallArgInfoByName($function, $class, $arg->name->name)
: $this->getAotCallArgInfo($function, $class, $index);
if ($parameter?->typedArray !== null) {
$this->fatalError($arg, 'Typed array parameters require a statically resolved native TypePHP call, not Zend dispatch');
}
$byRef = $function !== '' && ($arg->name !== null
? $this->isReferenceNamedArgument($function, $class, $arg->name->name)
: $this->isReferenceArgument($function, $class, $index));
$this->assertTypedArrayArgument($arg, $parameter, $byRef, $parameter !== null);
}
protected function functionUsesTypedArray(\TypePhp\Entity\FunctionDef $function): bool
{
foreach ($function->argInfoList as $parameter) {
if ($parameter->typedArray !== null) {
return true;
}
}
return false;
}
protected function functionRequiresNativeAbi(\TypePhp\Entity\FunctionDef $function): bool
{
return $this->functionUsesNativeObject($function) || $this->functionUsesTypedArray($function);
}
}