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)