@ -0,0 +1,231 @@ |
||||
# 局部非逃逸 Closure 原生化设计 |
||||
|
||||
## 1. 背景 |
||||
|
||||
TypePHP 当前把所有匿名函数和箭头函数都实现为真正的 Zend `Closure`。编译器先生成 |
||||
一个 `php::ClosureFn`,PHPX 再为每个闭包创建: |
||||
|
||||
- `zend_function` 及其参数信息; |
||||
- 保存 C++ 回调、绑定对象和捕获值的 `ClosureCarrier`; |
||||
- 由 `zend_create_fake_closure()` 创建的 Zend `Closure` 对象。 |
||||
|
||||
调用闭包时仍需经过 callable 解析、Zend 调用帧和参数数组。这个实现是 PHP 可见 |
||||
Closure 的必要兼容路径,但对于只在当前函数内直接调用的闭包,Closure 对象从未被 |
||||
观察,以上分配和动态调用没有语义价值。 |
||||
|
||||
本优化把编译器能够证明不逃逸的局部闭包生成为具体 C++ lambda,并把其调用点生成为 |
||||
直接 C++ 调用。不能证明安全时必须完整回退到现有 Closure Carrier 路径。 |
||||
|
||||
## 2. 目标 |
||||
|
||||
第一阶段目标如下: |
||||
|
||||
1. 消除局部闭包创建时的 `zend_function`、Carrier 和 Zend Closure 分配; |
||||
2. 消除每次调用的 callable 查询及 Zend execute frame; |
||||
3. 保留 PHP 的闭包创建时捕获、按值复制、可变捕获状态及参数求值顺序; |
||||
4. 不改变任何可能观察 Closure 对象身份或 Zend 调用帧的代码; |
||||
5. 以保守证明为前提,分析失败不产生诊断,只回退到原实现。 |
||||
|
||||
本优化属于 TypePHP 编译器内部实现,不新增 PHPX 公共 API。 |
||||
|
||||
## 3. 第一阶段适用范围 |
||||
|
||||
仅优化以下形态: |
||||
|
||||
```php |
||||
function calculate(int $value): int |
||||
{ |
||||
$offset = 1; |
||||
$callback = static fn (int $item): int => $item + $offset; |
||||
return $callback($value); |
||||
} |
||||
``` |
||||
|
||||
候选闭包必须同时满足: |
||||
|
||||
- 赋值是函数顶层的一条独立语句; |
||||
- 左值是普通局部变量; |
||||
- 该变量在函数中只有这一个定义; |
||||
- 至少存在一个调用点; |
||||
- 每个读取位置都只能是 `$callback(...)` 的直接调用目标; |
||||
- 赋值在源码和控制流上支配全部调用点; |
||||
- 调用只使用固定位置参数,暂不支持命名参数和 unpack; |
||||
- 参数没有默认值、引用或 variadic; |
||||
- 按值捕获,或对固定 `int/string/float/bool/array` 存储的按引用捕获; |
||||
- 闭包不是 generator,不按引用返回,也不包含嵌套函数或闭包; |
||||
- 闭包体不依赖 Zend 调用帧,例如不调用 `func_get_args()` 或 |
||||
`debug_backtrace()`; |
||||
- 第一阶段只处理全局函数体,不处理类方法中的 `$this`、词法 class scope 和 |
||||
late static binding。 |
||||
|
||||
这里的“函数顶层”限制很重要。TypePHP 通常把普通局部变量声明提升到 C++ 函数入口, |
||||
但 C++ lambda 的匿名类型必须通过 `auto` 在初始化位置声明;同时 PHP 的按值捕获发生 |
||||
在闭包表达式求值时,不能把初始化提前到函数入口。 |
||||
|
||||
## 4. 必须回退的情况 |
||||
|
||||
只要 Closure 值可能被 PHP 代码观察,就继续生成真实 Closure: |
||||
|
||||
```php |
||||
return $callback; |
||||
$array[] = $callback; |
||||
$object->callback = $callback; |
||||
$other = $callback; |
||||
array_map($callback, $values); |
||||
is_callable($callback); |
||||
new ReflectionFunction($callback); |
||||
``` |
||||
|
||||
以下情况也回退: |
||||
|
||||
- 多次赋值、条件赋值、循环内创建或 `unset()`; |
||||
- 动态 `var-ref` 捕获、引用参数或递归闭包; |
||||
- 不同闭包在同一个变量中汇合; |
||||
- generator Closure; |
||||
- 命名参数、参数展开、variadic 或默认参数; |
||||
- 捕获 Native Object、强类型容器或其他需要专门 GC root 的值; |
||||
- 嵌套闭包以及任何分析器未明确识别的使用方式。 |
||||
|
||||
回退不是错误,也不应产生 warning。优化器只能在正向证明完整时启用。 |
||||
|
||||
## 5. 生成模型 |
||||
|
||||
原始代码: |
||||
|
||||
```php |
||||
$base = 10; |
||||
$callback = static function ($value) use ($base) { |
||||
return $base + $value; |
||||
}; |
||||
$result = $callback(5); |
||||
``` |
||||
|
||||
目标代码的结构为: |
||||
|
||||
```cpp |
||||
auto callback = [base = base](php::Var value) mutable -> php::Var { |
||||
return base + value; |
||||
}; |
||||
result = callback(5); |
||||
``` |
||||
|
||||
使用显式 init-capture 有两个原因: |
||||
|
||||
1. 捕获在闭包创建位置形成快照,而不是引用外层变量; |
||||
2. `mutable` 允许闭包内部修改自己的捕获副本,并让该状态在多次直接调用之间保留。 |
||||
|
||||
`php::Var` 的复制构造会解除普通 PHP reference,符合 `use ($value)` 的按值语义。 |
||||
固定 `int/string/float/bool/array` 的 C++ 值则直接复制。第一阶段不接受需要特殊 |
||||
生命周期处理的 Native Object 和 std container 捕获。 |
||||
|
||||
固定类型的引用捕获不需要先制造 Zend reference。因为分析器已经证明 lambda 不逃逸, |
||||
其生命周期完全包含在外层 C++ 函数帧内,所以: |
||||
|
||||
```php |
||||
$callback = function () use (&$number, &$text, &$items): void { |
||||
$number++; |
||||
$text .= '!'; |
||||
$items[] = $number; |
||||
}; |
||||
``` |
||||
|
||||
可直接生成: |
||||
|
||||
```cpp |
||||
auto callback = [&number, &text, &items]() mutable -> php::Var { |
||||
// ... |
||||
}; |
||||
``` |
||||
|
||||
闭包内部把这三个名字登记为对应的 `INT_REF/STR_REF/ARRAY_REF`,继续复用 TypePHP |
||||
现有 typed-ref 赋值检查。这样没有 `php::Ref`、`RefWrap`、`zend_reference` 或写回步骤。 |
||||
动态 `php::Var` 引用以及对象、resource、stream、Native Object、std container 等捕获 |
||||
仍回退到真实 Zend Closure 路径。 |
||||
|
||||
第一阶段继续使用 `php::Var` 作为闭包参数和返回 ABI,以复用现有闭包体生成逻辑并 |
||||
降低语义风险。后续可以在独立优化中,根据参数和返回声明把 ABI 收窄到 |
||||
`php::Int`、`php::Str` 等固定类型。 |
||||
|
||||
## 6. 参数求值顺序 |
||||
|
||||
PHP 固定为从左到右求值参数,而 C++17 不保证不同函数实参之间的求值顺序。直接调用 |
||||
不能简单输出: |
||||
|
||||
```cpp |
||||
callback(parse(a), parse(b)); |
||||
``` |
||||
|
||||
多参数调用必须沿用 TypePHP 的 ordered operand 机制,在调用前按源码顺序物化存在 |
||||
副作用的值,然后再调用 lambda。第一阶段即使参数表达式较简单,也不能依赖 C++ |
||||
编译器碰巧采用的顺序。 |
||||
|
||||
## 7. 分析与代码生成 |
||||
|
||||
新增局部 Closure 分析器,在函数 SSA 分析完成后、convert 生成语句前运行: |
||||
|
||||
1. 收集函数顶层 `$var = Closure/ArrowFunction` 候选; |
||||
2. 遍历当前函数 AST,分类该变量的每个定义和使用; |
||||
3. 任一使用不是直接调用目标即淘汰候选; |
||||
4. 检查闭包和调用参数是否位于第一阶段支持集合; |
||||
5. 把证明结果写入当前 `FunctionContext`; |
||||
6. 赋值生成器在原始语句位置输出 `auto` lambda; |
||||
7. 函数调用生成器识别该局部变量并输出直接调用; |
||||
8. 普通局部变量声明生成器跳过已经在源码位置声明的 lambda。 |
||||
|
||||
分析数据只属于当前函数的 `FunctionContext`,不得写入 persistent/request runtime |
||||
cache,也不得跨函数复用。 |
||||
|
||||
## 8. 安全不变量 |
||||
|
||||
实现必须始终满足: |
||||
|
||||
1. **无误优化**:不能证明时回退,绝不猜测; |
||||
2. **创建时捕获**:不能把 lambda 初始化提升到原 PHP 表达式之前; |
||||
3. **生命周期包含**:lambda 及全部调用点位于同一 C++ 函数帧; |
||||
4. **无 PHP 可见身份**:优化后的值不能进入 zval、数组、属性、参数或返回值; |
||||
5. **求值顺序一致**:实参和闭包体中的副作用顺序必须与 PHP 一致; |
||||
6. **异常边界一致**:PHP 异常仍通过现有 C++ `zend_object *` 路径传播; |
||||
7. **回退等价**:移除优化标记后,同一源码必须仍可由 Carrier 路径编译运行。 |
||||
|
||||
## 9. 测试计划 |
||||
|
||||
正向测试至少覆盖: |
||||
|
||||
- 无捕获箭头函数; |
||||
- 按值捕获及创建时快照; |
||||
- 捕获副本在多次调用之间保持内部修改; |
||||
- 固定 int/string/float/bool/array 的直接引用捕获; |
||||
- 普通匿名函数; |
||||
- 多次直接调用; |
||||
- 参数表达式的左到右求值。 |
||||
|
||||
回退测试至少覆盖: |
||||
|
||||
- 作为参数传给 `array_map()`; |
||||
- 从函数返回; |
||||
- 写入数组或属性; |
||||
- 赋给另一个变量; |
||||
- 动态 `var-ref` 引用捕获; |
||||
- 默认参数、variadic、unpack 和命名参数; |
||||
- generator、递归及嵌套 Closure; |
||||
- class method 中的 `$this`; |
||||
- `ReflectionFunction`、`is_callable()` 等身份观察。 |
||||
|
||||
生成代码测试需要同时断言:正向候选不包含 |
||||
`newClosureWithParameters`/`ClosureCarrier` 调用,而回退用例仍包含真实 Closure 创建。 |
||||
|
||||
## 10. 性能验证 |
||||
|
||||
`benchmark/dynamic-call/benchmark.php` 中的 `closure_monomorphic` 是第一阶段的主要性能 |
||||
用例。还应保留 `closure_alternating` 作为不能原生化的对照组。 |
||||
|
||||
验证指标包括: |
||||
|
||||
- Closure 创建次数和堆分配; |
||||
- 每次调用耗时; |
||||
- TypePHP/Zend PHP 比值; |
||||
- 生成 C++ 大小及编译时间; |
||||
- checksum 与 Zend PHP 一致。 |
||||
|
||||
性能提升不是放宽安全条件的理由。任何会暴露 Closure 对象或无法静态证明的场景, |
||||
即使处于热点,也继续使用 Closure Carrier。 |
||||
@ -0,0 +1,42 @@ |
||||
#!/bin/sh |
||||
|
||||
set -eu |
||||
|
||||
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) |
||||
source_icon="$project_dir/ios-assets/AppIcon.svg" |
||||
logo_source="$project_dir/../../swoole-logo.svg" |
||||
output_dir="$project_dir/ios-assets" |
||||
master_icon="$output_dir/AppIcon-1024.png" |
||||
temporary_dir=$(mktemp -d "${TMPDIR:-/tmp}/typephp-ios-icon.XXXXXX") |
||||
temporary_logo="$temporary_dir/swoole-logo.svg" |
||||
trap 'rm -rf "$temporary_dir"' EXIT HUP INT TERM |
||||
|
||||
if ! command -v ffmpeg >/dev/null 2>&1; then |
||||
echo "FFmpeg is required to render the SVG app icon." >&2 |
||||
exit 1 |
||||
fi |
||||
|
||||
# Override only the SVG viewport size. Its vector paths remain unchanged and |
||||
# are rasterized directly at the size used by the 1024px master artwork. |
||||
sed 's/width="100px" height="60px"/width="820px" height="282px"/' \ |
||||
"$logo_source" > "$temporary_logo" |
||||
|
||||
ffmpeg -hide_banner -loglevel error -y \ |
||||
-i "$source_icon" \ |
||||
-i "$temporary_logo" \ |
||||
-filter_complex '[0:v][1:v]overlay=102:371:format=auto,format=rgb24' \ |
||||
-frames:v 1 \ |
||||
"$master_icon" |
||||
|
||||
for size in 40 58 60 80 87 120 180; do |
||||
ffmpeg -hide_banner -loglevel error -y \ |
||||
-i "$master_icon" \ |
||||
-vf "scale=${size}:${size}:flags=lanczos" \ |
||||
-frames:v 1 -pix_fmt rgb24 \ |
||||
"$output_dir/AppIcon-${size}.png" |
||||
done |
||||
|
||||
cp "$output_dir/AppIcon-120.png" "$output_dir/AppIcon60x60@2x.png" |
||||
cp "$output_dir/AppIcon-180.png" "$output_dir/AppIcon60x60@3x.png" |
||||
|
||||
echo "Generated iOS icons in $output_dir" |
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 351 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
@ -0,0 +1,23 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP(AOT). |
||||
* |
||||
* @link https://www.swoole.com/aot/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
function local_native_closure_codegen(): void |
||||
{ |
||||
$base = 1; |
||||
$direct = static fn (int $value): int => $base + $value; |
||||
var_dump($direct(2)); |
||||
|
||||
$escaped = static fn (int $value): int => $value + 1; |
||||
array_map($escaped, [1]); |
||||
|
||||
$dynamic = std::any(1); |
||||
$dynamicRef = function () use (&$dynamic): void { |
||||
$dynamic++; |
||||
}; |
||||
$dynamicRef(); |
||||
} |
||||
@ -0,0 +1,41 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP(AOT). |
||||
* |
||||
* @link https://www.swoole.com/aot/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
use TypePhp\CompilerTest; |
||||
|
||||
/** |
||||
* @internal |
||||
* @coversNothing |
||||
*/ |
||||
final class LocalClosureCodegenTest extends BaseTest |
||||
{ |
||||
public function testOnlyProvenLocalClosuresUseConcreteCppLambdas(): void |
||||
{ |
||||
global $translator; |
||||
|
||||
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); |
||||
$translator = $compiler; |
||||
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/local-native-closure-codegen.php'; |
||||
$compiler->addFiles([$source]); |
||||
$compiler->prepareFile($source); |
||||
$generated = $compiler->convertFile($source); |
||||
$code = file_get_contents($generated); |
||||
|
||||
self::assertIsString($code); |
||||
self::assertStringContainsString( |
||||
'auto direct = [base = base](php::Var value) mutable -> php::Var {', |
||||
$code, |
||||
); |
||||
self::assertStringContainsString('direct(2L)', $code); |
||||
self::assertStringNotContainsString('typephp_call_cached(direct', $code); |
||||
|
||||
// Escaped values and dynamic references remain real Zend Closures. |
||||
self::assertSame(2, substr_count($code, 'php::newClosureWithParameters(')); |
||||
self::assertStringContainsString('typephp_call_cached(dynamicRef', $code); |
||||
} |
||||
} |
||||
@ -0,0 +1,222 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP(AOT). |
||||
* |
||||
* @link https://www.swoole.com/aot/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
namespace TypePhp\Analysis; |
||||
|
||||
use PhpParser\Node; |
||||
use PhpParser\Node\Expr; |
||||
use PhpParser\Node\FunctionLike; |
||||
use PhpParser\Node\Stmt; |
||||
|
||||
/** |
||||
* Proves the deliberately small set of local Closures which can stay entirely |
||||
* in the generated C++ function. An unknown use is an escape: this analysis |
||||
* must reject it and leave the ordinary Zend Closure lowering untouched. |
||||
*/ |
||||
final class LocalClosureAnalyzer |
||||
{ |
||||
/** @var array<string, array{assignment: Expr\Assign, closure: Expr\Closure|Expr\ArrowFunction, calls: int}> */ |
||||
private array $candidates = []; |
||||
|
||||
/** @var array<string, true> */ |
||||
private array $invalid = []; |
||||
|
||||
/** @var array<string, true> */ |
||||
private array $defined = []; |
||||
|
||||
/** |
||||
* @param list<Stmt> $statements |
||||
* @return array<string, array{assignment: Expr\Assign, closure: Expr\Closure|Expr\ArrowFunction, calls: int}> |
||||
*/ |
||||
public function analyze(array $statements): array |
||||
{ |
||||
$this->candidates = []; |
||||
$this->invalid = []; |
||||
$this->defined = []; |
||||
|
||||
$duplicateNames = []; |
||||
foreach ($statements as $statement) { |
||||
if (!$statement instanceof Stmt\Expression |
||||
|| !$statement->expr instanceof Expr\Assign |
||||
|| !$statement->expr->var instanceof Expr\Variable |
||||
|| !is_string($statement->expr->var->name) |
||||
|| (!$statement->expr->expr instanceof Expr\Closure |
||||
&& !$statement->expr->expr instanceof Expr\ArrowFunction) |
||||
) { |
||||
continue; |
||||
} |
||||
|
||||
$name = $statement->expr->var->name; |
||||
if (isset($this->candidates[$name])) { |
||||
$duplicateNames[$name] = true; |
||||
continue; |
||||
} |
||||
if (!$this->isSupportedClosure($statement->expr->expr)) { |
||||
continue; |
||||
} |
||||
$this->candidates[$name] = [ |
||||
'assignment' => $statement->expr, |
||||
'closure' => $statement->expr->expr, |
||||
'calls' => 0, |
||||
]; |
||||
} |
||||
|
||||
foreach ($duplicateNames as $name => $_) { |
||||
unset($this->candidates[$name]); |
||||
} |
||||
if ($this->candidates === []) { |
||||
return []; |
||||
} |
||||
|
||||
foreach ($statements as $statement) { |
||||
$this->scanNode($statement); |
||||
} |
||||
|
||||
foreach ($this->candidates as $name => $candidate) { |
||||
if (isset($this->invalid[$name]) || $candidate['calls'] === 0) { |
||||
unset($this->candidates[$name]); |
||||
} |
||||
} |
||||
return $this->candidates; |
||||
} |
||||
|
||||
private function isSupportedClosure(Expr\Closure|Expr\ArrowFunction $closure): bool |
||||
{ |
||||
if ($closure->byRef) { |
||||
return false; |
||||
} |
||||
if ($closure->returnType instanceof Node\Identifier |
||||
&& strtolower($closure->returnType->name) === 'never' |
||||
) { |
||||
return false; |
||||
} |
||||
foreach ($closure->params as $parameter) { |
||||
if ($parameter->byRef || $parameter->variadic || $parameter->default !== null) { |
||||
return false; |
||||
} |
||||
} |
||||
$body = $closure instanceof Expr\ArrowFunction ? $closure->expr : $closure->stmts; |
||||
return !$this->containsUnsupportedClosureNode($body, false); |
||||
} |
||||
|
||||
private function containsUnsupportedClosureNode(mixed $value, bool $root = true): bool |
||||
{ |
||||
foreach (is_array($value) ? $value : [$value] as $node) { |
||||
if (!$node instanceof Node) { |
||||
continue; |
||||
} |
||||
if (!$root && $node instanceof FunctionLike) { |
||||
return true; |
||||
} |
||||
if ($node instanceof Expr\Yield_ |
||||
|| $node instanceof Expr\YieldFrom |
||||
|| $node instanceof Stmt\Static_ |
||||
|| $node instanceof Stmt\Global_ |
||||
) { |
||||
return true; |
||||
} |
||||
if ($node instanceof Expr\FuncCall && $node->name instanceof Node\Name) { |
||||
$name = strtolower(ltrim($node->name->toString(), '\\')); |
||||
if (in_array($name, [ |
||||
'func_get_arg', |
||||
'func_get_args', |
||||
'func_num_args', |
||||
'debug_backtrace', |
||||
'debug_print_backtrace', |
||||
], true)) { |
||||
return true; |
||||
} |
||||
} |
||||
foreach ($node->getSubNodeNames() as $field) { |
||||
if ($this->containsUnsupportedClosureNode($node->{$field}, false)) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
private function scanNode( |
||||
mixed $value, |
||||
?Node $parent = null, |
||||
string $parentField = '', |
||||
int $functionDepth = 0, |
||||
): void { |
||||
foreach (is_array($value) ? $value : [$value] as $node) { |
||||
if (!$node instanceof Node) { |
||||
continue; |
||||
} |
||||
|
||||
// Textual order is not a dominance proof in the presence of goto: |
||||
// a jump may bypass the lambda initialization or re-enter its |
||||
// scope. Keep all such functions on the Zend Closure path. |
||||
if ($node instanceof Stmt\Goto_ || $node instanceof Stmt\Label) { |
||||
foreach ($this->candidates as $name => $_candidate) { |
||||
$this->invalid[$name] = true; |
||||
} |
||||
return; |
||||
} |
||||
|
||||
if ($node instanceof Expr\Variable && is_string($node->name)) { |
||||
$this->classifyVariableUse($node->name, $parent, $parentField, $functionDepth); |
||||
} |
||||
|
||||
$childFunctionDepth = $functionDepth + ($node instanceof FunctionLike ? 1 : 0); |
||||
foreach ($node->getSubNodeNames() as $field) { |
||||
$this->scanNode($node->{$field}, $node, $field, $childFunctionDepth); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private function classifyVariableUse( |
||||
string $name, |
||||
?Node $parent, |
||||
string $parentField, |
||||
int $functionDepth, |
||||
): void { |
||||
if (!isset($this->candidates[$name]) || isset($this->invalid[$name])) { |
||||
return; |
||||
} |
||||
|
||||
$candidate = $this->candidates[$name]; |
||||
if ($parent instanceof Expr\Assign && $parentField === 'var') { |
||||
if ($parent === $candidate['assignment'] && !isset($this->defined[$name])) { |
||||
$this->defined[$name] = true; |
||||
return; |
||||
} |
||||
$this->invalid[$name] = true; |
||||
return; |
||||
} |
||||
|
||||
if ($functionDepth !== 0 |
||||
|| !$parent instanceof Expr\FuncCall |
||||
|| $parentField !== 'name' |
||||
|| $parent->isFirstClassCallable() |
||||
|| !isset($this->defined[$name]) |
||||
|| !$this->isSupportedDirectCall($parent, count($candidate['closure']->params)) |
||||
) { |
||||
$this->invalid[$name] = true; |
||||
return; |
||||
} |
||||
|
||||
$this->candidates[$name]['calls']++; |
||||
} |
||||
|
||||
private function isSupportedDirectCall(Expr\FuncCall $call, int $parameterCount): bool |
||||
{ |
||||
if (count($call->args) !== $parameterCount) { |
||||
return false; |
||||
} |
||||
foreach ($call->args as $argument) { |
||||
if (!$argument instanceof Node\Arg || $argument->unpack || $argument->name !== null) { |
||||
return false; |
||||
} |
||||
} |
||||
return true; |
||||
} |
||||
} |
||||
@ -0,0 +1,68 @@ |
||||
--TEST-- |
||||
Non-escaping local Closures preserve captures, typed references, checks and argument order |
||||
--FILE-- |
||||
<?php |
||||
|
||||
function main(): void |
||||
{ |
||||
$base = 10; |
||||
$copy = function (int $value) use ($base): int { |
||||
$base++; |
||||
return $base + $value; |
||||
}; |
||||
$base = 100; |
||||
var_dump($copy(1), $copy(1), $base); |
||||
|
||||
$number = 1; |
||||
$text = 'a'; |
||||
$ratio = 1.5; |
||||
$flag = true; |
||||
$items = []; |
||||
$mutate = function () use (&$number, &$text, &$ratio, &$flag, &$items): void { |
||||
$number++; |
||||
$text .= '!'; |
||||
$ratio += 0.5; |
||||
$flag = false; |
||||
$items[] = $number; |
||||
}; |
||||
$mutate(); |
||||
var_dump($number, $text, $ratio, $flag, $items); |
||||
|
||||
$counter = 0; |
||||
$next = function () use (&$counter): int { |
||||
return $counter++; |
||||
}; |
||||
$pair = static fn (int $left, int $right): string => $left . ':' . $right; |
||||
var_dump($pair($next(), $next()), $counter); |
||||
|
||||
$typed = static fn (int $value): int => $value; |
||||
try { |
||||
$typed(std::any('bad')); |
||||
} catch (TypeError $error) { |
||||
echo "parameter type checked\n"; |
||||
} |
||||
|
||||
$badReturn = static fn (): int => std::any('bad'); |
||||
try { |
||||
$badReturn(); |
||||
} catch (TypeError $error) { |
||||
echo "return type checked\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(12) |
||||
int(13) |
||||
int(100) |
||||
int(2) |
||||
string(2) "a!" |
||||
float(2) |
||||
bool(false) |
||||
array(1) { |
||||
[0]=> |
||||
int(2) |
||||
} |
||||
string(3) "0:1" |
||||
int(2) |
||||
parameter type checked |
||||
return type checked |
||||