diff --git a/docs/zh-cn/OBJECT_STORAGE_AND_PASSING_MODELS.md b/docs/zh-cn/OBJECT_STORAGE_AND_PASSING_MODELS.md index 4ba59f52..0abd1284 100644 --- a/docs/zh-cn/OBJECT_STORAGE_AND_PASSING_MODELS.md +++ b/docs/zh-cn/OBJECT_STORAGE_AND_PASSING_MODELS.md @@ -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 的边界 diff --git a/docs/zh-cn/STD_CONTAINER_PARAMETER_ATTRIBUTES.md b/docs/zh-cn/STD_CONTAINER_PARAMETER_ATTRIBUTES.md new file mode 100644 index 00000000..dce84f07 --- /dev/null +++ b/docs/zh-cn/STD_CONTAINER_PARAMETER_ATTRIBUTES.md @@ -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 中。修改参数注解会使源文件及依赖它的 +调用方重新转换;仅复用缓存时,入口类型恢复仍然保留。 diff --git a/phpunit/code/std-container-parameters.php b/phpunit/code/std-container-parameters.php new file mode 100644 index 00000000..173f640f --- /dev/null +++ b/phpunit/code/std-container-parameters.php @@ -0,0 +1,34 @@ +id; + } +} + +function main(): void +{ + $vec = std::vector(Type::Int); + $vec[] = 7; + var_dump(std_parameter_vector($vec)); +} diff --git a/phpunit/src/Build/IncrementalDeclarationTest.php b/phpunit/src/Build/IncrementalDeclarationTest.php index 301f7b9a..b22d1ae0 100644 --- a/phpunit/src/Build/IncrementalDeclarationTest.php +++ b/phpunit/src/Build/IncrementalDeclarationTest.php @@ -119,6 +119,36 @@ PHP); self::assertSame($this->provider, $symbols['constant:Incremental\\LIMIT']); } + public function testStdContainerParameterContractsSurviveWarmConversionAndInvalidateCallers(): void + { + file_put_contents($this->provider, <<<'PHP' +consumer, <<<'PHP' +convertProject(); + $providerCpp = $this->invoke($first, 'getCppFile', $this->provider); + $coldCode = file_get_contents($providerCpp); + self::assertStringContainsString('auto &vec_ref = php::toStdContainer>', $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', file_get_contents($providerCpp)); + } + public function testUnchangedGeneratedCppKeepsItsTimestamp(): void { $first = $this->convertProject(); diff --git a/phpunit/src/CompileTimeAttributeRegistryTest.php b/phpunit/src/CompileTimeAttributeRegistryTest.php index 68dccb17..cb7c7eba 100644 --- a/phpunit/src/CompileTimeAttributeRegistryTest.php +++ b/phpunit/src/CompileTimeAttributeRegistryTest.php @@ -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( diff --git a/phpunit/src/StdContainerParameterTest.php b/phpunit/src/StdContainerParameterTest.php new file mode 100644 index 00000000..fe60e5d6 --- /dev/null +++ b/phpunit/src/StdContainerParameterTest.php @@ -0,0 +1,93 @@ +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>(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, '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']; + } +} diff --git a/src/Context/CompilationStateTrait.php b/src/Context/CompilationStateTrait.php index 8a623547..be552c97 100644 --- a/src/Context/CompilationStateTrait.php +++ b/src/Context/CompilationStateTrait.php @@ -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 diff --git a/src/Entity/ArgInfo.php b/src/Entity/ArgInfo.php index ac6d3947..bee9389d 100644 --- a/src/Entity/ArgInfo.php +++ b/src/Entity/ArgInfo.php @@ -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; diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 4780fac1..8a69b583 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -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); diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index a7c362d1..eeb2c900 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -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); } diff --git a/src/Parser/StdContainerTrait.php b/src/Parser/StdContainerTrait.php index c72a2f97..5110b2e3 100644 --- a/src/Parser/StdContainerTrait.php +++ b/src/Parser/StdContainerTrait.php @@ -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 diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 75e2e67e..24f0340e 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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 diff --git a/src/Transform/CompileTimeAttribute.php b/src/Transform/CompileTimeAttribute.php index 3342d427..aa5aa6ac 100644 --- a/src/Transform/CompileTimeAttribute.php +++ b/src/Transform/CompileTimeAttribute.php @@ -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')) { diff --git a/src/Transform/CompileTimeAttributeRegistry.php b/src/Transform/CompileTimeAttributeRegistry.php index bed843a0..8d21dc66 100644 --- a/src/Transform/CompileTimeAttributeRegistry.php +++ b/src/Transform/CompileTimeAttributeRegistry.php @@ -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; } diff --git a/src/Transform/Visitor.php b/src/Transform/Visitor.php index 323411bf..c05152f5 100644 --- a/src/Transform/Visitor.php +++ b/src/Transform/Visitor.php @@ -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]); diff --git a/src/Translator.php b/src/Translator.php index bdd14331..be7e67be 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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); diff --git a/src/polyfills.php b/src/polyfills.php index 4710992b..91b9a7b5 100644 --- a/src/polyfills.php +++ b/src/polyfills.php @@ -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. diff --git a/tests/compiler/std-vector/parameter-attribute-trait.phpt b/tests/compiler/std-vector/parameter-attribute-trait.phpt new file mode 100644 index 00000000..2e4f4726 --- /dev/null +++ b/tests/compiler/std-vector/parameter-attribute-trait.phpt @@ -0,0 +1,26 @@ +--TEST-- +StdVector parameter contracts in traits resolve self in the consuming class +--FILE-- +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 diff --git a/tests/compiler/std-vector/parameter-attributes.phpt b/tests/compiler/std-vector/parameter-attributes.phpt new file mode 100644 index 00000000..fc6f2c3d --- /dev/null +++ b/tests/compiler/std-vector/parameter-attributes.phpt @@ -0,0 +1,71 @@ +--TEST-- +Std container parameter declarations restore checked shared containers without toStd calls +--FILE-- +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