Created
March 18, 2019 12:26
-
-
Save sma/99424474a607430ab81f2e05336697bb to your computer and use it in GitHub Desktop.
An interpreter for a subset of Altair BASIC which is enough to run the classic game of Hammurabi from the classic book 101 BASIC Computer Games.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// An interpreter for a subset of Altair BASIC which is enough to run the | |
/// classic game of Hammurabi from the classic book 101 BASIC Computer Games. | |
import 'dart:io'; | |
import 'dart:math'; | |
void main() { | |
run(File("hammurabi.bas").readAsStringSync()); | |
} | |
// ---------------------------------------------------------------------------- | |
/// Runs [source] which must be the BASIC program "Hammurabi" until the `END` | |
/// instruction is reached. Supported instructions are: | |
/// | |
/// * `LET` – assigns variable | |
/// * `PRINT` - prints a string or numeric expression | |
/// * `INPUT` - assigns input as numeric value to a variable | |
/// * `IF` - jumps to another line based on a condition | |
/// * `GOTO` - jump unconditionally to another line | |
/// * `GOSUB` - jumps to a subroutine | |
/// * `RETURN` - returns from a subroutine | |
void run(String source) { | |
_tokens = List.of(tokenize(source)); | |
_index = 1; | |
while (!at('END')) { | |
if (at('GOSUB')) | |
doGosub(); | |
else if (at('GOTO')) | |
doGoto(); | |
else if (at('IF')) | |
doIf(); | |
else if (at('INPUT')) | |
doInput(); | |
else if (at('PRINT')) | |
doPrint(); | |
else if (at('REM')) | |
doRem(); | |
else if (at('RETURN')) | |
doReturn(); | |
else | |
doLet(); | |
} | |
} | |
/// `GOSUB <line number>` | |
void doGosub() { | |
var l = line(); | |
expectEnd(); | |
_stack.add(_index); | |
goto(l); | |
} | |
/// `GOTO <line number>` | |
void doGoto() { | |
var l = line(); | |
expectEnd(); | |
goto(l); | |
} | |
/// `IF <condition> THEN <line number>` | |
void doIf() { | |
var c = condition(); | |
expect('THEN'); | |
var l = line(); | |
expectEnd(); | |
if (c) goto(l); | |
} | |
/// `INPUT <name>` | |
void doInput() { | |
var n = name(); | |
expectEnd(); | |
stdout.write('? '); | |
var i; | |
if (_inputs != null && _inputs.isNotEmpty) { | |
i = _inputs.removeAt(0); | |
stdout.writeln(i); | |
} else { | |
i = stdin.readLineSync(); | |
} | |
_variables[n] = num.tryParse(i) ?? 0; | |
} | |
final List<String> _inputs = null; // ['0', '0', '1800', '990']; | |
/// `PRINT [<expression> {; <expression>} [;]]` | |
void doPrint() { | |
String str(v) { | |
if (v is String) return v; | |
if (v == v.toInt()) v = v.toInt(); | |
return ' $v '; | |
} | |
var s = ""; | |
if (!atEnd()) { | |
s += str(expression()); | |
while (at(';')) { | |
if (atEnd()) { | |
stdout.write(s); | |
return; | |
} | |
s += str(expression()); | |
} | |
expectEnd(); | |
} | |
stdout.writeln(s); | |
} | |
/// `REM ...` | |
void doRem() { | |
expectEnd(); | |
} | |
/// `RETURN` | |
void doReturn() { | |
expectEnd(); | |
_index = _stack.removeLast(); | |
} | |
/// `[LET] <name> = <expression>` | |
void doLet() { | |
var n = name(); | |
expect('='); | |
_variables[n] = expression(); | |
expectEnd(); | |
} | |
// ---------------------------------------------------------------------------- | |
/// Parses and interprets the next conditional expression, returning the value. | |
bool condition() { | |
var e = expression(); | |
if (at('=')) return e == expression(); | |
if (at('<>')) return e != expression(); | |
if (at('<')) return e < expression(); | |
if (at('<=')) return e <= expression(); | |
if (at('>')) return e > expression(); | |
if (at('>=')) return e >= expression(); | |
expected('comparison operator'); | |
} | |
/// Parses and interprets the next expression, return the value. | |
expression() { | |
var v = term(); | |
while (true) { | |
if (at('+')) | |
v += term(); | |
else if (at('-')) | |
v -= term(); | |
else | |
return v; | |
} | |
} | |
term() { | |
var v = factor(); | |
while (true) { | |
if (at('*')) | |
v *= factor(); | |
else if (at('/')) | |
v /= factor(); | |
else | |
return v; | |
} | |
} | |
factor() { | |
if (at('-')) return -factor(); | |
if (at('(')) { | |
var v = expression(); | |
expect(')'); | |
return v; | |
} | |
var t = token; | |
if (t[0] == '"') { | |
advance(); | |
return t.substring(1, t.length - 1); | |
} | |
if (RegExp(r'\.?\d').matchAsPrefix(t) != null) { | |
advance(); | |
return num.parse(t); | |
} | |
var n = name(); | |
if (at('(')) { | |
var arg = expression(); | |
expect(')'); | |
if (n == 'TAB') return ' ' * arg; | |
if (n == 'RND') return _random.nextDouble(); | |
if (n == 'INT') return arg.toInt(); | |
expected('unknown function $n'); | |
} | |
return _variables[n] ?? 0; | |
} | |
/// Returns a name or throws an error if there is none. | |
String name() { | |
var t = token; | |
if (RegExp(r'[a-zA-Z]').matchAsPrefix(t) != null) { | |
advance(); | |
return t; | |
} | |
throw expected('name'); | |
} | |
/// Returns a line number or throws an error if there is none. | |
String line() { | |
var t = token; | |
if (RegExp(r'\d+').matchAsPrefix(t) != null) { | |
advance(); | |
return t; | |
} | |
throw expected('line number'); | |
} | |
final Map<String, num> _variables = {}; | |
final Random _random = Random(); | |
// ---------------------------------------------------------------------------- | |
/// Splits [source] into BASIC tokens. | |
Iterable<String> tokenize(String source) { | |
return RegExp(r'(\d+(?:\.\d*)?|\.\d+|".*?"|(REM).*|\w+|[-+*/:;()]|[<>=]+|\n)|\s+') | |
.allMatches(source) | |
.map((m) => m[2] ?? m[1]) | |
.where((m) => m != null); | |
} | |
/// Returns the current token. | |
String get token => _tokens[_index]; | |
/// Consumes the current token and makes [token] return the next token from the stream. | |
void advance() => _index++; | |
/// Returns whether the current token is [t] and if so, consumes it. | |
bool at(String t) { | |
if (token == t) { | |
advance(); | |
return true; | |
} | |
return false; | |
} | |
/// Throws an error if the current token isn't [t]. | |
void expect(String t) { | |
if (!at(t)) expected('$t'); | |
} | |
/// Throws an error expecting [message]. | |
Null expected(String message) { | |
var i = _index; | |
while (i >= 0 && _tokens[i] != '\n') --i; | |
var line = _tokens[i + 1]; | |
throw 'expected $message but found $token in $line'; | |
} | |
/// Returns whether the current token is either `:` or `\n`. | |
/// In the latter case, also skip the line number which must follow. | |
bool atEnd() { | |
if (at('\n')) { | |
advance(); | |
return true; | |
} | |
return at(':'); | |
} | |
/// Throws an error if the current token is neither `:` nor `\n`. | |
void expectEnd() { | |
if (!atEnd()) expected('end of instruction'); | |
} | |
/// Search for [line] and moves instruction pointer. | |
/// Throws an error there is no such line number. | |
void goto(String line) { | |
for (var i = 0; i < _tokens.length;) { | |
if (_tokens[i++] == line) { | |
_index = i; | |
return; | |
} | |
for (; i < _tokens.length && _tokens[i] != '\n'; i++); | |
i++; | |
} | |
throw 'missing line $line'; | |
} | |
/// The stream of tokens. | |
List<String> _tokens; | |
/// The index of the current token a.k.a. instruction pointer. | |
int _index; | |
/// Stack of instruction pointers. | |
List<int> _stack = []; | |
// ---------------------------------------------------------------------------- |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment