feat(parser): add std container parameter attribute support

- Introduce StdVector, StdMap, and StdOrderedMap parameter attributes
- Add explicit std-container contract to ArgInfo entity
- Implement parameter validation and type checking logic
- Add error handling for invalid container parameter usage
- Support container parameter contracts in function signatures
- Generate automatic type conversion at function entry points
- Add compilation tests for std container parameter contracts
- Update documentation with container parameter rules and examples
- Implement compatibility checks between child and parent classes
- Add support for container parameters in trait methods and inheritance
master
韩天峰 3 weeks ago
parent 30b6f43bb7
commit 8da7b5b4e5
  1. 6
      docs/zh-cn/OBJECT_STORAGE_AND_PASSING_MODELS.md
  2. 61
      docs/zh-cn/STD_CONTAINER_PARAMETER_ATTRIBUTES.md
  3. 34
      phpunit/code/std-container-parameters.php
  4. 30
      phpunit/src/Build/IncrementalDeclarationTest.php
  5. 4
      phpunit/src/CompileTimeAttributeRegistryTest.php
  6. 93
      phpunit/src/StdContainerParameterTest.php
  7. 3
      src/Context/CompilationStateTrait.php
  8. 2
      src/Entity/ArgInfo.php
  9. 3
      src/Parser/AssignOpTrait.php
  10. 3
      src/Parser/PropertyAccessTrait.php
  11. 76
      src/Parser/StdContainerTrait.php
  12. 9
      src/Preprocessor.php
  13. 20
      src/Transform/CompileTimeAttribute.php
  14. 5
      src/Transform/CompileTimeAttributeRegistry.php
  15. 3
      src/Transform/Visitor.php
  16. 9
      src/Translator.php
  17. 18
      src/polyfills.php
  18. 26
      tests/compiler/std-vector/parameter-attribute-trait.phpt
  19. 71
      tests/compiler/std-vector/parameter-attributes.phpt

@ -112,8 +112,10 @@ PHP 参数最多只能声明一个非泛型类名或伪类型,不能同时携
类型、数组维度和长度。当前使用 `UnsafePtr`/`std::unsafe_cast()` 加编译器类型 ID 校验,
而不是把所有组合生成为 PHP class。
理论上可以增加参数和返回值注解描述泛型,但这要求每个声明、调用、返回、属性和传播点
都维护额外元数据,PHP Reflection 仍无法完整表达它。当前不引入这套独立泛型 ABI。
具名函数和方法参数可以通过 `#[StdVector(Type::Int)] $values` 等内置注解声明容器类型,
由编译器自动生成类型校验及容器引用恢复。调用边界仍然使用 Box/`php::Var`,不引入独立
泛型 ABI;返回值、属性和其他传播点也没有因此获得完整泛型系统。
具体规则见 [Std 容器参数类型注解](STD_CONTAINER_PARAMETER_ATTRIBUTES.md)。
### 4.3 Box 的边界

@ -0,0 +1,61 @@
# Std 容器参数类型注解
`StdVector`、`StdMap`、`StdOrderedMap` 是 TypePHP 内置的编译期参数类型声明。
它们描述容器种类和元素类型,并自动完成以前需要手写的 `toStd*()` 类型恢复。
```php
function append(#[StdVector(Type::Int)] $vec): void
{
$vec[] = 42;
}
function update(#[StdMap(Type::String, User::class)] $users): void
{
$users['alice'] = new User();
}
function visit(#[StdOrderedMap(Type::Int, Type::String)] $names): void
{
foreach ($names as $key => $value) {
echo $key, ':', $value, "\n";
}
}
```
## 声明规则
- 一个参数只能有一个容器类型注解,不能重复或混用。
- 注解是唯一类型来源,不能再写 `mixed`、`array`、`box` 或其他 PHP 类型。
- `StdVector` 接受一个类型实参;`StdMap`、`StdOrderedMap` 接受 key/value 两个类型实参。
- 类型实参使用现有 std 工厂支持的 `Type::*` 或 `ClassName::class`;map 的 key 只支持 `Type::Int`、`Type::String`。
- 使用位置为具名函数和方法参数,包括接口、抽象方法和 trait 方法;支持 Attribute 别名导入。
- 首版不支持引用、可变、带默认值、构造器属性提升参数,以及 Closure、箭头函数和 Generator 参数。
- 容器内不能保存 Native 对象并通过 Box 参数边界传递;原有 Native 容器逃逸限制不变。
- 参数绑定不能换成其他 Box/值、`unset()` 或被 Closure 按引用捕获;同类型 std 容器赋值仍按现有规则复制内容。元素的读取、更新、追加和遍历继续遵循现有 std 容器规则。
以下声明会产生编译错误:
```php
function invalid(#[StdVector(Type::Int)] mixed $vec): void {}
```
## 调用和生命周期
函数的底层调用 ABI 仍为 `php::Var`,容器仍由 Box 管理生命周期。编译器在入口
检查 Box、容器种类及元素类型,取得具体 C++ 容器引用;不复制容器,也不逐个转换元素。
错误的容器类型、PHP 数组及其他非 Box 实参会产生 `TypeError`。
函数中修改容器内容时,调用者可观察到相同修改:
```php
$vec = std::vector(Type::Int);
append($vec);
var_dump($vec[0]); // int(42)
```
现有动态调用把已知 std 容器转换为 PHP 数组的规则不变。如果通过动态 callable
调用这类函数,需传入实际的 Box 值,而不是已经转换成 PHP 数组的值。
注解不会让普通 Zend PHP 获得 C++ 泛型容器支持,也不会把 C++ 模板类型变成 PHP 的原生参数类型。
容器契约保存在声明缓存和导出的 library stub 中。修改参数注解会使源文件及依赖它的
调用方重新转换;仅复用缓存时,入口类型恢复仍然保留。

@ -0,0 +1,34 @@
<?php
use StdVector as VectorOf;
class StdParameterUser
{
public function __construct(public int $id) {}
}
function std_parameter_vector(#[VectorOf(Type::Int)] $vec): int
{
$vec[] = 42;
return $vec[0];
}
function std_parameter_map(#[StdMap(Type::String, Type::Float)] $map): float
{
$map['value'] = 2.5;
return $map['value'];
}
class StdParameterReceiver
{
public function accept(#[StdOrderedMap(Type::Int, StdParameterUser::class)] $users): int
{
return $users[0]->id;
}
}
function main(): void
{
$vec = std::vector(Type::Int);
$vec[] = 7;
var_dump(std_parameter_vector($vec));
}

@ -119,6 +119,36 @@ PHP);
self::assertSame($this->provider, $symbols['constant:Incremental\\LIMIT']);
}
public function testStdContainerParameterContractsSurviveWarmConversionAndInvalidateCallers(): void
{
file_put_contents($this->provider, <<<'PHP'
<?php
namespace Incremental;
function answer(#[\StdVector(\Type::Int)] $vec): int { return $vec[0]; }
PHP);
file_put_contents($this->consumer, <<<'PHP'
<?php
function main(): void {
$vec = \std::vector(\Type::Int);
$vec[] = 7;
var_dump(\Incremental\answer($vec));
}
PHP);
$first = $this->convertProject();
$providerCpp = $this->invoke($first, 'getCppFile', $this->provider);
$coldCode = file_get_contents($providerCpp);
self::assertStringContainsString('auto &vec_ref = php::toStdContainer<php::StdVector<php::Int>>', $coldCode);
$second = $this->convertProject();
self::assertFalse($this->invoke($second, 'shouldRegeneratePhpFile', $this->provider));
self::assertSame($coldCode, file_get_contents($providerCpp));
$function = $this->invoke($second, 'getFunction', 'incremental__answer');
self::assertSame('vector', $function->argInfoList[0]->stdContainer['kind']);
file_put_contents($this->provider, str_replace('Type::Int', 'Type::Float', file_get_contents($this->provider)));
$third = $this->convertProject();
self::assertTrue($this->invoke($third, 'shouldRegeneratePhpFile', $this->consumer));
self::assertStringContainsString('php::StdVector<php::Float>', file_get_contents($providerCpp));
}
public function testUnchangedGeneratedCppKeepsItsTimestamp(): void
{
$first = $this->convertProject();

@ -10,7 +10,7 @@ final class CompileTimeAttributeRegistryTest extends TestCase
$expected = [
'Native', 'MethodsFor', 'NoExport', 'WasmExport', 'Getter', 'Setter', 'With', 'Printer', 'Arrayable',
'NotNull', 'NotEmpty', 'Validate', 'Override', 'MustUse', 'Immutable', 'Hot', 'Cold', 'Constructor',
'ArrayDef',
'ArrayDef', 'StdVector', 'StdMap', 'StdOrderedMap',
];
$this->assertSame($expected, CompileTimeAttributeRegistry::names());
@ -27,7 +27,7 @@ final class CompileTimeAttributeRegistryTest extends TestCase
$this->assertContains('Getter', CompileTimeAttributeRegistry::names(true));
$this->assertContains('Override', CompileTimeAttributeRegistry::names(true));
$this->assertSame(
['Native', 'MethodsFor', 'NoExport', 'WasmExport', 'ArrayDef'],
['Native', 'MethodsFor', 'NoExport', 'WasmExport', 'ArrayDef', 'StdVector', 'StdMap', 'StdOrderedMap'],
CompileTimeAttributeRegistry::namesForPhase(CompileTimeAttributeRegistry::PHASE_PREPROCESS),
);
$this->assertSame(

@ -0,0 +1,93 @@
<?php
use PHPUnit\Framework\Attributes\DataProvider;
use TypePhp\CompilerTest;
use TypePhp\Exception\TestError;
final class StdContainerParameterTest extends BaseTest
{
public function testLibraryStubPreservesContainerContracts(): void
{
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$generator = new \TypePhp\Generator\LibraryImportStubGenerator(
(new ReflectionProperty($compiler, 'parser'))->getValue($compiler),
(new ReflectionProperty($compiler, 'printer'))->getValue($compiler),
);
$code = $generator->generate([TYPEPHP_ROOT_PATH . '/phpunit/code/std-container-parameters.php'], []);
self::assertStringContainsString('StdVector(', $code);
self::assertStringContainsString('StdMap(', $code);
self::assertStringContainsString('StdOrderedMap(', $code);
self::assertStringContainsString('Type::Int', $code);
self::assertStringContainsString('StdParameterUser::class', $code);
}
public function testBoxAbiAndNativeContainerBindings(): void
{
$this->compile('std-container-parameters.php');
global $translator;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/std-container-parameters.php';
$code = file_get_contents($translator->getCppFile($source));
self::assertStringContainsString('php_std_parameter_vector(php::Var vec)', $code);
self::assertStringContainsString('auto &vec_ref = php::toStdContainer<php::StdVector<php::Int>>(vec, ', $code);
self::assertStringContainsString('auto &map_ref = php::toStdContainer<', $code);
self::assertStringContainsString('auto &users_ref = php::toStdContainer<', $code);
self::assertStringNotContainsString('php::Var vec;', $code);
$function = (new ReflectionMethod($translator, 'getFunction'))->invoke($translator, 'std_parameter_vector');
$parameter = $function->argInfoList[0];
self::assertFalse($parameter->undeclared);
self::assertFalse($parameter->nullable);
self::assertSame('vector', $parameter->stdContainer['kind']);
self::assertArrayNotHasKey('typeId', $parameter->stdContainer);
self::assertSame($parameter->stdContainer, unserialize(serialize($parameter))->stdContainer);
}
#[DataProvider('invalidDeclarations')]
public function testInvalidDeclarations(string $source, string $message): void
{
$directory = sys_get_temp_dir() . '/std-parameter-' . bin2hex(random_bytes(6));
mkdir($directory);
$file = $directory . '/invalid.php';
file_put_contents($file, '<?php ' . $source);
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
global $translator;
$translator = $compiler;
try {
$compiler->addFiles([$file]);
$compiler->prepareFile($file);
$compiler->convertFile($file);
self::fail('Invalid container parameter was accepted');
} catch (TestError|\TypePhp\Exception\SyntaxError $error) {
self::assertStringContainsString($message, $error->getMessage());
} finally {
unlink($file);
rmdir($directory);
}
}
public static function invalidDeclarations(): iterable
{
yield 'mixed conflict' => ['function foo(#[StdVector(Type::Int)] mixed $vec): void {}', 'a PHP type cannot also be declared'];
yield 'array conflict' => ['function foo(#[StdMap(Type::Int, Type::Int)] array $vec): void {}', 'a PHP type cannot also be declared'];
yield 'duplicate' => ['function foo(#[StdVector(Type::Int), StdVector(Type::Int)] $vec): void {}', 'cannot be repeated'];
yield 'two containers' => ['function foo(#[StdVector(Type::Int), StdMap(Type::Int, Type::Int)] $vec): void {}', 'cannot be applied to the same declaration'];
yield 'missing type' => ['function foo(#[StdVector] $vec): void {}', 'expects 1 type argument'];
yield 'vector size' => ['function foo(#[StdVector(Type::Int, 3)] $vec): void {}', 'expects 1 type argument'];
yield 'map missing value' => ['function foo(#[StdMap(Type::Int)] $vec): void {}', 'expects 2 type argument'];
yield 'invalid key' => ['function foo(#[StdMap(Type::Float, Type::Int)] $vec): void {}', 'key only supports Type::Int or Type::String'];
yield 'literal argument' => ['function foo(#[StdVector("int")] $vec): void {}', 'expects a Type constant'];
yield 'named argument' => ['function foo(#[StdVector(valueType: Type::Int)] $vec): void {}', 'requires positional type arguments'];
yield 'reference' => ['function foo(#[StdVector(Type::Int)] &$vec): void {}', 'does not support reference'];
yield 'variadic' => ['function foo(#[StdVector(Type::Int)] ...$vec): void {}', 'does not support reference'];
yield 'default' => ['function foo(#[StdVector(Type::Int)] $vec = null): void {}', 'does not support reference'];
yield 'closure' => ['$fn = function(#[StdVector(Type::Int)] $vec) {};', 'named function or method parameters'];
yield 'arrow' => ['$fn = fn(#[StdVector(Type::Int)] $vec) => 1;', 'named function or method parameters'];
yield 'wrong target' => ['#[StdVector(Type::Int)] function foo(): void {}', 'can only be applied'];
yield 'generator' => ['function foo(#[StdVector(Type::Int)] $vec) { yield 1; }', 'not supported on generators'];
yield 'native elements' => ['#[Native] class User {} function foo(#[StdVector(User::class)] $vec): void {}', 'cannot hold Native objects'];
yield 'reference capture' => ['function foo(#[StdVector(Type::Int)] $vec): void { $fn = function() use (&$vec) {}; }', 'cannot be captured by reference'];
yield 'unset binding' => ['function foo(#[StdVector(Type::Int)] $vec): void { unset($vec); }', 'bindings cannot be unset'];
yield 'replace boxed binding' => ['function foo(#[StdVector(Type::Int)] $vec, $other): void { $vec = $other; }', 'bindings cannot be replaced'];
yield 'incompatible override' => ['class A { public function foo(#[StdVector(Type::Int)] $vec): void {} } class B extends A { public function foo(#[StdVector(Type::Float)] $vec): void {} }', 'must be compatible'];
yield 'conflicting abstract traits' => ['trait A { abstract public function foo(#[StdVector(Type::Int)] $vec): void; } trait B { abstract public function foo(#[StdVector(Type::Float)] $vec): void; } abstract class C { use A, B; }', 'incompatible types'];
}
}

@ -40,6 +40,9 @@ trait CompilationStateTrait
foreach ($analysis['captures'] as $sourceName => $_) {
$name = $this->escapeVarName($sourceName);
if (isset($this->context->arguments[$name])) {
if (!empty($this->context->stdContainers[$name]['parameter'])) {
$this->fatalError(new Variable($sourceName), 'Std container parameters cannot be captured by reference');
}
$type = $this->getRawVarType($name);
if (!$degradeArguments
|| $type === Type::VAR

@ -19,6 +19,8 @@ class ArgInfo
public string $type;
public string $default = '';
public ?ArrayInitPlan $arrayInitPlan = null;
/** Explicit std-container contract; the call ABI remains php::Var. */
public ?array $stdContainer = null;
/** Original declaration AST; lowered to $default only in the convert phase. */
public ?Expr $defaultExpr = null;
public ?Expr $defaultValue = null;

@ -555,6 +555,9 @@ trait AssignOpTrait
if ($copyAssign !== null) {
return $copyAssign;
}
if (!empty($this->context->stdContainers[$var]['parameter'])) {
$this->fatalError($left, 'Std container parameter bindings cannot be replaced; modify the container contents instead');
}
}
// Infer the type and obtain the object's class name; return an empty string for non-objects
$rightClass = $this->detectClassOfExpr($right);

@ -1077,6 +1077,9 @@ trait PropertyAccessTrait
$this->fatalError($var, 'Attempt to unset static property ' . $this->parseIdentifier($var->class) . '::$' . $this->parseIdentifier($var->name));
} elseif ($this->isVarExpr($var)) {
$name = $this->parseIdentifier($var);
if (!empty($this->context->stdContainers[$name]['parameter'])) {
$this->fatalError($var, 'Std container parameter bindings cannot be unset');
}
if (!$this->hasVar($name)) {
$this->errorUndefinedVariable($var);
}

@ -17,9 +17,85 @@ use PhpParser\Node\Name;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\NodeAbstract;
use PhpParser\Node;
use TypePhp\Context\FunctionContext;
use TypePhp\Entity\FunctionDef;
use TypePhp\Transform\CompileTimeAttribute;
trait StdContainerTrait
{
protected function parseStdParameterDefinition(Node\Param $param): ?array
{
foreach (['StdVector' => 'vector', 'StdMap' => 'map', 'StdOrderedMap' => 'orderedMap'] as $name => $method) {
$attribute = CompileTimeAttribute::find($param, $name);
if ($attribute === null) {
continue;
}
if ($param->type !== null) {
$this->fatalError($param, $name . ' is the parameter type declaration; a PHP type cannot also be declared');
}
if ($param->byRef || $param->variadic || $param->default !== null || $param->isPromoted()) {
$this->fatalError($param, $name . ' does not support reference, variadic, defaulted or promoted parameters');
}
$expected = $method === 'vector' ? 1 : 2;
if (count($attribute->args) !== $expected) {
$this->fatalError($attribute, $name . ' expects ' . $expected . ' type argument(s)');
}
foreach ($attribute->args as $argument) {
if ($argument->name !== null || $argument->unpack || $argument->byRef) {
$this->fatalError($argument, $name . ' requires positional type arguments');
}
}
// Reuse factory parsing, including class-name resolution and the
// canonical template/type-ID key, without leaking locals into the
// declaration context. Cache only the contract, not a build's ID.
$context = $this->context;
$this->context = new FunctionContext();
try {
$call = new StaticCall(new Name('std'), new Identifier($method), $attribute->args);
if ($method === 'vector') {
$this->parseStdVector('__std_parameter', $call);
} elseif ($method === 'map') {
$this->parseStdMap('__std_parameter', $call);
} else {
$this->parseStdOrderedMap('__std_parameter', $call);
}
$info = $this->context->stdContainers['__std_parameter'];
if ($this->isNativeObjectClass($info['class'] ?? '')) {
$this->fatalError($attribute, 'Std container parameters cannot hold Native objects across a Box boundary');
}
unset($info['typeId']);
return $info;
} finally {
$this->context = $context;
}
}
return null;
}
protected function initializeStdContainerParameters(FunctionDef $function): string
{
$code = '';
foreach ($function->argInfoList as $argument) {
if ($argument->stdContainer === null) {
continue;
}
$info = $this->addStdTypeId($argument->stdContainer);
$info['parameter'] = true;
$this->context->stdContainers[$argument->name] = $info;
$type = match ($info['kind']) {
'vector' => Type::STD_VECTOR,
'map' => Type::STD_MAP,
'ordered_map' => Type::STD_ORDERED_MAP,
};
$this->addLocalVar($argument->name, $type);
$code .= 'if (UNEXPECTED(!' . $argument->name . '.isBox())) { php::throwStdContainerTypeMismatch(); }' . PHP_EOL;
$code .= 'auto &' . $argument->name . '_ref = php::toStdContainer<' . $info['decl'] . '>('
. $argument->name . ', ' . $info['typeId'] . ');' . PHP_EOL;
}
return $code;
}
/**
* Resolve the Native value class of a std container factory without
* creating container metadata. This is used before assignment lowering so

@ -1376,7 +1376,14 @@ class Preprocessor extends CompilerBase
$this->fatalError($param, 'Cannot use `$this` as parameter of class method');
}
$argInfo = new ArgInfo();
$argInfo->stdContainer = $this->parseStdParameterDefinition($param);
if ($argInfo->stdContainer !== null && $functionDef->generator) {
$this->fatalError($param, 'Std container parameter attributes are not supported on generators');
}
$type = $this->parseParameterType($param, $argInfo, $name);
if ($argInfo->stdContainer !== null) {
$argInfo->undeclared = false;
}
$argInfo->name = $name;
$argInfo->phpName = $phpName;
$argInfo->type = $type;
@ -1385,7 +1392,7 @@ class Preprocessor extends CompilerBase
$argInfo->property = $param->isPromoted();
$argInfo->immutable = \TypePhp\Transform\CompileTimeAttribute::consume($param, 'Immutable');
if ($param->type === null || $param->type instanceof NullableType) {
$argInfo->nullable = true;
$argInfo->nullable = $argInfo->stdContainer === null;
}
if (($param->byRef && $param->type !== null && !Type::isTypedRefType($type))
|| $param->type instanceof NullableType

@ -14,6 +14,26 @@ use TypePhp\Exception\SyntaxError;
final class CompileTimeAttribute
{
public static function validateStdContainerParameterTarget(Node\FunctionLike $node): void
{
if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
return;
}
foreach ($node->getParams() as $parameter) {
foreach (['StdVector', 'StdMap', 'StdOrderedMap'] as $name) {
$attribute = self::find($parameter, $name);
if ($attribute !== null) {
throw new CompileTimeAttributeError(
$name . ' can only be applied to named function or method parameters',
$parameter,
$name,
$attribute,
);
}
}
}
}
public static function validateNode(Node $node): void
{
if (!property_exists($node, 'attrGroups')) {

@ -26,6 +26,7 @@ final class CompileTimeAttributeRegistry
public const ARGUMENTS_VALIDATE = 'validate';
public const ARGUMENTS_WASM_EXPORT = 'wasm_export';
public const ARGUMENTS_ARRAY_DEF = 'array_def';
public const ARGUMENTS_STD_CONTAINER = 'std_container';
public const PHASE_PREPROCESS = 'preprocess';
public const PHASE_ENTER = 'enter';
@ -101,6 +102,10 @@ final class CompileTimeAttributeRegistry
$add('Cold', [self::TARGET_FUNCTION, self::TARGET_METHOD], 'Cold can only be applied to functions or methods', self::ARGUMENTS_NONE, self::PHASE_ENTER, true, ['Hot']);
$add('Constructor', [self::TARGET_DECLARED_PROPERTY], 'Constructor can only be applied to instance properties', self::ARGUMENTS_NONE, self::PHASE_CLASS_LEAVE);
$add('ArrayDef', [self::TARGET_PROPERTY], 'ArrayDef can only be applied to properties', self::ARGUMENTS_ARRAY_DEF, self::PHASE_PREPROCESS);
$containerAttributes = ['StdVector', 'StdMap', 'StdOrderedMap'];
foreach ($containerAttributes as $name) {
$add($name, [self::TARGET_PARAMETER], $name . ' can only be applied to function or method parameters', self::ARGUMENTS_STD_CONTAINER, self::PHASE_PREPROCESS, true, array_values(array_diff($containerAttributes, [$name])));
}
return $definitions;
}

@ -92,6 +92,9 @@ class Visitor extends NodeVisitorAbstract
public function leaveNode(Node $node): null
{
if ($node instanceof Node\FunctionLike) {
$this->guard($node, static fn () => CompileTimeAttribute::validateStdContainerParameterTarget($node));
}
$nodeId = spl_object_id($node);
if (isset($this->propertyMagicBoundaries[$nodeId])) {
unset($this->propertyMagicBoundaries[$nodeId]);

@ -5338,7 +5338,8 @@ CODE;
$paramB = $b->params[$i];
$typeA = $paramA->type ? $this->typeNodeToString($paramA->type) : null;
$typeB = $paramB->type ? $this->typeNodeToString($paramB->type) : null;
if ($typeA !== $typeB) {
if ($typeA !== $typeB
|| $this->parseStdParameterDefinition($paramA) !== $this->parseStdParameterDefinition($paramB)) {
$this->fatalError(
$classStmt,
"Trait `{$traitA}` and Trait `{$traitB}` define the same abstract method `{$methodName}` " .
@ -6104,6 +6105,7 @@ CODE;
$this->markNativeObjectNonNull($argInfo->name);
}
}
$stdParameterBindings = $this->initializeStdContainerParameters($this->functionDef);
$this->initializeImmutableFunctionContext();
$this->prepareReferenceCaptureDegradations($v->stmts, true);
@ -6226,6 +6228,7 @@ CODE;
? $this->getIndent() . 'auto &this_ = *this;' . PHP_EOL
: '';
$preamble .= $this->genDegradedArgumentLocals();
$preamble .= $stdParameterBindings;
$preamble .= $this->genScopeVarDecl();
$preamble .= $this->genNativeObjectParameterChecks($this->functionDef);
// Runtime union/nullable parameter type checks
@ -6933,6 +6936,10 @@ CODE;
if ($this->isTopParameterType($parentArg)) {
return false;
}
if ($childArg->stdContainer !== null || $parentArg->stdContainer !== null) {
return $childArg->stdContainer !== null && $parentArg->stdContainer !== null
&& $this->getStdTypeKey($childArg->stdContainer) === $this->getStdTypeKey($parentArg->stdContainer);
}
$parentAcceptedTypes = $this->getParameterAcceptedTypes($parentArg);
$childAcceptedTypes = $this->getParameterAcceptedTypes($childArg);

@ -123,6 +123,24 @@ final readonly class ArrayDef
}
}
#[Attribute(Attribute::TARGET_PARAMETER)]
final readonly class StdVector
{
public function __construct(string $valueType) {}
}
#[Attribute(Attribute::TARGET_PARAMETER)]
final readonly class StdMap
{
public function __construct(string $keyType, string $valueType) {}
}
#[Attribute(Attribute::TARGET_PARAMETER)]
final readonly class StdOrderedMap
{
public function __construct(string $keyType, string $valueType) {}
}
/**
* Public compile-time type symbols shared by MethodsFor providers and std containers.
* This root class is deliberately distinct from the compiler-internal TypePhp\Type.

@ -0,0 +1,26 @@
--TEST--
StdVector parameter contracts in traits resolve self in the consuming class
--FILE--
<?php
trait VectorParameterConsumer
{
public function accept(#[StdVector(self::class)] $vec): void
{
echo $vec[0]->id, "\n";
}
}
class VectorParameterUser
{
use VectorParameterConsumer;
public function __construct(public int $id) {}
}
function main(): void
{
$vec = std::vector(VectorParameterUser::class);
$vec[] = new VectorParameterUser(9);
$receiver = new VectorParameterUser(1);
$receiver->accept($vec);
}
?>
--EXPECT--
9

@ -0,0 +1,71 @@
--TEST--
Std container parameter declarations restore checked shared containers without toStd calls
--FILE--
<?php
use StdVector as VectorOf;
function append_values(#[VectorOf(Type::Int)] $vec): void
{
$vec[] = 42;
foreach ($vec as $value) { echo $value, "\n"; }
}
function fill_map(#[StdMap(Type::String, Type::Float)] $map): void
{
$map['value'] = 2.5;
}
function box_value($value) { return $value; }
class ParameterUser
{
public function __construct(public int $id) {}
}
class ParameterReceiver
{
public function accept(#[StdOrderedMap(Type::Int, ParameterUser::class)] $users): void
{
echo $users[0]->id, "\n";
}
}
function main(): void
{
$vec = std::vector(Type::Int);
$vec[] = 7;
append_values(vec: $vec);
var_dump($vec[1]);
$callback = 'append_values';
$box = box_value($vec);
$callback($box);
var_dump(count($vec));
$map = std::map(Type::String, Type::Float);
fill_map($map);
var_dump($map['value']);
$users = std::orderedMap(Type::Int, ParameterUser::class);
$users[0] = new ParameterUser(9);
$receiver = new ParameterReceiver();
$receiver->accept($users);
$wrong = std::vector(Type::Float);
try { append_values($wrong); } catch (TypeError $e) { echo "wrong value type\n"; }
try { append_values($map); } catch (TypeError $e) { echo "wrong container kind\n"; }
try { append_values([1, 2]); } catch (TypeError $e) { echo "not a container\n"; }
}
?>
--EXPECT--
7
42
int(42)
7
42
42
int(3)
float(2.5)
9
wrong value type
wrong container kind
not a container
Loading…
Cancel
Save