Files
SBL/interpreter.php

327 lines
8.8 KiB
PHP
Raw Normal View History

2025-06-28 09:41:16 +02:00
<?php
class SBLInterpreter
{
private array $vars = [];
private array $lines = [];
private int $pc = 0;
private array $labels = [];
private array $functions = [];
private array $keyHandlers = [];
private bool $listeningForKeys = false;
private bool $waitingForKeyInput = false;
public function __construct(string $file)
{
$this->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();