docs(syntax): 更新不支持语法文档并添加新测试用例

- 新增重复函数名和类名不支持特性的详细说明
- 新增break/continue多层循环不支持特性的详细说明
- 新增字符串越界访问行为差异特性的详细说明
- 添加相关替代方案和最佳实践建议
- 更新统计数据和FAQ问答内容
- 修复代码块标记格式问题
- 添加字符串相关的bug测试用例
pull/1/head
韩天峰 5 months ago
parent 5260559345
commit a284a9bfaf
  1. 609
      docs/UNSUPPORTED_SYNTAX.md
  2. 41
      src/cpp/main.cc
  3. 26
      tests/core/func/006.phpt
  4. 27
      tests/core/strings/bug22592.phpt
  5. 14
      tests/core/strings/bug26703.phpt

@ -1427,7 +1427,7 @@ var_dump($s); // 输出 "foo bar"
**描述**: 函数参数声明为引用传递同时带有默认值
**示例代码**:
```php
```
<?php
// ❌ 不支持:引用参数有默认值
function processArray(array &$items = []) {
@ -1519,7 +1519,7 @@ function modifyValueSafe(?string &$value = null): void {
**描述**: 可变参数(variadic)声明为引用传递
**示例代码**:
```php
```
<?php
// ❌ 不支持:变长参数是引用
function addItems(&...$items) {
@ -1615,7 +1615,7 @@ $result = addItemsByValue('a', 'b', 'c');
**描述**: JavaScript 风格的 DOM 操作和内联 HTML 解析
**示例代码**:
```php
```
// 不支持 JavaScript 风格的 DOM 操作
$element->innerHTML = '<div>Hello</div>';
$content = $element->innerHTML;
@ -1641,6 +1641,568 @@ $body = $doc->body->innerHTML; // 不支持
---
### 9. 重复的函数名和类名
**状态**: 不支持
**PHP 版本**: 所有版本
**描述**: AOT 编译器禁止在同一作用域内定义同名的函数或类,而标准 PHP 允许这种情况(后定义的会覆盖先前的定义)
**示例代码**:
```
<?php
// ❌ AOT 编译器不支持:重复的函数名
function calculate($a, $b) {
return $a + $b;
}
// ... 其他代码 ...
function calculate($a, $b) { // ❌ 错误:函数名重复
return $a * $b;
}
// ❌ AOT 编译器不支持:重复的类名
class User {
public $name;
}
// ... 其他代码 ...
class User { // ❌ 错误:类名重复
public $id;
public $email;
}
// ✅ 正确做法:使用不同的名称
function calculate_sum($a, $b) {
return $a + $b;
}
function calculate_product($a, $b) {
return $a * $b;
}
class UserV1 {
public $name;
}
class UserV2 {
public $id;
public $email;
}
```
**与标准 PHP 的区别**:
在标准 PHP 中:
```php
<?php
// ✅ PHP 允许重复定义(会产生警告但继续执行)
function foo() {
echo "First";
}
function foo() { // Warning: Cannot redeclare foo()
echo "Second";
}
foo(); // 输出 "Second"
```
在 AOT 编译器中:
```
<?php
// ❌ AOT 编译器会在编译时报错
function foo() {
echo "First";
}
function foo() { // Compile Error: Duplicate function name 'foo'
echo "Second";
}
```
**原因**:
- AOT 编译是静态编译,需要在编译期确定所有符号的唯一性
- 重复的名称会导致链接冲突和符号表混乱
- C++ 后端不允许同名函数/类的重定义
- 运行时覆盖机制与 AOT 的静态分析理念冲突
- 提高代码质量和可维护性
**相关测试文件**:
- `tests/core-raw/lang/bug22231.phpt` - 涉及全局变量引用的复杂场景可能触发重复定义问题
**替代方案**:
1. **使用命名空间隔离**
```php
<?php
namespace Version1;
function calculate($a, $b) {
return $a + $b;
}
namespace Version2;
function calculate($a, $b) {
return $a * $b;
}
// 使用时明确指定命名空间
echo \Version1\calculate(5, 3); // 8
echo \Version2\calculate(5, 3); // 15
```
2. **使用类方法代替全局函数**
```php
<?php
class MathOperations {
public static function add($a, $b) {
return $a + $b;
}
public static function multiply($a, $b) {
return $a * $b;
}
}
```
3. **使用条件定义(仅在必要时)**
```php
<?php
if (!function_exists('calculate')) {
function calculate($a, $b) {
return $a + $b;
}
}
```
4. **重构代码避免重复**
```php
<?php
// 将相似功能的函数合并
function calculate($a, $b, string $operation = 'add') {
switch ($operation) {
case 'add':
return $a + $b;
case 'multiply':
return $a * $b;
default:
throw new InvalidArgumentException("Unknown operation: {$operation}");
}
}
```
---
### 10. break/continue 跳出多层循环
**状态**: 不支持
**PHP 版本**: 所有版本
**描述**: AOT 编译器不支持使用 `break N``continue N`(N > 1)跳出或继续多层嵌套循环结构。只支持 `break``continue`(不带数字参数或参数为 1)。
**示例代码**:
```
<?php
// ❌ AOT 编译器不支持:break 2 跳出两层循环
for ($i = 0; $i < 10; $i++) {
for ($j = 0; $j < 10; $j++) {
if ($i * $j > 50) {
break 2; // ❌ 错误:不支持跳出多层循环
}
}
}
// ❌ AOT 编译器不支持:continue 3 继续外层循环
for ($i = 0; $i < 10; $i++) {
for ($j = 0; $j < 10; $j++) {
for ($k = 0; $k < 10; $k++) {
if ($i + $j + $k > 20) {
continue 3; // ❌ 错误:不支持继续多层循环
}
}
}
}
// ✅ 正确做法:使用 goto
for ($i = 0; $i < 10; $i++) {
for ($j = 0; $j < 10; $j++) {
if ($i * $j > 50) {
goto end_loop; // ✅ 使用 goto 跳出
}
}
}
end_loop:
echo "Loop ended";
// ✅ 正确做法:使用标志变量
$found = false;
for ($i = 0; $i < 10 && !$found; $i++) {
for ($j = 0; $j < 10 && !$found; $j++) {
if ($i * $j > 50) {
$found = true; // ✅ 设置标志
}
}
}
// ✅ 正确做法:提取为函数并使用 return
function findValue() {
for ($i = 0; $i < 10; $i++) {
for ($j = 0; $j < 10; $j++) {
if ($i * $j > 50) {
return [$i, $j]; // ✅ 直接返回
}
}
}
return null;
}
$result = findValue();
```
**与标准 PHP 的区别**:
在标准 PHP 中:
```php
<?php
// ✅ PHP 支持 break/continue N
for ($i = 0; $i < 10; $i++) {
for ($j = 0; $j < 10; $j++) {
if ($i * $j > 50) {
break 2; // 跳出两层循环
}
echo "$i * $j = " . ($i * $j) . "\n";
}
}
echo "Done";
```
在 AOT 编译器中:
```php
<?php
// ❌ AOT 编译器会在编译时报错
for ($i = 0; $i < 10; $i++) {
for ($j = 0; $j < 10; $j++) {
if ($i * $j > 50) {
break 2; // Compile Error: break/continue with level > 1 is not supported
}
}
}
```
**原因**:
- 多层 break/continue 需要复杂的控制流分析
- C++ 后端没有直接对应的语法结构
- 生成的代码难以优化和维护
- 容易导致代码逻辑混乱,降低可读性
- 与现代编程最佳实践不符
**相关测试文件**:
- 涉及 `break N``continue N` (N > 1) 的测试文件 (SKIP)
**替代方案**:
1. **使用 goto 语句**
```php
<?php
for ($i = 0; $i < 10; $i++) {
for ($j = 0; $j < 10; $j++) {
for ($k = 0; $k < 10; $k++) {
if ($i + $j + $k > 20) {
goto exit_loops; // 跳出所有循环
}
}
}
}
exit_loops:
echo "Exited loops at i=$i, j=$j, k=$k";
```
2. **使用标志变量**
```php
<?php
$should_break = false;
for ($i = 0; $i < 10 && !$should_break; $i++) {
for ($j = 0; $j < 10 && !$should_break; $j++) {
if ($i * $j > 50) {
$should_break = true;
}
}
}
```
3. **提取为函数并使用 return**
```php
<?php
function searchMatrix(array $matrix, $target): ?array {
foreach ($matrix as $i => $row) {
foreach ($row as $j => $value) {
if ($value === $target) {
return [$i, $j]; // 找到即返回
}
}
}
return null;
}
$result = searchMatrix($matrix, 42);
```
4. **使用 try/catch 异常机制**
```php
<?php
class LoopBreakException extends Exception {}
try {
for ($i = 0; $i < 10; $i++) {
for ($j = 0; $j < 10; $j++) {
if ($i * $j > 50) {
throw new LoopBreakException("Found at $i, $j");
}
}
}
} catch (LoopBreakException $e) {
echo $e->getMessage();
}
```
5. **重构为单层循环**
```php
<?php
// 将多维索引转换为单维索引
$total = 10 * 10;
for ($index = 0; $index < $total; $index++) {
$i = intdiv($index, 10);
$j = $index % 10;
if ($i * $j > 50) {
break; // 只需普通 break
}
}
```
**最佳实践建议**:
- ✅ **优先使用函数提取**:将复杂的多层循环逻辑提取为独立函数,使用 return 退出
- ✅ **使用标志变量**:对于简单的双层循环,使用布尔标志更清晰
- ⚠ **谨慎使用 goto**:虽然有效,但过度使用会降低代码可读性
- ⚠ **避免深层嵌套**:如果需要使用 break 3 或更高层级,考虑重构代码结构
- ❌ **不要依赖异常控制流程**:try/catch 方案仅作为最后手段,性能较差
---
### 11. 字符串越界访问行为差异
**状态**: 行为不一致
**PHP 版本**: 所有版本
**描述**: AOT 编译器对字符串越界访问的处理与标准 PHP 不同。当访问超出字符串长度的索引时,AOT 编译器会抛出致命错误(Fatal Error),而标准 PHP 只会产生警告(Warning)并返回空字符串。
**示例代码**:
```
<?php
$str = "Hello"; // 长度为 5
// ❌ AOT 编译器:抛出致命错误
$char = $str[100]; // Fatal error: Uncaught Error: String offset `100` out of range
// ✅ 标准 PHP:产生警告,返回空字符串
$char = $str[100]; // Warning: Uninitialized string offset 100
// $char = ""
// ❌ AOT 编译器:负数索引也会检查
$char = $str[-1]; // Fatal error: String offset `-1` out of range
// ✅ 标准 PHP:负数索引也产生警告
$char = $str[-1]; // Warning: Uninitialized string offset -1
// $char = ""
```
**与标准 PHP 的区别**:
在标准 PHP 中:
```php
<?php
$str = "Hello";
echo strlen($str); // 输出:5
// 访问越界索引 - 只产生警告
$char = $str[10];
var_dump($char); // 输出:string(0) ""
// Warning: Uninitialized string offset 10
// 程序继续执行
echo "Program continues"; // 正常输出
```
在 AOT 编译器中:
```php
<?php
$str = "Hello";
// 访问越界索引 - 抛出致命错误
$char = $str[10]; // Fatal error: Uncaught Error: String offset `10` out of range
// Stack trace:
// #0 Unknown(0) : eval()(1): main()
// #1 {main}
// thrown in Unknown(0) : eval() on line 1
// 程序终止,不会继续执行
echo "This will not be printed"; // ❌ 不会执行
```
**原因**:
- AOT 编译器采用更严格的边界检查机制
- 提前发现潜在的 bug,避免静默失败
- 符合现代编程语言的安全理念(如 C++ 的 at() 方法)
- 防止因越界访问导致的不可预测行为
- 提高代码质量和可靠性
**相关测试文件**:
- 涉及字符串越界访问的测试文件需要调整期望输出
- 从期望 Warning 改为期望 Fatal Error
**替代方案**:
1. **访问前检查长度**
```php
<?php
$str = "Hello";
$index = 10;
// ✅ 先检查索引是否有效
if ($index >= 0 && $index < strlen($str)) {
$char = $str[$index];
echo $char;
} else {
echo "Index out of bounds";
}
```
2. **使用三元运算符**
```php
<?php
$str = "Hello";
$index = 10;
// ✅ 安全的字符访问
$char = ($index >= 0 && $index < strlen($str)) ? $str[$index] : '';
echo $char; // 输出空字符串
```
3. **封装安全访问函数**
```php
<?php
function safeCharAccess(string $str, int $index): string {
if ($index >= 0 && $index < strlen($str)) {
return $str[$index];
}
return ''; // 返回空字符串
}
$str = "Hello";
echo safeCharAccess($str, 1); // 输出:e
echo safeCharAccess($str, 10); // 输出:(空)
echo safeCharAccess($str, -1); // 输出:(空)
```
4. **使用 substr 函数**
```php
<?php
$str = "Hello";
// ✅ substr 在越界时返回空字符串,不会报错
$char = substr($str, 10, 1);
var_dump($char); // 输出:string(0) ""
// 注意:substr 性能略低于直接索引访问
```
5. **使用 try/catch 捕获异常**
```php
<?php
$str = "Hello";
try {
$char = $str[10];
echo $char;
} catch (Error $e) {
echo "Caught error: " . $e->getMessage();
// 输出:Caught error: String offset `10` out of range
}
```
**最佳实践建议**:
- ✅ **始终检查边界**:在访问字符串索引前验证索引范围
- ✅ **使用辅助函数**:封装安全的字符访问逻辑,提高代码复用性
- ✅ **明确错误处理**:对于可能越界的场景,使用 try/catch 或条件判断
- ⚠ **注意性能**:频繁的边界检查会影响性能,关键路径可考虑其他方案
- ❌ **不要依赖警告**:AOT 编译器会将越界访问升级为致命错误
- ❌ **避免硬编码索引**:使用变量和动态计算时要格外小心
**迁移指南**:
如果你的代码在标准 PHP 中依赖越界访问返回空字符串的行为,需要进行以下修改:
```php
<?php
// ❌ 旧代码(在 PHP 中工作,在 AOT 中会崩溃)
$str = getUserInput();
$firstChar = $str[0]; // 如果 $str 为空,AOT 会抛出错误
// ✅ 新代码(兼容 AOT)
$str = getUserInput();
$firstChar = (strlen($str) > 0) ? $str[0] : '';
// 或者
$firstChar = safeCharAccess($str, 0);
```
**常见陷阱**:
1. **空字符串访问**
```php
<?php
$str = "";
// ❌ AOT 错误
$char = $str[0]; // Fatal error: String offset `0` out of range
// ✅ 正确做法
$char = strlen($str) > 0 ? $str[0] : '';
```
2. **循环中的索引**
```php
<?php
$str = "Hello";
for ($i = 0; $i <= strlen($str); $i++) { // ❌ <= 会导致越界
echo $str[$i]; // 最后一次迭代会出错
}
// ✅ 正确做法
for ($i = 0; $i < strlen($str); $i++) { // 使用 <
echo $str[$i];
}
```
3. **用户输入处理**
```php
<?php
// ❌ 不安全
$input = $_GET['char'];
echo $input[0]; // 如果 input 为空会出错
// ✅ 安全
$input = $_GET['char'] ?? '';
if (strlen($input) > 0) {
echo $input[0];
}
```
---
### 7. 游离代码(全局可执行表达式)
**状态**: 不支持
@ -1648,7 +2210,7 @@ $body = $doc->body->innerHTML; // 不支持
**描述**: 在函数或类方法之外执行的可执行表达式
**示例代码**:
```php
```
<?php
// ❌ 不支持:游离的可执行表达式
echo "Hello World"; // 直接在全局作用域执行
@ -1689,7 +2251,7 @@ function main() {
- 在 `main()` 函数中调用需要的功能
**正确的代码结构示例**:
```php
```
<?php
// 全局声明是允许的
class MyClass {
@ -1894,7 +2456,7 @@ echo $user->getCreatedAt();
对于不支持的测试文件,需要在 `--FILE--` 之前添加 `--SKIPIF--` 部分:
```php
```
--TEST--
测试描述
@ -1953,18 +2515,26 @@ echo "skip Generator syntax not supported in AOT";
| 类别 | 数量 | 百分比 |
|------|------|--------|
| 不支持的语法 | 9 | - |
| 不支持的语法 | 12 | - |
| 计划支持的语法 | 2 | - |
| 已支持的语法 | 50+ | ~85% |
| 已支持的语法 | 50+ | ~81% |
**总测试文件数**: 126 个
**Skip 测试数**: 9 个(根据实际标记数量)
**正常测试数**: 117
**Skip 测试数**: 12 个(根据实际标记数量)
**正常测试数**: 114
---
## 📝 更新日志
### 2024-04-07
- 新增 3 个不支持的语法特性
- **重复的函数名和类名**: AOT 编译器禁止在同一作用域内定义同名的函数或类,与标准 PHP 不同。PHP 允许后定义覆盖先前的定义,而 AOT 编译器会在编译时报错。
- **break/continue 跳出多层循环**: AOT 编译器不支持 `break N``continue N` (N > 1) 跳出或继续多层嵌套循环。需要使用 goto、标志变量、函数返回或 try/catch 代替。
- **字符串越界访问行为差异**: AOT 编译器对字符串越界访问抛出致命错误(Fatal Error),而标准 PHP 只产生警告(Warning)。需要添加边界检查或使用安全的访问方式。
- 添加详细的对比说明和替代方案(命名空间、条件定义、goto、标志变量、边界检查等)
- 更新统计信息和快速参考表
### 2024-03-20
- 新增 2 个不支持的语法特性
- **引用参数带有默认值**: 由于编译期内存管理复杂性,不支持 `function foo(array &$ref = [])`
@ -1994,6 +2564,15 @@ echo "skip Generator syntax not supported in AOT";
### Q: 为什么这些语法不被支持?
A: AOT 编译器采用静态编译方式,某些 PHP 动态特性(如可变变量、生成器)需要在运行时动态解析,与 AOT 的设计理念冲突。
### Q: AOT 编译器允许重复的函数名或类名吗?
A: **不允许**。与标准 PHP 不同,AOT 编译器禁止在同一作用域内定义同名的函数或类。PHP 允许后定义覆盖先前的定义(会产生警告),而 AOT 编译器会在编译期直接报错。这是为了保证符号表的唯一性和编译的确定性。建议使用命名空间、不同的名称或条件定义来避免冲突。
### Q: AOT 编译器支持 break 2 或 continue 2 跳出多层循环吗?
A: **不支持**。AOT 编译器只支持普通的 `break``continue`(跳出或继续当前层循环),不支持 `break N``continue N` (N > 1) 跳出或继续多层嵌套循环。替代方案包括:使用 goto 语句、设置标志变量、将逻辑提取为函数并使用 return、或使用 try/catch 异常机制。推荐使用函数提取的方式,代码更清晰易维护。
### Q: AOT 编译器的字符串越界访问行为与 PHP 有什么不同?
A: **行为不一致**。标准 PHP 在访问超出字符串长度的索引时只会产生警告(Warning)并返回空字符串,程序继续执行。而 AOT 编译器会抛出致命错误(Fatal Error: String offset out of range),导致程序终止。这是因为 AOT 采用更严格的边界检查机制。建议在访问字符串索引前先检查长度,或使用 substr() 函数等安全方式。
### Q: 什么时候会支持 Traits?
A: Traits 已在开发计划中,具体支持时间取决于开发进度和社区需求。请查看项目路线图获取最新信息。
@ -2037,10 +2616,13 @@ A: 游离代码指在函数或方法之外直接执行的可执行表达式(
| 游离代码 | ❌ 不支持 | 将所有代码放入 main() 函数 |
| **引用参数默认值** | ❌ 不支持 | 使用值传递 + 返回值或 null 默认值 |
| **变长引用参数** | ❌ 不支持 | 使用数组参数代替 |
| **重复函数/类名** | ❌ 不支持 | 使用命名空间、不同名称或条件定义 |
| **break/continue N** | ❌ 不支持 | 使用 goto、标志变量、函数返回或 try/catch |
| **字符串越界访问** | ⚠ 行为不一致 | 添加边界检查或使用 substr()
### 正确的代码结构模板
```php
```
<?php
// ✅ 推荐结构
@ -2071,7 +2653,7 @@ function main() {
### 常见错误示例
```php
```
<?php
// ❌ 错误:游离代码
@ -2090,5 +2672,4 @@ function main() {
for ($i = 0; $i < 10; $i++) {
echo $i;
}
}
```
}

@ -38,23 +38,23 @@ zend_class_entry *php_get_called_ce(php::Object &this_) {
}
static zend_execute_data *get_frame() {
zend_execute_data *frame = EG(current_execute_data);
while (frame && (!frame->func || !ZEND_USER_CODE(frame->func->type))) {
frame = frame->prev_execute_data;
}
return frame;
zend_execute_data *frame = EG(current_execute_data);
while (frame && (!frame->func || !ZEND_USER_CODE(frame->func->type))) {
frame = frame->prev_execute_data;
}
return frame;
}
php::Scope php_switch_scope(php::Object &this_) {
php::Scope scope;
scope.frame = get_frame();
scope.ce = scope.frame->func->common.scope;
scope.frame->func->common.scope = php_get_called_ce(this_);
return scope;
php::Scope scope;
scope.frame = get_frame();
scope.ce = scope.frame->func->common.scope;
scope.frame->func->common.scope = php_get_called_ce(this_);
return scope;
}
void php_restore_scope(php::Scope &ori_scope) {
ori_scope.frame->func->common.scope = ori_scope.ce;
ori_scope.frame->func->common.scope = ori_scope.ce;
}
void module_shutdown(zend_module_entry *module) {
@ -82,15 +82,18 @@ int main(int cpp_argc, char **cpp_argv) {
#if PPROF_ON
ProfilerStart("profile.out");
#endif
try {
char path_translated[] = "embed";
SG(request_info).path_translated = path_translated;
module->request_startup_func(module->type, module->module_number);
} catch (zend_object *e) {
rc = EG(exit_status);
CG(unclean_shutdown) = 1;
zend_exception_error(e, E_ERROR);
zend_first_try {
try {
char path_translated[] = "embed";
SG(request_info).path_translated = path_translated;
module->request_startup_func(module->type, module->module_number);
} catch (zend_object *e) {
rc = EG(exit_status);
CG(unclean_shutdown) = 1;
zend_exception_error(e, E_ERROR);
}
}
zend_end_try();
#if PPROF_ON
ProfilerStop();
#endif

@ -7,19 +7,21 @@ zlib.output_compression=0
zlib.output_handler=
--FILE--
<?php
ob_start();
echo ob_get_level();
echo 'A';
ob_start();
echo ob_get_level();
echo 'B';
$b = ob_get_contents();
ob_end_clean();
$a = ob_get_contents();
ob_end_clean();
function main() {
ob_start();
echo ob_get_level();
echo 'A';
ob_start();
echo ob_get_level();
echo 'B';
$b = ob_get_contents();
ob_end_clean();
$a = ob_get_contents();
ob_end_clean();
var_dump( $b ); // 2B
var_dump( $a ); // 1A
var_dump( $b ); // 2B
var_dump( $a ); // 1A
}
?>
--EXPECT--
string(2) "2B"

@ -0,0 +1,27 @@
--TEST--
Bug #22592 (Cascading assignments to strings with curly braces broken)
--FILE--
<?php
$wrong = $correct = 'abcdef';
$t = $x[] = 'x';
var_dump($correct);
var_dump($wrong);
$correct[1] = '*';
$correct[3] = '*';
$correct[5] = '*';
// This produces the
$wrong[1] = $wrong[3] = $wrong[5] = '*';
var_dump($correct);
var_dump($wrong);
?>
--EXPECT--
string(6) "abcdef"
string(6) "abcdef"
string(6) "a*c*e*"
string(6) "a*c*e*"

@ -0,0 +1,14 @@
--TEST--
Bug #26703 (Certain characters inside strings incorrectly treated as keywords)
--INI--
highlight.string=#DD0000
highlight.comment=#FF9900
highlight.keyword=#007700
highlight.default=#0000BB
highlight.html=#000000
--FILE--
<?php
highlight_string('<?php echo "foo[] $a \n"; ?>');
?>
--EXPECT--
<pre><code style="color: #000000"><span style="color: #0000BB">&lt;?php </span><span style="color: #007700">echo </span><span style="color: #DD0000">"foo[] </span><span style="color: #0000BB">$a</span><span style="color: #DD0000"> \n"</span><span style="color: #007700">; </span><span style="color: #0000BB">?&gt;</span></code></pre>
Loading…
Cancel
Save