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();