feat(parser): add support for PHP 8.5 void cast syntax

- Implement parsing and validation for (void) cast expressions
- Add VoidCastValidationVisitor to enforce statement-only usage rules
- Allow (void) cast in discarded for-expression-list positions
- Generate appropriate diagnostic errors for invalid (void) cast contexts
- Update documentation to reflect void cast support and limitations
- Add comprehensive tests for void cast functionality and error cases
master
韩天峰 1 day ago
parent 6dcafb652b
commit 9fc5755544
  1. 3
      docs/INCOMPATIBLE_PHP_FEATURES.md
  2. 5
      phpunit/src/ConstantExpressionValidatorTest.php
  3. 73
      phpunit/src/NegativeCompatibilityTest.php
  4. 7
      src/CompilerBase.php
  5. 21
      src/Parser/LoopControlTrait.php
  6. 12
      src/Parser/UnaryExpressionTrait.php
  7. 4
      src/Preprocessor.php
  8. 3
      src/Transform/ConstantExpressionValidator.php
  9. 106
      src/Transform/VoidCastValidationVisitor.php
  10. 4
      src/Translator.php
  11. 42
      tests/compiler/basic/void-cast.phpt

@ -14,7 +14,8 @@
## 声明与类型
- 不支持可变变量 `$$var`
- 不支持 PHP 8.5 `#[NoDiscard]` 及用于显式丢弃返回值的 `(void)` 语法。
- 暂不支持 PHP 8.5 `#[NoDiscard]`
- 支持 PHP 8.5 `(void)` 显式丢弃语句;操作数仍会求值并保留副作用,不能在赋值、返回、参数或条件等值上下文中使用。
- PHP 8.4 property hooks 会编译为 AOT getter/setter,并注册对应的 Zend hook 元数据;直接属性读写、Reflection 和对象遍历均受支持。当前不支持对 hook 属性取引用。
- PHP 8.4 Reflection Lazy Object 不能用于 TypePHP AOT 类。AOT 类以 persistent internal class 注册,而 Zend 的 `zend_object_make_lazy()` 明确拒绝 internal class;运行时动态加载的 ZendPHP user class 不受此限制。
- 支持 `private(set)``protected(set)` 非对称属性可见性,并通过 PHP 8.4+ 的类级对象 handler 执行同等作用域检查。

@ -68,6 +68,11 @@ final class ConstantExpressionValidatorTest extends PHPUnit\Framework\TestCase
'8.5',
'Constant expression contains invalid operations',
];
yield 'PHP 8.5 void cast is a statement' => [
'(void) 1',
'8.5',
'Constant expression contains invalid operations',
];
yield 'first-class callable before PHP 8.5' => [
'strlen(...)',
'8.4',

@ -304,6 +304,79 @@ function main(): void
foreach ($rows as [&$left, &$right]) { // @diagnostic
}
}
PHP,
];
yield 'void cast result assignment' => [
'prepare',
'The (void) cast can only be used as a statement',
<<<'PHP'
<?php
function result(): int
{
return 1;
}
function main(): void
{
$value = (void) result(); // @diagnostic
}
PHP,
];
yield 'void cast return value' => [
'prepare',
'The (void) cast can only be used as a statement',
<<<'PHP'
<?php
function result(): int
{
return 1;
}
function invalid(): mixed
{
return (void) result(); // @diagnostic
}
PHP,
];
yield 'void cast argument value' => [
'prepare',
'The (void) cast can only be used as a statement',
<<<'PHP'
<?php
function consume(mixed $value): void
{
}
function main(): void
{
consume((void) 1); // @diagnostic
}
PHP,
];
yield 'void cast condition value' => [
'prepare',
'The (void) cast can only be used as a statement',
<<<'PHP'
<?php
function main(): void
{
if ((void) true) { // @diagnostic
}
}
PHP,
];
yield 'void cast final for condition' => [
'prepare',
'The (void) cast can only be used as a statement',
<<<'PHP'
<?php
function main(): void
{
for (; (void) true; ) { // @diagnostic
}
}
PHP,
];
}

@ -910,6 +910,8 @@ class CompilerBase implements PropertyAccessContext
return $this->parseCastArray($expr);
case 'Expr_Cast_Object':
return $this->parseCastObject($expr);
case 'Expr_Cast_Void':
return $this->parseCastVoid($expr);
case 'Expr_ConstFetch':
return $this->parseConstFetch($expr);
case 'Expr_UnaryMinus':
@ -1181,6 +1183,9 @@ class CompilerBase implements PropertyAccessContext
protected function parseExprAsValue(NodeAbstract $expr): string
{
if ($expr instanceof Expr\Cast\Void_) {
return $this->parseExpr($expr);
}
$value = $this->wrapVoidExprAsNull($expr, $this->parseExpr($expr));
return $this->normalizeNativeObjectValueExpr($expr, $value);
}
@ -2970,6 +2975,8 @@ class CompilerBase implements PropertyAccessContext
case 'Expr_Cast_Bool':
case 'Scalar_Bool':
return Type::BOOL;
case 'Expr_Cast_Void':
return Type::VOID;
case 'Expr_Array':
case 'Expr_Cast_Array':
return Type::ARRAY;

@ -7,9 +7,8 @@
namespace TypePhp\Parser;
use TypePhp\Type;
use PhpParser\Node;
use TypePhp\Type;
trait LoopControlTrait
{
@ -38,12 +37,17 @@ trait LoopControlTrait
$list_cond_expr = [];
$hasCondStmts = false;
foreach ($cond as $expr) {
$this->assertExprCanBeUsedAsCondition($expr, 'for condition');
$voidCast = $expr instanceof Node\Expr\Cast\Void_;
if (!$voidCast) {
$this->assertExprCanBeUsedAsCondition($expr, 'for condition');
}
[$condExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr);
$condExpr = $this->stringifyParsedExpr($condExpr);
$hasCondStmts = $hasCondStmts || $beforeStmts || $afterStmts;
$list_cond[] = [$expr, $condExpr, $beforeStmts, $afterStmts];
$list_cond_expr[] = $this->convertConditionExpr($expr, $condExpr);
$list_cond[] = [$expr, $condExpr, $beforeStmts, $afterStmts, $voidCast];
$list_cond_expr[] = $voidCast
? $condExpr
: $this->convertConditionExpr($expr, $condExpr);
}
$code .= $this->parseBeforeStmtLines() . PHP_EOL;
@ -55,8 +59,13 @@ trait LoopControlTrait
} else {
$condResult = $this->genTmpVarName();
$condCode .= $this->getIndent() . 'bool ' . $condResult . ' = true;' . PHP_EOL;
foreach ($list_cond as [$condNode, $condExpr, $beforeStmts, $afterStmts]) {
foreach ($list_cond as [$condNode, $condExpr, $beforeStmts, $afterStmts, $voidCast]) {
$condCode .= $this->formatCapturedStmtLines($beforeStmts);
if ($voidCast) {
$condCode .= $this->getIndent() . $condExpr . ';' . PHP_EOL;
$condCode .= $this->formatCapturedStmtLines($afterStmts);
continue;
}
if ($afterStmts) {
$tmpVar = $this->addTmpVar(Type::VAR);
$condCode .= $this->getIndent() . $tmpVar . ' = ' . $condExpr . ';' . PHP_EOL;

@ -8,12 +8,20 @@
namespace TypePhp\Parser;
use TypePhp\Type;
use PhpParser\Node\Expr;
use TypePhp\Transform\VoidCastValidationVisitor;
use TypePhp\Type;
trait UnaryExpressionTrait
{
protected function parseCastVoid(Expr\Cast\Void_ $node): string
{
if (!$node->getAttribute(VoidCastValidationVisitor::ALLOWED_ATTRIBUTE, false)) {
$this->fatalError($node, 'The (void) cast can only be used as a statement');
}
return 'static_cast<void>(' . $this->parseExpr($node->expr) . ')';
}
protected function parseBitwiseNot(Expr\BitwiseNot $expr): string
{
$pythonOperator = $this->parsePythonUnaryOperator($expr);

@ -30,6 +30,7 @@ use TypePhp\Transform\FunctionAttributeLowering;
use TypePhp\Transform\ConstantExpressionValidationVisitor;
use TypePhp\Transform\RuntimeAttributeFactoryLowering;
use TypePhp\Transform\Visitor;
use TypePhp\Transform\VoidCastValidationVisitor;
use TypePhp\NativeClass\NativeGlobalDiscovery;
use TypePhp\NativeClass\NativeGlobalTypeResolver;
use PhpParser\Modifiers;
@ -281,6 +282,9 @@ class Preprocessor extends CompilerBase
$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
$traverser->addVisitor(new VoidCastValidationVisitor(
fn (Node $node, string $message) => $this->fatalError($node, $message),
));
$traverser->addVisitor(new Visitor(
fn (Node $node, string $message) => $this->warning($node, $message),
$this->file,

@ -92,7 +92,8 @@ final class ConstantExpressionValidator
|| $expression instanceof Expr\PropertyFetch
|| $expression instanceof Expr\NullsafePropertyFetch
|| $expression instanceof Expr\New_
|| ($this->php85 && ($expression instanceof Expr\Cast
|| ($this->php85 && (($expression instanceof Expr\Cast
&& !$expression instanceof Expr\Cast\Void_)
|| $expression instanceof Expr\Closure
|| $expression instanceof Expr\FuncCall
|| $expression instanceof Expr\StaticCall));

@ -0,0 +1,106 @@
<?php
/**
* This file is part of Swoole-Compiler(AOT).
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\Transform;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt;
use PhpParser\NodeVisitorAbstract;
use TypePhp\Exception\SyntaxError;
/**
* Enforces PHP 8.5's statement-only grammar for the (void) cast.
*
* php-parser represents (void) as an Expr in every context for parser-version
* compatibility. PHP itself only accepts it as a statement, or in a discarded
* for-expression-list position. Marking accepted nodes also makes codegen
* reject any synthetic/unvalidated Void_ node defensively.
*/
final class VoidCastValidationVisitor extends NodeVisitorAbstract
{
public const string ALLOWED_ATTRIBUTE = 'typephpVoidCastAllowed';
private const string ERROR_MESSAGE = 'The (void) cast can only be used as a statement';
/** @var list<Node> */
private array $stack = [];
/** @param \Closure(Node, string): never|null $fatalError */
public function __construct(private readonly ?\Closure $fatalError = null)
{
}
public function beforeTraverse(array $nodes): null
{
$this->stack = [];
return null;
}
public function enterNode(Node $node): null
{
if ($node instanceof Expr\Cast\Void_) {
$parent = $this->stack === [] ? null : $this->stack[array_key_last($this->stack)];
if (!$this->isDiscardedPosition($node, $parent)) {
$this->fail($node);
}
$node->setAttribute(self::ALLOWED_ATTRIBUTE, true);
}
$this->stack[] = $node;
return null;
}
public function leaveNode(Node $node): null
{
array_pop($this->stack);
return null;
}
private function isDiscardedPosition(Expr\Cast\Void_ $node, ?Node $parent): bool
{
if ($parent instanceof Stmt\Expression) {
return $parent->expr === $node;
}
if (!$parent instanceof Stmt\For_) {
return false;
}
if ($this->containsIdenticalNode($parent->init, $node)
|| $this->containsIdenticalNode($parent->loop, $node)) {
return true;
}
$lastCondition = array_key_last($parent->cond);
foreach ($parent->cond as $index => $condition) {
if ($condition === $node) {
// Only the final condition controls the loop. Earlier items
// are evaluated solely for side effects and may be void casts.
return $index !== $lastCondition;
}
}
return false;
}
/** @param list<Expr> $nodes */
private function containsIdenticalNode(array $nodes, Node $needle): bool
{
foreach ($nodes as $node) {
if ($node === $needle) {
return true;
}
}
return false;
}
private function fail(Node $node): never
{
if ($this->fatalError !== null) {
($this->fatalError)($node, self::ERROR_MESSAGE);
}
throw new SyntaxError(self::ERROR_MESSAGE);
}
}

@ -47,6 +47,7 @@ use TypePhp\Transform\Visitor;
use TypePhp\Transform\ConstructorLowering;
use TypePhp\Transform\ConstantExpressionValidationVisitor;
use TypePhp\Transform\RuntimeAttributeFactoryLowering;
use TypePhp\Transform\VoidCastValidationVisitor;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\NodeAbstract;
@ -2685,6 +2686,9 @@ CODE;
$ast = $this->parser->parse($phpCode);
$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
$traverser->addVisitor(new VoidCastValidationVisitor(
fn (Node $node, string $message) => $this->fatalError($node, $message),
));
$traverser->addVisitor(new Visitor(sourceFile: $this->file));
$traverser->addVisitor(new ConstantExpressionValidationVisitor(
$this->phpVersion,

@ -0,0 +1,42 @@
--TEST--
PHP 8.5 void cast preserves operand side effects in every discarded position
--FILE--
<?php
function void_cast_mark(string $label, int $value): int
{
echo $label, "\n";
return $value;
}
function main(): void
{
$value = 0;
(void) void_cast_mark('statement', 1);
(void) ($value = void_cast_mark('assignment operand', 7));
(void) $value;
for (
(void) void_cast_mark('for init', 0), $i = 0;
(void) @void_cast_mark('for condition', 0), $i < 2;
(void) void_cast_mark('for loop', 0), $i++
) {
echo 'body:', $i, "\n";
}
var_dump($value, $i);
}
?>
--EXPECT--
statement
assignment operand
for init
for condition
body:0
for loop
for condition
body:1
for loop
for condition
int(7)
int(2)
Loading…
Cancel
Save