diff options
Diffstat (limited to 'src/Interpreter/Interpreter.php')
-rw-r--r-- | src/Interpreter/Interpreter.php | 460 |
1 files changed, 460 insertions, 0 deletions
diff --git a/src/Interpreter/Interpreter.php b/src/Interpreter/Interpreter.php new file mode 100644 index 0000000..eed0004 --- /dev/null +++ b/src/Interpreter/Interpreter.php @@ -0,0 +1,460 @@ +<?php + +namespace Mnml\Interpreter; + +use Closure; +use Mnml\Lexer\Lexer; +use Mnml\Lexer\Token; +use Mnml\Lexer\TokenType; +use Mnml\Parser\ArrayMapAccessNode; +use Mnml\Parser\ArrayNode; +use Mnml\Parser\BoolNode; +use Mnml\Parser\CommentNode; +use Mnml\Parser\CompareExpression; +use Mnml\Parser\Condition; +use Mnml\Parser\ConstVariableDeclaration; +use Mnml\Parser\FunctionCall; +use Mnml\Parser\FunctionCallParameter; +use Mnml\Parser\FunctionDefinition; +use Mnml\Parser\FunctionReturn; +use Mnml\Parser\IdentifierNode; +use Mnml\Parser\IfArm; +use Mnml\Parser\IfNode; +use Mnml\Parser\MapNode; +use Mnml\Parser\Node; +use Mnml\Parser\NumberNode; +use Mnml\Parser\OperatorExpression; +use Mnml\Parser\Parenthesis; +use Mnml\Parser\Parser; +use Mnml\Parser\PipeExpression; +use Mnml\Parser\StringNode; +use Mnml\Parser\VariableAssignment; + +class Interpreter +{ + private array $nodes; + + /** + * @param Node[] $nodes + */ + public function __construct(array $nodes) + { + $this->nodes = $nodes; + } + + public function compile(array $parameters): Scope + { + $scope = new Scope(); + + // add standard library + $standardLibrary = include dirname(__DIR__) . "/standard.php"; + foreach ($standardLibrary as $identifier => $function) { + $scope->addExternal($identifier, $function); + } + + foreach ($this->nodes as $currentNode) { + $value = $this->evaluate($currentNode, $scope); + } + + return $scope; + } + + private function evaluate(Node $currentNode, Scope $scope): Node|null|bool + { + if ($currentNode instanceof ConstVariableDeclaration) { + $identifier = $currentNode->identifier; + $value = new ValueNode(null); + + /*if ($scope->has($identifier)) { + echo sprintf("%s already declared in current scope." . PHP_EOL, $identifier->token->value); + }*/ # TODO: problems with recursive functions. clear scope somehow? + + #else { + if (! is_null($currentNode->expression)) { + $value = $this->evaluate($currentNode->expression, $scope); + } + + $scope->add($identifier, $value); + #} + + return $value; + } + + if ($currentNode instanceof VariableAssignment) { + $identifier = $currentNode->identifier; + $value = new ValueNode(null); + + if ($scope->has($identifier)) { + $curentValue = $scope->get($identifier)->getValue(); + + if (is_null($curentValue)) { + $value = $this->evaluate($currentNode->expression, $scope); + $scope->set($identifier, $value); + } + else { + echo sprintf("%s already assigned in current scope." . PHP_EOL, $identifier->token->value); + } + } + + else { + echo sprintf("%s is not declared in current scope." . PHP_EOL, $identifier->token->value); + } + + return $value; + } + + else if ($currentNode instanceof IdentifierNode) { + $variable = $scope->get($currentNode); + + return $this->evaluate($variable, $scope); + } + + else if ($currentNode instanceof ArrayMapAccessNode) { + return $this->evaluateArrayOrMapAccess($currentNode, $scope); + } + + else if ($currentNode instanceof Parenthesis) { + return $this->evaluate($currentNode->content, $scope); + } + + else if ($currentNode instanceof OperatorExpression) { + $left = $this->evaluate($currentNode->left, $scope)->getValue(); + $right = $this->evaluate($currentNode->right, $scope)->getValue(); + + if ($currentNode->operator->value == "+") { + if (is_string($left) and is_string($right)) { + return new ValueNode($left . $right); + } + + return new ValueNode($left + $right); + } + + else if ($currentNode->operator->value == "-") { + return new ValueNode($left - $right); + } + + else if ($currentNode->operator->value == "*") { + return new ValueNode($left * $right); + } + + else if ($currentNode->operator->value == "**") { + return new ValueNode(pow($left, $right)); + } + + else if ($currentNode->operator->value == "/") { + return new ValueNode($left / $right); + } + + else if ($currentNode->operator->value == "%") { + return new ValueNode($left % $right); + } + } + + else if ($currentNode instanceof CompareExpression) { + $left = $this->evaluate($currentNode->left, $scope)->getValue(); + $right = $this->evaluate($currentNode->right, $scope)->getValue(); + + if ($currentNode->operator->value == "<") { + return new ValueNode($left < $right); + } + + else if ($currentNode->operator->value == ">") { + return new ValueNode($left > $right); + } + + else if ($currentNode->operator->value == "<=") { + return new ValueNode($left <= $right); + } + + else if ($currentNode->operator->value == ">=") { + return new ValueNode($left >= $right); + } + + else if ($currentNode->operator->value == "==") { + return new ValueNode($left == $right); + } + + else if ($currentNode->operator->value == "!=") { + return new ValueNode($left != $right); + } + } + + else if ($currentNode instanceof IfNode) { + foreach ($currentNode->arms as $arm) { + /**@var IfArm $arm*/ + $condition = true; + if ($arm->condition != null) { + $condition = $this->evaluate($arm->condition, $scope)->getValue(); + } + + if ($condition) { + return $this->evaluateIfArmBody($arm, $scope); + } + } + + return null; + } + + else if ($currentNode instanceof Condition) { + $left = $this->evaluate($currentNode->left, $scope)->getValue(); + $right = $this->evaluate($currentNode->right, $scope)->getValue(); + + if ($currentNode->operator->value == "and") { + return new ValueNode($left and $right); + } + + else if ($currentNode->operator->value == "or") { + return new ValueNode($left or $right); + } + } + + else if ($currentNode instanceof FunctionDefinition) { + return $currentNode; + } + + else if ($currentNode instanceof FunctionCall) { + $functionDefinition = $scope->get($currentNode->identifier); + + if ($functionDefinition instanceof FunctionDefinition) { + return $this->evaluateFunction($currentNode->identifier, $functionDefinition, $currentNode->parameters, $scope); + } + else if ($functionDefinition instanceof Closure) { + return $this->evaluateClosure($currentNode->identifier, $functionDefinition, $currentNode->parameters, $scope); + } + + return $currentNode; + } + + else if ($currentNode instanceof PipeExpression) { + $left = $this->evaluate($currentNode->left, $scope); + + $pipeScope = new Scope($scope); + $pipeScope->add(new IdentifierNode(new Token(TokenType::Identifier, "$", "$", 0, 0)), $left); + + $right = $this->evaluate($currentNode->right, $pipeScope); + + return $right; + } + + else if ($currentNode instanceof NumberNode) { + return $currentNode; + } + + else if ($currentNode instanceof StringNode) { + return $currentNode; + } + + else if ($currentNode instanceof BoolNode) { + return $currentNode; + } + + else if ($currentNode instanceof ArrayNode) { + return $currentNode; + } + + else if ($currentNode instanceof MapNode) { + return $currentNode; + } + + else if ($currentNode instanceof ValueNode) { + return $currentNode; + } + + else if ($currentNode instanceof CommentNode) { + return $currentNode; + } + + else if (is_callable($currentNode)) { + return true; + } + + else { + echo sprintf("Compiler Error: Unhandled Node type %s." . PHP_EOL, get_class($currentNode)); + return false; + } + } + + private function evaluateArrayOrMapAccess(ArrayMapAccessNode $arrayOrMapAccess, Scope $scope): Node + { + $arrayOrMap = $arrayOrMapAccess->arrayOrMap; + if ($arrayOrMap instanceof IdentifierNode) { + $arrayOrMap = $this->evaluate($arrayOrMap, $scope); + } + + $accessee = $arrayOrMap; + + foreach ($arrayOrMapAccess->items as $item) { + $evaluation = $this->evaluate($item, $scope)->getValue(); + + if ($accessee instanceof ArrayNode) { + $accessee = $accessee->values[$evaluation]; + } + else if ($accessee instanceof MapNode) { + foreach ($accessee->values as $value) { + $key = $this->evaluate($value->key, $scope)->getValue(); + if ($key == $evaluation) { + $accessee = $this->evaluate($value->value, $scope); + break; + } + } + } + else if ($accessee instanceof ValueNode) { + $accessee = new ValueNode($accessee->getValue()[$evaluation]); + } + } + + return $accessee; + } + + /** + * @param FunctionCallParameter[] $parameters + */ + private function evaluateFunction(IdentifierNode $identifier, FunctionDefinition $function, array $parameters, Scope $outerScope): mixed + { + $scope = new Scope($outerScope); + + foreach ($parameters as $parameter) { + /**@var FunctionCallParameter $parameter*/ + $scope->add($parameter->identifier, $this->evaluate($parameter->value, $outerScope)); + } + + foreach ($function->body as $currentNode) { + if ($currentNode instanceof FunctionReturn) { + return $this->evaluate($currentNode->returnValue, $scope); + } + + else if ($currentNode instanceof IfNode) { + $returnValue = $this->evaluate($currentNode, $scope); + + // return if "if" had a return inside + if ($returnValue) { + return $returnValue; + } + } + + else { + $this->evaluate($currentNode, $scope); + } + } + + echo sprintf("Function %s missing return value." . PHP_EOL, $identifier->token->value); + } + + /** + * @param FunctionCallParameter[] $parameters + */ + private function evaluateClosure(IdentifierNode $identifier, Closure $function, array $parameters, Scope $scope): mixed + { + $evaluatedParameters = []; + foreach ($parameters as $parameter) { + /**@var FunctionCallParameter $parameter*/ + $evaluatedParameters[] = $this->evaluate($parameter->value, $scope)->getValue(); + } + + return new ValueNode(call_user_func_array($function, $evaluatedParameters)); + } + + private function evaluateIfArmBody(IfArm $arm, Scope $outerScope): mixed + { + $scope = new Scope($outerScope); + + foreach ($arm->body as $currentNode) { + if ($currentNode instanceof FunctionReturn) { + return $this->evaluate($currentNode->returnValue, $scope); + } + + else { + $this->evaluate($currentNode, $scope); + } + } + + return null; + } +} + +class Scope +{ + private ?Scope $outerScope; + public array $values; + + public function __construct(?Scope $outerScope = null) + { + $this->outerScope = $outerScope; + } + + public function add(IdentifierNode $identifier, mixed $value): void + { + $key = $identifier->token->value; + + $this->values[$key] = $value; + } + + public function get(IdentifierNode $identifier): mixed + { + $key = $identifier->token->value; + + if (isset($this->values[$key])) { + $value = $this->values[$key]; + if ($value instanceof IdentifierNode and $value->token->value == $key) {} + else { + return $this->values[$key]; + } + } + + if (! isset($this->outerScope)) { + echo sprintf("Undefined variable \"%s\" at %d:%d" . PHP_EOL, $key, $identifier->token->line, $identifier->token->column); + + return null; + } + + return $this->outerScope->get($identifier); + } + + public function set(IdentifierNode $identifier, mixed $value): void + { + $key = $identifier->token->value; + + if (isset($this->values[$key])) { + $this->values[$key] = $value; + } + + else if (isset($this->outerScope)){ + $this->outerScope->set($identifier, $value); + } + + else { + echo sprintf("Can't set nonexistent variable %s." . PHP_EOL, $identifier->token->value); + } + } + + public function has(IdentifierNode $identifier): bool + { + $has = isset($this->values[$identifier->token->value]); + + if ($has) { + return $has; + } + + if (isset($this->outerScope)) { + return $this->outerScope->has($identifier); + } + + return false; + } + + public function addExternal(string $identifier, mixed $value): void + { + $this->values[$identifier] = $value; + } +} + +class ValueNode extends Node +{ + public function __construct(private mixed $value) + {} + + public function getValue(): mixed + { + return $this->value; + } +} |