- 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 inheritancemaster
parent
30b6f43bb7
commit
8da7b5b4e5
19 changed files with 470 additions and 6 deletions
@ -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)); |
||||
} |
||||
@ -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']; |
||||
} |
||||
} |
||||
@ -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…
Reference in new issue