Skip to content

Instantly share code, notes, and snippets.

@sma
Created March 18, 2019 12:26
Show Gist options
  • Save sma/99424474a607430ab81f2e05336697bb to your computer and use it in GitHub Desktop.
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.
/// 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