327 lines
8.8 KiB
PHP
327 lines
8.8 KiB
PHP
|
<?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();
|