Skip to content

Commit fcd0158

Browse files
authored
Merge pull request #2538 from zephir-lang/#1849-yield-recognition
#1849 - Add yield support: implement generator detection and AST handling
2 parents 691333c + bb3278e commit fcd0158

13 files changed

Lines changed: 690 additions & 0 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org).
66

77
## [Unreleased]
88

9+
### Added
10+
- Compiler now recognizes the parser-emitted `yield` AST node (bare `yield;`, `yield expr;`, `yield key, value;`) [#1849](https://github.com/zephir-lang/zephir/issues/1849)
11+
912
### Documentation
1013
- Documented the workaround for `[ClassName, "protectedOrPrivateMethod"]` arrays passed as callbacks to PHP higher-order functions (`array_reduce`, `usort`, `preg_replace_callback`, etc.) [#2167](https://github.com/zephir-lang/zephir/issues/2167)
1114

src/Class/Method/Method.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Zephir\Code\Printer;
2323
use Zephir\CompilationContext;
2424
use Zephir\Detectors\WriteDetector;
25+
use Zephir\Detectors\YieldDetector;
2526
use Zephir\Documentation\Docblock;
2627
use Zephir\Documentation\DocblockParser;
2728
use Zephir\Exception;
@@ -130,6 +131,11 @@ class Method
130131
*/
131132
protected bool $void = false;
132133

134+
/**
135+
* Cached generator-detection result. Populated lazily by isGenerator().
136+
*/
137+
protected ?bool $isGenerator = null;
138+
133139
public function __construct(
134140
protected ?Definition $classDefinition = null,
135141
protected array $visibility = [],
@@ -2250,6 +2256,32 @@ public function isFinal(): bool
22502256
return $this->isFinal;
22512257
}
22522258

2259+
/**
2260+
* Checks whether the method body contains a `yield` statement, which
2261+
* means the method is a PHP generator. Result is cached; the underlying
2262+
* AST walk runs at most once per method instance. Returns false when the
2263+
* method has no statements block (abstract/external methods).
2264+
*
2265+
* Code-generation of generator bodies is not yet implemented;
2266+
* The API is exposed now so passes and future codegen can branch on it cleanly.
2267+
*
2268+
* @see https://github.com/zephir-lang/zephir/issues/1849
2269+
*/
2270+
public function isGenerator(): bool
2271+
{
2272+
if ($this->isGenerator !== null) {
2273+
return $this->isGenerator;
2274+
}
2275+
2276+
if (!$this->statements instanceof StatementsBlock) {
2277+
return $this->isGenerator = false;
2278+
}
2279+
2280+
return $this->isGenerator = (new YieldDetector())->detect(
2281+
$this->statements->getStatements()
2282+
);
2283+
}
2284+
22532285
/**
22542286
* Checks whether the method is an initializer.
22552287
*/

src/Detectors/WriteDetector.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,18 @@ public function passStatementBlock(array $statements): void
342342
}
343343
break;
344344

345+
case 'yield':
346+
if (isset($statement['expr'])) {
347+
$this->passExpression($statement['expr']);
348+
}
349+
if (isset($statement['key'])) {
350+
$this->passExpression($statement['key']);
351+
}
352+
if (isset($statement['value'])) {
353+
$this->passExpression($statement['value']);
354+
}
355+
break;
356+
345357
case 'loop':
346358
if (isset($statement['statements'])) {
347359
$this->passStatementBlock($statement['statements']);

src/Detectors/YieldDetector.php

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Zephir.
5+
*
6+
* (c) Phalcon Team <team@zephir-lang.com>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Zephir\Detectors;
15+
16+
/**
17+
* Walks a statements tree to determine whether a method body contains any
18+
* `yield` statement. A method that contains a yield must compile to a PHP
19+
* generator at the engine level; Zephir tracks the answer here so callers
20+
* (`Method::isGenerator()`, future codegen) can dispatch on it before the
21+
* statement-level handler ever runs.
22+
*
23+
* See https://github.com/zephir-lang/zephir/issues/1849 for the broader
24+
* yield-support roadmap. The detector is intentionally narrow: it answers
25+
* a yes/no question against the AST, nothing more.
26+
*/
27+
final class YieldDetector
28+
{
29+
private bool $found = false;
30+
31+
/**
32+
* Returns true if any yield statement appears anywhere inside the given
33+
* statements tree (including nested blocks: if/else, loops, switch, try).
34+
* Yield inside a nested closure does NOT count: a closure body is a
35+
* separate function whose generator-ness is its own concern.
36+
*
37+
* @param array<int, array<string, mixed>> $statements
38+
*/
39+
public function detect(array $statements): bool
40+
{
41+
$this->found = false;
42+
$this->walk($statements);
43+
44+
return $this->found;
45+
}
46+
47+
/**
48+
* @param array<int, array<string, mixed>> $statements
49+
*/
50+
private function walk(array $statements): void
51+
{
52+
foreach ($statements as $statement) {
53+
if ($this->found) {
54+
return;
55+
}
56+
57+
$type = $statement['type'] ?? null;
58+
59+
if ($type === 'yield') {
60+
$this->found = true;
61+
62+
return;
63+
}
64+
65+
if (isset($statement['statements']) && is_array($statement['statements'])) {
66+
$this->walk($statement['statements']);
67+
}
68+
69+
if (isset($statement['else_statements']) && is_array($statement['else_statements'])) {
70+
$this->walk($statement['else_statements']);
71+
}
72+
73+
if (isset($statement['elseif_statements']) && is_array($statement['elseif_statements'])) {
74+
foreach ($statement['elseif_statements'] as $clause) {
75+
if (isset($clause['statements']) && is_array($clause['statements'])) {
76+
$this->walk($clause['statements']);
77+
}
78+
}
79+
}
80+
81+
if (isset($statement['clauses']) && is_array($statement['clauses'])) {
82+
foreach ($statement['clauses'] as $clause) {
83+
if (isset($clause['statements']) && is_array($clause['statements'])) {
84+
$this->walk($clause['statements']);
85+
}
86+
}
87+
}
88+
89+
if (isset($statement['catches']) && is_array($statement['catches'])) {
90+
foreach ($statement['catches'] as $catch) {
91+
if (isset($catch['statements']) && is_array($catch['statements'])) {
92+
$this->walk($catch['statements']);
93+
}
94+
}
95+
}
96+
}
97+
}
98+
}

src/Passes/CallGathererPass.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,18 @@ public function passStatementBlock(array $statements): void
347347
}
348348
break;
349349

350+
case 'yield':
351+
if (isset($statement['expr'])) {
352+
$this->passExpression($statement['expr']);
353+
}
354+
if (isset($statement['key'])) {
355+
$this->passExpression($statement['key']);
356+
}
357+
if (isset($statement['value'])) {
358+
$this->passExpression($statement['value']);
359+
}
360+
break;
361+
350362
case 'try-catch':
351363
case 'loop':
352364
if (isset($statement['statements'])) {

src/Passes/LocalContextPass.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,18 @@ public function passStatementBlock(array $statements): void
509509
$this->lastCallLine = $statement['line'];
510510
break;
511511

512+
case 'yield':
513+
if (isset($statement['expr'])) {
514+
$this->passExpression($statement['expr']);
515+
}
516+
if (isset($statement['key'])) {
517+
$this->passExpression($statement['key']);
518+
}
519+
if (isset($statement['value'])) {
520+
$this->passExpression($statement['value']);
521+
}
522+
break;
523+
512524
case 'fetch':
513525
$this->passExpression($statement['expr']);
514526
break;

src/Passes/MutateGathererPass.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,18 @@ public function passStatementBlock(array $statements): void
336336
}
337337
break;
338338

339+
case 'yield':
340+
if (isset($statement['expr'])) {
341+
$this->passExpression($statement['expr']);
342+
}
343+
if (isset($statement['key'])) {
344+
$this->passExpression($statement['key']);
345+
}
346+
if (isset($statement['value'])) {
347+
$this->passExpression($statement['value']);
348+
}
349+
break;
350+
339351
case 'loop':
340352
if (isset($statement['statements'])) {
341353
$this->passStatementBlock($statement['statements']);

src/Passes/StaticTypeInference.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,18 @@ public function passStatementBlock(array $statements): void
544544
}
545545
break;
546546

547+
case 'yield':
548+
if (isset($statement['expr'])) {
549+
$this->passExpression($statement['expr']);
550+
}
551+
if (isset($statement['key'])) {
552+
$this->passExpression($statement['key']);
553+
}
554+
if (isset($statement['value'])) {
555+
$this->passExpression($statement['value']);
556+
}
557+
break;
558+
547559
case 'try-catch':
548560
case 'loop':
549561
if (isset($statement['statements'])) {

src/Statements/StatementFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ final class StatementFactory
4242
'unset' => UnsetStatement::class,
4343
'throw' => ThrowStatement::class,
4444
'try-catch' => TryCatchStatement::class,
45+
'yield' => YieldStatement::class,
4546
];
4647

4748
/**

src/Statements/YieldStatement.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Zephir.
5+
*
6+
* (c) Phalcon Team <team@zephir-lang.com>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Zephir\Statements;
15+
16+
use Zephir\CompilationContext;
17+
use Zephir\Exception\CompilerException;
18+
19+
/**
20+
* Statement-level handler for `yield` AST nodes emitted by the parser.
21+
*
22+
* The Zephir compiler does not yet generate code for generator methods.
23+
* PHP's `ZEND_YIELD` opcode runs only inside `ZEND_USER_FUNCTION` op_arrays,
24+
* and Zephir methods compile to `ZEND_INTERNAL_FUNCTION` (a plain C function
25+
* pointer) which has no opcode VM frame to suspend. Bridging the two
26+
* requires synthesizing a PHP source body for generator methods and
27+
* `zend_compile_string()`-ing it at `MINIT` so the engine treats the body
28+
* as a user function. That work is tracked separately.
29+
*
30+
* This handler exists so that:
31+
* - parser-accepted `yield` syntax produces a precise, located diagnostic
32+
* instead of the previous noisy "Unsupported statement" fallout,
33+
* - the statement dispatch table covers the type, keeping
34+
* `StatementFactory::isSupported('yield')` honest.
35+
*
36+
* @see https://github.com/zephir-lang/zephir/issues/1849
37+
*/
38+
final class YieldStatement extends StatementAbstract
39+
{
40+
/**
41+
* @throws CompilerException
42+
*/
43+
public function compile(CompilationContext $compilationContext): void
44+
{
45+
throw new CompilerException(
46+
"'yield' is parsed but code generation for generator methods is not yet implemented. "
47+
. 'Place generator logic in a plain PHP file and load it through the extensions '
48+
. "'extra-classes' / 'extra-sources' configuration as a workaround. "
49+
. 'See https://github.com/zephir-lang/zephir/issues/1849',
50+
$this->statement
51+
);
52+
}
53+
}

0 commit comments

Comments
 (0)