diff --git a/Makefile b/Makefile index 8c0cbebd..87021ced 100755 --- a/Makefile +++ b/Makefile @@ -134,6 +134,10 @@ test-harness: test-docker-quick: docker run --rm -v $(shell pwd):/compiler -w /compiler $(LOCAL_DEV_IMAGE) php vendor/bin/phpunit --exclude-group llvm -.PHONY: bootstrap-inventory +.PHONY: bootstrap-inventory bootstrap-profile bootstrap-aot-lint bootstrap-inventory: php script/bootstrap-inventory.php +bootstrap-profile: bootstrap-inventory + php script/bootstrap-profile.php +bootstrap-aot-lint: bootstrap-profile + php script/bootstrap-aot-lint.php diff --git a/docs/bootstrap-profile.json b/docs/bootstrap-profile.json new file mode 100644 index 00000000..1234852c --- /dev/null +++ b/docs/bootstrap-profile.json @@ -0,0 +1,281 @@ +{ + "phase": "B", + "issue": 212, + "entry": "bin/vm.php", + "unsupported_constructs": [ + "try/catch", + "generator yield", + "enum", + "eval()", + "create_function()", + "shell_exec()", + "exec()", + "passthru()" + ], + "compiler_cfg_gaps": [ + "Unsupported class type: ", + "Unsupported class body element: ", + "Unknown Op Type: ", + "Unknown Stmt Type: ", + "Unknown BinaryOp Type: ", + "Unknown CastOp Type: ", + "Unknown UnaryOp Type: ", + "Unsupported expression: ", + "Unknown Literal Operand Type: ", + "Unknown Operand Type: ", + "Unknown Terminal Type: " + ], + "excluded_files": [ + "lib/AOT/Linker.php", + "lib/VM/HashTable.php" + ], + "eligible_files": [ + "bin/vm.php", + "ext/standard/JitBin2hex.php", + "ext/standard/JitDate.php", + "ext/standard/JitEnv.php", + "ext/standard/JitExplode.php", + "ext/standard/JitHeader.php", + "ext/standard/JitHex2bin.php", + "ext/standard/JitHtmlspecialchars.php", + "ext/standard/JitHttpResponseCode.php", + "ext/standard/JitImplode.php", + "ext/standard/JitNl2br.php", + "ext/standard/JitNumberFormat.php", + "ext/standard/JitParseUrl.php", + "ext/standard/JitPath.php", + "ext/standard/JitRandomBytes.php", + "ext/standard/JitRealpath.php", + "ext/standard/JitRequestBody.php", + "ext/standard/JitStrPad.php", + "ext/standard/JitStrRepeat.php", + "ext/standard/JitStrReplace.php", + "ext/standard/JitStrSplit.php", + "ext/standard/JitStringConcat.php", + "ext/standard/JitStringIndex.php", + "ext/standard/JitStripTags.php", + "ext/standard/JitStrpos.php", + "ext/standard/JitUrlencode.php", + "ext/standard/Module.php", + "ext/standard/VmDate.php", + "ext/standard/VmExit.php", + "ext/standard/VmFs.php", + "ext/standard/VmNumberFormat.php", + "ext/standard/VmScope.php", + "ext/standard/VmString.php", + "ext/standard/abs.php", + "ext/standard/array_combine.php", + "ext/standard/array_count.php", + "ext/standard/array_fill.php", + "ext/standard/array_flip.php", + "ext/standard/array_key_exists.php", + "ext/standard/array_keys.php", + "ext/standard/array_merge.php", + "ext/standard/array_pop.php", + "ext/standard/array_push.php", + "ext/standard/array_reverse.php", + "ext/standard/array_search.php", + "ext/standard/array_shift.php", + "ext/standard/array_slice.php", + "ext/standard/array_sum.php", + "ext/standard/array_unique.php", + "ext/standard/array_values.php", + "ext/standard/basename.php", + "ext/standard/bin2hex.php", + "ext/standard/bindec.php", + "ext/standard/boolval.php", + "ext/standard/ceil.php", + "ext/standard/chr.php", + "ext/standard/compact_.php", + "ext/standard/cos.php", + "ext/standard/date.php", + "ext/standard/decbin.php", + "ext/standard/dechex.php", + "ext/standard/decoct.php", + "ext/standard/deg2rad.php", + "ext/standard/dirname.php", + "ext/standard/exp.php", + "ext/standard/explode.php", + "ext/standard/extract_.php", + "ext/standard/file_get_contents.php", + "ext/standard/floatval.php", + "ext/standard/floor.php", + "ext/standard/fmod.php", + "ext/standard/getallheaders_.php", + "ext/standard/getenv_.php", + "ext/standard/gettype.php", + "ext/standard/glob_.php", + "ext/standard/gmdate.php", + "ext/standard/header_.php", + "ext/standard/header_list.php", + "ext/standard/header_remove.php", + "ext/standard/hex2bin.php", + "ext/standard/hexdec.php", + "ext/standard/htmlspecialchars.php", + "ext/standard/http_response_code.php", + "ext/standard/implode.php", + "ext/standard/in_array.php", + "ext/standard/int_max.php", + "ext/standard/int_min.php", + "ext/standard/intdiv.php", + "ext/standard/intval.php", + "ext/standard/is_finite.php", + "ext/standard/is_infinite.php", + "ext/standard/is_nan.php", + "ext/standard/is_numeric.php", + "ext/standard/is_scalar.php", + "ext/standard/lcfirst.php", + "ext/standard/log.php", + "ext/standard/nl2br.php", + "ext/standard/number_format.php", + "ext/standard/octdec.php", + "ext/standard/ord.php", + "ext/standard/parse_url.php", + "ext/standard/pi.php", + "ext/standard/pow.php", + "ext/standard/putenv_.php", + "ext/standard/rad2deg.php", + "ext/standard/random_bytes.php", + "ext/standard/range.php", + "ext/standard/rawurldecode.php", + "ext/standard/rawurlencode.php", + "ext/standard/realpath.php", + "ext/standard/round.php", + "ext/standard/scandir.php", + "ext/standard/sin.php", + "ext/standard/sort_.php", + "ext/standard/sqrt.php", + "ext/standard/str_contains.php", + "ext/standard/str_ends_with.php", + "ext/standard/str_pad.php", + "ext/standard/str_repeat.php", + "ext/standard/str_replace.php", + "ext/standard/str_split.php", + "ext/standard/str_starts_with.php", + "ext/standard/strcmp.php", + "ext/standard/string_ltrim.php", + "ext/standard/string_rtrim.php", + "ext/standard/string_trim.php", + "ext/standard/strip_tags.php", + "ext/standard/stripos.php", + "ext/standard/strncmp.php", + "ext/standard/strpos.php", + "ext/standard/strrev.php", + "ext/standard/strtolower.php", + "ext/standard/strtoupper.php", + "ext/standard/strval.php", + "ext/standard/substr.php", + "ext/standard/tan.php", + "ext/standard/time.php", + "ext/standard/ucfirst.php", + "ext/standard/ucwords.php", + "ext/standard/urldecode.php", + "ext/standard/urlencode.php", + "ext/standard/var_dump.php", + "ext/types/Module.php", + "ext/types/is_type.php", + "ext/types/mb_strlen.php", + "ext/types/strlen.php", + "lib/Block.php", + "lib/Cli/PhpcInit.php", + "lib/Compiler.php", + "lib/Doctor.php", + "lib/Frame.php", + "lib/Func.php", + "lib/Func/Internal.php", + "lib/Func/JIT.php", + "lib/Func/PHP.php", + "lib/Handler.php", + "lib/JIT.php", + "lib/JIT/Analyzer.php", + "lib/JIT/ArrayBuiltinHelper.php", + "lib/JIT/BasicBlockHelper.php", + "lib/JIT/Builtin.php", + "lib/JIT/Builtin/ErrorHandler.php", + "lib/JIT/Builtin/HttpResponseCode.php", + "lib/JIT/Builtin/Internal.php", + "lib/JIT/Builtin/MemoryManager.php", + "lib/JIT/Builtin/MemoryManager/Native.php", + "lib/JIT/Builtin/MemoryManager/PHP.php", + "lib/JIT/Builtin/Output.php", + "lib/JIT/Builtin/Refcount.php", + "lib/JIT/Builtin/ScriptExit.php", + "lib/JIT/Builtin/StringDateTime.php", + "lib/JIT/Builtin/StringGetenv.php", + "lib/JIT/Builtin/StringHtmlspecialchars.php", + "lib/JIT/Builtin/StringNl2br.php", + "lib/JIT/Builtin/StringRandomBytes.php", + "lib/JIT/Builtin/StringUcwords.php", + "lib/JIT/Builtin/StringUrldecode.php", + "lib/JIT/Builtin/StringUrlencode.php", + "lib/JIT/Builtin/Type.php", + "lib/JIT/Builtin/Type/HashTable.php", + "lib/JIT/Builtin/Type/MaskedArray.php", + "lib/JIT/Builtin/Type/NativeArray.php", + "lib/JIT/Builtin/Type/Object_.php", + "lib/JIT/Builtin/Type/String_.php", + "lib/JIT/Builtin/Type/Value.php", + "lib/JIT/Builtin/VarArg.php", + "lib/JIT/Call.php", + "lib/JIT/Call/Native.php", + "lib/JIT/Call/Vararg.php", + "lib/JIT/CoalesceHelper.php", + "lib/JIT/Context.php", + "lib/JIT/HashTableHelper.php", + "lib/JIT/Helper.php", + "lib/JIT/IssetHelper.php", + "lib/JIT/JitNativeString.php", + "lib/JIT/JitValueBox.php", + "lib/JIT/JitValueCompare.php", + "lib/JIT/NullsafeHelper.php", + "lib/JIT/OperandName.php", + "lib/JIT/Result.php", + "lib/JIT/Scope.php", + "lib/JIT/StringOffsetHelper.php", + "lib/JIT/SuperglobalInit.php", + "lib/JIT/ValueEchoHelper.php", + "lib/JIT/Variable.php", + "lib/Lint/IncrementDetector.php", + "lib/Lint/Issue.php", + "lib/Lint/LintCompiler.php", + "lib/Lint/Linter.php", + "lib/Lint/ListDestructuringDetector.php", + "lib/Lint/SwitchDetector.php", + "lib/Lint/UnsupportedRegistry.php", + "lib/Module.php", + "lib/ModuleAbstract.php", + "lib/OpCode.php", + "lib/Printer.php", + "lib/Runtime.php", + "lib/VM.php", + "lib/VM/ClassEntry.php", + "lib/VM/ClassProperty.php", + "lib/VM/Context.php", + "lib/VM/ErrorReporter.php", + "lib/VM/ObjectEntry.php", + "lib/VM/Optimizer.php", + "lib/VM/Optimizer/AssignOp.php", + "lib/VM/Refcount.php", + "lib/VM/ScriptExit.php", + "lib/VM/Variable.php", + "lib/Web/DevServer.php", + "lib/Web/ProjectManifest.php", + "lib/Web/ResponseContext.php", + "lib/Web/Superglobals.php", + "src/cli.php", + "src/llvm-env.php", + "src/macro_functions.php", + "src/tokenizer-compat.php", + "src/yay-php8-compat.php" + ], + "aot_lint_targets": [ + "examples/000-HelloWorld/example.php", + "test/bootstrap-aot/echo_hello.php" + ], + "totals": { + "inventory_files": 239, + "excluded": 2, + "eligible": 237, + "aot_lint_targets": 2 + } +} diff --git a/script/bootstrap-aot-lint.php b/script/bootstrap-aot-lint.php new file mode 100755 index 00000000..ff7f49c4 --- /dev/null +++ b/script/bootstrap-aot-lint.php @@ -0,0 +1,94 @@ +#!/usr/bin/env php + $profile */ +$profile = json_decode((string) file_get_contents($profileFile), true); +if (!is_array($profile) || !isset($profile['aot_lint_targets']) || !is_array($profile['aot_lint_targets'])) { + fwrite(STDERR, "Invalid bootstrap profile: {$profileFile}\n"); + exit(1); +} + +$llvmDir = bootstrapResolveLlvmDir($root); +if (null === $llvmDir) { + fwrite(STDERR, "bootstrap-aot-lint: LLVM 9 not found (skip)\n"); + exit(2); +} + +$compileBin = $root.'/bin/compile.php'; +if (!is_file($compileBin)) { + fwrite(STDERR, "Missing {$compileBin}\n"); + exit(1); +} + +$env = bootstrapLlvmProcessEnv($llvmDir); +$phpBin = PHP_BINARY; +$failures = []; + +foreach ($profile['aot_lint_targets'] as $rel) { + if (!is_string($rel)) { + continue; + } + $path = $root.'/'.$rel; + if (!is_file($path)) { + $failures[] = "{$rel}: file not found"; + continue; + } + $cmd = [$phpBin, $compileBin, '-l', $path]; + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $proc = proc_open($cmd, $descriptorSpec, $pipes, $root, $env); + if (!is_resource($proc)) { + $failures[] = "{$rel}: proc_open failed"; + continue; + } + fclose($pipes[0]); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $code = proc_close($proc); + if (0 !== $code) { + $detail = trim(($stderr !== false ? $stderr : '')."\n".($stdout !== false ? $stdout : '')); + $failures[] = "{$rel}: exit {$code}".('' !== $detail ? "\n".$detail : ''); + continue; + } + if ($verbose) { + fwrite(STDOUT, "OK {$rel}\n"); + } +} + +if ($failures !== []) { + fwrite(STDERR, "bootstrap-aot-lint failed:\n".implode("\n\n", $failures)."\n"); + exit(1); +} + +fwrite(STDOUT, 'bootstrap-aot-lint: '.count($profile['aot_lint_targets'])." target(s) OK\n"); +exit(0); diff --git a/script/bootstrap-inventory.php b/script/bootstrap-inventory.php index 3af6bd6c..63743c76 100644 --- a/script/bootstrap-inventory.php +++ b/script/bootstrap-inventory.php @@ -15,249 +15,15 @@ * php script/bootstrap-inventory.php --json # machine-readable report on stdout */ -use PhpParser\Node; -use PhpParser\NodeTraverser; -use PhpParser\NodeVisitorAbstract; -use PhpParser\ParserFactory; - $root = dirname(__DIR__); require $root.'/vendor/autoload.php'; - -final class BootstrapConstructVisitor extends NodeVisitorAbstract -{ - /** @var list */ - public array $blockers = []; - - /** @var list */ - public array $warnings = []; - - private int $classMethodCount = 0; - - private int $closureCount = 0; - - public function enterNode(Node $node) - { - if ($node instanceof Node\Stmt\Try_) { - $this->blockers[] = 'try/catch (line '.$node->getLine().')'; - } elseif ($node instanceof Node\Expr\Yield_ || $node instanceof Node\Expr\YieldFrom) { - $this->blockers[] = 'generator yield (line '.$node->getLine().')'; - } elseif ($node instanceof Node\Stmt\ClassMethod && $node->name->toString() !== '__construct') { - ++$this->classMethodCount; - } elseif ($node instanceof Node\Stmt\Enum_) { - $this->blockers[] = 'enum (line '.$node->getLine().')'; - } elseif ($node instanceof Node\Stmt\Trait_) { - $this->warnings[] = 'trait '.$node->name.' (line '.$node->getLine().')'; - } elseif ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction) { - ++$this->closureCount; - } elseif ($node instanceof Node\Expr\New_ && $node->class instanceof Node\Name) { - $name = $node->class->toString(); - if (!str_starts_with($name, 'PHPCompiler\\') - && !str_starts_with($name, 'PHPCfg\\') - && !str_starts_with($name, 'PHPTypes\\') - && !str_starts_with($name, 'PhpParser\\') - && !str_starts_with($name, 'PHPLLVM\\') - && !in_array($name, ['LogicException', 'RuntimeException', 'InvalidArgumentException', 'TypeError', 'ValueError', 'ReflectionClass', 'SplObjectStorage'], true) - ) { - $this->warnings[] = 'new '.$name.' (line '.$node->getLine().')'; - } - } elseif ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { - $fn = $node->name->toString(); - if (in_array($fn, ['eval', 'create_function', 'shell_exec', 'exec', 'passthru'], true)) { - $this->blockers[] = $fn.'() (line '.$node->getLine().')'; - } - } - } - - public function beforeTraverse(array $nodes) - { - $this->classMethodCount = 0; - $this->closureCount = 0; - } - - public function afterTraverse(array $nodes) - { - if ($this->classMethodCount > 0) { - $this->warnings[] = $this->classMethodCount.' class method(s) — PHPCfg Op\\Stmt\\ClassMethod not lowered in Compiler'; - } - if ($this->closureCount > 0) { - $this->warnings[] = $this->closureCount.' closure(s)'; - } - } -} - -/** - * @return list - */ -function bootstrapExtractCompilerBlockers(string $compilerFile): array -{ - $source = (string) file_get_contents($compilerFile); - $blockers = []; - if (preg_match_all('/throw new \\\\LogicException\([\'"]([^\'"]+)[\'"]/', $source, $m)) { - foreach ($m[1] as $msg) { - if (stripos($msg, 'unknown') !== false || stripos($msg, 'unsupported') !== false) { - $blockers[] = $msg; - } - } - } - - return array_values(array_unique($blockers)); -} - -/** - * @return array{blockers: list, warnings: list} - */ -function bootstrapScanConstructs(string $file): array -{ - $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); - $code = (string) file_get_contents($file); - try { - $ast = $parser->parse($code); - } catch (Throwable $e) { - return [ - 'blockers' => ['parse error: '.$e->getMessage()], - 'warnings' => [], - ]; - } - if (!is_array($ast)) { - return ['blockers' => [], 'warnings' => []]; - } - - $visitor = new BootstrapConstructVisitor(); - $traverser = new NodeTraverser(); - $traverser->addVisitor($visitor); - $traverser->traverse($ast); - - return [ - 'blockers' => $visitor->blockers, - 'warnings' => $visitor->warnings, - ]; -} - -/** - * @param array $report - */ -function bootstrapRenderMarkdown(array $report): string -{ - $lines = []; - $lines[] = '# Bootstrap inventory (vm.php path)'; - $lines[] = ''; - $lines[] = 'Auto-generated by `script/bootstrap-inventory.php`. Tracks **Phase A** of [#212](https://github.com/PurHur/php-compiler/issues/212) (self-host bootstrap).'; - $lines[] = ''; - $lines[] = 'Regenerate: `php script/bootstrap-inventory.php`'; - $lines[] = ''; - $lines[] = '## Summary'; - $lines[] = ''; - $lines[] = '| Metric | Count |'; - $lines[] = '|--------|------:|'; - $lines[] = '| PHP files on vm.php path | '.$report['totals']['files'].' |'; - $lines[] = '| Source constructs flagged (blockers) | '.$report['totals']['blockers'].' |'; - $lines[] = '| Source constructs flagged (warnings) | '.$report['totals']['warnings'].' |'; - $lines[] = ''; - $lines[] = '## Compiler CFG gaps (`lib/Compiler.php`)'; - $lines[] = ''; - $lines[] = 'These `LogicException` messages indicate CFG ops or expressions not yet lowered:'; - $lines[] = ''; - foreach ($report['compiler_blockers'] as $msg) { - $lines[] = '- `'.$msg.'`'; - } - $lines[] = ''; - $lines[] = '## Files'; - $lines[] = ''; - $lines[] = '| File | Blockers | Warnings |'; - $lines[] = '|------|----------|----------|'; - foreach ($report['files'] as $rel => $info) { - $b = count($info['blockers']); - $w = count($info['warnings']); - if ($b === 0 && $w === 0) { - continue; - } - $lines[] = '| `'.$rel.'` | '.$b.' | '.$w.' |'; - } - $lines[] = ''; - $lines[] = '## Per-file construct flags'; - $lines[] = ''; - foreach ($report['files'] as $rel => $info) { - if ($info['blockers'] === [] && $info['warnings'] === []) { - continue; - } - $lines[] = '### `'.$rel.'`'; - $lines[] = ''; - if ($info['blockers'] !== []) { - $lines[] = '**Blockers** (likely prevent AOT bootstrap compile):'; - foreach ($info['blockers'] as $item) { - $lines[] = '- '.$item; - } - $lines[] = ''; - } - if ($info['warnings'] !== []) { - $lines[] = '**Warnings** (review for bootstrap subset):'; - foreach ($info['warnings'] as $item) { - $lines[] = '- '.$item; - } - $lines[] = ''; - } - } - - return implode("\n", $lines)."\n"; -} +require __DIR__.'/bootstrap-lib.php'; $check = in_array('--check', $argv, true); $jsonOut = in_array('--json', $argv, true); $outFile = $root.'/docs/bootstrap-inventory.md'; -$entryFiles = [ - 'bin/vm.php', - 'src/cli.php', - 'src/tokenizer-compat.php', - 'src/yay-php8-compat.php', - 'src/llvm-env.php', - 'src/macro_functions.php', -]; - -$compilerBlockers = bootstrapExtractCompilerBlockers($root.'/lib/Compiler.php'); - -$files = []; -foreach ($entryFiles as $rel) { - $path = $root.'/'.$rel; - if (is_file($path)) { - $files[$path] = true; - } -} -foreach (['lib', 'ext', 'src'] as $dir) { - $base = $root.'/'.$dir; - if (!is_dir($base)) { - continue; - } - $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($base)); - foreach ($it as $file) { - if ($file->isFile() && str_ends_with($file->getPathname(), '.php')) { - $files[$file->getPathname()] = true; - } - } -} - -ksort($files, SORT_STRING); - -$fileReports = []; -$totals = ['files' => 0, 'blockers' => 0, 'warnings' => 0]; -foreach (array_keys($files) as $path) { - if (!is_file($path) || !str_ends_with($path, '.php')) { - continue; - } - $rel = substr($path, strlen($root) + 1); - $constructs = bootstrapScanConstructs($path); - $fileReports[$rel] = $constructs; - ++$totals['files']; - $totals['blockers'] += count($constructs['blockers']); - $totals['warnings'] += count($constructs['warnings']); -} - -$report = [ - 'entry' => 'bin/vm.php', - 'compiler_blockers' => $compilerBlockers, - 'totals' => $totals, - 'files' => $fileReports, -]; +$report = bootstrapCollectInventoryReport($root); if ($jsonOut) { echo json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n"; @@ -282,4 +48,4 @@ function bootstrapRenderMarkdown(array $report): string mkdir(dirname($outFile), 0775, true); } file_put_contents($outFile, $markdown); -fwrite(STDOUT, "Wrote {$outFile} ({$totals['files']} files, {$totals['blockers']} blockers)\n"); +fwrite(STDOUT, "Wrote {$outFile} ({$report['totals']['files']} files, {$report['totals']['blockers']} blockers)\n"); diff --git a/script/bootstrap-lib.php b/script/bootstrap-lib.php new file mode 100644 index 00000000..38fada28 --- /dev/null +++ b/script/bootstrap-lib.php @@ -0,0 +1,379 @@ + */ + public array $blockers = []; + + /** @var list */ + public array $warnings = []; + + private int $classMethodCount = 0; + + private int $closureCount = 0; + + public function enterNode(Node $node) + { + if ($node instanceof Node\Stmt\Try_) { + $this->blockers[] = 'try/catch (line '.$node->getLine().')'; + } elseif ($node instanceof Node\Expr\Yield_ || $node instanceof Node\Expr\YieldFrom) { + $this->blockers[] = 'generator yield (line '.$node->getLine().')'; + } elseif ($node instanceof Node\Stmt\ClassMethod && $node->name->toString() !== '__construct') { + ++$this->classMethodCount; + } elseif ($node instanceof Node\Stmt\Enum_) { + $this->blockers[] = 'enum (line '.$node->getLine().')'; + } elseif ($node instanceof Node\Stmt\Trait_) { + $this->warnings[] = 'trait '.$node->name.' (line '.$node->getLine().')'; + } elseif ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction) { + ++$this->closureCount; + } elseif ($node instanceof Node\Expr\New_ && $node->class instanceof Node\Name) { + $name = $node->class->toString(); + if (!str_starts_with($name, 'PHPCompiler\\') + && !str_starts_with($name, 'PHPCfg\\') + && !str_starts_with($name, 'PHPTypes\\') + && !str_starts_with($name, 'PhpParser\\') + && !str_starts_with($name, 'PHPLLVM\\') + && !in_array($name, ['LogicException', 'RuntimeException', 'InvalidArgumentException', 'TypeError', 'ValueError', 'ReflectionClass', 'SplObjectStorage'], true) + ) { + $this->warnings[] = 'new '.$name.' (line '.$node->getLine().')'; + } + } elseif ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { + $fn = $node->name->toString(); + if (in_array($fn, ['eval', 'create_function', 'shell_exec', 'exec', 'passthru'], true)) { + $this->blockers[] = $fn.'() (line '.$node->getLine().')'; + } + } + } + + public function beforeTraverse(array $nodes) + { + $this->classMethodCount = 0; + $this->closureCount = 0; + } + + public function afterTraverse(array $nodes) + { + if ($this->classMethodCount > 0) { + $this->warnings[] = $this->classMethodCount.' class method(s) — PHPCfg Op\\Stmt\\ClassMethod not lowered in Compiler'; + } + if ($this->closureCount > 0) { + $this->warnings[] = $this->closureCount.' closure(s)'; + } + } +} + +/** + * @return list + */ +function bootstrapExtractCompilerBlockers(string $compilerFile): array +{ + $source = (string) file_get_contents($compilerFile); + $blockers = []; + if (preg_match_all('/throw new \\\\LogicException\([\'"]([^\'"]+)[\'"]/', $source, $m)) { + foreach ($m[1] as $msg) { + if (stripos($msg, 'unknown') !== false || stripos($msg, 'unsupported') !== false) { + $blockers[] = $msg; + } + } + } + + return array_values(array_unique($blockers)); +} + +/** + * @return array{blockers: list, warnings: list} + */ +function bootstrapScanConstructs(string $file): array +{ + $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); + $code = (string) file_get_contents($file); + try { + $ast = $parser->parse($code); + } catch (Throwable $e) { + return [ + 'blockers' => ['parse error: '.$e->getMessage()], + 'warnings' => [], + ]; + } + if (!is_array($ast)) { + return ['blockers' => [], 'warnings' => []]; + } + + $visitor = new BootstrapConstructVisitor(); + $traverser = new NodeTraverser(); + $traverser->addVisitor($visitor); + $traverser->traverse($ast); + + return [ + 'blockers' => $visitor->blockers, + 'warnings' => $visitor->warnings, + ]; +} + +/** + * @return list + */ +function bootstrapVmPathPhpFiles(string $root): array +{ + $entryFiles = [ + 'bin/vm.php', + 'src/cli.php', + 'src/tokenizer-compat.php', + 'src/yay-php8-compat.php', + 'src/llvm-env.php', + 'src/macro_functions.php', + ]; + + $files = []; + foreach ($entryFiles as $rel) { + $path = $root.'/'.$rel; + if (is_file($path)) { + $files[$path] = true; + } + } + foreach (['lib', 'ext', 'src'] as $dir) { + $base = $root.'/'.$dir; + if (!is_dir($base)) { + continue; + } + $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($base)); + foreach ($it as $file) { + if ($file->isFile() && str_ends_with($file->getPathname(), '.php')) { + $files[$file->getPathname()] = true; + } + } + } + ksort($files, SORT_STRING); + + return array_keys($files); +} + +/** + * @return array + */ +function bootstrapCollectInventoryReport(string $root): array +{ + $compilerBlockers = bootstrapExtractCompilerBlockers($root.'/lib/Compiler.php'); + $fileReports = []; + $totals = ['files' => 0, 'blockers' => 0, 'warnings' => 0]; + foreach (bootstrapVmPathPhpFiles($root) as $path) { + if (!is_file($path) || !str_ends_with($path, '.php')) { + continue; + } + $rel = substr($path, strlen($root) + 1); + $constructs = bootstrapScanConstructs($path); + $fileReports[$rel] = $constructs; + ++$totals['files']; + $totals['blockers'] += count($constructs['blockers']); + $totals['warnings'] += count($constructs['warnings']); + } + + return [ + 'entry' => 'bin/vm.php', + 'compiler_blockers' => $compilerBlockers, + 'totals' => $totals, + 'files' => $fileReports, + ]; +} + +/** + * Procedural scripts used as AOT lint gates for the bootstrap subset (Phase B). + * + * @return list repo-relative paths + */ +function bootstrapDefaultAotLintTargets(string $root): array +{ + $targets = [ + 'examples/000-HelloWorld/example.php', + ]; + foreach (glob($root.'/test/bootstrap-aot/*.php') ?: [] as $path) { + $targets[] = substr($path, strlen($root) + 1); + } + sort($targets, SORT_STRING); + + return array_values(array_unique($targets)); +} + +/** + * @param array $inventory + * + * @return array + */ +function bootstrapBuildProfile(array $inventory, string $root): array +{ + $excluded = []; + $eligible = []; + foreach ($inventory['files'] as $rel => $info) { + if (count($info['blockers']) > 0) { + $excluded[] = $rel; + } else { + $eligible[] = $rel; + } + } + sort($excluded, SORT_STRING); + sort($eligible, SORT_STRING); + + $lintTargets = bootstrapDefaultAotLintTargets($root); + foreach ($lintTargets as $rel) { + if (!is_file($root.'/'.$rel)) { + throw new RuntimeException("bootstrap profile lint target missing: {$rel}"); + } + if (isset($inventory['files'][$rel]) && count($inventory['files'][$rel]['blockers']) > 0) { + throw new RuntimeException("bootstrap profile lint target has inventory blockers: {$rel}"); + } + } + + return [ + 'phase' => 'B', + 'issue' => 212, + 'entry' => $inventory['entry'], + 'unsupported_constructs' => BOOTSTRAP_UNSUPPORTED_CONSTRUCTS, + 'compiler_cfg_gaps' => $inventory['compiler_blockers'], + 'excluded_files' => $excluded, + 'eligible_files' => $eligible, + 'aot_lint_targets' => $lintTargets, + 'totals' => [ + 'inventory_files' => $inventory['totals']['files'], + 'excluded' => count($excluded), + 'eligible' => count($eligible), + 'aot_lint_targets' => count($lintTargets), + ], + ]; +} + +/** + * @param array $report + */ +function bootstrapRenderMarkdown(array $report): string +{ + $lines = []; + $lines[] = '# Bootstrap inventory (vm.php path)'; + $lines[] = ''; + $lines[] = 'Auto-generated by `script/bootstrap-inventory.php`. Tracks **Phase A** of [#212](https://github.com/PurHur/php-compiler/issues/212) (self-host bootstrap).'; + $lines[] = ''; + $lines[] = 'Regenerate: `php script/bootstrap-inventory.php`'; + $lines[] = ''; + $lines[] = '## Summary'; + $lines[] = ''; + $lines[] = '| Metric | Count |'; + $lines[] = '|--------|------:|'; + $lines[] = '| PHP files on vm.php path | '.$report['totals']['files'].' |'; + $lines[] = '| Source constructs flagged (blockers) | '.$report['totals']['blockers'].' |'; + $lines[] = '| Source constructs flagged (warnings) | '.$report['totals']['warnings'].' |'; + $lines[] = ''; + $lines[] = '## Compiler CFG gaps (`lib/Compiler.php`)'; + $lines[] = ''; + $lines[] = 'These `LogicException` messages indicate CFG ops or expressions not yet lowered:'; + $lines[] = ''; + foreach ($report['compiler_blockers'] as $msg) { + $lines[] = '- `'.$msg.'`'; + } + $lines[] = ''; + $lines[] = '## Files'; + $lines[] = ''; + $lines[] = '| File | Blockers | Warnings |'; + $lines[] = '|------|----------|----------|'; + foreach ($report['files'] as $rel => $info) { + $b = count($info['blockers']); + $w = count($info['warnings']); + if ($b === 0 && $w === 0) { + continue; + } + $lines[] = '| `'.$rel.'` | '.$b.' | '.$w.' |'; + } + $lines[] = ''; + $lines[] = '## Per-file construct flags'; + $lines[] = ''; + foreach ($report['files'] as $rel => $info) { + if ($info['blockers'] === [] && $info['warnings'] === []) { + continue; + } + $lines[] = '### `'.$rel.'`'; + $lines[] = ''; + if ($info['blockers'] !== []) { + $lines[] = '**Blockers** (likely prevent AOT bootstrap compile):'; + foreach ($info['blockers'] as $item) { + $lines[] = '- '.$item; + } + $lines[] = ''; + } + if ($info['warnings'] !== []) { + $lines[] = '**Warnings** (review for bootstrap subset):'; + foreach ($info['warnings'] as $item) { + $lines[] = '- '.$item; + } + $lines[] = ''; + } + } + + return implode("\n", $lines)."\n"; +} + +/** + * @param array $profile + */ +function bootstrapProfileJson(array $profile): string +{ + return json_encode($profile, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n"; +} + +/** + * Resolve LLVM 9 directory for bootstrap AOT lint (mirrors script/jit-runtime-probe.php). + */ +function bootstrapResolveLlvmDir(string $root): ?string +{ + $llvmDir = getenv('PHP_COMPILER_LLVM_PATH') ?: ''; + if ('' !== $llvmDir && is_file($llvmDir.'/libLLVM-9.so.1')) { + return realpath($llvmDir) ?: $llvmDir; + } + foreach ([$root.'/.llvm', '/opt/llvm9'] as $candidate) { + if (is_file($candidate.'/libLLVM-9.so.1')) { + return realpath($candidate) ?: $candidate; + } + } + + return null; +} + +/** + * @return array + */ +function bootstrapLlvmProcessEnv(string $llvmDir): array +{ + $env = []; + foreach (array_merge($_ENV, $_SERVER) as $key => $value) { + if (is_string($value)) { + $env[$key] = $value; + } + } + $env['PHP_COMPILER_LLVM_PATH'] = $llvmDir; + $ld = $env['LD_LIBRARY_PATH'] ?? ''; + $env['LD_LIBRARY_PATH'] = '' === $ld ? $llvmDir : $llvmDir.':'.$ld; + $path = $env['PATH'] ?? ''; + $env['PATH'] = '' === $path ? $llvmDir : $llvmDir.':'.$path; + + return $env; +} diff --git a/script/bootstrap-profile.php b/script/bootstrap-profile.php new file mode 100755 index 00000000..d4e71cba --- /dev/null +++ b/script/bootstrap-profile.php @@ -0,0 +1,45 @@ +#!/usr/bin/env php +markTestSkipped( + 'LLVM 9 toolchain not available. Run script/install-llvm9.sh from the repository root.' + ); + } + $root = dirname(__DIR__, 2); + $cmd = escapeshellarg(PHP_BINARY).' '.escapeshellarg($root.'/script/bootstrap-aot-lint.php').' 2>&1'; + exec($cmd, $out, $code); + $this->assertSame(0, $code, implode("\n", $out)); + $this->assertStringContainsString('target(s) OK', implode("\n", $out)); + } + + private static function isLlvmReady(): bool + { + if (null !== self::$llvmReady) { + return self::$llvmReady; + } + self::$llvmReady = LlvmToolchain::isReady(dirname(__DIR__, 2)); + + return self::$llvmReady; + } +} diff --git a/test/bootstrap-aot/echo_hello.php b/test/bootstrap-aot/echo_hello.php new file mode 100644 index 00000000..7cae5c83 --- /dev/null +++ b/test/bootstrap-aot/echo_hello.php @@ -0,0 +1,9 @@ +/dev/null'; + exec($cmd, $out, $code); + $this->assertSame(0, $code, implode("\n", $out)); + $json = (string) file_get_contents($root.'/docs/bootstrap-profile.json'); + $profile = json_decode($json, true); + $this->assertIsArray($profile); + $this->assertSame('B', $profile['phase']); + $this->assertContains('examples/000-HelloWorld/example.php', $profile['aot_lint_targets']); + $this->assertContains('test/bootstrap-aot/echo_hello.php', $profile['aot_lint_targets']); + $this->assertContains('lib/AOT/Linker.php', $profile['excluded_files']); + } + + public function testProfileDocIsFresh(): void + { + $root = dirname(__DIR__, 2); + $doc = $root.'/docs/bootstrap-profile.json'; + $this->assertFileExists($doc); + $cmd = escapeshellarg(PHP_BINARY).' '.escapeshellarg($root.'/script/bootstrap-profile.php').' --check 2>&1'; + exec($cmd, $out, $code); + $this->assertSame(0, $code, implode("\n", $out)); + } +}