From 011cb31882397f35f9ad6395d42e95ab1cd8763a Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Wed, 9 Sep 2026 08:29:34 +0800 Subject: [PATCH] feat(native-class): enable direct binding of fixed properties to typed references - Allow int, float, bool, string, and array Native Class properties to be passed directly to matching reference parameters on statically resolved calls - Generate call-scoped C++ T& bindings for typed reference parameters instead of creating Zend references - Maintain compile-time rooting of Native receiver objects during typed calls - Add new parseNativeTypedPropertyReferenceArg method to handle typed property reference arguments - Update documentation to reflect new typed reference capabilities for fixed Native properties - Create new test case covering typed property reference functionality - Modify changelog to document the --- CHANGELOG.md | 14 ++++ docs/en/INCOMPATIBLE_PHP_FEATURES.md | 6 ++ docs/en/NATIVE_CLASS_OBJECT.md | 8 +- src/NativeClass/NativeClassSupportTrait.php | 62 +++++++++++++- .../NativeTypeCompatibilityTrait.php | 18 ++++ .../native-class/any-property-reference.phpt | 10 +++ .../typed-property-reference.phpt | 84 +++++++++++++++++++ 7 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 tests/compiler/native-class/typed-property-reference.phpt diff --git a/CHANGELOG.md b/CHANGELOG.md index 500c5ac2..5e51217f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,14 @@ box the value or allocate a Zend reference. Bindings must be unconditional, one-time, function-local, and non-escaping. Rebinding, `unset`, reference capture/return, and storing such a reference into PHP storage are rejected. +Fixed `int`, `float`, `bool`, `string`, and `array` Native Class properties can +now be passed directly to an exactly matching reference parameter on a +statically resolved call. This lowers to a call-scoped C++ `T&` while precisely +rooting the Native receiver. It does not enable general PHP references for the +field: `=&`, `std::ref()`, dynamic calls, and escaping reference forms remain +forbidden. Explicit `any` properties continue to use the dynamic Zend reference +model. + Dynamic calls and Closures retain the existing Zend reference path and require explicit `std::ref()` / `toRef()`. A call-scoped bridge validates the value on write-back and rejects an escaping temporary reference. Use `std::any()` for @@ -112,6 +120,12 @@ should review the change log and run their full test suite before upgrading. 不装箱、不创建 Zend reference。绑定必须位于函数顶层、只发生一次且不得逃逸;重新绑定、 `unset`、引用捕获/返回,或把引用存入 PHP 槽位都会在编译期拒绝。 +Native Class 中固定类型为 `int`、`float`、`bool`、`string`、`array` 的属性,现在也可 +直接传给静态可解析调用中类型完全匹配的引用参数。编译器将其生成为仅在本次调用期间 +有效的 C++ `T&`,同时精确保活 Native 接收对象;这并不会为字段开放通用 PHP 引用, +`=&`、`std::ref()`、动态调用及其他可能逃逸的引用形式仍被禁止。显式声明为 `any` 的 +属性继续使用动态 Zend reference 模型。 + 动态调用与 Closure 继续使用既有 Zend reference 路径,并要求显式使用 `std::ref()` / `toRef()`。调用级 bridge 在返回时检查类型并拒绝临时引用逃逸;需要完整 PHP 引用身份时 应使用 `std::any()`。固定 object/resource/stream、高精度值、Native/typed object、Box diff --git a/docs/en/INCOMPATIBLE_PHP_FEATURES.md b/docs/en/INCOMPATIBLE_PHP_FEATURES.md index 2d2e0bbb..c5578323 100644 --- a/docs/en/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/en/INCOMPATIBLE_PHP_FEATURES.md @@ -122,6 +122,12 @@ incompatible with or more restrictive than standard PHP. slot would weaken the type system. Typed object/static properties remain reference-capable because Zend attaches property type sources; PHP array elements remain dynamic reference-capable slots. +- A fixed `int`, `float`, `bool`, `string`, or `array` Native Class property may + be passed directly to an exactly matching reference parameter on a statically + resolved call. This is a call-scoped C++ `T&`, not a PHP reference: `=&`, + `std::ref()`, dynamic calls, reference returns, and other escaping forms remain + forbidden for fixed Native properties. Only a Native property explicitly + declared `any` supports the ordinary dynamic PHP reference model. - A call that uses argument unpacking followed by named arguments falls back to dynamic dispatch and cannot use the native call path. diff --git a/docs/en/NATIVE_CLASS_OBJECT.md b/docs/en/NATIVE_CLASS_OBJECT.md index bccab245..d7be36cc 100644 --- a/docs/en/NATIVE_CLASS_OBJECT.md +++ b/docs/en/NATIVE_CLASS_OBJECT.md @@ -274,10 +274,12 @@ Allowing fields to hold ZendVM values does not mean the Native Class Object itse Whether a Native property may be taken by reference must be decided entirely at compile time from declaration metadata, without generating runtime type branches: +- A fixed `bool`, `int`, `float`, `string`, or `array` field may be passed directly to an exactly matching typed-reference parameter on a statically resolved call. The compiler binds the field as a call-scoped C++ `T&`; no `zval` or Zend reference is created, and the receiver remains precisely rooted through the complete statement. +- This typed-call path does not make general PHP reference acquisition legal. Fixed fields still reject `$ref =& $object->property`, `std::ref($object->property)`, returning by reference, dynamic calls, and every other form in which the reference could escape the statically resolved call. - Only `any` properties allow `$ref =& $object->property`; this is an explicit choice to allow Zend dynamic code to replace the slot value. - `mixed` also uses `php::Var` storage but still rejects taking references; in Native Class all declared types except `any` must maintain compile-time type constraints. -- `bool`, `int`, `float`, and other fixed-layout fields cannot represent PHP references and are rejected at compile time. -- `string`, `array`, `object`, Stream, and high-precision types have PHPX wrapper layers but are still fixed declared types; reference writes would bypass type constraints, so they are rejected at compile time. +- `bool`, `int`, `float`, `string`, and `array` fields cannot represent general PHP references; their only additional reference form is the exact, statically resolved typed-call path above. +- `object`, Stream, and high-precision types have PHPX wrapper layers but are still fixed declared types; reference writes would bypass type constraints, so they are rejected at compile time. - nullable, union, and intersection constrained `php::Var` fields also reject references; they cannot be allowed merely because the underlying storage is also `php::Var`. - Properties with Property Hook have no physical slot to expose, and always reject references. @@ -1201,7 +1203,7 @@ Explicit conversion makes the allocation cost and the object-graph conversion bo | Nullable Native parameters/returns | Supports `?NativeClass`, denoted by `nullptr`; member access must check or first prove non-null | | `&` on Native parameters/returns | Not supported; compile-time FatalError | | Taking references to Native Object variables | Not supported; ordinary assignment already shares object identity | -| Taking references to Native properties | Only fields explicitly declared `any` are supported; all other fields including `mixed` are compile-time FatalError | +| Taking references to Native properties | Fixed `bool`/`int`/`float`/`string`/`array` fields can be passed directly to an exact typed-reference parameter on a statically resolved call; only explicit `any` supports general PHP references; all other forms are compile-time FatalError | | Native variadic, union/intersection | Not supported; compile-time FatalError | | `__construct()` | Supported | | `clone` / `__clone()` | Supported | diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index a4069d2d..9b7515da 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -919,13 +919,69 @@ trait NativeClassSupportTrait } } + /** + * Lower a fixed Native property directly to the matching typed-reference + * ABI. The receiver is materialized as a precise Native root before the + * final C++ call so argument evaluation order cannot rebind or collect it. + * + * This is intentionally narrower than PHP reference acquisition: the + * resulting T& exists only for the statically resolved call. It does not + * make `$alias =& $object->property` or std::ref($object->property) legal. + */ + protected function parseNativeTypedPropertyReferenceArg( + NodeAbstract $expr, + string $referenceType, + NodeAbstract $errorNode, + ): ?string { + if (!$expr instanceof Node\Expr\PropertyFetch) { + return null; + } + + $receiverClass = $this->detectClassOfExpr($expr->var); + if (!$this->isNativeObjectClass($receiverClass)) { + return null; + } + 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->nullable + || $definition->getter !== null + || $definition->setter !== null + || Type::getReferenceType($definition->type) !== $referenceType + ) { + return null; + } + + $this->assertReadonlyPropertyReferenceForbidden($expr, $errorNode, false); + $this->assertPropertySetVisibility($expr); + + $receiver = $this->materializeNativeObjectReceiver($expr->var, $receiverClass); + $this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_VAR); + return $this->getNativeObjectMemberReceiver($receiver) + . $this->getNativeObjectPropertyCppName($definition, $resolution->classDef); + } + /** * Validate a Native reference entirely from compile-time metadata. * * A Native object variable is a typed pointer and must never expose its - * pointer slot as a PHP reference. A Native property may expose a reference - * only when it was explicitly declared `any`: that field intentionally - * permits arbitrary PHP values. Every other declaration, including + * pointer slot as a PHP reference. A Native property may expose a Zend + * reference only when it was explicitly declared `any`: that field + * intentionally permits arbitrary PHP values. Fixed fields passed directly + * to an exact typed-reference parameter are handled before this check and + * never expose a Zend reference. Every other declaration, including * `mixed`, must reject references because dynamic Zend code could replace * the referenced value with one that violates the Native field contract. */ diff --git a/src/TypeSystem/NativeTypeCompatibilityTrait.php b/src/TypeSystem/NativeTypeCompatibilityTrait.php index 3d4ce951..e030646d 100644 --- a/src/TypeSystem/NativeTypeCompatibilityTrait.php +++ b/src/TypeSystem/NativeTypeCompatibilityTrait.php @@ -232,6 +232,24 @@ trait NativeTypeCompatibilityTrait } if ($argInfo->byRef) { + // A fixed Native field already is the exact C++ storage required by + // a typed-reference parameter. Bind the call directly to that field + // instead of attempting to manufacture a Zend reference around it. + // Explicit std::ref()/toRef() remains a dynamic-reference request and + // deliberately follows the ordinary Native reference restrictions. + if (Type::isTypedRefType($argInfo->type) + && !$this->isReferenceWrapperCall($arg->value) + ) { + $nativeProperty = $this->parseNativeTypedPropertyReferenceArg( + $arg->value, + $argInfo->type, + $arg, + ); + if ($nativeProperty !== null) { + return $nativeProperty; + } + } + if ($this->isReferenceWrapperCall($arg->value)) { $inner = $this->unwrapReferenceWrapperCall($arg->value, $arg); $this->assertNativeObjectReferenceForbidden($inner, $arg); diff --git a/tests/compiler/native-class/any-property-reference.phpt b/tests/compiler/native-class/any-property-reference.phpt index 7c4c79aa..4e0b2339 100644 --- a/tests/compiler/native-class/any-property-reference.phpt +++ b/tests/compiler/native-class/any-property-reference.phpt @@ -15,6 +15,11 @@ function replaceAny(mixed &$value, mixed $replacement): void $value = $replacement; } +function appendNativeAnyString(string &$value): void +{ + $value .= ':typed'; +} + function &getNativeAnyReference(NativeAnyReference $object): mixed { return $object->value; @@ -46,6 +51,10 @@ function main(): void $returnedReference =& getNativeAnyReference($object); $returnedReference = 'returned reference'; var_dump($source); + + $object->value = 'dynamic'; + appendNativeAnyString($object->value); + var_dump($object->value); } ?> @@ -61,3 +70,4 @@ array(2) { string(15) "direct argument" string(14) "source changed" string(18) "returned reference" +string(13) "dynamic:typed" diff --git a/tests/compiler/native-class/typed-property-reference.phpt b/tests/compiler/native-class/typed-property-reference.phpt new file mode 100644 index 00000000..b3842322 --- /dev/null +++ b/tests/compiler/native-class/typed-property-reference.phpt @@ -0,0 +1,84 @@ +--TEST-- +Native class fixed properties bind directly to strong typed references +--FILE-- +number); + } +} + +function mutateNativeInt(int &$value): void +{ + $value += 10; +} + +function mutateNativeFloat(float &$value): void +{ + $value += 0.25; +} + +function mutateNativeBool(bool &$value): void +{ + $value = !$value; +} + +function mutateNativeString(string &$value): void +{ + $value .= ':changed'; +} + +function mutateNativeArray(array &$value): void +{ + $value[] = 2; +} + +function main(): void +{ + $object = new NativeTypedReferenceValues(); + mutateNativeInt($object->number); + mutateNativeFloat($object->decimal); + mutateNativeBool($object->enabled); + mutateNativeString($object->text); + mutateNativeArray($object->items); + var_dump($object->number, $object->decimal, $object->enabled, $object->text, $object->items); + + $object->increment($object->number); + $object->mutateOwnNumber(); + var_dump($object->number); + + $object->child = new NativeTypedReferenceValues(); + mutateNativeInt(value: $object->child->number); + var_dump($object->child->number); +} + +?> +--EXPECT-- +int(11) +float(1.75) +bool(true) +string(14) "native:changed" +array(2) { + [0]=> + int(1) + [1]=> + int(2) +} +int(22) +int(11)