From 41c4a578dde1253feb5fdc377aca8111bc6de321 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 14 Sep 2026 13:00:05 +0800 Subject: [PATCH] feat(compiler): implement native object stack promotion and improve call argument handling - Add materializeTrailingDefaults parameter to CallArgumentGenerator for default argument handling - Implement native object stack slot allocation and promotion analysis - Add nativeStackPromotions context tracking for non-escaping object allocations - Generate NativeStackSlot declarations in compiler base code generation - Update native method call generation to use direct member access patterns - Add comprehensive native object member name validation and conflict detection - Implement native destructor state management and finalization chains - Add test cases for native class member name conflicts and stack promotion scenarios - Replace nativeDeref calls with nativeRequireObject for improved object access safety --- ...e-class-inherited-member-name-conflict.php | 16 + ...class-inherited-property-name-conflict.php | 16 + .../native-class-member-name-conflict.php | 12 + phpunit/code/native-stack-promotion.php | 76 ++++ ...NativeObjectStackPromotionAnalyzerTest.php | 66 +++ .../NativeClass/NativeClassValidationTest.php | 29 +- .../NativeObjectStackPromotionCodegenTest.php | 92 +++++ .../NativeObjectStackPromotionAnalyzer.php | 234 +++++++++++ src/CompilerBase.php | 41 +- src/Context/FunctionContext.php | 10 + src/Generator/CallArgumentGenerator.php | 19 + src/NativeClass/NativeClassSupportTrait.php | 378 ++++++++++++++++-- src/Parser/MethodCallTrait.php | 26 +- src/Preprocessor.php | 1 - src/Transform/PropertyHookLowering.php | 6 +- src/Translator.php | 44 +- .../native-class/private-property-slots.phpt | 8 +- .../native-class/stack-promotion.phpt | 38 ++ 18 files changed, 1045 insertions(+), 67 deletions(-) create mode 100644 phpunit/code/native-class-inherited-member-name-conflict.php create mode 100644 phpunit/code/native-class-inherited-property-name-conflict.php create mode 100644 phpunit/code/native-class-member-name-conflict.php create mode 100644 phpunit/code/native-stack-promotion.php create mode 100644 phpunit/src/Analysis/NativeObjectStackPromotionAnalyzerTest.php create mode 100644 phpunit/src/NativeClass/NativeObjectStackPromotionCodegenTest.php create mode 100644 src/Analysis/NativeObjectStackPromotionAnalyzer.php create mode 100644 tests/compiler/native-class/stack-promotion.phpt diff --git a/phpunit/code/native-class-inherited-member-name-conflict.php b/phpunit/code/native-class-inherited-member-name-conflict.php new file mode 100644 index 00000000..188b4d2f --- /dev/null +++ b/phpunit/code/native-class-inherited-member-name-conflict.php @@ -0,0 +1,16 @@ +value; + } +} diff --git a/phpunit/code/native-stack-promotion.php b/phpunit/code/native-stack-promotion.php new file mode 100644 index 00000000..3523a253 --- /dev/null +++ b/phpunit/code/native-stack-promotion.php @@ -0,0 +1,76 @@ +value = $value; + } + + public function read(): int + { + return $this->value; + } +} + +function promotedNativeObject(): int +{ + $value = new NativeStackPromotionFixture(42); + return $value->read(); +} + +function escapedNativeObject(): NativeStackPromotionFixture +{ + $value = new NativeStackPromotionFixture(42); + return $value; +} + +#[Native] +class NativeStackFinalizerFixture +{ + public int $value = 42; + + public function __destruct() + { + } +} + +function finalizedNativeObject(): int +{ + $value = new NativeStackFinalizerFixture(); + return $value->value; +} + +#[Native] +class NativeStackPromotionBase +{ + public int $value = 42; + + private function readPrivate(): int + { + return $this->value; + } + + public function readFromBase(): int + { + return $this->readPrivate(); + } +} + +#[Native] +class NativeStackPromotionChild extends NativeStackPromotionBase +{ + public function readFromBase(): int + { + return parent::readFromBase() + 1; + } +} + +function inheritedNativeObject(): int +{ + $value = new NativeStackPromotionChild(); + return $value->readFromBase(); +} diff --git a/phpunit/src/Analysis/NativeObjectStackPromotionAnalyzerTest.php b/phpunit/src/Analysis/NativeObjectStackPromotionAnalyzerTest.php new file mode 100644 index 00000000..6a562896 --- /dev/null +++ b/phpunit/src/Analysis/NativeObjectStackPromotionAnalyzerTest.php @@ -0,0 +1,66 @@ +createForNewestSupportedVersion()->parse( + " $new->class instanceof Node\Name + ? $new->class->toString() + : null, + static fn (string $class): bool => str_starts_with($class, 'Native'), + static fn (string $_class, string $method): bool => !in_array($method, $unsafeMethods, true), + ); + return $analyzer->analyze($function->stmts); + } + + public function testPromotesPropertyAndSafeMethodReceiver(): void + { + $result = $this->analyze(<<<'PHP' +$point = new NativePoint(); +$point->x = 21; +echo $point->x * 2; +$point->touch(); +PHP); + + self::assertSame(['point'], array_keys($result)); + self::assertSame('NativePoint', $result['point']['class']); + } + + public function testRejectsAliasReturnArgumentAndUnsafeMethod(): void + { + self::assertSame([], $this->analyze('$value = new NativeValue(); $alias = $value;')); + self::assertSame([], $this->analyze('$value = new NativeValue(); consume($value);')); + self::assertSame([], $this->analyze('$value = new NativeValue(); return $value;')); + self::assertSame([], $this->analyze( + '$value = new NativeValue(); $value->publish();', + ['publish'], + )); + } + + public function testRejectsRepeatedOrReassignedAllocation(): void + { + self::assertSame([], $this->analyze('for ($i = 0; $i < 2; $i++) { $value = new NativeValue(); }')); + self::assertSame([], $this->analyze('$value = new NativeValue(); $value = null;')); + } + + public function testRejectsPropertyAddressEscape(): void + { + self::assertSame([], $this->analyze('$value = new NativeValue(); consume($value->field);')); + self::assertSame([], $this->analyze('$value = new NativeValue(); $ref =& $value->field;')); + } +} diff --git a/phpunit/src/NativeClass/NativeClassValidationTest.php b/phpunit/src/NativeClass/NativeClassValidationTest.php index f4c20594..f34cf599 100644 --- a/phpunit/src/NativeClass/NativeClassValidationTest.php +++ b/phpunit/src/NativeClass/NativeClassValidationTest.php @@ -6,6 +6,25 @@ use TypePhp\Exception\TestError; final class NativeClassValidationTest extends \BaseTest { + /** + * @dataProvider nativeMemberNameConflictProvider + */ + public function testRejectsNativePropertyAndMethodNameConflicts(string $fixture): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('conflicts with'); + $this->compile($fixture); + } + + public static function nativeMemberNameConflictProvider(): array + { + return [ + ['native-class-member-name-conflict.php'], + ['native-class-inherited-member-name-conflict.php'], + ['native-class-inherited-property-name-conflict.php'], + ]; + } + public function testDiscoversNativeTypesBeforeCrossFileSignaturePreprocessing(): void { global $translator; @@ -44,15 +63,15 @@ final class NativeClassValidationTest extends \BaseTest $code = file_get_contents($reader); self::assertIsString($code); self::assertStringContainsString( - 'php::nativeDeref(nativeForwardGlobal, "NativeForwardGlobalValue").value', + 'php::nativeRequireObject(nativeForwardGlobal, "NativeForwardGlobalValue")->value', $code, ); self::assertStringContainsString( - 'php::nativeDeref(nativeForwardPolymorphic, "NativeForwardBase").value', + 'php::nativeRequireObject(nativeForwardPolymorphic, "NativeForwardBase")->value', $code, ); self::assertStringContainsString( - 'php::nativeDeref(nativeForwardCoalesced, "NativeForwardGlobalValue").value', + 'php::nativeRequireObject(nativeForwardCoalesced, "NativeForwardGlobalValue")->value', $code, ); self::assertStringContainsString( @@ -60,11 +79,11 @@ final class NativeClassValidationTest extends \BaseTest $code, ); self::assertMatchesRegularExpression( - '/php::nativeDeref\\(tmp_var_\\d+, "NativeForwardGlobalValue"\\)\\.value/', + '/php::nativeRequireObject\\(tmp_var_\\d+, "NativeForwardGlobalValue"\\)->value/', $code, ); self::assertStringContainsString( - 'php::nativeDeref(nativeForwardClosureGlobal, "NativeForwardGlobalValue").value', + 'php::nativeRequireObject(nativeForwardClosureGlobal, "NativeForwardGlobalValue")->value', $code, ); self::assertStringNotContainsString('nativeForwardGlobal.attr(', $code); diff --git a/phpunit/src/NativeClass/NativeObjectStackPromotionCodegenTest.php b/phpunit/src/NativeClass/NativeObjectStackPromotionCodegenTest.php new file mode 100644 index 00000000..737db6f4 --- /dev/null +++ b/phpunit/src/NativeClass/NativeObjectStackPromotionCodegenTest.php @@ -0,0 +1,92 @@ +addFiles([$source]); + $compiler->prepareFile($source); + $headerFile = $compiler->getIncludeDir() . '/php_native_cpp_class_func_decl.h'; + $compiler->genFunctionDeclarations($headerFile); + $cppFile = $compiler->convertFile($source); + $code = file_get_contents($cppFile); + $header = file_get_contents($headerFile); + + self::assertIsString($code); + self::assertIsString($header); + self::assertStringContainsString('class php_nativestackpromotionfixture {', $header); + self::assertStringContainsString( + 'php_nativestackpromotionfixture(php::Int value);', + $header, + ); + self::assertStringContainsString( + 'php::Int read();', + $header, + ); + self::assertStringContainsString( + 'virtual php::Int __typephp_virtual_php_nativestackpromotionbase__readfrombase() override;', + $header, + ); + self::assertStringContainsString( + 'php_nativestackpromotionfixture::php_nativestackpromotionfixture(php::Int value)', + $code, + ); + self::assertStringContainsString( + 'php::Int php_nativestackpromotionfixture::read()', + $code, + ); + self::assertStringContainsString( + 'php_nativestackfinalizerfixture::~php_nativestackfinalizerfixture() noexcept(false)', + $code, + ); + self::assertStringNotContainsString( + '__typephp_method_', + $code, + ); + self::assertSame(1, preg_match( + '/php::Int php_promotednativeobject\(\) \{(?.*?)\n\}/s', + $code, + $promoted, + )); + self::assertSame(1, preg_match( + '/php_nativestackpromotionfixture \* php_escapednativeobject\(\) \{(?.*?)\n\}/s', + $code, + $escaped, + )); + self::assertSame(1, preg_match( + '/php::Int php_finalizednativeobject\(\) \{(?.*?)\n\}/s', + $code, + $finalized, + )); + self::assertSame(1, preg_match( + '/php::Int php_inheritednativeobject\(\) \{(?.*?)\n\}/s', + $code, + $inherited, + )); + self::assertStringContainsString('php::NativeStackSlot<', $promoted['body']); + self::assertStringContainsString('value__native_stack_slot.constructObject(', $promoted['body']); + self::assertStringNotContainsString( + 'php::NativeRootSlot _native_root_slots[] = {&value};', + $promoted['body'], + ); + self::assertStringContainsString('php::nativeConstructObject<', $escaped['body']); + self::assertStringContainsString( + 'php::NativeRootSlot _native_root_slots[] = {&value};', + $escaped['body'], + ); + self::assertStringContainsString('php::nativeConstructObject<', $finalized['body']); + self::assertStringNotContainsString('php::NativeStackSlot<', $finalized['body']); + self::assertStringContainsString('php::nativeConstructObject<', $inherited['body']); + self::assertStringNotContainsString('php::NativeStackSlot<', $inherited['body']); + } +} diff --git a/src/Analysis/NativeObjectStackPromotionAnalyzer.php b/src/Analysis/NativeObjectStackPromotionAnalyzer.php new file mode 100644 index 00000000..0f78b258 --- /dev/null +++ b/src/Analysis/NativeObjectStackPromotionAnalyzer.php @@ -0,0 +1,234 @@ + + */ + private array $candidates = []; + + /** @var array */ + private array $invalid = []; + + private bool $hasNonStructuredControlFlow = false; + + public function __construct( + callable $resolveClass, + callable $classIsEligible, + callable $methodPreservesReceiver, + ) { + $this->resolveClass = Closure::fromCallable($resolveClass); + $this->classIsEligible = Closure::fromCallable($classIsEligible); + $this->methodPreservesReceiver = Closure::fromCallable($methodPreservesReceiver); + } + + /** + * @param list $statements + * @return array + */ + public function analyze(array $statements): array + { + $this->candidates = []; + $this->invalid = []; + $this->hasNonStructuredControlFlow = false; + + $this->collectCandidates($statements); + if ($this->candidates === [] || $this->hasNonStructuredControlFlow) { + return []; + } + + $this->scanUses($statements); + foreach ($this->invalid as $name => $_) { + unset($this->candidates[$name]); + } + return $this->candidates; + } + + private function collectCandidates(mixed $value, int $loopDepth = 0): void + { + foreach (is_array($value) ? $value : [$value] as $node) { + if (!$node instanceof Node) { + continue; + } + + if ($node instanceof Stmt\Goto_ || $node instanceof Stmt\Label) { + // A backward goto can execute one source allocation more than + // once even though it is not lexically contained in a loop. + $this->hasNonStructuredControlFlow = true; + } + + if ($loopDepth === 0 + && $node instanceof Stmt\Expression + && $node->expr instanceof Expr\Assign + && $node->expr->var instanceof Expr\Variable + && is_string($node->expr->var->name) + && $node->expr->expr instanceof Expr\New_ + ) { + $class = ($this->resolveClass)($node->expr->expr); + if ($class !== null && ($this->classIsEligible)($class)) { + $name = $node->expr->var->name; + if (isset($this->candidates[$name])) { + $this->invalid[$name] = true; + } else { + $this->candidates[$name] = [ + 'assignment' => $node->expr, + 'allocation' => $node->expr->expr, + 'class' => $class, + ]; + } + } + } + + if ($node instanceof FunctionLike) { + // Candidate allocations belong to the current function only. + // Uses inside nested closures are still inspected by scanUses() + // and conservatively treated as escapes. + continue; + } + + $childLoopDepth = $loopDepth + ($this->isRepeatedRegion($node) ? 1 : 0); + foreach ($node->getSubNodeNames() as $field) { + $this->collectCandidates($node->{$field}, $childLoopDepth); + } + } + } + + /** @param list $ancestors */ + private function scanUses( + mixed $value, + ?Node $parent = null, + string $parentField = '', + int $functionDepth = 0, + array $ancestors = [], + ): void { + foreach (is_array($value) ? $value : [$value] as $node) { + if (!$node instanceof Node) { + continue; + } + + if ($node instanceof Expr\Variable + && is_string($node->name) + && isset($this->candidates[$node->name]) + ) { + $this->classifyUse($node->name, $parent, $parentField, $functionDepth, $ancestors); + } + + $childFunctionDepth = $functionDepth + ($node instanceof FunctionLike ? 1 : 0); + $childAncestors = [...$ancestors, $node]; + foreach ($node->getSubNodeNames() as $field) { + $this->scanUses($node->{$field}, $node, $field, $childFunctionDepth, $childAncestors); + } + } + } + + /** @param list $ancestors */ + private function classifyUse( + string $name, + ?Node $parent, + string $parentField, + int $functionDepth, + array $ancestors, + ): void { + if (isset($this->invalid[$name])) { + return; + } + + $candidate = $this->candidates[$name]; + if ($functionDepth === 0 + && $parent instanceof Expr\Assign + && $parentField === 'var' + && $parent === $candidate['assignment'] + ) { + return; + } + + if ($functionDepth !== 0) { + $this->invalid[$name] = true; + return; + } + + if (($parent instanceof Expr\PropertyFetch || $parent instanceof Expr\NullsafePropertyFetch) + && $parentField === 'var' + ) { + if ($this->propertyAddressMayEscape($ancestors)) { + $this->invalid[$name] = true; + } + return; + } + + if (($parent instanceof Expr\MethodCall || $parent instanceof Expr\NullsafeMethodCall) + && $parentField === 'var' + && $parent->name instanceof Node\Identifier + && ($this->methodPreservesReceiver)($candidate['class'], $parent->name->toString()) + ) { + return; + } + + // Aliases, returns, arguments, array/container storage, comparisons, + // references, unset and every dynamic operation are escapes unless a + // future analysis explicitly proves otherwise. + $this->invalid[$name] = true; + } + + /** @param list $ancestors */ + private function propertyAddressMayEscape(array $ancestors): bool + { + foreach ($ancestors as $ancestor) { + if ($ancestor instanceof Expr\AssignRef + || $ancestor instanceof Node\Arg + || $ancestor instanceof Stmt\Return_ + || $ancestor instanceof Expr\Yield_ + || $ancestor instanceof Expr\YieldFrom + ) { + return true; + } + } + return false; + } + + private function isRepeatedRegion(Node $node): bool + { + return $node instanceof Stmt\For_ + || $node instanceof Stmt\Foreach_ + || $node instanceof Stmt\While_ + || $node instanceof Stmt\Do_; + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index f5a4f9da..89fe57a9 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -4218,13 +4218,14 @@ class CompilerBase implements PropertyAccessContext } $cppClass = $this->getNativeObjectCppName($className); $descriptor = $this->getNativeObjectDescriptorName($className); + $stackSlot = $this->getNativeStackSlotForAllocation($expr); if ($constructor === null) { if ($expr->args !== []) { $this->fatalError($expr, "Native class `{$className}` does not have a constructor"); } - return 'php::nativeConstruct<' . $cppClass . '>(' . $descriptor - . ', [&](auto &this_) { ' - . $this->getNativeObjectInitializerName($className) . '(this_); })'; + return $stackSlot === null + ? 'php::nativeConstructObject<' . $cppClass . '>(' . $descriptor . ')' + : $stackSlot . '.constructObject()'; } $nativeCtor = $this->getNativeMethod($expr, $className, '__construct'); if ($nativeCtor === false) { @@ -4234,13 +4235,16 @@ class CompilerBase implements PropertyAccessContext // the AST argument array used by the ordinary-class // path below. The self-hosted compiler assigns one // fixed C++ type to each PHP local variable. - $nativeArgs = $expr->args === [] - ? '' - : ', ' . $this->parseNativeCallArgs($expr->args, $nativeCtor); - return 'php::nativeConstruct<' . $cppClass . '>(' . $descriptor - . ', [&](auto &this_) { ' - . $this->getNativeObjectInitializerName($className) . '(this_); ' - . self::PREFIX . $nativeCtor . '(this_' . $nativeArgs . '); })'; + $nativeArgs = $this->parseNativeCallArgs( + $expr->args, + $nativeCtor, + materializeTrailingDefaults: true, + ); + if ($stackSlot !== null) { + return $stackSlot . '.constructObject(' . $nativeArgs . ')'; + } + return 'php::nativeConstructObject<' . $cppClass . '>(' . $descriptor + . ($nativeArgs === '' ? '' : ', ' . $nativeArgs) . ')'; } $cePtr = $this->getLocalClassEntryPtr($className); } @@ -4278,12 +4282,7 @@ class CompilerBase implements PropertyAccessContext "Call to {$visibility} {$declaringClassName}::__clone()", ); } - $clone = self::PREFIX . $this->getNativeName( - '__clone', - $declaringClass->namespace, - $declaringClass->name, - ); - $initializer = $clone . '(this_); '; + $initializer = 'this_.' . $this->getNativeObjectMethodCppName('__clone') . '(); '; } if ($this->nativeObjectUsesVirtualClone($class)) { return $this->getNativeObjectReceiver($source) . '.' @@ -5527,6 +5526,13 @@ class CompilerBase implements PropertyAccessContext . PHP_EOL; } $code .= $this->genLocalVarDecl($this->context->localVars); + foreach ($this->context->nativeStackPromotions as $promotion) { + $code .= $this->getIndent() . 'php::NativeStackSlot<' + . $this->getNativeObjectCppName($promotion['class']) . '> ' + . $promotion['slot'] . '(' + . $this->getNativeObjectDescriptorName($promotion['class']) . ');' + . PHP_EOL; + } foreach ($this->context->classEntryPtrs as $className => $entry) { $code .= $this->getIndent() . 'zend_class_entry *' . $entry . ' = ' . $this->getClassEntryPtr($className) . ';' . PHP_EOL; @@ -5539,6 +5545,9 @@ class CompilerBase implements PropertyAccessContext // nativeClone do the same for lifecycle callbacks). Only // function-owned pointer slots must be registered here. if ($name !== 'this_' && !$this->hasArgument($name) && $this->hasLocalVar($name)) { + if (isset($this->context->nativeStackPromotions[$name])) { + continue; + } $rootSlots[] = '&' . $name; } } diff --git a/src/Context/FunctionContext.php b/src/Context/FunctionContext.php index d342bbf8..0ed55422 100644 --- a/src/Context/FunctionContext.php +++ b/src/Context/FunctionContext.php @@ -51,6 +51,14 @@ class FunctionContext */ public array $nonNullNativeObjects = []; + /** + * Native Object locals whose allocation is proven not to escape this + * function and is therefore backed by a NativeStackSlot instead of Wren. + * + * @var array + */ + public array $nativeStackPromotions = []; + /** * Declared object constraints that are not used for native-call dispatch. * @@ -163,6 +171,7 @@ class FunctionContext $this->objects = []; $this->nativeObjects = []; $this->nonNullNativeObjects = []; + $this->nativeStackPromotions = []; $this->declaredObjects = []; $this->stdArrays = []; $this->stdContainers = []; @@ -221,6 +230,7 @@ class FunctionContext $this->declaredObjects = $declaredObjects; $this->nativeObjects = $nativeObjects; $this->nonNullNativeObjects = $nonNullNativeObjects; + $this->nativeStackPromotions = []; $this->beforeStmtLines = []; $this->afterStmtLines = []; $this->objectProps = []; diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index 5c1c3359..2cd2d0f5 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -27,6 +27,7 @@ trait CallArgumentGenerator string $nativeFunc, int $parameterOffset = 0, bool $deferTrailingDefaults = false, + bool $materializeTrailingDefaults = false, ): string { $this->assertCallArgumentLimit($callArgs); $this->context->typedRefBridgeScopes[] = []; @@ -122,6 +123,24 @@ trait CallArgumentGenerator } } + // Generated Native C++ class members deliberately do not expose + // C++ default arguments: PHP defaults belong to the selected PHP + // declaration, and virtual calls use arity-specific adapters. + // Direct member/constructor calls therefore materialize every + // omitted trailing default at the call site. + if ($materializeTrailingDefaults) { + foreach ($functionDef->argInfoList as $k => $argInfo) { + if ($k < $parameterOffset || isset($providedArgs[$k]) || isset($defaultArgs[$k])) { + continue; + } + if ($argInfo->variadic) { + $defaultArgs[$k] = '{}'; + } elseif ($argInfo->hasDefaultValue()) { + $defaultArgs[$k] = $this->genDefaultArgumentExpr($nativeFunc, $k); + } + } + } + // If the function only accepts a single variadic parameter and the call // supplies no arguments, pass an empty array directly if (count($sourceArgs) === 0 diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index 0d42764b..73c84cfa 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -14,6 +14,7 @@ use TypePhp\Entity\FunctionDef; use TypePhp\Entity\MethodDef; use TypePhp\Entity\PropertyDef; use TypePhp\Resolver\Reflection; +use TypePhp\Transform\PropertyHookLowering; use PhpParser\Modifiers; use TypePhp\Type; use PhpParser\NodeAbstract; @@ -23,6 +24,160 @@ trait NativeClassSupportTrait { private const string NATIVE_VIRTUAL_CLONE_METHOD = '__typephp_native_clone'; + /** @var array */ + private array $nativeStackMethodSafety = []; + + /** @var array */ + private array $nativeStackMethodSafetyVisiting = []; + + protected function getNativeStackSlotForAllocation(Node\Expr\New_ $allocation): ?string + { + $allocationId = spl_object_id($allocation); + foreach ($this->context->nativeStackPromotions as $promotion) { + if ($promotion['allocationId'] === $allocationId) { + return $promotion['slot']; + } + } + return null; + } + + protected function nativeObjectClassCanUseStackStorage(string $class): bool + { + if (!$this->isNativeObjectClass($class)) { + return false; + } + + $exactClass = $this->getClass($class); + if ($exactClass->extends !== '' && $this->isNativeObjectClass($exactClass->extends)) { + // `$this->privateMethod()` is lexically bound to the declaring + // class rather than selected only from the exact runtime class. + // Model that distinction before promoting inherited layouts. + return false; + } + + $current = $class; + while ($current !== '' && $this->isNativeObjectClass($current)) { + $classDef = $this->getClass($current); + if ($classDef->hasMethod('__destruct')) { + // PHP-level finalizer exceptions and resurrection currently + // belong to the Wren finalization pipeline. Keep these objects + // on that path until stack finalization has identical semantics. + return false; + } + foreach ($classDef->properties as $property) { + if ($property->getter !== null || $property->setter !== null) { + // A property access invokes its hook implicitly. Private + // and overridden hooks have lexical dispatch subtleties; + // keep hooked layouts on the GC path until the escape pass + // models those call edges explicitly. + return false; + } + } + $current = $classDef->extends; + } + + $constructor = $this->findNativeObjectMethod($class, '__construct'); + return $constructor === null + || $this->nativeObjectMethodPreservesReceiver($class, '__construct'); + } + + protected function nativeObjectMethodPreservesReceiver(string $dispatchClass, string $method): bool + { + $resolved = $this->findNativeObjectMethod($dispatchClass, $method); + if ($resolved === null + || $resolved->functionDef === null + || $resolved->functionDef->returnsByRef + || $resolved->node?->stmts === null + ) { + return false; + } + + $key = strtolower(ltrim($dispatchClass, '\\') . '::' . $method); + if (array_key_exists($key, $this->nativeStackMethodSafety)) { + return $this->nativeStackMethodSafety[$key]; + } + if (isset($this->nativeStackMethodSafetyVisiting[$key])) { + // Proving a mutually recursive call graph requires a fixed-point + // pass. Keep recursive receivers on the GC heap for now. + return false; + } + + $this->nativeStackMethodSafetyVisiting[$key] = true; + $safe = !$this->nativeMethodBodyEscapesReceiver( + $resolved->node->stmts, + $dispatchClass, + ); + unset($this->nativeStackMethodSafetyVisiting[$key]); + return $this->nativeStackMethodSafety[$key] = $safe; + } + + /** @param list $ancestors */ + private function nativeMethodBodyEscapesReceiver( + mixed $value, + string $dispatchClass, + ?Node $parent = null, + string $parentField = '', + int $functionDepth = 0, + array $ancestors = [], + ): bool { + foreach (is_array($value) ? $value : [$value] as $node) { + if (!$node instanceof Node) { + continue; + } + + // parent::method() and similar calls can forward the implicit + // receiver without containing an explicit `$this` AST node. + if ($functionDepth === 0 && $node instanceof Node\Expr\StaticCall) { + return true; + } + + if ($node instanceof Node\Expr\Variable && $node->name === 'this') { + if ($functionDepth !== 0) { + return true; + } + if (($parent instanceof Node\Expr\PropertyFetch + || $parent instanceof Node\Expr\NullsafePropertyFetch) + && $parentField === 'var' + ) { + foreach ($ancestors as $ancestor) { + if ($ancestor instanceof Node\Expr\AssignRef || $ancestor instanceof Node\Arg) { + return true; + } + } + continue; + } + if (($parent instanceof Node\Expr\MethodCall + || $parent instanceof Node\Expr\NullsafeMethodCall) + && $parentField === 'var' + && $parent->name instanceof Node\Identifier + && $this->nativeObjectMethodPreservesReceiver( + $dispatchClass, + $parent->name->toString(), + ) + ) { + continue; + } + return true; + } + + $childFunctionDepth = $functionDepth + ($node instanceof Node\FunctionLike ? 1 : 0); + $childAncestors = [...$ancestors, $node]; + foreach ($node->getSubNodeNames() as $field) { + if ($this->nativeMethodBodyEscapesReceiver( + $node->{$field}, + $dispatchClass, + $node, + $field, + $childFunctionDepth, + $childAncestors, + )) { + return true; + } + } + } + return false; + } + /** * Magic methods whose semantics require Zend object handlers, runtime * method resolution, dynamic properties, or Zend serialization state. @@ -702,6 +857,86 @@ trait NativeClassSupportTrait return $this->getNativeObjectCppName($class) . '__type'; } + protected function getNativeObjectMethodCppName(string $method): string + { + return $this->escapeVarName(strtolower($method)); + } + + /** + * Validate the completed declaration graph at the convert boundary. + * prepare() deliberately permits arbitrary source order; by this point + * every parent and composed Trait is available, so inheritance-wide name + * rules do not depend on preparation order. + */ + protected function validateNativeObjectMemberNames(): void + { + foreach ($this->getNativeObjectClassesInDeclarationOrder() as $class) { + $lineage = []; + $current = $class; + while (true) { + $lineage[] = $current; + if ($current->extends === '' || !$this->hasClass($current->extends)) { + break; + } + $current = $this->getClass($current->extends); + } + + foreach ($class->properties as $property) { + foreach ($lineage as $owner) { + foreach ([...$owner->methods, ...$owner->abstractMethodDefs] as $method) { + if (strcasecmp($property->name, $method->name) !== 0) { + continue; + } + if ($property->node !== null) { + $this->fatalError( + $property->node, + "Native class property `\${$property->name}` conflicts with method `" + . $owner->getNamespacedName(false) . "::{$method->name}()`", + ); + } + } + } + } + + foreach ([...$class->methods, ...$class->abstractMethodDefs] as $method) { + if (str_starts_with(strtolower($method->name), '__typephp_') + && $method->node?->getAttribute( + PropertyHookLowering::INTERNAL_METHOD_ATTRIBUTE, + ) !== true + && $method->node !== null + ) { + $this->fatalError( + $method->node, + "Native class method `{$method->name}()` uses a compiler-reserved name", + ); + } + foreach ($lineage as $owner) { + foreach ($owner->properties as $property) { + if (strcasecmp($method->name, $property->name) !== 0) { + continue; + } + if ($method->node !== null) { + $this->fatalError( + $method->node, + "Native class method `{$method->name}()` conflicts with property `" + . $owner->getNamespacedName(false) . "::\${$property->name}`", + ); + } + } + } + } + } + } + + /** @return list */ + private function getNativeMethodArgumentNames(FunctionDef $function): array + { + return array_map( + static fn (ArgInfo $argument): string => $argument->name, + $function->argInfoList, + ); + } + protected function getNativeObjectPointerType(string|ClassDef $class): string { return $this->getNativeObjectCppName($class) . ' *'; @@ -1237,7 +1472,14 @@ trait NativeClassSupportTrait protected function getNativeObjectMemberReceiver(string $name): string { - return $this->getNativeObjectReceiver($name) . '.'; + if ($name === 'this_') { + return 'this_.'; + } + if ($this->isNativeObjectKnownNonNull($name)) { + return $name . '->'; + } + $class = $this->getNativeObjectVarClass($name); + return 'php::nativeRequireObject(' . $name . ', "' . addslashes($class) . '")->'; } /** @@ -1604,7 +1846,7 @@ trait NativeClassSupportTrait * uses the default declared by the dynamically selected override. Emit an * overload for every positional arity instead of putting C++ defaults on * a virtual declaration. Each override adapter can then call its concrete - * php_* function with the supplied prefix and let that declaration provide + * concrete C++ member function with the supplied prefix and let that declaration provide * the correct dynamic defaults. * * @return list @@ -1748,23 +1990,46 @@ trait NativeClassSupportTrait protected function genNativeObjectDeclarations(): string { + $this->validateNativeObjectMemberNames(); $classes = $this->getNativeObjectClassesInDeclarationOrder(); if ($classes === []) { return ''; } $code = '// TypePHP Native Object declarations' . PHP_EOL; + $code .= 'struct typephp_native_storage_constructor_t {};' . PHP_EOL; foreach ($classes as $class) { - $code .= 'struct ' . $this->getNativeObjectCppName($class) . ';' . PHP_EOL; + $code .= 'class ' . $this->getNativeObjectCppName($class) . ';' . PHP_EOL; } $code .= PHP_EOL; foreach ($classes as $class) { $name = $this->getNativeObjectCppName($class); + $hasNativeDestructor = $this->findNativeObjectMethod( + $class->getNamespacedName(false), + '__destruct', + ) !== null; $parent = $class->extends !== '' && $this->isNativeObjectClass($class->extends) ? ' : public ' . $this->getNativeObjectCppName($class->extends) : ''; - $code .= 'struct ' . $name . $parent . ' {' . PHP_EOL; + $code .= 'class ' . $name . $parent . ' {' . PHP_EOL; + $code .= 'public:' . PHP_EOL; + $code .= ' explicit ' . $name . '(typephp_native_storage_constructor_t) noexcept;' . PHP_EOL; + $constructor = $this->findNativeObjectMethod($class->getNamespacedName(false), '__construct'); + $code .= ' ' . $name . '('; + if ($constructor !== null && $constructor->functionDef !== null) { + $code .= $this->getNativeMethodParameterDeclarations($constructor->functionDef); + } + $code .= ');' . PHP_EOL; + $code .= ' virtual ~' . $name . '() ' + . ($hasNativeDestructor ? 'noexcept(false)' : 'noexcept') + . ($class->extends !== '' && $this->isNativeObjectClass($class->extends) ? ' override' : '') + . ';' . PHP_EOL; + if ($class->hasMethod('__destruct')) { + $code .= ' void __typephp_finalize_destructor();' . PHP_EOL; + $code .= ' void __typephp_suppress_destructor() noexcept;' . PHP_EOL; + $code .= ' php::NativeDestructorState __typephp_destructor_state;' . PHP_EOL; + } foreach ($class->properties as $property) { if ($property->flags & Modifiers::STATIC || $this->isNativeObjectInheritedPropertyRedeclaration($class, $property) @@ -1789,6 +2054,18 @@ trait NativeClassSupportTrait } $code .= ';' . PHP_EOL; } + foreach ($class->methods as $method) { + if ($method->flags & Modifiers::ABSTRACT) { + continue; + } + $function = $method->functionDef; + if ($function === null) { + continue; + } + $code .= ' ' . $this->getNativeMethodReturnCppType($function) + . ' ' . $this->getNativeObjectMethodCppName($method->name) . '(' + . $this->getNativeMethodParameterDeclarations($function) . ');' . PHP_EOL; + } foreach ([...$class->methods, ...$class->abstractMethodDefs] as $method) { foreach ($this->getNativeVirtualMethodSlots($class, $method) as [$slotClass, $slotMethod]) { $ownsSlot = $this->isSameClassName( @@ -1829,6 +2106,10 @@ trait NativeClassSupportTrait { $cpp = $this->getNativeObjectCppName($class); $prefix = $cpp . '__gc'; + $hasNativeDestructor = $this->findNativeObjectMethod( + $class->getNamespacedName(false), + '__destruct', + ) !== null; $code = ''; $code .= 'void ' . $this->getNativeObjectInitializerName($class) . '(' . $cpp . ' &this_) {' . PHP_EOL; @@ -1848,12 +2129,49 @@ trait NativeClassSupportTrait $code .= ' this_.' . $this->getNativeObjectPropertyCppName($property, $class) . ' = ' . $value . ';' . PHP_EOL; } $code .= '}' . PHP_EOL . PHP_EOL; + $baseConstructor = $class->extends !== '' && $this->isNativeObjectClass($class->extends) + ? ' : ' . $this->getNativeObjectCppName($class->extends) . '(typephp_native_storage_constructor_t{})' + : ''; + $code .= $cpp . '::' . $cpp . '(typephp_native_storage_constructor_t) noexcept' + . $baseConstructor . ' {}' . PHP_EOL . PHP_EOL; + + $constructor = $this->findNativeObjectMethod($class->getNamespacedName(false), '__construct'); + $constructorFunction = $constructor?->functionDef; + $code .= $cpp . '::' . $cpp . '('; + if ($constructorFunction !== null) { + $code .= $this->getNativeMethodParameterDeclarations($constructorFunction); + } + $code .= ')' . $baseConstructor . ' {' . PHP_EOL; + $code .= ' PHPX_TRY {' . PHP_EOL; + $code .= ' ' . $this->getNativeObjectInitializerName($class) . '(*this);' . PHP_EOL; + if ($constructorFunction !== null) { + $code .= ' this->' . $this->getNativeObjectMethodCppName('__construct') . '(' + . implode(', ', $this->getNativeMethodArgumentNames($constructorFunction)) . ');' . PHP_EOL; + } + $code .= ' } PHPX_CATCH_ALL {' . PHP_EOL; + $code .= ' php::nativeConstructorFailed();' . PHP_EOL; + $code .= ' }' . PHP_EOL; + $code .= '}' . PHP_EOL . PHP_EOL; + + $code .= $cpp . '::~' . $cpp . '() ' + . ($hasNativeDestructor ? 'noexcept(false)' : 'noexcept') . ' {' . PHP_EOL; + if ($class->hasMethod('__destruct')) { + $code .= ' __typephp_finalize_destructor();' . PHP_EOL; + } + $code .= '}' . PHP_EOL . PHP_EOL; + if ($class->hasMethod('__destruct')) { + $code .= 'void ' . $cpp . '::__typephp_finalize_destructor() {' . PHP_EOL; + $code .= ' if (!__typephp_destructor_state.beginFinalize()) {' . PHP_EOL; + $code .= ' return;' . PHP_EOL; + $code .= ' }' . PHP_EOL; + $code .= ' ' . $this->getNativeObjectMethodCppName('__destruct') . '();' . PHP_EOL; + $code .= '}' . PHP_EOL . PHP_EOL; + $code .= 'void ' . $cpp . '::__typephp_suppress_destructor() noexcept {' . PHP_EOL; + $code .= ' __typephp_destructor_state.suppress();' . PHP_EOL; + $code .= '}' . PHP_EOL . PHP_EOL; + } + foreach ($class->methods as $method) { - $nativeFunction = self::PREFIX . $this->getNativeName( - $method->name, - $class->namespace, - $class->name, - ); foreach ($this->getNativeVirtualMethodSlots($class, $method) as [$slotClass, $slotMethod]) { $slotFunction = $slotMethod->functionDef; $returnType = $this->getNativeMethodReturnCppType($slotFunction); @@ -1862,10 +2180,20 @@ trait NativeClassSupportTrait static fn (ArgInfo $arg): string => $arg->name, array_slice($slotFunction->argInfoList, 0, $arity), ); + $methodFunction = $method->functionDef; + $methodNativeName = $this->getNativeName( + $method->name, + $class->namespace, + $class->name, + ); + for ($i = $arity; $i < count($methodFunction->argInfoList); $i++) { + $args[] = $this->genDefaultArgumentExpr($methodNativeName, $i); + } $code .= $returnType . ' ' . $cpp . '::' . $this->getNativeVirtualMethodName($slotClass, $method->name) . '(' . $this->getNativeMethodParameterDeclarations($slotFunction, $arity) . ') {' . PHP_EOL; - $call = $nativeFunction . '(*this' . ($args === [] ? '' : ', ' . implode(', ', $args)) . ')'; + $call = 'this->' . $this->getNativeObjectMethodCppName($method->name) + . '(' . implode(', ', $args) . ')'; $code .= ' ' . ($returnType === Type::VOID ? '' : 'return ') . $call . ';' . PHP_EOL; $code .= '}' . PHP_EOL . PHP_EOL; } @@ -1877,12 +2205,7 @@ trait NativeClassSupportTrait $cloneMethod = $this->findNativeObjectMethod($class->getNamespacedName(false), '__clone'); if ($cloneMethod !== null) { $declaringClass = $this->getClass($cloneMethod->functionDef->declaringClass); - $clone = self::PREFIX . $this->getNativeName( - '__clone', - $declaringClass->namespace, - $declaringClass->name, - ); - $initializer = $clone . '(this_); '; + $initializer = 'this_.' . $this->getNativeObjectMethodCppName('__clone') . '(); '; } $code .= $cpp . ' *' . $cpp . '::' . self::NATIVE_VIRTUAL_CLONE_METHOD . '() const {' . PHP_EOL; $code .= ' return php::nativeClone<' . $cpp . '>(' @@ -1920,11 +2243,7 @@ trait NativeClassSupportTrait while (true) { if ($destructorClass->hasMethod('__destruct')) { $destructors[] = [ - self::PREFIX . $this->getNativeName( - '__destruct', - $destructorClass->namespace, - $destructorClass->name, - ), + '__typephp_finalize_destructor', $this->getNativeObjectCppName($destructorClass), ]; } @@ -1937,18 +2256,26 @@ trait NativeClassSupportTrait $code .= 'static void ' . $prefix . '_finalize(void *object) {' . PHP_EOL; if (count($destructors) === 1) { [$destructor, $destructorCpp] = $destructors[0]; - $code .= ' ' . $destructor - . '(*static_cast<' . $destructorCpp . ' *>(object));' . PHP_EOL; + $code .= ' static_cast<' . $destructorCpp . ' *>(object)->' + . $destructor . '();' . PHP_EOL; } else { $code .= ' php::NativeFinalizerChain chain;' . PHP_EOL; foreach ($destructors as [$destructor, $destructorCpp]) { - $code .= ' chain.run([&] { ' . $destructor - . '(*static_cast<' . $destructorCpp . ' *>(object)); });' . PHP_EOL; + $code .= ' chain.run([&] { static_cast<' . $destructorCpp + . ' *>(object)->' . $destructor . '(); });' . PHP_EOL; } $code .= ' chain.rethrow();' . PHP_EOL; } $code .= '}' . PHP_EOL; } + if ($destructors !== []) { + $code .= 'static void ' . $prefix . '_suppress_finalize(void *object) noexcept {' . PHP_EOL; + foreach ($destructors as [, $destructorCpp]) { + $code .= ' static_cast<' . $destructorCpp + . ' *>(object)->__typephp_suppress_destructor();' . PHP_EOL; + } + $code .= '}' . PHP_EOL; + } $code .= 'static void ' . $prefix . '_destroy(void *object) noexcept {' . PHP_EOL; $code .= ' static_cast<' . $cpp . ' *>(object)->~' . $cpp . '();' . PHP_EOL; $code .= '}' . PHP_EOL; @@ -1958,6 +2285,7 @@ trait NativeClassSupportTrait $code .= ' alignof(' . $cpp . '),' . PHP_EOL; $code .= ' ' . $prefix . '_trace,' . PHP_EOL; $code .= ' ' . ($destructors !== [] ? $prefix . '_finalize' : 'nullptr') . ',' . PHP_EOL; + $code .= ' ' . ($destructors !== [] ? $prefix . '_suppress_finalize' : 'nullptr') . ',' . PHP_EOL; $code .= ' ' . $prefix . '_destroy,' . PHP_EOL; $code .= '};' . PHP_EOL . PHP_EOL; return $code; diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index f1fe3ae3..aa1c64be 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -477,11 +477,13 @@ trait MethodCallTrait if ($nativeFunc === false) { $this->fatalError($expr, "Native parent class `{$parentClass}` has no method `{$method}()`"); } - if ($expr->args === []) { - return self::PREFIX . $nativeFunc . '(this_)'; - } - return self::PREFIX . $nativeFunc . '(this_, ' - . $this->parseNativeCallArgs($expr->args, $nativeFunc) . ')'; + return 'this_.' . $this->getNativeObjectCppName($parentClass) . '::' + . $this->getNativeObjectMethodCppName($method) . '(' + . $this->parseNativeCallArgs( + $expr->args, + $nativeFunc, + materializeTrailingDefaults: true, + ) . ')'; } $staticCall = false; if ($this->isIdExpr($expr->name)) { @@ -748,12 +750,14 @@ trait MethodCallTrait deferTrailingDefaults: true, ) . ')'; } - $receiver = $this->getNativeObjectReceiver($object); - if ($expr->args === []) { - return self::PREFIX . $nativeFunc . '(' . $receiver . ')'; - } - return self::PREFIX . $nativeFunc . '(' . $receiver . ', ' - . $this->parseNativeCallArgs($expr->args, $nativeFunc) . ')'; + $call = $this->getNativeObjectMemberReceiver($object) + . $this->getNativeObjectMethodCppName($methodName); + return $call . '(' + . $this->parseNativeCallArgs( + $expr->args, + $nativeFunc, + materializeTrailingDefaults: true, + ) . ')'; } // Method calls are allowed on references: use a native call when class info is available, otherwise a dynamic call if (!$this->checkArgType($type, Type::OBJECT) and $type !== Type::REF) { diff --git a/src/Preprocessor.php b/src/Preprocessor.php index c87f1b4a..545352c9 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -2744,7 +2744,6 @@ class Preprocessor extends CompilerBase if ($this->classDef->nativeObject && ($flags & Modifiers::STATIC)) { $this->fatalError($v, 'Native class static methods are not supported'); } - if (!$abstract) { $this->methodDef = new MethodDef($flags, $name); $this->methodDef->node = $v; diff --git a/src/Transform/PropertyHookLowering.php b/src/Transform/PropertyHookLowering.php index 0ebf9696..d2f24828 100644 --- a/src/Transform/PropertyHookLowering.php +++ b/src/Transform/PropertyHookLowering.php @@ -21,6 +21,7 @@ use PhpParser\NodeVisitorAbstract; final class PropertyHookLowering { public const string BACKING_ACCESS_ATTRIBUTE = 'typephpPropertyHookBackingAccess'; + public const string INTERNAL_METHOD_ATTRIBUTE = 'typephpPropertyHookInternalMethod'; public const string METHOD_ATTRIBUTE = 'typephpPropertyHookMethod'; public const string PROPERTY_ATTRIBUTE = 'typephpPropertyHooks'; private const string GET_PREFIX = '__typephp_property_get_'; @@ -116,6 +117,7 @@ final class PropertyHookLowering 'kind' => $kind, 'property' => $propertyName, ]); + $method->setAttribute(self::INTERNAL_METHOD_ATTRIBUTE, true); $methods[] = $method; $hookMethods[$kind] = $methodName; $hasBackingStorage = $hasBackingStorage || self::containsBackingAccess($stmts); @@ -168,13 +170,15 @@ final class PropertyHookLowering private static function visibilityMarker(string $name, array $attributes): Stmt\ClassMethod { - return new Stmt\ClassMethod($name, [ + $method = new Stmt\ClassMethod($name, [ // A child declaration may replace the generated visibility marker. // This method is metadata for the object handler, not a final PHP API. 'flags' => Modifiers::PUBLIC, 'returnType' => new Node\Identifier('void'), 'stmts' => [], ], $attributes); + $method->setAttribute(self::INTERNAL_METHOD_ATTRIBUTE, true); + return $method; } /** @return list */ diff --git a/src/Translator.php b/src/Translator.php index e72f9b65..8cde408e 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -12,6 +12,7 @@ use Ajaxray\AnsiKit\AnsiTerminal; use Ajaxray\AnsiKit\Components\Progressbar; use MJS\TopSort\Implementations\StringSort; use TypePhp\Analysis\LocalClosureAnalyzer; +use TypePhp\Analysis\NativeObjectStackPromotionAnalyzer; use TypePhp\Analysis\SsaBuilder; use TypePhp\Backend\CompilerFactory; use TypePhp\Build\CompileOptions; @@ -61,6 +62,7 @@ use TypePhp\Transform\NanoSyntaxValidationVisitor; use TypePhp\Transform\VoidCastValidationVisitor; use PhpParser\Modifiers; use PhpParser\Node; +use PhpParser\Node\Expr; use PhpParser\NodeAbstract; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; @@ -731,6 +733,7 @@ class Translator extends Preprocessor $previousPhase = null; try { $this->composeTraitDeclarations(array_keys($this->preparedFileAsts)); + $this->validateNativeObjectMemberNames(); $previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT); if (!$this->declarationExpressionsFinalized) { $this->finalizeDeclarationExpressions(array_keys($this->preparedFileAsts)); @@ -2420,6 +2423,11 @@ CODE; if ($func->abstractMethod) { continue; } + if ($func->method && $this->isNativeObjectClass($func->declaringClass)) { + // Native methods are declared directly in their generated C++ + // class. They deliberately have no php_* free-function ABI. + continue; + } $functionDeclarationPrefix = $this->getFunctionDeclarationPrefix($func); $list = []; if ($func->method) { @@ -5455,6 +5463,23 @@ CODE; foreach ($optimizedLoopVars as $varName => $type) { $this->context->localVars[$varName] = $type; } + + $promotions = (new NativeObjectStackPromotionAnalyzer( + fn (Expr\New_ $allocation): ?string => $this->resolveNewExprClass($allocation), + fn (string $class): bool => $this->nativeObjectClassCanUseStackStorage($class), + fn (string $class, string $method): bool => $this->nativeObjectMethodPreservesReceiver( + $class, + $method, + ), + ))->analyze($v->stmts); + foreach ($promotions as $varName => $promotion) { + $escapedName = $this->escapeVarName($varName); + $this->context->nativeStackPromotions[$escapedName] = [ + 'class' => $promotion['class'], + 'slot' => $escapedName . '__native_stack_slot', + 'allocationId' => spl_object_id($promotion['allocation']), + ]; + } } if ($v->stmts && !$this->class && $this->methodDef === null) { @@ -5485,9 +5510,17 @@ CODE; ? Type::REF : ($this->getNativeObjectReturnType($this->functionDef) ?? $this->getReturnType())); $nativeName = self::PREFIX . $name; + $nativeClassMethod = $this->classDef?->nativeObject === true; $functionAttribute = $this->getFunctionOptimizationAttribute($this->functionDef); - $functionDeclCode = $functionAttribute . $cppReturnType . ' ' . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '('; - if ($this->class) { + if ($nativeClassMethod) { + $functionDeclCode = $functionAttribute . $cppReturnType . ' ' + . $this->getNativeObjectCppName($this->classDef) . '::' + . $this->getNativeObjectMethodCppName($this->method) . '('; + } else { + $functionDeclCode = $functionAttribute . $cppReturnType . ' ' + . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '('; + } + if ($this->class && !$nativeClassMethod) { $functionDeclCode .= ($this->getNativeObjectMethodThisType($this->functionDef) ?? (Type::OBJECT . ' &')) . 'this_'; if ($this->functionDef->params) { @@ -5505,7 +5538,10 @@ CODE; $code = $functionDeclCode . ' {' . PHP_EOL; $this->indentLevel++; - $preamble = $this->genDegradedArgumentLocals(); + $preamble = $nativeClassMethod + ? $this->getIndent() . 'auto &this_ = *this;' . PHP_EOL + : ''; + $preamble .= $this->genDegradedArgumentLocals(); $preamble .= $this->genScopeVarDecl(); $preamble .= $this->genNativeObjectParameterChecks($this->functionDef); // Runtime union/nullable parameter type checks @@ -5546,7 +5582,7 @@ CODE; $code .= $stmts; $code .= "}\n"; - if ($multiReturn) { + if ($multiReturn && !$nativeClassMethod) { $forwardArgs = implode(', ', array_map( fn($argInfo) => $this->canConsumeForwardedArgument($argInfo) ? 'php::takeValue(' . $argInfo->name . ')' diff --git a/tests/compiler/native-class/private-property-slots.phpt b/tests/compiler/native-class/private-property-slots.phpt index 0cced0b4..ce1c9020 100644 --- a/tests/compiler/native-class/private-property-slots.phpt +++ b/tests/compiler/native-class/private-property-slots.phpt @@ -8,7 +8,7 @@ class NativePrivateBase { private int $baseValue = 10; - public function baseValue(): int + public function getBaseValue(): int { return $this->baseValue; } @@ -24,7 +24,7 @@ class NativePrivateChild extends NativePrivateBase { private int $childValue = 20; - public function childValue(): int + public function getChildValue(): int { return $this->childValue; } @@ -38,10 +38,10 @@ class NativePrivateChild extends NativePrivateBase function main(): void { $value = new NativePrivateChild(); - var_dump($value->baseValue(), $value->childValue()); + var_dump($value->getBaseValue(), $value->getChildValue()); $value->setBaseValue(11); $value->setChildValue(22); - var_dump($value->baseValue(), $value->childValue()); + var_dump($value->getBaseValue(), $value->getChildValue()); } ?> --EXPECT-- diff --git a/tests/compiler/native-class/stack-promotion.phpt b/tests/compiler/native-class/stack-promotion.phpt new file mode 100644 index 00000000..50b8d43d --- /dev/null +++ b/tests/compiler/native-class/stack-promotion.phpt @@ -0,0 +1,38 @@ +--TEST-- +Native class: non-escaping local allocation uses stack storage +--FILE-- +x = $x; + $this->y = $y; + } + + public function sum(): int + { + return $this->x + $this->y; + } +} + +function stackValue(): int +{ + $point = new StackPoint(20, 22); + $point->x++; + return $point->sum() - 1; +} + +function main(): void +{ + var_dump(stackValue()); +} + +?> +--EXPECT-- +int(42)