fix(generator): update exception throwing mechanism and add progress tracking

- Replace php::throwException with php::throwExceptionEx for better error handling
- Add return php::null statements after exception throws in closure generators
- Update argument count error messages to use format strings with proper escaping
- Modify type error messages to use format strings instead of concatenation
- Add completed callback parameter to dispatchParallel method in NativeBuilder
- Track and report completed task count during parallel compilation
- Add progress bar functionality using AnsiKit components
- Include failure reason reporting when compilation fails
- Add test case for parallel dispatcher completion tracking
- Add test for foreach indentation restoration in object iteration
- Increment version from 0.3.0 to 0.4.0
pull/18/head
韩天峰 1 month ago
parent bc8e4a685b
commit cf8bfdef06
  1. 11
      phpunit/code/object-foreach-indent.php
  2. 23
      phpunit/src/ForeachIndentTest.php
  3. 34
      phpunit/src/ParallelCompileTest.php
  4. 8
      src/Build/NativeBuilder.php
  5. 12
      src/Generator/ClosureGenerator.php
  6. 57
      src/Generator/TypeCheckGenerator.php
  7. 13
      src/Parser/PropertyAccessTrait.php
  8. 62
      src/Translator.php

@ -0,0 +1,11 @@
<?php
function visitObjects(object $first, object $second): void
{
foreach ($first as $value) {
echo $value;
}
foreach ($second as $value) {
echo $value;
}
}

@ -0,0 +1,23 @@
<?php
namespace TypePhp\Tests;
use PHPUnit\Framework\TestCase;
use TypePhp\CompilerTest;
class ForeachIndentTest extends TestCase
{
public function testObjectForeachRestoresCompilerIndentation(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$file = ROOT_PATH . '/phpunit/code/object-foreach-indent.php';
$compiler->addFiles([$file]);
$compiler->prepareFile($file);
$compiler->convertFile($file);
$reflection = new \ReflectionClass($compiler);
$this->assertSame(0, $reflection->getProperty('indentLevel')->getValue($compiler));
}
}

@ -3,6 +3,8 @@
namespace TypePhp\Tests;
use PHPUnit\Framework\TestCase;
use TypePhp\Backend\CompilerBackend;
use TypePhp\Build\NativeBuilder;
use TypePhp\CompilerTest;
class ParallelCompileTest extends TestCase
@ -55,6 +57,38 @@ class ParallelCompileTest extends TestCase
$this->assertSame(1, $compiler->getWaitCallCount());
}
public function testParallelDispatcherReportsEachCompletedTask(): void
{
$builder = new NativeBuilder($this->createMock(CompilerBackend::class));
$forkResults = [101, 102];
$waitResults = [[102, 0], [101, 1 << 8]];
$completed = [];
$result = $builder->dispatchParallel(
['first.cc', 'second.cc'],
2,
static fn(string $source): string => $source . '.o',
static function (): void {},
static function () use (&$forkResults): int {
return array_shift($forkResults);
},
static function () use (&$waitResults): array {
return array_shift($waitResults);
},
static fn(int $status): bool => $status === 0,
static function (string $source, string $object, int $status, bool $success, int $count) use (&$completed): void {
$completed[] = [$source, $object, $status, $success, $count];
},
);
$this->assertSame(['second.cc.o'], $result['objects']);
$this->assertSame(['first.cc'], $result['failures']);
$this->assertSame([
['second.cc', 'second.cc.o', 0, true, 1],
['first.cc', 'first.cc.o', 1 << 8, false, 2],
], $completed);
}
}
class ScriptedWaitCompiler extends CompilerTest

@ -63,6 +63,7 @@ final readonly class NativeBuilder
* @param Closure(): int $fork
* @param Closure(): array{int, int} $wait
* @param Closure(int): bool $succeeded
* @param null|Closure(string, string, int, bool, int): void $completed
* @return array{objects: list<string>, failures: list<string>}
*/
public function dispatchParallel(
@ -73,11 +74,13 @@ final readonly class NativeBuilder
Closure $fork,
Closure $wait,
Closure $succeeded,
?Closure $completed = null,
): array {
$queue = array_values($sources);
$running = [];
$objects = [];
$failures = [];
$completedCount = 0;
while ($queue !== [] || $running !== []) {
while (count($running) < $jobs && $queue !== []) {
@ -109,11 +112,14 @@ final readonly class NativeBuilder
if ($task === null) {
continue;
}
if ($succeeded($status)) {
$success = $succeeded($status);
if ($success) {
$objects[] = $task['object'];
} else {
$failures[] = $task['source'];
}
$completedCount++;
$completed?->__invoke($task['source'], $task['object'], $status, $success, $completedCount);
}
return ['objects' => $objects, 'failures' => $failures];
}

@ -108,14 +108,14 @@ trait ClosureGenerator
}
if ($requiredArgCount > 0) {
$expected = $requiredArgCount === count($params) ? 'exactly' : 'at least';
$message = 'php::concat({'
. 'php::Str(' . $this->genCharPtr('Too few arguments to function {closure}(), ', true) . '), '
. 'php::toString(php::getCallArgNum()), '
. 'php::Str(' . $this->genCharPtr(' passed and ' . $expected . ' ' . $requiredArgCount . ' expected', true) . ')'
. '})';
$message = $this->genCharPtr(
'Too few arguments to function {closure}(), %u passed and ' . $expected . ' ' . $requiredArgCount . ' expected',
true
);
$code .= $this->getIndent() . 'if (UNEXPECTED(php::getCallArgNum() < ' . $requiredArgCount . ')) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'return php::throwException(zend_ce_argument_count_error, (' . $message . ').toCString());' . PHP_EOL;
$code .= $this->getIndent() . 'php::throwExceptionEx(zend_ce_argument_count_error, 0, ' . $message . ', php::getCallArgNum());' . PHP_EOL;
$code .= $this->getIndent() . 'return php::null;' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
}

@ -252,12 +252,12 @@ trait TypeCheckGenerator
}
$orExpr = implode(' || ', $conditions);
$msgExpr = $this->genUnionParamTypeErrorExpr($argInfo, $varName, (string) ($argIndex + 1));
$throwExpr = $this->genUnionParamTypeErrorExpr($argInfo, $varName, (string) ($argIndex + 1));
$code = $this->genCompositeIntToFloatCoercion($varName, $argInfo->typeCheck);
$code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL;
$code .= $this->getIndent() . $throwExpr . ';' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
@ -282,7 +282,7 @@ trait TypeCheckGenerator
}
$orExpr = implode(' || ', $conditions);
$msgExpr = $this->genUnionParamTypeErrorExpr($argInfo, $valueVar, $argNoVar);
$throwExpr = $this->genUnionParamTypeErrorExpr($argInfo, $valueVar, $argNoVar);
$code = $this->getIndent() . 'for (auto ' . $iterVar . ' = ' . $argInfo->name . '.begin(); ' . $iterVar . ' != ' . $argInfo->name . '.end(); ++' . $iterVar . ') {' . PHP_EOL;
$this->indentLevel++;
@ -298,7 +298,7 @@ trait TypeCheckGenerator
$code .= $this->getIndent() . Type::INT . ' ' . $argNoVar . ' = ' . ($argIndex + 1) . ' + ' . $iterVar . '.index();' . PHP_EOL;
$code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL;
$code .= $this->getIndent() . $throwExpr . ';' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
$this->indentLevel--;
@ -311,15 +311,11 @@ trait TypeCheckGenerator
{
$fnName = $this->getTypeCheckCallableName();
$paramName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name);
return 'php::concat({'
. 'php::Str(' . $this->genCharPtr($fnName . '(): Argument #', true) . '), '
. 'php::toString(' . $argNoExpr . '), '
. 'php::Str(' . $this->genCharPtr(' ($' . $paramName . ') must be of type ', true) . '), '
. 'php::Str(' . $this->genCharPtr($argInfo->typeStr, true) . '), '
. 'php::Str(", "), '
. $valueExpr . '.typeStr(), '
. 'php::Str(" given")'
. '})';
$format = $this->genCharPtr($fnName . '(): Argument #', true)
. ' ZEND_LONG_FMT '
. $this->genCharPtr(' ($' . $paramName . ') must be of type ' . $argInfo->typeStr . ', %s given', true);
return 'php::throwExceptionEx(zend_ce_type_error, 0, ' . $format . ', '
. $argNoExpr . ', ' . $valueExpr . '.typeStr())';
}
protected function genUnionReturnCheck(string $varName): string
@ -344,13 +340,12 @@ trait TypeCheckGenerator
$fnName = $this->getTypeCheckCallableName();
$typeStr = $this->functionDef->returnTypeStr;
$msgExpr = 'php::concat(php::concat(php::Str(' . $this->genCharPtr($fnName, true) . ' "(): Return value must be of type " '
. $this->genCharPtr($typeStr, true) . ' ", "), ' . $varName . '.typeStr()), php::Str(" given"))';
$format = $this->genCharPtr($fnName . '(): Return value must be of type ' . $typeStr . ', %s given', true);
$code = $this->genCompositeIntToFloatCoercion($varName, $typeCheck);
$code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL;
$code .= $this->getIndent() . 'php::throwExceptionEx(zend_ce_type_error, 0, ' . $format . ', ' . $varName . '.typeStr());' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
@ -379,12 +374,13 @@ trait TypeCheckGenerator
}
$orExpr = implode(' || ', $conditions);
$msgExpr = $this->genClosureParamTypeErrorExpr($argInfo, $argInfo->name, (string) ($argIndex + 1));
$throwExpr = $this->genClosureParamTypeErrorExpr($argInfo, $argInfo->name, (string) ($argIndex + 1));
$code = $this->genCompositeIntToFloatCoercion($argInfo->name, $argInfo->typeCheck);
$code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'return php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL;
$code .= $this->getIndent() . $throwExpr . ';' . PHP_EOL;
$code .= $this->getIndent() . 'return php::null;' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
@ -409,7 +405,7 @@ trait TypeCheckGenerator
}
$orExpr = implode(' || ', $conditions);
$msgExpr = $this->genClosureParamTypeErrorExpr($argInfo, $valueVar, $argNoVar);
$throwExpr = $this->genClosureParamTypeErrorExpr($argInfo, $valueVar, $argNoVar);
$code = $this->getIndent() . 'for (auto ' . $iterVar . ' = ' . $argInfo->name . '.begin(); ' . $iterVar . ' != ' . $argInfo->name . '.end(); ++' . $iterVar . ') {' . PHP_EOL;
$this->indentLevel++;
@ -425,7 +421,8 @@ trait TypeCheckGenerator
$code .= $this->getIndent() . Type::INT . ' ' . $argNoVar . ' = ' . ($argIndex + 1) . ' + ' . $iterVar . '.index();' . PHP_EOL;
$code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'return php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL;
$code .= $this->getIndent() . $throwExpr . ';' . PHP_EOL;
$code .= $this->getIndent() . 'return php::null;' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
$this->indentLevel--;
@ -437,15 +434,11 @@ trait TypeCheckGenerator
protected function genClosureParamTypeErrorExpr(ArgInfo $argInfo, string $valueExpr, string $argNoExpr): string
{
$paramName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name);
return 'php::concat({'
. 'php::Str(' . $this->genCharPtr('{closure}(): Argument #', true) . '), '
. 'php::toString(' . $argNoExpr . '), '
. 'php::Str(' . $this->genCharPtr(' ($' . $paramName . ') must be of type ', true) . '), '
. 'php::Str(' . $this->genCharPtr($argInfo->typeStr, true) . '), '
. 'php::Str(", "), '
. $valueExpr . '.typeStr(), '
. 'php::Str(" given")'
. '})';
$format = $this->genCharPtr('{closure}(): Argument #', true)
. ' ZEND_LONG_FMT '
. $this->genCharPtr(' ($' . $paramName . ') must be of type ' . $argInfo->typeStr . ', %s given', true);
return 'php::throwExceptionEx(zend_ce_type_error, 0, ' . $format . ', '
. $argNoExpr . ', ' . $valueExpr . '.typeStr())';
}
protected function genClosureReturnCheck(string $varName): string
@ -468,13 +461,13 @@ trait TypeCheckGenerator
$orExpr = implode(' || ', $conditions);
$typeStr = $this->context->closureReturnTypeStr;
$msgExpr = 'php::concat(php::concat(php::Str(' . $this->genCharPtr('{closure}', true) . ' "(): Return value must be of type " '
. $this->genCharPtr($typeStr, true) . ' ", "), ' . $varName . '.typeStr()), php::Str(" given"))';
$format = $this->genCharPtr('{closure}(): Return value must be of type ' . $typeStr . ', %s given', true);
$code = $this->genCompositeIntToFloatCoercion($varName, $typeCheck);
$code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'return php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL;
$code .= $this->getIndent() . 'php::throwExceptionEx(zend_ce_type_error, 0, ' . $format . ', ' . $varName . '.typeStr());' . PHP_EOL;
$code .= $this->getIndent() . 'return php::null;' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;

@ -567,12 +567,13 @@ trait PropertyAccessTrait
$propDisplay = $this->getObjectPropertyTypeCheckDisplayName($left);
$typeStr = $this->getObjectPropertyTypeCheckTypeString($def);
if ($this->usesPhpStylePropertyAssignTypeError($def)) {
$msgExpr = 'php::concat({php::Str("Cannot assign "), ' . $tmpVar . '.typeStr(), php::Str(" to property "), '
. 'php::Str(' . $this->genCharPtr($propDisplay, true) . '), php::Str(" of type "), '
. 'php::Str(' . $this->genCharPtr($typeStr, true) . ')})';
$throwExpr = 'php::throwExceptionEx(zend_ce_type_error, 0, '
. $this->genCharPtr('Cannot assign %s to property ' . $propDisplay . ' of type ' . $typeStr, true)
. ', ' . $tmpVar . '.typeStr())';
} else {
$msgExpr = 'php::concat(php::concat(php::Str(' . $this->genCharPtr($propDisplay, true) . ' " must be of type " '
. $this->genCharPtr($typeStr, true) . ' ", "), ' . $tmpVar . '.typeStr()), php::Str(" given"))';
$throwExpr = 'php::throwExceptionEx(zend_ce_type_error, 0, '
. $this->genCharPtr($propDisplay . ' must be of type ' . $typeStr . ', %s given', true)
. ', ' . $tmpVar . '.typeStr())';
}
$coercion = $this->compositeTypeNeedsIntToFloatCoercion($typeCheck)
@ -583,7 +584,7 @@ trait PropertyAccessTrait
. $tmpVar . ' = ' . $rightExpr . '; '
. $coercion
. 'if (UNEXPECTED(!(' . implode(' || ', $conditions) . '))) { '
. 'php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString()); '
. $throwExpr . '; '
. '} '
. 'return ' . $tmpVar . '; '
. '}())';

@ -8,6 +8,8 @@
namespace TypePhp;
use Ajaxray\AnsiKit\AnsiTerminal;
use Ajaxray\AnsiKit\Components\Progressbar;
use MJS\TopSort\Implementations\StringSort;
use TypePhp\Analysis\SsaBuilder;
use TypePhp\Backend\CompilerFactory;
@ -49,7 +51,7 @@ class Translator extends Preprocessor
use ResourceCompilationTrait;
use ClassConstantValueTrait;
public const string VERSION = '0.3.0';
public const string VERSION = '0.4.0';
public const string APP_NAME = 'TypePHP Compiler (AOT)';
protected const string MODULE_NAME_PREFIX = 'app_';
@ -1312,6 +1314,17 @@ CODE;
return pcntl_wifexited($status) && pcntl_wexitstatus($status) === 0;
}
protected function getCompileChildFailureReason(int $status): string
{
if (pcntl_wifsignaled($status)) {
return 'terminated by signal ' . pcntl_wtermsig($status);
}
if (pcntl_wifexited($status)) {
return 'exited with status ' . pcntl_wexitstatus($status);
}
return 'terminated abnormally';
}
protected function compileWithPcntl(array $sourceFiles, int $job): array
{
if (!function_exists('pcntl_fork')) {
@ -1321,6 +1334,14 @@ CODE;
$totalFiles = count($sourceFiles);
$this->climate->lightBlue("Starting parallel compilation with {$job} jobs for {$totalFiles} files");
$progress = null;
if (!$this->noProgress) {
$progress = new Progressbar();
$progress->barStyle([AnsiTerminal::FG_GREEN])
->percentageStyle([AnsiTerminal::TEXT_BOLD])
->labelStyle([AnsiTerminal::FG_CYAN]);
$progress->renderInPlace(0, $totalFiles, 'Compiling');
}
$result = $this->getNativeBuilder()->dispatchParallel(
$sourceFiles,
$job,
@ -1331,8 +1352,25 @@ CODE;
fn(): int => $this->pcntlFork(),
fn(): array => $this->waitForCompileChild(),
fn(int $status): bool => $this->compileChildSucceeded($status),
function (string $source, string $object, int $status, bool $success, int $completed) use ($progress, $totalFiles): void {
if (!$success) {
echo PHP_EOL;
$this->climate->red("Compilation failed: {$source} ({$this->getCompileChildFailureReason($status)})");
}
if ($this->noProgress) {
$percent = (int) ($completed / $totalFiles * 100);
$shortSource = $this->removeCommonPrefix($this->buildDir, $source);
$this->climate->white("[{$completed}/{$totalFiles}] {$percent}% {$shortSource}");
} else {
$progress->renderInPlace($completed, $totalFiles, 'Compiling');
}
},
);
if (!$this->noProgress) {
echo PHP_EOL;
}
if ($result['failures'] !== []) {
throw new \Exception('Compilation failed for: ' . implode(', ', $result['failures']));
}
@ -2653,15 +2691,14 @@ CODE;
{
$required = $functionDef->argCountRequired;
$expected = $required === count($functionDef->argInfoList) ? 'exactly' : 'at least';
$message = 'php::concat({'
. 'php::Str(' . $this->genCharPtr('Too few arguments to function ' . $displayName . '(), ', true) . '), '
. 'php::toString(php::getCallArgNum()), '
. 'php::Str(' . $this->genCharPtr(' passed and ' . $expected . ' ' . $required . ' expected', true) . ')'
. '})';
$message = $this->genCharPtr(
'Too few arguments to function ' . $displayName . '(), %u passed and ' . $expected . ' ' . $required . ' expected',
true
);
$code = $this->getIndent() . 'if (UNEXPECTED(php::getCallArgNum() < ' . $required . ')) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'php::throwException(zend_ce_argument_count_error, (' . $message . ').toCString());' . PHP_EOL;
$code .= $this->getIndent() . 'php::throwExceptionEx(zend_ce_argument_count_error, 0, ' . $message . ', php::getCallArgNum());' . PHP_EOL;
$code .= $this->getIndent() . 'return;' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
@ -3560,11 +3597,9 @@ CODE;
$nextStr = $this->getLiteralString('next');
$rewindStr = $this->getLiteralString('rewind');
$invalidAggregateReturn = static function (string $aggregateObj): string {
return 'php::throwException(zend_ce_exception, (php::concat({'
. 'php::Str("Objects returned by "), '
. $aggregateObj . '.getClassName(), '
. 'php::Str("::getIterator() must be traversable or implement interface Iterator")'
. '})).toCString());';
return 'php::throwExceptionEx(zend_ce_exception, 0, '
. '"Objects returned by %s::getIterator() must be traversable or implement interface Iterator", '
. $aggregateObj . '.getClassName().toCString());';
};
$code = $iterableVar . ' = ' . $obj . ';' . PHP_EOL;
@ -3608,7 +3643,8 @@ CODE;
$code .= $this->getIndent() . $tmpArrayVar . ' = php::call(' . $this->getFuncPtr('get_object_vars') . ', {' . $obj . '});' . PHP_EOL;
$code .= $this->parseForeachArray($node, $tmpArrayVar);
$this->indentLevel--;
$code .= '}' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
return $code;
}

Loading…
Cancel
Save