- 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