diff --git a/docs/COMPILE_TIME_FUNCTIONS.md b/docs/COMPILE_TIME_FUNCTIONS.md index fe67eac0..29fdf09a 100644 --- a/docs/COMPILE_TIME_FUNCTIONS.md +++ b/docs/COMPILE_TIME_FUNCTIONS.md @@ -4,19 +4,22 @@ ## 核心编译期函数 -当前核心全局编译期函数共 3 个。 +当前核心全局编译期函数共 5 个。 | 名称 | 参数 | 作用 | 当前主要处理位置 | | --- | --- | --- | --- | | `any($value)` | 1 个 | 将表达式降级为 `mixed/any`,阻止继续按静态 native/object 类型处理。 | 通用函数调用表达式入口。 | | `refval($target)` | 1 个 | 显式把变量、数组元素或对象属性作为引用传给动态调用或无法静态识别引用参数的调用。 | 参数解析、动态调用、SSA/优化器引用逃逸分析。 | | `objval($value, ClassName::class 或 'ClassName')` | 2 个 | 告诉编译器 `$value` 是指定类对象,并生成 `php::toObject(..., target_ce)` 运行时兜底检查。 | 函数调用解析、对象类型推导。 | +| `expected($condition)` | 1 个 | 标记条件通常为真,生成 Zend `EXPECTED(...)` 分支预测宏。 | 通用函数调用表达式入口。 | +| `unexpected($condition)` | 1 个 | 标记条件通常为假,生成 Zend `UNEXPECTED(...)` 分支预测宏。 | 通用函数调用表达式入口。 | 约束: - `refval()` 只接受变量、数组元素或对象属性。 - `objval()` 第二个参数必须是编译期可解析的类名字符串或 `ClassName::class`。 - `any()` 可在任意表达式位置使用,编译时直接展开其唯一参数,不生成运行时函数调用。 +- `expected()` / `unexpected()` 只接受一个非展开参数,返回 bool;通常用于 `if`、`elseif` 和循环条件,不改变参数的求值次数及真假语义。 ## 关键词方法 @@ -84,6 +87,7 @@ - `any()` 已统一在普通函数调用表达式入口处理;赋值、参数、返回值、数组元素和运算子表达式共用相同语义。 - `refval()` / `toRef()` 在参数解析和动态调用路径中特判较多,后续应统一为一个“引用包装表达式”解析入口。 - `objval()` 当前通过函数调用解析和类型推导路径识别,整体较集中。 +- `expected()` / `unexpected()` 在普通函数调用入口分别生成 `EXPECTED(...)` / `UNEXPECTED(...)`,不产生 PHP 运行时函数调用。 后续重构目标: diff --git a/docs/README.md b/docs/README.md index aec26807..2600bceb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,7 +9,7 @@ - [编译器命令行](COMPILER_CLI.md):当前 CLI 参数和项目配置。 - [编译模式](COMPILATION_MODES.md):binary、extension、library 模式。 - [快速入门](QUICKSTART.md):最小编译流程。 -- [编译期函数](COMPILE_TIME_FUNCTIONS.md):`any()`、`refval()`、`objval()` 和关键词方法。 +- [编译期函数](COMPILE_TIME_FUNCTIONS.md):`any()`、`refval()`、`objval()`、`expected()`、`unexpected()` 和关键词方法。 - [原生类型](NATIVE_TYPES.md)、[高精度类型](HIGH_PRECISION_TYPES.md)、[Std 容器](STD_CONTAINERS.md)。 - [通用与扩展方法](UNIVERSAL_METHODS.md)、[Generator](YIELD_GENERATOR.md)。 - [类继承](CLASS_INHERITANCE.md)、[混合 C++/PHP](MIXED_CPP_PHP.md)。 diff --git a/phpunit/code/branch-prediction.php b/phpunit/code/branch-prediction.php new file mode 100644 index 00000000..d78280c1 --- /dev/null +++ b/phpunit/code/branch-prediction.php @@ -0,0 +1,12 @@ +addFiles([$file]); + $compiler->prepareFile($file); + $cppFile = $compiler->convertFile($file); + $code = file_get_contents($cppFile); + + $this->assertStringContainsString('if (static_cast(EXPECTED((likely))))', $code); + $this->assertStringContainsString( + 'if (static_cast(UNEXPECTED((php::toBool(unlikely)))))', + $code, + ); + $this->assertStringNotContainsString('php_expected(', $code); + $this->assertStringNotContainsString('php_unexpected(', $code); + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 4b556df5..85ced177 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -2488,6 +2488,7 @@ class CompilerBase implements PropertyAccessContext case 'Expr_FuncCall': if ($this->isNameExpr($expr->name)) { $name = $this->parseIdentifier($expr->name); + $globalName = ltrim($name, '\\'); // Math function optimization: propagate Big* return types if (in_array($name, ['abs', 'pow', 'sqrt', 'floor', 'ceil', 'round'], true) && !empty($expr->args)) { $argType = $this->detectTypeOfExpr($expr->args[0]->value); @@ -2513,6 +2514,9 @@ class CompilerBase implements PropertyAccessContext if (in_array($name, self::STREAM_FUNCTIONS)) { return Type::STREAM; } + if ($globalName === 'expected' || $globalName === 'unexpected') { + return Type::BOOL; + } if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) { return Type::OBJECT; } diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index ebf7d5cd..cc4706f7 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -77,6 +77,7 @@ trait FunctionCallTrait $name = ''; } elseif ($expr->name->getType() === 'Name' or $expr->name->getType() === 'Name_FullyQualified') { $name = $this->parseIdentifier($expr->name); + $globalName = ltrim($name, '\\'); if (in_array($name, Constants::UNSUPPORTED_FUNCTIONS)) { $this->fatalError($expr, 'Unsupported function: `' . $name . '`'); } @@ -86,6 +87,13 @@ trait FunctionCallTrait } return $this->parseExprAsValue($expr->args[0]->value); } + if ($globalName === 'expected' || $globalName === 'unexpected') { + if (count($expr->args) !== 1 || $expr->args[0]->unpack) { + $this->fatalError($expr, "The {$globalName} function expects exactly one non-unpacked argument"); + } + $condition = $this->parseExprAsValue($expr->args[0]->value); + return 'static_cast(' . strtoupper($globalName) . '((' . $condition . ')))'; + } if ($name === 'objval') { return $this->genObjvalCall($expr); } diff --git a/src/polyfills.php b/src/polyfills.php index a1db4125..f6478689 100644 --- a/src/polyfills.php +++ b/src/polyfills.php @@ -103,6 +103,16 @@ function any(mixed $var): mixed return $var; } +function expected(mixed $condition): bool +{ + return (bool) $condition; +} + +function unexpected(mixed $condition): bool +{ + return (bool) $condition; +} + /** * @throws Exception */ diff --git a/tests/compiler/control_flow/branch-prediction.phpt b/tests/compiler/control_flow/branch-prediction.phpt new file mode 100644 index 00000000..b8489474 --- /dev/null +++ b/tests/compiler/control_flow/branch-prediction.phpt @@ -0,0 +1,39 @@ +--TEST-- +expected and unexpected provide branch prediction hints without changing condition semantics +--FILE-- + +--EXPECT-- +expected +unexpected-false +fully-qualified +bool(true) +bool(false) +int(3) diff --git a/version.txt b/version.txt index 75864bc5..669ca49f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1086 +1087