Added native iOS application development support

master
韩天峰 1 month ago
parent 72b7ce9bf4
commit 393ea1d369
  1. 231
      docs/zh-cn/LOCAL_CLOSURE_OPTIMIZATION.md
  2. 18
      examples/objective-c-macos/Info-iOS.plist
  3. 36
      examples/objective-c-macos/README.md
  4. 42
      examples/objective-c-macos/generate-ios-icons.sh
  5. BIN
      examples/objective-c-macos/ios-assets/AppIcon-1024.png
  6. BIN
      examples/objective-c-macos/ios-assets/AppIcon-120.png
  7. BIN
      examples/objective-c-macos/ios-assets/AppIcon-180.png
  8. BIN
      examples/objective-c-macos/ios-assets/AppIcon-40.png
  9. BIN
      examples/objective-c-macos/ios-assets/AppIcon-58.png
  10. BIN
      examples/objective-c-macos/ios-assets/AppIcon-60.png
  11. BIN
      examples/objective-c-macos/ios-assets/AppIcon-80.png
  12. BIN
      examples/objective-c-macos/ios-assets/AppIcon-87.png
  13. 7
      examples/objective-c-macos/ios-assets/AppIcon.svg
  14. BIN
      examples/objective-c-macos/ios-assets/AppIcon60x60@2x.png
  15. BIN
      examples/objective-c-macos/ios-assets/AppIcon60x60@3x.png
  16. 37
      examples/objective-c-macos/ios-src/uikit_bridge.mm
  17. 2
      examples/objective-c-macos/ios-src/uikit_bridge.stub.php
  18. 4
      examples/objective-c-macos/ios.yml
  19. 11
      examples/objective-c-macos/objc-src/macos_bridge.mm
  20. 2
      examples/objective-c-macos/objc-src/macos_bridge.stub.php
  21. 5
      examples/objective-c-macos/package-ios-app.sh
  22. 110
      examples/objective-c-macos/php-src/application.php
  23. 23
      phpunit/code/local-native-closure-codegen.php
  24. 33
      phpunit/src/Build/PrecompiledHeaderManagerTest.php
  25. 5
      phpunit/src/ClosureTest.php
  26. 20
      phpunit/src/FixedReferenceStorageTest.php
  27. 41
      phpunit/src/LocalClosureCodegenTest.php
  28. 15
      phpunit/src/VarIntTypesTest.php
  29. 222
      src/Analysis/LocalClosureAnalyzer.php
  30. 10
      src/Build/PrecompiledHeaderManager.php
  31. 3
      src/CompilerBase.php
  32. 14
      src/Context/FunctionContext.php
  33. 247
      src/Generator/ClosureGenerator.php
  34. 5
      src/Parser/AssignOpTrait.php
  35. 8
      src/Parser/FunctionCallTrait.php
  36. 5
      src/Translator.php
  37. 68
      tests/compiler/closure/local-native-closure.phpt

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

@ -12,14 +12,28 @@
<string>org.swoole.typephp.ios-hello</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleIcons</key>
<dict>
<key>CFBundlePrimaryIcon</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>AppIcon60x60</string>
</array>
</dict>
</dict>
<key>CFBundleIconFiles</key>
<array>
<string>AppIcon60x60</string>
</array>
<key>CFBundleName</key>
<string>TypePHP iOS Hello</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<string>0.2.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>2</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>

@ -66,13 +66,29 @@ sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
xcrun --sdk iphoneos --show-sdk-path
```
Install this SDK at `ios/iphoneos-arm64` inside the PHPX checkout, matching the
integrated layouts already used by `full-static/sdk` and
`wasm/wasm32-wasip2`. Build the executable from the TypePHP repository root;
`PHPX_HOME` selects both PHPX sources and the target SDK:
Build the SDK with swoole-cli. PHP source updates must go through
`sync-source-code.php`; this also validates and synchronizes the generated Zend
parser/scanner sources from the official php.net release archive:
```sh
export PHPX_HOME=/path/to/phpx
cd /path/to/swoole-cli
php sync-source-code.php --action run
php prepare.php @iphoneos-arm64 --with-parallel-jobs=8
./make.sh all-library
./make.sh config
./make.sh libphp
./make.sh phpx
./make.sh sdk
```
The installed SDK is under
`thirdparty/phpx/ios/iphoneos-arm64`. The `php-version` major in `ios.yml` must
match `sapi/PHP-VERSION.conf` used by swoole-cli. Build the executable from the
TypePHP repository root; `PHPX_HOME` selects both PHPX sources and the target
SDK:
```sh
export PHPX_HOME=/path/to/swoole-cli/thirdparty/phpx
php bin/tpc.php examples/objective-c-macos/ios.yml --no-progress
```
@ -83,9 +99,19 @@ the login keychain:
```sh
export TYPEPHP_IOS_PROVISIONING_PROFILE=/path/to/profile.mobileprovision
export TYPEPHP_IOS_CODE_SIGN_IDENTITY='Apple Development: Your Name (TEAMID)'
# Set this when the profile uses a different identifier than the example.
export TYPEPHP_IOS_BUNDLE_IDENTIFIER='your.provisioned.bundle.identifier'
sh examples/objective-c-macos/package-ios-app.sh
```
The package includes iPhone icon sizes generated from the repository's
`swoole-logo.svg`. To regenerate them after changing the logo, install FFmpeg
and run:
```sh
sh examples/objective-c-macos/generate-ios-icons.sh
```
Find the connected iPhone and install the bundle:
```sh

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
width="1024" height="1024" viewBox="0 0 1024 1024">
<!-- iOS applies the platform corner mask; the source artwork stays square. -->
<rect width="1024" height="1024" fill="#e8f7ff"/>
<circle cx="512" cy="512" r="410" fill="#ffffff" opacity="0.94"/>
</svg>

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

@ -117,6 +117,7 @@ php::Int php_ui_add_label(
? [UIFont systemFontOfSize:static_cast<CGFloat>(fontSize)
weight:UIFontWeightSemibold]
: [UIFont systemFontOfSize:static_cast<CGFloat>(fontSize)];
label.textColor = bold ? UIColor.labelColor : UIColor.secondaryLabelColor;
label.textAlignment = NSTextAlignmentCenter;
label.numberOfLines = 0;
label.adjustsFontSizeToFitWidth = YES;
@ -131,15 +132,31 @@ php::Int php_ui_add_button(
php::Int x,
php::Int y,
php::Int width,
php::Int height)
php::Int height,
php::Int style)
{
@autoreleasepool {
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = logical_rect(x, y, width, height);
[button setTitle:to_ns_string(title) forState:UIControlStateNormal];
button.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
button.backgroundColor = UIColor.secondarySystemBackgroundColor;
button.layer.cornerRadius = 10;
UIButtonConfiguration *configuration;
if (style == 1) {
configuration = [UIButtonConfiguration filledButtonConfiguration];
configuration.baseBackgroundColor = UIColor.systemBlueColor;
configuration.baseForegroundColor = UIColor.whiteColor;
} else if (style == 2) {
configuration = [UIButtonConfiguration tintedButtonConfiguration];
configuration.baseBackgroundColor = UIColor.systemBlueColor;
} else {
configuration = [UIButtonConfiguration grayButtonConfiguration];
}
configuration.title = to_ns_string(title);
configuration.cornerStyle = UIButtonConfigurationCornerStyleLarge;
configuration.contentInsets = NSDirectionalEdgeInsetsMake(12, 18, 12, 18);
button.configuration = configuration;
button.titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightSemibold];
button.titleLabel.adjustsFontSizeToFitWidth = YES;
button.titleLabel.minimumScaleFactor = 0.75;
button.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
[button addTarget:bridge
action:@selector(controlActivated:)
forControlEvents:UIControlEventTouchUpInside];
@ -156,7 +173,15 @@ void php_ui_set_control_text(php::Int controlId, php::Str text)
if ([control isKindOfClass:UILabel.class]) {
((UILabel *) control).text = value;
} else if ([control isKindOfClass:UIButton.class]) {
[(UIButton *) control setTitle:value forState:UIControlStateNormal];
UIButton *button = (UIButton *) control;
UIButtonConfiguration *configuration = button.configuration;
if (configuration != nil) {
configuration.title = value;
button.configuration = configuration;
} else {
[button setTitle:value forState:UIControlStateNormal];
}
button.accessibilityLabel = value;
}
}
}

@ -16,7 +16,7 @@ function ui_add_label(
bool $bold
): int {}
function ui_add_button(string $title, int $x, int $y, int $width, int $height): int {}
function ui_add_button(string $title, int $x, int $y, int $width, int $height, int $style): int {}
function ui_set_control_text(int $controlId, string $text): void {}

@ -2,7 +2,7 @@ name: typephp_ios_hello
version: 0.1.0
mode: bin
output: typephp_ios_hello
php-version: "8.5"
php-version: "8.4"
cxx-std: c++17
cpp-compiler: xcrun --sdk iphoneos clang++
target-platform: arm64-apple-ios15.0
@ -22,3 +22,5 @@ ld-flags:
- -Wl,-dead_strip
- -framework UIKit
- -framework Foundation
- -framework CoreGraphics
- -lresolv

@ -135,7 +135,8 @@ php::Int php_ui_add_button(
php::Int x,
php::Int y,
php::Int width,
php::Int height)
php::Int height,
php::Int style)
{
@autoreleasepool {
NSButton *button = [NSButton buttonWithTitle:to_ns_string(title)
@ -147,7 +148,13 @@ php::Int php_ui_add_button(
static_cast<CGFloat>(width),
static_cast<CGFloat>(height));
button.bezelStyle = NSBezelStyleRounded;
button.keyEquivalent = @"\r";
button.controlSize = NSControlSizeLarge;
button.font = [NSFont systemFontOfSize:14 weight:NSFontWeightSemibold];
if (style == 1) {
button.bezelColor = NSColor.controlAccentColor;
button.contentTintColor = NSColor.whiteColor;
button.keyEquivalent = @"\r";
}
button.autoresizingMask = NSViewMinXMargin | NSViewMaxXMargin | NSViewMinYMargin;
[bridge.window.contentView addSubview:button];
return static_cast<php::Int>(store_control(button));

@ -15,7 +15,7 @@ function ui_add_label(
bool $bold
): int {}
function ui_add_button(string $title, int $x, int $y, int $width, int $height): int {}
function ui_add_button(string $title, int $x, int $y, int $width, int $height, int $style): int {}
function ui_set_control_text(int $controlId, string $text): void {}

@ -7,6 +7,7 @@ executable="$project_dir/typephp_ios_hello"
bundle="$project_dir/dist/TypePHP iOS Hello.app"
profile=${TYPEPHP_IOS_PROVISIONING_PROFILE:-}
identity=${TYPEPHP_IOS_CODE_SIGN_IDENTITY:-}
bundle_identifier=${TYPEPHP_IOS_BUNDLE_IDENTIFIER:-}
if [ ! -x "$executable" ]; then
echo "Missing $executable; build ios.yml first." >&2
@ -33,8 +34,12 @@ plutil -extract Entitlements xml1 -o "$entitlements" "$profile_plist"
rm -rf "$bundle"
mkdir -p "$bundle"
cp "$project_dir/Info-iOS.plist" "$bundle/Info.plist"
if [ -n "$bundle_identifier" ]; then
plutil -replace CFBundleIdentifier -string "$bundle_identifier" "$bundle/Info.plist"
fi
cp "$executable" "$bundle/typephp_ios_hello"
cp "$profile" "$bundle/embedded.mobileprovision"
cp "$project_dir"/ios-assets/AppIcon*.png "$bundle/"
codesign \
--force \

@ -2,40 +2,106 @@
final class HelloApplication
{
private const BUTTON_PRIMARY = 1;
private const BUTTON_SECONDARY = 2;
private const BUTTON_TERTIARY = 3;
private static int $titleLabel = 0;
private static int $descriptionLabel = 0;
private static int $statusLabel = 0;
private static int $button = 0;
private static int $countButton = 0;
private static int $resetButton = 0;
private static int $languageButton = 0;
private static int $clickCount = 0;
private static bool $isChinese = false;
public static function build(string $platformApi): void
{
ui_create_window('TypePHP Native Hello', 640, 380);
ui_add_label('TypePHP + Objective-C++', 44, 292, 552, 40, 28, true);
ui_add_label(
"The interface and application state are implemented in TypePHP.\n" .
"Objective-C++ only provides thin wrappers around $platformApi.",
64,
178,
512,
88,
15,
false,
);
self::$statusLabel = ui_add_label('Waiting for a native UI event.', 64, 130, 512, 24, 13, false);
self::$button = ui_add_button('Send event to TypePHP', 210, 64, 220, 40);
if ($platformApi === 'UIKit') {
self::buildPhoneLayout();
} else {
self::buildDesktopLayout();
}
self::renderText();
}
public static function handleEvent(int $controlId): void
{
if ($controlId !== self::$button) {
if ($controlId === self::$languageButton) {
self::$isChinese = !self::$isChinese;
self::renderText();
return;
}
if ($controlId === self::$resetButton) {
self::$clickCount = 0;
self::renderStatus();
return;
}
if ($controlId !== self::$countButton) {
return;
}
self::$clickCount++;
$suffix = self::$clickCount === 1 ? 'time' : 'times';
ui_set_control_text(
self::$statusLabel,
'Button clicked ' . self::$clickCount . " $suffix; handled by TypePHP.",
);
self::renderStatus();
}
private static function buildPhoneLayout(): void
{
ui_create_window('TypePHP Native Hello', 390, 844);
self::$titleLabel = ui_add_label('', 24, 672, 342, 76, 30, true);
self::$descriptionLabel = ui_add_label('', 30, 544, 330, 104, 16, false);
self::$statusLabel = ui_add_label('', 30, 454, 330, 58, 18, true);
self::$countButton = ui_add_button('', 30, 354, 330, 58, self::BUTTON_PRIMARY);
self::$resetButton = ui_add_button('', 30, 280, 158, 52, self::BUTTON_SECONDARY);
self::$languageButton = ui_add_button('', 202, 280, 158, 52, self::BUTTON_TERTIARY);
}
private static function buildDesktopLayout(): void
{
ui_create_window('TypePHP Native Hello', 640, 420);
self::$titleLabel = ui_add_label('', 44, 336, 552, 40, 28, true);
self::$descriptionLabel = ui_add_label('', 64, 222, 512, 82, 15, false);
self::$statusLabel = ui_add_label('', 64, 164, 512, 30, 16, true);
self::$countButton = ui_add_button('', 160, 92, 320, 46, self::BUTTON_PRIMARY);
self::$resetButton = ui_add_button('', 160, 34, 150, 40, self::BUTTON_SECONDARY);
self::$languageButton = ui_add_button('', 330, 34, 150, 40, self::BUTTON_TERTIARY);
}
private static function renderText(): void
{
if (self::$isChinese) {
ui_set_control_text(self::$titleLabel, 'TypePHP 原生应用');
ui_set_control_text(
self::$descriptionLabel,
"界面逻辑和应用状态均由 TypePHP 实现。\nObjective-C++ 仅提供轻量的原生 UI 桥接。",
);
ui_set_control_text(self::$countButton, '点击计数');
ui_set_control_text(self::$resetButton, '重置');
ui_set_control_text(self::$languageButton, 'English');
} else {
ui_set_control_text(self::$titleLabel, 'TypePHP Native App');
ui_set_control_text(
self::$descriptionLabel,
"Application logic and state are implemented in TypePHP.\n" .
'Objective-C++ provides only a thin native UI bridge.',
);
ui_set_control_text(self::$countButton, 'Count with TypePHP');
ui_set_control_text(self::$resetButton, 'Reset');
ui_set_control_text(self::$languageButton, '中文');
}
self::renderStatus();
}
private static function renderStatus(): void
{
if (self::$isChinese) {
ui_set_control_text(self::$statusLabel, '已点击 ' . self::$clickCount . ' 次');
return;
}
$suffix = self::$clickCount === 1 ? 'click' : 'clicks';
ui_set_control_text(self::$statusLabel, self::$clickCount . " $suffix");
}
}

@ -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();
}

@ -62,6 +62,39 @@ final class PrecompiledHeaderManagerTest extends TestCase
$this->assertCount(8, $managedEntries);
}
public function testDependencyMtimeChangeInvalidatesCachedArtifact(): void
{
$dependencyDirectory = $this->cacheDirectory . '/dependencies';
mkdir($dependencyDirectory);
$dependency = $dependencyDirectory . '/runtime.h';
file_put_contents($dependency, "#pragma once\n");
$backend = $this->createMock(CompilerBackend::class);
$backend->method('supportsPrecompiledHeaders')->willReturn(true);
$backend->method('getName')->willReturn('test');
$backend->method('getCompilerCommand')->willReturn('true');
$backend->method('getPrecompiledHeaderArtifact')
->willReturnCallback(static fn(string $header): string => $header . '.gch');
$backend->method('buildNativeCompileCommand')
->willReturnCallback(
static fn(string $source, string $object): string => 'touch ' . escapeshellarg($object),
);
$manager = new PrecompiledHeaderManager($backend, new NativeBuilder($backend));
$options = new CompileOptions([]);
$first = $manager->prepare(['runtime.h'], [$dependencyDirectory], $this->cacheDirectory, $options);
$cached = $manager->prepare(['runtime.h'], [$dependencyDirectory], $this->cacheDirectory, $options);
touch($dependency, filemtime($dependency) + 10);
clearstatcache(true, $dependency);
$refreshed = $manager->prepare(['runtime.h'], [$dependencyDirectory], $this->cacheDirectory, $options);
$this->assertFalse($first['cached']);
$this->assertTrue($cached['cached']);
$this->assertFalse($refreshed['cached']);
$this->assertNotSame($first['artifact'], $refreshed['artifact']);
}
private function createCacheEntry(int $number, int $mtime): string
{
$directory = $this->cacheDirectory . '/' . sprintf('%024x', $number);

@ -17,8 +17,9 @@ class ClosureTest extends \BaseTest
$code = file_get_contents($generated);
self::assertIsString($code);
self::assertSame(3, substr_count($code, 'php::newClosureWithParameters('));
self::assertSame(3, substr_count($code, 'php::ClosureStrictTypes::Enabled'));
self::assertStringContainsString('auto copy = [arr = arr]() mutable -> php::Var {', $code);
self::assertSame(2, substr_count($code, 'php::newClosureWithParameters('));
self::assertSame(2, substr_count($code, 'php::ClosureStrictTypes::Enabled'));
}
public function testClosureRebindingIsRejectedAtCompileTime(): void

@ -16,14 +16,30 @@ final class FixedReferenceStorageTest extends BaseTest
public static function fixedStorageProvider(): iterable
{
yield 'string' => ['fixed-reference-string.php', 'php::Str'];
yield 'array' => ['fixed-reference-array.php', 'php::Array'];
yield 'object' => ['fixed-reference-generic-object.php', 'php::Object'];
yield 'typed object' => ['fixed-reference-object.php', 'php::Object'];
yield 'stream' => ['fixed-reference-stream.php', 'php::Stream'];
yield 'std container' => ['fixed-reference-std-container.php', 'php::StdVector'];
}
/** @dataProvider localClosureFixedReferenceProvider */
public function testNonEscapingLocalClosureUsesNativeReferenceCapture(
string $fixture,
string $capture,
): void
{
$code = $this->compileFixture($fixture);
self::assertStringContainsString($capture, $code);
self::assertStringNotContainsString('php::newClosureWithParameters(', $code);
}
public static function localClosureFixedReferenceProvider(): iterable
{
yield 'string' => ['fixed-reference-string.php', 'auto closure = [&value]() mutable -> php::Var {'];
yield 'array' => ['fixed-reference-array.php', 'auto closure = [&value]() mutable -> php::Var {'];
}
public function testFixedStorageUsesNativeReferenceForKnownTypedParameter(): void
{
$code = $this->compileFixture('fixed-reference-argument.php');

@ -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);
}
}

@ -71,17 +71,18 @@ final class VarIntTypesTest extends BaseTest
}
}
public function testNativeScalarCannotBePromotedForReferenceCapture(): void
public function testNativeScalarUsesDirectReferenceCaptureInNonEscapingClosure(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage(
'Cannot create a reference to variable $changed of fixed type php::Bool; initialize it with std::any()',
);
$this->compileSource(
$code = $this->compileSource(
$this->createCompiler(),
TYPEPHP_ROOT_PATH . '/phpunit/code/native-scalar-reference-capture.php',
);
self::assertStringContainsString(
'auto set = [&changed]() mutable -> php::Var {',
$code,
);
self::assertStringNotContainsString('php::newClosureWithParameters(', $code);
}
public function testExplicitAnySupportsReferenceCapture(): void

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

@ -149,7 +149,15 @@ final readonly class PrecompiledHeaderManager
}
sort($files, SORT_STRING);
foreach ($files as $file) {
hash_update($context, $file . "\0");
$metadata = stat($file);
if ($metadata === false) {
throw new \RuntimeException('Cannot stat precompiled-header dependency: ' . $file);
}
// Clang records dependency mtimes in a PCH and rejects the PCH even
// when a rewritten header still has identical contents. Include
// the metadata as well as the content so such SDK refreshes select
// a new cache entry before native compilation begins.
hash_update($context, $file . "\0" . $metadata['size'] . "\0" . $metadata['mtime'] . "\0");
if (!hash_update_file($context, $file)) {
throw new \RuntimeException('Cannot fingerprint precompiled-header dependency: ' . $file);
}

@ -5184,6 +5184,9 @@ class CompilerBase implements PropertyAccessContext
{
$code = '';
foreach ($localVars as $name => $type) {
if (isset($this->context->nativeLocalClosures[$name])) {
continue;
}
if (isset($this->context->arguments[$name])) {
continue;
}

@ -72,6 +72,16 @@ class FunctionContext
public array $explicitNativeTypeVars = [];
/** @var array<string, string> C++ initializers folded into function-scope local declarations. */
public array $localVarInitializers = [];
/**
* Proven non-escaping local Closure candidates. These are declaration-site
* plans only; the generator moves a successfully lowered entry into
* nativeLocalClosures when it emits the concrete C++ lambda.
*
* @var array<string, array{assignment: \PhpParser\Node\Expr\Assign, closure: \PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction, calls: int}>
*/
public array $localClosureCandidates = [];
/** @var array<string, true> Local variables already emitted as concrete C++ lambdas. */
public array $nativeLocalClosures = [];
/** @var array<string, string> typed-ref local => directly referenced local. */
public array $typedRefBindings = [];
/** @var array<string, string> typed-ref local => canonical fixed-storage root. */
@ -137,6 +147,8 @@ class FunctionContext
$this->localVars = [];
$this->explicitNativeTypeVars = [];
$this->localVarInitializers = [];
$this->localClosureCandidates = [];
$this->nativeLocalClosures = [];
$this->typedRefBindings = [];
$this->typedRefRoots = [];
$this->typedRefAliases = [];
@ -196,6 +208,8 @@ class FunctionContext
{
$this->localVars = $localVars;
$this->localVarInitializers = [];
$this->localClosureCandidates = [];
$this->nativeLocalClosures = [];
$this->typedRefBindings = [];
$this->typedRefRoots = [];
$this->typedRefAliases = [];

@ -53,6 +53,12 @@ trait ClosureGenerator
}
protected function parseArrowFunction(Expr\ArrowFunction $expr): string
{
return $this->genClosure($expr, $expr->params, $this->collectArrowFunctionUses($expr));
}
/** @return list<Node\ClosureUse> */
private function collectArrowFunctionUses(Expr\ArrowFunction $expr): array
{
$nodeFinder = new NodeFinder();
$vars = $nodeFinder->findInstanceOf($expr->expr, Variable::class);
@ -75,9 +81,7 @@ trait ClosureGenerator
}
$uses[$varName] = new Node\ClosureUse($var);
}
$uses = array_values($uses);
return $this->genClosure($expr, $expr->params, $uses);
return array_values($uses);
}
protected function parseClosure(Expr\Closure $expr): string
@ -85,6 +89,243 @@ trait ClosureGenerator
return $this->genClosure($expr, $expr->params, $expr->uses);
}
/**
* Lower a proven non-escaping local Closure at its PHP creation site.
* Returning null deliberately selects the ordinary Zend Closure path.
*/
protected function parseNativeLocalClosureAssignment(Expr\Assign $assign): ?string
{
if (!$this->isVarExpr($assign->var) || !is_string($assign->var->name)) {
return null;
}
$sourceName = $assign->var->name;
$name = $this->parseIdentifier($assign->var);
$candidate = $this->context->localClosureCandidates[$sourceName] ?? null;
if ($candidate === null || $candidate['assignment'] !== $assign) {
return null;
}
$expr = $candidate['closure'];
$uses = $expr instanceof Expr\ArrowFunction
? $this->collectArrowFunctionUses($expr)
: $expr->uses;
$capturePlan = $this->buildNativeLocalClosureCapturePlan($uses);
if ($capturePlan === null) {
return null;
}
// The local lambda no longer crosses a Zend boundary, but its declared
// PHP signature remains observable at each direct call.
foreach ($expr->params as $param) {
if (!$param->type instanceof Node\Name) {
$this->resolveTypeDecl($param->type, self::DECL_TYPE_OF_PARAM);
}
}
if (!$expr->returnType instanceof Node\Name) {
$this->resolveTypeDecl($expr->returnType, self::DECL_TYPE_OF_RETURN);
}
$entryContext = $this->context;
$entryIndent = $this->indentLevel;
$entryInGeneratorBody = $this->inGeneratorBody;
$parameters = [];
foreach ($expr->params as $param) {
$parameters[] = Type::VAR . ' ' . $this->parseIdentifier($param->var);
}
$code = 'auto ' . $name . ' = [' . implode(', ', $capturePlan['cpp']) . ']('
. implode(', ', $parameters) . ') mutable -> ' . Type::VAR . ' {' . PHP_EOL;
try {
$this->context = new FunctionContext();
$this->context->inClosure = true;
$this->inGeneratorBody = false;
$this->indentLevel = $entryIndent + 1;
$returnType = $expr->returnType;
$returnTypeName = $returnType instanceof Node\Identifier
? strtolower($returnType->name)
: '';
if ($returnType !== null && $returnTypeName !== 'void') {
$returnTypeInfo = $this->buildTypeCheckFromNode($returnType, true);
if (!empty($returnTypeInfo['check'])) {
$this->context->closureReturnTypeCheck = $returnTypeInfo['check'];
$this->context->closureReturnTypeStr = $returnTypeInfo['typeStr'];
}
}
$parameterChecks = '';
foreach ($expr->params as $index => $param) {
$paramName = $this->parseIdentifier($param->var);
$this->addArgument($paramName, Type::VAR);
if (CompileTimeAttribute::consume($param, 'Immutable')) {
$this->context->immutableVars[$paramName] = true;
if ($this->immutableTypeNodeMayBeObject($param->type)) {
$this->context->immutableObjectVars[$paramName] = true;
}
if ($param->type !== null) {
[, $parameterClass] = $this->resolveTypeDecl(
$param->type,
self::DECL_TYPE_OF_PARAM,
);
if ($parameterClass !== '') {
$this->addObject($paramName, $parameterClass);
}
}
}
$parameterChecks .= $this->genNativeLocalClosureParamTypeCheck($param, $paramName, $index);
}
foreach ($capturePlan['bindings'] as $binding) {
$this->addArgument($binding['name'], $binding['type']);
if ($binding['class'] !== '') {
$this->addObject($binding['name'], $binding['class']);
}
if ($binding['immutable']) {
$this->context->immutableVars[$binding['name']] = true;
if ($binding['immutableObject']) {
$this->context->immutableObjectVars[$binding['name']] = true;
}
}
}
$body = $this->genClosureBody($expr);
if ($this->context->needsUserCodeCallableScope) {
$body = $this->genUserCodeCallableScopeGuard() . $body;
}
$code .= $this->genScopeVarDecl() . $parameterChecks . $body;
if (!str_ends_with($code, PHP_EOL)) {
$code .= PHP_EOL;
}
$code .= $this->getIndent(0) . '}';
} finally {
$this->context = $entryContext;
$this->indentLevel = $entryIndent;
$this->inGeneratorBody = $entryInGeneratorBody;
}
$this->addLocalVar($name, Type::OBJECT);
$this->context->nativeLocalClosures[$name] = true;
return $code;
}
/**
* @param list<Node\ClosureUse> $uses
* @return array{
* cpp: list<string>,
* bindings: list<array{name: string, type: string, class: string, immutable: bool, immutableObject: bool}>
* }|null
*/
private function buildNativeLocalClosureCapturePlan(array $uses): ?array
{
$cpp = [];
$bindings = [];
foreach ($uses as $useItem) {
if (!$this->isVarExpr($useItem->var) || !is_string($useItem->var->name)) {
return null;
}
$name = $this->parseIdentifier($useItem->var);
if (!$this->hasLocalVar($name)
|| $this->isNativeObjectVar($name)
|| $this->isStdContainer($name)
) {
return null;
}
$rawType = $this->getRawVarType($name);
$valueType = Type::getReferencedType($rawType);
if ($useItem->byRef) {
$captureType = Type::getReferenceType($valueType);
if ($captureType === null) {
return null;
}
$cpp[] = '&' . $name;
} else {
if ($rawType === Type::REF || !in_array($valueType, [
Type::VAR,
Type::BOOL,
Type::INT,
Type::FLOAT,
Type::OBJECT,
Type::ARRAY,
Type::STR,
], true)) {
return null;
}
$captureType = $valueType;
$cpp[] = $name . ' = ' . $name;
}
$bindings[] = [
'name' => $name,
'type' => $captureType,
'class' => $valueType === Type::OBJECT ? $this->getDeclaredObjectType($name) : '',
'immutable' => isset($this->context->immutableVars[$name]),
'immutableObject' => isset($this->context->immutableObjectVars[$name]),
];
}
return ['cpp' => $cpp, 'bindings' => $bindings];
}
private function genNativeLocalClosureParamTypeCheck(Node\Param $param, string $var, int $index): string
{
if ($param->type === null) {
return '';
}
$typeInfo = $this->buildTypeCheckFromNode($param->type, true);
if (empty($typeInfo['check'])) {
return '';
}
$argInfo = new ArgInfo();
$argInfo->name = $var;
$argInfo->phpName = is_string($param->var->name)
? $param->var->name
: $this->unescapeVarName($var);
$argInfo->type = Type::VAR;
$argInfo->typeCheck = $typeInfo['check'];
$argInfo->typeStr = $typeInfo['typeStr'];
$argInfo->typeNode = $param->type;
return $this->genClosureParamCheck($argInfo, $index);
}
protected function parseNativeLocalClosureCall(Expr\FuncCall $expr, string $name): ?string
{
if (!isset($this->context->nativeLocalClosures[$name])) {
return null;
}
$arguments = [];
$forceMaterialize = count($expr->args) > 1;
foreach ($expr->args as $argument) {
$this->assertExprCanBeUsedAsValue($argument->value, 'function argument');
if ($this->isVarExpr($argument->value)) {
$this->assertStdContainerDoesNotEscapeNativeObjects(
$argument,
$this->parseIdentifier($argument->value),
);
}
$class = $this->detectClassOfExpr($argument->value);
if ($class !== '' && $this->isNativeObjectClass($class)) {
$this->fatalError(
$argument,
'Native objects cannot cross a local Closure php::Var parameter boundary',
);
}
if ($this->isVarExpr($argument->value)
&& $this->isStdContainer($this->parseIdentifier($argument->value))
) {
$value = $this->parseOrderedArg($argument);
} else {
$value = $this->parseOrderedOperand($argument->value, false, $forceMaterialize);
}
$arguments[] = $this->materializeCallArgValue($argument->value, $value);
}
return $name . '(' . implode(', ', $arguments) . ')';
}
protected function isReturnStmtInLastLine(array $stmts): bool
{
if (count($stmts) === 0) {

@ -172,6 +172,11 @@ trait AssignOpTrait
protected function parseAssign(Expr\Assign $v): string
{
$nativeClosure = $this->parseNativeLocalClosureAssignment($v);
if ($nativeClosure !== null) {
return $nativeClosure;
}
$left = $v->var;
$right = $v->expr;
if ($this->isAssignExpr($right)) {

@ -83,6 +83,14 @@ trait FunctionCallTrait
return $pythonObjectCall;
}
if ($this->isVarExpr($expr->name) && is_string($expr->name->name)) {
$localName = $this->parseIdentifier($expr->name);
$nativeClosureCall = $this->parseNativeLocalClosureCall($expr, $localName);
if ($nativeClosureCall !== null) {
return $nativeClosureCall;
}
}
$callableClass = $this->detectClassOfExpr($expr->name);
if ($this->isNativeObjectClass($callableClass)) {
if ($expr->isFirstClassCallable()) {

@ -11,6 +11,7 @@ namespace TypePhp;
use Ajaxray\AnsiKit\AnsiTerminal;
use Ajaxray\AnsiKit\Components\Progressbar;
use MJS\TopSort\Implementations\StringSort;
use TypePhp\Analysis\LocalClosureAnalyzer;
use TypePhp\Analysis\SsaBuilder;
use TypePhp\Backend\CompilerFactory;
use TypePhp\Build\CompileOptions;
@ -5088,6 +5089,10 @@ CODE;
}
}
if ($v->stmts && !$this->class && $this->methodDef === null) {
$this->context->localClosureCandidates = (new LocalClosureAnalyzer())->analyze($v->stmts);
}
$stmts = '';
$this->indentLevel++;
try {

@ -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
Loading…
Cancel
Save