summaryrefslogtreecommitdiff
path: root/src/Interpreter
diff options
context:
space:
mode:
Diffstat (limited to 'src/Interpreter')
-rw-r--r--src/Interpreter/Interpreter.php460
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;
+ }
+}