diff --git a/interpreter.php b/interpreter.php new file mode 100644 index 0000000..36833d9 --- /dev/null +++ b/interpreter.php @@ -0,0 +1,327 @@ +lines = file($file, FILE_IGNORE_NEW_LINES); + $this->scanLabels(); + } + + private function scanLabels(): void + { + foreach ($this->lines as $i => $line) { + $line = trim($line); + if (preg_match('/^([A-Za-z_]\w*):$/', $line, $m)) { + $this->labels[$m[1]] = $i; + } + if (preg_match('/^~(\w+)$/', $line, $m)) { + $start = $i + 1; + $end = $start; + while ($end < count($this->lines) && trim($this->lines[$end]) !== '~') { + $end++; + } + $this->functions[$m[1]] = array_slice($this->lines, $start, $end - $start); + } + if (preg_match('/^on\s+(\w+)$/', $line, $m)) { + $start = $i + 1; + $end = $start; + while ($end < count($this->lines) && trim($this->lines[$end]) !== 'end') { + $end++; + } + $this->keyHandlers[$m[1]] = array_slice($this->lines, $start, $end - $start); + } + } + } + + public function run(): void + { + $this->listeningForKeys = true; + while ($this->pc < count($this->lines)) { + $line = trim($this->lines[$this->pc]); + try { + $this->execLine($line); + if ($this->waitingForKeyInput) { + $this->handleKeyInput(); + continue; + } + } catch (Exception $e) { + echo "[ERROR] " . $e->getMessage() . "\n"; + exit(1); + } + $this->pc++; + } + } + + private function execLine(string $line): void + { + if ($line === '' || $line[0] === '#') + return; + + if (preg_match('/^\w+:$/', $line)) + return; + + if (preg_match('/^\$(.+)$/', $line, $m)) { + echo $this->evalExpr($m[1]) . "\n"; + return; + } + + if (preg_match('/^\?(\w+)(?:\s+\"([^\"]+)\")?$/', $line, $m)) { + $prompt = $m[2] ?? $m[1]; + $input = trim(readline("$prompt: ")); + $this->vars[$m[1]] = is_numeric($input) ? (int) $input : $input; + return; + } + + if (preg_match('/^(\w+)\[(\d+)\]\s*=\s*(.+)$/', $line, $m)) { + $var = $m[1]; + $index = (int) $m[2]; + $value = $this->evalMath($m[3]); + if (!isset($this->vars[$var]) || !is_array($this->vars[$var])) { + $this->vars[$var] = []; + } + $this->vars[$var][$index] = $value; + return; + } + + if (preg_match('/^(\w+)\s*=\s*\[(.*)\]$/', $line, $m)) { + $arrayItems = array_map('trim', explode(',', $m[2])); + $parsed = array_map(function ($item) { + if (preg_match('/^\d+$/', $item)) + return (int) $item; + if (preg_match('/^\".*\"$/', $item)) + return trim($item, '"'); + return $item; + }, $arrayItems); + $this->vars[$m[1]] = $parsed; + return; + } + + if (preg_match('/^(\w+)\s*=\s*(.+)$/', $line, $m)) { + $this->vars[$m[1]] = $this->evalMath($m[2]); + return; + } + + if (preg_match('/^(\w+)\s*:\s*(int|str)$/', $line, $m)) { + $result = $this->validate($m[1], $m[2]); + $this->vars['__last_validation__'] = $result ? "1" : "0"; + return; + } + + if (preg_match('/^goto\s+(\w+)$/', $line, $m)) { + if (!isset($this->labels[$m[1]])) + throw new Exception("Label {$m[1]} not found"); + $this->pc = $this->labels[$m[1]]; + return; + } + + if (preg_match('/^if\s+(.+)$/', $line, $m)) { + $cond = $this->evalCond($m[1]); + if (!$cond) { + $depth = 1; + while (++$this->pc < count($this->lines)) { + $l = trim($this->lines[$this->pc]); + if (preg_match('/^if\s+/', $l)) + $depth++; + if ($l === 'fi') { + $depth--; + if ($depth === 0) + return; + } + if ($depth === 1 && preg_match('/^elseif\s+/', $l)) { + $newCond = $this->evalCond(trim(substr($l, 7))); + if ($newCond) + return; + } + if ($depth === 1 && $l === 'else') { + return; + } + } + } + return; + } + + if (preg_match('/^elseif\s+(.+)$/', $line, $m)) { + $cond = $this->evalCond($m[1]); + if (!$cond) { + $depth = 1; + while (++$this->pc < count($this->lines)) { + $l = trim($this->lines[$this->pc]); + if (preg_match('/^if\s+/', $l)) + $depth++; + if ($l === 'fi') { + $depth--; + if ($depth === 0) + return; + } + if ($depth === 1 && preg_match('/^elseif\s+/', $l)) { + $newCond = $this->evalCond(trim(substr($l, 7))); + if ($newCond) + return; + } + if ($depth === 1 && $l === 'else') { + return; + } + } + } + return; + } + + if ($line === 'else') { + $depth = 1; + while (++$this->pc < count($this->lines)) { + $l = trim($this->lines[$this->pc]); + if (preg_match('/^if\s+/', $l)) + $depth++; + if ($l === 'fi') { + $depth--; + if ($depth === 0) + return; + } + } + return; + } + + if ($line === 'fi') + return; + + if (preg_match('/^~(\w+)\(\)$/', $line, $m)) { + $funcName = $m[1]; + if (!isset($this->functions[$funcName])) + throw new Exception("Undefined function: ~$funcName"); + foreach ($this->functions[$funcName] as $funcLine) { + $this->execLine(trim($funcLine)); + } + return; + } + + if (preg_match('/^on\s+(\w+)$/', $line)) { + while (++$this->pc < count($this->lines)) { + if (trim($this->lines[$this->pc]) === 'end') + break; + } + return; + } + + if ($line === ';') { + $this->waitingForKeyInput = true; + return; + } + + throw new Exception("Unknown statement: $line"); + } + + private function handleKeyInput(): void + { + system('stty -icanon -echo'); + $key = fgetc(STDIN); + system('stty icanon echo'); + if ($key !== false && isset($this->keyHandlers[$key])) { + foreach ($this->keyHandlers[$key] as $handlerLine) { + $this->execLine(trim($handlerLine)); + } + } + $this->waitingForKeyInput = false; + } + + private function evalExpr(string $expr): string + { + $parts = preg_split('/\s*\+\s*/', $expr); + $out = ''; + foreach ($parts as $p) { + $p = trim($p); + if (preg_match('/^"(.*)"$/', $p, $m)) { + $out .= $m[1]; + } elseif (preg_match('/^(\w+)\[(\d+)\]$/', $p, $m)) { + $val = $this->vars[$m[1]][(int) $m[2]] ?? ''; + $out .= $val; + } elseif (isset($this->vars[$p])) { + $val = $this->vars[$p]; + $out .= is_array($val) ? json_encode($val) : $val; + } elseif (is_numeric($p)) { + $out .= $p; + } else { + throw new Exception("Invalid token in expression: $p"); + } + } + return $out; + } + + private function evalMath(string $expr): mixed + { + $expr = preg_replace_callback('/\brandom\((\d+),(\d+)\)/', function ($m) { + return random_int((int) $m[1], (int) $m[2]); + }, $expr); + + $expr = preg_replace_callback('/\b[a-zA-Z_]\w*(?:\[\d+\])?\b/', function ($matches) { + $token = $matches[0]; + if (preg_match('/^(\w+)\[(\d+)\]$/', $token, $m)) { + return is_numeric($this->vars[$m[1]][(int) $m[2]] ?? null) ? (string) ($this->vars[$m[1]][(int) $m[2]]) : '0'; + } else { + return is_numeric($this->vars[$token] ?? null) ? (string) ($this->vars[$token]) : '0'; + } + }, $expr); + + set_error_handler(function () { + throw new Exception("Math error in expression."); + }); + $result = eval ("return ($expr);"); + restore_error_handler(); + return $result; + } + + private function validate(string $var, string $type): bool + { + if (!isset($this->vars[$var])) + return false; + $val = trim((string) $this->vars[$var]); + return match ($type) { + 'int' => preg_match('/^[+-]?\d+$/', $val) === 1, + 'str' => is_string($val), + default => false + }; + } + + private function evalCond(string $cond): bool + { + if (preg_match('/^(\w+)\s*:\s*(\w+)$/', $cond, $m)) { + return $this->validate($m[1], $m[2]); + } + + $ops = ['==', '!=', '>=', '<=', '>', '<']; + foreach ($ops as $op) { + if (strpos($cond, $op) !== false) { + [$l, $r] = explode($op, $cond, 2); + $l = trim($l); + $r = trim($r); + $lv = $this->vars[$l] ?? $l; + $rv = $this->vars[$r] ?? $r; + if (is_numeric($lv)) + $lv = (int) $lv; + if (is_numeric($rv)) + $rv = (int) $rv; + return match ($op) { + '==' => $lv == $rv, + '!=' => $lv != $rv, + '>=' => $lv >= $rv, + '<=' => $lv <= $rv, + '>' => $lv > $rv, + '<' => $lv < $rv, + }; + } + } + throw new Exception("Invalid condition: $cond"); + } +} + +$it = new SBLInterpreter("program.sbl"); +$it->run(); \ No newline at end of file diff --git a/program.sbl b/program.sbl new file mode 100644 index 0000000..697f75c --- /dev/null +++ b/program.sbl @@ -0,0 +1,16 @@ +start: +?input "What do you want to oopify?" +$input + "oop!" +$"Do you want to oopify something else? (y/n)" +; + +on y +goto start +end + +on n +goto end +end + +end: +$"Goodbye!" \ No newline at end of file