diff --git a/README.md b/README.md index 556461a18..08fa5018c 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ Golang interpreter written in PHP. -# Example +## Example ```php use GoPhp\Interpreter; -$interpreter = new Interpreter(<<run(); +$result = $interp->run(); ``` See [examples](examples/) for more. - To run examples: ``` @@ -43,11 +42,9 @@ php main.php ## WIP -This is a work-in-progress project. - -Already implemented: +This is a toy project, currently work-in-progress. -* see [tests](tests/Functional/files/) +To see what is already implemented, refer to [tests](tests/Functional/files/). ## Development @@ -63,4 +60,4 @@ run tests: make test ``` -run `make help` for more commands. \ No newline at end of file +run `make help` for more commands. diff --git a/bin/go-php b/bin/go-php index 612675167..2f4efad69 100755 --- a/bin/go-php +++ b/bin/go-php @@ -184,7 +184,7 @@ function main(array $argv): never $stderr = new ResourceOutputStream(STDERR); $errorHandler = new OutputToStream($stderr); - $runtime = new Interpreter( + $runtime = Interpreter::create( source: $src, errorHandler: $errorHandler, envVars: new EnvVarSet( @@ -196,7 +196,7 @@ function main(array $argv): never $result = $runtime->run(); - exit($result->value); + exit($result->exitCode->value); } error_reporting(E_ALL); diff --git a/examples/helloworld/main.php b/examples/helloworld/main.php index 71c96ca1d..dfac332ac 100644 --- a/examples/helloworld/main.php +++ b/examples/helloworld/main.php @@ -8,7 +8,7 @@ use GoPhp\Stream\StringStreamProvider; $stdout = ''; -$interp = new Interpreter( +$interp = Interpreter::create( source: <<<'GO' package main @@ -19,7 +19,7 @@ streams: new StringStreamProvider($stdout, $stdout), ); -$exitCode = $interp->run(); +$result = $interp->run(); print "Output:\n$stdout\n"; -print "Exit code: $exitCode->value\n"; +print "Exit code: $result->exitCode->value\n"; diff --git a/examples/sorting/main.php b/examples/sorting/main.php index a266b870f..078b814c7 100644 --- a/examples/sorting/main.php +++ b/examples/sorting/main.php @@ -11,16 +11,15 @@ $goRoot = __DIR__; $goFile = __DIR__ . '/src/main.go'; $goSrc = file_get_contents($goFile); - $stdout = ''; -$interp = new Interpreter( +$interp = Interpreter::create( source: $goSrc, streams: new StringStreamProvider($stdout, $stdout), envVars: new EnvVarSet($goRoot) ); -$exitCode = $interp->run(); +$result = $interp->run(); print "Output:\n$stdout\n"; -print "Exit code: $exitCode->value\n"; +print "Exit code: $result->exitCode->value\n"; diff --git a/psalm.xml b/psalm.xml index cdfbfc47f..a4ef816b6 100644 --- a/psalm.xml +++ b/psalm.xml @@ -17,6 +17,5 @@ - diff --git a/src/CallStackCollectorDebugger.php b/src/CallStackCollectorDebugger.php new file mode 100644 index 000000000..9b6f7be20 --- /dev/null +++ b/src/CallStackCollectorDebugger.php @@ -0,0 +1,44 @@ + */ + private array $stackTrace = []; + + public function __construct( + private readonly bool $enableDebug = true, + private readonly int $maxTraceDepth = self::DEFAULT_STACK_TRACE_DEPTH, + ) {} + + public function addStackTrace(InvokableCall|PanicError $call): void + { + if (!$this->enableDebug) { + return; + } + + $this->stackTrace[] = $call; + + if (count($this->stackTrace) > $this->maxTraceDepth) { + array_shift($this->stackTrace); + } + } + + /** + * @return list + */ + public function getStackTrace(): array + { + return $this->stackTrace; + } +} diff --git a/src/Debugger.php b/src/Debugger.php new file mode 100644 index 000000000..ce66593b2 --- /dev/null +++ b/src/Debugger.php @@ -0,0 +1,17 @@ + + */ + public function getStackTrace(): array; +} diff --git a/src/Error/InterfaceTypeError.php b/src/Error/InterfaceTypeError.php index 6eb747046..6ec9acdf0 100644 --- a/src/Error/InterfaceTypeError.php +++ b/src/Error/InterfaceTypeError.php @@ -34,7 +34,7 @@ public static function cannotUseAsInterfaceType(WrappedValue|WrappedType $value, public static function fromOther(self $error, WrappedType $interfaceType): self { if (!isset($error->value, $error->missingMethod)) { - throw InternalError::unreachable('Cannot convert error from other error'); + throw InternalError::unreachable('cannot convert error from other error'); } return self::cannotUseAsType( diff --git a/src/GoValue/Slice/SliceValue.php b/src/GoValue/Slice/SliceValue.php index 1f573430a..b542b68a0 100644 --- a/src/GoValue/Slice/SliceValue.php +++ b/src/GoValue/Slice/SliceValue.php @@ -167,7 +167,6 @@ public function append(GoValue $value): void $this->grow(); } - /** @psalm-suppress PossiblyNullReference */ $this->values[$this->len++] = $value; } @@ -268,6 +267,9 @@ private function exceedsCapacity(): bool return $this->len - $this->pos + 1 > $this->cap; } + /** + * @psalm-assert !null $this->values + */ private function grow(): void { $copies = []; diff --git a/src/Interpreter.php b/src/Interpreter.php index 3ed477d3d..21bddf6e1 100644 --- a/src/Interpreter.php +++ b/src/Interpreter.php @@ -124,104 +124,161 @@ final class Interpreter { private static None $noneJump; + private readonly JumpStack $jumpStack; + private readonly DeferredStack $deferredStack; + private readonly ScopeResolver $scopeResolver; + private readonly InvokableCallList $initializers; + private readonly TypeResolver $typeResolver; private Ast $ast; - private Iota $iota; - private PanicPointer $panicPointer; - private Environment $env; - private JumpStack $jumpStack; - private DeferredStack $deferredStack; - private ScopeResolver $scopeResolver; - private InvokableCallList $initializers; private bool $constContext = false; private int $switchContext = 0; - private readonly Argv $argv; - private readonly ErrorHandler $errorHandler; private ?FuncValue $entryPoint = null; - private ImportHandler $importHandler; - private readonly string $source; - private readonly StreamProvider $streams; - private readonly FuncTypeValidator $entryPointValidator; - private readonly FuncTypeValidator $initValidator; - private readonly TypeResolver $typeResolver; - /** - * @param list $argv - */ public function __construct( - string $source, - array $argv = [], - ?BuiltinProvider $builtin = null, - ?ErrorHandler $errorHandler = null, - StreamProvider $streams = new StdStreamProvider(), - FuncTypeValidator $entryPointValidator = new ZeroArityValidator(ENTRY_POINT_FUNC, ENTRY_POINT_PACKAGE), - FuncTypeValidator $initValidator = new ZeroArityValidator(INITIALIZER_FUNC), - EnvVarSet $envVars = new EnvVarSet(), - bool $toplevel = false, + private readonly string $source, + private readonly Argv $argv, + private readonly ErrorHandler $errorHandler, + private readonly StreamProvider $streams, + private readonly FuncTypeValidator $entryPointValidator, + private readonly FuncTypeValidator $initValidator, + private readonly ImportHandler $importHandler, + private readonly Iota $iota, + private PanicPointer $panicPointer, + private Environment $env, + private readonly ?Debugger $debugger, ) { $this->jumpStack = new JumpStack(); $this->deferredStack = new DeferredStack(); $this->scopeResolver = new ScopeResolver(); $this->initializers = new InvokableCallList(); - $this->importHandler = new ImportHandler($envVars); - $this->streams = $streams; - $this->entryPointValidator = $entryPointValidator; - $this->initValidator = $initValidator; - - $this->errorHandler = $errorHandler === null - ? new OutputToStream($this->streams->stderr()) - : $errorHandler; - - if ($builtin === null) { - $builtin = new StdBuiltinProvider($this->streams->stderr()); - } - - $this->iota = $builtin->iota(); - $this->panicPointer = $builtin->panicPointer(); - $this->env = Environment::fromEnclosing($builtin->env()); - $this->argv = (new ArgvBuilder($argv))->build(); - $this->source = $toplevel ? self::wrapSource($source) : $source; $this->typeResolver = new TypeResolver( $this->scopeResolver, $this->tryEvalConstExpr(...), $this->env, ); - self::$noneJump = new None(); } /** - * @throws InternalError unexpected error during execution + * Creates an interpreter instance + * + * @param string $source Source code to execute + * @param array $argv Command line arguments + * @param BuiltinProvider|null $builtin Builtin package provider + * @param ErrorHandler|null $errorHandler Error handler + * @param StreamProvider $streams Stream provider of stdout, stderr, stdin + * @param FuncTypeValidator $entryPointValidator Validator for entry point function + * @param FuncTypeValidator $initValidator Validator for package initializer functions + * @param EnvVarSet $envVars Environment variables + * @param bool $toplevel Whether the source is a top level code or not + * @param bool $debug Whether to enable debug mode or not + * @param Debugger|null $debugger Debugger, if $debug is set to false, this is ignored + * @param array $customFileExtensions Custom file extensions to include when importing + * + * @return self */ - public function run(): ExitCode + public static function create( + string $source, + array $argv = [], + ?BuiltinProvider $builtin = null, + ?ErrorHandler $errorHandler = null, + StreamProvider $streams = new StdStreamProvider(), + FuncTypeValidator $entryPointValidator = new ZeroArityValidator( + ENTRY_POINT_FUNC, + ENTRY_POINT_PACKAGE, + ), + FuncTypeValidator $initValidator = new ZeroArityValidator(INITIALIZER_FUNC), + EnvVarSet $envVars = new EnvVarSet(), + bool $toplevel = false, + bool $debug = false, + ?Debugger $debugger = null, + array $customFileExtensions = [], + ): self { + static $init = false; + if (!$init) { + $init = true; + self::$noneJump = new None(); + } + + $errorHandler ??= new OutputToStream($streams->stderr()); + $builtin ??= new StdBuiltinProvider($streams->stderr()); + $importHandler = new ImportHandler($envVars, $customFileExtensions); + + if ($debug) { + $debugger ??= new CallStackCollectorDebugger(); + } else { + $debugger = null; + } + + return new self( + source: $toplevel ? self::wrapSource($source, $entryPointValidator) : $source, + argv: (new ArgvBuilder($argv))->build(), + errorHandler: $errorHandler, + streams: $streams, + entryPointValidator: $entryPointValidator, + initValidator: $initValidator, + importHandler: $importHandler, + iota: $builtin->iota(), + panicPointer: $builtin->panicPointer(), + env: Environment::fromEnclosing($builtin->env()), + debugger: $debugger, + ); + } + + /** + * @throws InternalError Unexpected error during execution + * @return RuntimeResult Result of execution + */ + public function run(): RuntimeResult { + $resultBuilder = new RuntimeResultBuilder(); + if ($this->debugger !== null) { + $resultBuilder->setDebugger($this->debugger); + } + try { $ast = $this->parseSourceToAst($this->source); $this->setAst($ast); $this->evalDeclsInOrder(); - - if ($this->entryPoint === null) { - if ($this->scopeResolver->entryPointPackage === $this->entryPointValidator->getPackageName()) { - throw RuntimeError::noEntryPointFunction( - $this->entryPointValidator->getFuncName(), - $this->entryPointValidator->getPackageName(), - ); - } - - throw RuntimeError::notEntryPointPackage( - $this->scopeResolver->entryPointPackage, - $this->entryPointValidator->getPackageName(), - ); - } - + $this->checkEntryPoint(); $call = new InvokableCall($this->entryPoint, Argv::fromEmpty()); $this->callFunc($call); } catch (RuntimeError|PanicError $error) { $this->errorHandler->onError($error); - return ExitCode::Failure; + $resultBuilder->setExitCode(ExitCode::Failure); + $resultBuilder->setError($error); + + return $resultBuilder->build(); } catch (AbortExecutionError) { - return ExitCode::Failure; + $resultBuilder->setExitCode(ExitCode::Failure); + + return $resultBuilder->build(); } - return ExitCode::Success; + $resultBuilder->setExitCode(ExitCode::Success); + + return $resultBuilder->build(); + } + + /** + * @psalm-assert !null $this->entryPoint + */ + private function checkEntryPoint(): void + { + if ($this->entryPoint !== null) { + return; + } + + if ($this->scopeResolver->entryPointPackage === $this->entryPointValidator->getPackageName()) { + throw RuntimeError::noEntryPointFunction( + $this->entryPointValidator->getFuncName(), + $this->entryPointValidator->getPackageName(), + ); + } + + throw RuntimeError::notEntryPointPackage( + $this->scopeResolver->entryPointPackage, + $this->entryPointValidator->getPackageName(), + ); } private function evalDeclsInOrder(): void @@ -626,6 +683,7 @@ private function callFunc(InvokableCall $fn): GoValue try { $value = $fn(); + $this->debugger?->addStackTrace($fn); $this->releaseDeferredStack(); return $value; @@ -1511,12 +1569,12 @@ private function releaseDeferredStack(): void } } - private function wrapSource(string $source): string + private static function wrapSource(string $source, FuncTypeValidator $entryPointValidator): string { return <<entryPointValidator->getPackageName()} + package {$entryPointValidator->getPackageName()} - func {$this->entryPointValidator->getFuncName()}() { + func {$entryPointValidator->getFuncName()}() { {$source} } GO; diff --git a/src/PanicPointer.php b/src/PanicPointer.php index 02dbeebef..82fb20e70 100644 --- a/src/PanicPointer.php +++ b/src/PanicPointer.php @@ -8,7 +8,5 @@ final class PanicPointer { - public function __construct( - public ?PanicError $panic = null, - ) {} + public ?PanicError $panic = null; } diff --git a/src/RuntimeResult.php b/src/RuntimeResult.php new file mode 100644 index 000000000..a0a6d8d65 --- /dev/null +++ b/src/RuntimeResult.php @@ -0,0 +1,18 @@ +exitCode = $exitCode; + } + + public function setResult(GoValue $result): void + { + $this->result = $result; + } + + public function setDebugger(Debugger $debugger): void + { + $this->debugger = $debugger; + } + + public function setError(GoError $error): void + { + $this->error = $error; + } + + public function build(): RuntimeResult + { + if ($this->exitCode === null) { + throw new InternalError('exit code is not set'); + } + + return new RuntimeResult( + $this->exitCode, + $this->result, + $this->debugger, + $this->error, + ); + } +} diff --git a/tests/Functional/InterpreterTest.php b/tests/Functional/InterpreterTest.php index 2f928eb1a..ece3ec2b1 100644 --- a/tests/Functional/InterpreterTest.php +++ b/tests/Functional/InterpreterTest.php @@ -33,7 +33,7 @@ public function testSourceFiles(string $goProgram, string $expectedOutput): void $stdin, ); - $interpreter = new Interpreter( + $interpreter = Interpreter::create( source: $goProgram, streams: $streams, envVars: new EnvVarSet(