Native Class 支持 [第四阶段]

master
韩天峰 1 week ago
parent de8fdaf464
commit 7cf265cbc7
  1. 19
      docs/NATIVE_CLASS_OBJECT.md
  2. 12
      examples/xml.php
  3. 14
      phpunit/code/native-class-any-property-reference.php
  4. 15
      phpunit/code/native-class-explicit-destructor-call.php
  5. 12
      phpunit/code/native-class-forward/a.php
  6. 7
      phpunit/code/native-class-forward/b.php
  7. 13
      phpunit/code/native-class-property-reference.php
  8. 13
      phpunit/code/native-class-property-unset.php
  9. 45
      phpunit/src/NativeClass/NativeClassValidationTest.php
  10. 1
      src/Build/SourcePipelineTrait.php
  11. 25
      src/CompilerBase.php
  12. 2
      src/Entity/PropertyDef.php
  13. 7
      src/Generator/CallArgumentGenerator.php
  14. 63
      src/NativeClass/NativeClassSupportTrait.php
  15. 66
      src/Parser/AssignOpTrait.php
  16. 6
      src/Parser/PropertyAccessTrait.php
  17. 81
      src/Preprocessor.php
  18. 44
      tests/compiler/native-class/any-property-reference.phpt
  19. 3
      tests/compiler/native-class/include-native-path.inc
  20. 37
      tests/compiler/native-class/mutual-reference-types.phpt
  21. 40
      tests/compiler/native-class/nested-property-write.phpt
  22. 29
      tests/compiler/native-class/shell-exec-conversion.phpt
  23. 34
      tests/compiler/native-class/string-language-operands.phpt

@ -240,7 +240,7 @@ final class InvalidContext
| `object` | `php::Object` | 保存任意 Zend Object | | `object` | `php::Object` | 保存任意 Zend Object |
| Native Class | `native_struct *` | 保存同一 Native Heap 内的裸指针 | | Native Class | `native_struct *` | 保存同一 Native Heap 内的裸指针 |
| `Stream` | `php::Var` | 保存 stream resource zval,并在赋值入口执行精确类型检查 | | `Stream` | `php::Var` | 保存 stream resource zval,并在赋值入口执行精确类型检查 |
| `mixed` | `php::Var` | 保存任意 PHP zval | | `mixed` / `any` | `php::Var` | 保存任意 PHP zval;两种声明具有相同的无约束槽语义 |
| 不含 Native Class 的 union/intersection/nullable | `php::Var` | 与普通类属性使用同一类型描述和运行时写入检查 | | 不含 Native Class 的 union/intersection/nullable | `php::Var` | 与普通类属性使用同一类型描述和运行时写入检查 |
| `?NativeClass` | `native_struct *` | `nullptr` 表示空值;包含 Native Class 的 union/intersection 不支持 | | `?NativeClass` | `native_struct *` | `nullptr` 表示空值;包含 Native Class 的 union/intersection 不支持 |
| BigInt/BigFloat/Decimal | `php::Var` | 保存 PHPX boxed 高精度值;字段寻址仍是固定偏移,运算复用现有 Variant ABI | | BigInt/BigFloat/Decimal | `php::Var` | 保存 PHPX boxed 高精度值;字段寻址仍是固定偏移,运算复用现有 Variant ABI |
@ -277,7 +277,19 @@ struct php_app__requestcontext final {
允许字段持有 ZendVM 值不代表 Native Class Object 本身进入 ZendVM。ZendVM 可以管理字段指向的 String、Array、Object 或 resource,但它不知道外层 Native Class 的存在。 允许字段持有 ZendVM 值不代表 Native Class Object 本身进入 ZendVM。ZendVM 可以管理字段指向的 String、Array、Object 或 resource,但它不知道外层 Native Class 的存在。
### 6.1 初始化状态 ### 6.1 属性引用
Native 属性是否允许取引用必须完全由声明元数据在编译期决定,不生成运行时类型分支:
- `mixed` / `any` 是无约束的 `php::Var` 槽,允许 `$ref =& $object->property`
- `bool`、`int`、`float` 等固定布局字段不能表示 PHP 引用,编译期拒绝。
- `string`、`array`、`object`、Stream 和高精度类型虽然具有 PHPX 包装层,但仍是固定声明类型,引用写入会绕过类型约束,因此编译期拒绝。
- nullable、union、intersection 等受约束的 `php::Var` 字段同样拒绝引用;不能仅因底层存储也是 `php::Var` 就允许。
- 带 Property Hook 的属性没有可暴露的实体槽,始终拒绝引用。
这项规则只允许引用无约束字段值,不允许引用 Native Object 指针变量本身。Native Object 变量之间的普通赋值已经共享对象身份。
### 6.2 初始化状态
Native Class 不保存 PHP typed property 的 `UNDEF` 状态,也不为字段增加额外状态位。对象创建时,每个没有显式默认值的字段直接使用类型零值: Native Class 不保存 PHP typed property 的 `UNDEF` 状态,也不为字段增加额外状态位。对象创建时,每个没有显式默认值的字段直接使用类型零值:
@ -293,7 +305,7 @@ Native Class 不保存 PHP typed property 的 `UNDEF` 状态,也不为字段
Property Hook 的虚拟属性没有实体字段,但 Hook 声明仍必须包含类型。 Property Hook 的虚拟属性没有实体字段,但 Hook 声明仍必须包含类型。
### 6.2 赋值检查 ### 6.3 赋值检查
已确定的赋值在编译期检查。来自 `mixed`、动态 PHP 返回值或其他无法静态确定的值,在写入字段前执行一次运行时类型检查。检查完成后直接写入对应字段,不经过 Zend property handler。 已确定的赋值在编译期检查。来自 `mixed`、动态 PHP 返回值或其他无法静态确定的值,在写入字段前执行一次运行时类型检查。检查完成后直接写入对应字段,不经过 Zend property handler。
@ -1212,6 +1224,7 @@ $json = json_encode($nativeObject->toArray());
| nullable Native 参数/返回值 | 支持 `?NativeClass`,以 `nullptr` 表示;成员访问必须检查或先证明非空 | | nullable Native 参数/返回值 | 支持 `?NativeClass`,以 `nullptr` 表示;成员访问必须检查或先证明非空 |
| Native 参数/返回值的 `&` | 不支持;编译期 FatalError | | Native 参数/返回值的 `&` | 不支持;编译期 FatalError |
| 对 Native Object 变量取引用 | 不支持;普通赋值已经共享对象身份 | | 对 Native Object 变量取引用 | 不支持;普通赋值已经共享对象身份 |
| 对 Native 属性取引用 | 仅显式声明为 `mixed` / `any` 的无约束字段支持;其他字段编译期 FatalError |
| Native variadic、union/intersection | 不支持;编译期 FatalError | | Native variadic、union/intersection | 不支持;编译期 FatalError |
| `__construct()` | 支持 | | `__construct()` | 支持 |
| `clone` / `__clone()` | 支持 | | `clone` / `__clone()` | 支持 |

@ -0,0 +1,12 @@
<?php
$xml = simplexml_load_string('<root></root>');
var_dump($xml);
var_dump((bool)$xml);
$obj = new stdClass();
var_dump((bool) $obj);
class UserClass {}
$user = new UserClass();
var_dump((bool) $user);

@ -0,0 +1,14 @@
<?php
#[Native]
class NativeAnyPropertyReference
{
public any $value = null;
}
function native_any_property_reference(): void
{
$object = new NativeAnyPropertyReference();
$reference =& $object->value;
$reference = 42;
}

@ -0,0 +1,15 @@
<?php
#[Native]
class NativeExplicitDestructor
{
public function __destruct()
{
}
}
function main(): void
{
$object = new NativeExplicitDestructor();
$object->__destruct();
}

@ -0,0 +1,12 @@
<?php
#[Native]
class NativeForwardA
{
public ?NativeForwardB $next;
}
function nativeForwardIdentity(?NativeForwardB $value): ?NativeForwardB
{
return $value;
}

@ -0,0 +1,7 @@
<?php
#[Native]
class NativeForwardB
{
public ?NativeForwardA $previous;
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeReferencedProperty
{
public int $value = 1;
}
function main(): void
{
$object = new NativeReferencedProperty();
$reference =& $object->value;
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeUnsetProperty
{
public int $value = 1;
}
function main(): void
{
$object = new NativeUnsetProperty();
unset($object->value);
}

@ -6,6 +6,25 @@ use TypePhp\Exception\TestError;
final class NativeClassValidationTest extends \BaseTest final class NativeClassValidationTest extends \BaseTest
{ {
public function testDiscoversNativeTypesBeforeCrossFileSignaturePreprocessing(): void
{
global $translator;
$compiler = \TypePhp\CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$directory = dirname(__DIR__, 2) . '/code/native-class-forward';
$files = [$directory . '/a.php', $directory . '/b.php'];
$compiler->discoverNativeClassDeclarations($files);
foreach ($files as $file) {
$compiler->prepareFile($file);
}
foreach ($files as $file) {
$compiler->convertFile($file);
}
$this->addToAssertionCount(1);
}
public function testRejectsNativeAttributeOnInterface(): void public function testRejectsNativeAttributeOnInterface(): void
{ {
$this->expectException(\TypePhp\Exception\SyntaxError::class); $this->expectException(\TypePhp\Exception\SyntaxError::class);
@ -244,6 +263,32 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-reference-assignment.php'); $this->compile('native-class-reference-assignment.php');
} }
public function testRejectsReferencesToNativeObjectProperties(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Only Native object properties declared as any or mixed can be referenced');
$this->compile('native-class-property-reference.php');
}
public function testAllowsReferencesToExplicitAnyNativeObjectProperties(): void
{
$this->compile('native-class-any-property-reference.php');
}
public function testRejectsUnsetOnNativeObjectProperties(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native object properties cannot be unset');
$this->compile('native-class-property-unset.php');
}
public function testRejectsExplicitNativeDestructorCall(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Explicit calls to native object destructors are not supported');
$this->compile('native-class-explicit-destructor-call.php');
}
public function testRejectsNativeObjectReferenceKeywordMethod(): void public function testRejectsNativeObjectReferenceKeywordMethod(): void
{ {
$this->expectException(TestError::class); $this->expectException(TestError::class);

@ -135,6 +135,7 @@ trait SourcePipelineTrait
} }
$files = $this->filterIgnoredFiles($files); $files = $this->filterIgnoredFiles($files);
$this->discoverNativeClassDeclarations($files);
// 分析 PHP 文件,预处理 // 分析 PHP 文件,预处理
foreach ($files as $k => $file) { foreach ($files as $k => $file) {
if (FileScanner::isPhpFile($file)) { if (FileScanner::isPhpFile($file)) {

@ -437,6 +437,8 @@ class CompilerBase implements PropertyAccessContext
protected array $globalVars = []; protected array $globalVars = [];
/** @var array<string, string> Global/static Native pointer slot => class name. */ /** @var array<string, string> Global/static Native pointer slot => class name. */
protected array $nativeGlobalObjects = []; protected array $nativeGlobalObjects = [];
/** @var array<string, string> Lowercase class name => declared Native class name. */
protected array $nativeClassDeclarations = [];
/** @var array<string, true> Request-reset initialization flags for Native static locals. */ /** @var array<string, true> Request-reset initialization flags for Native static locals. */
protected array $nativeStaticInitializers = []; protected array $nativeStaticInitializers = [];
protected bool $nativeTypes = false; protected bool $nativeTypes = false;
@ -4060,7 +4062,10 @@ class CompilerBase implements PropertyAccessContext
$this->assertExprCanBeUsedAsValue($expr->expr, 'eval operand'); $this->assertExprCanBeUsedAsValue($expr->expr, 'eval operand');
// 对 eval() 指令的 PHP 代码段禁止字面量优化 // 对 eval() 指令的 PHP 代码段禁止字面量优化
$expr->expr->setAttribute('noLiteralString', true); $expr->expr->setAttribute('noLiteralString', true);
return 'php::eval(' . $this->identifierToStr($expr->expr) . ')'; $source = $this->isNativeObjectClass($this->detectClassOfExpr($expr->expr))
? $this->parseExprToString($expr->expr)
: $this->identifierToStr($expr->expr);
return 'php::eval(' . $source . ')';
} }
protected function parseInclude(Expr\Include_ $expr): string protected function parseInclude(Expr\Include_ $expr): string
@ -4084,7 +4089,9 @@ class CompilerBase implements PropertyAccessContext
break; break;
} }
$fileName = $this->parseIdentifier($expr->expr); $fileName = $this->isNativeObjectClass($this->detectClassOfExpr($expr->expr))
? $this->parseExprToString($expr->expr)
: $this->parseIdentifier($expr->expr);
$scope = []; $scope = [];
foreach ($this->context->localVars as $name => $_type) { foreach ($this->context->localVars as $name => $_type) {
@ -4571,7 +4578,19 @@ class CompilerBase implements PropertyAccessContext
} }
$list = []; $list = [];
foreach ($expr->parts as $part) { foreach ($expr->parts as $part) {
$list[] = $this->identifierToStr($part); if (!$part instanceof Node\InterpolatedStringPart) {
$this->assertExprCanBeUsedAsValue($part, 'shell command interpolation value');
}
if ($part instanceof Node\InterpolatedStringPart) {
$list[] = $this->parseExpr($part);
} elseif ($this->isNativeObjectClass($this->detectClassOfExpr($part))) {
$list[] = $this->parseOrderedOperand(
new Expr\MethodCall($part, new Node\Identifier('toString')),
false,
);
} else {
$list[] = $this->parseOrderedOperand($part, false);
}
} }
return 'php::fn::shell_exec(php::concat({' . implode(', ', $list) . '}))'; return 'php::fn::shell_exec(php::concat({' . implode(', ', $list) . '}))';
} }

@ -18,6 +18,8 @@ class PropertyDef
public ?string $default = null; public ?string $default = null;
public ?ArrayInitPlan $arrayInitPlan = null; public ?ArrayInitPlan $arrayInitPlan = null;
public bool $nullable = false; public bool $nullable = false;
/** The declared type is the unconstrained `mixed`/`any` type. */
public bool $explicitMixed = false;
public string $class = ''; public string $class = '';
public array $typeCheck = []; public array $typeCheck = [];
public string $typeStr = ''; public string $typeStr = '';

@ -659,6 +659,13 @@ trait CallArgumentGenerator
protected function materializeCallArgValue(NodeAbstract $value, string $expr): string protected function materializeCallArgValue(NodeAbstract $value, string $expr): string
{ {
// A Native property fetch is a typed C++ pointer, never an INDIRECT
// zval. Passing it through php_deindirect() would box the pointer as a
// bool/Variant and break the Native ABI. Dynamic Zend calls reject the
// value before reaching here; direct Native calls keep it unchanged.
if ($this->isNativeObjectClass($this->detectClassOfExpr($value))) {
return $expr;
}
// A call that returns by reference yields a live php::Ref aliasing the // A call that returns by reference yields a live php::Ref aliasing the
// callee's storage. When such a call feeds a by-value argument, PHP takes // callee's storage. When such a call feeds a by-value argument, PHP takes
// a value snapshot at evaluation time (left to right), so later mutations // a value snapshot at evaluation time (left to right), so later mutations

@ -258,7 +258,13 @@ trait NativeClassSupportTrait
protected function isNativeObjectClass(string $class): bool protected function isNativeObjectClass(string $class): bool
{ {
$class = ltrim($class, '\\'); $class = ltrim($class, '\\');
return $class !== '' && $this->hasClass($class) && $this->getClass($class)->nativeObject; if ($class === '') {
return false;
}
if (isset($this->nativeClassDeclarations[strtolower($class)])) {
return true;
}
return $this->hasClass($class) && $this->getClass($class)->nativeObject;
} }
/** /**
@ -361,10 +367,25 @@ trait NativeClassSupportTrait
protected function getNativeObjectCppName(string|ClassDef $class): string protected function getNativeObjectCppName(string|ClassDef $class): string
{ {
$classDef = $class instanceof ClassDef ? $class : $this->getClass(ltrim($class, '\\')); if ($class instanceof ClassDef) {
return self::PREFIX . $this->getNativeName('', $class->namespace, $class->name);
}
$class = ltrim($class, '\\');
if ($this->hasClass($class)) {
$classDef = $this->getClass($class);
return self::PREFIX . $this->getNativeName('', $classDef->namespace, $classDef->name); return self::PREFIX . $this->getNativeName('', $classDef->namespace, $classDef->name);
} }
// The Native declaration catalog is built before semantic
// preprocessing, so signatures may name a Native class declared in a
// later file. Its C++ symbol is derivable from the fully-qualified PHP
// name without requiring the complete ClassDef yet.
$separator = strrpos($class, '\\');
$namespace = $separator === false ? '' : substr($class, 0, $separator);
$name = $separator === false ? $class : substr($class, $separator + 1);
return self::PREFIX . $this->getNativeName('', $namespace, $name);
}
protected function getNativeObjectDescriptorName(string|ClassDef $class): string protected function getNativeObjectDescriptorName(string|ClassDef $class): string
{ {
return $this->getNativeObjectCppName($class) . '__type'; return $this->getNativeObjectCppName($class) . '__type';
@ -564,16 +585,44 @@ trait NativeClassSupportTrait
} }
/** /**
* Native objects already have reference semantics: variables contain a * Validate a Native reference entirely from compile-time metadata.
* typed pointer and assignment copies only that pointer. PHP references *
* would alias the pointer slot itself, which has no useful Native ABI * A Native object variable is a typed pointer and must never expose its
* representation and would make a typed slot possible to rebind through * pointer slot as a PHP reference. A Native property may expose a reference
* an untyped reference. * only when it was explicitly declared `mixed`/`any`: that field is an
* unconstrained php::Var slot. Fixed-layout fields and constrained Variant
* fields must reject references because a later reference write could
* bypass their declared type.
*/ */
protected function assertNativeObjectReferenceForbidden( protected function assertNativeObjectReferenceForbidden(
NodeAbstract $expr, NodeAbstract $expr,
NodeAbstract $errorNode, NodeAbstract $errorNode,
): void { ): void {
if ($expr instanceof Node\Expr\PropertyFetch) {
$receiverClass = $this->detectClassOfExpr($expr->var);
if ($this->isNativeObjectClass($receiverClass)) {
if (!$expr->name instanceof Node\Identifier) {
$this->fatalError($errorNode, 'Dynamic native object property access is not supported');
}
$property = $expr->name->toString();
$resolution = $this->resolveNativeInstanceProperty($expr, $property, $receiverClass);
if ($resolution === null) {
$this->fatalError(
$errorNode,
"Native class `{$receiverClass}` has no property `\${$property}`",
);
}
$this->applyNativePropertyAccessResult($expr, $resolution);
$definition = $resolution->propertyDef;
if (!$definition->explicitMixed || $definition->getter !== null || $definition->setter !== null) {
$this->fatalError(
$errorNode,
'Only Native object properties declared as any or mixed can be referenced',
);
}
return;
}
}
$class = $this->detectDeclaredClassOfExpr($expr); $class = $this->detectDeclaredClassOfExpr($expr);
if ($this->isNativeObjectClass($class)) { if ($this->isNativeObjectClass($class)) {
$this->fatalError( $this->fatalError(

@ -246,20 +246,16 @@ trait AssignOpTrait
if ($allowed && ($this->hasScopeGlobalVar($leftName) || $this->hasStaticVar($leftName))) { if ($allowed && ($this->hasScopeGlobalVar($leftName) || $this->hasStaticVar($leftName))) {
$this->promoteGlobalOrStaticToNativeObject($leftName, $rightClass, $right); $this->promoteGlobalOrStaticToNativeObject($leftName, $rightClass, $right);
} }
} elseif ($left instanceof Expr\PropertyFetch } elseif ($left instanceof Expr\PropertyFetch && $this->isIdExpr($left->name)) {
&& $this->isVarExpr($left->var) $receiverClass = $this->detectClassOfExpr($left->var);
&& $this->isIdExpr($left->name) if ($this->isNativeObjectClass($receiverClass)) {
) { $propertyName = $left->name->toString();
$receiver = $this->parseVariable($left->var); $resolution = $this->resolveNativeInstanceProperty($left, $propertyName, $receiverClass);
if ($this->isNativeObjectVar($receiver)) { if ($resolution !== null) {
$property = $this->findNativeObjectProperty( $this->applyNativePropertyAccessResult($left, $resolution);
$this->getNativeObjectVarClass($receiver), }
$left->name->toString(), $property = $resolution?->propertyDef;
); if ($property !== null && $property->class !== '' && $this->isInterface($property->class)) {
if ($property !== null
&& $property->class !== ''
&& $this->isInterface($property->class)
) {
$this->fatalError( $this->fatalError(
$left, $left,
'Native objects cannot be assigned to interface-typed properties', 'Native objects cannot be assigned to interface-typed properties',
@ -306,42 +302,46 @@ trait AssignOpTrait
$this->fatalError($left, 'Cannot write to read-only hooked property'); $this->fatalError($left, 'Cannot write to read-only hooked property');
} }
if ($left instanceof Expr\PropertyFetch if ($left instanceof Expr\PropertyFetch && $this->isIdExpr($left->name)) {
&& $this->isVarExpr($left->var) $receiverClass = $this->detectClassOfExpr($left->var);
&& $this->isIdExpr($left->name) if ($this->isNativeObjectClass($receiverClass)) {
) { $property = $left->name->toString();
$object = $this->parseIdentifier($left->var);
if ($this->isNativeObjectVar($object)) {
$property = $this->parseIdentifier($left->name);
$class = $this->getNativeObjectVarClass($object);
$access = $this->getNativePropertyAccess($left); $access = $this->getNativePropertyAccess($left);
if ($access === null) { if ($access === null) {
$this->fatalError($left, "Native class `{$class}` has no property `\${$property}`"); $resolution = $this->resolveNativeInstanceProperty($left, $property, $receiverClass);
if ($resolution === null) {
$this->fatalError($left, "Native class `{$receiverClass}` has no property `\${$property}`");
}
$this->applyNativePropertyAccessResult($left, $resolution);
$access = $this->getNativePropertyAccess($left);
} }
$def = $access->getPropertyDef(); $def = $access->getPropertyDef();
$field = $this->getNativeObjectPropertyCppName($def, $access->getClassDef());
$rightExpr = $this->parseExprAsValue($right); // Parse and materialize the receiver before the right-hand
// expression. PHP evaluates an object/property target before
// its assigned value, and C++ operand order must not decide it.
$leftExpr = $this->parsePropertyFetch($left);
if ($def->type === Type::OBJECT && $this->isNativeObjectClass($def->class)) { if ($def->type === Type::OBJECT && $this->isNativeObjectClass($def->class)) {
if ($this->isNull($right)) { if ($this->isNull($right)) {
if (!$def->nullable) { if (!$def->nullable) {
$this->fatalError($right, "Cannot assign null to native property `{$class}::\${$property}`"); $this->fatalError($right, "Cannot assign null to native property `{$receiverClass}::\${$property}`");
} }
return $this->getNativeObjectMemberReceiver($object) return $leftExpr . ' = nullptr';
. $field . ' = nullptr';
} }
$rightClass = $this->detectClassOfExpr($right); $rightClass = $this->detectClassOfExpr($right);
if ($rightClass === '' || !$this->isObjectClassStaticallyAssignableTo($rightClass, $def->class)) { if ($rightClass === '' || !$this->isObjectClassStaticallyAssignableTo($rightClass, $def->class)) {
$this->fatalError($right, "Cannot assign value to native property `{$class}::\${$property}`"); $this->fatalError($right, "Cannot assign value to native property `{$receiverClass}::\${$property}`");
} }
} else { return $leftExpr . ' = ' . $this->parseExprAsValue($right);
}
$this->assertCanAssignPropertyWrite($propertyWriteTarget, $right); $this->assertCanAssignPropertyWrite($propertyWriteTarget, $right);
$rightExpr = $this->parseExprAsValue($right);
$rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr); $rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr);
if ($def->type !== Type::VAR) { if ($def->type !== Type::VAR) {
$rightExpr = $this->convertExprFromType($def->type, $rightExpr); $rightExpr = $this->convertExprFromType($def->type, $rightExpr);
} }
} return $leftExpr . ' = ' . $rightExpr;
return $this->getNativeObjectMemberReceiver($object)
. $field . ' = ' . $rightExpr;
} }
} }

@ -207,6 +207,12 @@ trait PropertyAccessTrait
protected function emitDynamicPropertyFetchRef(Expr\PropertyFetch $expr, NodeAbstract $errorNode): string protected function emitDynamicPropertyFetchRef(Expr\PropertyFetch $expr, NodeAbstract $errorNode): string
{ {
$receiverClass = $this->detectClassOfExpr($expr->var);
if ($this->isNativeObjectClass($receiverClass)) {
$this->assertNativeObjectReferenceForbidden($expr, $errorNode);
return $this->parsePropertyFetch($expr) . '.toReference()';
}
// Reference diagnostics are more specific than the generic readonly // Reference diagnostics are more specific than the generic readonly
// mutation error emitted by preparePropertyWriteTarget(). // mutation error emitted by preparePropertyWriteTarget().
$target = $this->preparePropertyWriteTarget($expr, true); $target = $this->preparePropertyWriteTarget($expr, true);

@ -21,6 +21,7 @@ use TypePhp\Entity\PropertyDef;
use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic; use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
use TypePhp\Exception\SyntaxError; use TypePhp\Exception\SyntaxError;
use TypePhp\Transform\PropertyHookLowering; use TypePhp\Transform\PropertyHookLowering;
use TypePhp\Transform\CompileTimeAttribute;
use TypePhp\Transform\NativeClassAttributeLowering; use TypePhp\Transform\NativeClassAttributeLowering;
use TypePhp\Transform\PrinterLowering; use TypePhp\Transform\PrinterLowering;
use TypePhp\Transform\ArrayableLowering; use TypePhp\Transform\ArrayableLowering;
@ -42,6 +43,69 @@ use PhpParser\NodeVisitor\NameResolver;
class Preprocessor extends CompilerBase class Preprocessor extends CompilerBase
{ {
/**
* Discover Native class names before parsing any signatures or fields.
*
* PHP permits forward class references across both declaration and file
* order. Native fields need the same property while choosing a concrete
* C++ pointer type, so waiting for prepareClass() would be order-dependent.
* Only files which mention both an attribute and "Native" are parsed in
* this lightweight pass; ordinary projects pay no second parse cost.
*
* @param list<string> $files
*/
public function discoverNativeClassDeclarations(array $files): void
{
foreach ($files as $file) {
if (!$this->isPhpFileForNativeDiscovery($file)) {
continue;
}
$source = file_get_contents($file);
if (!is_string($source)
|| !str_contains($source, '#[')
|| stripos($source, 'native') === false
) {
continue;
}
try {
$ast = $this->parser->parse($source);
} catch (\PhpParser\Error) {
// prepareFile() owns the normal source diagnostic, including
// the filename and compiler formatting. Avoid reporting a
// syntax error twice from this declaration-only pass.
continue;
}
$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
$ast = $traverser->traverse($ast);
$this->discoverNativeClassDeclarationsInAst($ast);
}
}
private function isPhpFileForNativeDiscovery(string $file): bool
{
return str_ends_with(strtolower($file), '.php');
}
/** @param array<Node\Stmt> $ast */
private function discoverNativeClassDeclarationsInAst(array $ast): void
{
$finder = new NodeFinder();
foreach ($finder->findInstanceOf($ast, Node\Stmt\Class_::class) as $class) {
if ($class->name === null
|| (!NativeClassAttributeLowering::isNative($class)
&& CompileTimeAttribute::find($class, 'Native') === null)
) {
continue;
}
$name = isset($class->namespacedName)
? $class->namespacedName->toString()
: $class->name->toString();
$name = ltrim($name, '\\');
$this->nativeClassDeclarations[strtolower($name)] = $name;
}
}
public function getSortedFiles(array $list): array public function getSortedFiles(array $list): array
{ {
$sorter = new StringSort(); $sorter = new StringSort();
@ -158,6 +222,10 @@ class Preprocessor extends CompilerBase
$traverser->addVisitor(new ConstantExpressionValidationVisitor($this->phpVersion)); $traverser->addVisitor(new ConstantExpressionValidationVisitor($this->phpVersion));
$traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file)); $traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file));
$stmts = $traverser->traverse($ast); $stmts = $traverser->traverse($ast);
// CompilerTest and embedding users may invoke prepareFile()
// directly instead of the project pipeline. Preserve same-file
// forward Native references for that public entry path as well.
$this->discoverNativeClassDeclarationsInAst($stmts);
foreach ($stmts as $v) { foreach ($stmts as $v) {
$type = $v->getType(); $type = $v->getType();
@ -1288,6 +1356,17 @@ class Preprocessor extends CompilerBase
} }
$propDef = new PropertyDef($name, $flags, $type, $default, $nullable); $propDef = new PropertyDef($name, $flags, $type, $default, $nullable);
if ($typeNode !== null
&& !$typeNode instanceof NullableType
&& !$typeNode instanceof UnionType
&& !$typeNode instanceof IntersectionType
) {
$propDef->explicitMixed = in_array(
strtolower($this->parseIdentifier($typeNode)),
['mixed', 'any'],
true,
);
}
$propDef->readonly = (bool) (($flags | $this->classDef->flags) & Modifiers::READONLY); $propDef->readonly = (bool) (($flags | $this->classDef->flags) & Modifiers::READONLY);
$propDef->class = $class; $propDef->class = $class;
$propDef->arrayInitPlan = $arrayInitPlan; $propDef->arrayInitPlan = $arrayInitPlan;
@ -1472,7 +1551,7 @@ class Preprocessor extends CompilerBase
'null' => ['null'], 'null' => ['null'],
'object' => [], // no literal object default exists 'object' => [], // no literal object default exists
'self', 'parent', 'static' => [], 'self', 'parent', 'static' => [],
'mixed' => null, 'mixed', 'any' => null,
'callable' => null, // string/array/closure — not checkable 'callable' => null, // string/array/closure — not checkable
default => [], // class type: only null via ?Type default => [], // class type: only null via ?Type
}; };

@ -0,0 +1,44 @@
--TEST--
Native any properties support PHP references without runtime type dispatch
--FILE--
<?php
#[Native]
class NativeAnyReference
{
// `mixed` is the PHP spelling of TypePHP's equivalent `any` storage.
public mixed $value = 1;
public ?NativeAnyReference $child;
}
function replaceAny(mixed &$value, mixed $replacement): void
{
$value = $replacement;
}
function main(): void
{
$object = new NativeAnyReference();
$reference =& $object->value;
$reference = 'changed';
var_dump($object->value);
$object->value = 42;
var_dump($reference);
$object->child = new NativeAnyReference();
$childReference =& $object->child->value;
replaceAny($childReference, ['native', 'reference']);
var_dump($object->child->value);
}
?>
--EXPECT--
string(7) "changed"
int(42)
array(2) {
[0]=>
string(6) "native"
[1]=>
string(9) "reference"
}

@ -0,0 +1,37 @@
--TEST--
Native class: mutually referencing property types use pointer fields and forward declarations
--FILE--
<?php
function roundTripMutual(?NativeMutualRight $value): ?NativeMutualRight
{
return $value;
}
#[Native]
class NativeMutualLeft
{
public string $name = 'left';
public ?NativeMutualRight $right;
}
#[Native]
class NativeMutualRight
{
public string $name = 'right';
public ?NativeMutualLeft $left;
}
function main(): void
{
$left = new NativeMutualLeft();
$right = new NativeMutualRight();
$left->right = $right;
$right->left = $left;
echo roundTripMutual($left->right)->name, ':', $right->left->name, "\n";
}
?>
--EXPECT--
right:left

@ -0,0 +1,40 @@
--TEST--
Native class: direct writes support chained and expression receivers
--FILE--
<?php
#[Native]
class NativeWriteNode
{
public int $value;
public ?NativeWriteNode $child;
public function __construct(int $value)
{
$this->value = $value;
}
}
function makeNativeWriteNode(int $value): NativeWriteNode
{
return new NativeWriteNode($value);
}
function main(): void
{
$root = new NativeWriteNode(1);
$root->child = new NativeWriteNode(2);
$leaf = new NativeWriteNode(3);
$root->child->child = $leaf;
echo $root->child->child->value, "\n";
$replacement = new NativeWriteNode(4);
makeNativeWriteNode(5)->child = $replacement;
echo $replacement->value, "\n";
}
?>
--EXPECT--
3
4

@ -0,0 +1,29 @@
--TEST--
Native class: shell command interpolation uses the declared toString method
--SKIPIF--
<?php
if (PHP_OS_FAMILY === 'Windows') {
die('skip shell command is POSIX-specific');
}
?>
--FILE--
<?php
#[Native]
class NativeShellCommand
{
public function toString(): string
{
return 'printf native-shell';
}
}
function main(): void
{
$command = new NativeShellCommand();
echo `$command`, "\n";
}
?>
--EXPECT--
native-shell

@ -0,0 +1,34 @@
--TEST--
Native class: include and eval operands use the declared toString method
--FILE--
<?php
#[Native]
class NativeStringOperand
{
public string $value;
public function __construct(string $value)
{
$this->value = $value;
}
public function toString(): string
{
return $this->value;
}
}
function main(): void
{
$path = new NativeStringOperand(__DIR__ . '/include-native-path.inc');
include $path;
$source = new NativeStringOperand('echo "evaluated\\n";');
eval($source);
}
?>
--EXPECT--
included
evaluated
Loading…
Cancel
Save