diff --git a/phpunit/src/ClassTest.php b/phpunit/src/ClassTest.php index d36ad068..7369a355 100644 --- a/phpunit/src/ClassTest.php +++ b/phpunit/src/ClassTest.php @@ -640,9 +640,12 @@ class ClassTest extends \BaseTest $this->exec('Cannot access private method `BaseSecret::secret()`', 'trait-parent-method-private.php'); } - public function testTraitMethodMayShadowPrivateParentMethod() + public function testComposedTraitMethodCannotShadowPrivateParentMethod() { - $this->compile('trait-method-shadows-private.php'); + $this->exec( + 'Cannot override private method `PrivateMethodParent::execute()`', + 'trait-method-shadows-private.php' + ); } public function testSelfCanBePartOfUnionType() diff --git a/phpunit/src/InheritanceErrorTest.php b/phpunit/src/InheritanceErrorTest.php index 0190376c..bb509a13 100644 --- a/phpunit/src/InheritanceErrorTest.php +++ b/phpunit/src/InheritanceErrorTest.php @@ -363,6 +363,6 @@ class InheritanceErrorTest extends TestCase public function testTraitParentCallRequiresParentClass() { - $this->exec('has no parent', 'trait-parent-without-parent.php'); + $this->exec('does not extend any class', 'trait-parent-without-parent.php'); } } diff --git a/phpunit/src/TraitFuncDeclTest.php b/phpunit/src/TraitFuncDeclTest.php index 4d5129e6..5fbaf47e 100644 --- a/phpunit/src/TraitFuncDeclTest.php +++ b/phpunit/src/TraitFuncDeclTest.php @@ -1,19 +1,9 @@ compile('trait-aliased-constructor-parent-call.php'); @@ -30,16 +20,16 @@ class TraitFuncDeclTest extends \BaseTest $compiler->genFunctionDeclarations($headerPath); $decl = file_get_contents($headerPath); - $this->assertMatchesRegularExpression( - '/extern void php_tpdodriver____construct\([^;\n]*trait_parent_ce[^;\n]*\);/', + $this->assertDoesNotMatchRegularExpression( + '/extern void php_tpdodriver____construct\(/', $decl, - 'The trait function declaration must include its implicit parent scope' + 'A trait must not have a standalone native function declaration' ); - $this->assertDoesNotMatchRegularExpression( - '/extern void php_(?:driver__tpdodriverconstruct|directdriver____construct)' - . '\([^;\n]*trait_parent_ce[^;\n]*\);/', + $this->assertMatchesRegularExpression( + '/extern void php_(?:driver__tpdodriverconstruct|directdriver____construct)\(/', $decl, - 'Composing-class wrapper declarations must not expose the implicit parent scope' + 'Each composing class must receive its own native method declaration' ); + $this->assertStringNotContainsString('trait_parent_ce', $decl); } } diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 0a1361cd..c0534d7b 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1051,16 +1051,29 @@ class CompilerBase implements PropertyAccessContext protected function getFunctionName(FunctionLike $v): string { + if ($this->methodDef !== null && $this->classDef !== null) { + return $this->getNativeName( + $this->parseIdentifier($v->name), + $this->classDef->namespace, + $this->classDef->name, + ); + } return $this->getNativeName($this->parseIdentifier($v->name), $this->namespace, $this->class); } protected function getFullClassName(): string { + if ($this->classDef !== null) { + return $this->classDef->getNamespacedName(false); + } return ltrim($this->namespace . '\\' . $this->class, '\\'); } protected function getFullClassLikeName(): string { + if ($this->classDef !== null) { + return $this->classDef->getNamespacedName(false); + } $name = $this->class !== '' ? $this->class : $this->interface; return ltrim($this->namespace . '\\' . $name, '\\'); } @@ -3254,6 +3267,7 @@ class CompilerBase implements PropertyAccessContext $classDef->implements[$i] = new Node\Name\FullyQualified($ifaceName); } } + $this->flattenEmbeddedClassTraits($classDef); // 将匿名类内部的类型引用(方法参数、返回值、属性等)转为全限定名称 $this->resolveAnonClassTypeNames($classDef); $this->context->beforeStmtLines[] = 'static THREAD_LOCAL bool ' . $className . '_defined = false;'; diff --git a/src/Entity/ClassDef.php b/src/Entity/ClassDef.php index ad9754c3..d6103d9b 100644 --- a/src/Entity/ClassDef.php +++ b/src/Entity/ClassDef.php @@ -66,6 +66,14 @@ class ClassDef extends ClassLikeDef */ public array $abstractMethodDefs = []; public ?Trait_ $trait = null; + /** @var list */ + public array $traitUseNamespaces = []; + /** @var array */ + public array $traitUseAliases = []; + /** @var array */ + public array $traitUseFunctions = []; + /** @var array */ + public array $traitUseConstants = []; /** * FullMethodName -> alias list diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index afb39254..c8b5fc1f 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -33,8 +33,6 @@ class FunctionDef public string $attributeFactoryScope = ''; /** External library imported by the stub containing this function. */ public string $importLibrary = ''; - /** Whether the native signature includes an implicit trait parent scope. */ - public bool $hasTraitParentCeParameter = false; public bool $returnTypeUndeclared = false; public bool $returnsByRef = false; public bool $generator = false; diff --git a/src/Entity/MethodDef.php b/src/Entity/MethodDef.php index 3973e385..93eca879 100644 --- a/src/Entity/MethodDef.php +++ b/src/Entity/MethodDef.php @@ -22,14 +22,8 @@ class MethodDef */ public ?\PhpParser\Node\Stmt\ClassMethod $node = null; - /** - * For methods defined inside a trait, records `parent::method()` calls so - * the compiler can validate their visibility against the parent of each - * class that uses the trait (the trait itself has no parent at compile time). - * - * @var array - */ - public array $parentMethodCalls = []; + /** Source trait, retained only for diagnostics and the __TRAIT__ constant. */ + public string $traitOrigin = ''; public function __construct(int $flags, string $name) { diff --git a/src/Generator/AnonClassGenerator.php b/src/Generator/AnonClassGenerator.php index 211125d4..ac89c901 100644 --- a/src/Generator/AnonClassGenerator.php +++ b/src/Generator/AnonClassGenerator.php @@ -21,6 +21,7 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassConst; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Property; +use PhpParser\Node\Stmt\TraitUse; use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; use TypePhp\Resolver\Reflection; @@ -32,6 +33,49 @@ trait AnonClassGenerator return self::ANON_CLASS . $this->anonClassIndex++; } + /** Flatten trait templates before an anonymous class is evaluated by ZendVM. */ + protected function flattenEmbeddedClassTraits(Class_ $class): void + { + $declaredMethods = []; + foreach ($class->stmts as $stmt) { + if ($stmt instanceof ClassMethod) { + $declaredMethods[strtolower($stmt->name->toString())] = true; + } + } + + $injected = []; + foreach ($class->stmts as $stmt) { + if (!$stmt instanceof TraitUse) { + continue; + } + foreach ($stmt->traits as $traitName) { + $fullName = $this->getNamespacedClassName($traitName->toString()); + if (!$this->hasClass($fullName)) { + $this->fatalError($stmt, "Trait `{$fullName}` not found"); + } + $traitDef = $this->getClass($fullName); + $traitAst = clone $traitDef->trait; + foreach ($traitAst->stmts as $traitStmt) { + if ($traitStmt instanceof ClassMethod) { + $name = strtolower($traitStmt->name->toString()); + if (isset($declaredMethods[$name])) { + continue; + } + $declaredMethods[$name] = true; + } + if (!$traitStmt instanceof TraitUse) { + $injected[] = $traitStmt; + } + } + } + } + $class->stmts = array_values(array_filter( + $class->stmts, + static fn (Node $stmt): bool => !$stmt instanceof TraitUse, + )); + array_push($class->stmts, ...$injected); + } + /** * Resolve all relative type names in an anonymous class to fully qualified names. * The generated eval code runs without use imports, so all type references must be FQN. diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index 512d4f1b..ff128c4d 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -302,9 +302,9 @@ trait ClosureGenerator $this->indentLevel++; try { - foreach ($capturedNames as $i => $name) { - $code .= $this->getIndent() . Type::VAR . ' ' . $name . ' = vars_.get(' . $i . ');' . PHP_EOL; - $this->addArgument($name, Type::VAR); + foreach ($capturedNames as $i => $capturedName) { + $code .= $this->getIndent() . Type::VAR . ' ' . $capturedName . ' = vars_.get(' . $i . ');' . PHP_EOL; + $this->addArgument($capturedName, Type::VAR); } if ($this->methodDef) { $this->addArgument('this_', Type::OBJECT); diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index c347e319..5b2501a1 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -702,13 +702,21 @@ trait FuncCallOptimizer return 'php::Decimal::round(' . $a0 . ')'; } $args = count($e->args); + // Keep PHP's left-to-right argument evaluation explicit here. This + // optimizer is itself compiled by TypePHP, and embedding several + // getArg() calls in one C++ string-concatenation expression would let + // the host C++ compiler choose a different evaluation order. + $a0 = $this->getArg($e, 0); if ($args >= 3) { - return 'php::fn::round(' . $this->getArg($e, 0) . ', ' . $this->convertIntExpr($this->getArg($e, 1)) . ', ' . $this->convertIntExpr($this->getArg($e, 2)) . ')'; + $a1 = $this->convertIntExpr($this->getArg($e, 1)); + $a2 = $this->convertIntExpr($this->getArg($e, 2)); + return 'php::fn::round(' . $a0 . ', ' . $a1 . ', ' . $a2 . ')'; } if ($args >= 2) { - return 'php::fn::round(' . $this->getArg($e, 0) . ', ' . $this->convertIntExpr($this->getArg($e, 1)) . ')'; + $a1 = $this->convertIntExpr($this->getArg($e, 1)); + return 'php::fn::round(' . $a0 . ', ' . $a1 . ')'; } - return 'php::fn::round(' . $this->getArg($e, 0) . ')'; + return 'php::fn::round(' . $a0 . ')'; } protected function genCount(string $n, Node\Expr\FuncCall $e, array $c): string diff --git a/src/Optimizer/SsaTypeOptimizer.php b/src/Optimizer/SsaTypeOptimizer.php index b8907422..e235b0af 100644 --- a/src/Optimizer/SsaTypeOptimizer.php +++ b/src/Optimizer/SsaTypeOptimizer.php @@ -106,8 +106,8 @@ trait SsaTypeOptimizer } } - foreach ($groups as $varName => $varList) { - $varName = $this->escapeVarName($varName); + foreach ($groups as $groupName => $varList) { + $varName = $this->escapeVarName($groupName); // Skip parameters — they already have declared types if (isset($this->context->arguments[$varName])) { continue; diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index f15aec98..e1aa501f 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -157,9 +157,9 @@ trait AssignOpTrait $variables[] = $name; } - foreach ($variables as $name) { - if (!$this->hasVar($name)) { - $this->addLocalVar($name, Type::VAR); + foreach ($variables as $variableName) { + if (!$this->hasVar($variableName)) { + $this->addLocalVar($variableName, Type::VAR); } } $tieItems = array_merge( diff --git a/src/Parser/ClassConstantFetchTrait.php b/src/Parser/ClassConstantFetchTrait.php index d5b01a94..26c67685 100644 --- a/src/Parser/ClassConstantFetchTrait.php +++ b/src/Parser/ClassConstantFetchTrait.php @@ -29,7 +29,11 @@ trait ClassConstantFetchTrait $class = 'static'; } else { $self = true; - $class = $this->class; + // Trait-composed methods are parsed under the trait's lexical + // namespace, while `self` still denotes the consuming class. + // Keep it fully qualified so the lexical namespace is not + // applied to the class identity below. + $class = '\\' . $this->getFullClassName(); } } elseif ($class === 'parent') { if (!$this->classDef || !$this->classDef->extends) { diff --git a/src/Parser/ConstantExpressionTrait.php b/src/Parser/ConstantExpressionTrait.php index 8e79a8f3..b9844eee 100644 --- a/src/Parser/ConstantExpressionTrait.php +++ b/src/Parser/ConstantExpressionTrait.php @@ -89,7 +89,8 @@ trait ConstantExpressionTrait protected function parseMagicConst(MagicConst $expr): string { - $class = ($this->namespace ? $this->namespace . '\\' : '') . $this->class; + $class = $this->classDef?->getNamespacedName(false) + ?? (($this->namespace ? $this->namespace . '\\' : '') . $this->class); $function = ($this->namespace ? $this->namespace . '\\' : '') . $this->function; switch ($expr->getType()) { case 'Scalar_MagicConst_Dir': @@ -109,6 +110,9 @@ trait ConstantExpressionTrait } return '"' . $this->escapeString($class) . '"'; case 'Scalar_MagicConst_Trait': + if ($this->methodDef?->traitOrigin !== '') { + return '"' . $this->escapeString($this->methodDef->traitOrigin) . '"'; + } if (!$this->classDef or !$this->classDef->trait) { $this->fatalError($expr, 'The magic constant `__TRAIT__` is not allowed in global scope'); } diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 7b2b43e0..9b95aa53 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -243,33 +243,6 @@ trait MethodCallTrait protected function parseParentMethodCall(Expr\StaticCall $expr): string { - // A trait's parent scope is supplied by the wrapper generated for the - // class that composes it. It must not be derived from the runtime - // object's class: an inherited trait method is still lexically bound to - // the parent of the composing class, not to the runtime object's parent. - if ($this->classDef !== null && $this->classDef->trait !== null) { - $method = $this->isIdExpr($expr->name) ? $this->parseIdentifier($expr->name) : ''; - // Record the parent:: call so it can be validated against the parent - // of every class that uses this trait (the trait itself has no parent - // at compile time). Dynamic method names cannot be validated statically. - if ($method !== '' && isset($this->methodDef)) { - $this->methodDef->parentMethodCalls[] = ['method' => $method, 'node' => $expr]; - } - $methodPtr = 'php::getMethod(trait_parent_ce, ' . $this->identifierToStr($expr->name) . ')'; - if (empty($expr->args)) { - return 'this_.call(' . $methodPtr . ')'; - } - // The concrete parent signature is only known at each trait use - // site. Preserve arguments that are already references so forwarding - // a by-reference trait parameter does not silently drop its alias. - return 'this_.call(' . $methodPtr . ', ' . $this->parseCallArgs( - $expr->args, - $method, - '', - preserveExistingReferences: true - ) . ')'; - } - if (!$this->classDef->extends) { $this->fatalError($expr, 'Cannot call parent method because class `' . $this->classDef->name . '` does not extend any class'); } diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 83820500..86177e86 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -796,6 +796,13 @@ class Preprocessor extends CompilerBase $this->classDef->implements = $this->parseImplements($class->implements); } else { $this->classDef->trait = $class; + // Trait members are compiled later in the consuming class, but + // names inside them retain the lexical imports of the trait + // declaration. + $this->classDef->traitUseNamespaces = $this->useNamespaces; + $this->classDef->traitUseAliases = $this->useAliases; + $this->classDef->traitUseFunctions = $this->useFunctions; + $this->classDef->traitUseConstants = $this->useConstants; } $this->symbolDeclInFile[$fullClassNameLower] = $this->file; @@ -1463,6 +1470,10 @@ class Preprocessor extends CompilerBase if (!$abstract) { $this->methodDef = new MethodDef($flags, $name); $this->methodDef->node = $v; + $traitOrigin = $v->getAttribute('typephp_trait_origin'); + if (is_string($traitOrigin)) { + $this->methodDef->traitOrigin = $traitOrigin; + } if ($this->classDef->hasMethod($name)) { $generatedBy = $v->getAttribute(CompileTimeAttributeDiagnostic::GENERATED_BY); $generatedTarget = $v->getAttribute(CompileTimeAttributeDiagnostic::GENERATED_TARGET); @@ -1479,6 +1490,12 @@ class Preprocessor extends CompilerBase $this->fatalError($v, "Duplicate method `{$this->method}`"); } $this->prepareFunction($v); + if ($class instanceof Node\Stmt\Trait_) { + // Trait functions are templates, not native symbols. The + // FunctionDef remains owned by MethodDef and is cloned into + // every class that composes the trait. + $this->symbols->removeFunction($this->getFunctionName($v)); + } $this->checkRequiredArgNum($name, $this->methodDef, $v); $this->classDef->addMethod($this->methodDef); } else { @@ -1490,6 +1507,10 @@ class Preprocessor extends CompilerBase } $this->methodDef = new MethodDef($flags, $name); $this->methodDef->node = $v; + $traitOrigin = $v->getAttribute('typephp_trait_origin'); + if (is_string($traitOrigin)) { + $this->methodDef->traitOrigin = $traitOrigin; + } $this->methodDef->functionDef = $this->parseFunctionDecl($v); $this->methodDef->functionDef->method = true; $this->checkRequiredArgNum($name, $this->methodDef, $v); diff --git a/src/Translator.php b/src/Translator.php index d3426cc3..0a56fc7b 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -51,9 +51,12 @@ use PhpParser\Node; use PhpParser\NodeAbstract; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; +use PhpParser\NodeVisitor\CloningVisitor; class Translator extends Preprocessor { + private const string TRAIT_ORIGIN_ATTRIBUTE = 'typephp_trait_origin'; + private const string TRAIT_METHOD_ATTRIBUTE = 'typephp_trait_method'; use DefaultArgumentGenerator; use NativeCommandOptionsTrait; use SourcePipelineTrait; @@ -1651,9 +1654,6 @@ CODE; $list = []; if ($func->method) { $list[] = Type::OBJECT . ' &this_'; - if ($func->hasTraitParentCeParameter) { - $list[] = 'zend_class_entry *trait_parent_ce'; - } } $argInfoList = $func->argInfoList; if ($argInfoList) { @@ -2011,7 +2011,11 @@ CODE; */ private function getClassLikesWithConstants(): array { - return array_merge($this->symbols->classes(), $this->symbols->interfaces()); + $classes = array_filter( + $this->symbols->classes(), + static fn (ClassDef $classDef): bool => $classDef->trait === null, + ); + return array_merge($classes, $this->symbols->interfaces()); } protected function getFilesFromDir(string $path): array @@ -2509,6 +2513,9 @@ CODE; } foreach ($this->symbols->classes() as $classDef) { + if ($classDef->trait !== null) { + continue; + } $ce = $this->getClassCe($classDef); $deps = []; $parent = $classDef->extends; @@ -2542,7 +2549,13 @@ CODE; $sorter->add($ce, $deps); } - $this->classCeList = $sorter->sort(); + // StringSort yields an empty placeholder when the symbol table contains + // only compile-time traits. Never turn that placeholder into a bogus + // `zend_class_entry *;` declaration. + $this->classCeList = array_values(array_filter( + $sorter->sort(), + static fn (mixed $ce): bool => is_string($ce) && $ce !== '', + )); } protected function getNativeMethodName(ClassDef $classDef, MethodDef $methodDef): string @@ -2645,7 +2658,7 @@ CODE; $this->argInfoHeaderFiles[] = $headerFile; } - public function parseTraitUseForStub(Node\Stmt\ClassLike $stmt, Node\Name $className): void + public function composeTraitAst(Node\Stmt\ClassLike $stmt, Node\Name $className): void { $methods = []; $constants = []; @@ -2690,12 +2703,20 @@ CODE; $this->fatalError($classStmt, "Trait `{$traitFullName}` not found"); } - $traitAst = clone $traitDef->trait; + /** @var Node\Stmt\Trait_ $traitAst */ + $traitAst = $this->cloneAstNode($traitDef->trait); + // Recursively flatten traits used by this trait before copying + // its members into the final class. + $this->composeTraitAst($traitAst, new Node\Name($traitFullName)); $traitStmts = $traitAst->stmts; $aliasStmts = []; foreach ($traitStmts as $k1 => $traitStmt) { if ($traitStmt instanceof Node\Stmt\ClassMethod) { $methodName = strtolower($traitStmt->name->toString()); + if ($traitStmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE) === null) { + $traitStmt->setAttribute(self::TRAIT_ORIGIN_ATTRIBUTE, $traitFullName); + $traitStmt->setAttribute(self::TRAIT_METHOD_ATTRIBUTE, $traitStmt->name->toString()); + } $fullMethodName = $this->getFullMethodName($traitFullName, $methodName); // A trait method's `self`/`static`/`parent` return and parameter // types refer to the class that uses the trait, not the trait @@ -2820,6 +2841,14 @@ CODE; $stmt->stmts = array_merge($stmt->stmts, $traitStmts, $aliasStmts); } } + + } + + private function cloneAstNode(Node $node): Node + { + $traverser = new NodeTraverser(); + $traverser->addVisitor(new CloningVisitor()); + return $traverser->traverse([$node])[0]; } /** @@ -3177,6 +3206,15 @@ CODE; } } + if ($class instanceof Node\Stmt\Class_ || $class instanceof Node\Stmt\Enum_) { + /** @var Node\Stmt\Class_|Node\Stmt\Enum_ $composedClass */ + $composedClass = $this->cloneAstNode($class); + $this->composeTraitAst($composedClass, new Node\Name($fullName)); + $this->installComposedTraitDataMembers($composedClass); + } else { + $composedClass = null; + } + $this->checkPropertyOverride($class); $this->checkConstantOverride($class); @@ -3194,7 +3232,9 @@ CODE; case 'Stmt_EnumCase': break; case 'Stmt_ClassMethod': - $this->parseClassMethod($v, $methodCodes); + if (!$class instanceof Node\Stmt\Trait_) { + $this->parseClassMethod($v, $methodCodes); + } break; case 'Stmt_TraitUse': $this->parseTraitUse($v, $methodCodes); @@ -3204,6 +3244,29 @@ CODE; break; } } + if ($composedClass !== null) { + $composedTraitMethods = []; + foreach ($composedClass->stmts as $stmt) { + if (!$stmt instanceof Node\Stmt\ClassMethod + || !is_string($stmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE))) { + continue; + } + $origin = (string) $stmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE); + $this->withTraitNameContext($origin, function () use ($stmt): void { + $this->installComposedTraitMethod($stmt); + }); + $composedTraitMethods[] = [$stmt, $origin]; + } + // Every method must be visible before any body is lowered. Trait + // methods may call a private helper declared later in the same + // trait; compiling as we install would incorrectly lower that call + // as a dynamic callback instead of a native class method call. + foreach ($composedTraitMethods as [$stmt, $origin]) { + $this->withTraitNameContext($origin, function () use ($stmt, &$methodCodes): void { + $this->parseClassMethod($stmt, $methodCodes); + }); + } + } if (!$class instanceof Node\Stmt\Trait_) { $this->validateOverrideAttributes($class); $this->checkInterfaceImplementations($class); @@ -3225,9 +3288,8 @@ CODE; protected function genNativeMethod(array $methodCodes): string { $code = ''; - $classDef = $this->classDef; - foreach ($classDef->methods as $method) { - $code .= $methodCodes[$method->name] . PHP_EOL; + foreach ($methodCodes as $methodCode) { + $code .= $methodCode . PHP_EOL; } $code .= PHP_EOL; @@ -3394,17 +3456,10 @@ CODE; $cppCode = 'ZEND_METHOD(' . $name . ', ' . $methodDef->name . '){' . PHP_EOL; $cppCode .= $this->getIndent() . Type::OBJECT . ' this_(&execute_data->This);' . PHP_EOL; $fn = self::PREFIX . $this->getNativeMethodName($classDef, $methodDef); - $implicitMethodArgs = []; - if ($classDef->trait !== null && $methodDef->parentMethodCalls) { - // Trait methods are not directly callable without a composing class, - // but keep the generated Zend wrapper well-formed. - $implicitMethodArgs[] = 'this_.parent_ce()'; - } $cppCode .= $this->genWrapperFunctionArgs( $fn, $methodDef->functionDef, $classDef->getNamespacedName(false) . '::' . $methodDef->name, - $implicitMethodArgs ); return $cppCode; @@ -3428,7 +3483,7 @@ CODE; $cppCode = ''; // 接口没有方法实体 - if ($classDef instanceof ClassDef) { + if ($classDef instanceof ClassDef && $classDef->trait === null) { $defaultPropCount = 0; foreach ($classDef->properties as $property) { if (!$property->isStatic() && $property->default !== null) { @@ -3598,16 +3653,11 @@ CODE; $cppReturnType = $multiReturn ? $this->functionDef->getMultiReturnCppType() : ($this->functionDef->returnsByRef ? Type::REF : $this->getReturnType()); - $this->functionDef->hasTraitParentCeParameter = - $this->classDef?->trait !== null && (bool) $this->methodDef?->parentMethodCalls; $nativeName = self::PREFIX . $name; $functionAttribute = $this->getFunctionOptimizationAttribute($this->functionDef); $functionDeclCode = $functionAttribute . $cppReturnType . ' ' . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '('; if ($this->class) { $functionDeclCode .= Type::OBJECT . ' &this_'; - if ($this->functionDef->hasTraitParentCeParameter) { - $functionDeclCode .= ', zend_class_entry *trait_parent_ce'; - } if ($this->functionDef->params) { $functionDeclCode .= ', '; } @@ -4504,7 +4554,6 @@ CODE; protected function parseTraitUse(Node\Stmt\TraitUse $v, array &$methodCodes): void { $classDef = $this->classDef; - foreach ($v->traits as $trait) { $traitName = $this->parseIdentifier($trait); $traitFullName = $this->getNamespacedClassName($traitName); @@ -4512,7 +4561,6 @@ CODE; $this->fatalError($v, $traitFullName . ' not found'); } $traitDef = $this->getClass($traitFullName); - // 将 Trait 中定义的 常量、静态常量、属性、方法、静态属性复制到当前类中 foreach ($traitDef->constants as $const) { if ($classDef->hasConstant($const->name)) { if (!$this->isCompatibleTraitConstant($classDef->getConstant($const->name), $const)) { @@ -4531,196 +4579,104 @@ CODE; } $classDef->properties[$prop->name] = $prop; } - foreach ($traitDef->methods as $methodDef) { - $classMethodName = $traitMethodName = $methodDef->name; - $fullMethodName = $this->getFullMethodName($traitFullName, $traitMethodName); - $originalMethodDef = $methodDef; - $aliasMethodDefs = []; - foreach ($classDef->traitAliases[$fullMethodName] ?? [] as $alias) { - if (strtolower($alias['newName']) === strtolower($traitMethodName)) { - if ($alias['newModifier']) { - if ($originalMethodDef === $methodDef) { - $originalMethodDef = clone $methodDef; - } - $originalMethodDef->flags = $this->parseModifiers($alias['newModifier']); - } - continue; - } - $aliasMethodDef = clone $methodDef; - $classMethodName = $aliasMethodDef->name = $alias['newName']; - if ($alias['newModifier']) { - $aliasMethodDef->flags = $this->parseModifiers($alias['newModifier']); - } - $aliasMethodDefs[strtolower($classMethodName)] = [$classMethodName, $aliasMethodDef]; - } - // 设置了 insteadof 选项,此 Trait 的方法将不会被使用 - if (isset($classDef->traitIgnored[$fullMethodName])) { - foreach ($aliasMethodDefs as [$aliasMethodName, $aliasMethodDef]) { - if ($classDef->hasMethod($aliasMethodName)) { - continue; - } - $methodCodes[$aliasMethodName] = $this->addTraitMethodWrapper( - $classDef, - $traitDef, - $aliasMethodDef, - $traitMethodName, - $aliasMethodName - ); + } + } + + private function installComposedTraitDataMembers(Node\Stmt\ClassLike $class): void + { + foreach ($class->stmts as $stmt) { + if ($stmt instanceof Node\Stmt\ClassConst) { + foreach ($stmt->consts as $const) { + if (!$this->classDef->hasConstant($const->name->toString())) { + $this->parseClassConstDef($stmt); + break; } - continue; - } - // 类中已经有同名方法,则不使用 Trait 中的方法 - if (!$classDef->hasMethod($traitMethodName)) { - $methodCodes[$traitMethodName] = $this->addTraitMethodWrapper( - $classDef, - $traitDef, - $originalMethodDef, - $traitMethodName, - $traitMethodName - ); } - - foreach ($aliasMethodDefs as [$aliasMethodName, $aliasMethodDef]) { - if ($classDef->hasMethod($aliasMethodName)) { - continue; + } elseif ($stmt instanceof Node\Stmt\Property) { + foreach ($stmt->props as $prop) { + if (!$this->classDef->hasProperty($prop->name->toString())) { + $this->parseClassPropertyDef($stmt); + break; } - $methodCodes[$aliasMethodName] = $this->addTraitMethodWrapper( - $classDef, - $traitDef, - $aliasMethodDef, - $traitMethodName, - $aliasMethodName - ); } } } } - private function addTraitMethodWrapper( - ClassDef $classDef, - ClassDef $traitDef, - MethodDef $methodDef, - string $traitMethodName, - string $classMethodName - ): string { - // A trait may be composed by multiple classes and aliases. Each wrapper - // needs independent signature metadata. - $methodDef = clone $methodDef; - - // A trait method's `self`/`static`/`parent` return and parameter types - // refer to the class that uses the trait, not the trait itself. Re-resolve - // them to the consuming class so signature-compatibility checks (against - // parent classes and interfaces) and `detectClassOfExpr()` observe the - // correct type. The cloned FunctionDef keeps the trait's own native - // function untouched. - $this->reresolveTraitLateBoundTypes($classDef, $methodDef); - - // The wrapper computes the composing class's parent scope and passes it - // to the trait function; it does not expose that scope in its signature. - $methodDef->functionDef->hasTraitParentCeParameter = false; - - // Validate `parent::` calls emitted from this trait method against the - // parent of the class that is composing the trait. The trait itself has - // no parent at compile time, so this is the only place the parent class - // (and therefore the visibility of its methods) is known. - foreach ($methodDef->parentMethodCalls as $parentCall) { - $this->validateTraitParentCall($traitDef, $classDef, $parentCall['method'], $parentCall['node']); - } - - // A trait method flattened into a class participates in the inheritance - // hierarchy: it must remain signature-compatible with any same-named - // parent method, exactly as a directly-declared override would. PHP - // enforces this at class declaration time ("Declaration of X::m() must - // be compatible with Y::m()"); without this check the incompatibility - // only surfaces as a runtime fatal error that the compiled binary would - // otherwise ignore and keep executing past. - $this->checkTraitMethodOverrideCompatibility($classDef, $methodDef, $classMethodName); - - $classDef->addMethod($methodDef); - $traitMethodNativeName = $this->getNativeName($traitMethodName, $traitDef->namespace, $traitDef->name); - $classMethodNativeName = $this->getNativeName($classMethodName, $classDef->namespace, $classDef->name); - $argList = ['this_']; - if ($methodDef->parentMethodCalls) { - // Bind parent:: to the class that actually composes the trait. This - // remains correct when the generated wrapper is inherited further. - $argList[] = $this->getClassEntryPtr($classDef->extends); - } - foreach ($methodDef->functionDef->argInfoList as $argInfo) { - $argList[] = $argInfo->name; - } - $argv = implode(', ', $argList); - - $cppReturnType = $methodDef->functionDef->returnsByRef ? Type::REF : $methodDef->getReturnType(); - $code = $cppReturnType . ' ' . self::PREFIX . $classMethodNativeName . '('; - if ($this->class) { - $code .= Type::OBJECT . ' &this_'; - if ($methodDef->functionDef->params) { - $code .= ', '; - } + private function installComposedTraitMethod(Node\Stmt\ClassMethod $methodStmt): void + { + $name = $methodStmt->name->toString(); + if ($this->classDef->hasMethod($name)) { + return; } - $this->addFunction($classMethodNativeName, $methodDef->functionDef); + $flags = $this->parseModifiers($methodStmt->flags); + $methodDef = new MethodDef($flags, $name); + $methodDef->node = $methodStmt; + $methodDef->traitOrigin = (string) $methodStmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE, ''); - $code .= $methodDef->functionDef->params . ')'; - $code .= '{' . PHP_EOL; - $this->indentLevel++; - // The trait body is compiled once, but its private/protected property - // scope is the class that composes it. Keep that lexical scope fixed - // when this wrapper is inherited by a child class. - $scope = $this->getClassEntryPtr($classDef->getNamespacedName(false)); - $code .= $this->getIndent() . 'php::FakeScopeGuard fake_scope_guard{' . $scope . '};' . PHP_EOL; - $methodCall = self::PREFIX . $traitMethodNativeName . '(' . $argv . ')'; - if ($cppReturnType !== Type::VOID) { - $methodCall = 'return ' . $methodCall; - } - $code .= $this->getIndent() . $methodCall . ';' . PHP_EOL; - $this->indentLevel--; - $code .= $this->getIndent() . '}' . PHP_EOL; - return $code; + $this->method = $name; + $this->methodDef = $methodDef; + if ($flags & Modifiers::ABSTRACT) { + $methodDef->functionDef = $this->parseFunctionDecl($methodStmt); + $methodDef->functionDef->method = true; + $this->checkRequiredArgNum($name, $methodDef, $methodStmt); + $this->classDef->addAbstractMethod($name, $flags, $methodDef); + } else { + $this->prepareFunction($methodStmt); + $this->checkRequiredArgNum($name, $methodDef, $methodStmt); + $this->classDef->addMethod($methodDef); + } + $this->resetMethod(); } - /** - * Re-resolve a trait method's late-bound `self`/`static`/`parent` return and - * parameter types to the class that is composing the trait. - * - * In PHP, `self` (and `static`) inside a trait refers to the using class, and - * `parent` refers to the using class's parent. The compiler records these as - * the trait's own name at parse time, which is wrong once the method is - * flattened into a class: interface/trait `self` comparisons and - * `detectClassOfExpr()` would otherwise observe the trait name instead of the - * consuming class. We clone the FunctionDef so the trait's standalone native - * function keeps its original (trait-context) types. - */ - private function reresolveTraitLateBoundTypes(ClassDef $usingClassDef, MethodDef $methodDef): void - { - $fn = $methodDef->functionDef; - // Always produce a distinct FunctionDef for the composing-class wrapper. - // The wrapper has a separate native signature from the trait function. - $newFn = clone $fn; - if ($fn->returnTypeKeyword !== '') { - $resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword); - if ($resolved !== null && $resolved !== $newFn->returnClass) { - $newFn->returnClass = $resolved; - } - } - $newArgs = []; - foreach ($newFn->argInfoList as $arg) { - $newArg = clone $arg; - if ($newArg->typeKeyword !== '') { - $resolved = $this->resolveLateBoundClass($usingClassDef, $newArg->typeKeyword); - if ($resolved !== null) { - if ($newArg->class !== '') { - $newArg->class = $resolved; - } - if ($newArg->declaredClass !== '') { - $newArg->declaredClass = $resolved; - } - } - } - $newArgs[] = $newArg; + private function withTraitNameContext(string $traitName, callable $callback): mixed + { + if (!$this->hasClass($traitName)) { + $this->error("Internal compiler error: trait `{$traitName}` is not available while composing AST"); + } + $traitDef = $this->getClass($traitName); + if ($traitDef->trait === null) { + $this->error("Internal compiler error: `{$traitName}` is not a trait AST template"); + } + + $savedNamespace = $this->namespace; + $savedUseNamespaces = $this->useNamespaces; + $savedUseAliases = $this->useAliases; + $savedUseFunctions = $this->useFunctions; + $savedUseConstants = $this->useConstants; + + $this->namespace = $traitDef->namespace; + $this->useNamespaces = $traitDef->traitUseNamespaces; + $this->useAliases = $traitDef->traitUseAliases; + $this->useFunctions = $traitDef->traitUseFunctions; + $this->useConstants = $traitDef->traitUseConstants; + try { + return $callback(); + } finally { + $this->namespace = $savedNamespace; + $this->useNamespaces = $savedUseNamespaces; + $this->useAliases = $savedUseAliases; + $this->useFunctions = $savedUseFunctions; + $this->useConstants = $savedUseConstants; } - $newFn->argInfoList = $newArgs; - $methodDef->functionDef = $newFn; + } + + private function isCompatibleTraitConstant(ConstantDef $existing, ConstantDef $incoming): bool + { + return $existing->flags === $incoming->flags + && $existing->type === $incoming->type + && $existing->class === $incoming->class + && $existing->value === $incoming->value; + } + + private function isCompatibleTraitProperty(PropertyDef $existing, PropertyDef $incoming): bool + { + return $existing->flags === $incoming->flags + && $existing->type === $incoming->type + && $existing->class === $incoming->class + && $existing->nullable === $incoming->nullable + && $existing->default === $incoming->default; } private function resolveLateBoundClass(ClassDef $usingClassDef, string $keyword): ?string @@ -4739,122 +4695,6 @@ CODE; return null; } - /** - * Validate a `parent::method()` call recorded inside a trait method. - * - * The trait has no parent of its own, so the only point at which the parent - * class is known is when a class actually uses the trait. At that moment we - * can statically resolve the parent method and reject private methods, which - * PHP would otherwise only report as a runtime "Call to private method" error. - */ - private function validateTraitParentCall(ClassDef $traitDef, ClassDef $usingClassDef, string $method, NodeAbstract $node): void - { - if (!$usingClassDef->extends) { - $this->fatalError( - $node, - "Cannot access parent when class `{$usingClassDef->getNamespacedName(false)}` has no parent" - ); - } - $parentClass = $usingClassDef->extends; - // Internal / not-compiled parents are opaque to the compiler; let the - // runtime enforce visibility for those. - if (!$this->hasClass($parentClass)) { - return; - } - if ($this->getMethodFlags($parentClass, $method) & Modifiers::PRIVATE) { - $this->fatalError( - $node, - "Cannot access private method `{$parentClass}::{$method}()` via parent:: in trait `{$traitDef->name}`" - ); - } - } - - /** - * Validate that a trait method being flattened into a class remains - * signature-compatible with any same-named method declared up the parent - * chain — the same compatibility contract a directly-declared override must - * satisfy (see `checkParentMethodCanBeOverridden`). - * - * Only the signature contract is enforced here (not the "cannot override - * private/final" rule), because a trait method is flattened into the class - * and, like a normal subclass method, is allowed to shadow a private parent - * method. PHP reports the incompatibility as a class-declaration fatal error - * ("Declaration of X::m() must be compatible with Y::m()"), which we surface - * at compile time so the broken program is rejected instead of being emitted - * and executed past a runtime fatal error. - */ - private function checkTraitMethodOverrideCompatibility(ClassDef $usingClassDef, MethodDef $methodDef, string $methodName): void - { - if ($methodName === '__construct' || $methodDef->node === null) { - return; - } - $classDef = $usingClassDef; - while (true) { - $extends = $classDef->extends; - if (!$extends) { - break; - } - if ($classDef->inheritedFromInternalClass) { - $modifiers = Reflection::getClassMethodModifiers($extends, $methodName); - if ($modifiers !== null && ($modifiers & \ReflectionMethod::IS_FINAL)) { - $this->fatalError($methodDef->node, "Cannot override final method `{$extends}::{$methodName}()`"); - } - break; - } - // Dynamically supplied parents are opaque to the compiler. - if (!$this->hasClass($extends)) { - break; - } - $classDef = $this->getClass($extends); - if ($classDef->hasMethod($methodName)) { - $parentMethodDef = $classDef->getMethod($methodName); - // A private method is a separate slot and may be shadowed by the - // method imported from the trait. - if ($parentMethodDef->flags & Modifiers::PRIVATE) { - break; - } - if ($parentMethodDef->flags & Modifiers::FINAL) { - $this->fatalError($methodDef->node, "Cannot override final method `{$extends}::{$methodName}()`"); - } - $this->validateMethodOverrideSignature( - $methodDef->node, - $methodName, - $methodDef, - $parentMethodDef, - $extends - ); - break; - } - if ($classDef->hasAbstractMethod($methodName) && isset($classDef->abstractMethodDefs[strtolower($methodName)])) { - $this->validateMethodOverrideSignature( - $methodDef->node, - $methodName, - $methodDef, - $classDef->getAbstractMethod($methodName), - $extends - ); - break; - } - } - } - - private function isCompatibleTraitConstant(ConstantDef $existing, ConstantDef $incoming): bool - { - return $existing->flags === $incoming->flags && - $existing->type === $incoming->type && - $existing->class === $incoming->class && - $existing->value === $incoming->value; - } - - private function isCompatibleTraitProperty(PropertyDef $existing, PropertyDef $incoming): bool - { - return $existing->flags === $incoming->flags && - $existing->type === $incoming->type && - $existing->class === $incoming->class && - $existing->nullable === $incoming->nullable && - $existing->default === $incoming->default; - } - private function getRegisterClassFunctionArgDef(ClassDef|InterfaceDef $classDef): string { $depsCeList = $this->getRegisterClassFunctionCeList($classDef); diff --git a/src/gen_stub.php b/src/gen_stub.php index 779d39c8..09741cd2 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -4637,6 +4637,12 @@ class FileInfo { continue; } + // TypePHP traits are compile-time AST templates and must not become + // Zend class entries in the generated arginfo registration code. + if ($stmt instanceof Trait_) { + continue; + } + if ($stmt instanceof Stmt\ClassLike) { $className = $stmt->namespacedName; $constInfos = []; @@ -4645,7 +4651,7 @@ class FileInfo { $enumCaseInfos = []; if (!$stmt instanceof PhpParser\Node\Stmt\Interface_) { - getTranslator()->parseTraitUseForStub($stmt, $className); + getTranslator()->composeTraitAst($stmt, $className); } ClassInfo::$currentClass = $className->toString(); diff --git a/tests/compiler/stdlib/class-exists-class-constant.phpt b/tests/compiler/stdlib/class-exists-class-constant.phpt index 42a7a37c..dc03eb45 100644 --- a/tests/compiler/stdlib/class-exists-class-constant.phpt +++ b/tests/compiler/stdlib/class-exists-class-constant.phpt @@ -40,7 +40,7 @@ bool(true) pick:interface bool(true) pick:trait -bool(true) +bool(false) pick:enum bool(true) bool(false) diff --git a/tests/compiler/stdlib/class_exists.phpt b/tests/compiler/stdlib/class_exists.phpt index 78dbf93a..bfab60a7 100644 --- a/tests/compiler/stdlib/class_exists.phpt +++ b/tests/compiler/stdlib/class_exists.phpt @@ -20,7 +20,7 @@ function main() { var_dump(interface_exists("MyInterface")); var_dump(interface_exists("NonexistentInterface")); - // trait_exists + // TypePHP traits are compile-time-only AST templates. var_dump(trait_exists("MyTrait")); var_dump(trait_exists("NonexistentTrait")); @@ -40,7 +40,7 @@ bool(false) bool(false) bool(true) bool(false) -bool(true) +bool(false) bool(false) bool(true) bool(false) diff --git a/tests/compiler/trait/002.phpt b/tests/compiler/trait/002.phpt index d6526880..6194b31b 100644 --- a/tests/compiler/trait/002.phpt +++ b/tests/compiler/trait/002.phpt @@ -10,7 +10,7 @@ trait HelloTrait { } function main() { - eval("(new class { use HelloTrait;})->hello();"); + (new class { use HelloTrait; })->hello(); } ?> --EXPECT-- diff --git a/tests/compiler/trait/trait-ast-composition.phpt b/tests/compiler/trait/trait-ast-composition.phpt new file mode 100644 index 00000000..57e00ad8 --- /dev/null +++ b/tests/compiler/trait/trait-ast-composition.phpt @@ -0,0 +1,52 @@ +--TEST-- +Trait methods are compiled as class methods for every consuming class +--FILE-- +value = $value; + } + + public function getValue(): int { + return $this->value; + } +} + +trait OuterTemplate { + use InnerTemplate; +} + +class FirstConsumer { + use OuterTemplate; +} + +class SecondConsumer { + use OuterTemplate { getValue as readValue; } +} + +function main() { + $first = new FirstConsumer(); + $second = new SecondConsumer(); + $first->setValue(11); + $second->setValue(22); + var_dump($first->className(), $first->traitName(), $first->getValue()); + var_dump($second->className(), $second->traitName(), $second->readValue()); +} +?> +--EXPECT-- +string(13) "FirstConsumer" +string(13) "InnerTemplate" +int(11) +string(14) "SecondConsumer" +string(13) "InnerTemplate" +int(22) diff --git a/tests/compiler/trait/trait-import-context.phpt b/tests/compiler/trait/trait-import-context.phpt new file mode 100644 index 00000000..c286143d --- /dev/null +++ b/tests/compiler/trait/trait-import-context.phpt @@ -0,0 +1,55 @@ +--TEST-- +Trait methods retain their declaring file import context after AST composition +--FILE-- +value, importedLabel(), importedValue, self::SELF_VALUE]; + } + } +} + +namespace TraitImports\Consumer { + use TraitImports\Template\ImportedNames; + + class Example { + use ImportedNames; + public const string SELF_VALUE = 'self'; + } +} + +namespace { + function main(): void { + var_dump((new TraitImports\Consumer\Example())->values()); + } +} +?> +--EXPECT-- +array(4) { + [0]=> + string(5) "class" + [1]=> + string(8) "function" + [2]=> + string(8) "constant" + [3]=> + string(4) "self" +} diff --git a/tests/std/core/002_class_exists.phpt b/tests/std/core/002_class_exists.phpt index 2709d355..18f3c780 100644 --- a/tests/std/core/002_class_exists.phpt +++ b/tests/std/core/002_class_exists.phpt @@ -18,7 +18,7 @@ function main() { echo interface_exists("NonExistentInterface") ? "fail\n" : "ok-false\n"; echo "trait_exists:\n"; - echo trait_exists("MyTrait") ? "ok-true\n" : "fail\n"; + echo trait_exists("MyTrait") ? "fail\n" : "ok-false\n"; echo trait_exists("NonExistentTrait") ? "fail\n" : "ok-false\n"; echo "enum_exists:\n"; @@ -36,7 +36,7 @@ interface_exists: ok-true ok-false trait_exists: -ok-true +ok-false ok-false enum_exists: ok-true