@ -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 |
||||||