diff --git a/README.md b/README.md index a899097..1d28e14 100644 --- a/README.md +++ b/README.md @@ -152,28 +152,16 @@ docker run --rm -v (pwd):/monkey -w /monkey php:8.4-cli ./monkey run examples/fi Clone this repository, execute `composer install`, then: ```bash -docker run --rm -v (pwd):/monkey -w /monkey php:8.4-cli ./monkey repl +docker run -it --rm -v (pwd):/monkey -w /monkey php:8.4-cli ./monkey repl ``` Example: ```text - __,__ - .--. .-" "-. .--. - / .. \/ .-. .-. \/ .. \ - | | '| / Y \ |' | | - | \ \ \ 0 | 0 / / / | - \ '- ,\.-"`` ``"-./, -' / - `'-' /_ ^ ^ _\ '-'` - | \._ _./ | - \ \ `~` / / - '._ '-=-' _.' - '~---~' -------------------------------- -| Monkey Programming Language | -------------------------------- - - > let a = 20 + fn(x){ return x + 10; }(2); +šŸ’ Monkey Programming Language v1.0.0 +Type ':h' for help, ':c' for clear, ':q' to quit + +āžœ let a = 20 + fn(x){ return x + 10; }(2); 32 ``` diff --git a/composer.json b/composer.json index 9f0a378..bc332ec 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ "rector/rector": "^2.0.3" }, "require": { - "php": "^8.4" + "php": "^8.4", + "ext-readline": "*" }, "autoload": { "psr-4": { diff --git a/monkey b/monkey index cb421b4..eb78a6d 100755 --- a/monkey +++ b/monkey @@ -3,10 +3,17 @@ declare(strict_types=1); +require 'vendor/autoload.php'; + +use Monkey\Monkey; + if (PHP_SAPI !== 'cli') { exit; } -require 'vendor/autoload.php'; +if (!isset($GLOBALS['argv']) || !is_array($GLOBALS['argv'])) { + exit(1); +} -require __DIR__ . '/src/monkey.php'; +$cli = new Monkey(); +exit($cli->run($GLOBALS['argv'])); diff --git a/src/Monkey.php b/src/Monkey.php new file mode 100644 index 0000000..f024a2f --- /dev/null +++ b/src/Monkey.php @@ -0,0 +1,246 @@ +environment = new Environment(); + } + + /** + * @param array $argv + */ + public function run(array $argv): int + { + if (count($argv) <= 1) { + return $this->showHelp(); + } + + try { + return match ($argv[1]) { + 'repl' => $this->startRepl(), + 'run' => $this->runFile($argv[2] ?? null), + '--version', '-v' => $this->showVersion(), + '--help', '-h' => $this->showHelp(), + '--debug' => $this->enableDebugMode(), + default => $this->showHelp(), + }; + } catch (Throwable $throwable) { + $this->writeError($throwable->getMessage()); + + return 1; + } + } + + private function startRepl(): int + { + $this->showWelcomeBanner(); + + while (true) { + $input = $this->readInput(); + + if ($input === false) { + echo PHP_EOL . 'Goodbye!' . PHP_EOL; + + return 0; + } + + if (trim($input) === '') { + continue; + } + + if ($this->handleSpecialCommand($input)) { + continue; + } + + try { + $this->writeOutput($this->evaluate($input)); + } catch (Throwable $e) { + $this->writeError($e->getMessage()); + } + } + } + + private function runFile(?string $filename): int + { + if ($filename === null) { + throw new RuntimeException('No input file specified'); + } + + if (!file_exists($filename)) { + throw new RuntimeException("File not found: {$filename}"); + } + + $contents = file_get_contents($filename); + + if ($contents === false) { + throw new RuntimeException("Could not read file: {$filename}"); + } + + $this->writeOutput($this->evaluate($contents)); + + return 0; + } + + private function evaluate(string $input): MonkeyObject + { + $lexer = new Lexer($input); + $parser = new Parser($lexer); + + $errors = $parser->errors(); + + if ($errors !== []) { + throw new RuntimeException("Parser errors:\n" . implode("\n", $errors)); + } + + $program = new ProgramParser()($parser); + $evaluator = new Evaluator(); + + return $evaluator->eval($program, $this->environment); + } + + private function handleSpecialCommand(string $input): bool + { + return match (trim($input)) { + ':q', ':quit', 'exit' => $this->handleQuit(), + ':h', ':help' => $this->handleHelp(), + ':c', ':clear' => $this->handleClear(), + ':d', ':debug' => $this->handleDebugToggle(), + default => false, + }; + } + + #[NoReturn] private function handleQuit(): bool + { + echo 'Goodbye!' . PHP_EOL; + + exit(0); + } + + private function handleHelp(): bool + { + echo <<showWelcomeBanner(); + + return true; + } + + private function handleDebugToggle(): bool + { + $this->debugMode = !$this->debugMode; + + echo 'Debug mode: ' . ($this->debugMode ? 'Enabled' : 'Disabled') . PHP_EOL; + + return true; + } + + private function showWelcomeBanner(): void + { + $version = self::VERSION; + + echo <<debugMode) { + return; + } + + if ($this->debugMode) { + echo $result::class . ': '; + } + + echo $result->inspect() . PHP_EOL; + } + + private function writeError(string $message): void + { + fwrite(STDERR, 'Error: ' . $message . PHP_EOL); + } + + private function showVersion(): int + { + echo 'Monkey Programming Language v' . self::VERSION . PHP_EOL; + + return 0; + } + + private function showHelp(): int + { + echo << Execute a Monkey source file + --version, -v Show version information + --help, -h Show this help message + --debug Enable debug mode + + Examples: + monkey repl + monkey run example.monkey + HELP . PHP_EOL; + + return 0; + } + + private function enableDebugMode(): int + { + $this->debugMode = true; + + return $this->startRepl(); + } +} diff --git a/src/monkey.php b/src/monkey.php deleted file mode 100644 index d1aca71..0000000 --- a/src/monkey.php +++ /dev/null @@ -1,35 +0,0 @@ - Repl::start(), - 'run' => Repl::eval((string)file_get_contents($argv[2]), new Environment()), - default => help(), -};