# Tiny Great Languages: BASIC

*This is part 2 from series “Tiny Great Languages”.*

*Final code is on Github.**Part 1: Assembly.**Part 2: BASIC.**Part 3: Forth/MOUSE.**Part 4: Lisp.**Part 5: APL/K.**Part 6: PL/0.*

Meet BASIC, the king of home computing in the late 1970s. Originally designed to promote computer literacy in schools, BASIC inspired a whole generation of professional software engineers.

BASIC typically combined a simple text editor with a command shell and interpreter. Lines starting with a number were added, deleted, or edited in the program space, while lines without numbers were executed immediately. For example:

```
NEW
20 B=1
10 A=5
30 PRINT A+B
20 B=7
RUN
```

Here, NEW and RUN are immediate commands – `NEW`

clears the user program, and `RUN`

executes it. Numbered lines are stored in order, and typing the `LIST`

command would display the entire program:

```
10 A=5
20 B=7
30 PRINT A+B
```

The outer loop of our BASIC would be simple, we have a luxury to store lines of code in a hash map (unlike the old BASICs that had to run on just a few kilobytes of RAM and stored very compact tokenised input instead):

```
code = {}
# Cut a number from a string and return the rest,
# i.e. "10 A=3" -> (10, " A=3"), "LIST" -> (0, "LIST")
def num(s):
n = 0
while s and s[0].isdigit(): n, s = n * 10 + int(s[0]), s[1:]
return n, s.strip()
def stmt(s):
....
for line in sys.stdin:
lineno, line = num(line)
if lineno: code[lineno] = line.strip()
else: stmt(line)
```

Numbered lines are stored in `code`

, empty lines effectively “delete” code. Otherwise, lines are executed by the `stmt()`

function, which is the core of our BASIC interpreter. Simple commands can be handled with one-liners in Python – `NEW`

is `code.clear()`

, `BYE`

is `sys.exit(0)`

, `REM`

is a no-op and `LIST`

is:

```
print("\n".join([f"{n:3} {ln}" for (n, ln) in sorted(code.items()) if ln]))
```

Although we are targeting an extremely tiny BASIC subset, we still need to support `GOTO`

. Our `stmt()`

function will handle jumping between the lines by keeping track of the “current line number”. For simplicity, we’ll introduce a variable table (dict), and make line number a special variable named `#`

(because it’s a number, plus there were a few BASIC dialects that used this notation).

Now we can complete the `stmt()`

function:

```
def stmt(s):
def do_if(s):
n, ln = expr(s)
if n:
stmt(ln)
while s != None:
cmd = s.split(None, 1)
vars["#"] += 1 if vars["#"] else 0
ops = {
"rem": lambda args: None,
"new": lambda args: code.clear(),
"bye": lambda args: sys.exit(0),
"list": lambda args: print(
"\n".join([f"{n:3} {ln}" for (n, ln) in sorted(code.items()) if ln])
),
"print": lambda args: print(
args[1:-1] if args and args[0] == '"' else expr(args)[0]
),
"input": lambda args: vars.update({args[0]: int(input("] "))}),
"goto": lambda args: vars.update({"#": expr(args)[0]}),
"if": lambda args: do_if(args),
"run": lambda args: vars.update({"#": 1}),
}
if cmd and cmd[0].lower() in ops:
ops[cmd[0].lower()](cmd[1] if len(cmd) > 1 else "")
elif s:
assign = s.split("=", 1)
vars[assign[0]], _ = expr(assign[1])
if vars["#"] <= 0:
break
vars["#"], s = next(
((n, ln) for (n, ln) in sorted(code.items()) if n >= vars["#"]), (0, None)
)
```

Our BASIC only supports 4 commands: `PRINT`

for output, `INPUT`

for reading numbers from console, `GOTO`

for loops and jumps, `IF`

for conditional execution. The rest are rather editing commands and provide an interactive shell: `REM`

is a comment, `NEW`

clears the code space, `BYE`

exits back to the OS shell (although in most cases BASIC *was* the OS shell), `LIST`

prints full code on display.

Out `stmt()`

function however relies on another `expr()`

function for evaluating math expressions, which can be implemented as a recursive descent parser:

```
vars = {"#": 0}
def expr(s):
res, s = term(s); op = ""
while s and s[0] in "+-":
op = s[0]
n, s = term(s[1:])
res += n if op == "+" else -n
return res, s
def term(s):
res, s = factor(s)
while s and s[0] in "*/":
op = s[0]
n, s = factor(s[1:])
res *= n if op == "*" else 1 / n if n != 0 else 0
return res, s
def factor(s):
if s and s[0] == "(": res, s = expr(s[1:]); return res, s[1:]
elif s and s[0].isdigit(): return num(s)
else:
i = 0
while i < len(s) and s[i].isalnum(): i += 1
return vars.get(s[:i], 0), s[i:]
```

Adding more operations isn’t that hard (to add `<`

, `>`

, `=`

and `#`

as a not-equal operator), but is left as an exercise to the reader.

Putting all the code pieces together - we get a BASIC interpreter/shell in 53 lines of code!

```
import sys
code, vars = {}, {"#": 0}
def num(s):
n = 0
while s and s[0].isdigit(): n, s = n * 10 + int(s[0]), s[1:]
return n, s.strip()
def expr(s):
res, s = term(s); op = ""
while s and s[0] in "+-":
op = s[0]
n, s = term(s[1:])
res += n if op == "+" else -n
return res, s
def term(s):
res, s = factor(s)
while s and s[0] in "*/":
op = s[0]
n, s = factor(s[1:])
res *= n if op == "*" else 1 / n if n != 0 else 0
return res, s
def factor(s):
if s and s[0] == "(": res, s = expr(s[1:]); return res, s[1:]
elif s and s[0].isdigit(): return num(s)
else:
i = 0
while i < len(s) and s[i].isalnum(): i += 1
return vars.get(s[:i], 0), s[i:]
def stmt(s):
def do_if(s):
n, ln = expr(s)
if n: stmt(ln)
while s != None:
cmd = s.split(None, 1)
vars["#"] += 1 if vars["#"] else 0
ops = {
"rem": lambda args: None,
"new": lambda args: code.clear(),
"bye": lambda args: sys.exit(0),
"list": lambda args: print("\n".join([f"{n:3} {ln}" for (n, ln) in sorted(code.items()) if ln])),
"print": lambda args: print(args[1:-1] if args and args[0] == '"' else expr(args)[0]),
"input": lambda args: vars.update({args[0]: int(input("] "))}),
"goto": lambda args: vars.update({"#": expr(args)[0]}),
"if": lambda args: do_if(args),
"run": lambda args: vars.update({"#": 1}),
}
if cmd and cmd[0].lower() in ops: ops[cmd[0].lower()](cmd[1] if len(cmd) > 1 else "")
elif s: assign = s.split("=", 1); vars[assign[0]], _ = expr(assign[1])
if vars["#"] <= 0: break
vars["#"], s = next(((n, ln) for (n, ln) in sorted(code.items()) if n >= vars["#"]), (0, None))
for line in sys.stdin:
lineno, line = num(line)
if lineno: code[lineno] = line.strip()
else: stmt(line)
```

Despite its size, it is surprisingly powerful and easy to extend. You can add more commands by simply dropping lambdas into the ops dictionary or enhance the expr/factor/term logic for better arithmetic support. Adding loops (`FOR .. TO .. NEXT`

) is also possible and would require keeping track of loop start addresses and counter boundaries. Adding `GOSUB`

and `RETURN`

is even simpler and requires storing a global call stack (to remember return addresses when entering a subroutine).

Can our BASIC calculate a factorial, though?

```
REM
REM Factorial program (fac.bas)
REM
NEW
10 PRINT "Enter N:"
20 INPUT N
30 F=1
50 IF N GOTO 70
60 GOTO 100
70 F=F*N
80 N=N-1
90 GOTO 50
100 PRINT F
LIST
PRINT ""
PRINT "Try 10!..."
RUN
```

Running `python3 basic.py < fac.bas`

gives us the correct factorials, try it yourself!

While BASIC has been criticized for its lack of structured programming features, I still believe it’s a fantastic language for beginners. I’ve seen 10-year-olds struggle with Python, JavaScript and other “modern” languages, yet they light up when creating simple games in BASIC on retrocomputer emulators!

As an inspiration for exploring BASICs I’d suggest implementing a full TinyBASIC dialect, then add strings and arrays, maybe named functions and of course run a “STARTREK.BAS” on it! Or try writing a BASIC in Assembly to get the real taste of how the original BASICs were made.

In the next part we’ll move on to concatenative languages, stay tuned!

I hope you’ve enjoyed this article. You can follow – and contribute to – on Github, Mastodon, Twitter or subscribe via rss.

*Sep 10, 2024*

See also: Tiny Great Languages: Assembly and more.