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
master
韩天峰 4 weeks ago
parent 8332bfedd8
commit 41c4a578dd
  1. 16
      phpunit/code/native-class-inherited-member-name-conflict.php
  2. 16
      phpunit/code/native-class-inherited-property-name-conflict.php
  3. 12
      phpunit/code/native-class-member-name-conflict.php
  4. 76
      phpunit/code/native-stack-promotion.php
  5. 66
      phpunit/src/Analysis/NativeObjectStackPromotionAnalyzerTest.php
  6. 29
      phpunit/src/NativeClass/NativeClassValidationTest.php
  7. 92
      phpunit/src/NativeClass/NativeObjectStackPromotionCodegenTest.php
  8. 234
      src/Analysis/NativeObjectStackPromotionAnalyzer.php
  9. 41
      src/CompilerBase.php
  10. 10
      src/Context/FunctionContext.php
  11. 19
      src/Generator/CallArgumentGenerator.php
  12. 378
      src/NativeClass/NativeClassSupportTrait.php
  13. 26
      src/Parser/MethodCallTrait.php
  14. 1
      src/Preprocessor.php
  15. 6
      src/Transform/PropertyHookLowering.php
  16. 44
      src/Translator.php
  17. 8
      tests/compiler/native-class/private-property-slots.phpt
  18. 38
      tests/compiler/native-class/stack-promotion.phpt

@ -0,0 +1,16 @@
<?php
#[Native]
class NativeInheritedMemberNameConflictBase
{
public int $value = 0;
}
#[Native]
class NativeInheritedMemberNameConflictChild extends NativeInheritedMemberNameConflictBase
{
public function value(): int
{
return 1;
}
}

@ -0,0 +1,16 @@
<?php
#[Native]
class NativeInheritedPropertyNameConflictChild extends NativeInheritedPropertyNameConflictBase
{
public int $value = 0;
}
#[Native]
class NativeInheritedPropertyNameConflictBase
{
public function value(): int
{
return 1;
}
}

@ -0,0 +1,12 @@
<?php
#[Native]
class NativeMemberNameConflict
{
public int $value = 0;
public function value(): int
{
return $this->value;
}
}

@ -0,0 +1,76 @@
<?php
#[Native]
class NativeStackPromotionFixture
{
public int $value = 0;
public function __construct(int $value)
{
$this->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();
}

@ -0,0 +1,66 @@
<?php
namespace TypePhp\Tests\Analysis;
use PhpParser\Node;
use PhpParser\ParserFactory;
use PHPUnit\Framework\TestCase;
use TypePhp\Analysis\NativeObjectStackPromotionAnalyzer;
final class NativeObjectStackPromotionAnalyzerTest extends TestCase
{
private function analyze(string $body, array $unsafeMethods = []): array
{
$ast = (new ParserFactory())->createForNewestSupportedVersion()->parse(
"<?php function test(): void {\n{$body}\n}",
);
self::assertNotNull($ast);
$function = $ast[0];
self::assertInstanceOf(Node\Stmt\Function_::class, $function);
$analyzer = new NativeObjectStackPromotionAnalyzer(
static fn (Node\Expr\New_ $new): ?string => $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;'));
}
}

@ -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);

@ -0,0 +1,92 @@
<?php
namespace TypePhp\Tests\NativeClass;
use PHPUnit\Framework\TestCase;
use TypePhp\CompilerTest;
final class NativeObjectStackPromotionCodegenTest extends TestCase
{
public function testOnlyNonEscapingAllocationUsesStackSlot(): void
{
global $translator;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/native-stack-promotion.php';
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$compiler->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\(\) \{(?<body>.*?)\n\}/s',
$code,
$promoted,
));
self::assertSame(1, preg_match(
'/php_nativestackpromotionfixture \* php_escapednativeobject\(\) \{(?<body>.*?)\n\}/s',
$code,
$escaped,
));
self::assertSame(1, preg_match(
'/php::Int php_finalizednativeobject\(\) \{(?<body>.*?)\n\}/s',
$code,
$finalized,
));
self::assertSame(1, preg_match(
'/php::Int php_inheritednativeobject\(\) \{(?<body>.*?)\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']);
}
}

@ -0,0 +1,234 @@
<?php
/**
* This file is part of TypePHP(AOT).
*
* @link https://www.swoole.com/aot/
* @contact service@swoole.com
*/
namespace TypePhp\Analysis;
use Closure;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt;
/**
* Finds Native Object allocations whose address cannot leave the current
* function. This is deliberately an allocation-site analysis: proving that a
* PHP variable is stable is insufficient when an alias of the same pointer can
* escape.
*
* The first implementation accepts one direct `$local = new NativeClass()`
* definition outside loops. Every use must remain a property receiver or a
* receiver-preserving Native method call. Anything not explicitly understood
* invalidates the candidate and leaves it on the Wren GC heap.
*/
final class NativeObjectStackPromotionAnalyzer
{
/** @var Closure(Expr\New_): ?string */
private Closure $resolveClass;
/** @var Closure(string): bool */
private Closure $classIsEligible;
/** @var Closure(string, string): bool */
private Closure $methodPreservesReceiver;
/**
* @var array<string, array{
* assignment: Expr\Assign,
* allocation: Expr\New_,
* class: string
* }>
*/
private array $candidates = [];
/** @var array<string, true> */
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<Stmt> $statements
* @return array<string, array{assignment: Expr\Assign, allocation: Expr\New_, class: string}>
*/
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<Node> $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<Node> $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<Node> $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_;
}
}

@ -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;
}
}

@ -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<string, array{class: string, slot: string, allocationId: int}>
*/
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 = [];

@ -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

@ -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<string, bool> */
private array $nativeStackMethodSafety = [];
/** @var array<string, true> */
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<Node> $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<string> */
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<int>
@ -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;

@ -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) {

@ -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;

@ -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<Stmt> */

@ -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 . ')'

@ -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--

@ -0,0 +1,38 @@
--TEST--
Native class: non-escaping local allocation uses stack storage
--FILE--
<?php
#[Native]
class StackPoint
{
public int $x = 0;
public int $y = 0;
public function __construct(int $x, int $y)
{
$this->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)
Loading…
Cancel
Save