diff --git a/phpunit/code/object-foreach-indent.php b/phpunit/code/object-foreach-indent.php new file mode 100644 index 00000000..755af6a6 --- /dev/null +++ b/phpunit/code/object-foreach-indent.php @@ -0,0 +1,11 @@ +addFiles([$file]); + $compiler->prepareFile($file); + $compiler->convertFile($file); + + $reflection = new \ReflectionClass($compiler); + $this->assertSame(0, $reflection->getProperty('indentLevel')->getValue($compiler)); + } +} diff --git a/phpunit/src/ParallelCompileTest.php b/phpunit/src/ParallelCompileTest.php index 0ad5c871..be9c4cdb 100644 --- a/phpunit/src/ParallelCompileTest.php +++ b/phpunit/src/ParallelCompileTest.php @@ -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 diff --git a/src/Build/NativeBuilder.php b/src/Build/NativeBuilder.php index 223d0f1c..4e804930 100644 --- a/src/Build/NativeBuilder.php +++ b/src/Build/NativeBuilder.php @@ -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, failures: list} */ 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]; } diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index 8a27fbd3..1ea60f53 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -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; } diff --git a/src/Generator/TypeCheckGenerator.php b/src/Generator/TypeCheckGenerator.php index 07d45cb2..16dc4ae3 100644 --- a/src/Generator/TypeCheckGenerator.php +++ b/src/Generator/TypeCheckGenerator.php @@ -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; diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 6bb55ef9..acc0548e 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -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 . '; ' . '}())'; diff --git a/src/Translator.php b/src/Translator.php index f9a99c8f..782bd5af 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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; }