Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

https://github.com/skx/gobasic

A BASIC interpreter written in golang.
https://github.com/skx/gobasic

basic golang interpreted-programming-language interpreter

Last synced: 17 days ago
JSON representation

A BASIC interpreter written in golang.

Lists

README

        

[![Go Report Card](https://goreportcard.com/badge/github.com/skx/gobasic)](https://goreportcard.com/report/github.com/skx/gobasic)
[![license](https://img.shields.io/github/license/skx/gobasic.svg)](https://github.com/skx/gobasic/blob/master/LICENSE)
[![Release](https://img.shields.io/github/release/skx/gobasic.svg)](https://github.com/skx/gobasic/releases/latest)

# 05 PRINT "Index"

* [10 PRINT "GOBASIC!"](#10-print-gobasic)
* [20 PRINT "Limitations"](#20-print-limitations)
* [Arrays](#arrays)
* [Line Numbers](#line-numbers)
* [IF Statement](#if-statement)
* [DATA / READ Statements](#data--read-statements)
* [Builtin Functions](#builtin-functions)
* [Types](#types)
* [30 PRINT "Installation"](#30-print-installation)
* [Build without Go Modules (Go before 1.11)](#build-without-go-modules-go-before-111)
* [Build with Go Modules (Go 1.11 or higher)](#build-with-go-modules-go-111-or-higher)
* [40 PRINT "Usage"](#40-print-usage)
* [50 PRINT "Implementation"](#50-print-implementation)
* [60 PRINT "Sample Code"](#60-print-sample-code)
* [70 PRINT "Embedding"](#70-print-embedding)
* [75 PRINT "DoS"](#75-print-dos)
* [80 PRINT "Visual BASIC!"](#80-print-visual-basic)
* [90 PRINT "Bugs?"](#90-print-bugs)
* [100 PRINT "Project Goals / Links"](#100-print-project-goals--links)








# 10 PRINT "GOBASIC!"

This repository contains a naive implementation of BASIC, written in Golang.

> If you'd prefer to see a more "real" interpreted language, implemented in Go, you might prefer [monkey](https://github.com/skx/monkey/).

The implementation is simple for two main reasons:

* There is no UI, which means any and all graphics-primitives are ruled out.
* However the embedded sample, described later in this file, demonstrates using BASIC to create a PNG image.
* There is also a HTTP-based BASIC server, also described later, which allows you to create images "interactively".
* I didn't implement the full BASIC set of primitives.
* Although most of the commands available to the ZX Spectrum are implemented. I only excluded things relating to tape, PEEK, POKE, etc.
* If you want to add new BASIC keywords this is easy, and the samples mentioned above do that.

The following obvious primitives work as you'd expect:

* `DIM`
* Create an array. Note that only one and two-dimensional arrays are supported.
* See [examples/95-arrays.bas](examples/95-arrays) and [examples/40-array-sort.bas](examples/40-array-sort.bas) for quick samples.
* `END`
* Exit the program.
* `GOTO`
* Jump to the given line.
* `GOSUB` / `RETURN`
* Used to call the subroutines at the specified line.
* `IF` / `THEN` / `ELSE`
* Conditional execution.
* `INPUT`
* Allow reading a string, or number (see later note about types).
* `LET`
* Assign a value to a variable, creating it if necessary.
* `FOR` & `NEXT`
* Looping constructs.
* `PRINT`
* Print a string, an integer, or variable.
* Multiple arguments may be separated by commas.
* `REM`
* A single-line comment (BASIC has no notion of multi-line comments).
* `READ` & `DATA`
* Allow reading from stored data within the program.
* See [examples/35-read-data.bas](examples/35-read-data.bas) for a demonstration, along with [examples/100-array-sort.bas](examples/100-array-sort.bas).
* `SWAP`
* Allow swapping the contents of two variables.
* Useful for sorting arrays, as shown in [examples/100-array-sort.bas](examples/100-array-sort.bas).
* `DEF FN` & `FN`
* Allow user-defined functions to be defined or invoked.
* See [examples/25-def-fn.bas](examples/25-def-fn.bas) for an example.

Most of the maths-related primitives I'm familiar with are also present, for example SIN, COS, PI, ABS, along with the similar string-related primitives:

* `LEN "STEVE"`
* Returns the length of a string "STEVE" (5).
* `LEFT$ "STEVE", 2`
* Returns the left-most 2 characters of "STEVE" ("ST").
* `RIGHT$ "STEVE", 2`
* Returns the right-most 2 characters of "STEVE" ("VE").
* `CHR$ 42`
* Converts the integer 42 to a character (`*`). (i.e. ASCII value.)
* `CODE " "`
* Converts the given character to the integer value (32).








## 20 PRINT "Limitations"

This project was started as [a weekend-project](https://blog.steve.fi/so_i_wrote_a_basic_basic.html), although it has subsequently been improved and extended.

The code has near-total test-coverage, and has been hammered with multiple days of fuzz-testing (i.e. Feeding random programs into the interpreter to see if it will die - see [FUZZING.md](FUZZING.md) for more details on that.)

That said there are some (obvious) limitations:

* Only a single statement is allowed upon each line.
* Only a subset of the language is implemented.
* If there are specific primitives you miss, then please [report a bug](https://github.com/skx/gobasic/issues/).
* The project is open to suggestions, but do bear in mind the [project goals]((#100-print-project-goals--links)) listed later on.
* When it comes to types only floating-point and string values are permitted.
* There is support for arrays but only one or two dimensional ones.

### Arrays

Arrays are used just like normal variables, but they need to be declared using the `DIM` statement. Individual elements are accessed using the offsets in brackets after the variable name:

10 DIM a(10,10)
20 LET a[1,1]=10
30 PRINT a[1,1], "\n"

Arrays are indexed from 0-N, so with an array size of ten you can access eleven
elements:

10 DIM a(10)
20 a[0] = 0
30 a[1] = 1
40 ..
90 a[9] = 9
100 a[10] = 10

ZX Spectrum BASIC indexed arrays from 1, denying the ability to use the zeroth element, which I've long considered a mistake.

### Line Numbers

Line numbers are _mostly_ optional, for example the following program is valid and correct:

10 READ a
20 IF a = 999 THEN GOTO 100
30 PRINT a, "\n"
40 GOTO 10
100 END
DATA 1, 2, 3, 4, 999

The main reason you need line-numbers is for the `GOTO` and `GOSUB` functions,
if you prefer to avoid them then you're welcome to do so.

### `IF` Statement

The handling of the IF statement is perhaps a little unusual, since I'm
used to the BASIC provided by the ZX Spectrum which had no ELSE clause.
The general form of the IF statement I've implemented is:

IF $CONDITIONAL THEN $STATEMENT1 [ELSE $STATEMENT2]

Only a single statement is permitted between "THEN" and "ELSE", and again between "ELSE" and NEWLINE. These are valid IF statements:

IF 1 > 0 THEN PRINT "OK"
IF 1 > 3 THEN PRINT "SOMETHING IS BROKEN": ELSE PRINT "Weird!"

In that second example you'll see that "`:`" was used to terminate the `PRINT` statement, which otherwise would have tried to consume all input until it hit a newline.

The set of comparison functions _probably_ includes everything you need:

* `IF a < b THEN ..`
* `IF a > b THEN ..`
* `IF a <= b THEN ..`
* `IF a >= b THEN ..`
* `IF a = b THEN ..`
* `IF a <> b THEN ..`
* `IF a THEN ..`
* This passes if `a` is a number which is not zero.
* This passes if `a` is a string which is non-empty.

You can see several examples of the IF statement in use in the example [examples/70-if.bas](examples/70-if.bas).

### `DATA` / `READ` Statements

The `READ` statement allows you to read the next value from the data stored
in the program, via `DATA`. There is no support for the `RESTORE` function,
so once your data is read it cannot be re-read.

### Builtin Functions

You'll also notice that the primitives which are present all suffer from the flaw that they don't allow brackets around their arguments. So this is valid:

10 PRINT RND 100

But this is not:

10 PRINT RND(100)

This particular problem could be fixed, but I've not considered it significant.

### Types

There are no type restrictions on variable names vs. their contents, so these statements are each valid:

* `LET a = "steve"`
* `LET a = 3.2`
* `LET a% = ""`
* `LET a$ = "steve"`
* `LET a$ = 17 + 3`
* `LET a% = "string"`

The __sole__ exception relates to the `INPUT` statement. The `INPUT` statement prompts a user for input, and returns it as a value - it doesn't know whether to return a "string" or a "number". So it returns a string if it sees a `$` in the variable name.

This means this reads a string:

10 INPUT "Enter a string", a$

But this prompts for a number:

10 INPUT "Enter a number", a

This seemed better than trying to return a string, unless the input looked like a number (i.e. the input matched `/^([0-9\.]+)$/` we could store a number, otherwise a string).








## 30 PRINT "Installation"

We don't pull in any external dependencies, except for the embedded examples,
so installation is simple.

git clone https://github.com/skx/gobasic
cd gobasic
go install

You can also install directly via:

go install github.com/skx/gobasic@latest

If you don't have a golang environment setup you should be able to download a binary release from our release page:

* [Binary Releases](https://github.com/skx/gobasic/releases)








## 40 PRINT "Usage"

gobasic is very simple, and just requires the name of a BASIC-program to
execute. Write your input in a file and invoke `gobasic` with the path.

For example the following program was useful to test my implementation of the `GOTO` primitive:

10 GOTO 80
20 GOTO 70
30 GOTO 60
40 PRINT "Hello, world!\n"
50 END
60 GOTO 40
70 GOTO 30
80 GOTO 20

Execute it like this:

$ gobasic examples/10-goto.bas

**NOTE**: I feel nostalgic seeing keywords in upper-case, but `PRINT` and `print` are treated identically.








## 50 PRINT "Implementation"

A traditional interpreter for a scripting/toy language would have a series of
well-defined steps:

* Split the input into a series of tokens ("lexing").
* Parse those tokens and build an abstract syntax tree (AST).
* Walk that tree, evaluating as you go.

As is common with early 8-bit home-computers this implementation is a little more BASIC:

* We parse the input into a series of tokens, defined in [token/token.go](token/token.go)
* The parsing happens in [tokenizer/tokenizer.go](tokenizer/tokenizer.go)
* We then __directly__ execute those tokens.
* The execution happens in [eval/eval.go](eval/eval.go) with a couple of small helpers:
* [eval/for_loop.go](eval/for_loop.go) holds a simple data-structure for handling `FOR`/`NEXT` loops.
* [eval/stack.go](eval/stack.go) holds a call-stack to handle `GOSUB`/`RETURN`
* [eval/vars.go](eval/vars.go) holds all our variable references.
* We have a facility to allow golang code to be made available to BASIC programs, and we use that facility to implement a bunch of our functions as "builtins".
* Our builtin-functions are implemented beneath [builtin/](builtin/).
* Because we support both strings and ints/floats in our BASIC scripts we use a wrapper to hold them on the golang-side. This can be found in [object/object.go](object/object.go).

As there is no AST step errors cannot be detected prior to the execution of programs - because we only hit them after we've started running.








## 60 PRINT "Sample Code"

There are a small number of sample-programs located beneath [examples/](examples/). These were written in an adhoc fashion to test various parts of the implementation.

Perhaps the best demonstration of the code are the following two samples:

* [examples/90-stars.bas](examples/90-stars.bas)
* Prompt the user for their name and the number of stars to print.
* Then print them. Riveting! Fascinating! A program for the whole family!
* [examples/55-game.bas](examples/55-game.bas)
* A classic game where you guess the random number the computer has thought of.








## 70 PRINT "Embedding"

The interpreter is designed to be easy to embed into your application(s)
if you're crazy enough to want to do that!

You can see an example in the file [embed/main.go](embed/main.go).

The example defines several new functions which can be called by BASIC:

* `PEEK`
* `POKE`
* `PLOT`
* `SAVE`
* `CIRCLE`

When the script runs it does some BASIC variable manipulation and it also
creates a PNG file - the `PLOT` function allows your script to set a pixel
and the `CIRCLE` primitive draws an outline of a circle. Finally the
`SAVE` function writes out the result.

Extending this example to draw filled circles, boxes, etc, is left as an
exercise ;)

Hopefully this example shows that making your own functions available to
BASIC scripts is pretty simple. (This is how SIN, COS, etc are implemented
in the standalone interpreter.)








## 75 PRINT "DoS"

When it comes to security problems the most obvious issue we might suffer from is denial-of-service attacks; it is certainly possible for this library to be given faulty programs, for example invalid syntax, or references to undefined functions. Failures such as those would be detected at parse/run time, as appropriate.

In short running user-supplied scripts should be safe, but there is one obvious exception, the following program is valid:

```
10 PRINT "STEVE ROCKS!"
20 GOTO 10
```

This program will __never__ terminate! If you're handling untrusted user-scripts, you'll want to ensure that you explicitly setup a timeout period.

The following will do what you expect:

```
// Setup a timeout period of five seconds
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Now create the interpreter, via the tokenizer
t := tokenizer.New(string(`10 GOTO 10`))

// Ensure we pass the context over
e, err := eval.NewWithContext(ctx,t)
if err != nil {
fmt.Printf("error creating interpreter: %s\n", err.Error())
panic(err) // proper handling here
}

// Now run the program
err = e.Run()
if err != nil {
fmt.Printf("error running: %s\n", err.Error())
panic(err) // proper handling here
}

// Here we'll see a timeout eror

```

The program will be terminated with an error after five seconds, which means that your host application will continue to run rather than being blocked forever!

## 80 PRINT "Visual BASIC!"

Building upon the code in the embedded-example I've also implemented a simple
HTTP-server which will accept BASIC code, and render images!

To run this:

cd goserver ; go build . ; ./goserver

Once running open your browser at the URL:

* [http://localhost:8080](http://localhost:8080)

The view will have an area of entering code, and once you run it the result will
be shown in the bottom of the screen. Something like this:

![alt text](https://github.com/skx/gobasic/raw/master/goserver/screenshot.png "Sample view")

There are several included examples which you can load/launch by clicking upon them.








## 90 PRINT "Bugs?"

It is probable that bugs exist in this interpreter, but I've tried to do
as much testing as I can. If you spot anything that
seems wrong please do [report an issue](https://github.com/skx/gobasic/issues).

* If the interpreter segfaults that is a bug.
* Even if the program is invalid, bogus, or malformed the interpreter should cope with it.
* If a valid program produces the wrong output then that is also a bug.

The project contain a number of test-cases, which you can execute like so:

$ go test ./...

Finally __if our test-coverage drops beneath 95%__ that is __a bug__. The
test coverage of most of our packages is 100%, unfortunately the main `eval/`
package is not yet completely covered.

You can see the __global__ coverage via:

$ ./test-coverage
97.9%

In addition to the test-cases which have been manually written the interpreter
has also been fuzz-tested, which has resulted in some significant improvements.

See [FUZZING.md](FUZZING.md) for details of how to run the fuzz-tests.








## 100 PRINT "Project Goals / Links"

It is never the intention of this project to support _all_ things that are
possible in the various dialects of BASIC.

There are facilities which will make porting programs useful, such as
the ability to use `WHILE`/`END` loops, functions with named-parameters,
and primitives such as SLEEP, BEEP, & etc.

Above all else this project is supposed to be fun, for me. Which means
if there are two ways of implementing something I'll pick the way I remember
back when I was 12 and computers were .. fun!

If there are feature-requests which seem less fun, and less immediately
useful to me - with my biased memories of coding on a ZX Spectrum - I will
tag them "wontfix". If you contribute a pull-request to support them I will
accept them, but I'm probably not likely to work upon them directly.

That said there are cases where I can be persuaded, and there are a lot
of other BASIC intepreters out there, so I won't feel bad if this particular
project doesn't suit your needs.

One project, slightly related to this, which might be worth checking up
on is this one:

* https://github.com/udhos/basgo

## Github Setup

This repository is configured to run tests upon every commit, and when
pull-requests are created/updated. The testing is carried out via
[.github/run-tests.sh](.github/run-tests.sh) which is used by the
[github-action-tester](https://github.com/skx/github-action-tester) action.

Releases are automated in a similar fashion via [.github/build](.github/build),
and the [github-action-publish-binaries](https://github.com/skx/github-action-publish-binaries) action.

Steve
--