---
title: "BASIC Interpreter Part Two: Variables"
date: 2023-01-13T14:02:11
slug: basic-interpreter-part-two-variables

---
[Part One](https://lewisdale.dev/post/creating-a-sinclair-basic-interpreter/)

It's been a bit longer than I would have liked between the first post and this one, but life has a habit of getting in the way.

In my last post, I created a skeleton interpreter that could read very basic programs: we could `PRINT`, and use `GO TO` to jump to a different line in the program.

## Variables

That's not much of a program at all - programs have *data*, so the next logical step is adding support for variables.

Variables in BASIC have essentially three types:

- Number
- String
- Array

`Numbers` and strings are defined with the `LET` keyword, while arrays are denoted by `DIM`. There are also restrictions on variable naming. A number variable name can have any length, and can contain (but not begin with) a number. In this example, lines 10, 20, and 30 are all valid, however line 40 is not:

```basic
10 LET a=10
20 LET apples=5
30 LET b1234=10
40 LET 123=50
```

Strings are limited to single alphabetical character variable names, terminated by `$`:

```basic
10 a$=apples
```

For the sake of simplicity and brevity, we're not going to implement `Arrays` in this section - they'll come later. For now, I just want to focus on allowing us to read a variable, but not perform any operations on it.

To start with, we need to define an `Enum` to hold our variable data:

```rust
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Primitive {
    Int(i64),
    String(String),
}
```

I'm defining numbers as signed 64-bit integers, which is overkill when the original system only had 16-bit registers (despite being an 8-bit machine). This will *probably* lead to weird behaviour for programs that rely on hitting the ceiling for integers, so I'll go back and change it at some point. But for now, this is fine.

Next, we need to update the `Command` enum to accept variables, which I'm storing as a tuple of (`Variable name`, `Variable value`).

```rust
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Command {
    Print(String),
    GoTo(usize),
    Var((String, Primitive)),
    None,
}
```

First up, let's parse strings, because we've done that already for `Print` and in theory, it should be simple.

```rust
use nom::combinator::{map, verify};

fn parse_str(i: &str) -> IResult<&str, (String, Primitive)> {
    let (i, id) = verify(anychar, |c| c.is_alphabetic())(i)?;
    let (i, _) = tag($)(i)?;
    let (i, _) = tag(=)(i)?;
    let (i, var) = map(read_string, Primitive::String)(i)?;
    let var_name = format!({}$, id);
    Ok((i, (var_name, var)))
}
```

So, first of all we verify that the variable name conforms to the standard - we read a single `char`, and then use `verify` to confirm that it's alphabetic. The next two characters are fixed, `$` and `=`, and then finally we re-use `read_string` from our `Print` parser, and map it to our `Primitive::String` enum value. Then we just return the variable name and the value as a tuple.

Next, we want to parse numbers:

```rust
use nom::{
    character::complete::{i64 as cci64, alphanumeric1, digit1},
    combinator::{map, not}
}

fn parse_int(i: &str) -> IResult<&str, (String, Primitive)> {
    let (i, _) = not(digit1)(i)?;
    let (i, id) = alphanumeric1(i)?;
    let (i, _) = tag(=)(i)?;
    let (i, var) = map(cci64, Primitive::Int)(i)?;

    Ok((i, (id.to_string(), var)))
}
```

Similar to parsing strings, we first check that the first character of the variable is not a digit, using the `not` parser. Then we read one-or-more alphanumeric characters, check the assignment operator is there, and then read and map a 64-bit signed integer to our `Primitive::Int`.

Finally, combine the two into a single parser using `alt`:

```rust
fn parse_var(i: &str) -> IResult<&str, (String, Primitive)> {
    alt((
        parse_int,
        parse_str
    ))(i)
}
```

For the final step, we need to update our `Program` struct to store and handle variables. I'm lazy, so I'm going to use `HashMap` and not do any real checks before inserting:

```rust
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Program {
    nodes: Node,
    current: Node,
    vars: HashMap<String, Primitive>
}

pub fn execute(&mut self) {
        let mut iter = self.clone();

        while let Some(node) = iter.next() {
            if let Node::Link { item, next: _ } = node {
                match item.1 {
                    Command::Print(line) => println!({}, line),
                    Command::GoTo(line) => iter.jump_to_line(line),
                    Command::Var((id, var)) => {
                        self.vars.insert(id, var);
                    },
                    _ => panic!(Unrecognised command),
                }
            };
        }
    }
}
```

And that's it! We've not added any additional output, but putting a `println` at the end of `execute` does show variables being assigned:

```basic
10 PRINT Hello world
20 LET apples=5
30 LET b$=Hello
```

```command-line
    Finished dev [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/basic-interpreter`
Hello World
{apple: Int(10), b$: String(Hello)}
```

Next up, we'll update `PRINT` so that it can print out a variable, and maybe perform simple operations on variables.
