From b3898c329de1e33a568c4556e6f667882bf71a5a Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 22 Sep 2026 14:10:03 +0800 Subject: [PATCH] feat(native): add fixed-width scalar fields --- docs/en/NATIVE_CLASS_OBJECT.md | 5 + docs/zh-cn/NATIVE_CLASS_OBJECT.md | 5 + phpunit/code/native-class-uint64-property.php | 7 ++ phpunit/code/native-stack-promotion.php | 9 ++ .../NativeClass/NativeClassValidationTest.php | 9 ++ .../NativeObjectStackPromotionCodegenTest.php | 4 + src/CompilerBase.php | 18 ++- src/Entity/PropertyDef.php | 2 + src/NativeClass/NativeClassSupportTrait.php | 12 ++ src/Parser/AssignOpTrait.php | 13 +- src/Parser/BinaryOpTrait.php | 7 ++ src/Parser/PropertyAccessTrait.php | 10 +- src/Preprocessor.php | 2 + src/Transform/NativePropertyTypeLowering.php | 119 ++++++++++++++++++ src/Transform/Visitor.php | 7 ++ src/Translator.php | 5 +- .../native-class/fixed-width-properties.phpt | 99 +++++++++++++++ 17 files changed, 324 insertions(+), 9 deletions(-) create mode 100644 phpunit/code/native-class-uint64-property.php create mode 100644 src/Transform/NativePropertyTypeLowering.php create mode 100644 tests/compiler/native-class/fixed-width-properties.phpt diff --git a/docs/en/NATIVE_CLASS_OBJECT.md b/docs/en/NATIVE_CLASS_OBJECT.md index 1e985963..0839888e 100644 --- a/docs/en/NATIVE_CLASS_OBJECT.md +++ b/docs/en/NATIVE_CLASS_OBJECT.md @@ -227,6 +227,9 @@ Type declarations are used to determine the C++ field layout at compile time. Th | `bool` | `php::Bool` | Native value field | | `int` | `php::Int` | Native value field | | `float` | `php::Float` | Native value field | +| `int8` / `int16` / `int32` | `int8_t` / `int16_t` / `int32_t` | Stored at the requested width; expressions use `php::Int` | +| `uint8` / `uint16` / `uint32` | `uint8_t` / `uint16_t` / `uint32_t` | Stored at the requested width; expressions use `php::Int` | +| `float32` | `float` | Four-byte storage; expressions use `php::Float` | | `string` | `php::Str` | The PHPX RAII string type currently used by TypePHP | | `array` | `php::Array` | PHPX RAII array, preserving PHP COW semantics | | Concrete Zend class | `php::Object` | Stores a Zend Object and validates the class at the assignment entry | @@ -240,6 +243,8 @@ Type declarations are used to determine the C++ field layout at compile time. Th `string`, `array`, Zend Object, Stream, and mixed fields are still located directly at fixed offsets in the C++ `struct`. The underlying zval or zend object they hold is managed by the PHPX RAII type, but property reads do not require a property hash table, object handler, or ZendVM dispatch. +Fixed-width scalar names apply only to Native Class properties. They select field storage rather than introducing new PHP expression types. Natural C++ alignment and tail padding still apply; TypePHP does not emit packed classes. A flat Native Class containing only scalar fields has the same payload layout and size as its generated C++ class. The Native Heap keeps its GC header immediately before that payload, outside the C++ object size. + PHP itself does not allow `resource` as a property type; stream resources in TypePHP should be declared with the existing `Stream` pseudo-type. Types that PHP itself forbids in property declarations, such as `void`, `never`, and `callable`, are likewise forbidden in Native Class. The following types are explicitly forbidden as Native Class property types: diff --git a/docs/zh-cn/NATIVE_CLASS_OBJECT.md b/docs/zh-cn/NATIVE_CLASS_OBJECT.md index eff8f7fc..58a2de55 100644 --- a/docs/zh-cn/NATIVE_CLASS_OBJECT.md +++ b/docs/zh-cn/NATIVE_CLASS_OBJECT.md @@ -242,6 +242,9 @@ final class InvalidContext | `bool` | `php::Bool` | 原生值字段 | | `int` | `php::Int` | 原生值字段 | | `float` | `php::Float` | 原生值字段 | +| `int8` / `int16` / `int32` | `int8_t` / `int16_t` / `int32_t` | 按指定位宽存储;表达式类型仍为 `php::Int` | +| `uint8` / `uint16` / `uint32` | `uint8_t` / `uint16_t` / `uint32_t` | 按指定位宽存储;表达式类型仍为 `php::Int` | +| `float32` | `float` | 4 字节存储;表达式类型仍为 `php::Float` | | `string` | `php::Str` | TypePHP 当前使用的 PHPX RAII 字符串类型 | | `array` | `php::Array` | PHPX RAII 数组,保留 PHP COW 语义 | | 确定的 Zend class | `php::Object` | 保存 Zend Object,并在赋值入口验证 class | @@ -255,6 +258,8 @@ final class InvalidContext `string`、`array`、Zend Object、Stream 和 mixed 字段仍然直接位于 C++ `struct` 的固定偏移处。它们持有的底层 zval 或 zend 对象由 PHPX RAII 类型管理,但属性读取不需要属性哈希表、object handler 或 ZendVM 分派。 +定宽标量名称只适用于 Native Class 属性,用于选择字段存储,不会引入新的 PHP 表达式类型。布局仍遵循 C++ 的自然对齐和尾部填充,TypePHP 不生成 packed class。仅包含标量字段且没有多态机制的 Native Class,其 payload 布局和尺寸与生成的 C++ class 一致;Native Heap 的 GC header 位于 payload 之前,不计入 C++ 对象尺寸。 + PHP 本身不允许将 `resource` 写成属性类型;TypePHP 中的 stream resource 应使用已有的 `Stream` 伪类型声明。`void`、`never`、`callable` 等 PHP 本身禁止用于属性声明的类型,在 Native Class 中同样禁止。 以下类型明确禁止作为 Native Class 属性类型: diff --git a/phpunit/code/native-class-uint64-property.php b/phpunit/code/native-class-uint64-property.php new file mode 100644 index 00000000..63bee51c --- /dev/null +++ b/phpunit/code/native-class-uint64-property.php @@ -0,0 +1,7 @@ +readFromBase(); } + +#[Native] +final class NativeCompactScalarFixture +{ + public int32 $number = 0; + public int8 $tag = 0; + public uint16 $flags = 0; + public float32 $ratio = 0.0; +} diff --git a/phpunit/src/NativeClass/NativeClassValidationTest.php b/phpunit/src/NativeClass/NativeClassValidationTest.php index 88fccfbe..ec807ef2 100644 --- a/phpunit/src/NativeClass/NativeClassValidationTest.php +++ b/phpunit/src/NativeClass/NativeClassValidationTest.php @@ -140,6 +140,15 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-static-property.php'); } + public function testRejectsUint64Property(): void + { + $this->expectException(\TypePhp\Exception\SyntaxError::class); + $this->expectExceptionMessage( + 'Native property type `uint64` is not supported because it cannot be represented by php::Int', + ); + $this->compile('native-class-uint64-property.php'); + } + public function testRejectsInheritanceAcrossObjectModels(): void { $this->expectException(TestError::class); diff --git a/phpunit/src/NativeClass/NativeObjectStackPromotionCodegenTest.php b/phpunit/src/NativeClass/NativeObjectStackPromotionCodegenTest.php index 62f61467..7ef22bf9 100644 --- a/phpunit/src/NativeClass/NativeObjectStackPromotionCodegenTest.php +++ b/phpunit/src/NativeClass/NativeObjectStackPromotionCodegenTest.php @@ -57,6 +57,10 @@ final class NativeObjectStackPromotionCodegenTest extends TestCase 'virtual ~php_nativestackpromotionchild() noexcept override;', $header, ); + self::assertStringContainsString('int32_t number = 0;', $header); + self::assertStringContainsString('int8_t tag = 0;', $header); + self::assertStringContainsString('uint16_t flags = 0;', $header); + self::assertStringContainsString('float ratio = 0;', $header); self::assertStringContainsString( 'virtual php::Int __typephp_virtual_php_nativestackpromotionbase__readfrombase() override;', $header, diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 7b397abf..dc7427ef 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -3768,7 +3768,11 @@ class CompilerBase implements PropertyAccessContext if ($this->isVarExpr($expr->var) && !$this->hasVar($var)) { $this->errorUndefinedVariable($expr->var); } - return '++' . $var; + $result = '++' . $var; + $property = $this->getNativePropertyDef($expr->var); + return $property === null + ? $result + : $this->promoteNativeObjectPropertyValue($property, $result); } /** @@ -4184,7 +4188,11 @@ class CompilerBase implements PropertyAccessContext $opName = $op === '+' ? '++' : '--'; $this->fatalError($expr, "Cannot use {$opName} on {$type}. Use " . ($op === '+' ? '+= 1' : '-= 1') . ' instead (Big* types are immutable).'); } - return $var . str_repeat($op, 2); + $result = $var . str_repeat($op, 2); + $property = $this->getNativePropertyDef($expr->var); + return $property === null + ? $result + : $this->promoteNativeObjectPropertyValue($property, $result); } if ($this->isStaticPropertyFetch($expr->var)) { $native = $this->parseNativeStaticPropertyFetch($expr->var); @@ -4237,7 +4245,11 @@ class CompilerBase implements PropertyAccessContext if ($this->isVarExpr($expr->var) && !$this->hasVar($var)) { $this->errorUndefinedVariable($expr->var); } - return '--' . $var; + $result = '--' . $var; + $property = $this->getNativePropertyDef($expr->var); + return $property === null + ? $result + : $this->promoteNativeObjectPropertyValue($property, $result); } protected function parsePrint(Expr\Print_ $expr): string diff --git a/src/Entity/PropertyDef.php b/src/Entity/PropertyDef.php index 52949792..0ce29174 100644 --- a/src/Entity/PropertyDef.php +++ b/src/Entity/PropertyDef.php @@ -28,6 +28,8 @@ class PropertyDef public string $class = ''; public array $typeCheck = []; public string $typeStr = ''; + /** Optional fixed-width C++ storage used only by Native Class fields. */ + public string $nativeStorageType = ''; public bool $promoted = false; public bool $readonly = false; /** The generated Zend property table cannot represent this default exactly. */ diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index 9001104e..bb8ccc45 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -1204,6 +1204,7 @@ trait NativeClassSupportTrait $definition = $resolution->propertyDef; if ($definition->nullable + || $definition->nativeStorageType !== '' || $definition->getter !== null || $definition->setter !== null || Type::getReferenceType($definition->type) !== $referenceType @@ -1871,6 +1872,9 @@ trait NativeClassSupportTrait protected function getNativeObjectPropertyType(PropertyDef $property): string { + if ($property->nativeStorageType !== '') { + return $property->nativeStorageType; + } if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) { return $this->getNativeObjectPointerType($property->class); } @@ -1884,6 +1888,14 @@ trait NativeClassSupportTrait }; } + protected function promoteNativeObjectPropertyValue(PropertyDef $property, string $expression): string + { + if ($property->nativeStorageType === '') { + return $expression; + } + return 'static_cast<' . $property->type . '>(' . $expression . ')'; + } + protected function getNativeObjectInitializerName(string|ClassDef $class): string { // Keep compiler-owned helpers outside the php_* user symbol namespace. diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 32f1b5d7..5e46785f 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -517,7 +517,7 @@ trait AssignOpTrait // 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); + $leftExpr = $this->parsePropertyFetchUpdate($left); if ($def->type === Type::OBJECT && $this->isNativeObjectClass($def->class)) { if ($this->isNull($right)) { if (!$def->nullable) { @@ -538,7 +538,10 @@ trait AssignOpTrait if ($def->type !== Type::VAR) { $rightExpr = $this->convertExprFromType($def->type, $rightExpr); } - return $leftExpr . ' = ' . $rightExpr; + return $this->promoteNativeObjectPropertyValue( + $def, + $leftExpr . ' = ' . $rightExpr, + ); } } @@ -1308,7 +1311,11 @@ trait AssignOpTrait ? $def->type : $rightType; - return $var . ' ' . $op . ' (' . $this->convertNativePropertyWriteExpr($def->type, $effectiveRightType, $rightExpr) . ')'; + return $this->promoteNativeObjectPropertyValue( + $def, + $var . ' ' . $op . ' (' + . $this->convertNativePropertyWriteExpr($def->type, $effectiveRightType, $rightExpr) . ')', + ); } protected function convertNativePropertyWriteExpr(string $propertyType, string $rightType, string $rightExpr): string diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index 62c34430..fed76ccb 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -759,6 +759,13 @@ trait BinaryOpTrait return $def->type; } } + $def = $this->getNativePropertyDef($expr); + if ($def !== null + && in_array($def->type, [Type::INT, Type::FLOAT, Type::BOOL], true) + && $this->isNativePropertyTypedValue($expr) + ) { + return $def->type; + } return Type::VAR; } diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 7cdbba18..c0b7931e 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -1256,8 +1256,11 @@ trait PropertyAccessTrait if ($this->isNativeObjectClass($nativeExpressionClass)) { $objectName = $this->materializeNativeObjectReceiver($object, $nativeExpressionClass); $this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_VAR); - return $this->getNativeObjectMemberReceiver($objectName) + $field = $this->getNativeObjectMemberReceiver($objectName) . $this->getNativeObjectPropertyCppName($resolution->propertyDef, $resolution->classDef); + return $this->isPropertyFetchUpdate($expr) + ? $field + : $this->promoteNativeObjectPropertyValue($resolution->propertyDef, $field); } $update = $this->isPropertyFetchUpdate($expr); @@ -1276,8 +1279,11 @@ trait PropertyAccessTrait $this->fatalError($expr, "Native class `{$class}` has no property `\${$propertyName}`"); } $this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_VAR); - return $this->getNativeObjectMemberReceiver($objectName) + $field = $this->getNativeObjectMemberReceiver($objectName) . $this->getNativeObjectPropertyCppName($resolution->propertyDef, $resolution->classDef); + return $this->isPropertyFetchUpdate($expr) + ? $field + : $this->promoteNativeObjectPropertyValue($resolution->propertyDef, $field); } $objectVar = $this->parenthesizeOpenOperand($objectName); $directMagic = !$update && !$this->isNativePropertyAccess($expr) diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 0a18b8ce..f34b5fb0 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -22,6 +22,7 @@ use TypePhp\Exception\SyntaxError; use TypePhp\Transform\PropertyHookLowering; use TypePhp\Transform\CompileTimeAttribute; use TypePhp\Transform\NativeClassAttributeLowering; +use TypePhp\Transform\NativePropertyTypeLowering; use TypePhp\Transform\NanoSyntaxValidationVisitor; use TypePhp\Transform\PrinterLowering; use TypePhp\Transform\ArrayableLowering; @@ -2439,6 +2440,7 @@ class Preprocessor extends CompilerBase false, ); $propDef->node = $errorNode; + $propDef->nativeStorageType = NativePropertyTypeLowering::getStorageType($errorNode); if ($typeNode !== null && !$typeNode instanceof NullableType && !$typeNode instanceof UnionType diff --git a/src/Transform/NativePropertyTypeLowering.php b/src/Transform/NativePropertyTypeLowering.php new file mode 100644 index 00000000..ce75332c --- /dev/null +++ b/src/Transform/NativePropertyTypeLowering.php @@ -0,0 +1,119 @@ + */ + private const array TYPES = [ + 'int8' => ['php' => 'int', 'cpp' => 'int8_t'], + 'int16' => ['php' => 'int', 'cpp' => 'int16_t'], + 'int32' => ['php' => 'int', 'cpp' => 'int32_t'], + 'uint8' => ['php' => 'int', 'cpp' => 'uint8_t'], + 'uint16' => ['php' => 'int', 'cpp' => 'uint16_t'], + 'uint32' => ['php' => 'int', 'cpp' => 'uint32_t'], + 'float32' => ['php' => 'float', 'cpp' => 'float'], + ]; + + private const array UNSUPPORTED_TYPES = [ + 'uint64' => 'it cannot be represented by php::Int', + ]; + + public static function lowerClass(Stmt\Class_ $class): void + { + foreach ($class->stmts as $stmt) { + if ($stmt instanceof Stmt\Property) { + $stmt->type = self::lowerDeclaration($stmt, $stmt->type); + continue; + } + if (!$stmt instanceof Stmt\ClassMethod || $stmt->name->toLowerString() !== '__construct') { + continue; + } + foreach ($stmt->params as $param) { + if ($param->isPromoted()) { + $param->type = self::lowerDeclaration($param, $param->type); + } + } + } + } + + public static function getStorageType(Node $declaration): string + { + $type = $declaration->getAttribute(self::STORAGE_TYPE_ATTRIBUTE, ''); + return is_string($type) ? $type : ''; + } + + private static function lowerDeclaration( + Node $declaration, + Node\ComplexType|Node\Identifier|Node\Name|null $type, + ): Node\ComplexType|Node\Identifier|Node\Name|null { + $unsupportedType = self::findType($type, self::UNSUPPORTED_TYPES); + if ($unsupportedType !== null) { + throw new SyntaxError( + "Native property type `{$unsupportedType}` is not supported because " + . self::UNSUPPORTED_TYPES[$unsupportedType], + ); + } + $fixedType = self::findFixedType($type); + if ($fixedType === null) { + return $type; + } + if (!$type instanceof Node\Name && !$type instanceof Node\Identifier) { + throw new SyntaxError( + "Native property type `{$fixedType}` must be declared directly and cannot be nullable or composite", + ); + } + + $mapping = self::TYPES[$fixedType]; + $declaration->setAttribute(self::STORAGE_TYPE_ATTRIBUTE, $mapping['cpp']); + return new Node\Identifier($mapping['php'], $type->getAttributes()); + } + + private static function findFixedType(?NodeAbstract $type): ?string + { + return self::findType($type, self::TYPES); + } + + /** @param array $types */ + private static function findType(?NodeAbstract $type, array $types): ?string + { + if ($type === null) { + return null; + } + if ($type instanceof Node\NullableType) { + return self::findType($type->type, $types); + } + if ($type instanceof Node\UnionType || $type instanceof Node\IntersectionType) { + foreach ($type->types as $member) { + $matchedType = self::findType($member, $types); + if ($matchedType !== null) { + return $matchedType; + } + } + return null; + } + if (!$type instanceof Node\Name && !$type instanceof Node\Identifier) { + return null; + } + + $name = strtolower($type->toString()); + return isset($types[$name]) ? $name : null; + } +} diff --git a/src/Transform/Visitor.php b/src/Transform/Visitor.php index c05152f5..3bca9867 100644 --- a/src/Transform/Visitor.php +++ b/src/Transform/Visitor.php @@ -132,6 +132,13 @@ class Visitor extends NodeVisitorAbstract $methods = []; $classReadonly = $node instanceof Stmt\Class_ && $node->isReadonly(); + if ($node instanceof Stmt\Class_ && NativeClassAttributeLowering::isNative($node)) { + $this->guard( + $node, + static fn () => NativePropertyTypeLowering::lowerClass($node), + 'Native', + ); + } foreach ($node->stmts as $stmt) { if ($stmt instanceof Stmt\Property) { foreach (PropertyHookLowering::lowerProperty($stmt) as $method) { diff --git a/src/Translator.php b/src/Translator.php index 4c97d1ad..24bc5b06 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -7900,7 +7900,10 @@ CODE; "Cannot override final property {$parentClass}::\${$name}" ); } - if ($childProp->type !== $parentProp->type || $childProp->class !== $parentProp->class) { + if ($childProp->type !== $parentProp->type + || $childProp->class !== $parentProp->class + || $childProp->nativeStorageType !== $parentProp->nativeStorageType + ) { $this->fatalError($classStmt, "Declaration of `{$className}::\${$name}` must be compatible " . "with `{$parentClass}::\${$name}`"); diff --git a/tests/compiler/native-class/fixed-width-properties.phpt b/tests/compiler/native-class/fixed-width-properties.phpt new file mode 100644 index 00000000..cee07cad --- /dev/null +++ b/tests/compiler/native-class/fixed-width-properties.phpt @@ -0,0 +1,99 @@ +--TEST-- +Native class: fixed-width scalar fields retain PHP scalar expression types +--FILE-- +i8 += 2; + $this->u16 *= 2; + } + + public function intSum(): int + { + return $this->u32 + $this->one; + } + + public function intCompare(): bool + { + return $this->u32 > $this->negative; + } + + public function floatSum(): float + { + return $this->f32 + $this->f32unit; + } +} + +#[Native] +final class MixedScalars +{ + public int $integer = 3; + public float $floating = 0.5; + public int8 $smallInteger = 2; + public float32 $smallFloat = 0.25; + + public function nativeSum(): float + { + return $this->integer + $this->floating; + } + + public function fixedIntSum(): float + { + return $this->smallInteger + $this->floating; + } + + public function fixedFloatSum(): float + { + return $this->integer + $this->smallFloat; + } +} + +function main(): void +{ + $value = new CompactScalars(); + $value->update(); + + var_dump( + $value->i8, + $value->i16, + $value->i32, + $value->u8, + $value->u16, + $value->u32, + $value->intSum(), + $value->intCompare(), + $value->floatSum(), + ); + + $mixed = new MixedScalars(); + var_dump($mixed->nativeSum(), $mixed->fixedIntSum(), $mixed->fixedFloatSum()); +} +?> +--EXPECT-- +int(-6) +int(-1600) +int(-320000) +int(8) +int(3200) +int(4294967295) +int(4294967296) +bool(true) +float(16777217) +float(3.5) +float(2.5) +float(3.25)